mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-04-20 14:19:08 +00:00
first commit
This commit is contained in:
commit
797ee4f5c6
2784 changed files with 343578 additions and 0 deletions
9
.gitignore
vendored
Executable file
9
.gitignore
vendored
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
*.iml
|
||||||
|
.idea
|
||||||
|
.gradle
|
||||||
|
local.properties
|
||||||
|
.DS_Store
|
||||||
|
build
|
||||||
|
app/build
|
||||||
|
app/release
|
||||||
|
app/.cxx
|
39
app/build.gradle
Executable file
39
app/build.gradle
Executable file
|
@ -0,0 +1,39 @@
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 29
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "org.dslul.openboard.inputmethod.latin"
|
||||||
|
minSdkVersion 14
|
||||||
|
targetSdkVersion 29
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aaptOptions {
|
||||||
|
noCompress 'dict'
|
||||||
|
}
|
||||||
|
|
||||||
|
externalNativeBuild {
|
||||||
|
ndkBuild {
|
||||||
|
path 'src/main/native/jni/Android.mk'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lintOptions {
|
||||||
|
checkReleaseBuilds false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation 'com.google.code.findbugs:jsr305:3.0.2'
|
||||||
|
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
|
}
|
17
app/proguard-rules.pro
vendored
Executable file
17
app/proguard-rules.pro
vendored
Executable file
|
@ -0,0 +1,17 @@
|
||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# By default, the flags in this file are appended to flags specified
|
||||||
|
# in /home/iwo/android-sdk/tools/proguard/proguard-android.txt
|
||||||
|
# You can edit the include path and order by changing the proguardFiles
|
||||||
|
# directive in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# Add any project specific keep options here:
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
192
app/src/main/AndroidManifest.xml
Normal file
192
app/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Copyright (C) 2012 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
coreApp="true"
|
||||||
|
package="org.dslul.openboard.inputmethod.latin">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
||||||
|
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_PROFILE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_USER_DICTIONARY" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_USER_DICTIONARY" />
|
||||||
|
|
||||||
|
<!-- A signature-protected permission to ask AOSP Keyboard to close the software keyboard.
|
||||||
|
To use this, add the following line into calling application's AndroidManifest.xml
|
||||||
|
<pre>
|
||||||
|
{@code
|
||||||
|
<uses-permission android:name="org.dslul.openboard.inputmethod.latin.HIDE_SOFT_INPUT"/>
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
then call {@link android.content.Context#sendBroadcast(Intent)} as follows:
|
||||||
|
<pre>
|
||||||
|
{@code
|
||||||
|
sendBroadcast(new Intent("org.dslul.openboard.inputmethod.latin.HIDE_SOFT_INPUT")
|
||||||
|
.setPackage("org.dslul.openboard.inputmethod.latin"));
|
||||||
|
}
|
||||||
|
</pre> -->
|
||||||
|
<permission android:name="org.dslul.openboard.inputmethod.latin.HIDE_SOFT_INPUT"
|
||||||
|
android:protectionLevel="signature" />
|
||||||
|
|
||||||
|
<application android:label="@string/english_ime_name"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:defaultToDeviceProtectedStorage="true"
|
||||||
|
android:directBootAware="true">
|
||||||
|
|
||||||
|
<!-- Services -->
|
||||||
|
<service android:name="LatinIME"
|
||||||
|
android:label="@string/english_ime_name"
|
||||||
|
android:permission="android.permission.BIND_INPUT_METHOD">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.view.InputMethod" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data android:name="android.view.im" android:resource="@xml/method" />
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<service android:name=".spellcheck.AndroidSpellCheckerService"
|
||||||
|
android:label="@string/spell_checker_service_name"
|
||||||
|
android:permission="android.permission.BIND_TEXT_SERVICE">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.textservice.SpellCheckerService" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data android:name="android.view.textservice.scs"
|
||||||
|
android:resource="@xml/spellchecker" />
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<service android:name="org.dslul.openboard.inputmethod.dictionarypack.DictionaryService"
|
||||||
|
android:label="@string/dictionary_service_name">
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<!-- Activities -->
|
||||||
|
<activity android:name=".setup.SetupActivity"
|
||||||
|
android:theme="@style/platformActivityTheme"
|
||||||
|
android:label="@string/english_ime_name"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:noHistory="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".permissions.PermissionsActivity"
|
||||||
|
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||||
|
android:exported="false"
|
||||||
|
android:taskAffinity="" >
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name=".setup.SetupWizardActivity"
|
||||||
|
android:theme="@style/platformActivityTheme"
|
||||||
|
android:label="@string/english_ime_name"
|
||||||
|
android:clearTaskOnLaunch="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name=".settings.SettingsActivity"
|
||||||
|
android:theme="@style/platformSettingsTheme"
|
||||||
|
android:label="@string/english_ime_settings">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name=".spellcheck.SpellCheckerSettingsActivity"
|
||||||
|
android:theme="@style/platformSettingsTheme"
|
||||||
|
android:label="@string/android_spell_checker_settings">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name="org.dslul.openboard.inputmethod.dictionarypack.DictionarySettingsActivity"
|
||||||
|
android:theme="@style/platformSettingsTheme"
|
||||||
|
android:label="@string/dictionary_settings_title"
|
||||||
|
android:uiOptions="splitActionBarWhenNarrow">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name="org.dslul.openboard.inputmethod.dictionarypack.DownloadOverMeteredDialog"
|
||||||
|
android:theme="@style/platformActivityTheme"
|
||||||
|
android:label="@string/dictionary_install_over_metered_network_prompt">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<!-- Unexported activity used for tests. -->
|
||||||
|
<activity android:name=".settings.TestFragmentActivity"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<!-- Broadcast receivers -->
|
||||||
|
<receiver android:name="SystemBroadcastReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||||
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
<action android:name="android.intent.action.USER_INITIALIZE" />
|
||||||
|
<action android:name="android.intent.action.LOCALE_CHANGED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver android:name="DictionaryPackInstallBroadcastReceiver" android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="org.dslul.openboard.inputmethod.dictionarypack.aosp.UNKNOWN_CLIENT" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<receiver android:name="org.dslul.openboard.inputmethod.dictionarypack.EventHandler">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
|
||||||
|
<action android:name="android.intent.action.DATE_CHANGED" />
|
||||||
|
<action android:name="org.dslul.openboard.inputmethod.dictionarypack.aosp.UPDATE_NOW" />
|
||||||
|
<action android:name="org.dslul.openboard.inputmethod.dictionarypack.aosp.INIT_AND_UPDATE_NOW" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<!-- Broadcast receiver for AccountManager#LOGIN_ACCOUNTS_CHANGED_ACTION. -->
|
||||||
|
<receiver android:name=".accounts.AccountsChangedReceiver">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.accounts.LOGIN_ACCOUNTS_CHANGED" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
|
||||||
|
<!-- Content providers -->
|
||||||
|
<provider android:name="org.dslul.openboard.inputmethod.dictionarypack.DictionaryProvider"
|
||||||
|
android:grantUriPermissions="true"
|
||||||
|
android:exported="false"
|
||||||
|
android:authorities="@string/authority"
|
||||||
|
android:multiprocess="false"
|
||||||
|
android:label="@string/dictionary_provider_name">
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
</manifest>
|
BIN
app/src/main/dictionaries/bg_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/bg_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/cs_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/cs_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/da_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/da_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/de_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/de_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/el_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/el_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/en_AU_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/en_AU_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/en_GB_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/en_GB_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/en_US_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/en_US_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/en_emoji.combined.gz
Executable file
BIN
app/src/main/dictionaries/en_emoji.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/en_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/en_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/es_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/es_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/fi_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/fi_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/fr_emoji.combined.gz
Executable file
BIN
app/src/main/dictionaries/fr_emoji.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/fr_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/fr_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/hr_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/hr_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/it_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/it_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/iw_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/iw_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/ka_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/ka_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/lb_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/lb_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/lt_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/lt_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/lv_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/lv_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/nb_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/nb_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/nl_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/nl_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/pl_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/pl_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/pt_BR_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/pt_BR_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/pt_PT_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/pt_PT_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/ro_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/ro_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/ru_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/ru_wordlist.combined.gz
Executable file
Binary file not shown.
38
app/src/main/dictionaries/sample.combined
Executable file
38
app/src/main/dictionaries/sample.combined
Executable file
|
@ -0,0 +1,38 @@
|
||||||
|
# This is a sample wordlist that can be converted to a binary dictionary
|
||||||
|
# for use by the Latin IME.
|
||||||
|
# The file is essentially a CSV file, with indent level denoting nesting.
|
||||||
|
#
|
||||||
|
# The file starts with a single CSV line with the header attributes. Whatever
|
||||||
|
# the content, these are included as is in the binary file. The first attribute
|
||||||
|
# of the file should be `dictionary'. Usual fields are `locale', `description',
|
||||||
|
# `date', `version', `options'.
|
||||||
|
#
|
||||||
|
# Each word has a `word' entry and at least a `f' argument denoting its
|
||||||
|
# probability, as an integer between 0 and 255 on a logarithmic scale, with
|
||||||
|
# 255 meaning 1 and each decrement in 1 dividing probability by 1.15.
|
||||||
|
# As a special case, a weight of 0 is taken to mean profanity - words that
|
||||||
|
# should not be considered a typo, but that should never be suggested
|
||||||
|
# explicitly. An entry may be made not a word by adding a `not_a_word'
|
||||||
|
# field with a value of `true'. The main reason for putting such entries
|
||||||
|
# into the dictionary is to add shortcut targets and maybe a whitelist
|
||||||
|
# replacement.
|
||||||
|
#
|
||||||
|
# Each word may or may not have any number of shortcut target lines
|
||||||
|
# starting with a `shortcut' entry and having at least a `f' frequency
|
||||||
|
# value between 0 and 14, or the special value `whitelist' which becomes
|
||||||
|
# 15, which is then taken to be the whitelist target of this word.
|
||||||
|
#
|
||||||
|
# Each word may also have any number of bigram lines starting with a
|
||||||
|
# `bigram' entry containing the following word whose frequency should
|
||||||
|
# override the unigram frequency when following the word this bigram is
|
||||||
|
# for.
|
||||||
|
#
|
||||||
|
dictionary=main:en,locale=en,description=Sample wordlist,date=1351495318,version=1
|
||||||
|
word=sample,f=200
|
||||||
|
bigram=wordlist,f=243
|
||||||
|
word=wordlist,f=180
|
||||||
|
word=shortcut,f=176
|
||||||
|
shortcut=target,f=10
|
||||||
|
word=witelisted,f=10,not_a_word=true
|
||||||
|
shortcut=whitelisted,f=whitelist
|
||||||
|
word=profanity,f=0
|
BIN
app/src/main/dictionaries/sl_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/sl_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/sr_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/sr_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/sv_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/sv_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/tr_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/tr_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/dictionaries/uk_wordlist.combined.gz
Executable file
BIN
app/src/main/dictionaries/uk_wordlist.combined.gz
Executable file
Binary file not shown.
BIN
app/src/main/ic_launcher-web.png
Normal file
BIN
app/src/main/ic_launcher-web.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.accessibility;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Message;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.Key;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.R;
|
||||||
|
|
||||||
|
// Handling long press timer to show a more keys keyboard.
|
||||||
|
final class AccessibilityLongPressTimer extends Handler {
|
||||||
|
public interface LongPressTimerCallback {
|
||||||
|
public void performLongClickOn(Key key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int MSG_LONG_PRESS = 1;
|
||||||
|
|
||||||
|
private final LongPressTimerCallback mCallback;
|
||||||
|
private final long mConfigAccessibilityLongPressTimeout;
|
||||||
|
|
||||||
|
public AccessibilityLongPressTimer(final LongPressTimerCallback callback,
|
||||||
|
final Context context) {
|
||||||
|
super();
|
||||||
|
mCallback = callback;
|
||||||
|
mConfigAccessibilityLongPressTimeout = context.getResources().getInteger(
|
||||||
|
R.integer.config_accessibility_long_press_key_timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMessage(final Message msg) {
|
||||||
|
switch (msg.what) {
|
||||||
|
case MSG_LONG_PRESS:
|
||||||
|
cancelLongPress();
|
||||||
|
mCallback.performLongClickOn((Key)msg.obj);
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
super.handleMessage(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startLongPress(final Key key) {
|
||||||
|
cancelLongPress();
|
||||||
|
final Message longPressMessage = obtainMessage(MSG_LONG_PRESS, key);
|
||||||
|
sendMessageDelayed(longPressMessage, mConfigAccessibilityLongPressTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelLongPress() {
|
||||||
|
removeMessages(MSG_LONG_PRESS);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,266 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.accessibility;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.media.AudioManager;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import android.provider.Settings;
|
||||||
|
import androidx.core.view.accessibility.AccessibilityEventCompat;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewParent;
|
||||||
|
import android.view.accessibility.AccessibilityEvent;
|
||||||
|
import android.view.accessibility.AccessibilityManager;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.compat.SettingsSecureCompatUtils;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.R;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.SuggestedWords;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.utils.InputTypeUtils;
|
||||||
|
|
||||||
|
public final class AccessibilityUtils {
|
||||||
|
private static final String TAG = AccessibilityUtils.class.getSimpleName();
|
||||||
|
private static final String CLASS = AccessibilityUtils.class.getName();
|
||||||
|
private static final String PACKAGE =
|
||||||
|
AccessibilityUtils.class.getPackage().getName();
|
||||||
|
|
||||||
|
private static final AccessibilityUtils sInstance = new AccessibilityUtils();
|
||||||
|
|
||||||
|
private Context mContext;
|
||||||
|
private AccessibilityManager mAccessibilityManager;
|
||||||
|
private AudioManager mAudioManager;
|
||||||
|
|
||||||
|
/** The most recent auto-correction. */
|
||||||
|
private String mAutoCorrectionWord;
|
||||||
|
|
||||||
|
/** The most recent typed word for auto-correction. */
|
||||||
|
private String mTypedWord;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Setting this constant to {@code false} will disable all keyboard
|
||||||
|
* accessibility code, regardless of whether Accessibility is turned on in
|
||||||
|
* the system settings. It should ONLY be used in the event of an emergency.
|
||||||
|
*/
|
||||||
|
private static final boolean ENABLE_ACCESSIBILITY = true;
|
||||||
|
|
||||||
|
public static void init(final Context context) {
|
||||||
|
if (!ENABLE_ACCESSIBILITY) return;
|
||||||
|
|
||||||
|
// These only need to be initialized if the kill switch is off.
|
||||||
|
sInstance.initInternal(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AccessibilityUtils getInstance() {
|
||||||
|
return sInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AccessibilityUtils() {
|
||||||
|
// This class is not publicly instantiable.
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initInternal(final Context context) {
|
||||||
|
mContext = context;
|
||||||
|
mAccessibilityManager =
|
||||||
|
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
|
||||||
|
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if accessibility is enabled. Currently, this means
|
||||||
|
* that the kill switch is off and system accessibility is turned on.
|
||||||
|
*
|
||||||
|
* @return {@code true} if accessibility is enabled.
|
||||||
|
*/
|
||||||
|
public boolean isAccessibilityEnabled() {
|
||||||
|
return ENABLE_ACCESSIBILITY && mAccessibilityManager.isEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@code true} if touch exploration is enabled. Currently, this
|
||||||
|
* means that the kill switch is off, the device supports touch exploration,
|
||||||
|
* and system accessibility is turned on.
|
||||||
|
*
|
||||||
|
* @return {@code true} if touch exploration is enabled.
|
||||||
|
*/
|
||||||
|
public boolean isTouchExplorationEnabled() {
|
||||||
|
return isAccessibilityEnabled() && mAccessibilityManager.isTouchExplorationEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@true} if the provided event is a touch exploration (e.g. hover)
|
||||||
|
* event. This is used to determine whether the event should be processed by
|
||||||
|
* the touch exploration code within the keyboard.
|
||||||
|
*
|
||||||
|
* @param event The event to check.
|
||||||
|
* @return {@true} is the event is a touch exploration event
|
||||||
|
*/
|
||||||
|
public static boolean isTouchExplorationEvent(final MotionEvent event) {
|
||||||
|
final int action = event.getAction();
|
||||||
|
return action == MotionEvent.ACTION_HOVER_ENTER
|
||||||
|
|| action == MotionEvent.ACTION_HOVER_EXIT
|
||||||
|
|| action == MotionEvent.ACTION_HOVER_MOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the device should obscure typed password characters.
|
||||||
|
* Typically this means speaking "dot" in place of non-control characters.
|
||||||
|
*
|
||||||
|
* @return {@code true} if the device should obscure password characters.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public boolean shouldObscureInput(final EditorInfo editorInfo) {
|
||||||
|
if (editorInfo == null) return false;
|
||||||
|
|
||||||
|
// The user can optionally force speaking passwords.
|
||||||
|
if (SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD != null) {
|
||||||
|
final boolean speakPassword = Settings.Secure.getInt(mContext.getContentResolver(),
|
||||||
|
SettingsSecureCompatUtils.ACCESSIBILITY_SPEAK_PASSWORD, 0) != 0;
|
||||||
|
if (speakPassword) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always speak if the user is listening through headphones.
|
||||||
|
if (mAudioManager.isWiredHeadsetOn() || mAudioManager.isBluetoothA2dpOn()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't speak if the IME is connected to a password field.
|
||||||
|
return InputTypeUtils.isPasswordInputType(editorInfo.inputType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the current auto-correction word and typed word. These may be used
|
||||||
|
* to provide the user with a spoken description of what auto-correction
|
||||||
|
* will occur when a key is typed.
|
||||||
|
*
|
||||||
|
* @param suggestedWords the list of suggested auto-correction words
|
||||||
|
*/
|
||||||
|
public void setAutoCorrection(final SuggestedWords suggestedWords) {
|
||||||
|
if (suggestedWords.mWillAutoCorrect) {
|
||||||
|
mAutoCorrectionWord = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION);
|
||||||
|
final SuggestedWords.SuggestedWordInfo typedWordInfo = suggestedWords.mTypedWordInfo;
|
||||||
|
if (null == typedWordInfo) {
|
||||||
|
mTypedWord = null;
|
||||||
|
} else {
|
||||||
|
mTypedWord = typedWordInfo.mWord;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mAutoCorrectionWord = null;
|
||||||
|
mTypedWord = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains a description for an auto-correction key, taking into account the
|
||||||
|
* currently typed word and auto-correction.
|
||||||
|
*
|
||||||
|
* @param keyCodeDescription spoken description of the key that will insert
|
||||||
|
* an auto-correction
|
||||||
|
* @param shouldObscure whether the key should be obscured
|
||||||
|
* @return a description including a description of the auto-correction, if
|
||||||
|
* needed
|
||||||
|
*/
|
||||||
|
public String getAutoCorrectionDescription(
|
||||||
|
final String keyCodeDescription, final boolean shouldObscure) {
|
||||||
|
if (!TextUtils.isEmpty(mAutoCorrectionWord)) {
|
||||||
|
if (!TextUtils.equals(mAutoCorrectionWord, mTypedWord)) {
|
||||||
|
if (shouldObscure) {
|
||||||
|
// This should never happen, but just in case...
|
||||||
|
return mContext.getString(R.string.spoken_auto_correct_obscured,
|
||||||
|
keyCodeDescription);
|
||||||
|
}
|
||||||
|
return mContext.getString(R.string.spoken_auto_correct, keyCodeDescription,
|
||||||
|
mTypedWord, mAutoCorrectionWord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyCodeDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the specified text to the {@link AccessibilityManager} to be
|
||||||
|
* spoken.
|
||||||
|
*
|
||||||
|
* @param view The source view.
|
||||||
|
* @param text The text to speak.
|
||||||
|
*/
|
||||||
|
public void announceForAccessibility(final View view, final CharSequence text) {
|
||||||
|
if (!mAccessibilityManager.isEnabled()) {
|
||||||
|
Log.e(TAG, "Attempted to speak when accessibility was disabled!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following is a hack to avoid using the heavy-weight TextToSpeech
|
||||||
|
// class. Instead, we're just forcing a fake AccessibilityEvent into
|
||||||
|
// the screen reader to make it speak.
|
||||||
|
final AccessibilityEvent event = AccessibilityEvent.obtain();
|
||||||
|
|
||||||
|
event.setPackageName(PACKAGE);
|
||||||
|
event.setClassName(CLASS);
|
||||||
|
event.setEventTime(SystemClock.uptimeMillis());
|
||||||
|
event.setEnabled(true);
|
||||||
|
event.getText().add(text);
|
||||||
|
|
||||||
|
// Platforms starting at SDK version 16 (Build.VERSION_CODES.JELLY_BEAN) should use
|
||||||
|
// announce events.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
|
event.setEventType(AccessibilityEventCompat.TYPE_ANNOUNCEMENT);
|
||||||
|
} else {
|
||||||
|
event.setEventType(AccessibilityEvent.TYPE_VIEW_FOCUSED);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ViewParent viewParent = view.getParent();
|
||||||
|
if ((viewParent == null) || !(viewParent instanceof ViewGroup)) {
|
||||||
|
Log.e(TAG, "Failed to obtain ViewParent in announceForAccessibility");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
viewParent.requestSendAccessibilityEvent(view, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles speaking the "connect a headset to hear passwords" notification
|
||||||
|
* when connecting to a password field.
|
||||||
|
*
|
||||||
|
* @param view The source view.
|
||||||
|
* @param editorInfo The input connection's editor info attribute.
|
||||||
|
* @param restarting Whether the connection is being restarted.
|
||||||
|
*/
|
||||||
|
public void onStartInputViewInternal(final View view, final EditorInfo editorInfo,
|
||||||
|
final boolean restarting) {
|
||||||
|
if (shouldObscureInput(editorInfo)) {
|
||||||
|
final CharSequence text = mContext.getText(R.string.spoken_use_headphones);
|
||||||
|
announceForAccessibility(view, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends the specified {@link AccessibilityEvent} if accessibility is
|
||||||
|
* enabled. No operation if accessibility is disabled.
|
||||||
|
*
|
||||||
|
* @param event The event to send.
|
||||||
|
*/
|
||||||
|
public void requestSendAccessibilityEvent(final AccessibilityEvent event) {
|
||||||
|
if (mAccessibilityManager.isEnabled()) {
|
||||||
|
mAccessibilityManager.sendAccessibilityEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,365 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.accessibility;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.util.SparseIntArray;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.Key;
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.Keyboard;
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.KeyboardId;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.R;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.common.Constants;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.common.StringUtils;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
final class KeyCodeDescriptionMapper {
|
||||||
|
private static final String TAG = KeyCodeDescriptionMapper.class.getSimpleName();
|
||||||
|
private static final String SPOKEN_LETTER_RESOURCE_NAME_FORMAT = "spoken_accented_letter_%04X";
|
||||||
|
private static final String SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT = "spoken_symbol_%04X";
|
||||||
|
private static final String SPOKEN_EMOJI_RESOURCE_NAME_FORMAT = "spoken_emoji_%04X";
|
||||||
|
private static final String SPOKEN_EMOTICON_RESOURCE_NAME_PREFIX = "spoken_emoticon";
|
||||||
|
private static final String SPOKEN_EMOTICON_CODE_POINT_FORMAT = "_%02X";
|
||||||
|
|
||||||
|
// The resource ID of the string spoken for obscured keys
|
||||||
|
private static final int OBSCURED_KEY_RES_ID = R.string.spoken_description_dot;
|
||||||
|
|
||||||
|
private static final KeyCodeDescriptionMapper sInstance = new KeyCodeDescriptionMapper();
|
||||||
|
|
||||||
|
public static KeyCodeDescriptionMapper getInstance() {
|
||||||
|
return sInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sparse array of spoken description resource IDs indexed by key codes
|
||||||
|
private final SparseIntArray mKeyCodeMap = new SparseIntArray();
|
||||||
|
|
||||||
|
private KeyCodeDescriptionMapper() {
|
||||||
|
// Special non-character codes defined in Keyboard
|
||||||
|
mKeyCodeMap.put(Constants.CODE_SPACE, R.string.spoken_description_space);
|
||||||
|
mKeyCodeMap.put(Constants.CODE_DELETE, R.string.spoken_description_delete);
|
||||||
|
mKeyCodeMap.put(Constants.CODE_ENTER, R.string.spoken_description_return);
|
||||||
|
mKeyCodeMap.put(Constants.CODE_SETTINGS, R.string.spoken_description_settings);
|
||||||
|
mKeyCodeMap.put(Constants.CODE_SHIFT, R.string.spoken_description_shift);
|
||||||
|
mKeyCodeMap.put(Constants.CODE_SHORTCUT, R.string.spoken_description_mic);
|
||||||
|
mKeyCodeMap.put(Constants.CODE_SWITCH_ALPHA_SYMBOL, R.string.spoken_description_to_symbol);
|
||||||
|
mKeyCodeMap.put(Constants.CODE_TAB, R.string.spoken_description_tab);
|
||||||
|
mKeyCodeMap.put(Constants.CODE_LANGUAGE_SWITCH,
|
||||||
|
R.string.spoken_description_language_switch);
|
||||||
|
mKeyCodeMap.put(Constants.CODE_ACTION_NEXT, R.string.spoken_description_action_next);
|
||||||
|
mKeyCodeMap.put(Constants.CODE_ACTION_PREVIOUS,
|
||||||
|
R.string.spoken_description_action_previous);
|
||||||
|
mKeyCodeMap.put(Constants.CODE_EMOJI, R.string.spoken_description_emoji);
|
||||||
|
// Because the upper-case and lower-case mappings of the following letters is depending on
|
||||||
|
// the locale, the upper case descriptions should be defined here. The lower case
|
||||||
|
// descriptions are handled in {@link #getSpokenLetterDescriptionId(Context,int)}.
|
||||||
|
// U+0049: "I" LATIN CAPITAL LETTER I
|
||||||
|
// U+0069: "i" LATIN SMALL LETTER I
|
||||||
|
// U+0130: "İ" LATIN CAPITAL LETTER I WITH DOT ABOVE
|
||||||
|
// U+0131: "ı" LATIN SMALL LETTER DOTLESS I
|
||||||
|
mKeyCodeMap.put(0x0049, R.string.spoken_letter_0049);
|
||||||
|
mKeyCodeMap.put(0x0130, R.string.spoken_letter_0130);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the localized description of the action performed by a specified
|
||||||
|
* key based on the current keyboard state.
|
||||||
|
*
|
||||||
|
* @param context The package's context.
|
||||||
|
* @param keyboard The keyboard on which the key resides.
|
||||||
|
* @param key The key from which to obtain a description.
|
||||||
|
* @param shouldObscure {@true} if text (e.g. non-control) characters should be obscured.
|
||||||
|
* @return a character sequence describing the action performed by pressing the key
|
||||||
|
*/
|
||||||
|
public String getDescriptionForKey(final Context context, final Keyboard keyboard,
|
||||||
|
final Key key, final boolean shouldObscure) {
|
||||||
|
final int code = key.getCode();
|
||||||
|
|
||||||
|
if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
|
||||||
|
final String description = getDescriptionForSwitchAlphaSymbol(context, keyboard);
|
||||||
|
if (description != null) {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code == Constants.CODE_SHIFT) {
|
||||||
|
return getDescriptionForShiftKey(context, keyboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code == Constants.CODE_ENTER) {
|
||||||
|
// The following function returns the correct description in all action and
|
||||||
|
// regular enter cases, taking care of all modes.
|
||||||
|
return getDescriptionForActionKey(context, keyboard, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code == Constants.CODE_OUTPUT_TEXT) {
|
||||||
|
final String outputText = key.getOutputText();
|
||||||
|
final String description = getSpokenEmoticonDescription(context, outputText);
|
||||||
|
return TextUtils.isEmpty(description) ? outputText : description;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just attempt to speak the description.
|
||||||
|
if (code != Constants.CODE_UNSPECIFIED) {
|
||||||
|
// If the key description should be obscured, now is the time to do it.
|
||||||
|
final boolean isDefinedNonCtrl = Character.isDefined(code)
|
||||||
|
&& !Character.isISOControl(code);
|
||||||
|
if (shouldObscure && isDefinedNonCtrl) {
|
||||||
|
return context.getString(OBSCURED_KEY_RES_ID);
|
||||||
|
}
|
||||||
|
final String description = getDescriptionForCodePoint(context, code);
|
||||||
|
if (description != null) {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
if (!TextUtils.isEmpty(key.getLabel())) {
|
||||||
|
return key.getLabel();
|
||||||
|
}
|
||||||
|
return context.getString(R.string.spoken_description_unknown);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a context-specific description for the CODE_SWITCH_ALPHA_SYMBOL
|
||||||
|
* key or {@code null} if there is not a description provided for the
|
||||||
|
* current keyboard context.
|
||||||
|
*
|
||||||
|
* @param context The package's context.
|
||||||
|
* @param keyboard The keyboard on which the key resides.
|
||||||
|
* @return a character sequence describing the action performed by pressing the key
|
||||||
|
*/
|
||||||
|
private static String getDescriptionForSwitchAlphaSymbol(final Context context,
|
||||||
|
final Keyboard keyboard) {
|
||||||
|
final KeyboardId keyboardId = keyboard.mId;
|
||||||
|
final int elementId = keyboardId.mElementId;
|
||||||
|
final int resId;
|
||||||
|
|
||||||
|
switch (elementId) {
|
||||||
|
case KeyboardId.ELEMENT_ALPHABET:
|
||||||
|
case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
|
||||||
|
case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
|
||||||
|
case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
|
||||||
|
case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
|
||||||
|
resId = R.string.spoken_description_to_symbol;
|
||||||
|
break;
|
||||||
|
case KeyboardId.ELEMENT_SYMBOLS:
|
||||||
|
case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
|
||||||
|
resId = R.string.spoken_description_to_alpha;
|
||||||
|
break;
|
||||||
|
case KeyboardId.ELEMENT_PHONE:
|
||||||
|
resId = R.string.spoken_description_to_symbol;
|
||||||
|
break;
|
||||||
|
case KeyboardId.ELEMENT_PHONE_SYMBOLS:
|
||||||
|
resId = R.string.spoken_description_to_numeric;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.e(TAG, "Missing description for keyboard element ID:" + elementId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return context.getString(resId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a context-sensitive description of the "Shift" key.
|
||||||
|
*
|
||||||
|
* @param context The package's context.
|
||||||
|
* @param keyboard The keyboard on which the key resides.
|
||||||
|
* @return A context-sensitive description of the "Shift" key.
|
||||||
|
*/
|
||||||
|
private static String getDescriptionForShiftKey(final Context context,
|
||||||
|
final Keyboard keyboard) {
|
||||||
|
final KeyboardId keyboardId = keyboard.mId;
|
||||||
|
final int elementId = keyboardId.mElementId;
|
||||||
|
final int resId;
|
||||||
|
|
||||||
|
switch (elementId) {
|
||||||
|
case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
|
||||||
|
case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
|
||||||
|
resId = R.string.spoken_description_caps_lock;
|
||||||
|
break;
|
||||||
|
case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
|
||||||
|
case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
|
||||||
|
resId = R.string.spoken_description_shift_shifted;
|
||||||
|
break;
|
||||||
|
case KeyboardId.ELEMENT_SYMBOLS:
|
||||||
|
resId = R.string.spoken_description_symbols_shift;
|
||||||
|
break;
|
||||||
|
case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
|
||||||
|
resId = R.string.spoken_description_symbols_shift_shifted;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
resId = R.string.spoken_description_shift;
|
||||||
|
}
|
||||||
|
return context.getString(resId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a context-sensitive description of the "Enter" action key.
|
||||||
|
*
|
||||||
|
* @param context The package's context.
|
||||||
|
* @param keyboard The keyboard on which the key resides.
|
||||||
|
* @param key The key to describe.
|
||||||
|
* @return Returns a context-sensitive description of the "Enter" action key.
|
||||||
|
*/
|
||||||
|
private static String getDescriptionForActionKey(final Context context, final Keyboard keyboard,
|
||||||
|
final Key key) {
|
||||||
|
final KeyboardId keyboardId = keyboard.mId;
|
||||||
|
final int actionId = keyboardId.imeAction();
|
||||||
|
final int resId;
|
||||||
|
|
||||||
|
// Always use the label, if available.
|
||||||
|
if (!TextUtils.isEmpty(key.getLabel())) {
|
||||||
|
return key.getLabel().trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, use the action ID.
|
||||||
|
switch (actionId) {
|
||||||
|
case EditorInfo.IME_ACTION_SEARCH:
|
||||||
|
resId = R.string.spoken_description_search;
|
||||||
|
break;
|
||||||
|
case EditorInfo.IME_ACTION_GO:
|
||||||
|
resId = R.string.label_go_key;
|
||||||
|
break;
|
||||||
|
case EditorInfo.IME_ACTION_SEND:
|
||||||
|
resId = R.string.label_send_key;
|
||||||
|
break;
|
||||||
|
case EditorInfo.IME_ACTION_NEXT:
|
||||||
|
resId = R.string.label_next_key;
|
||||||
|
break;
|
||||||
|
case EditorInfo.IME_ACTION_DONE:
|
||||||
|
resId = R.string.label_done_key;
|
||||||
|
break;
|
||||||
|
case EditorInfo.IME_ACTION_PREVIOUS:
|
||||||
|
resId = R.string.label_previous_key;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
resId = R.string.spoken_description_return;
|
||||||
|
}
|
||||||
|
return context.getString(resId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a localized character sequence describing what will happen when
|
||||||
|
* the specified key is pressed based on its key code point.
|
||||||
|
*
|
||||||
|
* @param context The package's context.
|
||||||
|
* @param codePoint The code point from which to obtain a description.
|
||||||
|
* @return a character sequence describing the code point.
|
||||||
|
*/
|
||||||
|
public String getDescriptionForCodePoint(final Context context, final int codePoint) {
|
||||||
|
// If the key description should be obscured, now is the time to do it.
|
||||||
|
final int index = mKeyCodeMap.indexOfKey(codePoint);
|
||||||
|
if (index >= 0) {
|
||||||
|
return context.getString(mKeyCodeMap.valueAt(index));
|
||||||
|
}
|
||||||
|
final String accentedLetter = getSpokenAccentedLetterDescription(context, codePoint);
|
||||||
|
if (accentedLetter != null) {
|
||||||
|
return accentedLetter;
|
||||||
|
}
|
||||||
|
// Here, <code>code</code> may be a base (non-accented) letter.
|
||||||
|
final String unsupportedSymbol = getSpokenSymbolDescription(context, codePoint);
|
||||||
|
if (unsupportedSymbol != null) {
|
||||||
|
return unsupportedSymbol;
|
||||||
|
}
|
||||||
|
final String emojiDescription = getSpokenEmojiDescription(context, codePoint);
|
||||||
|
if (emojiDescription != null) {
|
||||||
|
return emojiDescription;
|
||||||
|
}
|
||||||
|
if (Character.isDefined(codePoint) && !Character.isISOControl(codePoint)) {
|
||||||
|
return StringUtils.newSingleCodePointString(codePoint);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove this method once TTS supports those accented letters' verbalization.
|
||||||
|
private String getSpokenAccentedLetterDescription(final Context context, final int code) {
|
||||||
|
final boolean isUpperCase = Character.isUpperCase(code);
|
||||||
|
final int baseCode = isUpperCase ? Character.toLowerCase(code) : code;
|
||||||
|
final int baseIndex = mKeyCodeMap.indexOfKey(baseCode);
|
||||||
|
final int resId = (baseIndex >= 0) ? mKeyCodeMap.valueAt(baseIndex)
|
||||||
|
: getSpokenDescriptionId(context, baseCode, SPOKEN_LETTER_RESOURCE_NAME_FORMAT);
|
||||||
|
if (resId == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final String spokenText = context.getString(resId);
|
||||||
|
return isUpperCase ? context.getString(R.string.spoken_description_upper_case, spokenText)
|
||||||
|
: spokenText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove this method once TTS supports those symbols' verbalization.
|
||||||
|
private String getSpokenSymbolDescription(final Context context, final int code) {
|
||||||
|
final int resId = getSpokenDescriptionId(context, code, SPOKEN_SYMBOL_RESOURCE_NAME_FORMAT);
|
||||||
|
if (resId == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final String spokenText = context.getString(resId);
|
||||||
|
if (!TextUtils.isEmpty(spokenText)) {
|
||||||
|
return spokenText;
|
||||||
|
}
|
||||||
|
// If a translated description is empty, fall back to unknown symbol description.
|
||||||
|
return context.getString(R.string.spoken_symbol_unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove this method once TTS supports emoji verbalization.
|
||||||
|
private String getSpokenEmojiDescription(final Context context, final int code) {
|
||||||
|
final int resId = getSpokenDescriptionId(context, code, SPOKEN_EMOJI_RESOURCE_NAME_FORMAT);
|
||||||
|
if (resId == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final String spokenText = context.getString(resId);
|
||||||
|
if (!TextUtils.isEmpty(spokenText)) {
|
||||||
|
return spokenText;
|
||||||
|
}
|
||||||
|
// If a translated description is empty, fall back to unknown emoji description.
|
||||||
|
return context.getString(R.string.spoken_emoji_unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getSpokenDescriptionId(final Context context, final int code,
|
||||||
|
final String resourceNameFormat) {
|
||||||
|
final String resourceName = String.format(Locale.ROOT, resourceNameFormat, code);
|
||||||
|
final Resources resources = context.getResources();
|
||||||
|
// Note that the resource package name may differ from the context package name.
|
||||||
|
final String resourcePackageName = resources.getResourcePackageName(
|
||||||
|
R.string.spoken_description_unknown);
|
||||||
|
final int resId = resources.getIdentifier(resourceName, "string", resourcePackageName);
|
||||||
|
if (resId != 0) {
|
||||||
|
mKeyCodeMap.append(code, resId);
|
||||||
|
}
|
||||||
|
return resId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove this method once TTS supports emoticon verbalization.
|
||||||
|
private static String getSpokenEmoticonDescription(final Context context,
|
||||||
|
final String outputText) {
|
||||||
|
final StringBuilder sb = new StringBuilder(SPOKEN_EMOTICON_RESOURCE_NAME_PREFIX);
|
||||||
|
final int textLength = outputText.length();
|
||||||
|
for (int index = 0; index < textLength; index = outputText.offsetByCodePoints(index, 1)) {
|
||||||
|
final int codePoint = outputText.codePointAt(index);
|
||||||
|
sb.append(String.format(Locale.ROOT, SPOKEN_EMOTICON_CODE_POINT_FORMAT, codePoint));
|
||||||
|
}
|
||||||
|
final String resourceName = sb.toString();
|
||||||
|
final Resources resources = context.getResources();
|
||||||
|
// Note that the resource package name may differ from the context package name.
|
||||||
|
final String resourcePackageName = resources.getResourcePackageName(
|
||||||
|
R.string.spoken_description_unknown);
|
||||||
|
final int resId = resources.getIdentifier(resourceName, "string", resourcePackageName);
|
||||||
|
return (resId == 0) ? null : resources.getString(resId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,327 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.accessibility;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import androidx.core.view.AccessibilityDelegateCompat;
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewParent;
|
||||||
|
import android.view.accessibility.AccessibilityEvent;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.Key;
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.KeyDetector;
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.Keyboard;
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.KeyboardView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents a delegate that can be registered in a class that extends
|
||||||
|
* {@link KeyboardView} to enhance accessibility support via composition rather via inheritance.
|
||||||
|
*
|
||||||
|
* To implement accessibility mode, the target keyboard view has to:<p>
|
||||||
|
* - Call {@link #setKeyboard(Keyboard)} when a new keyboard is set to the keyboard view.
|
||||||
|
* - Dispatch a hover event by calling {@link #onHoverEnter(MotionEvent)}.
|
||||||
|
*
|
||||||
|
* @param <KV> The keyboard view class type.
|
||||||
|
*/
|
||||||
|
public class KeyboardAccessibilityDelegate<KV extends KeyboardView>
|
||||||
|
extends AccessibilityDelegateCompat {
|
||||||
|
private static final String TAG = KeyboardAccessibilityDelegate.class.getSimpleName();
|
||||||
|
protected static final boolean DEBUG_HOVER = false;
|
||||||
|
|
||||||
|
protected final KV mKeyboardView;
|
||||||
|
protected final KeyDetector mKeyDetector;
|
||||||
|
private Keyboard mKeyboard;
|
||||||
|
private KeyboardAccessibilityNodeProvider<KV> mAccessibilityNodeProvider;
|
||||||
|
private Key mLastHoverKey;
|
||||||
|
|
||||||
|
public static final int HOVER_EVENT_POINTER_ID = 0;
|
||||||
|
|
||||||
|
public KeyboardAccessibilityDelegate(final KV keyboardView, final KeyDetector keyDetector) {
|
||||||
|
super();
|
||||||
|
mKeyboardView = keyboardView;
|
||||||
|
mKeyDetector = keyDetector;
|
||||||
|
|
||||||
|
// Ensure that the view has an accessibility delegate.
|
||||||
|
ViewCompat.setAccessibilityDelegate(keyboardView, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the keyboard layout changes.
|
||||||
|
* <p>
|
||||||
|
* <b>Note:</b> This method will be called even if accessibility is not
|
||||||
|
* enabled.
|
||||||
|
* @param keyboard The keyboard that is being set to the wrapping view.
|
||||||
|
*/
|
||||||
|
public void setKeyboard(final Keyboard keyboard) {
|
||||||
|
if (keyboard == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mAccessibilityNodeProvider != null) {
|
||||||
|
mAccessibilityNodeProvider.setKeyboard(keyboard);
|
||||||
|
}
|
||||||
|
mKeyboard = keyboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final Keyboard getKeyboard() {
|
||||||
|
return mKeyboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final void setLastHoverKey(final Key key) {
|
||||||
|
mLastHoverKey = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected final Key getLastHoverKey() {
|
||||||
|
return mLastHoverKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a window state change event with the specified string resource id.
|
||||||
|
*
|
||||||
|
* @param resId The string resource id of the text to send with the event.
|
||||||
|
*/
|
||||||
|
protected void sendWindowStateChanged(final int resId) {
|
||||||
|
if (resId == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Context context = mKeyboardView.getContext();
|
||||||
|
sendWindowStateChanged(context.getString(resId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a window state change event with the specified text.
|
||||||
|
*
|
||||||
|
* @param text The text to send with the event.
|
||||||
|
*/
|
||||||
|
protected void sendWindowStateChanged(final String text) {
|
||||||
|
final AccessibilityEvent stateChange = AccessibilityEvent.obtain(
|
||||||
|
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
|
||||||
|
mKeyboardView.onInitializeAccessibilityEvent(stateChange);
|
||||||
|
stateChange.getText().add(text);
|
||||||
|
stateChange.setContentDescription(null);
|
||||||
|
|
||||||
|
final ViewParent parent = mKeyboardView.getParent();
|
||||||
|
if (parent != null) {
|
||||||
|
parent.requestSendAccessibilityEvent(mKeyboardView, stateChange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegate method for View.getAccessibilityNodeProvider(). This method is called in SDK
|
||||||
|
* version 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) and higher to obtain the virtual
|
||||||
|
* node hierarchy provider.
|
||||||
|
*
|
||||||
|
* @param host The host view for the provider.
|
||||||
|
* @return The accessibility node provider for the current keyboard.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public KeyboardAccessibilityNodeProvider<KV> getAccessibilityNodeProvider(final View host) {
|
||||||
|
return getAccessibilityNodeProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A lazily-instantiated node provider for this view delegate.
|
||||||
|
*/
|
||||||
|
protected KeyboardAccessibilityNodeProvider<KV> getAccessibilityNodeProvider() {
|
||||||
|
// Instantiate the provide only when requested. Since the system
|
||||||
|
// will call this method multiple times it is a good practice to
|
||||||
|
// cache the provider instance.
|
||||||
|
if (mAccessibilityNodeProvider == null) {
|
||||||
|
mAccessibilityNodeProvider =
|
||||||
|
new KeyboardAccessibilityNodeProvider<>(mKeyboardView, this);
|
||||||
|
}
|
||||||
|
return mAccessibilityNodeProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a key that a hover event is on.
|
||||||
|
*
|
||||||
|
* @param event The hover event.
|
||||||
|
* @return key The key that the <code>event</code> is on.
|
||||||
|
*/
|
||||||
|
protected final Key getHoverKeyOf(final MotionEvent event) {
|
||||||
|
final int actionIndex = event.getActionIndex();
|
||||||
|
final int x = (int)event.getX(actionIndex);
|
||||||
|
final int y = (int)event.getY(actionIndex);
|
||||||
|
return mKeyDetector.detectHitKey(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives hover events when touch exploration is turned on in SDK versions ICS and higher.
|
||||||
|
*
|
||||||
|
* @param event The hover event.
|
||||||
|
* @return {@code true} if the event is handled.
|
||||||
|
*/
|
||||||
|
public boolean onHoverEvent(final MotionEvent event) {
|
||||||
|
switch (event.getActionMasked()) {
|
||||||
|
case MotionEvent.ACTION_HOVER_ENTER:
|
||||||
|
onHoverEnter(event);
|
||||||
|
break;
|
||||||
|
case MotionEvent.ACTION_HOVER_MOVE:
|
||||||
|
onHoverMove(event);
|
||||||
|
break;
|
||||||
|
case MotionEvent.ACTION_HOVER_EXIT:
|
||||||
|
onHoverExit(event);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.w(getClass().getSimpleName(), "Unknown hover event: " + event);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process {@link MotionEvent#ACTION_HOVER_ENTER} event.
|
||||||
|
*
|
||||||
|
* @param event A hover enter event.
|
||||||
|
*/
|
||||||
|
protected void onHoverEnter(final MotionEvent event) {
|
||||||
|
final Key key = getHoverKeyOf(event);
|
||||||
|
if (DEBUG_HOVER) {
|
||||||
|
Log.d(TAG, "onHoverEnter: key=" + key);
|
||||||
|
}
|
||||||
|
if (key != null) {
|
||||||
|
onHoverEnterTo(key);
|
||||||
|
}
|
||||||
|
setLastHoverKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process {@link MotionEvent#ACTION_HOVER_MOVE} event.
|
||||||
|
*
|
||||||
|
* @param event A hover move event.
|
||||||
|
*/
|
||||||
|
protected void onHoverMove(final MotionEvent event) {
|
||||||
|
final Key lastKey = getLastHoverKey();
|
||||||
|
final Key key = getHoverKeyOf(event);
|
||||||
|
if (key != lastKey) {
|
||||||
|
if (lastKey != null) {
|
||||||
|
onHoverExitFrom(lastKey);
|
||||||
|
}
|
||||||
|
if (key != null) {
|
||||||
|
onHoverEnterTo(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (key != null) {
|
||||||
|
onHoverMoveWithin(key);
|
||||||
|
}
|
||||||
|
setLastHoverKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process {@link MotionEvent#ACTION_HOVER_EXIT} event.
|
||||||
|
*
|
||||||
|
* @param event A hover exit event.
|
||||||
|
*/
|
||||||
|
protected void onHoverExit(final MotionEvent event) {
|
||||||
|
final Key lastKey = getLastHoverKey();
|
||||||
|
if (DEBUG_HOVER) {
|
||||||
|
Log.d(TAG, "onHoverExit: key=" + getHoverKeyOf(event) + " last=" + lastKey);
|
||||||
|
}
|
||||||
|
if (lastKey != null) {
|
||||||
|
onHoverExitFrom(lastKey);
|
||||||
|
}
|
||||||
|
final Key key = getHoverKeyOf(event);
|
||||||
|
// Make sure we're not getting an EXIT event because the user slid
|
||||||
|
// off the keyboard area, then force a key press.
|
||||||
|
if (key != null) {
|
||||||
|
performClickOn(key);
|
||||||
|
onHoverExitFrom(key);
|
||||||
|
}
|
||||||
|
setLastHoverKey(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform click on a key.
|
||||||
|
*
|
||||||
|
* @param key A key to be registered.
|
||||||
|
*/
|
||||||
|
public void performClickOn(final Key key) {
|
||||||
|
if (DEBUG_HOVER) {
|
||||||
|
Log.d(TAG, "performClickOn: key=" + key);
|
||||||
|
}
|
||||||
|
simulateTouchEvent(MotionEvent.ACTION_DOWN, key);
|
||||||
|
simulateTouchEvent(MotionEvent.ACTION_UP, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simulating a touch event by injecting a synthesized touch event into {@link KeyboardView}.
|
||||||
|
*
|
||||||
|
* @param touchAction The action of the synthesizing touch event.
|
||||||
|
* @param key The key that a synthesized touch event is on.
|
||||||
|
*/
|
||||||
|
private void simulateTouchEvent(final int touchAction, final Key key) {
|
||||||
|
final int x = key.getHitBox().centerX();
|
||||||
|
final int y = key.getHitBox().centerY();
|
||||||
|
final long eventTime = SystemClock.uptimeMillis();
|
||||||
|
final MotionEvent touchEvent = MotionEvent.obtain(
|
||||||
|
eventTime, eventTime, touchAction, x, y, 0 /* metaState */);
|
||||||
|
mKeyboardView.onTouchEvent(touchEvent);
|
||||||
|
touchEvent.recycle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a hover enter event on a key.
|
||||||
|
*
|
||||||
|
* @param key The currently hovered key.
|
||||||
|
*/
|
||||||
|
protected void onHoverEnterTo(final Key key) {
|
||||||
|
if (DEBUG_HOVER) {
|
||||||
|
Log.d(TAG, "onHoverEnterTo: key=" + key);
|
||||||
|
}
|
||||||
|
key.onPressed();
|
||||||
|
mKeyboardView.invalidateKey(key);
|
||||||
|
final KeyboardAccessibilityNodeProvider<KV> provider = getAccessibilityNodeProvider();
|
||||||
|
provider.onHoverEnterTo(key);
|
||||||
|
provider.performActionForKey(key, AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a hover move event on a key.
|
||||||
|
*
|
||||||
|
* @param key The currently hovered key.
|
||||||
|
*/
|
||||||
|
protected void onHoverMoveWithin(final Key key) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles a hover exit event on a key.
|
||||||
|
*
|
||||||
|
* @param key The currently hovered key.
|
||||||
|
*/
|
||||||
|
protected void onHoverExitFrom(final Key key) {
|
||||||
|
if (DEBUG_HOVER) {
|
||||||
|
Log.d(TAG, "onHoverExitFrom: key=" + key);
|
||||||
|
}
|
||||||
|
key.onReleased();
|
||||||
|
mKeyboardView.invalidateKey(key);
|
||||||
|
final KeyboardAccessibilityNodeProvider<KV> provider = getAccessibilityNodeProvider();
|
||||||
|
provider.onHoverExitFrom(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform long click on a key.
|
||||||
|
*
|
||||||
|
* @param key A key to be long pressed on.
|
||||||
|
*/
|
||||||
|
public void performLongClickOn(final Key key) {
|
||||||
|
// A extended class should override this method to implement long press.
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,342 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2012 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.accessibility;
|
||||||
|
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.core.view.accessibility.AccessibilityEventCompat;
|
||||||
|
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
|
||||||
|
import androidx.core.view.accessibility.AccessibilityNodeProviderCompat;
|
||||||
|
import androidx.core.view.accessibility.AccessibilityRecordCompat;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.accessibility.AccessibilityEvent;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.Key;
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.Keyboard;
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.KeyboardView;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.common.CoordinateUtils;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.settings.Settings;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.settings.SettingsValues;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes a virtual view sub-tree for {@link KeyboardView} and generates
|
||||||
|
* {@link AccessibilityEvent}s for individual {@link Key}s.
|
||||||
|
* <p>
|
||||||
|
* A virtual sub-tree is composed of imaginary {@link View}s that are reported
|
||||||
|
* as a part of the view hierarchy for accessibility purposes. This enables
|
||||||
|
* custom views that draw complex content to report them selves as a tree of
|
||||||
|
* virtual views, thus conveying their logical structure.
|
||||||
|
* </p>
|
||||||
|
*/
|
||||||
|
final class KeyboardAccessibilityNodeProvider<KV extends KeyboardView>
|
||||||
|
extends AccessibilityNodeProviderCompat {
|
||||||
|
private static final String TAG = KeyboardAccessibilityNodeProvider.class.getSimpleName();
|
||||||
|
|
||||||
|
// From {@link android.view.accessibility.AccessibilityNodeInfo#UNDEFINED_ITEM_ID}.
|
||||||
|
private static final int UNDEFINED = Integer.MAX_VALUE;
|
||||||
|
|
||||||
|
private final KeyCodeDescriptionMapper mKeyCodeDescriptionMapper;
|
||||||
|
private final AccessibilityUtils mAccessibilityUtils;
|
||||||
|
|
||||||
|
/** Temporary rect used to calculate in-screen bounds. */
|
||||||
|
private final Rect mTempBoundsInScreen = new Rect();
|
||||||
|
|
||||||
|
/** The parent view's cached on-screen location. */
|
||||||
|
private final int[] mParentLocation = CoordinateUtils.newInstance();
|
||||||
|
|
||||||
|
/** The virtual view identifier for the focused node. */
|
||||||
|
private int mAccessibilityFocusedView = UNDEFINED;
|
||||||
|
|
||||||
|
/** The virtual view identifier for the hovering node. */
|
||||||
|
private int mHoveringNodeId = UNDEFINED;
|
||||||
|
|
||||||
|
/** The keyboard view to provide an accessibility node info. */
|
||||||
|
private final KV mKeyboardView;
|
||||||
|
/** The accessibility delegate. */
|
||||||
|
private final KeyboardAccessibilityDelegate<KV> mDelegate;
|
||||||
|
|
||||||
|
/** The current keyboard. */
|
||||||
|
private Keyboard mKeyboard;
|
||||||
|
|
||||||
|
public KeyboardAccessibilityNodeProvider(final KV keyboardView,
|
||||||
|
final KeyboardAccessibilityDelegate<KV> delegate) {
|
||||||
|
super();
|
||||||
|
mKeyCodeDescriptionMapper = KeyCodeDescriptionMapper.getInstance();
|
||||||
|
mAccessibilityUtils = AccessibilityUtils.getInstance();
|
||||||
|
mKeyboardView = keyboardView;
|
||||||
|
mDelegate = delegate;
|
||||||
|
|
||||||
|
// Since this class is constructed lazily, we might not get a subsequent
|
||||||
|
// call to setKeyboard() and therefore need to call it now.
|
||||||
|
setKeyboard(keyboardView.getKeyboard());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the keyboard represented by this node provider.
|
||||||
|
*
|
||||||
|
* @param keyboard The keyboard that is being set to the keyboard view.
|
||||||
|
*/
|
||||||
|
public void setKeyboard(final Keyboard keyboard) {
|
||||||
|
mKeyboard = keyboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Key getKeyOf(final int virtualViewId) {
|
||||||
|
if (mKeyboard == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final List<Key> sortedKeys = mKeyboard.getSortedKeys();
|
||||||
|
// Use a virtual view id as an index of the sorted keys list.
|
||||||
|
if (virtualViewId >= 0 && virtualViewId < sortedKeys.size()) {
|
||||||
|
return sortedKeys.get(virtualViewId);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getVirtualViewIdOf(final Key key) {
|
||||||
|
if (mKeyboard == null) {
|
||||||
|
return View.NO_ID;
|
||||||
|
}
|
||||||
|
final List<Key> sortedKeys = mKeyboard.getSortedKeys();
|
||||||
|
final int size = sortedKeys.size();
|
||||||
|
for (int index = 0; index < size; index++) {
|
||||||
|
if (sortedKeys.get(index) == key) {
|
||||||
|
// Use an index of the sorted keys list as a virtual view id.
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return View.NO_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and populates an {@link AccessibilityEvent} for the specified key
|
||||||
|
* and event type.
|
||||||
|
*
|
||||||
|
* @param key A key on the host keyboard view.
|
||||||
|
* @param eventType The event type to create.
|
||||||
|
* @return A populated {@link AccessibilityEvent} for the key.
|
||||||
|
* @see AccessibilityEvent
|
||||||
|
*/
|
||||||
|
public AccessibilityEvent createAccessibilityEvent(final Key key, final int eventType) {
|
||||||
|
final int virtualViewId = getVirtualViewIdOf(key);
|
||||||
|
final String keyDescription = getKeyDescription(key);
|
||||||
|
final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
|
||||||
|
event.setPackageName(mKeyboardView.getContext().getPackageName());
|
||||||
|
event.setClassName(key.getClass().getName());
|
||||||
|
event.setContentDescription(keyDescription);
|
||||||
|
event.setEnabled(true);
|
||||||
|
final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event);
|
||||||
|
record.setSource(mKeyboardView, virtualViewId);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onHoverEnterTo(final Key key) {
|
||||||
|
final int id = getVirtualViewIdOf(key);
|
||||||
|
if (id == View.NO_ID) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Start hovering on the key. Because our accessibility model is lift-to-type, we should
|
||||||
|
// report the node info without click and long click actions to avoid unnecessary
|
||||||
|
// announcements.
|
||||||
|
mHoveringNodeId = id;
|
||||||
|
// Invalidate the node info of the key.
|
||||||
|
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED);
|
||||||
|
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onHoverExitFrom(final Key key) {
|
||||||
|
mHoveringNodeId = UNDEFINED;
|
||||||
|
// Invalidate the node info of the key to be able to revert the change we have done
|
||||||
|
// in {@link #onHoverEnterTo(Key)}.
|
||||||
|
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED);
|
||||||
|
sendAccessibilityEventForKey(key, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an {@link AccessibilityNodeInfoCompat} representing a virtual
|
||||||
|
* view, i.e. a descendant of the host View, with the given <code>virtualViewId</code> or
|
||||||
|
* the host View itself if <code>virtualViewId</code> equals to {@link View#NO_ID}.
|
||||||
|
* <p>
|
||||||
|
* A virtual descendant is an imaginary View that is reported as a part of
|
||||||
|
* the view hierarchy for accessibility purposes. This enables custom views
|
||||||
|
* that draw complex content to report them selves as a tree of virtual
|
||||||
|
* views, thus conveying their logical structure.
|
||||||
|
* </p>
|
||||||
|
* <p>
|
||||||
|
* The implementer is responsible for obtaining an accessibility node info
|
||||||
|
* from the pool of reusable instances and setting the desired properties of
|
||||||
|
* the node info before returning it.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param virtualViewId A client defined virtual view id.
|
||||||
|
* @return A populated {@link AccessibilityNodeInfoCompat} for a virtual descendant or the host
|
||||||
|
* View.
|
||||||
|
* @see AccessibilityNodeInfoCompat
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(final int virtualViewId) {
|
||||||
|
if (virtualViewId == UNDEFINED) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (virtualViewId == View.NO_ID) {
|
||||||
|
// We are requested to create an AccessibilityNodeInfo describing
|
||||||
|
// this View, i.e. the root of the virtual sub-tree.
|
||||||
|
final AccessibilityNodeInfoCompat rootInfo =
|
||||||
|
AccessibilityNodeInfoCompat.obtain(mKeyboardView);
|
||||||
|
ViewCompat.onInitializeAccessibilityNodeInfo(mKeyboardView, rootInfo);
|
||||||
|
updateParentLocation();
|
||||||
|
|
||||||
|
// Add the virtual children of the root View.
|
||||||
|
final List<Key> sortedKeys = mKeyboard.getSortedKeys();
|
||||||
|
final int size = sortedKeys.size();
|
||||||
|
for (int index = 0; index < size; index++) {
|
||||||
|
final Key key = sortedKeys.get(index);
|
||||||
|
if (key.isSpacer()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Use an index of the sorted keys list as a virtual view id.
|
||||||
|
rootInfo.addChild(mKeyboardView, index);
|
||||||
|
}
|
||||||
|
return rootInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the key that corresponds to the given virtual view id.
|
||||||
|
final Key key = getKeyOf(virtualViewId);
|
||||||
|
if (key == null) {
|
||||||
|
Log.e(TAG, "Invalid virtual view ID: " + virtualViewId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final String keyDescription = getKeyDescription(key);
|
||||||
|
final Rect boundsInParent = key.getHitBox();
|
||||||
|
|
||||||
|
// Calculate the key's in-screen bounds.
|
||||||
|
mTempBoundsInScreen.set(boundsInParent);
|
||||||
|
mTempBoundsInScreen.offset(
|
||||||
|
CoordinateUtils.x(mParentLocation), CoordinateUtils.y(mParentLocation));
|
||||||
|
final Rect boundsInScreen = mTempBoundsInScreen;
|
||||||
|
|
||||||
|
// Obtain and initialize an AccessibilityNodeInfo with information about the virtual view.
|
||||||
|
final AccessibilityNodeInfoCompat info = AccessibilityNodeInfoCompat.obtain();
|
||||||
|
info.setPackageName(mKeyboardView.getContext().getPackageName());
|
||||||
|
info.setClassName(key.getClass().getName());
|
||||||
|
info.setContentDescription(keyDescription);
|
||||||
|
info.setBoundsInParent(boundsInParent);
|
||||||
|
info.setBoundsInScreen(boundsInScreen);
|
||||||
|
info.setParent(mKeyboardView);
|
||||||
|
info.setSource(mKeyboardView, virtualViewId);
|
||||||
|
info.setEnabled(key.isEnabled());
|
||||||
|
info.setVisibleToUser(true);
|
||||||
|
// Don't add ACTION_CLICK and ACTION_LONG_CLOCK actions while hovering on the key.
|
||||||
|
// See {@link #onHoverEnterTo(Key)} and {@link #onHoverExitFrom(Key)}.
|
||||||
|
if (virtualViewId != mHoveringNodeId) {
|
||||||
|
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
|
||||||
|
if (key.isLongPressEnabled()) {
|
||||||
|
info.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mAccessibilityFocusedView == virtualViewId) {
|
||||||
|
info.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
|
||||||
|
} else {
|
||||||
|
info.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean performAction(final int virtualViewId, final int action,
|
||||||
|
final Bundle arguments) {
|
||||||
|
final Key key = getKeyOf(virtualViewId);
|
||||||
|
if (key == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return performActionForKey(key, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the specified accessibility action for the given key.
|
||||||
|
*
|
||||||
|
* @param key The on which to perform the action.
|
||||||
|
* @param action The action to perform.
|
||||||
|
* @return The result of performing the action, or false if the action is not supported.
|
||||||
|
*/
|
||||||
|
boolean performActionForKey(final Key key, final int action) {
|
||||||
|
switch (action) {
|
||||||
|
case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS:
|
||||||
|
mAccessibilityFocusedView = getVirtualViewIdOf(key);
|
||||||
|
sendAccessibilityEventForKey(
|
||||||
|
key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
|
||||||
|
return true;
|
||||||
|
case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
|
||||||
|
mAccessibilityFocusedView = UNDEFINED;
|
||||||
|
sendAccessibilityEventForKey(
|
||||||
|
key, AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
|
||||||
|
return true;
|
||||||
|
case AccessibilityNodeInfoCompat.ACTION_CLICK:
|
||||||
|
sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_CLICKED);
|
||||||
|
mDelegate.performClickOn(key);
|
||||||
|
return true;
|
||||||
|
case AccessibilityNodeInfoCompat.ACTION_LONG_CLICK:
|
||||||
|
sendAccessibilityEventForKey(key, AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);
|
||||||
|
mDelegate.performLongClickOn(key);
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an accessibility event for the given {@link Key}.
|
||||||
|
*
|
||||||
|
* @param key The key that's sending the event.
|
||||||
|
* @param eventType The type of event to send.
|
||||||
|
*/
|
||||||
|
void sendAccessibilityEventForKey(final Key key, final int eventType) {
|
||||||
|
final AccessibilityEvent event = createAccessibilityEvent(key, eventType);
|
||||||
|
mAccessibilityUtils.requestSendAccessibilityEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the context-specific description for a {@link Key}.
|
||||||
|
*
|
||||||
|
* @param key The key to describe.
|
||||||
|
* @return The context-specific description of the key.
|
||||||
|
*/
|
||||||
|
private String getKeyDescription(final Key key) {
|
||||||
|
final EditorInfo editorInfo = mKeyboard.mId.mEditorInfo;
|
||||||
|
final boolean shouldObscure = mAccessibilityUtils.shouldObscureInput(editorInfo);
|
||||||
|
final SettingsValues currentSettings = Settings.getInstance().getCurrent();
|
||||||
|
final String keyCodeDescription = mKeyCodeDescriptionMapper.getDescriptionForKey(
|
||||||
|
mKeyboardView.getContext(), mKeyboard, key, shouldObscure);
|
||||||
|
if (currentSettings.isWordSeparator(key.getCode())) {
|
||||||
|
return mAccessibilityUtils.getAutoCorrectionDescription(
|
||||||
|
keyCodeDescription, shouldObscure);
|
||||||
|
}
|
||||||
|
return keyCodeDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the parent's on-screen location.
|
||||||
|
*/
|
||||||
|
private void updateParentLocation() {
|
||||||
|
mKeyboardView.getLocationOnScreen(mParentLocation);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,301 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.accessibility;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.util.SparseIntArray;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.Key;
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.KeyDetector;
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.Keyboard;
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.KeyboardId;
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.MainKeyboardView;
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.PointerTracker;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.R;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.utils.SubtypeLocaleUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents a delegate that can be registered in {@link MainKeyboardView} to enhance
|
||||||
|
* accessibility support via composition rather via inheritance.
|
||||||
|
*/
|
||||||
|
public final class MainKeyboardAccessibilityDelegate
|
||||||
|
extends KeyboardAccessibilityDelegate<MainKeyboardView>
|
||||||
|
implements AccessibilityLongPressTimer.LongPressTimerCallback {
|
||||||
|
private static final String TAG = MainKeyboardAccessibilityDelegate.class.getSimpleName();
|
||||||
|
|
||||||
|
/** Map of keyboard modes to resource IDs. */
|
||||||
|
private static final SparseIntArray KEYBOARD_MODE_RES_IDS = new SparseIntArray();
|
||||||
|
|
||||||
|
static {
|
||||||
|
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATE, R.string.keyboard_mode_date);
|
||||||
|
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_DATETIME, R.string.keyboard_mode_date_time);
|
||||||
|
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_EMAIL, R.string.keyboard_mode_email);
|
||||||
|
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_IM, R.string.keyboard_mode_im);
|
||||||
|
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_NUMBER, R.string.keyboard_mode_number);
|
||||||
|
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_PHONE, R.string.keyboard_mode_phone);
|
||||||
|
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TEXT, R.string.keyboard_mode_text);
|
||||||
|
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_TIME, R.string.keyboard_mode_time);
|
||||||
|
KEYBOARD_MODE_RES_IDS.put(KeyboardId.MODE_URL, R.string.keyboard_mode_url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The most recently set keyboard mode. */
|
||||||
|
private int mLastKeyboardMode = KEYBOARD_IS_HIDDEN;
|
||||||
|
private static final int KEYBOARD_IS_HIDDEN = -1;
|
||||||
|
// The rectangle region to ignore hover events.
|
||||||
|
private final Rect mBoundsToIgnoreHoverEvent = new Rect();
|
||||||
|
|
||||||
|
private final AccessibilityLongPressTimer mAccessibilityLongPressTimer;
|
||||||
|
|
||||||
|
public MainKeyboardAccessibilityDelegate(final MainKeyboardView mainKeyboardView,
|
||||||
|
final KeyDetector keyDetector) {
|
||||||
|
super(mainKeyboardView, keyDetector);
|
||||||
|
mAccessibilityLongPressTimer = new AccessibilityLongPressTimer(
|
||||||
|
this /* callback */, mainKeyboardView.getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setKeyboard(final Keyboard keyboard) {
|
||||||
|
if (keyboard == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Keyboard lastKeyboard = getKeyboard();
|
||||||
|
super.setKeyboard(keyboard);
|
||||||
|
final int lastKeyboardMode = mLastKeyboardMode;
|
||||||
|
mLastKeyboardMode = keyboard.mId.mMode;
|
||||||
|
|
||||||
|
// Since this method is called even when accessibility is off, make sure
|
||||||
|
// to check the state before announcing anything.
|
||||||
|
if (!AccessibilityUtils.getInstance().isAccessibilityEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Announce the language name only when the language is changed.
|
||||||
|
if (lastKeyboard == null || !keyboard.mId.mSubtype.equals(lastKeyboard.mId.mSubtype)) {
|
||||||
|
announceKeyboardLanguage(keyboard);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Announce the mode only when the mode is changed.
|
||||||
|
if (keyboard.mId.mMode != lastKeyboardMode) {
|
||||||
|
announceKeyboardMode(keyboard);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Announce the keyboard type only when the type is changed.
|
||||||
|
if (keyboard.mId.mElementId != lastKeyboard.mId.mElementId) {
|
||||||
|
announceKeyboardType(keyboard, lastKeyboard);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the keyboard is hidden and accessibility is enabled.
|
||||||
|
*/
|
||||||
|
public void onHideWindow() {
|
||||||
|
if (mLastKeyboardMode != KEYBOARD_IS_HIDDEN) {
|
||||||
|
announceKeyboardHidden();
|
||||||
|
}
|
||||||
|
mLastKeyboardMode = KEYBOARD_IS_HIDDEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Announces which language of keyboard is being displayed.
|
||||||
|
*
|
||||||
|
* @param keyboard The new keyboard.
|
||||||
|
*/
|
||||||
|
private void announceKeyboardLanguage(final Keyboard keyboard) {
|
||||||
|
final String languageText = SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(
|
||||||
|
keyboard.mId.mSubtype.getRawSubtype());
|
||||||
|
sendWindowStateChanged(languageText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Announces which type of keyboard is being displayed.
|
||||||
|
* If the keyboard type is unknown, no announcement is made.
|
||||||
|
*
|
||||||
|
* @param keyboard The new keyboard.
|
||||||
|
*/
|
||||||
|
private void announceKeyboardMode(final Keyboard keyboard) {
|
||||||
|
final Context context = mKeyboardView.getContext();
|
||||||
|
final int modeTextResId = KEYBOARD_MODE_RES_IDS.get(keyboard.mId.mMode);
|
||||||
|
if (modeTextResId == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final String modeText = context.getString(modeTextResId);
|
||||||
|
final String text = context.getString(R.string.announce_keyboard_mode, modeText);
|
||||||
|
sendWindowStateChanged(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Announces which type of keyboard is being displayed.
|
||||||
|
*
|
||||||
|
* @param keyboard The new keyboard.
|
||||||
|
* @param lastKeyboard The last keyboard.
|
||||||
|
*/
|
||||||
|
private void announceKeyboardType(final Keyboard keyboard, final Keyboard lastKeyboard) {
|
||||||
|
final int lastElementId = lastKeyboard.mId.mElementId;
|
||||||
|
final int resId;
|
||||||
|
switch (keyboard.mId.mElementId) {
|
||||||
|
case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED:
|
||||||
|
case KeyboardId.ELEMENT_ALPHABET:
|
||||||
|
if (lastElementId == KeyboardId.ELEMENT_ALPHABET
|
||||||
|
|| lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) {
|
||||||
|
// Transition between alphabet mode and automatic shifted mode should be silently
|
||||||
|
// ignored because it can be determined by each key's talk back announce.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resId = R.string.spoken_description_mode_alpha;
|
||||||
|
break;
|
||||||
|
case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED:
|
||||||
|
if (lastElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) {
|
||||||
|
// Resetting automatic shifted mode by pressing the shift key causes the transition
|
||||||
|
// from automatic shifted to manual shifted that should be silently ignored.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resId = R.string.spoken_description_shiftmode_on;
|
||||||
|
break;
|
||||||
|
case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED:
|
||||||
|
if (lastElementId == KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED) {
|
||||||
|
// Resetting caps locked mode by pressing the shift key causes the transition
|
||||||
|
// from shift locked to shift lock shifted that should be silently ignored.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resId = R.string.spoken_description_shiftmode_locked;
|
||||||
|
break;
|
||||||
|
case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED:
|
||||||
|
resId = R.string.spoken_description_shiftmode_locked;
|
||||||
|
break;
|
||||||
|
case KeyboardId.ELEMENT_SYMBOLS:
|
||||||
|
resId = R.string.spoken_description_mode_symbol;
|
||||||
|
break;
|
||||||
|
case KeyboardId.ELEMENT_SYMBOLS_SHIFTED:
|
||||||
|
resId = R.string.spoken_description_mode_symbol_shift;
|
||||||
|
break;
|
||||||
|
case KeyboardId.ELEMENT_PHONE:
|
||||||
|
resId = R.string.spoken_description_mode_phone;
|
||||||
|
break;
|
||||||
|
case KeyboardId.ELEMENT_PHONE_SYMBOLS:
|
||||||
|
resId = R.string.spoken_description_mode_phone_shift;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendWindowStateChanged(resId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Announces that the keyboard has been hidden.
|
||||||
|
*/
|
||||||
|
private void announceKeyboardHidden() {
|
||||||
|
sendWindowStateChanged(R.string.announce_keyboard_hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void performClickOn(final Key key) {
|
||||||
|
final int x = key.getHitBox().centerX();
|
||||||
|
final int y = key.getHitBox().centerY();
|
||||||
|
if (DEBUG_HOVER) {
|
||||||
|
Log.d(TAG, "performClickOn: key=" + key
|
||||||
|
+ " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y));
|
||||||
|
}
|
||||||
|
if (mBoundsToIgnoreHoverEvent.contains(x, y)) {
|
||||||
|
// This hover exit event points to the key that should be ignored.
|
||||||
|
// Clear the ignoring region to handle further hover events.
|
||||||
|
mBoundsToIgnoreHoverEvent.setEmpty();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
super.performClickOn(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onHoverEnterTo(final Key key) {
|
||||||
|
final int x = key.getHitBox().centerX();
|
||||||
|
final int y = key.getHitBox().centerY();
|
||||||
|
if (DEBUG_HOVER) {
|
||||||
|
Log.d(TAG, "onHoverEnterTo: key=" + key
|
||||||
|
+ " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y));
|
||||||
|
}
|
||||||
|
mAccessibilityLongPressTimer.cancelLongPress();
|
||||||
|
if (mBoundsToIgnoreHoverEvent.contains(x, y)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// This hover enter event points to the key that isn't in the ignoring region.
|
||||||
|
// Further hover events should be handled.
|
||||||
|
mBoundsToIgnoreHoverEvent.setEmpty();
|
||||||
|
super.onHoverEnterTo(key);
|
||||||
|
if (key.isLongPressEnabled()) {
|
||||||
|
mAccessibilityLongPressTimer.startLongPress(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onHoverExitFrom(final Key key) {
|
||||||
|
final int x = key.getHitBox().centerX();
|
||||||
|
final int y = key.getHitBox().centerY();
|
||||||
|
if (DEBUG_HOVER) {
|
||||||
|
Log.d(TAG, "onHoverExitFrom: key=" + key
|
||||||
|
+ " inIgnoreBounds=" + mBoundsToIgnoreHoverEvent.contains(x, y));
|
||||||
|
}
|
||||||
|
mAccessibilityLongPressTimer.cancelLongPress();
|
||||||
|
super.onHoverExitFrom(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void performLongClickOn(final Key key) {
|
||||||
|
if (DEBUG_HOVER) {
|
||||||
|
Log.d(TAG, "performLongClickOn: key=" + key);
|
||||||
|
}
|
||||||
|
final PointerTracker tracker = PointerTracker.getPointerTracker(HOVER_EVENT_POINTER_ID);
|
||||||
|
final long eventTime = SystemClock.uptimeMillis();
|
||||||
|
final int x = key.getHitBox().centerX();
|
||||||
|
final int y = key.getHitBox().centerY();
|
||||||
|
final MotionEvent downEvent = MotionEvent.obtain(
|
||||||
|
eventTime, eventTime, MotionEvent.ACTION_DOWN, x, y, 0 /* metaState */);
|
||||||
|
// Inject a fake down event to {@link PointerTracker} to handle a long press correctly.
|
||||||
|
tracker.processMotionEvent(downEvent, mKeyDetector);
|
||||||
|
downEvent.recycle();
|
||||||
|
// Invoke {@link PointerTracker#onLongPressed()} as if a long press timeout has passed.
|
||||||
|
tracker.onLongPressed();
|
||||||
|
// If {@link Key#hasNoPanelAutoMoreKeys()} is true (such as "0 +" key on the phone layout)
|
||||||
|
// or a key invokes IME switcher dialog, we should just ignore the next
|
||||||
|
// {@link #onRegisterHoverKey(Key,MotionEvent)}. It can be determined by whether
|
||||||
|
// {@link PointerTracker} is in operation or not.
|
||||||
|
if (tracker.isInOperation()) {
|
||||||
|
// This long press shows a more keys keyboard and further hover events should be
|
||||||
|
// handled.
|
||||||
|
mBoundsToIgnoreHoverEvent.setEmpty();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// This long press has handled at {@link MainKeyboardView#onLongPress(PointerTracker)}.
|
||||||
|
// We should ignore further hover events on this key.
|
||||||
|
mBoundsToIgnoreHoverEvent.set(key.getHitBox());
|
||||||
|
if (key.hasNoPanelAutoMoreKey()) {
|
||||||
|
// This long press has registered a code point without showing a more keys keyboard.
|
||||||
|
// We should talk back the code point if possible.
|
||||||
|
final int codePointOfNoPanelAutoMoreKey = key.getMoreKeys()[0].mCode;
|
||||||
|
final String text = KeyCodeDescriptionMapper.getInstance().getDescriptionForCodePoint(
|
||||||
|
mKeyboardView.getContext(), codePointOfNoPanelAutoMoreKey);
|
||||||
|
if (text != null) {
|
||||||
|
sendWindowStateChanged(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.accessibility;
|
||||||
|
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.Key;
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.KeyDetector;
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.MoreKeysKeyboardView;
|
||||||
|
import org.dslul.openboard.inputmethod.keyboard.PointerTracker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents a delegate that can be registered in {@link MoreKeysKeyboardView} to
|
||||||
|
* enhance accessibility support via composition rather via inheritance.
|
||||||
|
*/
|
||||||
|
public class MoreKeysKeyboardAccessibilityDelegate
|
||||||
|
extends KeyboardAccessibilityDelegate<MoreKeysKeyboardView> {
|
||||||
|
private static final String TAG = MoreKeysKeyboardAccessibilityDelegate.class.getSimpleName();
|
||||||
|
|
||||||
|
private final Rect mMoreKeysKeyboardValidBounds = new Rect();
|
||||||
|
private static final int CLOSING_INSET_IN_PIXEL = 1;
|
||||||
|
private int mOpenAnnounceResId;
|
||||||
|
private int mCloseAnnounceResId;
|
||||||
|
|
||||||
|
public MoreKeysKeyboardAccessibilityDelegate(final MoreKeysKeyboardView moreKeysKeyboardView,
|
||||||
|
final KeyDetector keyDetector) {
|
||||||
|
super(moreKeysKeyboardView, keyDetector);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOpenAnnounce(final int resId) {
|
||||||
|
mOpenAnnounceResId = resId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCloseAnnounce(final int resId) {
|
||||||
|
mCloseAnnounceResId = resId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onShowMoreKeysKeyboard() {
|
||||||
|
sendWindowStateChanged(mOpenAnnounceResId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onDismissMoreKeysKeyboard() {
|
||||||
|
sendWindowStateChanged(mCloseAnnounceResId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onHoverEnter(final MotionEvent event) {
|
||||||
|
if (DEBUG_HOVER) {
|
||||||
|
Log.d(TAG, "onHoverEnter: key=" + getHoverKeyOf(event));
|
||||||
|
}
|
||||||
|
super.onHoverEnter(event);
|
||||||
|
final int actionIndex = event.getActionIndex();
|
||||||
|
final int x = (int)event.getX(actionIndex);
|
||||||
|
final int y = (int)event.getY(actionIndex);
|
||||||
|
final int pointerId = event.getPointerId(actionIndex);
|
||||||
|
final long eventTime = event.getEventTime();
|
||||||
|
mKeyboardView.onDownEvent(x, y, pointerId, eventTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onHoverMove(final MotionEvent event) {
|
||||||
|
super.onHoverMove(event);
|
||||||
|
final int actionIndex = event.getActionIndex();
|
||||||
|
final int x = (int)event.getX(actionIndex);
|
||||||
|
final int y = (int)event.getY(actionIndex);
|
||||||
|
final int pointerId = event.getPointerId(actionIndex);
|
||||||
|
final long eventTime = event.getEventTime();
|
||||||
|
mKeyboardView.onMoveEvent(x, y, pointerId, eventTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onHoverExit(final MotionEvent event) {
|
||||||
|
final Key lastKey = getLastHoverKey();
|
||||||
|
if (DEBUG_HOVER) {
|
||||||
|
Log.d(TAG, "onHoverExit: key=" + getHoverKeyOf(event) + " last=" + lastKey);
|
||||||
|
}
|
||||||
|
if (lastKey != null) {
|
||||||
|
super.onHoverExitFrom(lastKey);
|
||||||
|
}
|
||||||
|
setLastHoverKey(null);
|
||||||
|
final int actionIndex = event.getActionIndex();
|
||||||
|
final int x = (int)event.getX(actionIndex);
|
||||||
|
final int y = (int)event.getY(actionIndex);
|
||||||
|
final int pointerId = event.getPointerId(actionIndex);
|
||||||
|
final long eventTime = event.getEventTime();
|
||||||
|
// A hover exit event at one pixel width or height area on the edges of more keys keyboard
|
||||||
|
// are treated as closing.
|
||||||
|
mMoreKeysKeyboardValidBounds.set(0, 0, mKeyboardView.getWidth(), mKeyboardView.getHeight());
|
||||||
|
mMoreKeysKeyboardValidBounds.inset(CLOSING_INSET_IN_PIXEL, CLOSING_INSET_IN_PIXEL);
|
||||||
|
if (mMoreKeysKeyboardValidBounds.contains(x, y)) {
|
||||||
|
// Invoke {@link MoreKeysKeyboardView#onUpEvent(int,int,int,long)} as if this hover
|
||||||
|
// exit event selects a key.
|
||||||
|
mKeyboardView.onUpEvent(x, y, pointerId, eventTime);
|
||||||
|
// TODO: Should fix this reference. This is a hack to clear the state of
|
||||||
|
// {@link PointerTracker}.
|
||||||
|
PointerTracker.dismissAllMoreKeysPanels();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Close the more keys keyboard.
|
||||||
|
// TODO: Should fix this reference. This is a hack to clear the state of
|
||||||
|
// {@link PointerTracker}.
|
||||||
|
PointerTracker.dismissAllMoreKeysPanels();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2012 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.annotations;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Denotes that the class, method or field should not be eliminated by ProGuard,
|
||||||
|
* because it is externally referenced. (See proguard.flags)
|
||||||
|
*/
|
||||||
|
public @interface ExternallyReferenced {
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2012 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.annotations;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Denotes that the class, method or field should not be eliminated by ProGuard,
|
||||||
|
* so that unit tests can access it. (See proguard.flags)
|
||||||
|
*/
|
||||||
|
public @interface UsedForTesting {
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.app.ActivityManager;
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
public class ActivityManagerCompatUtils {
|
||||||
|
private static final Object LOCK = new Object();
|
||||||
|
private static volatile Boolean sBoolean = null;
|
||||||
|
private static final Method METHOD_isLowRamDevice = CompatUtils.getMethod(
|
||||||
|
ActivityManager.class, "isLowRamDevice");
|
||||||
|
|
||||||
|
private ActivityManagerCompatUtils() {
|
||||||
|
// Do not instantiate this class.
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isLowRamDevice(Context context) {
|
||||||
|
if (sBoolean == null) {
|
||||||
|
synchronized(LOCK) {
|
||||||
|
if (sBoolean == null) {
|
||||||
|
final ActivityManager am =
|
||||||
|
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||||
|
sBoolean = (Boolean)CompatUtils.invoke(am, false, METHOD_isLowRamDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sBoolean;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.content.pm.PackageInfo;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class AppWorkaroundsHelper {
|
||||||
|
private AppWorkaroundsHelper() {
|
||||||
|
// This helper class is not publicly instantiable.
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean evaluateIsBrokenByRecorrection(final PackageInfo info) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.content.pm.PackageInfo;
|
||||||
|
import android.os.Build.VERSION_CODES;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class to encapsulate work-arounds specific to particular apps.
|
||||||
|
*/
|
||||||
|
public class AppWorkaroundsUtils {
|
||||||
|
private final PackageInfo mPackageInfo; // May be null
|
||||||
|
private final boolean mIsBrokenByRecorrection;
|
||||||
|
|
||||||
|
public AppWorkaroundsUtils(final PackageInfo packageInfo) {
|
||||||
|
mPackageInfo = packageInfo;
|
||||||
|
mIsBrokenByRecorrection = AppWorkaroundsHelper.evaluateIsBrokenByRecorrection(
|
||||||
|
packageInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBrokenByRecorrection() {
|
||||||
|
return mIsBrokenByRecorrection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBeforeJellyBean() {
|
||||||
|
if (null == mPackageInfo || null == mPackageInfo.applicationInfo) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return mPackageInfo.applicationInfo.targetSdkVersion < VERSION_CODES.JELLY_BEAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
if (null == mPackageInfo || null == mPackageInfo.applicationInfo) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
final StringBuilder s = new StringBuilder();
|
||||||
|
s.append("Target application : ")
|
||||||
|
.append(mPackageInfo.applicationInfo.name)
|
||||||
|
.append("\nPackage : ")
|
||||||
|
.append(mPackageInfo.applicationInfo.packageName)
|
||||||
|
.append("\nTarget app sdk version : ")
|
||||||
|
.append(mPackageInfo.applicationInfo.targetSdkVersion);
|
||||||
|
return s.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
public final class BuildCompatUtils {
|
||||||
|
private BuildCompatUtils() {
|
||||||
|
// This utility class is not publicly instantiable.
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final boolean IS_RELEASE_BUILD = Build.VERSION.CODENAME.equals("REL");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The "effective" API version.
|
||||||
|
* {@link android.os.Build.VERSION#SDK_INT} if the platform is a release build.
|
||||||
|
* {@link android.os.Build.VERSION#SDK_INT} plus 1 if the platform is a development build.
|
||||||
|
*/
|
||||||
|
public static final int EFFECTIVE_SDK_INT = IS_RELEASE_BUILD
|
||||||
|
? Build.VERSION.SDK_INT
|
||||||
|
: Build.VERSION.SDK_INT + 1;
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
public final class CharacterCompat {
|
||||||
|
// Note that Character.isAlphabetic(int), has been introduced in API level 19
|
||||||
|
// (Build.VERSION_CODE.KITKAT).
|
||||||
|
private static final Method METHOD_isAlphabetic = CompatUtils.getMethod(
|
||||||
|
Character.class, "isAlphabetic", int.class);
|
||||||
|
|
||||||
|
private CharacterCompat() {
|
||||||
|
// This utility class is not publicly instantiable.
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAlphabetic(final int code) {
|
||||||
|
if (METHOD_isAlphabetic != null) {
|
||||||
|
return (Boolean)CompatUtils.invoke(null, false, METHOD_isAlphabetic, code);
|
||||||
|
}
|
||||||
|
switch (Character.getType(code)) {
|
||||||
|
case Character.UPPERCASE_LETTER:
|
||||||
|
case Character.LOWERCASE_LETTER:
|
||||||
|
case Character.TITLECASE_LETTER:
|
||||||
|
case Character.MODIFIER_LETTER:
|
||||||
|
case Character.OTHER_LETTER:
|
||||||
|
case Character.LETTER_NUMBER:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,218 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
public final class CompatUtils {
|
||||||
|
private static final String TAG = CompatUtils.class.getSimpleName();
|
||||||
|
|
||||||
|
private CompatUtils() {
|
||||||
|
// This utility class is not publicly instantiable.
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Class<?> getClass(final String className) {
|
||||||
|
try {
|
||||||
|
return Class.forName(className);
|
||||||
|
} catch (final ClassNotFoundException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Method getMethod(final Class<?> targetClass, final String name,
|
||||||
|
final Class<?>... parameterTypes) {
|
||||||
|
if (targetClass == null || TextUtils.isEmpty(name)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return targetClass.getMethod(name, parameterTypes);
|
||||||
|
} catch (final SecurityException | NoSuchMethodException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Field getField(final Class<?> targetClass, final String name) {
|
||||||
|
if (targetClass == null || TextUtils.isEmpty(name)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return targetClass.getField(name);
|
||||||
|
} catch (final SecurityException | NoSuchFieldException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Constructor<?> getConstructor(final Class<?> targetClass,
|
||||||
|
final Class<?> ... types) {
|
||||||
|
if (targetClass == null || types == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return targetClass.getConstructor(types);
|
||||||
|
} catch (final SecurityException | NoSuchMethodException e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Object newInstance(final Constructor<?> constructor, final Object ... args) {
|
||||||
|
if (constructor == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return constructor.newInstance(args);
|
||||||
|
} catch (final InstantiationException | IllegalAccessException | IllegalArgumentException
|
||||||
|
| InvocationTargetException e) {
|
||||||
|
Log.e(TAG, "Exception in newInstance", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Object invoke(final Object receiver, final Object defaultValue,
|
||||||
|
final Method method, final Object... args) {
|
||||||
|
if (method == null) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return method.invoke(receiver, args);
|
||||||
|
} catch (final IllegalAccessException | IllegalArgumentException
|
||||||
|
| InvocationTargetException e) {
|
||||||
|
Log.e(TAG, "Exception in invoke", e);
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Object getFieldValue(final Object receiver, final Object defaultValue,
|
||||||
|
final Field field) {
|
||||||
|
if (field == null) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return field.get(receiver);
|
||||||
|
} catch (final IllegalAccessException | IllegalArgumentException e) {
|
||||||
|
Log.e(TAG, "Exception in getFieldValue", e);
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setFieldValue(final Object receiver, final Field field, final Object value) {
|
||||||
|
if (field == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
field.set(receiver, value);
|
||||||
|
} catch (final IllegalAccessException | IllegalArgumentException e) {
|
||||||
|
Log.e(TAG, "Exception in setFieldValue", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ClassWrapper getClassWrapper(final String className) {
|
||||||
|
return new ClassWrapper(getClass(className));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class ClassWrapper {
|
||||||
|
private final Class<?> mClass;
|
||||||
|
public ClassWrapper(final Class<?> targetClass) {
|
||||||
|
mClass = targetClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean exists() {
|
||||||
|
return mClass != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> ToObjectMethodWrapper<T> getMethod(final String name,
|
||||||
|
final T defaultValue, final Class<?>... parameterTypes) {
|
||||||
|
return new ToObjectMethodWrapper<>(CompatUtils.getMethod(mClass, name, parameterTypes),
|
||||||
|
defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ToIntMethodWrapper getPrimitiveMethod(final String name, final int defaultValue,
|
||||||
|
final Class<?>... parameterTypes) {
|
||||||
|
return new ToIntMethodWrapper(CompatUtils.getMethod(mClass, name, parameterTypes),
|
||||||
|
defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ToFloatMethodWrapper getPrimitiveMethod(final String name, final float defaultValue,
|
||||||
|
final Class<?>... parameterTypes) {
|
||||||
|
return new ToFloatMethodWrapper(CompatUtils.getMethod(mClass, name, parameterTypes),
|
||||||
|
defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ToBooleanMethodWrapper getPrimitiveMethod(final String name,
|
||||||
|
final boolean defaultValue, final Class<?>... parameterTypes) {
|
||||||
|
return new ToBooleanMethodWrapper(CompatUtils.getMethod(mClass, name, parameterTypes),
|
||||||
|
defaultValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class ToObjectMethodWrapper<T> {
|
||||||
|
private final Method mMethod;
|
||||||
|
private final T mDefaultValue;
|
||||||
|
public ToObjectMethodWrapper(final Method method, final T defaultValue) {
|
||||||
|
mMethod = method;
|
||||||
|
mDefaultValue = defaultValue;
|
||||||
|
}
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public T invoke(final Object receiver, final Object... args) {
|
||||||
|
return (T) CompatUtils.invoke(receiver, mDefaultValue, mMethod, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class ToIntMethodWrapper {
|
||||||
|
private final Method mMethod;
|
||||||
|
private final int mDefaultValue;
|
||||||
|
public ToIntMethodWrapper(final Method method, final int defaultValue) {
|
||||||
|
mMethod = method;
|
||||||
|
mDefaultValue = defaultValue;
|
||||||
|
}
|
||||||
|
public int invoke(final Object receiver, final Object... args) {
|
||||||
|
return (int) CompatUtils.invoke(receiver, mDefaultValue, mMethod, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class ToFloatMethodWrapper {
|
||||||
|
private final Method mMethod;
|
||||||
|
private final float mDefaultValue;
|
||||||
|
public ToFloatMethodWrapper(final Method method, final float defaultValue) {
|
||||||
|
mMethod = method;
|
||||||
|
mDefaultValue = defaultValue;
|
||||||
|
}
|
||||||
|
public float invoke(final Object receiver, final Object... args) {
|
||||||
|
return (float) CompatUtils.invoke(receiver, mDefaultValue, mMethod, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class ToBooleanMethodWrapper {
|
||||||
|
private final Method mMethod;
|
||||||
|
private final boolean mDefaultValue;
|
||||||
|
public ToBooleanMethodWrapper(final Method method, final boolean defaultValue) {
|
||||||
|
mMethod = method;
|
||||||
|
mDefaultValue = defaultValue;
|
||||||
|
}
|
||||||
|
public boolean invoke(final Object receiver, final Object... args) {
|
||||||
|
return (boolean) CompatUtils.invoke(receiver, mDefaultValue, mMethod, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
public final class ConnectivityManagerCompatUtils {
|
||||||
|
// ConnectivityManager#isActiveNetworkMetered() has been introduced
|
||||||
|
// in API level 16 (Build.VERSION_CODES.JELLY_BEAN).
|
||||||
|
private static final Method METHOD_isActiveNetworkMetered = CompatUtils.getMethod(
|
||||||
|
ConnectivityManager.class, "isActiveNetworkMetered");
|
||||||
|
|
||||||
|
public static boolean isActiveNetworkMetered(final ConnectivityManager manager) {
|
||||||
|
return (Boolean)CompatUtils.invoke(manager,
|
||||||
|
// If the API telling whether the network is metered or not is not available,
|
||||||
|
// then the closest thing is "if it's a mobile connection".
|
||||||
|
manager.getActiveNetworkInfo().getType() == ConnectivityManager.TYPE_MOBILE,
|
||||||
|
METHOD_isActiveNetworkMetered);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,185 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.graphics.Matrix;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.inputmethod.CursorAnchorInfo;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper for {@link CursorAnchorInfo}, which has been introduced in API Level 21. You can use
|
||||||
|
* this wrapper to avoid direct dependency on newly introduced types.
|
||||||
|
*/
|
||||||
|
public class CursorAnchorInfoCompatWrapper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The insertion marker or character bounds have at least one visible region.
|
||||||
|
*/
|
||||||
|
public static final int FLAG_HAS_VISIBLE_REGION = 0x01;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The insertion marker or character bounds have at least one invisible (clipped) region.
|
||||||
|
*/
|
||||||
|
public static final int FLAG_HAS_INVISIBLE_REGION = 0x02;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The insertion marker or character bounds is placed at right-to-left (RTL) character.
|
||||||
|
*/
|
||||||
|
public static final int FLAG_IS_RTL = 0x04;
|
||||||
|
|
||||||
|
CursorAnchorInfoCompatWrapper() {
|
||||||
|
// This class is not publicly instantiable.
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
@Nullable
|
||||||
|
public static CursorAnchorInfoCompatWrapper wrap(@Nullable final CursorAnchorInfo instance) {
|
||||||
|
if (BuildCompatUtils.EFFECTIVE_SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (instance == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new RealWrapper(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSelectionStart() {
|
||||||
|
throw new UnsupportedOperationException("not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSelectionEnd() {
|
||||||
|
throw new UnsupportedOperationException("not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public CharSequence getComposingText() {
|
||||||
|
throw new UnsupportedOperationException("not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getComposingTextStart() {
|
||||||
|
throw new UnsupportedOperationException("not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Matrix getMatrix() {
|
||||||
|
throw new UnsupportedOperationException("not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public RectF getCharacterBounds(final int index) {
|
||||||
|
throw new UnsupportedOperationException("not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public int getCharacterBoundsFlags(final int index) {
|
||||||
|
throw new UnsupportedOperationException("not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getInsertionMarkerBaseline() {
|
||||||
|
throw new UnsupportedOperationException("not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getInsertionMarkerBottom() {
|
||||||
|
throw new UnsupportedOperationException("not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getInsertionMarkerHorizontal() {
|
||||||
|
throw new UnsupportedOperationException("not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getInsertionMarkerTop() {
|
||||||
|
throw new UnsupportedOperationException("not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getInsertionMarkerFlags() {
|
||||||
|
throw new UnsupportedOperationException("not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
private static final class RealWrapper extends CursorAnchorInfoCompatWrapper {
|
||||||
|
|
||||||
|
@Nonnull
|
||||||
|
private final CursorAnchorInfo mInstance;
|
||||||
|
|
||||||
|
public RealWrapper(@Nonnull final CursorAnchorInfo info) {
|
||||||
|
mInstance = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSelectionStart() {
|
||||||
|
return mInstance.getSelectionStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSelectionEnd() {
|
||||||
|
return mInstance.getSelectionEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CharSequence getComposingText() {
|
||||||
|
return mInstance.getComposingText();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getComposingTextStart() {
|
||||||
|
return mInstance.getComposingTextStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Matrix getMatrix() {
|
||||||
|
return mInstance.getMatrix();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RectF getCharacterBounds(final int index) {
|
||||||
|
return mInstance.getCharacterBounds(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCharacterBoundsFlags(final int index) {
|
||||||
|
return mInstance.getCharacterBoundsFlags(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getInsertionMarkerBaseline() {
|
||||||
|
return mInstance.getInsertionMarkerBaseline();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getInsertionMarkerBottom() {
|
||||||
|
return mInstance.getInsertionMarkerBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getInsertionMarkerHorizontal() {
|
||||||
|
return mInstance.getInsertionMarkerHorizontal();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getInsertionMarkerTop() {
|
||||||
|
return mInstance.getInsertionMarkerTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getInsertionMarkerFlags() {
|
||||||
|
return mInstance.getInsertionMarkerFlags();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public final class EditorInfoCompatUtils {
|
||||||
|
// Note that EditorInfo.IME_FLAG_FORCE_ASCII has been introduced
|
||||||
|
// in API level 16 (Build.VERSION_CODES.JELLY_BEAN).
|
||||||
|
private static final Field FIELD_IME_FLAG_FORCE_ASCII = CompatUtils.getField(
|
||||||
|
EditorInfo.class, "IME_FLAG_FORCE_ASCII");
|
||||||
|
private static final Integer OBJ_IME_FLAG_FORCE_ASCII = (Integer) CompatUtils.getFieldValue(
|
||||||
|
null /* receiver */, null /* defaultValue */, FIELD_IME_FLAG_FORCE_ASCII);
|
||||||
|
private static final Field FIELD_HINT_LOCALES = CompatUtils.getField(
|
||||||
|
EditorInfo.class, "hintLocales");
|
||||||
|
|
||||||
|
private EditorInfoCompatUtils() {
|
||||||
|
// This utility class is not publicly instantiable.
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasFlagForceAscii(final int imeOptions) {
|
||||||
|
if (OBJ_IME_FLAG_FORCE_ASCII == null) return false;
|
||||||
|
return (imeOptions & OBJ_IME_FLAG_FORCE_ASCII) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String imeActionName(final int imeOptions) {
|
||||||
|
final int actionId = imeOptions & EditorInfo.IME_MASK_ACTION;
|
||||||
|
switch (actionId) {
|
||||||
|
case EditorInfo.IME_ACTION_UNSPECIFIED:
|
||||||
|
return "actionUnspecified";
|
||||||
|
case EditorInfo.IME_ACTION_NONE:
|
||||||
|
return "actionNone";
|
||||||
|
case EditorInfo.IME_ACTION_GO:
|
||||||
|
return "actionGo";
|
||||||
|
case EditorInfo.IME_ACTION_SEARCH:
|
||||||
|
return "actionSearch";
|
||||||
|
case EditorInfo.IME_ACTION_SEND:
|
||||||
|
return "actionSend";
|
||||||
|
case EditorInfo.IME_ACTION_NEXT:
|
||||||
|
return "actionNext";
|
||||||
|
case EditorInfo.IME_ACTION_DONE:
|
||||||
|
return "actionDone";
|
||||||
|
case EditorInfo.IME_ACTION_PREVIOUS:
|
||||||
|
return "actionPrevious";
|
||||||
|
default:
|
||||||
|
return "actionUnknown(" + actionId + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String imeOptionsName(final int imeOptions) {
|
||||||
|
final String action = imeActionName(imeOptions);
|
||||||
|
final StringBuilder flags = new StringBuilder();
|
||||||
|
if ((imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) {
|
||||||
|
flags.append("flagNoEnterAction|");
|
||||||
|
}
|
||||||
|
if ((imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) {
|
||||||
|
flags.append("flagNavigateNext|");
|
||||||
|
}
|
||||||
|
if ((imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0) {
|
||||||
|
flags.append("flagNavigatePrevious|");
|
||||||
|
}
|
||||||
|
if (hasFlagForceAscii(imeOptions)) {
|
||||||
|
flags.append("flagForceAscii|");
|
||||||
|
}
|
||||||
|
return (action != null) ? flags + action : flags.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Locale getPrimaryHintLocale(final EditorInfo editorInfo) {
|
||||||
|
if (editorInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final Object localeList = CompatUtils.getFieldValue(editorInfo, null, FIELD_HINT_LOCALES);
|
||||||
|
if (localeList == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (LocaleListCompatUtils.isEmpty(localeList)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return LocaleListCompatUtils.get(localeList, 0);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.view.inputmethod.InputConnection;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
|
||||||
|
public final class InputConnectionCompatUtils {
|
||||||
|
private static final CompatUtils.ClassWrapper sInputConnectionType;
|
||||||
|
private static final CompatUtils.ToBooleanMethodWrapper sRequestCursorUpdatesMethod;
|
||||||
|
static {
|
||||||
|
sInputConnectionType = new CompatUtils.ClassWrapper(InputConnection.class);
|
||||||
|
sRequestCursorUpdatesMethod = sInputConnectionType.getPrimitiveMethod(
|
||||||
|
"requestCursorUpdates", false, int.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isRequestCursorUpdatesAvailable() {
|
||||||
|
return sRequestCursorUpdatesMethod != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local copies of some constants in InputConnection until the SDK becomes publicly available.
|
||||||
|
*/
|
||||||
|
private static int CURSOR_UPDATE_IMMEDIATE = 1 << 0;
|
||||||
|
private static int CURSOR_UPDATE_MONITOR = 1 << 1;
|
||||||
|
|
||||||
|
private static boolean requestCursorUpdatesImpl(final InputConnection inputConnection,
|
||||||
|
final int cursorUpdateMode) {
|
||||||
|
if (!isRequestCursorUpdatesAvailable()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return sRequestCursorUpdatesMethod.invoke(inputConnection, cursorUpdateMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests the editor to call back {@link InputMethodManager#updateCursorAnchorInfo}.
|
||||||
|
* @param inputConnection the input connection to which the request is to be sent.
|
||||||
|
* @param enableMonitor {@code true} to request the editor to call back the method whenever the
|
||||||
|
* cursor/anchor position is changed.
|
||||||
|
* @param requestImmediateCallback {@code true} to request the editor to call back the method
|
||||||
|
* as soon as possible to notify the current cursor/anchor position to the input method.
|
||||||
|
* @return {@code false} if the request is not handled. Otherwise returns {@code true}.
|
||||||
|
*/
|
||||||
|
public static boolean requestCursorUpdates(final InputConnection inputConnection,
|
||||||
|
final boolean enableMonitor, final boolean requestImmediateCallback) {
|
||||||
|
final int cursorUpdateMode = (enableMonitor ? CURSOR_UPDATE_MONITOR : 0)
|
||||||
|
| (requestImmediateCallback ? CURSOR_UPDATE_IMMEDIATE : 0);
|
||||||
|
return requestCursorUpdatesImpl(inputConnection, cursorUpdateMode);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
public final class InputMethodManagerCompatWrapper {
|
||||||
|
// Note that InputMethodManager.switchToNextInputMethod() has been introduced
|
||||||
|
// in API level 16 (Build.VERSION_CODES.JELLY_BEAN).
|
||||||
|
private static final Method METHOD_switchToNextInputMethod = CompatUtils.getMethod(
|
||||||
|
InputMethodManager.class, "switchToNextInputMethod", IBinder.class, boolean.class);
|
||||||
|
|
||||||
|
// Note that InputMethodManager.shouldOfferSwitchingToNextInputMethod() has been introduced
|
||||||
|
// in API level 19 (Build.VERSION_CODES.KITKAT).
|
||||||
|
private static final Method METHOD_shouldOfferSwitchingToNextInputMethod =
|
||||||
|
CompatUtils.getMethod(InputMethodManager.class,
|
||||||
|
"shouldOfferSwitchingToNextInputMethod", IBinder.class);
|
||||||
|
|
||||||
|
public final InputMethodManager mImm;
|
||||||
|
|
||||||
|
public InputMethodManagerCompatWrapper(final Context context) {
|
||||||
|
mImm = (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean switchToNextInputMethod(final IBinder token, final boolean onlyCurrentIme) {
|
||||||
|
return (Boolean)CompatUtils.invoke(mImm, false /* defaultValue */,
|
||||||
|
METHOD_switchToNextInputMethod, token, onlyCurrentIme);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean shouldOfferSwitchingToNextInputMethod(final IBinder token) {
|
||||||
|
return (Boolean)CompatUtils.invoke(mImm, false /* defaultValue */,
|
||||||
|
METHOD_shouldOfferSwitchingToNextInputMethod, token);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2012 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.inputmethodservice.InputMethodService;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
public final class InputMethodServiceCompatUtils {
|
||||||
|
// Note that {@link InputMethodService#enableHardwareAcceleration} has been introduced
|
||||||
|
// in API level 17 (Build.VERSION_CODES.JELLY_BEAN_MR1).
|
||||||
|
private static final Method METHOD_enableHardwareAcceleration =
|
||||||
|
CompatUtils.getMethod(InputMethodService.class, "enableHardwareAcceleration");
|
||||||
|
|
||||||
|
private InputMethodServiceCompatUtils() {
|
||||||
|
// This utility class is not publicly instantiable.
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean enableHardwareAcceleration(final InputMethodService ims) {
|
||||||
|
return (Boolean)CompatUtils.invoke(ims, false /* defaultValue */,
|
||||||
|
METHOD_enableHardwareAcceleration);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.inputmethod.InputMethodSubtype;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.annotations.UsedForTesting;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.RichInputMethodSubtype;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.common.Constants;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
public final class InputMethodSubtypeCompatUtils {
|
||||||
|
private static final String TAG = InputMethodSubtypeCompatUtils.class.getSimpleName();
|
||||||
|
// Note that InputMethodSubtype(int nameId, int iconId, String locale, String mode,
|
||||||
|
// String extraValue, boolean isAuxiliary, boolean overridesImplicitlyEnabledSubtype, int id)
|
||||||
|
// has been introduced in API level 17 (Build.VERSION_CODE.JELLY_BEAN_MR1).
|
||||||
|
private static final Constructor<?> CONSTRUCTOR_INPUT_METHOD_SUBTYPE =
|
||||||
|
CompatUtils.getConstructor(InputMethodSubtype.class,
|
||||||
|
int.class, int.class, String.class, String.class, String.class, boolean.class,
|
||||||
|
boolean.class, int.class);
|
||||||
|
static {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
|
if (CONSTRUCTOR_INPUT_METHOD_SUBTYPE == null) {
|
||||||
|
android.util.Log.w(TAG, "Warning!!! Constructor is not defined.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that {@link InputMethodSubtype#isAsciiCapable()} has been introduced in API level 19
|
||||||
|
// (Build.VERSION_CODE.KITKAT).
|
||||||
|
private static final Method METHOD_isAsciiCapable = CompatUtils.getMethod(
|
||||||
|
InputMethodSubtype.class, "isAsciiCapable");
|
||||||
|
|
||||||
|
private InputMethodSubtypeCompatUtils() {
|
||||||
|
// This utility class is not publicly instantiable.
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Nonnull
|
||||||
|
public static InputMethodSubtype newInputMethodSubtype(int nameId, int iconId, String locale,
|
||||||
|
String mode, String extraValue, boolean isAuxiliary,
|
||||||
|
boolean overridesImplicitlyEnabledSubtype, int id) {
|
||||||
|
if (CONSTRUCTOR_INPUT_METHOD_SUBTYPE == null
|
||||||
|
|| Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
|
return new InputMethodSubtype(nameId, iconId, locale, mode, extraValue, isAuxiliary,
|
||||||
|
overridesImplicitlyEnabledSubtype);
|
||||||
|
}
|
||||||
|
return (InputMethodSubtype) CompatUtils.newInstance(CONSTRUCTOR_INPUT_METHOD_SUBTYPE,
|
||||||
|
nameId, iconId, locale, mode, extraValue, isAuxiliary,
|
||||||
|
overridesImplicitlyEnabledSubtype, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAsciiCapable(final RichInputMethodSubtype subtype) {
|
||||||
|
return isAsciiCapable(subtype.getRawSubtype());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isAsciiCapable(final InputMethodSubtype subtype) {
|
||||||
|
return isAsciiCapableWithAPI(subtype)
|
||||||
|
|| subtype.containsExtraValueKey(Constants.Subtype.ExtraValue.ASCII_CAPABLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note that InputMethodSubtype.getLanguageTag() is expected to be available in Android N+.
|
||||||
|
private static final Method GET_LANGUAGE_TAG =
|
||||||
|
CompatUtils.getMethod(InputMethodSubtype.class, "getLanguageTag");
|
||||||
|
|
||||||
|
public static Locale getLocaleObject(final InputMethodSubtype subtype) {
|
||||||
|
// Locale.forLanguageTag() is available only in Android L and later.
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
final String languageTag = (String) CompatUtils.invoke(subtype, null, GET_LANGUAGE_TAG);
|
||||||
|
if (!TextUtils.isEmpty(languageTag)) {
|
||||||
|
return Locale.forLanguageTag(languageTag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return LocaleUtils.constructLocaleFromString(subtype.getLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
@UsedForTesting
|
||||||
|
public static boolean isAsciiCapableWithAPI(final InputMethodSubtype subtype) {
|
||||||
|
return (Boolean)CompatUtils.invoke(subtype, false, METHOD_isAsciiCapable);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
public final class IntentCompatUtils {
|
||||||
|
// Note that Intent.ACTION_USER_INITIALIZE have been introduced in API level 17
|
||||||
|
// (Build.VERSION_CODE.JELLY_BEAN_MR1).
|
||||||
|
private static final String ACTION_USER_INITIALIZE =
|
||||||
|
(String)CompatUtils.getFieldValue(null /* receiver */, null /* defaultValue */,
|
||||||
|
CompatUtils.getField(Intent.class, "ACTION_USER_INITIALIZE"));
|
||||||
|
|
||||||
|
private IntentCompatUtils() {
|
||||||
|
// This utility class is not publicly instantiable.
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean is_ACTION_USER_INITIALIZE(final String action) {
|
||||||
|
return ACTION_USER_INITIALIZE != null && ACTION_USER_INITIALIZE.equals(action);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public final class LocaleListCompatUtils {
|
||||||
|
private static final Class CLASS_LocaleList = CompatUtils.getClass("android.os.LocaleList");
|
||||||
|
private static final Method METHOD_get =
|
||||||
|
CompatUtils.getMethod(CLASS_LocaleList, "get", int.class);
|
||||||
|
private static final Method METHOD_isEmpty =
|
||||||
|
CompatUtils.getMethod(CLASS_LocaleList, "isEmpty");
|
||||||
|
|
||||||
|
private LocaleListCompatUtils() {
|
||||||
|
// This utility class is not publicly instantiable.
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isEmpty(final Object localeList) {
|
||||||
|
return (Boolean) CompatUtils.invoke(localeList, Boolean.FALSE, METHOD_isEmpty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Locale get(final Object localeList, final int index) {
|
||||||
|
return (Locale) CompatUtils.invoke(localeList, null, METHOD_get, index);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,218 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.text.Spannable;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.style.LocaleSpan;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.annotations.UsedForTesting;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
@UsedForTesting
|
||||||
|
public final class LocaleSpanCompatUtils {
|
||||||
|
private static final String TAG = LocaleSpanCompatUtils.class.getSimpleName();
|
||||||
|
|
||||||
|
// Note that LocaleSpan(Locale locale) has been introduced in API level 17
|
||||||
|
// (Build.VERSION_CODE.JELLY_BEAN_MR1).
|
||||||
|
private static Class<?> getLocaleSpanClass() {
|
||||||
|
try {
|
||||||
|
return Class.forName("android.text.style.LocaleSpan");
|
||||||
|
} catch (ClassNotFoundException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private static final Class<?> LOCALE_SPAN_TYPE;
|
||||||
|
private static final Constructor<?> LOCALE_SPAN_CONSTRUCTOR;
|
||||||
|
private static final Method LOCALE_SPAN_GET_LOCALE;
|
||||||
|
static {
|
||||||
|
LOCALE_SPAN_TYPE = getLocaleSpanClass();
|
||||||
|
LOCALE_SPAN_CONSTRUCTOR = CompatUtils.getConstructor(LOCALE_SPAN_TYPE, Locale.class);
|
||||||
|
LOCALE_SPAN_GET_LOCALE = CompatUtils.getMethod(LOCALE_SPAN_TYPE, "getLocale");
|
||||||
|
}
|
||||||
|
|
||||||
|
@UsedForTesting
|
||||||
|
public static boolean isLocaleSpanAvailable() {
|
||||||
|
return (LOCALE_SPAN_CONSTRUCTOR != null && LOCALE_SPAN_GET_LOCALE != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UsedForTesting
|
||||||
|
public static Object newLocaleSpan(final Locale locale) {
|
||||||
|
return CompatUtils.newInstance(LOCALE_SPAN_CONSTRUCTOR, locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
@UsedForTesting
|
||||||
|
public static Locale getLocaleFromLocaleSpan(final Object localeSpan) {
|
||||||
|
return (Locale) CompatUtils.invoke(localeSpan, null, LOCALE_SPAN_GET_LOCALE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures that the specified range is covered with only one {@link LocaleSpan} with the given
|
||||||
|
* locale. If the region is already covered by one or more {@link LocaleSpan}, their ranges are
|
||||||
|
* updated so that each character has only one locale.
|
||||||
|
* @param spannable the spannable object to be updated.
|
||||||
|
* @param start the start index from which {@link LocaleSpan} is attached (inclusive).
|
||||||
|
* @param end the end index to which {@link LocaleSpan} is attached (exclusive).
|
||||||
|
* @param locale the locale to be attached to the specified range.
|
||||||
|
*/
|
||||||
|
@UsedForTesting
|
||||||
|
public static void updateLocaleSpan(final Spannable spannable, final int start,
|
||||||
|
final int end, final Locale locale) {
|
||||||
|
if (end < start) {
|
||||||
|
Log.e(TAG, "Invalid range: start=" + start + " end=" + end);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!isLocaleSpanAvailable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A brief summary of our strategy;
|
||||||
|
// 1. Enumerate all LocaleSpans between [start - 1, end + 1].
|
||||||
|
// 2. For each LocaleSpan S:
|
||||||
|
// - Update the range of S so as not to cover [start, end] if S doesn't have the
|
||||||
|
// expected locale.
|
||||||
|
// - Mark S as "to be merged" if S has the expected locale.
|
||||||
|
// 3. Merge all the LocaleSpans that are marked as "to be merged" into one LocaleSpan.
|
||||||
|
// If no appropriate span is found, create a new one with newLocaleSpan method.
|
||||||
|
final int searchStart = Math.max(start - 1, 0);
|
||||||
|
final int searchEnd = Math.min(end + 1, spannable.length());
|
||||||
|
// LocaleSpans found in the target range. See the step 1 in the above comment.
|
||||||
|
final Object[] existingLocaleSpans = spannable.getSpans(searchStart, searchEnd,
|
||||||
|
LOCALE_SPAN_TYPE);
|
||||||
|
// LocaleSpans that are marked as "to be merged". See the step 2 in the above comment.
|
||||||
|
final ArrayList<Object> existingLocaleSpansToBeMerged = new ArrayList<>();
|
||||||
|
boolean isStartExclusive = true;
|
||||||
|
boolean isEndExclusive = true;
|
||||||
|
int newStart = start;
|
||||||
|
int newEnd = end;
|
||||||
|
for (final Object existingLocaleSpan : existingLocaleSpans) {
|
||||||
|
final Locale attachedLocale = getLocaleFromLocaleSpan(existingLocaleSpan);
|
||||||
|
if (!locale.equals(attachedLocale)) {
|
||||||
|
// This LocaleSpan does not have the expected locale. Update its range if it has
|
||||||
|
// an intersection with the range [start, end] (the first case of the step 2 in the
|
||||||
|
// above comment).
|
||||||
|
removeLocaleSpanFromRange(existingLocaleSpan, spannable, start, end);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final int spanStart = spannable.getSpanStart(existingLocaleSpan);
|
||||||
|
final int spanEnd = spannable.getSpanEnd(existingLocaleSpan);
|
||||||
|
if (spanEnd < spanStart) {
|
||||||
|
Log.e(TAG, "Invalid span: spanStart=" + spanStart + " spanEnd=" + spanEnd);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (spanEnd < start || end < spanStart) {
|
||||||
|
// No intersection found.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here existingLocaleSpan has the expected locale and an intersection with the
|
||||||
|
// range [start, end] (the second case of the the step 2 in the above comment).
|
||||||
|
final int spanFlag = spannable.getSpanFlags(existingLocaleSpan);
|
||||||
|
if (spanStart < newStart) {
|
||||||
|
newStart = spanStart;
|
||||||
|
isStartExclusive = ((spanFlag & Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) ==
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
if (newEnd < spanEnd) {
|
||||||
|
newEnd = spanEnd;
|
||||||
|
isEndExclusive = ((spanFlag & Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) ==
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
existingLocaleSpansToBeMerged.add(existingLocaleSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
int originalLocaleSpanFlag = 0;
|
||||||
|
Object localeSpan = null;
|
||||||
|
if (existingLocaleSpansToBeMerged.isEmpty()) {
|
||||||
|
// If there is no LocaleSpan that is marked as to be merged, create a new one.
|
||||||
|
localeSpan = newLocaleSpan(locale);
|
||||||
|
} else {
|
||||||
|
// Reuse the first LocaleSpan to avoid unnecessary object instantiation.
|
||||||
|
localeSpan = existingLocaleSpansToBeMerged.get(0);
|
||||||
|
originalLocaleSpanFlag = spannable.getSpanFlags(localeSpan);
|
||||||
|
// No need to keep other instances.
|
||||||
|
for (int i = 1; i < existingLocaleSpansToBeMerged.size(); ++i) {
|
||||||
|
spannable.removeSpan(existingLocaleSpansToBeMerged.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final int localeSpanFlag = getSpanFlag(originalLocaleSpanFlag, isStartExclusive,
|
||||||
|
isEndExclusive);
|
||||||
|
spannable.setSpan(localeSpan, newStart, newEnd, localeSpanFlag);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void removeLocaleSpanFromRange(final Object localeSpan,
|
||||||
|
final Spannable spannable, final int removeStart, final int removeEnd) {
|
||||||
|
if (!isLocaleSpanAvailable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final int spanStart = spannable.getSpanStart(localeSpan);
|
||||||
|
final int spanEnd = spannable.getSpanEnd(localeSpan);
|
||||||
|
if (spanStart > spanEnd) {
|
||||||
|
Log.e(TAG, "Invalid span: spanStart=" + spanStart + " spanEnd=" + spanEnd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (spanEnd < removeStart) {
|
||||||
|
// spanStart < spanEnd < removeStart < removeEnd
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (removeEnd < spanStart) {
|
||||||
|
// spanStart < removeEnd < spanStart < spanEnd
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final int spanFlags = spannable.getSpanFlags(localeSpan);
|
||||||
|
if (spanStart < removeStart) {
|
||||||
|
if (removeEnd < spanEnd) {
|
||||||
|
// spanStart < removeStart < removeEnd < spanEnd
|
||||||
|
final Locale locale = getLocaleFromLocaleSpan(localeSpan);
|
||||||
|
spannable.setSpan(localeSpan, spanStart, removeStart, spanFlags);
|
||||||
|
final Object attionalLocaleSpan = newLocaleSpan(locale);
|
||||||
|
spannable.setSpan(attionalLocaleSpan, removeEnd, spanEnd, spanFlags);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// spanStart < removeStart < spanEnd <= removeEnd
|
||||||
|
spannable.setSpan(localeSpan, spanStart, removeStart, spanFlags);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (removeEnd < spanEnd) {
|
||||||
|
// removeStart <= spanStart < removeEnd < spanEnd
|
||||||
|
spannable.setSpan(localeSpan, removeEnd, spanEnd, spanFlags);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// removeStart <= spanStart < spanEnd < removeEnd
|
||||||
|
spannable.removeSpan(localeSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getSpanFlag(final int originalFlag,
|
||||||
|
final boolean isStartExclusive, final boolean isEndExclusive) {
|
||||||
|
return (originalFlag & ~Spanned.SPAN_POINT_MARK_MASK) |
|
||||||
|
getSpanPointMarkFlag(isStartExclusive, isEndExclusive);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getSpanPointMarkFlag(final boolean isStartExclusive,
|
||||||
|
final boolean isEndExclusive) {
|
||||||
|
if (isStartExclusive) {
|
||||||
|
return isEndExclusive ? Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
: Spanned.SPAN_EXCLUSIVE_INCLUSIVE;
|
||||||
|
}
|
||||||
|
return isEndExclusive ? Spanned.SPAN_INCLUSIVE_EXCLUSIVE
|
||||||
|
: Spanned.SPAN_INCLUSIVE_INCLUSIVE;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.os.Looper;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to call Looper#quitSafely, which was introduced in API
|
||||||
|
* level 18 (Build.VERSION_CODES.JELLY_BEAN_MR2).
|
||||||
|
*
|
||||||
|
* In unit tests, we create lots of instances of LatinIME, which means we need to clean up
|
||||||
|
* some Loopers lest we leak file descriptors. In normal use on a device though, this is never
|
||||||
|
* necessary (although it does not hurt).
|
||||||
|
*/
|
||||||
|
public final class LooperCompatUtils {
|
||||||
|
private static final Method METHOD_quitSafely = CompatUtils.getMethod(
|
||||||
|
Looper.class, "quitSafely");
|
||||||
|
|
||||||
|
public static void quitSafely(final Looper looper) {
|
||||||
|
if (null != METHOD_quitSafely) {
|
||||||
|
CompatUtils.invoke(looper, null /* default return value */, METHOD_quitSafely);
|
||||||
|
} else {
|
||||||
|
looper.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
public class NotificationCompatUtils {
|
||||||
|
// Note that TextInfo.getCharSequence() is supposed to be available in API level 21 and later.
|
||||||
|
private static final Method METHOD_setColor =
|
||||||
|
CompatUtils.getMethod(Notification.Builder.class, "setColor", int.class);
|
||||||
|
private static final Method METHOD_setVisibility =
|
||||||
|
CompatUtils.getMethod(Notification.Builder.class, "setVisibility", int.class);
|
||||||
|
private static final Method METHOD_setCategory =
|
||||||
|
CompatUtils.getMethod(Notification.Builder.class, "setCategory", String.class);
|
||||||
|
private static final Method METHOD_setPriority =
|
||||||
|
CompatUtils.getMethod(Notification.Builder.class, "setPriority", int.class);
|
||||||
|
private static final Method METHOD_build =
|
||||||
|
CompatUtils.getMethod(Notification.Builder.class, "build");
|
||||||
|
private static final Field FIELD_VISIBILITY_SECRET =
|
||||||
|
CompatUtils.getField(Notification.class, "VISIBILITY_SECRET");
|
||||||
|
private static final int VISIBILITY_SECRET = null == FIELD_VISIBILITY_SECRET ? 0
|
||||||
|
: (Integer) CompatUtils.getFieldValue(null /* receiver */, null /* defaultValue */,
|
||||||
|
FIELD_VISIBILITY_SECRET);
|
||||||
|
private static final Field FIELD_CATEGORY_RECOMMENDATION =
|
||||||
|
CompatUtils.getField(Notification.class, "CATEGORY_RECOMMENDATION");
|
||||||
|
private static final String CATEGORY_RECOMMENDATION = null == FIELD_CATEGORY_RECOMMENDATION ? ""
|
||||||
|
: (String) CompatUtils.getFieldValue(null /* receiver */, null /* defaultValue */,
|
||||||
|
FIELD_CATEGORY_RECOMMENDATION);
|
||||||
|
private static final Field FIELD_PRIORITY_LOW =
|
||||||
|
CompatUtils.getField(Notification.class, "PRIORITY_LOW");
|
||||||
|
private static final int PRIORITY_LOW = null == FIELD_PRIORITY_LOW ? 0
|
||||||
|
: (Integer) CompatUtils.getFieldValue(null /* receiver */, null /* defaultValue */,
|
||||||
|
FIELD_PRIORITY_LOW);
|
||||||
|
|
||||||
|
private NotificationCompatUtils() {
|
||||||
|
// This class is non-instantiable.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets the accent color
|
||||||
|
public static void setColor(final Notification.Builder builder, final int color) {
|
||||||
|
CompatUtils.invoke(builder, null, METHOD_setColor, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setVisibilityToSecret(final Notification.Builder builder) {
|
||||||
|
CompatUtils.invoke(builder, null, METHOD_setVisibility, VISIBILITY_SECRET);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setCategoryToRecommendation(final Notification.Builder builder) {
|
||||||
|
CompatUtils.invoke(builder, null, METHOD_setCategory, CATEGORY_RECOMMENDATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setPriorityToLow(final Notification.Builder builder) {
|
||||||
|
CompatUtils.invoke(builder, null, METHOD_setPriority, PRIORITY_LOW);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public static Notification build(final Notification.Builder builder) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
|
// #build was added in API level 16, JELLY_BEAN
|
||||||
|
return (Notification) CompatUtils.invoke(builder, null, METHOD_build);
|
||||||
|
}
|
||||||
|
// #getNotification was deprecated in API level 16, JELLY_BEAN
|
||||||
|
return builder.getNotification();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
|
||||||
|
public final class SettingsSecureCompatUtils {
|
||||||
|
// Note that Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD has been introduced
|
||||||
|
// in API level 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1).
|
||||||
|
private static final Field FIELD_ACCESSIBILITY_SPEAK_PASSWORD = CompatUtils.getField(
|
||||||
|
android.provider.Settings.Secure.class, "ACCESSIBILITY_SPEAK_PASSWORD");
|
||||||
|
|
||||||
|
private SettingsSecureCompatUtils() {
|
||||||
|
// This class is non-instantiable.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to speak passwords while in accessibility mode.
|
||||||
|
*/
|
||||||
|
public static final String ACCESSIBILITY_SPEAK_PASSWORD = (String) CompatUtils.getFieldValue(
|
||||||
|
null /* receiver */, null /* defaultValue */, FIELD_ACCESSIBILITY_SPEAK_PASSWORD);
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.text.Spannable;
|
||||||
|
import android.text.SpannableString;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.style.SuggestionSpan;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.annotations.UsedForTesting;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.SuggestedWords;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.define.DebugFlags;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
public final class SuggestionSpanUtils {
|
||||||
|
// Note that SuggestionSpan.FLAG_AUTO_CORRECTION has been introduced
|
||||||
|
// in API level 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1).
|
||||||
|
private static final Field FIELD_FLAG_AUTO_CORRECTION = CompatUtils.getField(
|
||||||
|
SuggestionSpan.class, "FLAG_AUTO_CORRECTION");
|
||||||
|
private static final Integer OBJ_FLAG_AUTO_CORRECTION = (Integer) CompatUtils.getFieldValue(
|
||||||
|
null /* receiver */, null /* defaultValue */, FIELD_FLAG_AUTO_CORRECTION);
|
||||||
|
|
||||||
|
static {
|
||||||
|
if (DebugFlags.DEBUG_ENABLED) {
|
||||||
|
if (OBJ_FLAG_AUTO_CORRECTION == null) {
|
||||||
|
throw new RuntimeException("Field is accidentially null.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SuggestionSpanUtils() {
|
||||||
|
// This utility class is not publicly instantiable.
|
||||||
|
}
|
||||||
|
|
||||||
|
@UsedForTesting
|
||||||
|
public static CharSequence getTextWithAutoCorrectionIndicatorUnderline(
|
||||||
|
final Context context, final String text, @Nonnull final Locale locale) {
|
||||||
|
if (TextUtils.isEmpty(text) || OBJ_FLAG_AUTO_CORRECTION == null) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
final Spannable spannable = new SpannableString(text);
|
||||||
|
final SuggestionSpan suggestionSpan = new SuggestionSpan(context, locale,
|
||||||
|
new String[] {} /* suggestions */, OBJ_FLAG_AUTO_CORRECTION, null);
|
||||||
|
spannable.setSpan(suggestionSpan, 0, text.length(),
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING);
|
||||||
|
return spannable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@UsedForTesting
|
||||||
|
public static CharSequence getTextWithSuggestionSpan(final Context context,
|
||||||
|
final String pickedWord, final SuggestedWords suggestedWords, final Locale locale) {
|
||||||
|
if (TextUtils.isEmpty(pickedWord) || suggestedWords.isEmpty()
|
||||||
|
|| suggestedWords.isPrediction() || suggestedWords.isPunctuationSuggestions()) {
|
||||||
|
return pickedWord;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ArrayList<String> suggestionsList = new ArrayList<>();
|
||||||
|
for (int i = 0; i < suggestedWords.size(); ++i) {
|
||||||
|
if (suggestionsList.size() >= SuggestionSpan.SUGGESTIONS_MAX_SIZE) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
final SuggestedWordInfo info = suggestedWords.getInfo(i);
|
||||||
|
if (info.isKindOf(SuggestedWordInfo.KIND_PREDICTION)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final String word = suggestedWords.getWord(i);
|
||||||
|
if (!TextUtils.equals(pickedWord, word)) {
|
||||||
|
suggestionsList.add(word.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final SuggestionSpan suggestionSpan = new SuggestionSpan(context, locale,
|
||||||
|
suggestionsList.toArray(new String[suggestionsList.size()]), 0 /* flags */, null);
|
||||||
|
final Spannable spannable = new SpannableString(pickedWord);
|
||||||
|
spannable.setSpan(suggestionSpan, 0, pickedWord.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
return spannable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns first {@link Locale} found in the given array of {@link SuggestionSpan}.
|
||||||
|
* @param suggestionSpans the array of {@link SuggestionSpan} to be examined.
|
||||||
|
* @return the first {@link Locale} found in {@code suggestionSpans}. {@code null} when not
|
||||||
|
* found.
|
||||||
|
*/
|
||||||
|
@UsedForTesting
|
||||||
|
@Nullable
|
||||||
|
public static Locale findFirstLocaleFromSuggestionSpans(
|
||||||
|
final SuggestionSpan[] suggestionSpans) {
|
||||||
|
for (final SuggestionSpan suggestionSpan : suggestionSpans) {
|
||||||
|
final String localeString = suggestionSpan.getLocale();
|
||||||
|
if (TextUtils.isEmpty(localeString)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return LocaleUtils.constructLocaleFromString(localeString);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.view.textservice.SuggestionsInfo;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
|
||||||
|
public final class SuggestionsInfoCompatUtils {
|
||||||
|
// Note that SuggestionsInfo.RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS has been introduced
|
||||||
|
// in API level 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1).
|
||||||
|
private static final Field FIELD_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS =
|
||||||
|
CompatUtils.getField(SuggestionsInfo.class, "RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS");
|
||||||
|
private static final Integer OBJ_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS =
|
||||||
|
(Integer) CompatUtils.getFieldValue(null /* receiver */, null /* defaultValue */,
|
||||||
|
FIELD_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS);
|
||||||
|
private static final int RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS =
|
||||||
|
OBJ_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS != null
|
||||||
|
? OBJ_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS : 0;
|
||||||
|
|
||||||
|
private SuggestionsInfoCompatUtils() {
|
||||||
|
// This utility class is not publicly instantiable.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the flag value of the attributes of the suggestions that can be obtained by
|
||||||
|
* {@link SuggestionsInfo#getSuggestionsAttributes()}: this tells that the text service thinks
|
||||||
|
* the result suggestions include highly recommended ones.
|
||||||
|
*/
|
||||||
|
public static int getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS() {
|
||||||
|
return RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.view.textservice.TextInfo;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.annotations.UsedForTesting;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
@UsedForTesting
|
||||||
|
public final class TextInfoCompatUtils {
|
||||||
|
// Note that TextInfo.getCharSequence() is supposed to be available in API level 21 and later.
|
||||||
|
private static final Method TEXT_INFO_GET_CHAR_SEQUENCE =
|
||||||
|
CompatUtils.getMethod(TextInfo.class, "getCharSequence");
|
||||||
|
private static final Constructor<?> TEXT_INFO_CONSTRUCTOR_FOR_CHAR_SEQUENCE =
|
||||||
|
CompatUtils.getConstructor(TextInfo.class, CharSequence.class, int.class, int.class,
|
||||||
|
int.class, int.class);
|
||||||
|
|
||||||
|
@UsedForTesting
|
||||||
|
public static boolean isCharSequenceSupported() {
|
||||||
|
return TEXT_INFO_GET_CHAR_SEQUENCE != null &&
|
||||||
|
TEXT_INFO_CONSTRUCTOR_FOR_CHAR_SEQUENCE != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@UsedForTesting
|
||||||
|
public static TextInfo newInstance(CharSequence charSequence, int start, int end, int cookie,
|
||||||
|
int sequenceNumber) {
|
||||||
|
if (TEXT_INFO_CONSTRUCTOR_FOR_CHAR_SEQUENCE != null) {
|
||||||
|
return (TextInfo) CompatUtils.newInstance(TEXT_INFO_CONSTRUCTOR_FOR_CHAR_SEQUENCE,
|
||||||
|
charSequence, start, end, cookie, sequenceNumber);
|
||||||
|
}
|
||||||
|
return new TextInfo(charSequence.subSequence(start, end).toString(), cookie,
|
||||||
|
sequenceNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the result of {@link TextInfo#getCharSequence()} when available. Otherwise returns
|
||||||
|
* the result of {@link TextInfo#getText()} as fall back.
|
||||||
|
* @param textInfo the instance for which {@link TextInfo#getCharSequence()} or
|
||||||
|
* {@link TextInfo#getText()} is called.
|
||||||
|
* @return the result of {@link TextInfo#getCharSequence()} when available. Otherwise returns
|
||||||
|
* the result of {@link TextInfo#getText()} as fall back. If {@code textInfo} is {@code null},
|
||||||
|
* returns {@code null}.
|
||||||
|
*/
|
||||||
|
@UsedForTesting
|
||||||
|
public static CharSequence getCharSequenceOrString(final TextInfo textInfo) {
|
||||||
|
final CharSequence defaultValue = (textInfo == null ? null : textInfo.getText());
|
||||||
|
return (CharSequence) CompatUtils.invoke(textInfo, defaultValue,
|
||||||
|
TEXT_INFO_GET_CHAR_SEQUENCE);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
public final class TextViewCompatUtils {
|
||||||
|
// Note that TextView.setCompoundDrawablesRelativeWithIntrinsicBounds(Drawable,Drawable,
|
||||||
|
// Drawable,Drawable) has been introduced in API level 17 (Build.VERSION_CODE.JELLY_BEAN_MR1).
|
||||||
|
private static final Method METHOD_setCompoundDrawablesRelativeWithIntrinsicBounds =
|
||||||
|
CompatUtils.getMethod(TextView.class, "setCompoundDrawablesRelativeWithIntrinsicBounds",
|
||||||
|
Drawable.class, Drawable.class, Drawable.class, Drawable.class);
|
||||||
|
|
||||||
|
private TextViewCompatUtils() {
|
||||||
|
// This utility class is not publicly instantiable.
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setCompoundDrawablesRelativeWithIntrinsicBounds(final TextView textView,
|
||||||
|
final Drawable start, final Drawable top, final Drawable end, final Drawable bottom) {
|
||||||
|
if (METHOD_setCompoundDrawablesRelativeWithIntrinsicBounds == null) {
|
||||||
|
textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CompatUtils.invoke(textView, null, METHOD_setCompoundDrawablesRelativeWithIntrinsicBounds,
|
||||||
|
start, top, end, bottom);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.provider.UserDictionary;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
public final class UserDictionaryCompatUtils {
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public static void addWord(final Context context, final String word,
|
||||||
|
final int freq, final String shortcut, final Locale locale) {
|
||||||
|
if (BuildCompatUtils.EFFECTIVE_SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
|
addWordWithShortcut(context, word, freq, shortcut, locale);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Fall back to the pre-JellyBean method.
|
||||||
|
final Locale currentLocale = context.getResources().getConfiguration().locale;
|
||||||
|
final int localeType = currentLocale.equals(locale)
|
||||||
|
? UserDictionary.Words.LOCALE_TYPE_CURRENT : UserDictionary.Words.LOCALE_TYPE_ALL;
|
||||||
|
UserDictionary.Words.addWord(context, word, freq, localeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// {@link UserDictionary.Words#addWord(Context,String,int,String,Locale)} was introduced
|
||||||
|
// in API level 16 (Build.VERSION_CODES.JELLY_BEAN).
|
||||||
|
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||||
|
private static void addWordWithShortcut(final Context context, final String word,
|
||||||
|
final int freq, final String shortcut, final Locale locale) {
|
||||||
|
UserDictionary.Words.addWord(context, word, freq, shortcut, locale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.UserManager;
|
||||||
|
import androidx.annotation.IntDef;
|
||||||
|
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A temporary solution until {@code UserManagerCompat.isUserUnlocked()} in the support-v4 library
|
||||||
|
* becomes publicly available.
|
||||||
|
*/
|
||||||
|
public final class UserManagerCompatUtils {
|
||||||
|
private static final Method METHOD_isUserUnlocked;
|
||||||
|
|
||||||
|
static {
|
||||||
|
// We do not try to search the method in Android M and prior.
|
||||||
|
if (BuildCompatUtils.EFFECTIVE_SDK_INT <= Build.VERSION_CODES.M) {
|
||||||
|
METHOD_isUserUnlocked = null;
|
||||||
|
} else {
|
||||||
|
METHOD_isUserUnlocked = CompatUtils.getMethod(UserManager.class, "isUserUnlocked");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserManagerCompatUtils() {
|
||||||
|
// This utility class is not publicly instantiable.
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final int LOCK_STATE_UNKNOWN = 0;
|
||||||
|
public static final int LOCK_STATE_UNLOCKED = 1;
|
||||||
|
public static final int LOCK_STATE_LOCKED = 2;
|
||||||
|
|
||||||
|
@Retention(SOURCE)
|
||||||
|
@IntDef({LOCK_STATE_UNKNOWN, LOCK_STATE_UNLOCKED, LOCK_STATE_LOCKED})
|
||||||
|
public @interface LockState {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the calling user is running in an "unlocked" state. A user is unlocked only after
|
||||||
|
* they've entered their credentials (such as a lock pattern or PIN), and credential-encrypted
|
||||||
|
* private app data storage is available.
|
||||||
|
* @param context context from which {@link UserManager} should be obtained.
|
||||||
|
* @return One of {@link LockState}.
|
||||||
|
*/
|
||||||
|
@LockState
|
||||||
|
public static int getUserLockState(final Context context) {
|
||||||
|
if (METHOD_isUserUnlocked == null) {
|
||||||
|
return LOCK_STATE_UNKNOWN;
|
||||||
|
}
|
||||||
|
final UserManager userManager = context.getSystemService(UserManager.class);
|
||||||
|
if (userManager == null) {
|
||||||
|
return LOCK_STATE_UNKNOWN;
|
||||||
|
}
|
||||||
|
final Boolean result =
|
||||||
|
(Boolean) CompatUtils.invoke(userManager, null, METHOD_isUserUnlocked);
|
||||||
|
if (result == null) {
|
||||||
|
return LOCK_STATE_UNKNOWN;
|
||||||
|
}
|
||||||
|
return result ? LOCK_STATE_UNLOCKED : LOCK_STATE_LOCKED;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
// TODO: Use {@link androidx.core.view.ViewCompat} instead of this utility class.
|
||||||
|
// Currently {@link #getPaddingEnd(View)} and {@link #setPaddingRelative(View,int,int,int,int)}
|
||||||
|
// are missing from android-support-v4 static library in KitKat SDK.
|
||||||
|
public final class ViewCompatUtils {
|
||||||
|
// Note that View.getPaddingEnd(), View.setPaddingRelative(int,int,int,int) have been
|
||||||
|
// introduced in API level 17 (Build.VERSION_CODE.JELLY_BEAN_MR1).
|
||||||
|
private static final Method METHOD_getPaddingEnd = CompatUtils.getMethod(
|
||||||
|
View.class, "getPaddingEnd");
|
||||||
|
private static final Method METHOD_setPaddingRelative = CompatUtils.getMethod(
|
||||||
|
View.class, "setPaddingRelative",
|
||||||
|
int.class, int.class, int.class, int.class);
|
||||||
|
// Note that View.setTextAlignment(int) has been introduced in API level 17.
|
||||||
|
private static final Method METHOD_setTextAlignment = CompatUtils.getMethod(
|
||||||
|
View.class, "setTextAlignment", int.class);
|
||||||
|
|
||||||
|
private ViewCompatUtils() {
|
||||||
|
// This utility class is not publicly instantiable.
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getPaddingEnd(final View view) {
|
||||||
|
if (METHOD_getPaddingEnd == null) {
|
||||||
|
return view.getPaddingRight();
|
||||||
|
}
|
||||||
|
return (Integer)CompatUtils.invoke(view, 0, METHOD_getPaddingEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setPaddingRelative(final View view, final int start, final int top,
|
||||||
|
final int end, final int bottom) {
|
||||||
|
if (METHOD_setPaddingRelative == null) {
|
||||||
|
view.setPadding(start, top, end, bottom);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CompatUtils.invoke(view, null, METHOD_setPaddingRelative, start, top, end, bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
// These TEXT_ALIGNMENT_* constants have been introduced in API 17.
|
||||||
|
public static final int TEXT_ALIGNMENT_INHERIT = 0;
|
||||||
|
public static final int TEXT_ALIGNMENT_GRAVITY = 1;
|
||||||
|
public static final int TEXT_ALIGNMENT_TEXT_START = 2;
|
||||||
|
public static final int TEXT_ALIGNMENT_TEXT_END = 3;
|
||||||
|
public static final int TEXT_ALIGNMENT_CENTER = 4;
|
||||||
|
public static final int TEXT_ALIGNMENT_VIEW_START = 5;
|
||||||
|
public static final int TEXT_ALIGNMENT_VIEW_END = 6;
|
||||||
|
|
||||||
|
public static void setTextAlignment(final View view, final int textAlignment) {
|
||||||
|
CompatUtils.invoke(view, null, METHOD_setTextAlignment, textAlignment);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.inputmethodservice.InputMethodService;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
public class ViewOutlineProviderCompatUtils {
|
||||||
|
private ViewOutlineProviderCompatUtils() {
|
||||||
|
// This utility class is not publicly instantiable.
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface InsetsUpdater {
|
||||||
|
public void setInsets(final InputMethodService.Insets insets);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final InsetsUpdater EMPTY_INSETS_UPDATER = new InsetsUpdater() {
|
||||||
|
@Override
|
||||||
|
public void setInsets(final InputMethodService.Insets insets) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static InsetsUpdater setInsetsOutlineProvider(final View view) {
|
||||||
|
if (BuildCompatUtils.EFFECTIVE_SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
return EMPTY_INSETS_UPDATER;
|
||||||
|
}
|
||||||
|
return ViewOutlineProviderCompatUtilsLXX.setInsetsOutlineProvider(view);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.compat;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.graphics.Outline;
|
||||||
|
import android.inputmethodservice.InputMethodService;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewOutlineProvider;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.compat.ViewOutlineProviderCompatUtils.InsetsUpdater;
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||||
|
class ViewOutlineProviderCompatUtilsLXX {
|
||||||
|
private ViewOutlineProviderCompatUtilsLXX() {
|
||||||
|
// This utility class is not publicly instantiable.
|
||||||
|
}
|
||||||
|
|
||||||
|
static InsetsUpdater setInsetsOutlineProvider(final View view) {
|
||||||
|
final InsetsOutlineProvider provider = new InsetsOutlineProvider(view);
|
||||||
|
view.setOutlineProvider(provider);
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class InsetsOutlineProvider extends ViewOutlineProvider
|
||||||
|
implements InsetsUpdater {
|
||||||
|
private final View mView;
|
||||||
|
private static final int NO_DATA = -1;
|
||||||
|
private int mLastVisibleTopInsets = NO_DATA;
|
||||||
|
|
||||||
|
public InsetsOutlineProvider(final View view) {
|
||||||
|
mView = view;
|
||||||
|
view.setOutlineProvider(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setInsets(final InputMethodService.Insets insets) {
|
||||||
|
final int visibleTopInsets = insets.visibleTopInsets;
|
||||||
|
if (mLastVisibleTopInsets != visibleTopInsets) {
|
||||||
|
mLastVisibleTopInsets = visibleTopInsets;
|
||||||
|
mView.invalidateOutline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void getOutline(final View view, final Outline outline) {
|
||||||
|
if (mLastVisibleTopInsets == NO_DATA) {
|
||||||
|
// Call default implementation.
|
||||||
|
ViewOutlineProvider.BACKGROUND.getOutline(view, outline);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// TODO: Revisit this when floating/resize keyboard is supported.
|
||||||
|
outline.setRect(
|
||||||
|
view.getLeft(), mLastVisibleTopInsets, view.getRight(), view.getBottom());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,625 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
import android.app.DownloadManager.Request;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.latin.BinaryDictionaryFileDumper;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.R;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.utils.ApplicationUtils;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.utils.DebugLogUtils;
|
||||||
|
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.Queue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Object representing an upgrade from one state to another.
|
||||||
|
*
|
||||||
|
* This implementation basically encapsulates a list of Runnable objects. In the future
|
||||||
|
* it may manage dependencies between them. Concretely, it does not use Runnable because the
|
||||||
|
* actions need an argument.
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
|
||||||
|
The state of a word list follows the following scheme.
|
||||||
|
|
||||||
|
| ^
|
||||||
|
MakeAvailable |
|
||||||
|
| .------------Forget--------'
|
||||||
|
V |
|
||||||
|
STATUS_AVAILABLE <-------------------------.
|
||||||
|
| |
|
||||||
|
StartDownloadAction FinishDeleteAction
|
||||||
|
| |
|
||||||
|
V |
|
||||||
|
STATUS_DOWNLOADING EnableAction-- STATUS_DELETING
|
||||||
|
| | ^
|
||||||
|
InstallAfterDownloadAction | |
|
||||||
|
| .---------------' StartDeleteAction
|
||||||
|
| | |
|
||||||
|
V V |
|
||||||
|
STATUS_INSTALLED <--EnableAction-- STATUS_DISABLED
|
||||||
|
--DisableAction-->
|
||||||
|
|
||||||
|
It may also be possible that DisableAction or StartDeleteAction or
|
||||||
|
DownloadAction run when the file is still downloading. This cancels
|
||||||
|
the download and returns to STATUS_AVAILABLE.
|
||||||
|
Also, an UpdateDataAction may apply in any state. It does not affect
|
||||||
|
the state in any way (nor type, local filename, id or version) but
|
||||||
|
may update other attributes like description or remote filename.
|
||||||
|
|
||||||
|
Forget is an DB maintenance action that removes the entry if it is not installed or disabled.
|
||||||
|
This happens when the word list information disappeared from the server, or when a new version
|
||||||
|
is available and we should forget about the old one.
|
||||||
|
*/
|
||||||
|
public final class ActionBatch {
|
||||||
|
/**
|
||||||
|
* A piece of update.
|
||||||
|
*
|
||||||
|
* Action is basically like a Runnable that takes an argument.
|
||||||
|
*/
|
||||||
|
public interface Action {
|
||||||
|
/**
|
||||||
|
* Execute this action NOW.
|
||||||
|
* @param context the context to get system services, resources, databases
|
||||||
|
*/
|
||||||
|
void execute(final Context context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An action that starts downloading an available word list.
|
||||||
|
*/
|
||||||
|
public static final class StartDownloadAction implements Action {
|
||||||
|
static final String TAG = "DictionaryProvider:" + StartDownloadAction.class.getSimpleName();
|
||||||
|
|
||||||
|
private final String mClientId;
|
||||||
|
// The data to download. May not be null.
|
||||||
|
final WordListMetadata mWordList;
|
||||||
|
public StartDownloadAction(final String clientId, final WordListMetadata wordList) {
|
||||||
|
DebugLogUtils.l("New download action for client ", clientId, " : ", wordList);
|
||||||
|
mClientId = clientId;
|
||||||
|
mWordList = wordList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(final Context context) {
|
||||||
|
if (null == mWordList) { // This should never happen
|
||||||
|
Log.e(TAG, "UpdateAction with a null parameter!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DebugLogUtils.l("Downloading word list");
|
||||||
|
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
|
||||||
|
final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
|
||||||
|
mWordList.mId, mWordList.mVersion);
|
||||||
|
final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
|
||||||
|
final DownloadManagerWrapper manager = new DownloadManagerWrapper(context);
|
||||||
|
if (MetadataDbHelper.STATUS_DOWNLOADING == status) {
|
||||||
|
// The word list is still downloading. Cancel the download and revert the
|
||||||
|
// word list status to "available".
|
||||||
|
manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN));
|
||||||
|
MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion);
|
||||||
|
} else if (MetadataDbHelper.STATUS_AVAILABLE != status
|
||||||
|
&& MetadataDbHelper.STATUS_RETRYING != status) {
|
||||||
|
// Should never happen
|
||||||
|
Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' : " + status
|
||||||
|
+ " for an upgrade action. Fall back to download.");
|
||||||
|
}
|
||||||
|
// Download it.
|
||||||
|
DebugLogUtils.l("Upgrade word list, downloading", mWordList.mRemoteFilename);
|
||||||
|
|
||||||
|
// This is an upgraded word list: we should download it.
|
||||||
|
// Adding a disambiguator to circumvent a bug in older versions of DownloadManager.
|
||||||
|
// DownloadManager also stupidly cuts the extension to replace with its own that it
|
||||||
|
// gets from the content-type. We need to circumvent this.
|
||||||
|
final String disambiguator = "#" + System.currentTimeMillis()
|
||||||
|
+ ApplicationUtils.getVersionName(context) + ".dict";
|
||||||
|
final Uri uri = Uri.parse(mWordList.mRemoteFilename + disambiguator);
|
||||||
|
final Request request = new Request(uri);
|
||||||
|
|
||||||
|
final Resources res = context.getResources();
|
||||||
|
request.setAllowedNetworkTypes(Request.NETWORK_WIFI | Request.NETWORK_MOBILE);
|
||||||
|
request.setTitle(mWordList.mDescription);
|
||||||
|
request.setNotificationVisibility(Request.VISIBILITY_HIDDEN);
|
||||||
|
request.setVisibleInDownloadsUi(
|
||||||
|
res.getBoolean(R.bool.dict_downloads_visible_in_download_UI));
|
||||||
|
|
||||||
|
final long downloadId = UpdateHandler.registerDownloadRequest(manager, request, db,
|
||||||
|
mWordList.mId, mWordList.mVersion);
|
||||||
|
Log.i(TAG, String.format("Starting the dictionary download with version:"
|
||||||
|
+ " %d and Url: %s", mWordList.mVersion, uri));
|
||||||
|
DebugLogUtils.l("Starting download of", uri, "with id", downloadId);
|
||||||
|
PrivateLog.log("Starting download of " + uri + ", id : " + downloadId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An action that updates the database to reflect the status of a newly installed word list.
|
||||||
|
*/
|
||||||
|
public static final class InstallAfterDownloadAction implements Action {
|
||||||
|
static final String TAG = "DictionaryProvider:"
|
||||||
|
+ InstallAfterDownloadAction.class.getSimpleName();
|
||||||
|
private final String mClientId;
|
||||||
|
// The state to upgrade from. May not be null.
|
||||||
|
final ContentValues mWordListValues;
|
||||||
|
|
||||||
|
public InstallAfterDownloadAction(final String clientId,
|
||||||
|
final ContentValues wordListValues) {
|
||||||
|
DebugLogUtils.l("New InstallAfterDownloadAction for client ", clientId, " : ",
|
||||||
|
wordListValues);
|
||||||
|
mClientId = clientId;
|
||||||
|
mWordListValues = wordListValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(final Context context) {
|
||||||
|
if (null == mWordListValues) {
|
||||||
|
Log.e(TAG, "InstallAfterDownloadAction with a null parameter!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final int status = mWordListValues.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
|
||||||
|
if (MetadataDbHelper.STATUS_DOWNLOADING != status) {
|
||||||
|
final String id = mWordListValues.getAsString(MetadataDbHelper.WORDLISTID_COLUMN);
|
||||||
|
Log.e(TAG, "Unexpected state of the word list '" + id + "' : " + status
|
||||||
|
+ " for an InstallAfterDownload action. Bailing out.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugLogUtils.l("Setting word list as installed");
|
||||||
|
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
|
||||||
|
MetadataDbHelper.markEntryAsFinishedDownloadingAndInstalled(db, mWordListValues);
|
||||||
|
|
||||||
|
// Install the downloaded file by un-compressing and moving it to the staging
|
||||||
|
// directory. Ideally, we should do this before updating the DB, but the
|
||||||
|
// installDictToStagingFromContentProvider() relies on the db being updated.
|
||||||
|
final String localeString = mWordListValues.getAsString(MetadataDbHelper.LOCALE_COLUMN);
|
||||||
|
BinaryDictionaryFileDumper.installDictToStagingFromContentProvider(
|
||||||
|
LocaleUtils.constructLocaleFromString(localeString), context, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An action that enables an existing word list.
|
||||||
|
*/
|
||||||
|
public static final class EnableAction implements Action {
|
||||||
|
static final String TAG = "DictionaryProvider:" + EnableAction.class.getSimpleName();
|
||||||
|
private final String mClientId;
|
||||||
|
// The state to upgrade from. May not be null.
|
||||||
|
final WordListMetadata mWordList;
|
||||||
|
|
||||||
|
public EnableAction(final String clientId, final WordListMetadata wordList) {
|
||||||
|
DebugLogUtils.l("New EnableAction for client ", clientId, " : ", wordList);
|
||||||
|
mClientId = clientId;
|
||||||
|
mWordList = wordList;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(final Context context) {
|
||||||
|
if (null == mWordList) {
|
||||||
|
Log.e(TAG, "EnableAction with a null parameter!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DebugLogUtils.l("Enabling word list");
|
||||||
|
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
|
||||||
|
final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
|
||||||
|
mWordList.mId, mWordList.mVersion);
|
||||||
|
final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
|
||||||
|
if (MetadataDbHelper.STATUS_DISABLED != status
|
||||||
|
&& MetadataDbHelper.STATUS_DELETING != status) {
|
||||||
|
Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + " : " + status
|
||||||
|
+ " for an enable action. Cancelling");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
MetadataDbHelper.markEntryAsEnabled(db, mWordList.mId, mWordList.mVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An action that disables a word list.
|
||||||
|
*/
|
||||||
|
public static final class DisableAction implements Action {
|
||||||
|
static final String TAG = "DictionaryProvider:" + DisableAction.class.getSimpleName();
|
||||||
|
private final String mClientId;
|
||||||
|
// The word list to disable. May not be null.
|
||||||
|
final WordListMetadata mWordList;
|
||||||
|
public DisableAction(final String clientId, final WordListMetadata wordlist) {
|
||||||
|
DebugLogUtils.l("New Disable action for client ", clientId, " : ", wordlist);
|
||||||
|
mClientId = clientId;
|
||||||
|
mWordList = wordlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(final Context context) {
|
||||||
|
if (null == mWordList) { // This should never happen
|
||||||
|
Log.e(TAG, "DisableAction with a null word list!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DebugLogUtils.l("Disabling word list : " + mWordList);
|
||||||
|
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
|
||||||
|
final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
|
||||||
|
mWordList.mId, mWordList.mVersion);
|
||||||
|
final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
|
||||||
|
if (MetadataDbHelper.STATUS_INSTALLED == status) {
|
||||||
|
// Disabling an installed word list
|
||||||
|
MetadataDbHelper.markEntryAsDisabled(db, mWordList.mId, mWordList.mVersion);
|
||||||
|
} else {
|
||||||
|
if (MetadataDbHelper.STATUS_DOWNLOADING != status) {
|
||||||
|
Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' : "
|
||||||
|
+ status + " for a disable action. Fall back to marking as available.");
|
||||||
|
}
|
||||||
|
// The word list is still downloading. Cancel the download and revert the
|
||||||
|
// word list status to "available".
|
||||||
|
final DownloadManagerWrapper manager = new DownloadManagerWrapper(context);
|
||||||
|
manager.remove(values.getAsLong(MetadataDbHelper.PENDINGID_COLUMN));
|
||||||
|
MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An action that makes a word list available.
|
||||||
|
*/
|
||||||
|
public static final class MakeAvailableAction implements Action {
|
||||||
|
static final String TAG = "DictionaryProvider:" + MakeAvailableAction.class.getSimpleName();
|
||||||
|
private final String mClientId;
|
||||||
|
// The word list to make available. May not be null.
|
||||||
|
final WordListMetadata mWordList;
|
||||||
|
public MakeAvailableAction(final String clientId, final WordListMetadata wordlist) {
|
||||||
|
DebugLogUtils.l("New MakeAvailable action", clientId, " : ", wordlist);
|
||||||
|
mClientId = clientId;
|
||||||
|
mWordList = wordlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(final Context context) {
|
||||||
|
if (null == mWordList) { // This should never happen
|
||||||
|
Log.e(TAG, "MakeAvailableAction with a null word list!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
|
||||||
|
if (null != MetadataDbHelper.getContentValuesByWordListId(db,
|
||||||
|
mWordList.mId, mWordList.mVersion)) {
|
||||||
|
Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' "
|
||||||
|
+ " for a makeavailable action. Marking as available anyway.");
|
||||||
|
}
|
||||||
|
DebugLogUtils.l("Making word list available : " + mWordList);
|
||||||
|
// If mLocalFilename is null, then it's a remote file that hasn't been downloaded
|
||||||
|
// yet, so we set the local filename to the empty string.
|
||||||
|
final ContentValues values = MetadataDbHelper.makeContentValues(0,
|
||||||
|
MetadataDbHelper.TYPE_BULK, MetadataDbHelper.STATUS_AVAILABLE,
|
||||||
|
mWordList.mId, mWordList.mLocale, mWordList.mDescription,
|
||||||
|
null == mWordList.mLocalFilename ? "" : mWordList.mLocalFilename,
|
||||||
|
mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mRawChecksum,
|
||||||
|
mWordList.mChecksum, mWordList.mRetryCount, mWordList.mFileSize,
|
||||||
|
mWordList.mVersion, mWordList.mFormatVersion);
|
||||||
|
PrivateLog.log("Insert 'available' record for " + mWordList.mDescription
|
||||||
|
+ " and locale " + mWordList.mLocale);
|
||||||
|
db.insert(MetadataDbHelper.METADATA_TABLE_NAME, null, values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An action that marks a word list as pre-installed.
|
||||||
|
*
|
||||||
|
* This is almost the same as MakeAvailableAction, as it only inserts a line with parameters
|
||||||
|
* received from outside.
|
||||||
|
* Unlike MakeAvailableAction, the parameters are not received from a downloaded metadata file
|
||||||
|
* but from the client directly; it marks a word list as being "installed" and not "available".
|
||||||
|
* It also explicitly sets the filename to the empty string, so that we don't try to open
|
||||||
|
* it on our side.
|
||||||
|
*/
|
||||||
|
public static final class MarkPreInstalledAction implements Action {
|
||||||
|
static final String TAG = "DictionaryProvider:"
|
||||||
|
+ MarkPreInstalledAction.class.getSimpleName();
|
||||||
|
private final String mClientId;
|
||||||
|
// The word list to mark pre-installed. May not be null.
|
||||||
|
final WordListMetadata mWordList;
|
||||||
|
public MarkPreInstalledAction(final String clientId, final WordListMetadata wordlist) {
|
||||||
|
DebugLogUtils.l("New MarkPreInstalled action", clientId, " : ", wordlist);
|
||||||
|
mClientId = clientId;
|
||||||
|
mWordList = wordlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(final Context context) {
|
||||||
|
if (null == mWordList) { // This should never happen
|
||||||
|
Log.e(TAG, "MarkPreInstalledAction with a null word list!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
|
||||||
|
if (null != MetadataDbHelper.getContentValuesByWordListId(db,
|
||||||
|
mWordList.mId, mWordList.mVersion)) {
|
||||||
|
Log.e(TAG, "Unexpected state of the word list '" + mWordList.mId + "' "
|
||||||
|
+ " for a markpreinstalled action. Marking as preinstalled anyway.");
|
||||||
|
}
|
||||||
|
DebugLogUtils.l("Marking word list preinstalled : " + mWordList);
|
||||||
|
// This word list is pre-installed : we don't have its file. We should reset
|
||||||
|
// the local file name to the empty string so that we don't try to open it
|
||||||
|
// accidentally. The remote filename may be set by the application if it so wishes.
|
||||||
|
final ContentValues values = MetadataDbHelper.makeContentValues(0,
|
||||||
|
MetadataDbHelper.TYPE_BULK, MetadataDbHelper.STATUS_INSTALLED,
|
||||||
|
mWordList.mId, mWordList.mLocale, mWordList.mDescription,
|
||||||
|
TextUtils.isEmpty(mWordList.mLocalFilename) ? "" : mWordList.mLocalFilename,
|
||||||
|
mWordList.mRemoteFilename, mWordList.mLastUpdate,
|
||||||
|
mWordList.mRawChecksum, mWordList.mChecksum, mWordList.mRetryCount,
|
||||||
|
mWordList.mFileSize, mWordList.mVersion, mWordList.mFormatVersion);
|
||||||
|
PrivateLog.log("Insert 'preinstalled' record for " + mWordList.mDescription
|
||||||
|
+ " and locale " + mWordList.mLocale);
|
||||||
|
db.insert(MetadataDbHelper.METADATA_TABLE_NAME, null, values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An action that updates information about a word list - description, locale etc
|
||||||
|
*/
|
||||||
|
public static final class UpdateDataAction implements Action {
|
||||||
|
static final String TAG = "DictionaryProvider:" + UpdateDataAction.class.getSimpleName();
|
||||||
|
private final String mClientId;
|
||||||
|
final WordListMetadata mWordList;
|
||||||
|
public UpdateDataAction(final String clientId, final WordListMetadata wordlist) {
|
||||||
|
DebugLogUtils.l("New UpdateData action for client ", clientId, " : ", wordlist);
|
||||||
|
mClientId = clientId;
|
||||||
|
mWordList = wordlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(final Context context) {
|
||||||
|
if (null == mWordList) { // This should never happen
|
||||||
|
Log.e(TAG, "UpdateDataAction with a null word list!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
|
||||||
|
ContentValues oldValues = MetadataDbHelper.getContentValuesByWordListId(db,
|
||||||
|
mWordList.mId, mWordList.mVersion);
|
||||||
|
if (null == oldValues) {
|
||||||
|
Log.e(TAG, "Trying to update data about a non-existing word list. Bailing out.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DebugLogUtils.l("Updating data about a word list : " + mWordList);
|
||||||
|
final ContentValues values = MetadataDbHelper.makeContentValues(
|
||||||
|
oldValues.getAsInteger(MetadataDbHelper.PENDINGID_COLUMN),
|
||||||
|
oldValues.getAsInteger(MetadataDbHelper.TYPE_COLUMN),
|
||||||
|
oldValues.getAsInteger(MetadataDbHelper.STATUS_COLUMN),
|
||||||
|
mWordList.mId, mWordList.mLocale, mWordList.mDescription,
|
||||||
|
oldValues.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN),
|
||||||
|
mWordList.mRemoteFilename, mWordList.mLastUpdate, mWordList.mRawChecksum,
|
||||||
|
mWordList.mChecksum, mWordList.mRetryCount, mWordList.mFileSize,
|
||||||
|
mWordList.mVersion, mWordList.mFormatVersion);
|
||||||
|
PrivateLog.log("Updating record for " + mWordList.mDescription
|
||||||
|
+ " and locale " + mWordList.mLocale);
|
||||||
|
db.update(MetadataDbHelper.METADATA_TABLE_NAME, values,
|
||||||
|
MetadataDbHelper.WORDLISTID_COLUMN + " = ? AND "
|
||||||
|
+ MetadataDbHelper.VERSION_COLUMN + " = ?",
|
||||||
|
new String[] { mWordList.mId, Integer.toString(mWordList.mVersion) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An action that deletes the metadata about a word list if possible.
|
||||||
|
*
|
||||||
|
* This is triggered when a specific word list disappeared from the server, or when a fresher
|
||||||
|
* word list is available and the old one was not installed.
|
||||||
|
* If the word list has not been installed, it's possible to delete its associated metadata.
|
||||||
|
* Otherwise, the settings are retained so that the user can still administrate it.
|
||||||
|
*/
|
||||||
|
public static final class ForgetAction implements Action {
|
||||||
|
static final String TAG = "DictionaryProvider:" + ForgetAction.class.getSimpleName();
|
||||||
|
private final String mClientId;
|
||||||
|
// The word list to remove. May not be null.
|
||||||
|
final WordListMetadata mWordList;
|
||||||
|
final boolean mHasNewerVersion;
|
||||||
|
public ForgetAction(final String clientId, final WordListMetadata wordlist,
|
||||||
|
final boolean hasNewerVersion) {
|
||||||
|
DebugLogUtils.l("New TryRemove action for client ", clientId, " : ", wordlist);
|
||||||
|
mClientId = clientId;
|
||||||
|
mWordList = wordlist;
|
||||||
|
mHasNewerVersion = hasNewerVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(final Context context) {
|
||||||
|
if (null == mWordList) { // This should never happen
|
||||||
|
Log.e(TAG, "TryRemoveAction with a null word list!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DebugLogUtils.l("Trying to remove word list : " + mWordList);
|
||||||
|
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
|
||||||
|
final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
|
||||||
|
mWordList.mId, mWordList.mVersion);
|
||||||
|
if (null == values) {
|
||||||
|
Log.e(TAG, "Trying to update the metadata of a non-existing wordlist. Cancelling.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
|
||||||
|
if (mHasNewerVersion && MetadataDbHelper.STATUS_AVAILABLE != status) {
|
||||||
|
// If we have a newer version of this word list, we should be here ONLY if it was
|
||||||
|
// not installed - else we should be upgrading it.
|
||||||
|
Log.e(TAG, "Unexpected status for forgetting a word list info : " + status
|
||||||
|
+ ", removing URL to prevent re-download");
|
||||||
|
}
|
||||||
|
if (MetadataDbHelper.STATUS_INSTALLED == status
|
||||||
|
|| MetadataDbHelper.STATUS_DISABLED == status
|
||||||
|
|| MetadataDbHelper.STATUS_DELETING == status) {
|
||||||
|
// If it is installed or disabled, we need to mark it as deleted so that LatinIME
|
||||||
|
// will remove it next time it enquires for dictionaries.
|
||||||
|
// If it is deleting and we don't have a new version, then we have to wait until
|
||||||
|
// LatinIME actually has deleted it before we can remove its metadata.
|
||||||
|
// In both cases, remove the URI from the database since it is not supposed to
|
||||||
|
// be accessible any more.
|
||||||
|
values.put(MetadataDbHelper.REMOTE_FILENAME_COLUMN, "");
|
||||||
|
values.put(MetadataDbHelper.STATUS_COLUMN, MetadataDbHelper.STATUS_DELETING);
|
||||||
|
db.update(MetadataDbHelper.METADATA_TABLE_NAME, values,
|
||||||
|
MetadataDbHelper.WORDLISTID_COLUMN + " = ? AND "
|
||||||
|
+ MetadataDbHelper.VERSION_COLUMN + " = ?",
|
||||||
|
new String[] { mWordList.mId, Integer.toString(mWordList.mVersion) });
|
||||||
|
} else {
|
||||||
|
// If it's AVAILABLE or DOWNLOADING or even UNKNOWN, delete the entry.
|
||||||
|
db.delete(MetadataDbHelper.METADATA_TABLE_NAME,
|
||||||
|
MetadataDbHelper.WORDLISTID_COLUMN + " = ? AND "
|
||||||
|
+ MetadataDbHelper.VERSION_COLUMN + " = ?",
|
||||||
|
new String[] { mWordList.mId, Integer.toString(mWordList.mVersion) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An action that sets the word list for deletion as soon as possible.
|
||||||
|
*
|
||||||
|
* This is triggered when the user requests deletion of a word list. This will mark it as
|
||||||
|
* deleted in the database, and fire an intent for Android Keyboard to take notice and
|
||||||
|
* reload its dictionaries right away if it is up. If it is not up now, then it will
|
||||||
|
* delete the actual file the next time it gets up.
|
||||||
|
* A file marked as deleted causes the content provider to supply a zero-sized file to
|
||||||
|
* Android Keyboard, which will overwrite any existing file and provide no words for this
|
||||||
|
* word list. This is not exactly a "deletion", since there is an actual file which takes up
|
||||||
|
* a few bytes on the disk, but this allows to override a default dictionary with an empty
|
||||||
|
* dictionary. This way, there is no need for the user to make a distinction between
|
||||||
|
* dictionaries installed by default and add-on dictionaries.
|
||||||
|
*/
|
||||||
|
public static final class StartDeleteAction implements Action {
|
||||||
|
static final String TAG = "DictionaryProvider:" + StartDeleteAction.class.getSimpleName();
|
||||||
|
private final String mClientId;
|
||||||
|
// The word list to delete. May not be null.
|
||||||
|
final WordListMetadata mWordList;
|
||||||
|
public StartDeleteAction(final String clientId, final WordListMetadata wordlist) {
|
||||||
|
DebugLogUtils.l("New StartDelete action for client ", clientId, " : ", wordlist);
|
||||||
|
mClientId = clientId;
|
||||||
|
mWordList = wordlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(final Context context) {
|
||||||
|
if (null == mWordList) { // This should never happen
|
||||||
|
Log.e(TAG, "StartDeleteAction with a null word list!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DebugLogUtils.l("Trying to delete word list : " + mWordList);
|
||||||
|
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
|
||||||
|
final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
|
||||||
|
mWordList.mId, mWordList.mVersion);
|
||||||
|
if (null == values) {
|
||||||
|
Log.e(TAG, "Trying to set a non-existing wordlist for removal. Cancelling.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
|
||||||
|
if (MetadataDbHelper.STATUS_DISABLED != status) {
|
||||||
|
Log.e(TAG, "Unexpected status for deleting a word list info : " + status);
|
||||||
|
}
|
||||||
|
MetadataDbHelper.markEntryAsDeleting(db, mWordList.mId, mWordList.mVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An action that validates a word list as deleted.
|
||||||
|
*
|
||||||
|
* This will restore the word list as available if it still is, or remove the entry if
|
||||||
|
* it is not any more.
|
||||||
|
*/
|
||||||
|
public static final class FinishDeleteAction implements Action {
|
||||||
|
static final String TAG = "DictionaryProvider:" + FinishDeleteAction.class.getSimpleName();
|
||||||
|
private final String mClientId;
|
||||||
|
// The word list to delete. May not be null.
|
||||||
|
final WordListMetadata mWordList;
|
||||||
|
public FinishDeleteAction(final String clientId, final WordListMetadata wordlist) {
|
||||||
|
DebugLogUtils.l("New FinishDelete action for client", clientId, " : ", wordlist);
|
||||||
|
mClientId = clientId;
|
||||||
|
mWordList = wordlist;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(final Context context) {
|
||||||
|
if (null == mWordList) { // This should never happen
|
||||||
|
Log.e(TAG, "FinishDeleteAction with a null word list!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DebugLogUtils.l("Trying to delete word list : " + mWordList);
|
||||||
|
final SQLiteDatabase db = MetadataDbHelper.getDb(context, mClientId);
|
||||||
|
final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db,
|
||||||
|
mWordList.mId, mWordList.mVersion);
|
||||||
|
if (null == values) {
|
||||||
|
Log.e(TAG, "Trying to set a non-existing wordlist for removal. Cancelling.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
|
||||||
|
if (MetadataDbHelper.STATUS_DELETING != status) {
|
||||||
|
Log.e(TAG, "Unexpected status for finish-deleting a word list info : " + status);
|
||||||
|
}
|
||||||
|
final String remoteFilename =
|
||||||
|
values.getAsString(MetadataDbHelper.REMOTE_FILENAME_COLUMN);
|
||||||
|
// If there isn't a remote filename any more, then we don't know where to get the file
|
||||||
|
// from any more, so we remove the entry entirely. As a matter of fact, if the file was
|
||||||
|
// marked DELETING but disappeared from the metadata on the server, it ended up
|
||||||
|
// this way.
|
||||||
|
if (TextUtils.isEmpty(remoteFilename)) {
|
||||||
|
db.delete(MetadataDbHelper.METADATA_TABLE_NAME,
|
||||||
|
MetadataDbHelper.WORDLISTID_COLUMN + " = ? AND "
|
||||||
|
+ MetadataDbHelper.VERSION_COLUMN + " = ?",
|
||||||
|
new String[] { mWordList.mId, Integer.toString(mWordList.mVersion) });
|
||||||
|
} else {
|
||||||
|
MetadataDbHelper.markEntryAsAvailable(db, mWordList.mId, mWordList.mVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An action batch consists of an ordered queue of Actions that can execute.
|
||||||
|
private final Queue<Action> mActions;
|
||||||
|
|
||||||
|
public ActionBatch() {
|
||||||
|
mActions = new LinkedList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(final Action a) {
|
||||||
|
mActions.add(a);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append all the actions of another action batch.
|
||||||
|
* @param that the upgrade to merge into this one.
|
||||||
|
*/
|
||||||
|
public void append(final ActionBatch that) {
|
||||||
|
for (final Action a : that.mActions) {
|
||||||
|
add(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute this batch.
|
||||||
|
*
|
||||||
|
* @param context the context for getting resources, databases, system services.
|
||||||
|
* @param reporter a Reporter to send errors to.
|
||||||
|
*/
|
||||||
|
public void execute(final Context context, final ProblemReporter reporter) {
|
||||||
|
DebugLogUtils.l("Executing a batch of actions");
|
||||||
|
Queue<Action> remainingActions = mActions;
|
||||||
|
while (!remainingActions.isEmpty()) {
|
||||||
|
final Action a = remainingActions.poll();
|
||||||
|
try {
|
||||||
|
a.execute(context);
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (null != reporter)
|
||||||
|
reporter.report(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immutable class to hold the address of an asset.
|
||||||
|
* As opposed to a normal file, an asset is usually represented as a contiguous byte array in
|
||||||
|
* the package file. Open it correctly thus requires the name of the package it is in, but
|
||||||
|
* also the offset in the file and the length of this data. This class encapsulates these three.
|
||||||
|
*/
|
||||||
|
final class AssetFileAddress {
|
||||||
|
public final String mFilename;
|
||||||
|
public final long mOffset;
|
||||||
|
public final long mLength;
|
||||||
|
|
||||||
|
public AssetFileAddress(final String filename, final long offset, final long length) {
|
||||||
|
mFilename = filename;
|
||||||
|
mOffset = offset;
|
||||||
|
mLength = length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes an AssetFileAddress. This may return null.
|
||||||
|
*
|
||||||
|
* @param filename the filename.
|
||||||
|
* @return the address, or null if the file does not exist or the parameters are not valid.
|
||||||
|
*/
|
||||||
|
public static AssetFileAddress makeFromFileName(final String filename) {
|
||||||
|
if (null == filename) return null;
|
||||||
|
final File f = new File(filename);
|
||||||
|
if (!f.isFile()) return null;
|
||||||
|
return new AssetFileAddress(filename, 0l, f.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes an AssetFileAddress. This may return null.
|
||||||
|
*
|
||||||
|
* @param filename the filename.
|
||||||
|
* @param offset the offset.
|
||||||
|
* @param length the length.
|
||||||
|
* @return the address, or null if the file does not exist or the parameters are not valid.
|
||||||
|
*/
|
||||||
|
public static AssetFileAddress makeFromFileNameAndOffset(final String filename,
|
||||||
|
final long offset, final long length) {
|
||||||
|
if (null == filename) return null;
|
||||||
|
final File f = new File(filename);
|
||||||
|
if (!f.isFile()) return null;
|
||||||
|
return new AssetFileAddress(filename, offset, length);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception thrown when the metadata for the dictionary does not comply to a known format.
|
||||||
|
*/
|
||||||
|
public final class BadFormatException extends Exception {
|
||||||
|
public BadFormatException() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public BadFormatException(final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,170 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy
|
||||||
|
* of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.AnimatorListenerAdapter;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewPropertyAnimator;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.latin.R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A view that handles buttons inside it according to a status.
|
||||||
|
*/
|
||||||
|
public class ButtonSwitcher extends FrameLayout {
|
||||||
|
public static final int NOT_INITIALIZED = -1;
|
||||||
|
public static final int STATUS_NO_BUTTON = 0;
|
||||||
|
public static final int STATUS_INSTALL = 1;
|
||||||
|
public static final int STATUS_CANCEL = 2;
|
||||||
|
public static final int STATUS_DELETE = 3;
|
||||||
|
// One of the above
|
||||||
|
private int mStatus = NOT_INITIALIZED;
|
||||||
|
private int mAnimateToStatus = NOT_INITIALIZED;
|
||||||
|
|
||||||
|
// Animation directions
|
||||||
|
public static final int ANIMATION_IN = 1;
|
||||||
|
public static final int ANIMATION_OUT = 2;
|
||||||
|
|
||||||
|
private Button mInstallButton;
|
||||||
|
private Button mCancelButton;
|
||||||
|
private Button mDeleteButton;
|
||||||
|
private DictionaryListInterfaceState mInterfaceState;
|
||||||
|
private OnClickListener mOnClickListener;
|
||||||
|
|
||||||
|
public ButtonSwitcher(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ButtonSwitcher(Context context, AttributeSet attrs, int defStyle) {
|
||||||
|
super(context, attrs, defStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset(final DictionaryListInterfaceState interfaceState) {
|
||||||
|
mStatus = NOT_INITIALIZED;
|
||||||
|
mAnimateToStatus = NOT_INITIALIZED;
|
||||||
|
mInterfaceState = interfaceState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onLayout(final boolean changed, final int left, final int top, final int right,
|
||||||
|
final int bottom) {
|
||||||
|
super.onLayout(changed, left, top, right, bottom);
|
||||||
|
mInstallButton = (Button)findViewById(R.id.dict_install_button);
|
||||||
|
mCancelButton = (Button)findViewById(R.id.dict_cancel_button);
|
||||||
|
mDeleteButton = (Button)findViewById(R.id.dict_delete_button);
|
||||||
|
setInternalOnClickListener(mOnClickListener);
|
||||||
|
setButtonPositionWithoutAnimation(mStatus);
|
||||||
|
if (mAnimateToStatus != NOT_INITIALIZED) {
|
||||||
|
// We have been asked to animate before we were ready, so we took a note of it.
|
||||||
|
// We are now ready: launch the animation.
|
||||||
|
animateButtonPosition(mStatus, mAnimateToStatus);
|
||||||
|
mStatus = mAnimateToStatus;
|
||||||
|
mAnimateToStatus = NOT_INITIALIZED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button getButton(final int status) {
|
||||||
|
switch(status) {
|
||||||
|
case STATUS_INSTALL:
|
||||||
|
return mInstallButton;
|
||||||
|
case STATUS_CANCEL:
|
||||||
|
return mCancelButton;
|
||||||
|
case STATUS_DELETE:
|
||||||
|
return mDeleteButton;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStatusAndUpdateVisuals(final int status) {
|
||||||
|
if (mStatus == NOT_INITIALIZED) {
|
||||||
|
setButtonPositionWithoutAnimation(status);
|
||||||
|
mStatus = status;
|
||||||
|
} else {
|
||||||
|
if (null == mInstallButton) {
|
||||||
|
// We may come here before we have been layout. In this case we don't know our
|
||||||
|
// size yet so we can't start animations so we need to remember what animation to
|
||||||
|
// start once layout has gone through.
|
||||||
|
mAnimateToStatus = status;
|
||||||
|
} else {
|
||||||
|
animateButtonPosition(mStatus, status);
|
||||||
|
mStatus = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setButtonPositionWithoutAnimation(final int status) {
|
||||||
|
// This may be called by setStatus() before the layout has come yet.
|
||||||
|
if (null == mInstallButton) return;
|
||||||
|
final int width = getWidth();
|
||||||
|
// Set to out of the screen if that's not the currently displayed status
|
||||||
|
mInstallButton.setTranslationX(STATUS_INSTALL == status ? 0 : width);
|
||||||
|
mCancelButton.setTranslationX(STATUS_CANCEL == status ? 0 : width);
|
||||||
|
mDeleteButton.setTranslationX(STATUS_DELETE == status ? 0 : width);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The helper method for {@link AnimatorListenerAdapter}.
|
||||||
|
void animateButtonIfStatusIsEqual(final View newButton, final int newStatus) {
|
||||||
|
if (newStatus != mStatus) return;
|
||||||
|
animateButton(newButton, ANIMATION_IN);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void animateButtonPosition(final int oldStatus, final int newStatus) {
|
||||||
|
final View oldButton = getButton(oldStatus);
|
||||||
|
final View newButton = getButton(newStatus);
|
||||||
|
if (null != oldButton && null != newButton) {
|
||||||
|
// Transition between two buttons : animate out, then in
|
||||||
|
animateButton(oldButton, ANIMATION_OUT).setListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(final Animator animation) {
|
||||||
|
animateButtonIfStatusIsEqual(newButton, newStatus);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (null != oldButton) {
|
||||||
|
animateButton(oldButton, ANIMATION_OUT);
|
||||||
|
} else if (null != newButton) {
|
||||||
|
animateButton(newButton, ANIMATION_IN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setInternalOnClickListener(final OnClickListener listener) {
|
||||||
|
mOnClickListener = listener;
|
||||||
|
if (null != mInstallButton) {
|
||||||
|
// Already laid out : do it now
|
||||||
|
mInstallButton.setOnClickListener(mOnClickListener);
|
||||||
|
mCancelButton.setOnClickListener(mOnClickListener);
|
||||||
|
mDeleteButton.setOnClickListener(mOnClickListener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ViewPropertyAnimator animateButton(final View button, final int direction) {
|
||||||
|
final float outerX = getWidth();
|
||||||
|
final float innerX = button.getX() - button.getTranslationX();
|
||||||
|
mInterfaceState.removeFromCache((View)getParent());
|
||||||
|
if (ANIMATION_IN == direction) {
|
||||||
|
button.setClickable(true);
|
||||||
|
return button.animate().translationX(0);
|
||||||
|
}
|
||||||
|
button.setClickable(false);
|
||||||
|
return button.animate().translationX(outerX - innerX);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy
|
||||||
|
* of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
public final class CommonPreferences {
|
||||||
|
private static final String COMMON_PREFERENCES_NAME = "LatinImeDictPrefs";
|
||||||
|
|
||||||
|
public static SharedPreferences getCommonPreferences(final Context context) {
|
||||||
|
return context.getSharedPreferences(COMMON_PREFERENCES_NAME, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void enable(final SharedPreferences pref, final String id) {
|
||||||
|
final SharedPreferences.Editor editor = pref.edit();
|
||||||
|
editor.putBoolean(id, true);
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void disable(final SharedPreferences pref, final String id) {
|
||||||
|
final SharedPreferences.Editor editor = pref.edit();
|
||||||
|
editor.putBoolean(id, false);
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
import android.app.DownloadManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Struct class to encapsulate the result of a completed download.
|
||||||
|
*/
|
||||||
|
public class CompletedDownloadInfo {
|
||||||
|
final String mUri;
|
||||||
|
final long mDownloadId;
|
||||||
|
final int mStatus;
|
||||||
|
public CompletedDownloadInfo(final String uri, final long downloadId, final int status) {
|
||||||
|
mUri = uri;
|
||||||
|
mDownloadId = downloadId;
|
||||||
|
mStatus = status;
|
||||||
|
}
|
||||||
|
public boolean wasSuccessful() {
|
||||||
|
return DownloadManager.STATUS_SUCCESSFUL == mStatus;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,173 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy
|
||||||
|
* of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
import android.app.DownloadManager;
|
||||||
|
import android.app.DownloadManager.Query;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
|
||||||
|
public class DictionaryDownloadProgressBar extends ProgressBar {
|
||||||
|
private static final String TAG = DictionaryDownloadProgressBar.class.getSimpleName();
|
||||||
|
private static final int NOT_A_DOWNLOADMANAGER_PENDING_ID = 0;
|
||||||
|
|
||||||
|
private String mClientId;
|
||||||
|
private String mWordlistId;
|
||||||
|
private boolean mIsCurrentlyAttachedToWindow = false;
|
||||||
|
private Thread mReporterThread = null;
|
||||||
|
|
||||||
|
public DictionaryDownloadProgressBar(final Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DictionaryDownloadProgressBar(final Context context, final AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIds(final String clientId, final String wordlistId) {
|
||||||
|
mClientId = clientId;
|
||||||
|
mWordlistId = wordlistId;
|
||||||
|
}
|
||||||
|
|
||||||
|
static private int getDownloadManagerPendingIdFromWordlistId(final Context context,
|
||||||
|
final String clientId, final String wordlistId) {
|
||||||
|
final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId);
|
||||||
|
final ContentValues wordlistValues =
|
||||||
|
MetadataDbHelper.getContentValuesOfLatestAvailableWordlistById(db, wordlistId);
|
||||||
|
if (null == wordlistValues) {
|
||||||
|
// We don't know anything about a word list with this id. Bug? This should never
|
||||||
|
// happen, but still return to prevent a crash.
|
||||||
|
Log.e(TAG, "Unexpected word list ID: " + wordlistId);
|
||||||
|
return NOT_A_DOWNLOADMANAGER_PENDING_ID;
|
||||||
|
}
|
||||||
|
return wordlistValues.getAsInteger(MetadataDbHelper.PENDINGID_COLUMN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This method will stop any running updater thread for this progress bar and create and run
|
||||||
|
* a new one only if the progress bar is visible.
|
||||||
|
* Hence, as a result of calling this method, the progress bar will have an updater thread
|
||||||
|
* running if and only if the progress bar is visible.
|
||||||
|
*/
|
||||||
|
private void updateReporterThreadRunningStatusAccordingToVisibility() {
|
||||||
|
if (null != mReporterThread) mReporterThread.interrupt();
|
||||||
|
if (mIsCurrentlyAttachedToWindow && View.VISIBLE == getVisibility()) {
|
||||||
|
final int downloadManagerPendingId =
|
||||||
|
getDownloadManagerPendingIdFromWordlistId(getContext(), mClientId, mWordlistId);
|
||||||
|
if (NOT_A_DOWNLOADMANAGER_PENDING_ID == downloadManagerPendingId) {
|
||||||
|
// Can't get the ID. This is never supposed to happen, but still clear the updater
|
||||||
|
// thread and return to avoid a crash.
|
||||||
|
mReporterThread = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final UpdaterThread updaterThread =
|
||||||
|
new UpdaterThread(getContext(), downloadManagerPendingId);
|
||||||
|
updaterThread.start();
|
||||||
|
mReporterThread = updaterThread;
|
||||||
|
} else {
|
||||||
|
// We're not going to restart the thread anyway, so we may as well garbage collect it.
|
||||||
|
mReporterThread = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onAttachedToWindow() {
|
||||||
|
mIsCurrentlyAttachedToWindow = true;
|
||||||
|
updateReporterThreadRunningStatusAccordingToVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDetachedFromWindow() {
|
||||||
|
super.onDetachedFromWindow();
|
||||||
|
mIsCurrentlyAttachedToWindow = false;
|
||||||
|
updateReporterThreadRunningStatusAccordingToVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class UpdaterThread extends Thread {
|
||||||
|
private final static int REPORT_PERIOD = 150; // how often to report progress, in ms
|
||||||
|
final DownloadManagerWrapper mDownloadManagerWrapper;
|
||||||
|
final int mId;
|
||||||
|
public UpdaterThread(final Context context, final int id) {
|
||||||
|
super();
|
||||||
|
mDownloadManagerWrapper = new DownloadManagerWrapper(context);
|
||||||
|
mId = id;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
final UpdateHelper updateHelper = new UpdateHelper();
|
||||||
|
final Query query = new Query().setFilterById(mId);
|
||||||
|
setIndeterminate(true);
|
||||||
|
while (!isInterrupted()) {
|
||||||
|
final Cursor cursor = mDownloadManagerWrapper.query(query);
|
||||||
|
if (null == cursor) {
|
||||||
|
// Can't contact DownloadManager: this should never happen.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (cursor.moveToNext()) {
|
||||||
|
final int columnBytesDownloadedSoFar = cursor.getColumnIndex(
|
||||||
|
DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR);
|
||||||
|
final int bytesDownloadedSoFar =
|
||||||
|
cursor.getInt(columnBytesDownloadedSoFar);
|
||||||
|
updateHelper.setProgressFromAnotherThread(bytesDownloadedSoFar);
|
||||||
|
} else {
|
||||||
|
// Download has finished and DownloadManager has already been asked to
|
||||||
|
// clean up the db entry.
|
||||||
|
updateHelper.setProgressFromAnotherThread(getMax());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
Thread.sleep(REPORT_PERIOD);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Do nothing and terminate normally.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UpdateHelper implements Runnable {
|
||||||
|
private int mProgress;
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
setIndeterminate(false);
|
||||||
|
setProgress(mProgress);
|
||||||
|
}
|
||||||
|
public void setProgressFromAnotherThread(final int progress) {
|
||||||
|
if (mProgress != progress) {
|
||||||
|
mProgress = progress;
|
||||||
|
// For some unknown reason, setProgress just does not work from a separate
|
||||||
|
// thread, although the code in ProgressBar looks like it should. Thus, we
|
||||||
|
// resort to a runnable posted to the handler of the view.
|
||||||
|
final Handler handler = getHandler();
|
||||||
|
// It's possible to come here before this view has been laid out. If so,
|
||||||
|
// just ignore the call - it will be updated again later.
|
||||||
|
if (null == handler) return;
|
||||||
|
handler.post(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy
|
||||||
|
* of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to maintain the interface state of word list preferences.
|
||||||
|
*
|
||||||
|
* This is necessary because the views are created on-demand by calling code. There are many
|
||||||
|
* situations where views are renewed with little relation with user interaction. For example,
|
||||||
|
* when scrolling, the view is reused so it doesn't keep its state, which means we need to keep
|
||||||
|
* it separately. Also whenever the underlying dictionary list undergoes a change (for example,
|
||||||
|
* update the metadata, or finish downloading) the whole list has to be thrown out and recreated
|
||||||
|
* in case some dictionaries appeared, disappeared, changed states etc.
|
||||||
|
*/
|
||||||
|
public class DictionaryListInterfaceState {
|
||||||
|
static class State {
|
||||||
|
public boolean mOpen = false;
|
||||||
|
public int mStatus = MetadataDbHelper.STATUS_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HashMap<String, State> mWordlistToState = new HashMap<>();
|
||||||
|
private ArrayList<View> mViewCache = new ArrayList<>();
|
||||||
|
|
||||||
|
public boolean isOpen(final String wordlistId) {
|
||||||
|
final State state = mWordlistToState.get(wordlistId);
|
||||||
|
if (null == state) return false;
|
||||||
|
return state.mOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getStatus(final String wordlistId) {
|
||||||
|
final State state = mWordlistToState.get(wordlistId);
|
||||||
|
if (null == state) return MetadataDbHelper.STATUS_UNKNOWN;
|
||||||
|
return state.mStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOpen(final String wordlistId, final int status) {
|
||||||
|
final State newState;
|
||||||
|
final State state = mWordlistToState.get(wordlistId);
|
||||||
|
newState = null == state ? new State() : state;
|
||||||
|
newState.mOpen = true;
|
||||||
|
newState.mStatus = status;
|
||||||
|
mWordlistToState.put(wordlistId, newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void closeAll() {
|
||||||
|
for (final State state : mWordlistToState.values()) {
|
||||||
|
state.mOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public View findFirstOrphanedView() {
|
||||||
|
for (final View v : mViewCache) {
|
||||||
|
if (null == v.getParent()) return v;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public View addToCacheAndReturnView(final View view) {
|
||||||
|
mViewCache.add(view);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeFromCache(final View view) {
|
||||||
|
mViewCache.remove(view);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class to group constants for dictionary pack usage.
|
||||||
|
*
|
||||||
|
* This class only defines constants. It should not make any references to outside code as far as
|
||||||
|
* possible, as it's used to separate cleanly the keyboard code from the dictionary pack code; this
|
||||||
|
* is needed in particular to cleanly compile regression tests.
|
||||||
|
*/
|
||||||
|
public class DictionaryPackConstants {
|
||||||
|
/**
|
||||||
|
* The root domain for the dictionary pack, upon which authorities and actions will append
|
||||||
|
* their own distinctive strings.
|
||||||
|
*/
|
||||||
|
private static final String DICTIONARY_DOMAIN = "org.dslul.openboard.inputmethod.dictionarypack.aosp";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authority for the ContentProvider protocol.
|
||||||
|
*/
|
||||||
|
// TODO: find some way to factorize this string with the one in the resources
|
||||||
|
public static final String AUTHORITY = DICTIONARY_DOMAIN;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The action of the intent for publishing that new dictionary data is available.
|
||||||
|
*/
|
||||||
|
// TODO: make this different across different packages. A suggested course of action is
|
||||||
|
// to use the package name inside this string.
|
||||||
|
// NOTE: The appended string should be uppercase like all other actions, but it's not for
|
||||||
|
// historical reasons.
|
||||||
|
public static final String NEW_DICTIONARY_INTENT_ACTION = DICTIONARY_DOMAIN + ".newdict";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The action of the intent sent by the dictionary pack to ask for a client to make
|
||||||
|
* itself known. This is used when the settings activity is brought up for a client the
|
||||||
|
* dictionary pack does not know about.
|
||||||
|
*/
|
||||||
|
public static final String UNKNOWN_DICTIONARY_PROVIDER_CLIENT = DICTIONARY_DOMAIN
|
||||||
|
+ ".UNKNOWN_CLIENT";
|
||||||
|
|
||||||
|
// In the above intents, the name of the string extra that contains the name of the client
|
||||||
|
// we want information about.
|
||||||
|
public static final String DICTIONARY_PROVIDER_CLIENT_EXTRA = "client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The action of the intent to tell the dictionary provider to update now.
|
||||||
|
*/
|
||||||
|
public static final String UPDATE_NOW_INTENT_ACTION = DICTIONARY_DOMAIN
|
||||||
|
+ ".UPDATE_NOW";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The intent action to inform the dictionary provider to initialize the db
|
||||||
|
* and update now.
|
||||||
|
*/
|
||||||
|
public static final String INIT_AND_UPDATE_NOW_INTENT_ACTION = DICTIONARY_DOMAIN
|
||||||
|
+ ".INIT_AND_UPDATE_NOW";
|
||||||
|
}
|
|
@ -0,0 +1,541 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
import android.content.ContentProvider;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.UriMatcher;
|
||||||
|
import android.content.res.AssetFileDescriptor;
|
||||||
|
import android.database.AbstractCursor;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDatabase;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.latin.R;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.utils.DebugLogUtils;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provider for dictionaries.
|
||||||
|
*
|
||||||
|
* This class is a ContentProvider exposing all available dictionary data as managed by
|
||||||
|
* the dictionary pack.
|
||||||
|
*/
|
||||||
|
public final class DictionaryProvider extends ContentProvider {
|
||||||
|
private static final String TAG = DictionaryProvider.class.getSimpleName();
|
||||||
|
public static final boolean DEBUG = false;
|
||||||
|
|
||||||
|
public static final Uri CONTENT_URI =
|
||||||
|
Uri.parse(ContentResolver.SCHEME_CONTENT + "://" + DictionaryPackConstants.AUTHORITY);
|
||||||
|
private static final String QUERY_PARAMETER_MAY_PROMPT_USER = "mayPrompt";
|
||||||
|
private static final String QUERY_PARAMETER_TRUE = "true";
|
||||||
|
private static final String QUERY_PARAMETER_DELETE_RESULT = "result";
|
||||||
|
private static final String QUERY_PARAMETER_FAILURE = "failure";
|
||||||
|
public static final String QUERY_PARAMETER_PROTOCOL_VERSION = "protocol";
|
||||||
|
private static final int NO_MATCH = 0;
|
||||||
|
private static final int DICTIONARY_V1_WHOLE_LIST = 1;
|
||||||
|
private static final int DICTIONARY_V1_DICT_INFO = 2;
|
||||||
|
private static final int DICTIONARY_V2_METADATA = 3;
|
||||||
|
private static final int DICTIONARY_V2_WHOLE_LIST = 4;
|
||||||
|
private static final int DICTIONARY_V2_DICT_INFO = 5;
|
||||||
|
private static final int DICTIONARY_V2_DATAFILE = 6;
|
||||||
|
private static final UriMatcher sUriMatcherV1 = new UriMatcher(NO_MATCH);
|
||||||
|
private static final UriMatcher sUriMatcherV2 = new UriMatcher(NO_MATCH);
|
||||||
|
static
|
||||||
|
{
|
||||||
|
sUriMatcherV1.addURI(DictionaryPackConstants.AUTHORITY, "list", DICTIONARY_V1_WHOLE_LIST);
|
||||||
|
sUriMatcherV1.addURI(DictionaryPackConstants.AUTHORITY, "*", DICTIONARY_V1_DICT_INFO);
|
||||||
|
sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/metadata",
|
||||||
|
DICTIONARY_V2_METADATA);
|
||||||
|
sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/list", DICTIONARY_V2_WHOLE_LIST);
|
||||||
|
sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/dict/*",
|
||||||
|
DICTIONARY_V2_DICT_INFO);
|
||||||
|
sUriMatcherV2.addURI(DictionaryPackConstants.AUTHORITY, "*/datafile/*",
|
||||||
|
DICTIONARY_V2_DATAFILE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MIME types for dictionary and dictionary list, as required by ContentProvider contract.
|
||||||
|
public static final String DICT_LIST_MIME_TYPE =
|
||||||
|
"vnd.android.cursor.item/vnd.google.dictionarylist";
|
||||||
|
public static final String DICT_DATAFILE_MIME_TYPE =
|
||||||
|
"vnd.android.cursor.item/vnd.google.dictionary";
|
||||||
|
|
||||||
|
public static final String ID_CATEGORY_SEPARATOR = ":";
|
||||||
|
|
||||||
|
private static final class WordListInfo {
|
||||||
|
public final String mId;
|
||||||
|
public final String mLocale;
|
||||||
|
public final String mRawChecksum;
|
||||||
|
public final int mMatchLevel;
|
||||||
|
public WordListInfo(final String id, final String locale, final String rawChecksum,
|
||||||
|
final int matchLevel) {
|
||||||
|
mId = id;
|
||||||
|
mLocale = locale;
|
||||||
|
mRawChecksum = rawChecksum;
|
||||||
|
mMatchLevel = matchLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cursor for returning a list of file ids from a List of strings.
|
||||||
|
*
|
||||||
|
* This simulates only the necessary methods. It has no error handling to speak of,
|
||||||
|
* and does not support everything a database does, only a few select necessary methods.
|
||||||
|
*/
|
||||||
|
private static final class ResourcePathCursor extends AbstractCursor {
|
||||||
|
|
||||||
|
// Column names for the cursor returned by this content provider.
|
||||||
|
static private final String[] columnNames = { MetadataDbHelper.WORDLISTID_COLUMN,
|
||||||
|
MetadataDbHelper.LOCALE_COLUMN, MetadataDbHelper.RAW_CHECKSUM_COLUMN };
|
||||||
|
|
||||||
|
// The list of word lists served by this provider that match the client request.
|
||||||
|
final WordListInfo[] mWordLists;
|
||||||
|
// Note : the cursor also uses mPos, which is defined in AbstractCursor.
|
||||||
|
|
||||||
|
public ResourcePathCursor(final Collection<WordListInfo> wordLists) {
|
||||||
|
// Allocating a 0-size WordListInfo here allows the toArray() method
|
||||||
|
// to ensure we have a strongly-typed array. It's thrown out. That's
|
||||||
|
// what the documentation of #toArray says to do in order to get a
|
||||||
|
// new strongly typed array of the correct size.
|
||||||
|
mWordLists = wordLists.toArray(new WordListInfo[0]);
|
||||||
|
mPos = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getColumnNames() {
|
||||||
|
return columnNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCount() {
|
||||||
|
return mWordLists.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public double getDouble(int column) { return 0; }
|
||||||
|
@Override public float getFloat(int column) { return 0; }
|
||||||
|
@Override public int getInt(int column) { return 0; }
|
||||||
|
@Override public short getShort(int column) { return 0; }
|
||||||
|
@Override public long getLong(int column) { return 0; }
|
||||||
|
|
||||||
|
@Override public String getString(final int column) {
|
||||||
|
switch (column) {
|
||||||
|
case 0: return mWordLists[mPos].mId;
|
||||||
|
case 1: return mWordLists[mPos].mLocale;
|
||||||
|
case 2: return mWordLists[mPos].mRawChecksum;
|
||||||
|
default : return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isNull(final int column) {
|
||||||
|
if (mPos >= mWordLists.length) return true;
|
||||||
|
return column != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreate() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int matchUri(final Uri uri) {
|
||||||
|
int protocolVersion = 1;
|
||||||
|
final String protocolVersionArg = uri.getQueryParameter(QUERY_PARAMETER_PROTOCOL_VERSION);
|
||||||
|
if ("2".equals(protocolVersionArg)) protocolVersion = 2;
|
||||||
|
switch (protocolVersion) {
|
||||||
|
case 1: return sUriMatcherV1.match(uri);
|
||||||
|
case 2: return sUriMatcherV2.match(uri);
|
||||||
|
default: return NO_MATCH;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getClientId(final Uri uri) {
|
||||||
|
int protocolVersion = 1;
|
||||||
|
final String protocolVersionArg = uri.getQueryParameter(QUERY_PARAMETER_PROTOCOL_VERSION);
|
||||||
|
if ("2".equals(protocolVersionArg)) protocolVersion = 2;
|
||||||
|
switch (protocolVersion) {
|
||||||
|
case 1: return null; // In protocol 1, the client ID is always null.
|
||||||
|
case 2: return uri.getPathSegments().get(0);
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the MIME type of the content associated with an Uri
|
||||||
|
*
|
||||||
|
* @see android.content.ContentProvider#getType(android.net.Uri)
|
||||||
|
*
|
||||||
|
* @param uri the URI of the content the type of which should be returned.
|
||||||
|
* @return the MIME type, or null if the URL is not recognized.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public String getType(final Uri uri) {
|
||||||
|
PrivateLog.log("Asked for type of : " + uri);
|
||||||
|
final int match = matchUri(uri);
|
||||||
|
switch (match) {
|
||||||
|
case NO_MATCH: return null;
|
||||||
|
case DICTIONARY_V1_WHOLE_LIST:
|
||||||
|
case DICTIONARY_V1_DICT_INFO:
|
||||||
|
case DICTIONARY_V2_WHOLE_LIST:
|
||||||
|
case DICTIONARY_V2_DICT_INFO: return DICT_LIST_MIME_TYPE;
|
||||||
|
case DICTIONARY_V2_DATAFILE: return DICT_DATAFILE_MIME_TYPE;
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query the provider for dictionary files.
|
||||||
|
*
|
||||||
|
* This version dispatches the query according to the protocol version found in the
|
||||||
|
* ?protocol= query parameter. If absent or not well-formed, it defaults to 1.
|
||||||
|
* @see android.content.ContentProvider#query(Uri, String[], String, String[], String)
|
||||||
|
*
|
||||||
|
* @param uri a content uri (see sUriMatcherV{1,2} at the top of this file for format)
|
||||||
|
* @param projection ignored. All columns are always returned.
|
||||||
|
* @param selection ignored.
|
||||||
|
* @param selectionArgs ignored.
|
||||||
|
* @param sortOrder ignored. The results are always returned in no particular order.
|
||||||
|
* @return a cursor matching the uri, or null if the URI was not recognized.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Cursor query(final Uri uri, final String[] projection, final String selection,
|
||||||
|
final String[] selectionArgs, final String sortOrder) {
|
||||||
|
DebugLogUtils.l("Uri =", uri);
|
||||||
|
PrivateLog.log("Query : " + uri);
|
||||||
|
final String clientId = getClientId(uri);
|
||||||
|
final int match = matchUri(uri);
|
||||||
|
switch (match) {
|
||||||
|
case DICTIONARY_V1_WHOLE_LIST:
|
||||||
|
case DICTIONARY_V2_WHOLE_LIST:
|
||||||
|
final Cursor c = MetadataDbHelper.queryDictionaries(getContext(), clientId);
|
||||||
|
DebugLogUtils.l("List of dictionaries with count", c.getCount());
|
||||||
|
PrivateLog.log("Returned a list of " + c.getCount() + " items");
|
||||||
|
return c;
|
||||||
|
case DICTIONARY_V2_DICT_INFO:
|
||||||
|
// In protocol version 2, we return null if the client is unknown. Otherwise
|
||||||
|
// we behave exactly like for protocol 1.
|
||||||
|
if (!MetadataDbHelper.isClientKnown(getContext(), clientId)) return null;
|
||||||
|
// Fall through
|
||||||
|
case DICTIONARY_V1_DICT_INFO:
|
||||||
|
final String locale = uri.getLastPathSegment();
|
||||||
|
final Collection<WordListInfo> dictFiles =
|
||||||
|
getDictionaryWordListsForLocale(clientId, locale);
|
||||||
|
// TODO: pass clientId to the following function
|
||||||
|
DictionaryService.updateNowIfNotUpdatedInAVeryLongTime(getContext());
|
||||||
|
if (null != dictFiles && dictFiles.size() > 0) {
|
||||||
|
PrivateLog.log("Returned " + dictFiles.size() + " files");
|
||||||
|
return new ResourcePathCursor(dictFiles);
|
||||||
|
}
|
||||||
|
PrivateLog.log("No dictionary files for this URL");
|
||||||
|
return new ResourcePathCursor(Collections.<WordListInfo>emptyList());
|
||||||
|
// V2_METADATA and V2_DATAFILE are not supported for query()
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to get the wordlist metadata associated with a wordlist ID.
|
||||||
|
*
|
||||||
|
* @param clientId the ID of the client
|
||||||
|
* @param wordlistId the ID of the wordlist for which to get the metadata.
|
||||||
|
* @return the metadata for this wordlist ID, or null if none could be found.
|
||||||
|
*/
|
||||||
|
private ContentValues getWordlistMetadataForWordlistId(final String clientId,
|
||||||
|
final String wordlistId) {
|
||||||
|
final Context context = getContext();
|
||||||
|
if (TextUtils.isEmpty(wordlistId)) return null;
|
||||||
|
final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId);
|
||||||
|
return MetadataDbHelper.getInstalledOrDeletingWordListContentValuesByWordListId(
|
||||||
|
db, wordlistId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens an asset file for an URI.
|
||||||
|
*
|
||||||
|
* Called by {@link android.content.ContentResolver#openAssetFileDescriptor(Uri, String)} or
|
||||||
|
* {@link android.content.ContentResolver#openInputStream(Uri)} from a client requesting a
|
||||||
|
* dictionary.
|
||||||
|
* @see android.content.ContentProvider#openAssetFile(Uri, String)
|
||||||
|
*
|
||||||
|
* @param uri the URI the file is for.
|
||||||
|
* @param mode the mode to read the file. MUST be "r" for readonly.
|
||||||
|
* @return the descriptor, or null if the file is not found or if mode is not equals to "r".
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public AssetFileDescriptor openAssetFile(final Uri uri, final String mode) {
|
||||||
|
if (null == mode || !"r".equals(mode)) return null;
|
||||||
|
|
||||||
|
final int match = matchUri(uri);
|
||||||
|
if (DICTIONARY_V1_DICT_INFO != match && DICTIONARY_V2_DATAFILE != match) {
|
||||||
|
// Unsupported URI for openAssetFile
|
||||||
|
Log.w(TAG, "Unsupported URI for openAssetFile : " + uri);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final String wordlistId = uri.getLastPathSegment();
|
||||||
|
final String clientId = getClientId(uri);
|
||||||
|
final ContentValues wordList = getWordlistMetadataForWordlistId(clientId, wordlistId);
|
||||||
|
|
||||||
|
if (null == wordList) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final int status = wordList.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
|
||||||
|
if (MetadataDbHelper.STATUS_DELETING == status) {
|
||||||
|
// This will return an empty file (R.raw.empty points at an empty dictionary)
|
||||||
|
// This is how we "delete" the files. It allows Android Keyboard to fake deleting
|
||||||
|
// a default dictionary - which is actually in its assets and can't be really
|
||||||
|
// deleted.
|
||||||
|
final AssetFileDescriptor afd = getContext().getResources().openRawResourceFd(
|
||||||
|
R.raw.empty);
|
||||||
|
return afd;
|
||||||
|
}
|
||||||
|
final String localFilename =
|
||||||
|
wordList.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN);
|
||||||
|
final File f = getContext().getFileStreamPath(localFilename);
|
||||||
|
final ParcelFileDescriptor pfd =
|
||||||
|
ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||||
|
return new AssetFileDescriptor(pfd, 0, pfd.getStatSize());
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
// No file : fall through and return null
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the metadata and returns the collection of dictionaries for a given locale.
|
||||||
|
*
|
||||||
|
* Word list IDs are expected to be in the form category:manual_id. This method
|
||||||
|
* will select only one word list for each category: the one with the most specific
|
||||||
|
* locale matching the locale specified in the URI. The manual id serves only to
|
||||||
|
* distinguish a word list from another for the purpose of updating, and is arbitrary
|
||||||
|
* but may not contain a colon.
|
||||||
|
*
|
||||||
|
* @param clientId the ID of the client requesting the list
|
||||||
|
* @param locale the locale for which we want the list, as a String
|
||||||
|
* @return a collection of ids. It is guaranteed to be non-null, but may be empty.
|
||||||
|
*/
|
||||||
|
private Collection<WordListInfo> getDictionaryWordListsForLocale(final String clientId,
|
||||||
|
final String locale) {
|
||||||
|
final Context context = getContext();
|
||||||
|
final Cursor results =
|
||||||
|
MetadataDbHelper.queryInstalledOrDeletingOrAvailableDictionaryMetadata(context,
|
||||||
|
clientId);
|
||||||
|
if (null == results) {
|
||||||
|
return Collections.<WordListInfo>emptyList();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final HashMap<String, WordListInfo> dicts = new HashMap<>();
|
||||||
|
final int idIndex = results.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN);
|
||||||
|
final int localeIndex = results.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN);
|
||||||
|
final int localFileNameIndex =
|
||||||
|
results.getColumnIndex(MetadataDbHelper.LOCAL_FILENAME_COLUMN);
|
||||||
|
final int rawChecksumIndex =
|
||||||
|
results.getColumnIndex(MetadataDbHelper.RAW_CHECKSUM_COLUMN);
|
||||||
|
final int statusIndex = results.getColumnIndex(MetadataDbHelper.STATUS_COLUMN);
|
||||||
|
if (results.moveToFirst()) {
|
||||||
|
do {
|
||||||
|
final String wordListId = results.getString(idIndex);
|
||||||
|
if (TextUtils.isEmpty(wordListId)) continue;
|
||||||
|
final String[] wordListIdArray =
|
||||||
|
TextUtils.split(wordListId, ID_CATEGORY_SEPARATOR);
|
||||||
|
final String wordListCategory;
|
||||||
|
if (2 == wordListIdArray.length) {
|
||||||
|
// This is at the category:manual_id format.
|
||||||
|
wordListCategory = wordListIdArray[0];
|
||||||
|
// We don't need to read wordListIdArray[1] here, because it's irrelevant to
|
||||||
|
// word list selection - it's just a name we use to identify which data file
|
||||||
|
// is a newer version of which word list. We do however return the full id
|
||||||
|
// string for each selected word list, so in this sense we are 'using' it.
|
||||||
|
} else {
|
||||||
|
// This does not contain a colon, like the old format does. Old-format IDs
|
||||||
|
// always point to main dictionaries, so we force the main category upon it.
|
||||||
|
wordListCategory = UpdateHandler.MAIN_DICTIONARY_CATEGORY;
|
||||||
|
}
|
||||||
|
final String wordListLocale = results.getString(localeIndex);
|
||||||
|
final String wordListLocalFilename = results.getString(localFileNameIndex);
|
||||||
|
final String wordListRawChecksum = results.getString(rawChecksumIndex);
|
||||||
|
final int wordListStatus = results.getInt(statusIndex);
|
||||||
|
// Test the requested locale against this wordlist locale. The requested locale
|
||||||
|
// has to either match exactly or be more specific than the dictionary - a
|
||||||
|
// dictionary for "en" would match both a request for "en" or for "en_US", but a
|
||||||
|
// dictionary for "en_GB" would not match a request for "en_US". Thus if all
|
||||||
|
// three of "en" "en_US" and "en_GB" dictionaries are installed, a request for
|
||||||
|
// "en_US" would match "en" and "en_US", and a request for "en" only would only
|
||||||
|
// match the generic "en" dictionary. For more details, see the documentation
|
||||||
|
// for LocaleUtils#getMatchLevel.
|
||||||
|
final int matchLevel = LocaleUtils.getMatchLevel(wordListLocale, locale);
|
||||||
|
if (!LocaleUtils.isMatch(matchLevel)) {
|
||||||
|
// The locale of this wordlist does not match the required locale.
|
||||||
|
// Skip this wordlist and go to the next.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (MetadataDbHelper.STATUS_INSTALLED == wordListStatus) {
|
||||||
|
// If the file does not exist, it has been deleted and the IME should
|
||||||
|
// already have it. Do not return it. However, this only applies if the
|
||||||
|
// word list is INSTALLED, for if it is DELETING we should return it always
|
||||||
|
// so that Android Keyboard can perform the actual deletion.
|
||||||
|
final File f = getContext().getFileStreamPath(wordListLocalFilename);
|
||||||
|
if (!f.isFile()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if (MetadataDbHelper.STATUS_AVAILABLE == wordListStatus) {
|
||||||
|
// The locale is the id for the main dictionary.
|
||||||
|
UpdateHandler.installIfNeverRequested(context, clientId, wordListId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final WordListInfo currentBestMatch = dicts.get(wordListCategory);
|
||||||
|
if (null == currentBestMatch
|
||||||
|
|| currentBestMatch.mMatchLevel < matchLevel) {
|
||||||
|
dicts.put(wordListCategory, new WordListInfo(wordListId, wordListLocale,
|
||||||
|
wordListRawChecksum, matchLevel));
|
||||||
|
}
|
||||||
|
} while (results.moveToNext());
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableCollection(dicts.values());
|
||||||
|
} finally {
|
||||||
|
results.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the file pointed by Uri, as returned by openAssetFile.
|
||||||
|
*
|
||||||
|
* @param uri the URI the file is for.
|
||||||
|
* @param selection ignored
|
||||||
|
* @param selectionArgs ignored
|
||||||
|
* @return the number of files deleted (0 or 1 in the current implementation)
|
||||||
|
* @see android.content.ContentProvider#delete(Uri, String, String[])
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int delete(final Uri uri, final String selection, final String[] selectionArgs)
|
||||||
|
throws UnsupportedOperationException {
|
||||||
|
final int match = matchUri(uri);
|
||||||
|
if (DICTIONARY_V1_DICT_INFO == match || DICTIONARY_V2_DATAFILE == match) {
|
||||||
|
return deleteDataFile(uri);
|
||||||
|
}
|
||||||
|
if (DICTIONARY_V2_METADATA == match) {
|
||||||
|
if (MetadataDbHelper.deleteClient(getContext(), getClientId(uri))) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// Unsupported URI for delete
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int deleteDataFile(final Uri uri) {
|
||||||
|
final String wordlistId = uri.getLastPathSegment();
|
||||||
|
final String clientId = getClientId(uri);
|
||||||
|
final ContentValues wordList = getWordlistMetadataForWordlistId(clientId, wordlistId);
|
||||||
|
if (null == wordList) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
final int status = wordList.getAsInteger(MetadataDbHelper.STATUS_COLUMN);
|
||||||
|
final int version = wordList.getAsInteger(MetadataDbHelper.VERSION_COLUMN);
|
||||||
|
if (MetadataDbHelper.STATUS_DELETING == status) {
|
||||||
|
UpdateHandler.markAsDeleted(getContext(), clientId, wordlistId, version, status);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (MetadataDbHelper.STATUS_INSTALLED == status) {
|
||||||
|
final String result = uri.getQueryParameter(QUERY_PARAMETER_DELETE_RESULT);
|
||||||
|
if (QUERY_PARAMETER_FAILURE.equals(result)) {
|
||||||
|
if (DEBUG) {
|
||||||
|
Log.d(TAG,
|
||||||
|
"Dictionary is broken, attempting to retry download & installation.");
|
||||||
|
}
|
||||||
|
UpdateHandler.markAsBrokenOrRetrying(getContext(), clientId, wordlistId, version);
|
||||||
|
}
|
||||||
|
final String localFilename =
|
||||||
|
wordList.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN);
|
||||||
|
final File f = getContext().getFileStreamPath(localFilename);
|
||||||
|
// f.delete() returns true if the file was successfully deleted, false otherwise
|
||||||
|
return f.delete() ? 1 : 0;
|
||||||
|
}
|
||||||
|
Log.e(TAG, "Attempt to delete a file whose status is " + status);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert data into the provider. May be either a metadata source URL or some dictionary info.
|
||||||
|
*
|
||||||
|
* @param uri the designated content URI. See sUriMatcherV{1,2} for available URIs.
|
||||||
|
* @param values the values to insert for this content uri
|
||||||
|
* @return the URI for the newly inserted item. May be null if arguments don't allow for insert
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Uri insert(final Uri uri, final ContentValues values)
|
||||||
|
throws UnsupportedOperationException {
|
||||||
|
if (null == uri || null == values) return null; // Should never happen but let's be safe
|
||||||
|
PrivateLog.log("Insert, uri = " + uri.toString());
|
||||||
|
final String clientId = getClientId(uri);
|
||||||
|
switch (matchUri(uri)) {
|
||||||
|
case DICTIONARY_V2_METADATA:
|
||||||
|
// The values should contain a valid client ID and a valid URI for the metadata.
|
||||||
|
// The client ID may not be null, nor may it be empty because the empty client ID
|
||||||
|
// is reserved for internal use.
|
||||||
|
// The metadata URI may not be null, but it may be empty if the client does not
|
||||||
|
// want the dictionary pack to update the metadata automatically.
|
||||||
|
MetadataDbHelper.updateClientInfo(getContext(), clientId, values);
|
||||||
|
break;
|
||||||
|
case DICTIONARY_V2_DICT_INFO:
|
||||||
|
try {
|
||||||
|
final WordListMetadata newDictionaryMetadata =
|
||||||
|
WordListMetadata.createFromContentValues(
|
||||||
|
MetadataDbHelper.completeWithDefaultValues(values));
|
||||||
|
new ActionBatch.MarkPreInstalledAction(clientId, newDictionaryMetadata)
|
||||||
|
.execute(getContext());
|
||||||
|
} catch (final BadFormatException e) {
|
||||||
|
Log.w(TAG, "Not enough information to insert this dictionary " + values, e);
|
||||||
|
}
|
||||||
|
// We just received new information about the list of dictionary for this client.
|
||||||
|
// For all intents and purposes, this is new metadata, so we should publish it
|
||||||
|
// so that any listeners (like the Settings interface for example) can update
|
||||||
|
// themselves.
|
||||||
|
UpdateHandler.publishUpdateMetadataCompleted(getContext(), true);
|
||||||
|
break;
|
||||||
|
case DICTIONARY_V1_WHOLE_LIST:
|
||||||
|
case DICTIONARY_V1_DICT_INFO:
|
||||||
|
PrivateLog.log("Attempt to insert : " + uri);
|
||||||
|
throw new UnsupportedOperationException(
|
||||||
|
"Insertion in the dictionary is not supported in this version");
|
||||||
|
}
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updating data is not supported, and will throw an exception.
|
||||||
|
* @see android.content.ContentProvider#update(Uri, ContentValues, String, String[])
|
||||||
|
* @see android.content.ContentProvider#insert(Uri, ContentValues)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int update(final Uri uri, final ContentValues values, final String selection,
|
||||||
|
final String[] selectionArgs) throws UnsupportedOperationException {
|
||||||
|
PrivateLog.log("Attempt to update : " + uri);
|
||||||
|
throw new UnsupportedOperationException("Updating dictionary words is not supported");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,274 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
import android.app.AlarmManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.latin.BinaryDictionaryFileDumper;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.R;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
import java.util.concurrent.ThreadPoolExecutor;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service that handles background tasks for the dictionary provider.
|
||||||
|
*
|
||||||
|
* This service provides the context for the long-running operations done by the
|
||||||
|
* dictionary provider. Those include:
|
||||||
|
* - Checking for the last update date and scheduling the next update. This runs every
|
||||||
|
* day around midnight, upon reception of the DATE_CHANGED_INTENT_ACTION broadcast.
|
||||||
|
* Every four days, it schedules an update of the metadata with the alarm manager.
|
||||||
|
* - Issuing the order to update the metadata. This runs every four days, between 0 and
|
||||||
|
* 6, upon reception of the UPDATE_NOW_INTENT_ACTION broadcast sent by the alarm manager
|
||||||
|
* as a result of the above action.
|
||||||
|
* - Handling a download that just ended. These come in two flavors:
|
||||||
|
* - Metadata is finished downloading. We should check whether there are new dictionaries
|
||||||
|
* available, and download those that we need that have new versions.
|
||||||
|
* - A dictionary file finished downloading. We should put the file ready for a client IME
|
||||||
|
* to access, and mark the current state as such.
|
||||||
|
*/
|
||||||
|
public final class DictionaryService extends Service {
|
||||||
|
private static final String TAG = DictionaryService.class.getSimpleName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The package name, to use in the intent actions.
|
||||||
|
*/
|
||||||
|
private static final String PACKAGE_NAME = "org.dslul.openboard.inputmethod.latin";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The action of the date changing, used to schedule a periodic freshness check
|
||||||
|
*/
|
||||||
|
private static final String DATE_CHANGED_INTENT_ACTION =
|
||||||
|
Intent.ACTION_DATE_CHANGED;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The action of displaying a toast to warn the user an automatic download is starting.
|
||||||
|
*/
|
||||||
|
/* package */ static final String SHOW_DOWNLOAD_TOAST_INTENT_ACTION =
|
||||||
|
PACKAGE_NAME + ".SHOW_DOWNLOAD_TOAST_INTENT_ACTION";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A locale argument, as a String.
|
||||||
|
*/
|
||||||
|
/* package */ static final String LOCALE_INTENT_ARGUMENT = "locale";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How often, in milliseconds, we want to update the metadata. This is a
|
||||||
|
* floor value; actually, it may happen several hours later, or even more.
|
||||||
|
*/
|
||||||
|
private static final long UPDATE_FREQUENCY_MILLIS = TimeUnit.DAYS.toMillis(4);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We are waked around midnight, local time. We want to wake between midnight and 6 am,
|
||||||
|
* roughly. So use a random time between 0 and this delay.
|
||||||
|
*/
|
||||||
|
private static final int MAX_ALARM_DELAY_MILLIS = (int)TimeUnit.HOURS.toMillis(6);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How long we consider a "very long time". If no update took place in this time,
|
||||||
|
* the content provider will trigger an update in the background.
|
||||||
|
*/
|
||||||
|
private static final long VERY_LONG_TIME_MILLIS = TimeUnit.DAYS.toMillis(14);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After starting a download, how long we wait before considering it may be stuck. After this
|
||||||
|
* period is elapsed, if the keyboard tries to download again, then we cancel and re-register
|
||||||
|
* the request; if it's within this time, we just leave it be.
|
||||||
|
* It's important to note that we do not re-submit the request merely because the time is up.
|
||||||
|
* This is only to decide whether to cancel the old one and re-requesting when the keyboard
|
||||||
|
* fires a new request for the same data.
|
||||||
|
*/
|
||||||
|
public static final long NO_CANCEL_DOWNLOAD_PERIOD_MILLIS = TimeUnit.SECONDS.toMillis(30);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An executor that serializes tasks given to it.
|
||||||
|
*/
|
||||||
|
private ThreadPoolExecutor mExecutor;
|
||||||
|
private static final int WORKER_THREAD_TIMEOUT_SECONDS = 15;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
// By default, a thread pool executor does not timeout its core threads, so it will
|
||||||
|
// never kill them when there isn't any work to do any more. That would mean the service
|
||||||
|
// can never die! By creating it this way and calling allowCoreThreadTimeOut, we allow
|
||||||
|
// the single thread to time out after WORKER_THREAD_TIMEOUT_SECONDS = 15 seconds, allowing
|
||||||
|
// the process to be reclaimed by the system any time after that if it's not doing
|
||||||
|
// anything else.
|
||||||
|
// Executors#newSingleThreadExecutor creates a ThreadPoolExecutor but it returns the
|
||||||
|
// superclass ExecutorService which does not have the #allowCoreThreadTimeOut method,
|
||||||
|
// so we can't use that.
|
||||||
|
mExecutor = new ThreadPoolExecutor(1 /* corePoolSize */, 1 /* maximumPoolSize */,
|
||||||
|
WORKER_THREAD_TIMEOUT_SECONDS /* keepAliveTime */,
|
||||||
|
TimeUnit.SECONDS /* unit for keepAliveTime */,
|
||||||
|
new LinkedBlockingQueue<Runnable>() /* workQueue */);
|
||||||
|
mExecutor.allowCoreThreadTimeOut(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
// This service cannot be bound
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes an explicit command.
|
||||||
|
*
|
||||||
|
* This is the entry point for arbitrary commands that are executed upon reception of certain
|
||||||
|
* events that should be executed on the context of this service. The supported commands are:
|
||||||
|
* - Check last update time and possibly schedule an update of the data for later.
|
||||||
|
* This is triggered every day, upon reception of the DATE_CHANGED_INTENT_ACTION broadcast.
|
||||||
|
* - Update data NOW.
|
||||||
|
* This is normally received upon trigger of the scheduled update.
|
||||||
|
* - Handle a finished download.
|
||||||
|
* This executes the actions that must be taken after a file (metadata or dictionary data
|
||||||
|
* has been downloaded (or failed to download).
|
||||||
|
* The commands that can be spun an another thread will be executed serially, in order, on
|
||||||
|
* a worker thread that is created on demand and terminates after a short while if there isn't
|
||||||
|
* any work left to do.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized int onStartCommand(final Intent intent, final int flags,
|
||||||
|
final int startId) {
|
||||||
|
final DictionaryService self = this;
|
||||||
|
if (SHOW_DOWNLOAD_TOAST_INTENT_ACTION.equals(intent.getAction())) {
|
||||||
|
final String localeString = intent.getStringExtra(LOCALE_INTENT_ARGUMENT);
|
||||||
|
if (localeString == null) {
|
||||||
|
Log.e(TAG, "Received " + intent.getAction() + " without locale; skipped");
|
||||||
|
} else {
|
||||||
|
// This is a UI action, it can't be run in another thread
|
||||||
|
showStartDownloadingToast(
|
||||||
|
this, LocaleUtils.constructLocaleFromString(localeString));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If it's a command that does not require UI, arrange for the work to be done on a
|
||||||
|
// separate thread, so that we can return right away. The executor will spawn a thread
|
||||||
|
// if necessary, or reuse a thread that has become idle as appropriate.
|
||||||
|
// DATE_CHANGED or UPDATE_NOW are examples of commands that can be done on another
|
||||||
|
// thread.
|
||||||
|
mExecutor.submit(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
dispatchBroadcast(self, intent);
|
||||||
|
// Since calls to onStartCommand are serialized, the submissions to the executor
|
||||||
|
// are serialized. That means we are guaranteed to call the stopSelfResult()
|
||||||
|
// in the same order that we got them, so we don't need to take care of the
|
||||||
|
// order.
|
||||||
|
stopSelfResult(startId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Service.START_REDELIVER_INTENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void dispatchBroadcast(final Context context, final Intent intent) {
|
||||||
|
final String action = intent.getAction();
|
||||||
|
if (DATE_CHANGED_INTENT_ACTION.equals(action)) {
|
||||||
|
// This happens when the date of the device changes. This normally happens
|
||||||
|
// at midnight local time, but it may happen if the user changes the date
|
||||||
|
// by hand or something similar happens.
|
||||||
|
checkTimeAndMaybeSetupUpdateAlarm(context);
|
||||||
|
} else if (DictionaryPackConstants.UPDATE_NOW_INTENT_ACTION.equals(action)) {
|
||||||
|
// Intent to trigger an update now.
|
||||||
|
UpdateHandler.tryUpdate(context);
|
||||||
|
} else if (DictionaryPackConstants.INIT_AND_UPDATE_NOW_INTENT_ACTION.equals(action)) {
|
||||||
|
// Initialize the client Db.
|
||||||
|
final String mClientId = context.getString(R.string.dictionary_pack_client_id);
|
||||||
|
BinaryDictionaryFileDumper.initializeClientRecordHelper(context, mClientId);
|
||||||
|
|
||||||
|
// Updates the metadata and the download the dictionaries.
|
||||||
|
UpdateHandler.tryUpdate(context);
|
||||||
|
} else {
|
||||||
|
UpdateHandler.downloadFinished(context, intent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setups an alarm to check for updates if an update is due.
|
||||||
|
*/
|
||||||
|
private static void checkTimeAndMaybeSetupUpdateAlarm(final Context context) {
|
||||||
|
// Of all clients, if the one that hasn't been updated for the longest
|
||||||
|
// is still more recent than UPDATE_FREQUENCY_MILLIS, do nothing.
|
||||||
|
if (!isLastUpdateAtLeastThisOld(context, UPDATE_FREQUENCY_MILLIS)) return;
|
||||||
|
|
||||||
|
PrivateLog.log("Date changed - registering alarm");
|
||||||
|
AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
|
||||||
|
|
||||||
|
// Best effort to wake between midnight and MAX_ALARM_DELAY_MILLIS in the morning.
|
||||||
|
// It doesn't matter too much if this is very inexact.
|
||||||
|
final long now = System.currentTimeMillis();
|
||||||
|
final long alarmTime = now + new Random().nextInt(MAX_ALARM_DELAY_MILLIS);
|
||||||
|
final Intent updateIntent = new Intent(DictionaryPackConstants.UPDATE_NOW_INTENT_ACTION);
|
||||||
|
final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,
|
||||||
|
updateIntent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||||
|
|
||||||
|
// We set the alarm in the type that doesn't forcefully wake the device
|
||||||
|
// from sleep, but fires the next time the device actually wakes for any
|
||||||
|
// other reason.
|
||||||
|
if (null != alarmManager) alarmManager.set(AlarmManager.RTC, alarmTime, pendingIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to decide whether the last update is older than a certain time.
|
||||||
|
*
|
||||||
|
* @return true if at least `time' milliseconds have elapsed since last update, false otherwise.
|
||||||
|
*/
|
||||||
|
private static boolean isLastUpdateAtLeastThisOld(final Context context, final long time) {
|
||||||
|
final long now = System.currentTimeMillis();
|
||||||
|
final long lastUpdate = MetadataDbHelper.getOldestUpdateTime(context);
|
||||||
|
PrivateLog.log("Last update was " + lastUpdate);
|
||||||
|
return lastUpdate + time < now;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes data if it hasn't been refreshed in a very long time.
|
||||||
|
*
|
||||||
|
* This will check the last update time, and if it's been more than VERY_LONG_TIME_MILLIS,
|
||||||
|
* update metadata now - and possibly take subsequent update actions.
|
||||||
|
*/
|
||||||
|
public static void updateNowIfNotUpdatedInAVeryLongTime(final Context context) {
|
||||||
|
if (!isLastUpdateAtLeastThisOld(context, VERY_LONG_TIME_MILLIS)) return;
|
||||||
|
UpdateHandler.tryUpdate(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a toast informing the user that an automatic dictionary download is starting.
|
||||||
|
*/
|
||||||
|
private static void showStartDownloadingToast(final Context context,
|
||||||
|
@Nonnull final Locale locale) {
|
||||||
|
final String toastText = String.format(
|
||||||
|
context.getString(R.string.toast_downloading_suggestions),
|
||||||
|
locale.getDisplayName());
|
||||||
|
Toast.makeText(context, toastText, Toast.LENGTH_LONG).show();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy
|
||||||
|
* of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.latin.utils.FragmentUtils;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.preference.PreferenceActivity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preference screen.
|
||||||
|
*/
|
||||||
|
public final class DictionarySettingsActivity extends PreferenceActivity {
|
||||||
|
private static final String DEFAULT_FRAGMENT = DictionarySettingsFragment.class.getName();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Intent getIntent() {
|
||||||
|
final Intent modIntent = new Intent(super.getIntent());
|
||||||
|
modIntent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT);
|
||||||
|
modIntent.putExtra(EXTRA_NO_HEADERS, true);
|
||||||
|
// Important note : the original intent should contain a String extra with the key
|
||||||
|
// DictionarySettingsFragment.DICT_SETTINGS_FRAGMENT_CLIENT_ID_ARGUMENT so that the
|
||||||
|
// fragment can know who the client is.
|
||||||
|
return modIntent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||||
|
@Override
|
||||||
|
public boolean isValidFragment(String fragmentName) {
|
||||||
|
return FragmentUtils.isValidFragment(fragmentName);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,438 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy
|
||||||
|
* of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.IntentFilter;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.ConnectivityManager;
|
||||||
|
import android.net.NetworkInfo;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import android.preference.PreferenceFragment;
|
||||||
|
import android.preference.PreferenceGroup;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.animation.AnimationUtils;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.latin.R;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preference screen.
|
||||||
|
*/
|
||||||
|
public final class DictionarySettingsFragment extends PreferenceFragment
|
||||||
|
implements UpdateHandler.UpdateEventListener {
|
||||||
|
private static final String TAG = DictionarySettingsFragment.class.getSimpleName();
|
||||||
|
|
||||||
|
static final private String DICT_LIST_ID = "list";
|
||||||
|
static final public String DICT_SETTINGS_FRAGMENT_CLIENT_ID_ARGUMENT = "clientId";
|
||||||
|
|
||||||
|
static final private int MENU_UPDATE_NOW = Menu.FIRST;
|
||||||
|
|
||||||
|
private View mLoadingView;
|
||||||
|
private String mClientId;
|
||||||
|
private ConnectivityManager mConnectivityManager;
|
||||||
|
private MenuItem mUpdateNowMenu;
|
||||||
|
private boolean mChangedSettings;
|
||||||
|
private DictionaryListInterfaceState mDictionaryListInterfaceState =
|
||||||
|
new DictionaryListInterfaceState();
|
||||||
|
// never null
|
||||||
|
private TreeMap<String, WordListPreference> mCurrentPreferenceMap = new TreeMap<>();
|
||||||
|
|
||||||
|
private final BroadcastReceiver mConnectivityChangedReceiver = new BroadcastReceiver() {
|
||||||
|
@Override
|
||||||
|
public void onReceive(final Context context, final Intent intent) {
|
||||||
|
refreshNetworkState();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Empty constructor for fragment generation.
|
||||||
|
*/
|
||||||
|
public DictionarySettingsFragment() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
|
||||||
|
final Bundle savedInstanceState) {
|
||||||
|
final View v = inflater.inflate(R.layout.loading_page, container, true);
|
||||||
|
mLoadingView = v.findViewById(R.id.loading_container);
|
||||||
|
return super.onCreateView(inflater, container, savedInstanceState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityCreated(final Bundle savedInstanceState) {
|
||||||
|
super.onActivityCreated(savedInstanceState);
|
||||||
|
final Activity activity = getActivity();
|
||||||
|
mClientId = activity.getIntent().getStringExtra(DICT_SETTINGS_FRAGMENT_CLIENT_ID_ARGUMENT);
|
||||||
|
mConnectivityManager =
|
||||||
|
(ConnectivityManager)activity.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||||
|
addPreferencesFromResource(R.xml.dictionary_settings);
|
||||||
|
refreshInterface();
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
|
||||||
|
new AsyncTask<Void, Void, String>() {
|
||||||
|
@Override
|
||||||
|
protected String doInBackground(Void... params) {
|
||||||
|
return MetadataDbHelper.getMetadataUriAsString(getActivity(), mClientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(String metadataUri) {
|
||||||
|
// We only add the "Refresh" button if we have a non-empty URL to refresh from. If
|
||||||
|
// the URL is empty, of course we can't refresh so it makes no sense to display
|
||||||
|
// this.
|
||||||
|
if (!TextUtils.isEmpty(metadataUri)) {
|
||||||
|
if (mUpdateNowMenu == null) {
|
||||||
|
mUpdateNowMenu = menu.add(Menu.NONE, MENU_UPDATE_NOW, 0,
|
||||||
|
R.string.check_for_updates_now);
|
||||||
|
mUpdateNowMenu.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
||||||
|
}
|
||||||
|
refreshNetworkState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
mChangedSettings = false;
|
||||||
|
UpdateHandler.registerUpdateEventListener(this);
|
||||||
|
final Activity activity = getActivity();
|
||||||
|
final IntentFilter filter = new IntentFilter();
|
||||||
|
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
|
||||||
|
getActivity().registerReceiver(mConnectivityChangedReceiver, filter);
|
||||||
|
refreshNetworkState();
|
||||||
|
|
||||||
|
new Thread("onResume") {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
if (!MetadataDbHelper.isClientKnown(activity, mClientId)) {
|
||||||
|
Log.i(TAG, "Unknown dictionary pack client: " + mClientId
|
||||||
|
+ ". Requesting info.");
|
||||||
|
final Intent unknownClientBroadcast =
|
||||||
|
new Intent(DictionaryPackConstants.UNKNOWN_DICTIONARY_PROVIDER_CLIENT);
|
||||||
|
unknownClientBroadcast.putExtra(
|
||||||
|
DictionaryPackConstants.DICTIONARY_PROVIDER_CLIENT_EXTRA, mClientId);
|
||||||
|
activity.sendBroadcast(unknownClientBroadcast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
super.onPause();
|
||||||
|
final Activity activity = getActivity();
|
||||||
|
UpdateHandler.unregisterUpdateEventListener(this);
|
||||||
|
activity.unregisterReceiver(mConnectivityChangedReceiver);
|
||||||
|
if (mChangedSettings) {
|
||||||
|
final Intent newDictBroadcast =
|
||||||
|
new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION);
|
||||||
|
activity.sendBroadcast(newDictBroadcast);
|
||||||
|
mChangedSettings = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void downloadedMetadata(final boolean succeeded) {
|
||||||
|
stopLoadingAnimation();
|
||||||
|
if (!succeeded) return; // If the download failed nothing changed, so no need to refresh
|
||||||
|
new Thread("refreshInterface") {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
refreshInterface();
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void wordListDownloadFinished(final String wordListId, final boolean succeeded) {
|
||||||
|
final WordListPreference pref = findWordListPreference(wordListId);
|
||||||
|
if (null == pref) return;
|
||||||
|
// TODO: Report to the user if !succeeded
|
||||||
|
final Activity activity = getActivity();
|
||||||
|
if (null == activity) return;
|
||||||
|
activity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// We have to re-read the db in case the description has changed, and to
|
||||||
|
// find out what state it ended up if the download wasn't successful
|
||||||
|
// TODO: don't redo everything, only re-read and set this word list status
|
||||||
|
refreshInterface();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private WordListPreference findWordListPreference(final String id) {
|
||||||
|
final PreferenceGroup prefScreen = getPreferenceScreen();
|
||||||
|
if (null == prefScreen) {
|
||||||
|
Log.e(TAG, "Could not find the preference group");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (int i = prefScreen.getPreferenceCount() - 1; i >= 0; --i) {
|
||||||
|
final Preference pref = prefScreen.getPreference(i);
|
||||||
|
if (pref instanceof WordListPreference) {
|
||||||
|
final WordListPreference wlPref = (WordListPreference)pref;
|
||||||
|
if (id.equals(wlPref.mWordlistId)) {
|
||||||
|
return wlPref;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.e(TAG, "Could not find the preference for a word list id " + id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateCycleCompleted() {}
|
||||||
|
|
||||||
|
void refreshNetworkState() {
|
||||||
|
NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
|
||||||
|
boolean isConnected = null == info ? false : info.isConnected();
|
||||||
|
if (null != mUpdateNowMenu) mUpdateNowMenu.setEnabled(isConnected);
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshInterface() {
|
||||||
|
final Activity activity = getActivity();
|
||||||
|
if (null == activity) return;
|
||||||
|
final PreferenceGroup prefScreen = getPreferenceScreen();
|
||||||
|
final Collection<? extends Preference> prefList =
|
||||||
|
createInstalledDictSettingsCollection(mClientId);
|
||||||
|
|
||||||
|
activity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// TODO: display this somewhere
|
||||||
|
// if (0 != lastUpdate) mUpdateNowPreference.setSummary(updateNowSummary);
|
||||||
|
refreshNetworkState();
|
||||||
|
|
||||||
|
removeAnyDictSettings(prefScreen);
|
||||||
|
int i = 0;
|
||||||
|
for (Preference preference : prefList) {
|
||||||
|
preference.setOrder(i++);
|
||||||
|
prefScreen.addPreference(preference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Preference createErrorMessage(final Activity activity, final int messageResource) {
|
||||||
|
final Preference message = new Preference(activity);
|
||||||
|
message.setTitle(messageResource);
|
||||||
|
message.setEnabled(false);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void removeAnyDictSettings(final PreferenceGroup prefGroup) {
|
||||||
|
for (int i = prefGroup.getPreferenceCount() - 1; i >= 0; --i) {
|
||||||
|
prefGroup.removePreference(prefGroup.getPreference(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a WordListPreference list to be added to the screen.
|
||||||
|
*
|
||||||
|
* This method only creates the preferences but does not add them.
|
||||||
|
* Thus, it can be called on another thread.
|
||||||
|
*
|
||||||
|
* @param clientId the id of the client for which we want to display the dictionary list
|
||||||
|
* @return A collection of preferences ready to add to the interface.
|
||||||
|
*/
|
||||||
|
private Collection<? extends Preference> createInstalledDictSettingsCollection(
|
||||||
|
final String clientId) {
|
||||||
|
// This will directly contact the DictionaryProvider and request the list exactly like
|
||||||
|
// any regular client would do.
|
||||||
|
// Considering the respective value of the respective constants used here for each path,
|
||||||
|
// segment, the url generated by this is of the form (assuming "clientId" as a clientId)
|
||||||
|
// content://org.dslul.openboard.inputmethod.latin.dictionarypack/clientId/list?procotol=2
|
||||||
|
final Uri contentUri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
|
||||||
|
.authority(getString(R.string.authority))
|
||||||
|
.appendPath(clientId)
|
||||||
|
.appendPath(DICT_LIST_ID)
|
||||||
|
// Need to use version 2 to get this client's list
|
||||||
|
.appendQueryParameter(DictionaryProvider.QUERY_PARAMETER_PROTOCOL_VERSION, "2")
|
||||||
|
.build();
|
||||||
|
final Activity activity = getActivity();
|
||||||
|
final Cursor cursor = (null == activity) ? null
|
||||||
|
: activity.getContentResolver().query(contentUri, null, null, null, null);
|
||||||
|
|
||||||
|
if (null == cursor) {
|
||||||
|
final ArrayList<Preference> result = new ArrayList<>();
|
||||||
|
result.add(createErrorMessage(activity, R.string.cannot_connect_to_dict_service));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (!cursor.moveToFirst()) {
|
||||||
|
final ArrayList<Preference> result = new ArrayList<>();
|
||||||
|
result.add(createErrorMessage(activity, R.string.no_dictionaries_available));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
final String systemLocaleString = Locale.getDefault().toString();
|
||||||
|
final TreeMap<String, WordListPreference> prefMap = new TreeMap<>();
|
||||||
|
final int idIndex = cursor.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN);
|
||||||
|
final int versionIndex = cursor.getColumnIndex(MetadataDbHelper.VERSION_COLUMN);
|
||||||
|
final int localeIndex = cursor.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN);
|
||||||
|
final int descriptionIndex = cursor.getColumnIndex(MetadataDbHelper.DESCRIPTION_COLUMN);
|
||||||
|
final int statusIndex = cursor.getColumnIndex(MetadataDbHelper.STATUS_COLUMN);
|
||||||
|
final int filesizeIndex = cursor.getColumnIndex(MetadataDbHelper.FILESIZE_COLUMN);
|
||||||
|
do {
|
||||||
|
final String wordlistId = cursor.getString(idIndex);
|
||||||
|
final int version = cursor.getInt(versionIndex);
|
||||||
|
final String localeString = cursor.getString(localeIndex);
|
||||||
|
final Locale locale = new Locale(localeString);
|
||||||
|
final String description = cursor.getString(descriptionIndex);
|
||||||
|
final int status = cursor.getInt(statusIndex);
|
||||||
|
final int matchLevel = LocaleUtils.getMatchLevel(systemLocaleString, localeString);
|
||||||
|
final String matchLevelString = LocaleUtils.getMatchLevelSortedString(matchLevel);
|
||||||
|
final int filesize = cursor.getInt(filesizeIndex);
|
||||||
|
// The key is sorted in lexicographic order, according to the match level, then
|
||||||
|
// the description.
|
||||||
|
final String key = matchLevelString + "." + description + "." + wordlistId;
|
||||||
|
final WordListPreference existingPref = prefMap.get(key);
|
||||||
|
if (null == existingPref || existingPref.hasPriorityOver(status)) {
|
||||||
|
final WordListPreference oldPreference = mCurrentPreferenceMap.get(key);
|
||||||
|
final WordListPreference pref;
|
||||||
|
if (null != oldPreference
|
||||||
|
&& oldPreference.mVersion == version
|
||||||
|
&& oldPreference.hasStatus(status)
|
||||||
|
&& oldPreference.mLocale.equals(locale)) {
|
||||||
|
// If the old preference has all the new attributes, reuse it. Ideally,
|
||||||
|
// we should reuse the old pref even if its status is different and call
|
||||||
|
// setStatus here, but setStatus calls Preference#setSummary() which
|
||||||
|
// needs to be done on the UI thread and we're not on the UI thread
|
||||||
|
// here. We could do all this work on the UI thread, but in this case
|
||||||
|
// it's probably lighter to stay on a background thread and throw this
|
||||||
|
// old preference out.
|
||||||
|
pref = oldPreference;
|
||||||
|
} else {
|
||||||
|
// Otherwise, discard it and create a new one instead.
|
||||||
|
// TODO: when the status is different from the old one, we need to
|
||||||
|
// animate the old one out before animating the new one in.
|
||||||
|
pref = new WordListPreference(activity, mDictionaryListInterfaceState,
|
||||||
|
mClientId, wordlistId, version, locale, description, status,
|
||||||
|
filesize);
|
||||||
|
}
|
||||||
|
prefMap.put(key, pref);
|
||||||
|
}
|
||||||
|
} while (cursor.moveToNext());
|
||||||
|
mCurrentPreferenceMap = prefMap;
|
||||||
|
return prefMap.values();
|
||||||
|
} finally {
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
|
switch (item.getItemId()) {
|
||||||
|
case MENU_UPDATE_NOW:
|
||||||
|
if (View.GONE == mLoadingView.getVisibility()) {
|
||||||
|
startRefresh();
|
||||||
|
} else {
|
||||||
|
cancelRefresh();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startRefresh() {
|
||||||
|
startLoadingAnimation();
|
||||||
|
mChangedSettings = true;
|
||||||
|
UpdateHandler.registerUpdateEventListener(this);
|
||||||
|
final Activity activity = getActivity();
|
||||||
|
new Thread("updateByHand") {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// We call tryUpdate(), which returns whether we could successfully start an update.
|
||||||
|
// If we couldn't, we'll never receive the end callback, so we stop the loading
|
||||||
|
// animation and return to the previous screen.
|
||||||
|
if (!UpdateHandler.tryUpdate(activity)) {
|
||||||
|
stopLoadingAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelRefresh() {
|
||||||
|
UpdateHandler.unregisterUpdateEventListener(this);
|
||||||
|
final Context context = getActivity();
|
||||||
|
new Thread("cancelByHand") {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
UpdateHandler.cancelUpdate(context, mClientId);
|
||||||
|
stopLoadingAnimation();
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startLoadingAnimation() {
|
||||||
|
mLoadingView.setVisibility(View.VISIBLE);
|
||||||
|
getView().setVisibility(View.GONE);
|
||||||
|
// We come here when the menu element is pressed so presumably it can't be null. But
|
||||||
|
// better safe than sorry.
|
||||||
|
if (null != mUpdateNowMenu) mUpdateNowMenu.setTitle(R.string.cancel);
|
||||||
|
}
|
||||||
|
|
||||||
|
void stopLoadingAnimation() {
|
||||||
|
final View preferenceView = getView();
|
||||||
|
final Activity activity = getActivity();
|
||||||
|
if (null == activity) return;
|
||||||
|
final View loadingView = mLoadingView;
|
||||||
|
final MenuItem updateNowMenu = mUpdateNowMenu;
|
||||||
|
activity.runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
loadingView.setVisibility(View.GONE);
|
||||||
|
preferenceView.setVisibility(View.VISIBLE);
|
||||||
|
loadingView.startAnimation(AnimationUtils.loadAnimation(
|
||||||
|
activity, android.R.anim.fade_out));
|
||||||
|
preferenceView.startAnimation(AnimationUtils.loadAnimation(
|
||||||
|
activity, android.R.anim.fade_in));
|
||||||
|
// The menu is created by the framework asynchronously after the activity,
|
||||||
|
// which means it's possible to have the activity running but the menu not
|
||||||
|
// created yet - hence the necessity for a null check here.
|
||||||
|
if (null != updateNowMenu) {
|
||||||
|
updateNowMenu.setTitle(R.string.check_for_updates_now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple container of download ID and download start date.
|
||||||
|
*/
|
||||||
|
public class DownloadIdAndStartDate {
|
||||||
|
public final long mId;
|
||||||
|
public final long mStartDate;
|
||||||
|
public DownloadIdAndStartDate(final long id, final long startDate) {
|
||||||
|
mId = id;
|
||||||
|
mStartDate = startDate;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2014 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
import android.app.DownloadManager;
|
||||||
|
import android.app.DownloadManager.Query;
|
||||||
|
import android.app.DownloadManager.Request;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteException;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class to help with calling DownloadManager methods.
|
||||||
|
*
|
||||||
|
* Mostly, the problem here is that most methods from DownloadManager may throw SQL exceptions if
|
||||||
|
* they can't open the database on disk. We want to avoid crashing in these cases but can't do
|
||||||
|
* much more, so this class insulates the callers from these. SQLiteException also inherit from
|
||||||
|
* RuntimeException so they are unchecked :(
|
||||||
|
* While we're at it, we also insulate callers from the cases where DownloadManager is disabled,
|
||||||
|
* and getSystemService returns null.
|
||||||
|
*/
|
||||||
|
public class DownloadManagerWrapper {
|
||||||
|
private final static String TAG = DownloadManagerWrapper.class.getSimpleName();
|
||||||
|
private final DownloadManager mDownloadManager;
|
||||||
|
|
||||||
|
public DownloadManagerWrapper(final Context context) {
|
||||||
|
this((DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE));
|
||||||
|
}
|
||||||
|
|
||||||
|
private DownloadManagerWrapper(final DownloadManager downloadManager) {
|
||||||
|
mDownloadManager = downloadManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove(final long... ids) {
|
||||||
|
try {
|
||||||
|
if (null != mDownloadManager) {
|
||||||
|
mDownloadManager.remove(ids);
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// This is expected to happen on boot when the device is encrypted.
|
||||||
|
} catch (SQLiteException e) {
|
||||||
|
// We couldn't remove the file from DownloadManager. Apparently, the database can't
|
||||||
|
// be opened. It may be a problem with file system corruption. In any case, there is
|
||||||
|
// not much we can do apart from avoiding crashing.
|
||||||
|
Log.e(TAG, "Can't remove files with ID " + ids + " from download manager", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ParcelFileDescriptor openDownloadedFile(final long fileId) throws FileNotFoundException {
|
||||||
|
try {
|
||||||
|
if (null != mDownloadManager) {
|
||||||
|
return mDownloadManager.openDownloadedFile(fileId);
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// This is expected to happen on boot when the device is encrypted.
|
||||||
|
} catch (SQLiteException e) {
|
||||||
|
Log.e(TAG, "Can't open downloaded file with ID " + fileId, e);
|
||||||
|
}
|
||||||
|
// We come here if mDownloadManager is null or if an exception was thrown.
|
||||||
|
throw new FileNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public Cursor query(final Query query) {
|
||||||
|
try {
|
||||||
|
if (null != mDownloadManager) {
|
||||||
|
return mDownloadManager.query(query);
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// This is expected to happen on boot when the device is encrypted.
|
||||||
|
} catch (SQLiteException e) {
|
||||||
|
Log.e(TAG, "Can't query the download manager", e);
|
||||||
|
}
|
||||||
|
// We come here if mDownloadManager is null or if an exception was thrown.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long enqueue(final Request request) {
|
||||||
|
try {
|
||||||
|
if (null != mDownloadManager) {
|
||||||
|
return mDownloadManager.enqueue(request);
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// This is expected to happen on boot when the device is encrypted.
|
||||||
|
} catch (SQLiteException e) {
|
||||||
|
Log.e(TAG, "Can't enqueue a request with the download manager", e);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2012 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.Html;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.dslul.openboard.inputmethod.annotations.ExternallyReferenced;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.R;
|
||||||
|
import org.dslul.openboard.inputmethod.latin.common.LocaleUtils;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This implements the dialog for asking the user whether it's okay to download dictionaries over
|
||||||
|
* a metered connection or not (e.g. their mobile data plan).
|
||||||
|
*/
|
||||||
|
public final class DownloadOverMeteredDialog extends Activity {
|
||||||
|
final public static String CLIENT_ID_KEY = "client_id";
|
||||||
|
final public static String WORDLIST_TO_DOWNLOAD_KEY = "wordlist_to_download";
|
||||||
|
final public static String SIZE_KEY = "size";
|
||||||
|
final public static String LOCALE_KEY = "locale";
|
||||||
|
private String mClientId;
|
||||||
|
private String mWordListToDownload;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
final Intent intent = getIntent();
|
||||||
|
mClientId = intent.getStringExtra(CLIENT_ID_KEY);
|
||||||
|
mWordListToDownload = intent.getStringExtra(WORDLIST_TO_DOWNLOAD_KEY);
|
||||||
|
final String localeString = intent.getStringExtra(LOCALE_KEY);
|
||||||
|
final long size = intent.getIntExtra(SIZE_KEY, 0);
|
||||||
|
setContentView(R.layout.download_over_metered);
|
||||||
|
setTexts(localeString, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setTexts(@Nullable final String localeString, final long size) {
|
||||||
|
final String promptFormat = getString(R.string.should_download_over_metered_prompt);
|
||||||
|
final String allowButtonFormat = getString(R.string.download_over_metered);
|
||||||
|
final String language = (null == localeString) ? ""
|
||||||
|
: LocaleUtils.constructLocaleFromString(localeString).getDisplayLanguage();
|
||||||
|
final TextView prompt = (TextView)findViewById(R.id.download_over_metered_prompt);
|
||||||
|
prompt.setText(Html.fromHtml(String.format(promptFormat, language)));
|
||||||
|
final Button allowButton = (Button)findViewById(R.id.allow_button);
|
||||||
|
allowButton.setText(String.format(allowButtonFormat, ((float)size)/(1024*1024)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method is externally referenced from layout/download_over_metered.xml using onClick
|
||||||
|
// attribute of Button.
|
||||||
|
@ExternallyReferenced
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public void onClickDeny(final View v) {
|
||||||
|
UpdateHandler.setDownloadOverMeteredSetting(this, false);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method is externally referenced from layout/download_over_metered.xml using onClick
|
||||||
|
// attribute of Button.
|
||||||
|
@ExternallyReferenced
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public void onClickAllow(final View v) {
|
||||||
|
UpdateHandler.setDownloadOverMeteredSetting(this, true);
|
||||||
|
UpdateHandler.installIfNeverRequested(this, mClientId, mWordListToDownload);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Struct class to encapsulate a client ID with content values about a download.
|
||||||
|
*/
|
||||||
|
public class DownloadRecord {
|
||||||
|
public final String mClientId;
|
||||||
|
// Only word lists have attributes, and the ContentValues should contain the same
|
||||||
|
// keys as they do for all MetadataDbHelper functions. Since only word lists have
|
||||||
|
// attributes, a null pointer here means this record represents metadata.
|
||||||
|
public final ContentValues mAttributes;
|
||||||
|
public DownloadRecord(final String clientId, final ContentValues attributes) {
|
||||||
|
mClientId = clientId;
|
||||||
|
mAttributes = attributes;
|
||||||
|
}
|
||||||
|
public boolean isMetadata() {
|
||||||
|
return null == mAttributes;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
|
||||||
|
public final class EventHandler extends BroadcastReceiver {
|
||||||
|
/**
|
||||||
|
* Receives a intent broadcast.
|
||||||
|
*
|
||||||
|
* We receive every day a broadcast indicating that date changed.
|
||||||
|
* Then we wait a random amount of time before actually registering
|
||||||
|
* the download, to avoid concentrating too many accesses around
|
||||||
|
* midnight in more populated timezones.
|
||||||
|
* We receive all broadcasts here, so this can be either the DATE_CHANGED broadcast, the
|
||||||
|
* UPDATE_NOW private broadcast that we receive when the time-randomizing alarm triggers
|
||||||
|
* for regular update or from applications that want to test the dictionary pack, or a
|
||||||
|
* broadcast from DownloadManager telling that a download has finished.
|
||||||
|
* See inside of AndroidManifest.xml to see which events are caught.
|
||||||
|
* Also @see {@link BroadcastReceiver#onReceive(Context, Intent)}
|
||||||
|
*
|
||||||
|
* @param context the context of the application.
|
||||||
|
* @param intent the intent that was broadcast.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onReceive(final Context context, final Intent intent) {
|
||||||
|
intent.setClass(context, DictionaryService.class);
|
||||||
|
context.startService(intent);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A very simple problem reporter.
|
||||||
|
*/
|
||||||
|
final class LogProblemReporter implements ProblemReporter {
|
||||||
|
private final String TAG;
|
||||||
|
|
||||||
|
public LogProblemReporter(final String tag) {
|
||||||
|
TAG = tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void report(final Exception e) {
|
||||||
|
Log.e(TAG, "Reporting problem", e);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
/**
|
||||||
|
* Copyright (C) 2012 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
|
||||||
|
public final class MD5Calculator {
|
||||||
|
private MD5Calculator() {} // This helper class is not instantiable
|
||||||
|
|
||||||
|
public static String checksum(final InputStream in) throws IOException {
|
||||||
|
// This code from the Android documentation for MessageDigest. Nearly verbatim.
|
||||||
|
MessageDigest digester;
|
||||||
|
try {
|
||||||
|
digester = MessageDigest.getInstance("MD5");
|
||||||
|
} catch (java.security.NoSuchAlgorithmException e) {
|
||||||
|
return null; // Platform does not support MD5 : can't check, so return null
|
||||||
|
}
|
||||||
|
final byte[] bytes = new byte[8192];
|
||||||
|
int byteCount;
|
||||||
|
while ((byteCount = in.read(bytes)) > 0) {
|
||||||
|
digester.update(bytes, 0, byteCount);
|
||||||
|
}
|
||||||
|
final byte[] digest = digester.digest();
|
||||||
|
final StringBuilder s = new StringBuilder();
|
||||||
|
for (int i = 0; i < digest.length; ++i) {
|
||||||
|
s.append(String.format("%1$02x", digest[i]));
|
||||||
|
}
|
||||||
|
return s.toString();
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,173 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class to easy up manipulation of dictionary pack metadata.
|
||||||
|
*/
|
||||||
|
public class MetadataHandler {
|
||||||
|
|
||||||
|
public static final String TAG = MetadataHandler.class.getSimpleName();
|
||||||
|
|
||||||
|
// The canonical file name for metadata. This is not the name of a real file on the
|
||||||
|
// device, but a symbolic name used in the database and in metadata handling. It is never
|
||||||
|
// tested against, only used for human-readability as the file name for the metadata.
|
||||||
|
public static final String METADATA_FILENAME = "metadata.json";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the data from the cursor and store it in metadata objects.
|
||||||
|
* @param results the cursor to read data from.
|
||||||
|
* @return the constructed list of wordlist metadata.
|
||||||
|
*/
|
||||||
|
private static List<WordListMetadata> makeMetadataObject(final Cursor results) {
|
||||||
|
final ArrayList<WordListMetadata> buildingMetadata = new ArrayList<>();
|
||||||
|
if (null != results && results.moveToFirst()) {
|
||||||
|
final int localeColumn = results.getColumnIndex(MetadataDbHelper.LOCALE_COLUMN);
|
||||||
|
final int typeColumn = results.getColumnIndex(MetadataDbHelper.TYPE_COLUMN);
|
||||||
|
final int descriptionColumn =
|
||||||
|
results.getColumnIndex(MetadataDbHelper.DESCRIPTION_COLUMN);
|
||||||
|
final int idIndex = results.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN);
|
||||||
|
final int updateIndex = results.getColumnIndex(MetadataDbHelper.DATE_COLUMN);
|
||||||
|
final int fileSizeIndex = results.getColumnIndex(MetadataDbHelper.FILESIZE_COLUMN);
|
||||||
|
final int rawChecksumIndex =
|
||||||
|
results.getColumnIndex(MetadataDbHelper.RAW_CHECKSUM_COLUMN);
|
||||||
|
final int checksumIndex = results.getColumnIndex(MetadataDbHelper.CHECKSUM_COLUMN);
|
||||||
|
final int retryCountIndex = results.getColumnIndex(MetadataDbHelper.RETRY_COUNT_COLUMN);
|
||||||
|
final int localFilenameIndex =
|
||||||
|
results.getColumnIndex(MetadataDbHelper.LOCAL_FILENAME_COLUMN);
|
||||||
|
final int remoteFilenameIndex =
|
||||||
|
results.getColumnIndex(MetadataDbHelper.REMOTE_FILENAME_COLUMN);
|
||||||
|
final int versionIndex = results.getColumnIndex(MetadataDbHelper.VERSION_COLUMN);
|
||||||
|
final int formatVersionIndex =
|
||||||
|
results.getColumnIndex(MetadataDbHelper.FORMATVERSION_COLUMN);
|
||||||
|
do {
|
||||||
|
buildingMetadata.add(new WordListMetadata(results.getString(idIndex),
|
||||||
|
results.getInt(typeColumn),
|
||||||
|
results.getString(descriptionColumn),
|
||||||
|
results.getLong(updateIndex),
|
||||||
|
results.getLong(fileSizeIndex),
|
||||||
|
results.getString(rawChecksumIndex),
|
||||||
|
results.getString(checksumIndex),
|
||||||
|
results.getInt(retryCountIndex),
|
||||||
|
results.getString(localFilenameIndex),
|
||||||
|
results.getString(remoteFilenameIndex),
|
||||||
|
results.getInt(versionIndex),
|
||||||
|
results.getInt(formatVersionIndex),
|
||||||
|
0, results.getString(localeColumn)));
|
||||||
|
} while (results.moveToNext());
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableList(buildingMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the whole metadata, for installed and not installed dictionaries.
|
||||||
|
* @param context The context to open files over.
|
||||||
|
* @param clientId the client id for retrieving the database. null for default (deprecated)
|
||||||
|
* @return The current metadata.
|
||||||
|
*/
|
||||||
|
public static List<WordListMetadata> getCurrentMetadata(final Context context,
|
||||||
|
final String clientId) {
|
||||||
|
// If clientId is null, we get a cursor on the default database (see
|
||||||
|
// MetadataDbHelper#getInstance() for more on this)
|
||||||
|
final Cursor results = MetadataDbHelper.queryCurrentMetadata(context, clientId);
|
||||||
|
// If null, we should return makeMetadataObject(null), so we go through.
|
||||||
|
try {
|
||||||
|
return makeMetadataObject(results);
|
||||||
|
} finally {
|
||||||
|
if (null != results) {
|
||||||
|
results.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the metadata, for a specific dictionary.
|
||||||
|
*
|
||||||
|
* @param context The context to open files over.
|
||||||
|
* @param clientId the client id for retrieving the database. null for default (deprecated).
|
||||||
|
* @param wordListId the word list ID.
|
||||||
|
* @param version the word list version.
|
||||||
|
* @return the current metaData
|
||||||
|
*/
|
||||||
|
public static WordListMetadata getCurrentMetadataForWordList(final Context context,
|
||||||
|
final String clientId, final String wordListId, final int version) {
|
||||||
|
final ContentValues contentValues = MetadataDbHelper.getContentValuesByWordListId(
|
||||||
|
MetadataDbHelper.getDb(context, clientId), wordListId, version);
|
||||||
|
if (contentValues == null) {
|
||||||
|
// TODO: Figure out why this would happen.
|
||||||
|
// Check if this happens when the metadata gets updated in the background.
|
||||||
|
Log.e(TAG, String.format( "Unable to find the current metadata for wordlist "
|
||||||
|
+ "(clientId=%s, wordListId=%s, version=%d) on the database",
|
||||||
|
clientId, wordListId, version));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return WordListMetadata.createFromContentValues(contentValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read metadata from a stream.
|
||||||
|
* @param input The stream to read from.
|
||||||
|
* @return The read metadata.
|
||||||
|
* @throws IOException if the input stream cannot be read
|
||||||
|
* @throws BadFormatException if the stream is not in a known format
|
||||||
|
*/
|
||||||
|
public static List<WordListMetadata> readMetadata(final InputStreamReader input)
|
||||||
|
throws IOException, BadFormatException {
|
||||||
|
return MetadataParser.parseMetadata(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a single WordListMetadata inside a whole metadata chunk.
|
||||||
|
*
|
||||||
|
* Searches through the whole passed metadata for the first WordListMetadata associated
|
||||||
|
* with the passed ID. If several metadata chunks with the same id are found, it will
|
||||||
|
* always return the one with the bigger FormatVersion that is less or equal than the
|
||||||
|
* maximum supported format version (as listed in UpdateHandler).
|
||||||
|
* This will NEVER return the metadata with a FormatVersion bigger than what is supported,
|
||||||
|
* even if it is the only word list with this ID.
|
||||||
|
*
|
||||||
|
* @param metadata the metadata to search into.
|
||||||
|
* @param id the word list ID of the metadata to find.
|
||||||
|
* @return the associated metadata, or null if not found.
|
||||||
|
*/
|
||||||
|
public static WordListMetadata findWordListById(final List<WordListMetadata> metadata,
|
||||||
|
final String id) {
|
||||||
|
WordListMetadata bestWordList = null;
|
||||||
|
int bestFormatVersion = Integer.MIN_VALUE; // To be sure we can't be inadvertently smaller
|
||||||
|
for (WordListMetadata wordList : metadata) {
|
||||||
|
if (id.equals(wordList.mId)
|
||||||
|
&& wordList.mFormatVersion <= UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION
|
||||||
|
&& wordList.mFormatVersion > bestFormatVersion) {
|
||||||
|
bestWordList = wordList;
|
||||||
|
bestFormatVersion = wordList.mFormatVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we didn't find any match we'll return null.
|
||||||
|
return bestWordList;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2011 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.JsonReader;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class containing functions to parse the dictionary metadata.
|
||||||
|
*/
|
||||||
|
public class MetadataParser {
|
||||||
|
|
||||||
|
// Name of the fields in the JSON-formatted file.
|
||||||
|
private static final String ID_FIELD_NAME = MetadataDbHelper.WORDLISTID_COLUMN;
|
||||||
|
private static final String LOCALE_FIELD_NAME = "locale";
|
||||||
|
private static final String DESCRIPTION_FIELD_NAME = MetadataDbHelper.DESCRIPTION_COLUMN;
|
||||||
|
private static final String UPDATE_FIELD_NAME = "update";
|
||||||
|
private static final String FILESIZE_FIELD_NAME = MetadataDbHelper.FILESIZE_COLUMN;
|
||||||
|
private static final String RAW_CHECKSUM_FIELD_NAME = MetadataDbHelper.RAW_CHECKSUM_COLUMN;
|
||||||
|
private static final String CHECKSUM_FIELD_NAME = MetadataDbHelper.CHECKSUM_COLUMN;
|
||||||
|
private static final String REMOTE_FILENAME_FIELD_NAME =
|
||||||
|
MetadataDbHelper.REMOTE_FILENAME_COLUMN;
|
||||||
|
private static final String VERSION_FIELD_NAME = MetadataDbHelper.VERSION_COLUMN;
|
||||||
|
private static final String FORMATVERSION_FIELD_NAME = MetadataDbHelper.FORMATVERSION_COLUMN;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse one JSON-formatted word list metadata.
|
||||||
|
* @param reader the reader containing the data.
|
||||||
|
* @return a WordListMetadata object from the parsed data.
|
||||||
|
* @throws IOException if the underlying reader throws IOException during reading.
|
||||||
|
*/
|
||||||
|
private static WordListMetadata parseOneWordList(final JsonReader reader)
|
||||||
|
throws IOException, BadFormatException {
|
||||||
|
final TreeMap<String, String> arguments = new TreeMap<>();
|
||||||
|
reader.beginObject();
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
final String name = reader.nextName();
|
||||||
|
if (!TextUtils.isEmpty(name)) {
|
||||||
|
arguments.put(name, reader.nextString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.endObject();
|
||||||
|
if (TextUtils.isEmpty(arguments.get(ID_FIELD_NAME))
|
||||||
|
|| TextUtils.isEmpty(arguments.get(LOCALE_FIELD_NAME))
|
||||||
|
|| TextUtils.isEmpty(arguments.get(DESCRIPTION_FIELD_NAME))
|
||||||
|
|| TextUtils.isEmpty(arguments.get(UPDATE_FIELD_NAME))
|
||||||
|
|| TextUtils.isEmpty(arguments.get(FILESIZE_FIELD_NAME))
|
||||||
|
|| TextUtils.isEmpty(arguments.get(CHECKSUM_FIELD_NAME))
|
||||||
|
|| TextUtils.isEmpty(arguments.get(REMOTE_FILENAME_FIELD_NAME))
|
||||||
|
|| TextUtils.isEmpty(arguments.get(VERSION_FIELD_NAME))
|
||||||
|
|| TextUtils.isEmpty(arguments.get(FORMATVERSION_FIELD_NAME))) {
|
||||||
|
throw new BadFormatException(arguments.toString());
|
||||||
|
}
|
||||||
|
// TODO: need to find out whether it's bulk or update
|
||||||
|
// The null argument is the local file name, which is not known at this time and will
|
||||||
|
// be decided later.
|
||||||
|
return new WordListMetadata(
|
||||||
|
arguments.get(ID_FIELD_NAME),
|
||||||
|
MetadataDbHelper.TYPE_BULK,
|
||||||
|
arguments.get(DESCRIPTION_FIELD_NAME),
|
||||||
|
Long.parseLong(arguments.get(UPDATE_FIELD_NAME)),
|
||||||
|
Long.parseLong(arguments.get(FILESIZE_FIELD_NAME)),
|
||||||
|
arguments.get(RAW_CHECKSUM_FIELD_NAME),
|
||||||
|
arguments.get(CHECKSUM_FIELD_NAME),
|
||||||
|
MetadataDbHelper.DICTIONARY_RETRY_THRESHOLD /* retryCount */,
|
||||||
|
null,
|
||||||
|
arguments.get(REMOTE_FILENAME_FIELD_NAME),
|
||||||
|
Integer.parseInt(arguments.get(VERSION_FIELD_NAME)),
|
||||||
|
Integer.parseInt(arguments.get(FORMATVERSION_FIELD_NAME)),
|
||||||
|
0, arguments.get(LOCALE_FIELD_NAME));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses metadata in the JSON format.
|
||||||
|
* @param input a stream reader expected to contain JSON formatted metadata.
|
||||||
|
* @return dictionary metadata, as an array of WordListMetadata objects.
|
||||||
|
* @throws IOException if the underlying reader throws IOException during reading.
|
||||||
|
* @throws BadFormatException if the data was not in the expected format.
|
||||||
|
*/
|
||||||
|
public static List<WordListMetadata> parseMetadata(final InputStreamReader input)
|
||||||
|
throws IOException, BadFormatException {
|
||||||
|
JsonReader reader = new JsonReader(input);
|
||||||
|
final ArrayList<WordListMetadata> readInfo = new ArrayList<>();
|
||||||
|
reader.beginArray();
|
||||||
|
while (reader.hasNext()) {
|
||||||
|
final WordListMetadata thisMetadata = parseOneWordList(reader);
|
||||||
|
if (!TextUtils.isEmpty(thisMetadata.mLocale))
|
||||||
|
readInfo.add(thisMetadata);
|
||||||
|
}
|
||||||
|
return Collections.unmodifiableList(readInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2013 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.dslul.openboard.inputmethod.dictionarypack;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to get the metadata URI from its base URI.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class MetadataUriGetter {
|
||||||
|
public static String getUri(final Context context, final String baseUri) {
|
||||||
|
return baseUri;
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue