commit 797ee4f5c616490e9fde04de09bbde0d55651172 Author: dslul Date: Tue Dec 31 18:19:35 2019 +0100 first commit diff --git a/.gitignore b/.gitignore new file mode 100755 index 00000000..e14db7f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.iml +.idea +.gradle +local.properties +.DS_Store +build +app/build +app/release +app/.cxx diff --git a/app/build.gradle b/app/build.gradle new file mode 100755 index 00000000..946539e2 --- /dev/null +++ b/app/build.gradle @@ -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' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100755 index 00000000..f7bfc889 --- /dev/null +++ b/app/proguard-rules.pro @@ -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 *; +#} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..0bf7c151 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/dictionaries/bg_wordlist.combined.gz b/app/src/main/dictionaries/bg_wordlist.combined.gz new file mode 100755 index 00000000..f851b945 Binary files /dev/null and b/app/src/main/dictionaries/bg_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/cs_wordlist.combined.gz b/app/src/main/dictionaries/cs_wordlist.combined.gz new file mode 100755 index 00000000..94ba863a Binary files /dev/null and b/app/src/main/dictionaries/cs_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/da_wordlist.combined.gz b/app/src/main/dictionaries/da_wordlist.combined.gz new file mode 100755 index 00000000..b4baf627 Binary files /dev/null and b/app/src/main/dictionaries/da_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/de_wordlist.combined.gz b/app/src/main/dictionaries/de_wordlist.combined.gz new file mode 100755 index 00000000..92c95540 Binary files /dev/null and b/app/src/main/dictionaries/de_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/el_wordlist.combined.gz b/app/src/main/dictionaries/el_wordlist.combined.gz new file mode 100755 index 00000000..599734cf Binary files /dev/null and b/app/src/main/dictionaries/el_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/en_AU_wordlist.combined.gz b/app/src/main/dictionaries/en_AU_wordlist.combined.gz new file mode 100755 index 00000000..e08ff37b Binary files /dev/null and b/app/src/main/dictionaries/en_AU_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/en_GB_wordlist.combined.gz b/app/src/main/dictionaries/en_GB_wordlist.combined.gz new file mode 100755 index 00000000..217660fc Binary files /dev/null and b/app/src/main/dictionaries/en_GB_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/en_US_wordlist.combined.gz b/app/src/main/dictionaries/en_US_wordlist.combined.gz new file mode 100755 index 00000000..8aed9c5e Binary files /dev/null and b/app/src/main/dictionaries/en_US_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/en_emoji.combined.gz b/app/src/main/dictionaries/en_emoji.combined.gz new file mode 100755 index 00000000..4d9cf1b5 Binary files /dev/null and b/app/src/main/dictionaries/en_emoji.combined.gz differ diff --git a/app/src/main/dictionaries/en_wordlist.combined.gz b/app/src/main/dictionaries/en_wordlist.combined.gz new file mode 100755 index 00000000..7fe6618c Binary files /dev/null and b/app/src/main/dictionaries/en_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/es_wordlist.combined.gz b/app/src/main/dictionaries/es_wordlist.combined.gz new file mode 100755 index 00000000..71e7309f Binary files /dev/null and b/app/src/main/dictionaries/es_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/fi_wordlist.combined.gz b/app/src/main/dictionaries/fi_wordlist.combined.gz new file mode 100755 index 00000000..b7332ad3 Binary files /dev/null and b/app/src/main/dictionaries/fi_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/fr_emoji.combined.gz b/app/src/main/dictionaries/fr_emoji.combined.gz new file mode 100755 index 00000000..5c9c7a09 Binary files /dev/null and b/app/src/main/dictionaries/fr_emoji.combined.gz differ diff --git a/app/src/main/dictionaries/fr_wordlist.combined.gz b/app/src/main/dictionaries/fr_wordlist.combined.gz new file mode 100755 index 00000000..afe44a6d Binary files /dev/null and b/app/src/main/dictionaries/fr_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/hr_wordlist.combined.gz b/app/src/main/dictionaries/hr_wordlist.combined.gz new file mode 100755 index 00000000..9a2086f1 Binary files /dev/null and b/app/src/main/dictionaries/hr_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/it_wordlist.combined.gz b/app/src/main/dictionaries/it_wordlist.combined.gz new file mode 100755 index 00000000..ed58a12c Binary files /dev/null and b/app/src/main/dictionaries/it_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/iw_wordlist.combined.gz b/app/src/main/dictionaries/iw_wordlist.combined.gz new file mode 100755 index 00000000..13eab9f1 Binary files /dev/null and b/app/src/main/dictionaries/iw_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/ka_wordlist.combined.gz b/app/src/main/dictionaries/ka_wordlist.combined.gz new file mode 100755 index 00000000..02bacba3 Binary files /dev/null and b/app/src/main/dictionaries/ka_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/lb_wordlist.combined.gz b/app/src/main/dictionaries/lb_wordlist.combined.gz new file mode 100755 index 00000000..9e0579b3 Binary files /dev/null and b/app/src/main/dictionaries/lb_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/lt_wordlist.combined.gz b/app/src/main/dictionaries/lt_wordlist.combined.gz new file mode 100755 index 00000000..961266bb Binary files /dev/null and b/app/src/main/dictionaries/lt_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/lv_wordlist.combined.gz b/app/src/main/dictionaries/lv_wordlist.combined.gz new file mode 100755 index 00000000..ae906a9d Binary files /dev/null and b/app/src/main/dictionaries/lv_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/nb_wordlist.combined.gz b/app/src/main/dictionaries/nb_wordlist.combined.gz new file mode 100755 index 00000000..1c0f2cfb Binary files /dev/null and b/app/src/main/dictionaries/nb_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/nl_wordlist.combined.gz b/app/src/main/dictionaries/nl_wordlist.combined.gz new file mode 100755 index 00000000..19c3a7ea Binary files /dev/null and b/app/src/main/dictionaries/nl_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/pl_wordlist.combined.gz b/app/src/main/dictionaries/pl_wordlist.combined.gz new file mode 100755 index 00000000..2b84eecf Binary files /dev/null and b/app/src/main/dictionaries/pl_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/pt_BR_wordlist.combined.gz b/app/src/main/dictionaries/pt_BR_wordlist.combined.gz new file mode 100755 index 00000000..7aac61e5 Binary files /dev/null and b/app/src/main/dictionaries/pt_BR_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/pt_PT_wordlist.combined.gz b/app/src/main/dictionaries/pt_PT_wordlist.combined.gz new file mode 100755 index 00000000..5bf9a60e Binary files /dev/null and b/app/src/main/dictionaries/pt_PT_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/ro_wordlist.combined.gz b/app/src/main/dictionaries/ro_wordlist.combined.gz new file mode 100755 index 00000000..53078553 Binary files /dev/null and b/app/src/main/dictionaries/ro_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/ru_wordlist.combined.gz b/app/src/main/dictionaries/ru_wordlist.combined.gz new file mode 100755 index 00000000..5e926622 Binary files /dev/null and b/app/src/main/dictionaries/ru_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/sample.combined b/app/src/main/dictionaries/sample.combined new file mode 100755 index 00000000..4fa595e1 --- /dev/null +++ b/app/src/main/dictionaries/sample.combined @@ -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 diff --git a/app/src/main/dictionaries/sl_wordlist.combined.gz b/app/src/main/dictionaries/sl_wordlist.combined.gz new file mode 100755 index 00000000..a7240fe5 Binary files /dev/null and b/app/src/main/dictionaries/sl_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/sr_wordlist.combined.gz b/app/src/main/dictionaries/sr_wordlist.combined.gz new file mode 100755 index 00000000..30ce9967 Binary files /dev/null and b/app/src/main/dictionaries/sr_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/sv_wordlist.combined.gz b/app/src/main/dictionaries/sv_wordlist.combined.gz new file mode 100755 index 00000000..db44ae4c Binary files /dev/null and b/app/src/main/dictionaries/sv_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/tr_wordlist.combined.gz b/app/src/main/dictionaries/tr_wordlist.combined.gz new file mode 100755 index 00000000..d3c8825b Binary files /dev/null and b/app/src/main/dictionaries/tr_wordlist.combined.gz differ diff --git a/app/src/main/dictionaries/uk_wordlist.combined.gz b/app/src/main/dictionaries/uk_wordlist.combined.gz new file mode 100755 index 00000000..ed8420d7 Binary files /dev/null and b/app/src/main/dictionaries/uk_wordlist.combined.gz differ diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png new file mode 100644 index 00000000..04126598 Binary files /dev/null and b/app/src/main/ic_launcher-web.png differ diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/AccessibilityLongPressTimer.java b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/AccessibilityLongPressTimer.java new file mode 100644 index 00000000..3843b26b --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/AccessibilityLongPressTimer.java @@ -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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/AccessibilityUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/AccessibilityUtils.java new file mode 100644 index 00000000..1fe68501 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/AccessibilityUtils.java @@ -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); + } + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyCodeDescriptionMapper.java new file mode 100644 index 00000000..903e073f --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyCodeDescriptionMapper.java @@ -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 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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityDelegate.java b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityDelegate.java new file mode 100644 index 00000000..57b83154 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityDelegate.java @@ -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:

+ * - Call {@link #setKeyboard(Keyboard)} when a new keyboard is set to the keyboard view. + * - Dispatch a hover event by calling {@link #onHoverEnter(MotionEvent)}. + * + * @param The keyboard view class type. + */ +public class KeyboardAccessibilityDelegate + 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 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. + *

+ * Note: 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 getAccessibilityNodeProvider(final View host) { + return getAccessibilityNodeProvider(); + } + + /** + * @return A lazily-instantiated node provider for this view delegate. + */ + protected KeyboardAccessibilityNodeProvider 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 event 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 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 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. + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.java b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.java new file mode 100644 index 00000000..138d1179 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.java @@ -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. + *

+ * 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. + *

+ */ +final class KeyboardAccessibilityNodeProvider + 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 mDelegate; + + /** The current keyboard. */ + private Keyboard mKeyboard; + + public KeyboardAccessibilityNodeProvider(final KV keyboardView, + final KeyboardAccessibilityDelegate 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 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 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 virtualViewId or + * the host View itself if virtualViewId equals to {@link View#NO_ID}. + *

+ * 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. + *

+ *

+ * 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. + *

+ * + * @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 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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java new file mode 100644 index 00000000..911f8a64 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java @@ -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 + 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); + } + } + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/MoreKeysKeyboardAccessibilityDelegate.java b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/MoreKeysKeyboardAccessibilityDelegate.java new file mode 100644 index 00000000..ec98f54d --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/accessibility/MoreKeysKeyboardAccessibilityDelegate.java @@ -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 { + 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(); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/annotations/ExternallyReferenced.java b/app/src/main/java/org/dslul/openboard/inputmethod/annotations/ExternallyReferenced.java new file mode 100644 index 00000000..b08ab46d --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/annotations/ExternallyReferenced.java @@ -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 { +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/annotations/UsedForTesting.java b/app/src/main/java/org/dslul/openboard/inputmethod/annotations/UsedForTesting.java new file mode 100644 index 00000000..f586d56e --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/annotations/UsedForTesting.java @@ -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 { +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/ActivityManagerCompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/ActivityManagerCompatUtils.java new file mode 100644 index 00000000..d9e9869a --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/ActivityManagerCompatUtils.java @@ -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; + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/AppWorkaroundsHelper.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/AppWorkaroundsHelper.java new file mode 100644 index 00000000..b84eb3d1 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/AppWorkaroundsHelper.java @@ -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; + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/AppWorkaroundsUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/AppWorkaroundsUtils.java new file mode 100644 index 00000000..ec9c5047 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/AppWorkaroundsUtils.java @@ -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(); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/BuildCompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/BuildCompatUtils.java new file mode 100644 index 00000000..b0b014aa --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/BuildCompatUtils.java @@ -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; +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/CharacterCompat.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/CharacterCompat.java new file mode 100644 index 00000000..69d7c097 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/CharacterCompat.java @@ -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; + } + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/CompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/CompatUtils.java new file mode 100644 index 00000000..729ebb51 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/CompatUtils.java @@ -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 ToObjectMethodWrapper 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 { + 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); + } + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/ConnectivityManagerCompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/ConnectivityManagerCompatUtils.java new file mode 100644 index 00000000..59b871f7 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/ConnectivityManagerCompatUtils.java @@ -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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/CursorAnchorInfoCompatWrapper.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/CursorAnchorInfoCompatWrapper.java new file mode 100644 index 00000000..69609a6e --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/CursorAnchorInfoCompatWrapper.java @@ -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(); + } + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/EditorInfoCompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/EditorInfoCompatUtils.java new file mode 100644 index 00000000..0696ce59 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/EditorInfoCompatUtils.java @@ -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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/InputConnectionCompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/InputConnectionCompatUtils.java new file mode 100644 index 00000000..4701d6dd --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/InputConnectionCompatUtils.java @@ -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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/InputMethodManagerCompatWrapper.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/InputMethodManagerCompatWrapper.java new file mode 100644 index 00000000..7f39fc1a --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/InputMethodManagerCompatWrapper.java @@ -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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/InputMethodServiceCompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/InputMethodServiceCompatUtils.java new file mode 100644 index 00000000..c46b2840 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/InputMethodServiceCompatUtils.java @@ -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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/InputMethodSubtypeCompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/InputMethodSubtypeCompatUtils.java new file mode 100644 index 00000000..8331fa6e --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/InputMethodSubtypeCompatUtils.java @@ -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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/IntentCompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/IntentCompatUtils.java new file mode 100644 index 00000000..f4ddfaa4 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/IntentCompatUtils.java @@ -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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/LocaleListCompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/LocaleListCompatUtils.java new file mode 100644 index 00000000..ab125416 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/LocaleListCompatUtils.java @@ -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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/LocaleSpanCompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/LocaleSpanCompatUtils.java new file mode 100644 index 00000000..8ab02e7f --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/LocaleSpanCompatUtils.java @@ -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 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; + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/LooperCompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/LooperCompatUtils.java new file mode 100644 index 00000000..817cfc29 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/LooperCompatUtils.java @@ -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(); + } + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/NotificationCompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/NotificationCompatUtils.java new file mode 100644 index 00000000..ba899e24 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/NotificationCompatUtils.java @@ -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(); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/SettingsSecureCompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/SettingsSecureCompatUtils.java new file mode 100644 index 00000000..16094ffb --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/SettingsSecureCompatUtils.java @@ -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); +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/SuggestionSpanUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/SuggestionSpanUtils.java new file mode 100644 index 00000000..865fbf9a --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/SuggestionSpanUtils.java @@ -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 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; + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/SuggestionsInfoCompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/SuggestionsInfoCompatUtils.java new file mode 100644 index 00000000..7fad5b10 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/SuggestionsInfoCompatUtils.java @@ -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; + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/TextInfoCompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/TextInfoCompatUtils.java new file mode 100644 index 00000000..f8d0f71e --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/TextInfoCompatUtils.java @@ -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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/TextViewCompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/TextViewCompatUtils.java new file mode 100644 index 00000000..bf5590b0 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/TextViewCompatUtils.java @@ -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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/UserDictionaryCompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/UserDictionaryCompatUtils.java new file mode 100644 index 00000000..f1fbb92f --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/UserDictionaryCompatUtils.java @@ -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); + } +} + diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/UserManagerCompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/UserManagerCompatUtils.java new file mode 100644 index 00000000..20007961 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/UserManagerCompatUtils.java @@ -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; + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/ViewCompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/ViewCompatUtils.java new file mode 100644 index 00000000..719f39ab --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/ViewCompatUtils.java @@ -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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/ViewOutlineProviderCompatUtils.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/ViewOutlineProviderCompatUtils.java new file mode 100644 index 00000000..37afdc90 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/ViewOutlineProviderCompatUtils.java @@ -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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/compat/ViewOutlineProviderCompatUtilsLXX.java b/app/src/main/java/org/dslul/openboard/inputmethod/compat/ViewOutlineProviderCompatUtilsLXX.java new file mode 100644 index 00000000..a6339bad --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/compat/ViewOutlineProviderCompatUtilsLXX.java @@ -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()); + } + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ActionBatch.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ActionBatch.java new file mode 100644 index 00000000..a13f8814 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ActionBatch.java @@ -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 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 remainingActions = mActions; + while (!remainingActions.isEmpty()) { + final Action a = remainingActions.poll(); + try { + a.execute(context); + } catch (Exception e) { + if (null != reporter) + reporter.report(e); + } + } + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/AssetFileAddress.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/AssetFileAddress.java new file mode 100644 index 00000000..b64888c1 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/AssetFileAddress.java @@ -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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/BadFormatException.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/BadFormatException.java new file mode 100644 index 00000000..92acb7b0 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/BadFormatException.java @@ -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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ButtonSwitcher.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ButtonSwitcher.java new file mode 100644 index 00000000..b17e707a --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ButtonSwitcher.java @@ -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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/CommonPreferences.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/CommonPreferences.java new file mode 100644 index 00000000..2c02fce2 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/CommonPreferences.java @@ -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(); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/CompletedDownloadInfo.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/CompletedDownloadInfo.java new file mode 100644 index 00000000..9b4872a5 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/CompletedDownloadInfo.java @@ -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; + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java new file mode 100644 index 00000000..ef6c92eb --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryDownloadProgressBar.java @@ -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); + } + } + } + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryListInterfaceState.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryListInterfaceState.java new file mode 100644 index 00000000..8008c6f6 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryListInterfaceState.java @@ -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 mWordlistToState = new HashMap<>(); + private ArrayList 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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryPackConstants.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryPackConstants.java new file mode 100644 index 00000000..6e3883c2 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryPackConstants.java @@ -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"; +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryProvider.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryProvider.java new file mode 100644 index 00000000..609b3ac5 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryProvider.java @@ -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 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 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.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 getDictionaryWordListsForLocale(final String clientId, + final String locale) { + final Context context = getContext(); + final Cursor results = + MetadataDbHelper.queryInstalledOrDeletingOrAvailableDictionaryMetadata(context, + clientId); + if (null == results) { + return Collections.emptyList(); + } + try { + final HashMap 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"); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryService.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryService.java new file mode 100644 index 00000000..55e4484c --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionaryService.java @@ -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() /* 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(); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionarySettingsActivity.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionarySettingsActivity.java new file mode 100644 index 00000000..1d95c522 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionarySettingsActivity.java @@ -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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionarySettingsFragment.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionarySettingsFragment.java new file mode 100644 index 00000000..f6e5d7bf --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DictionarySettingsFragment.java @@ -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 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() { + @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 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 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 result = new ArrayList<>(); + result.add(createErrorMessage(activity, R.string.cannot_connect_to_dict_service)); + return result; + } + try { + if (!cursor.moveToFirst()) { + final ArrayList result = new ArrayList<>(); + result.add(createErrorMessage(activity, R.string.no_dictionaries_available)); + return result; + } + final String systemLocaleString = Locale.getDefault().toString(); + final TreeMap 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); + } + } + }); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadIdAndStartDate.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadIdAndStartDate.java new file mode 100644 index 00000000..bdc92148 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadIdAndStartDate.java @@ -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; + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadManagerWrapper.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadManagerWrapper.java new file mode 100644 index 00000000..75caff66 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadManagerWrapper.java @@ -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; + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadOverMeteredDialog.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadOverMeteredDialog.java new file mode 100644 index 00000000..a3e518df --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadOverMeteredDialog.java @@ -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(); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadRecord.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadRecord.java new file mode 100644 index 00000000..2c2a4a65 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/DownloadRecord.java @@ -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; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/EventHandler.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/EventHandler.java new file mode 100644 index 00000000..b329344a --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/EventHandler.java @@ -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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/LogProblemReporter.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/LogProblemReporter.java new file mode 100644 index 00000000..edcea630 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/LogProblemReporter.java @@ -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); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MD5Calculator.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MD5Calculator.java new file mode 100644 index 00000000..dcd6cb44 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MD5Calculator.java @@ -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(); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataDbHelper.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataDbHelper.java new file mode 100644 index 00000000..e4f6e8fd --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataDbHelper.java @@ -0,0 +1,1155 @@ +/* + * 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.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteException; +import android.database.sqlite.SQLiteOpenHelper; +import android.text.TextUtils; +import android.util.Log; + +import org.dslul.openboard.inputmethod.latin.R; +import org.dslul.openboard.inputmethod.latin.utils.DebugLogUtils; + +import java.io.File; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.TreeMap; + +import javax.annotation.Nullable; + +/** + * Various helper functions for the state database + */ +public class MetadataDbHelper extends SQLiteOpenHelper { + private static final String TAG = MetadataDbHelper.class.getSimpleName(); + + // This was the initial release version of the database. It should never be + // changed going forward. + private static final int METADATA_DATABASE_INITIAL_VERSION = 3; + // This is the first released version of the database that implements CLIENTID. It is + // used to identify the versions for upgrades. This should never change going forward. + private static final int METADATA_DATABASE_VERSION_WITH_CLIENTID = 6; + // The current database version. + // This MUST be increased every time the dictionary pack metadata URL changes. + private static final int CURRENT_METADATA_DATABASE_VERSION = 16; + + private final static long NOT_A_DOWNLOAD_ID = -1; + + // The number of retries allowed when attempting to download a broken dictionary. + public static final int DICTIONARY_RETRY_THRESHOLD = 2; + + public static final String METADATA_TABLE_NAME = "pendingUpdates"; + static final String CLIENT_TABLE_NAME = "clients"; + public static final String PENDINGID_COLUMN = "pendingid"; // Download Manager ID + public static final String TYPE_COLUMN = "type"; + public static final String STATUS_COLUMN = "status"; + public static final String LOCALE_COLUMN = "locale"; + public static final String WORDLISTID_COLUMN = "id"; + public static final String DESCRIPTION_COLUMN = "description"; + public static final String LOCAL_FILENAME_COLUMN = "filename"; + public static final String REMOTE_FILENAME_COLUMN = "url"; + public static final String DATE_COLUMN = "date"; + public static final String CHECKSUM_COLUMN = "checksum"; + public static final String FILESIZE_COLUMN = "filesize"; + public static final String VERSION_COLUMN = "version"; + public static final String FORMATVERSION_COLUMN = "formatversion"; + public static final String FLAGS_COLUMN = "flags"; + public static final String RAW_CHECKSUM_COLUMN = "rawChecksum"; + public static final String RETRY_COUNT_COLUMN = "remainingRetries"; + public static final int COLUMN_COUNT = 15; + + private static final String CLIENT_CLIENT_ID_COLUMN = "clientid"; + private static final String CLIENT_METADATA_URI_COLUMN = "uri"; + private static final String CLIENT_METADATA_ADDITIONAL_ID_COLUMN = "additionalid"; + private static final String CLIENT_LAST_UPDATE_DATE_COLUMN = "lastupdate"; + private static final String CLIENT_PENDINGID_COLUMN = "pendingid"; // Download Manager ID + + public static final String METADATA_DATABASE_NAME_STEM = "pendingUpdates"; + public static final String METADATA_UPDATE_DESCRIPTION = "metadata"; + + public static final String DICTIONARIES_ASSETS_PATH = "dictionaries"; + + // Statuses, for storing in the STATUS_COLUMN + // IMPORTANT: The following are used as index arrays in ../WordListPreference + // Do not change their values without updating the matched code. + // Unknown status: this should never happen. + public static final int STATUS_UNKNOWN = 0; + // Available: this word list is available, but it is not downloaded (not downloading), because + // it is set not to be used. + public static final int STATUS_AVAILABLE = 1; + // Downloading: this word list is being downloaded. + public static final int STATUS_DOWNLOADING = 2; + // Installed: this word list is installed and usable. + public static final int STATUS_INSTALLED = 3; + // Disabled: this word list is installed, but has been disabled by the user. + public static final int STATUS_DISABLED = 4; + // Deleting: the user marked this word list to be deleted, but it has not been yet because + // Latin IME is not up yet. + public static final int STATUS_DELETING = 5; + // Retry: dictionary got corrupted, so an attempt must be done to download & install it again. + public static final int STATUS_RETRYING = 6; + + // Types, for storing in the TYPE_COLUMN + // This is metadata about what is available. + public static final int TYPE_METADATA = 1; + // This is a bulk file. It should replace older files. + public static final int TYPE_BULK = 2; + // This is an incremental update, expected to be small, and meaningless on its own. + public static final int TYPE_UPDATE = 3; + + private static final String METADATA_TABLE_CREATE = + "CREATE TABLE " + METADATA_TABLE_NAME + " (" + + PENDINGID_COLUMN + " INTEGER, " + + TYPE_COLUMN + " INTEGER, " + + STATUS_COLUMN + " INTEGER, " + + WORDLISTID_COLUMN + " TEXT, " + + LOCALE_COLUMN + " TEXT, " + + DESCRIPTION_COLUMN + " TEXT, " + + LOCAL_FILENAME_COLUMN + " TEXT, " + + REMOTE_FILENAME_COLUMN + " TEXT, " + + DATE_COLUMN + " INTEGER, " + + CHECKSUM_COLUMN + " TEXT, " + + FILESIZE_COLUMN + " INTEGER, " + + VERSION_COLUMN + " INTEGER," + + FORMATVERSION_COLUMN + " INTEGER, " + + FLAGS_COLUMN + " INTEGER, " + + RAW_CHECKSUM_COLUMN + " TEXT," + + RETRY_COUNT_COLUMN + " INTEGER, " + + "PRIMARY KEY (" + WORDLISTID_COLUMN + "," + VERSION_COLUMN + "));"; + private static final String METADATA_CREATE_CLIENT_TABLE = + "CREATE TABLE IF NOT EXISTS " + CLIENT_TABLE_NAME + " (" + + CLIENT_CLIENT_ID_COLUMN + " TEXT, " + + CLIENT_METADATA_URI_COLUMN + " TEXT, " + + CLIENT_METADATA_ADDITIONAL_ID_COLUMN + " TEXT, " + + CLIENT_LAST_UPDATE_DATE_COLUMN + " INTEGER NOT NULL DEFAULT 0, " + + CLIENT_PENDINGID_COLUMN + " INTEGER, " + + FLAGS_COLUMN + " INTEGER, " + + "PRIMARY KEY (" + CLIENT_CLIENT_ID_COLUMN + "));"; + + // List of all metadata table columns. + static final String[] METADATA_TABLE_COLUMNS = { PENDINGID_COLUMN, TYPE_COLUMN, + STATUS_COLUMN, WORDLISTID_COLUMN, LOCALE_COLUMN, DESCRIPTION_COLUMN, + LOCAL_FILENAME_COLUMN, REMOTE_FILENAME_COLUMN, DATE_COLUMN, CHECKSUM_COLUMN, + FILESIZE_COLUMN, VERSION_COLUMN, FORMATVERSION_COLUMN, FLAGS_COLUMN, + RAW_CHECKSUM_COLUMN, RETRY_COUNT_COLUMN }; + // List of all client table columns. + static final String[] CLIENT_TABLE_COLUMNS = { CLIENT_CLIENT_ID_COLUMN, + CLIENT_METADATA_URI_COLUMN, CLIENT_PENDINGID_COLUMN, FLAGS_COLUMN }; + // List of public columns returned to clients. Everything that is not in this list is + // private and implementation-dependent. + static final String[] DICTIONARIES_LIST_PUBLIC_COLUMNS = { STATUS_COLUMN, WORDLISTID_COLUMN, + LOCALE_COLUMN, DESCRIPTION_COLUMN, DATE_COLUMN, FILESIZE_COLUMN, VERSION_COLUMN }; + + // This class exhibits a singleton-like behavior by client ID, so it is getInstance'd + // and has a private c'tor. + private static TreeMap sInstanceMap = null; + public static synchronized MetadataDbHelper getInstance(final Context context, + final String clientIdOrNull) { + // As a backward compatibility feature, null can be passed here to retrieve the "default" + // database. Before multi-client support, the dictionary packed used only one database + // and would not be able to handle several dictionary sets. Passing null here retrieves + // this legacy database. New clients should make sure to always pass a client ID so as + // to avoid conflicts. + final String clientId = null != clientIdOrNull ? clientIdOrNull : ""; + if (null == sInstanceMap) sInstanceMap = new TreeMap<>(); + MetadataDbHelper helper = sInstanceMap.get(clientId); + if (null == helper) { + helper = new MetadataDbHelper(context, clientId); + sInstanceMap.put(clientId, helper); + } + return helper; + } + private MetadataDbHelper(final Context context, final String clientId) { + super(context, + METADATA_DATABASE_NAME_STEM + (TextUtils.isEmpty(clientId) ? "" : "." + clientId), + null, CURRENT_METADATA_DATABASE_VERSION); + mContext = context; + mClientId = clientId; + } + + private final Context mContext; + private final String mClientId; + + /** + * Get the database itself. This always returns the same object for any client ID. If the + * client ID is null, a default database is returned for backward compatibility. Don't + * pass null for new calls. + * + * @param context the context to create the database from. This is ignored after the first call. + * @param clientId the client id to retrieve the database of. null for default (deprecated) + * @return the database. + */ + public static SQLiteDatabase getDb(final Context context, final String clientId) { + return getInstance(context, clientId).getWritableDatabase(); + } + + private void createClientTable(final SQLiteDatabase db) { + // The clients table only exists in the primary db, the one that has an empty client id + if (!TextUtils.isEmpty(mClientId)) return; + db.execSQL(METADATA_CREATE_CLIENT_TABLE); + final String defaultMetadataUri = mContext.getString(R.string.default_metadata_uri); + if (!TextUtils.isEmpty(defaultMetadataUri)) { + final ContentValues defaultMetadataValues = new ContentValues(); + defaultMetadataValues.put(CLIENT_CLIENT_ID_COLUMN, ""); + defaultMetadataValues.put(CLIENT_METADATA_URI_COLUMN, defaultMetadataUri); + defaultMetadataValues.put(CLIENT_PENDINGID_COLUMN, UpdateHandler.NOT_AN_ID); + db.insert(CLIENT_TABLE_NAME, null, defaultMetadataValues); + } + } + + /** + * Create the table and populate it with the resources found inside the apk. + * + * @see SQLiteOpenHelper#onCreate(SQLiteDatabase) + * + * @param db the database to create and populate. + */ + @Override + public void onCreate(final SQLiteDatabase db) { + db.execSQL(METADATA_TABLE_CREATE); + createClientTable(db); + } + + private static void addRawChecksumColumnUnlessPresent(final SQLiteDatabase db) { + try { + db.execSQL("SELECT " + RAW_CHECKSUM_COLUMN + " FROM " + + METADATA_TABLE_NAME + " LIMIT 0;"); + } catch (SQLiteException e) { + Log.i(TAG, "No " + RAW_CHECKSUM_COLUMN + " column : creating it"); + db.execSQL("ALTER TABLE " + METADATA_TABLE_NAME + " ADD COLUMN " + + RAW_CHECKSUM_COLUMN + " TEXT;"); + } + } + + private static void addRetryCountColumnUnlessPresent(final SQLiteDatabase db) { + try { + db.execSQL("SELECT " + RETRY_COUNT_COLUMN + " FROM " + + METADATA_TABLE_NAME + " LIMIT 0;"); + } catch (SQLiteException e) { + Log.i(TAG, "No " + RETRY_COUNT_COLUMN + " column : creating it"); + db.execSQL("ALTER TABLE " + METADATA_TABLE_NAME + " ADD COLUMN " + + RETRY_COUNT_COLUMN + " INTEGER DEFAULT " + DICTIONARY_RETRY_THRESHOLD + ";"); + } + } + + /** + * Upgrade the database. Upgrade from version 3 is supported. + * Version 3 has a DB named METADATA_DATABASE_NAME_STEM containing a table METADATA_TABLE_NAME. + * Version 6 and above has a DB named METADATA_DATABASE_NAME_STEM containing a + * table CLIENT_TABLE_NAME, and for each client a table called METADATA_TABLE_STEM + "." + the + * name of the client and contains a table METADATA_TABLE_NAME. + * For schemas, see the above create statements. The schemas have never changed so far. + * + * This method is called by the framework. See {@link SQLiteOpenHelper#onUpgrade} + * @param db The database we are upgrading + * @param oldVersion The old database version (the one on the disk) + * @param newVersion The new database version as supplied to the constructor of SQLiteOpenHelper + */ + @Override + public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { + if (METADATA_DATABASE_INITIAL_VERSION == oldVersion + && METADATA_DATABASE_VERSION_WITH_CLIENTID <= newVersion + && CURRENT_METADATA_DATABASE_VERSION >= newVersion) { + // Upgrade from version METADATA_DATABASE_INITIAL_VERSION to version + // METADATA_DATABASE_VERSION_WITH_CLIENT_ID + // Only the default database should contain the client table, so we test for mClientId. + if (TextUtils.isEmpty(mClientId)) { + // Anyway in version 3 only the default table existed so the emptiness + // test should always be true, but better check to be sure. + createClientTable(db); + } + } else if (METADATA_DATABASE_VERSION_WITH_CLIENTID < newVersion + && CURRENT_METADATA_DATABASE_VERSION >= newVersion) { + // Here we drop the client table, so that all clients send us their information again. + // The client table contains the URL to hit to update the available dictionaries list, + // but the info about the dictionaries themselves is stored in the table called + // METADATA_TABLE_NAME and we want to keep it, so we only drop the client table. + db.execSQL("DROP TABLE IF EXISTS " + CLIENT_TABLE_NAME); + // Only the default database should contain the client table, so we test for mClientId. + if (TextUtils.isEmpty(mClientId)) { + createClientTable(db); + } + } else { + // If we're not in the above case, either we are upgrading from an earlier versionCode + // and we should wipe the database, or we are handling a version we never heard about + // (can only be a bug) so it's safer to wipe the database. + db.execSQL("DROP TABLE IF EXISTS " + METADATA_TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + CLIENT_TABLE_NAME); + onCreate(db); + } + // A rawChecksum column that did not exist in the previous versions was added that + // corresponds to the md5 checksum of the file after decompression/decryption. This is to + // strengthen the system against corrupted dictionary files. + // The most secure way to upgrade a database is to just test for the column presence, and + // add it if it's not there. + addRawChecksumColumnUnlessPresent(db); + + // A retry count column that did not exist in the previous versions was added that + // corresponds to the number of download & installation attempts that have been made + // in order to strengthen the system recovery from corrupted dictionary files. + // The most secure way to upgrade a database is to just test for the column presence, and + // add it if it's not there. + addRetryCountColumnUnlessPresent(db); + } + + /** + * Downgrade the database. This drops and recreates the table in all cases. + */ + @Override + public void onDowngrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { + // No matter what the numerical values of oldVersion and newVersion are, we know this + // is a downgrade (newVersion < oldVersion). There is no way to know what the future + // databases will look like, but we know it's extremely likely that it's okay to just + // drop the tables and start from scratch. Hence, we ignore the versions and just wipe + // everything we want to use. + if (oldVersion <= newVersion) { + Log.e(TAG, "onDowngrade database but new version is higher? " + oldVersion + " <= " + + newVersion); + } + db.execSQL("DROP TABLE IF EXISTS " + METADATA_TABLE_NAME); + db.execSQL("DROP TABLE IF EXISTS " + CLIENT_TABLE_NAME); + onCreate(db); + } + + /** + * Given a client ID, returns whether this client exists. + * + * @param context a context to open the database + * @param clientId the client ID to check + * @return true if the client is known, false otherwise + */ + public static boolean isClientKnown(final Context context, final String clientId) { + // If the client is known, they'll have a non-null metadata URI. An empty string is + // allowed as a metadata URI, if the client doesn't want any updates to happen. + return null != getMetadataUriAsString(context, clientId); + } + + private static final MetadataUriGetter sMetadataUriGetter = new MetadataUriGetter(); + + /** + * Returns the metadata URI as a string. + * + * If the client is not known, this will return null. If it is known, it will return + * the URI as a string. Note that the empty string is a valid value. + * + * @param context a context instance to open the database on + * @param clientId the ID of the client we want the metadata URI of + * @return the string representation of the URI + */ + public static String getMetadataUriAsString(final Context context, final String clientId) { + SQLiteDatabase defaultDb = MetadataDbHelper.getDb(context, null); + final Cursor cursor = defaultDb.query(MetadataDbHelper.CLIENT_TABLE_NAME, + new String[] { MetadataDbHelper.CLIENT_METADATA_URI_COLUMN }, + MetadataDbHelper.CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId }, + null, null, null, null); + try { + if (!cursor.moveToFirst()) return null; + return sMetadataUriGetter.getUri(context, cursor.getString(0)); + } finally { + cursor.close(); + } + } + + /** + * Update the last metadata update time for all clients using a particular URI. + * + * This method searches for all clients using a particular URI and updates the last + * update time for this client. + * The current time is used as the latest update time. This saved date will be what + * is returned henceforth by {@link #getLastUpdateDateForClient(Context, String)}, + * until this method is called again. + * + * @param context a context instance to open the database on + * @param uri the metadata URI we just downloaded + */ + public static void saveLastUpdateTimeOfUri(final Context context, final String uri) { + PrivateLog.log("Save last update time of URI : " + uri + " " + System.currentTimeMillis()); + final ContentValues values = new ContentValues(); + values.put(CLIENT_LAST_UPDATE_DATE_COLUMN, System.currentTimeMillis()); + final SQLiteDatabase defaultDb = getDb(context, null); + final Cursor cursor = MetadataDbHelper.queryClientIds(context); + if (null == cursor) return; + try { + if (!cursor.moveToFirst()) return; + do { + final String clientId = cursor.getString(0); + final String metadataUri = + MetadataDbHelper.getMetadataUriAsString(context, clientId); + if (metadataUri.equals(uri)) { + defaultDb.update(CLIENT_TABLE_NAME, values, + CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId }); + } + } while (cursor.moveToNext()); + } finally { + cursor.close(); + } + } + + /** + * Retrieves the last date at which we updated the metadata for this client. + * + * The returned date is in milliseconds from the EPOCH; this is the same unit as + * returned by {@link System#currentTimeMillis()}. + * + * @param context a context instance to open the database on + * @param clientId the client ID to get the latest update date of + * @return the last date at which this client was updated, as a long. + */ + public static long getLastUpdateDateForClient(final Context context, final String clientId) { + SQLiteDatabase defaultDb = getDb(context, null); + final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME, + new String[] { CLIENT_LAST_UPDATE_DATE_COLUMN }, + CLIENT_CLIENT_ID_COLUMN + " = ?", + new String[] { null == clientId ? "" : clientId }, + null, null, null, null); + try { + if (!cursor.moveToFirst()) return 0; + return cursor.getLong(0); // Only one column, return it + } finally { + cursor.close(); + } + } + + /** + * Get the metadata download ID for a metadata URI. + * + * This will retrieve the download ID for the metadata file that has the passed URI. + * If this URI is not being downloaded right now, it will return NOT_AN_ID. + * + * @param context a context instance to open the database on + * @param uri the URI to retrieve the metadata download ID of + * @return the download id and start date, or null if the URL is not known + */ + public static DownloadIdAndStartDate getMetadataDownloadIdAndStartDateForURI( + final Context context, final String uri) { + SQLiteDatabase defaultDb = getDb(context, null); + final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME, + new String[] { CLIENT_PENDINGID_COLUMN, CLIENT_LAST_UPDATE_DATE_COLUMN }, + CLIENT_METADATA_URI_COLUMN + " = ?", new String[] { uri }, + null, null, null, null); + try { + if (!cursor.moveToFirst()) return null; + return new DownloadIdAndStartDate(cursor.getInt(0), cursor.getLong(1)); + } finally { + cursor.close(); + } + } + + public static long getOldestUpdateTime(final Context context) { + SQLiteDatabase defaultDb = getDb(context, null); + final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME, + new String[] { CLIENT_LAST_UPDATE_DATE_COLUMN }, + null, null, null, null, null); + try { + if (!cursor.moveToFirst()) return 0; + final int columnIndex = 0; // Only one column queried + // Initialize the earliestTime to the largest possible value. + long earliestTime = Long.MAX_VALUE; // Almost 300 million years in the future + do { + final long thisTime = cursor.getLong(columnIndex); + earliestTime = Math.min(thisTime, earliestTime); + } while (cursor.moveToNext()); + return earliestTime; + } finally { + cursor.close(); + } + } + + /** + * Helper method to make content values to write into the database. + * @return content values with all the arguments put with the right column names. + */ + public static ContentValues makeContentValues(final int pendingId, final int type, + final int status, final String wordlistId, final String locale, + final String description, final String filename, final String url, final long date, + final String rawChecksum, final String checksum, final int retryCount, + final long filesize, final int version, final int formatVersion) { + final ContentValues result = new ContentValues(COLUMN_COUNT); + result.put(PENDINGID_COLUMN, pendingId); + result.put(TYPE_COLUMN, type); + result.put(WORDLISTID_COLUMN, wordlistId); + result.put(STATUS_COLUMN, status); + result.put(LOCALE_COLUMN, locale); + result.put(DESCRIPTION_COLUMN, description); + result.put(LOCAL_FILENAME_COLUMN, filename); + result.put(REMOTE_FILENAME_COLUMN, url); + result.put(DATE_COLUMN, date); + result.put(RAW_CHECKSUM_COLUMN, rawChecksum); + result.put(RETRY_COUNT_COLUMN, retryCount); + result.put(CHECKSUM_COLUMN, checksum); + result.put(FILESIZE_COLUMN, filesize); + result.put(VERSION_COLUMN, version); + result.put(FORMATVERSION_COLUMN, formatVersion); + result.put(FLAGS_COLUMN, 0); + return result; + } + + /** + * Helper method to fill in an incomplete ContentValues with default values. + * A wordlist ID and a locale are required, otherwise BadFormatException is thrown. + * @return the same object that was passed in, completed with default values. + */ + public static ContentValues completeWithDefaultValues(final ContentValues result) + throws BadFormatException { + if (null == result.get(WORDLISTID_COLUMN) || null == result.get(LOCALE_COLUMN)) { + throw new BadFormatException(); + } + // 0 for the pending id, because there is none + if (null == result.get(PENDINGID_COLUMN)) result.put(PENDINGID_COLUMN, 0); + // This is a binary blob of a dictionary + if (null == result.get(TYPE_COLUMN)) result.put(TYPE_COLUMN, TYPE_BULK); + // This word list is unknown, but it's present, else we wouldn't be here, so INSTALLED + if (null == result.get(STATUS_COLUMN)) result.put(STATUS_COLUMN, STATUS_INSTALLED); + // No description unless specified, because we can't guess it + if (null == result.get(DESCRIPTION_COLUMN)) result.put(DESCRIPTION_COLUMN, ""); + // File name - this is an asset, so it works as an already deleted file. + // hence, we need to supply a non-existent file name. Anything will + // do as long as it returns false when tested with File#exist(), and + // the empty string does not, so it's set to "_". + if (null == result.get(LOCAL_FILENAME_COLUMN)) result.put(LOCAL_FILENAME_COLUMN, "_"); + // No remote file name : this can't be downloaded. Unless specified. + if (null == result.get(REMOTE_FILENAME_COLUMN)) result.put(REMOTE_FILENAME_COLUMN, ""); + // 0 for the update date : 1970/1/1. Unless specified. + if (null == result.get(DATE_COLUMN)) result.put(DATE_COLUMN, 0); + // Raw checksum unknown unless specified + if (null == result.get(RAW_CHECKSUM_COLUMN)) result.put(RAW_CHECKSUM_COLUMN, ""); + // Retry column 0 unless specified + if (null == result.get(RETRY_COUNT_COLUMN)) result.put(RETRY_COUNT_COLUMN, + DICTIONARY_RETRY_THRESHOLD); + // Checksum unknown unless specified + if (null == result.get(CHECKSUM_COLUMN)) result.put(CHECKSUM_COLUMN, ""); + // No filesize unless specified + if (null == result.get(FILESIZE_COLUMN)) result.put(FILESIZE_COLUMN, 0); + // Smallest possible version unless specified + if (null == result.get(VERSION_COLUMN)) result.put(VERSION_COLUMN, 1); + // Assume current format unless specified + if (null == result.get(FORMATVERSION_COLUMN)) + result.put(FORMATVERSION_COLUMN, UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION); + // No flags unless specified + if (null == result.get(FLAGS_COLUMN)) result.put(FLAGS_COLUMN, 0); + return result; + } + + /** + * Reads a column in a Cursor as a String and stores it in a ContentValues object. + * @param result the ContentValues object to store the result in. + * @param cursor the Cursor to read the column from. + * @param columnId the column ID to read. + */ + private static void putStringResult(ContentValues result, Cursor cursor, String columnId) { + result.put(columnId, cursor.getString(cursor.getColumnIndex(columnId))); + } + + /** + * Reads a column in a Cursor as an int and stores it in a ContentValues object. + * @param result the ContentValues object to store the result in. + * @param cursor the Cursor to read the column from. + * @param columnId the column ID to read. + */ + private static void putIntResult(ContentValues result, Cursor cursor, String columnId) { + result.put(columnId, cursor.getInt(cursor.getColumnIndex(columnId))); + } + + private static ContentValues getFirstLineAsContentValues(final Cursor cursor) { + final ContentValues result; + if (cursor.moveToFirst()) { + result = new ContentValues(COLUMN_COUNT); + putIntResult(result, cursor, PENDINGID_COLUMN); + putIntResult(result, cursor, TYPE_COLUMN); + putIntResult(result, cursor, STATUS_COLUMN); + putStringResult(result, cursor, WORDLISTID_COLUMN); + putStringResult(result, cursor, LOCALE_COLUMN); + putStringResult(result, cursor, DESCRIPTION_COLUMN); + putStringResult(result, cursor, LOCAL_FILENAME_COLUMN); + putStringResult(result, cursor, REMOTE_FILENAME_COLUMN); + putIntResult(result, cursor, DATE_COLUMN); + putStringResult(result, cursor, RAW_CHECKSUM_COLUMN); + putStringResult(result, cursor, CHECKSUM_COLUMN); + putIntResult(result, cursor, RETRY_COUNT_COLUMN); + putIntResult(result, cursor, FILESIZE_COLUMN); + putIntResult(result, cursor, VERSION_COLUMN); + putIntResult(result, cursor, FORMATVERSION_COLUMN); + putIntResult(result, cursor, FLAGS_COLUMN); + if (cursor.moveToNext()) { + // TODO: print the second level of the stack to the log so that we know + // in which code path the error happened + Log.e(TAG, "Several SQL results when we expected only one!"); + } + } else { + result = null; + } + return result; + } + + /** + * Gets the info about as specific download, indexed by its DownloadManager ID. + * @param db the database to get the information from. + * @param id the DownloadManager id. + * @return metadata about this download. This returns all columns in the database. + */ + public static ContentValues getContentValuesByPendingId(final SQLiteDatabase db, + final long id) { + final Cursor cursor = db.query(METADATA_TABLE_NAME, + METADATA_TABLE_COLUMNS, + PENDINGID_COLUMN + "= ?", + new String[] { Long.toString(id) }, + null, null, null); + if (null == cursor) { + return null; + } + try { + // There should never be more than one result. If because of some bug there are, + // returning only one result is the right thing to do, because we couldn't handle + // several anyway and we should still handle one. + return getFirstLineAsContentValues(cursor); + } finally { + cursor.close(); + } + } + + /** + * Gets the info about an installed OR deleting word list with a specified id. + * + * Basically, this is the word list that we want to return to Android Keyboard when + * it asks for a specific id. + * + * @param db the database to get the information from. + * @param id the word list ID. + * @return the metadata about this word list. + */ + public static ContentValues getInstalledOrDeletingWordListContentValuesByWordListId( + final SQLiteDatabase db, final String id) { + final Cursor cursor = db.query(METADATA_TABLE_NAME, + METADATA_TABLE_COLUMNS, + WORDLISTID_COLUMN + "=? AND (" + STATUS_COLUMN + "=? OR " + STATUS_COLUMN + "=?)", + new String[] { id, Integer.toString(STATUS_INSTALLED), + Integer.toString(STATUS_DELETING) }, + null, null, null); + if (null == cursor) { + return null; + } + try { + // There should only be one result, but if there are several, we can't tell which + // is the best, so we just return the first one. + return getFirstLineAsContentValues(cursor); + } finally { + cursor.close(); + } + } + + /** + * Given a specific download ID, return records for all pending downloads across all clients. + * + * If several clients use the same metadata URL, we know to only download it once, and + * dispatch the update process across all relevant clients when the download ends. This means + * several clients may share a single download ID if they share a metadata URI. + * The dispatching is done in + * {@link UpdateHandler#downloadFinished(Context, android.content.Intent)}, which + * finds out about the list of relevant clients by calling this method. + * + * @param context a context instance to open the databases + * @param downloadId the download ID to query about + * @return the list of records. Never null, but may be empty. + */ + public static ArrayList getDownloadRecordsForDownloadId(final Context context, + final long downloadId) { + final SQLiteDatabase defaultDb = getDb(context, ""); + final ArrayList results = new ArrayList<>(); + final Cursor cursor = defaultDb.query(CLIENT_TABLE_NAME, CLIENT_TABLE_COLUMNS, + null, null, null, null, null); + try { + if (!cursor.moveToFirst()) return results; + final int clientIdIndex = cursor.getColumnIndex(CLIENT_CLIENT_ID_COLUMN); + final int pendingIdColumn = cursor.getColumnIndex(CLIENT_PENDINGID_COLUMN); + do { + final long pendingId = cursor.getInt(pendingIdColumn); + final String clientId = cursor.getString(clientIdIndex); + if (pendingId == downloadId) { + results.add(new DownloadRecord(clientId, null)); + } + final ContentValues valuesForThisClient = + getContentValuesByPendingId(getDb(context, clientId), downloadId); + if (null != valuesForThisClient) { + results.add(new DownloadRecord(clientId, valuesForThisClient)); + } + } while (cursor.moveToNext()); + } finally { + cursor.close(); + } + return results; + } + + /** + * Gets the info about a specific word list. + * + * @param db the database to get the information from. + * @param id the word list ID. + * @param version the word list version. + * @return the metadata about this word list. + */ + @Nullable + public static ContentValues getContentValuesByWordListId(final SQLiteDatabase db, + final String id, final int version) { + final Cursor cursor = db.query(METADATA_TABLE_NAME, + METADATA_TABLE_COLUMNS, + WORDLISTID_COLUMN + "= ? AND " + VERSION_COLUMN + "= ? AND " + + FORMATVERSION_COLUMN + "<= ?", + new String[] + { id, + Integer.toString(version), + Integer.toString(UpdateHandler.MAXIMUM_SUPPORTED_FORMAT_VERSION) + }, + null /* groupBy */, + null /* having */, + FORMATVERSION_COLUMN + " DESC"/* orderBy */); + if (null == cursor) { + return null; + } + try { + // This is a lookup by primary key, so there can't be more than one result. + return getFirstLineAsContentValues(cursor); + } finally { + cursor.close(); + } + } + + /** + * Gets the info about the latest word list with an id. + * + * @param db the database to get the information from. + * @param id the word list ID. + * @return the metadata about the word list with this id and the latest version number. + */ + public static ContentValues getContentValuesOfLatestAvailableWordlistById( + final SQLiteDatabase db, final String id) { + final Cursor cursor = db.query(METADATA_TABLE_NAME, + METADATA_TABLE_COLUMNS, + WORDLISTID_COLUMN + "= ?", + new String[] { id }, null, null, VERSION_COLUMN + " DESC", "1"); + if (null == cursor) { + return null; + } + try { + // Return the first result from the list of results. + return getFirstLineAsContentValues(cursor); + } finally { + cursor.close(); + } + } + + /** + * Gets the current metadata about INSTALLED, AVAILABLE or DELETING dictionaries. + * + * This odd method is tailored to the needs of + * DictionaryProvider#getDictionaryWordListsForContentUri, which needs the word list if + * it is: + * - INSTALLED: this should be returned to LatinIME if the file is still inside the dictionary + * pack, so that it can be copied. If the file is not there, it's been copied already and should + * not be returned, so getDictionaryWordListsForContentUri takes care of this. + * - DELETING: this should be returned to LatinIME so that it can actually delete the file. + * - AVAILABLE: this should not be returned, but should be checked for auto-installation. + * + * @param context the context for getting the database. + * @param clientId the client id for retrieving the database. null for default (deprecated) + * @return a cursor with metadata about usable dictionaries. + */ + public static Cursor queryInstalledOrDeletingOrAvailableDictionaryMetadata( + final Context context, final String clientId) { + // If clientId is null, we get the defaut DB (see #getInstance() for more about this) + final Cursor results = getDb(context, clientId).query(METADATA_TABLE_NAME, + METADATA_TABLE_COLUMNS, + STATUS_COLUMN + " = ? OR " + STATUS_COLUMN + " = ? OR " + STATUS_COLUMN + " = ?", + new String[] { Integer.toString(STATUS_INSTALLED), + Integer.toString(STATUS_DELETING), + Integer.toString(STATUS_AVAILABLE) }, + null, null, LOCALE_COLUMN); + return results; + } + + /** + * Gets the current metadata about all dictionaries. + * + * This will retrieve the metadata about all dictionaries, including + * older files, or files not yet downloaded. + * + * @param context the context for getting the database. + * @param clientId the client id for retrieving the database. null for default (deprecated) + * @return a cursor with metadata about usable dictionaries. + */ + public static Cursor queryCurrentMetadata(final Context context, final String clientId) { + // If clientId is null, we get the defaut DB (see #getInstance() for more about this) + final Cursor results = getDb(context, clientId).query(METADATA_TABLE_NAME, + METADATA_TABLE_COLUMNS, null, null, null, null, LOCALE_COLUMN); + return results; + } + + /** + * Gets the list of all dictionaries known to the dictionary provider, with only public columns. + * + * This will retrieve information about all known dictionaries, and their status. As such, + * it will also return information about dictionaries on the server that have not been + * downloaded yet, but may be requested. + * This only returns public columns. It does not populate internal columns in the returned + * cursor. + * The value returned by this method is intended to be good to be returned directly for a + * request of the list of dictionaries by a client. + * + * @param context the context to read the database from. + * @param clientId the client id for retrieving the database. null for default (deprecated) + * @return a cursor that lists all available dictionaries and their metadata. + */ + public static Cursor queryDictionaries(final Context context, final String clientId) { + // If clientId is null, we get the defaut DB (see #getInstance() for more about this) + final Cursor results = getDb(context, clientId).query(METADATA_TABLE_NAME, + DICTIONARIES_LIST_PUBLIC_COLUMNS, + // Filter out empty locales so as not to return auxiliary data, like a + // data line for downloading metadata: + MetadataDbHelper.LOCALE_COLUMN + " != ?", new String[] {""}, + // TODO: Reinstate the following code for bulk, then implement partial updates + /* MetadataDbHelper.TYPE_COLUMN + " = ?", + new String[] { Integer.toString(MetadataDbHelper.TYPE_BULK) }, */ + null, null, LOCALE_COLUMN); + return results; + } + + /** + * Deletes all data associated with a client. + * + * @param context the context for opening the database + * @param clientId the ID of the client to delete. + * @return true if the client was successfully deleted, false otherwise. + */ + public static boolean deleteClient(final Context context, final String clientId) { + // Remove all metadata associated with this client + final SQLiteDatabase db = getDb(context, clientId); + db.execSQL("DROP TABLE IF EXISTS " + METADATA_TABLE_NAME); + db.execSQL(METADATA_TABLE_CREATE); + // Remove this client's entry in the clients table + final SQLiteDatabase defaultDb = getDb(context, ""); + if (0 == defaultDb.delete(CLIENT_TABLE_NAME, + CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId })) { + return false; + } + return true; + } + + /** + * Updates information relative to a specific client. + * + * Updatable information includes the metadata URI and the additional ID column. It may be + * expanded in the future. + * The passed values must include a client ID in the key CLIENT_CLIENT_ID_COLUMN, and it must + * be equal to the string passed as an argument for clientId. It may not be empty. + * The passed values must also include a non-null metadata URI in the + * CLIENT_METADATA_URI_COLUMN column, as well as a non-null additional ID in the + * CLIENT_METADATA_ADDITIONAL_ID_COLUMN. Both these strings may be empty. + * If any of the above is not complied with, this function returns without updating data. + * + * @param context the context, to open the database + * @param clientId the ID of the client to update + * @param values the values to update. Must conform to the protocol (see above) + */ + public static void updateClientInfo(final Context context, final String clientId, + final ContentValues values) { + // Sanity check the content values + final String valuesClientId = values.getAsString(CLIENT_CLIENT_ID_COLUMN); + final String valuesMetadataUri = values.getAsString(CLIENT_METADATA_URI_COLUMN); + final String valuesMetadataAdditionalId = + values.getAsString(CLIENT_METADATA_ADDITIONAL_ID_COLUMN); + // Empty string is a valid client ID, but external apps may not configure it, so disallow + // both null and empty string. + // Empty string is a valid metadata URI if the client does not want updates, so allow + // empty string but disallow null. + // Empty string is a valid additional ID so allow empty string but disallow null. + if (TextUtils.isEmpty(valuesClientId) || null == valuesMetadataUri + || null == valuesMetadataAdditionalId) { + // We need all these columns to be filled in + DebugLogUtils.l("Missing parameter for updateClientInfo"); + return; + } + if (!clientId.equals(valuesClientId)) { + // Mismatch! The client violates the protocol. + DebugLogUtils.l("Received an updateClientInfo request for ", clientId, + " but the values " + "contain a different ID : ", valuesClientId); + return; + } + // Default value for a pending ID is NOT_AN_ID + values.put(CLIENT_PENDINGID_COLUMN, UpdateHandler.NOT_AN_ID); + final SQLiteDatabase defaultDb = getDb(context, ""); + if (-1 == defaultDb.insert(CLIENT_TABLE_NAME, null, values)) { + defaultDb.update(CLIENT_TABLE_NAME, values, + CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId }); + } + } + + /** + * Retrieves the list of existing client IDs. + * @param context the context to open the database + * @return a cursor containing only one column, and one client ID per line. + */ + public static Cursor queryClientIds(final Context context) { + return getDb(context, null).query(CLIENT_TABLE_NAME, + new String[] { CLIENT_CLIENT_ID_COLUMN }, null, null, null, null, null); + } + + /** + * Register a download ID for a specific metadata URI. + * + * This method should be called when a download for a metadata URI is starting. It will + * search for all clients using this metadata URI and will register for each of them + * the download ID into the database for later retrieval by + * {@link #getDownloadRecordsForDownloadId(Context, long)}. + * + * @param context a context for opening databases + * @param uri the metadata URI + * @param downloadId the download ID + */ + public static void registerMetadataDownloadId(final Context context, final String uri, + final long downloadId) { + final ContentValues values = new ContentValues(); + values.put(CLIENT_PENDINGID_COLUMN, downloadId); + values.put(CLIENT_LAST_UPDATE_DATE_COLUMN, System.currentTimeMillis()); + final SQLiteDatabase defaultDb = getDb(context, ""); + final Cursor cursor = MetadataDbHelper.queryClientIds(context); + if (null == cursor) return; + try { + if (!cursor.moveToFirst()) return; + do { + final String clientId = cursor.getString(0); + final String metadataUri = + MetadataDbHelper.getMetadataUriAsString(context, clientId); + if (metadataUri.equals(uri)) { + defaultDb.update(CLIENT_TABLE_NAME, values, + CLIENT_CLIENT_ID_COLUMN + " = ?", new String[] { clientId }); + } + } while (cursor.moveToNext()); + } finally { + cursor.close(); + } + } + + /** + * Marks a downloading entry as having successfully downloaded and being installed. + * + * The metadata database contains information about ongoing processes, typically ongoing + * downloads. This marks such an entry as having finished and having installed successfully, + * so it becomes INSTALLED. + * + * @param db the metadata database. + * @param r content values about the entry to mark as processed. + */ + public static void markEntryAsFinishedDownloadingAndInstalled(final SQLiteDatabase db, + final ContentValues r) { + switch (r.getAsInteger(TYPE_COLUMN)) { + case TYPE_BULK: + DebugLogUtils.l("Ended processing a wordlist"); + // Updating a bulk word list is a three-step operation: + // - Add the new entry to the table + // - Remove the old entry from the table + // - Erase the old file + // We start by gathering the names of the files we should delete. + final List filenames = new LinkedList<>(); + final Cursor c = db.query(METADATA_TABLE_NAME, + new String[] { LOCAL_FILENAME_COLUMN }, + LOCALE_COLUMN + " = ? AND " + + WORDLISTID_COLUMN + " = ? AND " + STATUS_COLUMN + " = ?", + new String[] { r.getAsString(LOCALE_COLUMN), + r.getAsString(WORDLISTID_COLUMN), + Integer.toString(STATUS_INSTALLED) }, + null, null, null); + try { + if (c.moveToFirst()) { + // There should never be more than one file, but if there are, it's a bug + // and we should remove them all. I think it might happen if the power of + // the phone is suddenly cut during an update. + final int filenameIndex = c.getColumnIndex(LOCAL_FILENAME_COLUMN); + do { + DebugLogUtils.l("Setting for removal", c.getString(filenameIndex)); + filenames.add(c.getString(filenameIndex)); + } while (c.moveToNext()); + } + } finally { + c.close(); + } + r.put(STATUS_COLUMN, STATUS_INSTALLED); + db.beginTransactionNonExclusive(); + // Delete all old entries. There should never be any stalled entries, but if + // there are, this deletes them. + db.delete(METADATA_TABLE_NAME, + WORDLISTID_COLUMN + " = ?", + new String[] { r.getAsString(WORDLISTID_COLUMN) }); + db.insert(METADATA_TABLE_NAME, null, r); + db.setTransactionSuccessful(); + db.endTransaction(); + for (String filename : filenames) { + try { + final File f = new File(filename); + f.delete(); + } catch (SecurityException e) { + // No permissions to delete. Um. Can't do anything. + } // I don't think anything else can be thrown + } + break; + default: + // Unknown type: do nothing. + break; + } + } + + /** + * Removes a downloading entry from the database. + * + * This is invoked when a download fails. Either we tried to download, but + * we received a permanent failure and we should remove it, or we got manually + * cancelled and we should leave it at that. + * + * @param db the metadata database. + * @param id the DownloadManager id of the file. + */ + public static void deleteDownloadingEntry(final SQLiteDatabase db, final long id) { + db.delete(METADATA_TABLE_NAME, PENDINGID_COLUMN + " = ? AND " + STATUS_COLUMN + " = ?", + new String[] { Long.toString(id), Integer.toString(STATUS_DOWNLOADING) }); + } + + /** + * Forcefully removes an entry from the database. + * + * This is invoked when a file is broken. The file has been downloaded, but Android + * Keyboard is telling us it could not open it. + * + * @param db the metadata database. + * @param id the id of the word list. + * @param version the version of the word list. + */ + public static void deleteEntry(final SQLiteDatabase db, final String id, final int version) { + db.delete(METADATA_TABLE_NAME, WORDLISTID_COLUMN + " = ? AND " + VERSION_COLUMN + " = ?", + new String[] { id, Integer.toString(version) }); + } + + /** + * Internal method that sets the current status of an entry of the database. + * + * @param db the metadata database. + * @param id the id of the word list. + * @param version the version of the word list. + * @param status the status to set the word list to. + * @param downloadId an optional download id to write, or NOT_A_DOWNLOAD_ID + */ + private static void markEntryAs(final SQLiteDatabase db, final String id, + final int version, final int status, final long downloadId) { + final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, id, version); + values.put(STATUS_COLUMN, status); + if (NOT_A_DOWNLOAD_ID != downloadId) { + values.put(MetadataDbHelper.PENDINGID_COLUMN, downloadId); + } + db.update(METADATA_TABLE_NAME, values, + WORDLISTID_COLUMN + " = ? AND " + VERSION_COLUMN + " = ?", + new String[] { id, Integer.toString(version) }); + } + + /** + * Writes the status column for the wordlist with this id as enabled. Typically this + * means the word list is currently disabled and we want to set its status to INSTALLED. + * + * @param db the metadata database. + * @param id the id of the word list. + * @param version the version of the word list. + */ + public static void markEntryAsEnabled(final SQLiteDatabase db, final String id, + final int version) { + markEntryAs(db, id, version, STATUS_INSTALLED, NOT_A_DOWNLOAD_ID); + } + + /** + * Writes the status column for the wordlist with this id as disabled. Typically this + * means the word list is currently installed and we want to set its status to DISABLED. + * + * @param db the metadata database. + * @param id the id of the word list. + * @param version the version of the word list. + */ + public static void markEntryAsDisabled(final SQLiteDatabase db, final String id, + final int version) { + markEntryAs(db, id, version, STATUS_DISABLED, NOT_A_DOWNLOAD_ID); + } + + /** + * Writes the status column for the wordlist with this id as available. This happens for + * example when a word list has been deleted but can be downloaded again. + * + * @param db the metadata database. + * @param id the id of the word list. + * @param version the version of the word list. + */ + public static void markEntryAsAvailable(final SQLiteDatabase db, final String id, + final int version) { + markEntryAs(db, id, version, STATUS_AVAILABLE, NOT_A_DOWNLOAD_ID); + } + + /** + * Writes the designated word list as downloadable, alongside with its download id. + * + * @param db the metadata database. + * @param id the id of the word list. + * @param version the version of the word list. + * @param downloadId the download id. + */ + public static void markEntryAsDownloading(final SQLiteDatabase db, final String id, + final int version, final long downloadId) { + markEntryAs(db, id, version, STATUS_DOWNLOADING, downloadId); + } + + /** + * Writes the designated word list as deleting. + * + * @param db the metadata database. + * @param id the id of the word list. + * @param version the version of the word list. + */ + public static void markEntryAsDeleting(final SQLiteDatabase db, final String id, + final int version) { + markEntryAs(db, id, version, STATUS_DELETING, NOT_A_DOWNLOAD_ID); + } + + /** + * Checks retry counts and marks the word list as retrying if retry is possible. + * + * @param db the metadata database. + * @param id the id of the word list. + * @param version the version of the word list. + * @return {@code true} if the retry is possible. + */ + public static boolean maybeMarkEntryAsRetrying(final SQLiteDatabase db, final String id, + final int version) { + final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, id, version); + int retryCount = values.getAsInteger(MetadataDbHelper.RETRY_COUNT_COLUMN); + if (retryCount > 1) { + values.put(STATUS_COLUMN, STATUS_RETRYING); + values.put(RETRY_COUNT_COLUMN, retryCount - 1); + db.update(METADATA_TABLE_NAME, values, + WORDLISTID_COLUMN + " = ? AND " + VERSION_COLUMN + " = ?", + new String[] { id, Integer.toString(version) }); + return true; + } + return false; + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataHandler.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataHandler.java new file mode 100644 index 00000000..1c3f9b30 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataHandler.java @@ -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 makeMetadataObject(final Cursor results) { + final ArrayList 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 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 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 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; + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataParser.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataParser.java new file mode 100644 index 00000000..378f9ab6 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataParser.java @@ -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 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 parseMetadata(final InputStreamReader input) + throws IOException, BadFormatException { + JsonReader reader = new JsonReader(input); + final ArrayList 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); + } + +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataUriGetter.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataUriGetter.java new file mode 100644 index 00000000..f3c460d6 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/MetadataUriGetter.java @@ -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; + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/PrivateLog.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/PrivateLog.java new file mode 100644 index 00000000..3d9be650 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/PrivateLog.java @@ -0,0 +1,102 @@ +/* + * 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.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * Class to keep long-term log. This is inactive in production, and is only for debug purposes. + */ +public class PrivateLog { + + public static final boolean DEBUG = DictionaryProvider.DEBUG; + + private static final String LOG_DATABASE_NAME = "log"; + private static final String LOG_TABLE_NAME = "log"; + private static final int LOG_DATABASE_VERSION = 1; + + private static final String COLUMN_DATE = "date"; + private static final String COLUMN_EVENT = "event"; + + private static final String LOG_TABLE_CREATE = "CREATE TABLE " + LOG_TABLE_NAME + " (" + + COLUMN_DATE + " TEXT," + + COLUMN_EVENT + " TEXT);"; + + static final SimpleDateFormat sDateFormat = new SimpleDateFormat( + "yyyy/MM/dd HH:mm:ss", Locale.ROOT); + + private static PrivateLog sInstance = new PrivateLog(); + private static DebugHelper sDebugHelper = null; + + private PrivateLog() { + } + + public static synchronized PrivateLog getInstance(final Context context) { + if (!DEBUG) return sInstance; + synchronized(PrivateLog.class) { + if (sDebugHelper == null) { + sDebugHelper = new DebugHelper(context); + } + return sInstance; + } + } + + static class DebugHelper extends SQLiteOpenHelper { + + DebugHelper(final Context context) { + super(context, LOG_DATABASE_NAME, null, LOG_DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + if (!DEBUG) return; + db.execSQL(LOG_TABLE_CREATE); + insert(db, "Created table"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (!DEBUG) return; + // Remove all data. + db.execSQL("DROP TABLE IF EXISTS " + LOG_TABLE_NAME); + onCreate(db); + insert(db, "Upgrade finished"); + } + + static void insert(SQLiteDatabase db, String event) { + if (!DEBUG) return; + final ContentValues c = new ContentValues(2); + c.put(COLUMN_DATE, sDateFormat.format(new Date(System.currentTimeMillis()))); + c.put(COLUMN_EVENT, event); + db.insert(LOG_TABLE_NAME, null, c); + } + + } + + public static void log(String event) { + if (!DEBUG) return; + final SQLiteDatabase l = sDebugHelper.getWritableDatabase(); + DebugHelper.insert(l, event); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ProblemReporter.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ProblemReporter.java new file mode 100644 index 00000000..eb52c13f --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/ProblemReporter.java @@ -0,0 +1,24 @@ +/* + * 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; + +/** + * A simple interface to report problems. + */ +public interface ProblemReporter { + public void report(Exception e); +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/UpdateHandler.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/UpdateHandler.java new file mode 100644 index 00000000..dd22784f --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/UpdateHandler.java @@ -0,0 +1,1137 @@ +/* + * 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; +import android.app.DownloadManager.Query; +import android.app.DownloadManager.Request; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.ConnectivityManager; +import android.net.Uri; +import android.os.ParcelFileDescriptor; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Log; + +import org.dslul.openboard.inputmethod.compat.ConnectivityManagerCompatUtils; +import org.dslul.openboard.inputmethod.compat.NotificationCompatUtils; +import org.dslul.openboard.inputmethod.latin.R; +import org.dslul.openboard.inputmethod.latin.common.LocaleUtils; +import org.dslul.openboard.inputmethod.latin.makedict.FormatSpec; +import org.dslul.openboard.inputmethod.latin.utils.ApplicationUtils; +import org.dslul.openboard.inputmethod.latin.utils.DebugLogUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import javax.annotation.Nullable; + +/** + * Handler for the update process. + * + * This class is in charge of coordinating the update process for the various dictionaries + * stored in the dictionary pack. + */ +public final class UpdateHandler { + static final String TAG = "DictionaryProvider:" + UpdateHandler.class.getSimpleName(); + private static final boolean DEBUG = DictionaryProvider.DEBUG; + + // Used to prevent trying to read the id of the downloaded file before it is written + static final Object sSharedIdProtector = new Object(); + + // Value used to mean this is not a real DownloadManager downloaded file id + // DownloadManager uses as an ID numbers returned out of an AUTOINCREMENT column + // in SQLite, so it should never return anything < 0. + public static final int NOT_AN_ID = -1; + public static final int MAXIMUM_SUPPORTED_FORMAT_VERSION = + FormatSpec.MAXIMUM_SUPPORTED_STATIC_VERSION; + + // Arbitrary. Probably good if it's a power of 2, and a couple thousand bytes long. + private static final int FILE_COPY_BUFFER_SIZE = 8192; + + // Table fixed values for metadata / downloads + final static String METADATA_NAME = "metadata"; + final static int METADATA_TYPE = 0; + final static int WORDLIST_TYPE = 1; + + // Suffix for generated dictionary files + private static final String DICT_FILE_SUFFIX = ".dict"; + // Name of the category for the main dictionary + public static final String MAIN_DICTIONARY_CATEGORY = "main"; + + public static final String TEMP_DICT_FILE_SUB = "___"; + + // The id for the "dictionary available" notification. + static final int DICT_AVAILABLE_NOTIFICATION_ID = 1; + + /** + * An interface for UIs or services that want to know when something happened. + * + * This is chiefly used by the dictionary manager UI. + */ + public interface UpdateEventListener { + void downloadedMetadata(boolean succeeded); + void wordListDownloadFinished(String wordListId, boolean succeeded); + void updateCycleCompleted(); + } + + /** + * The list of currently registered listeners. + */ + private static List sUpdateEventListeners + = Collections.synchronizedList(new LinkedList()); + + /** + * Register a new listener to be notified of updates. + * + * Don't forget to call unregisterUpdateEventListener when done with it, or + * it will leak the register. + */ + public static void registerUpdateEventListener(final UpdateEventListener listener) { + sUpdateEventListeners.add(listener); + } + + /** + * Unregister a previously registered listener. + */ + public static void unregisterUpdateEventListener(final UpdateEventListener listener) { + sUpdateEventListeners.remove(listener); + } + + private static final String DOWNLOAD_OVER_METERED_SETTING_PREFS_KEY = "downloadOverMetered"; + + /** + * Write the DownloadManager ID of the currently downloading metadata to permanent storage. + * + * @param context to open shared prefs + * @param uri the uri of the metadata + * @param downloadId the id returned by DownloadManager + */ + private static void writeMetadataDownloadId(final Context context, final String uri, + final long downloadId) { + MetadataDbHelper.registerMetadataDownloadId(context, uri, downloadId); + } + + public static final int DOWNLOAD_OVER_METERED_SETTING_UNKNOWN = 0; + public static final int DOWNLOAD_OVER_METERED_ALLOWED = 1; + public static final int DOWNLOAD_OVER_METERED_DISALLOWED = 2; + + /** + * Sets the setting that tells us whether we may download over a metered connection. + */ + public static void setDownloadOverMeteredSetting(final Context context, + final boolean shouldDownloadOverMetered) { + final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context); + final SharedPreferences.Editor editor = prefs.edit(); + editor.putInt(DOWNLOAD_OVER_METERED_SETTING_PREFS_KEY, shouldDownloadOverMetered + ? DOWNLOAD_OVER_METERED_ALLOWED : DOWNLOAD_OVER_METERED_DISALLOWED); + editor.apply(); + } + + /** + * Gets the setting that tells us whether we may download over a metered connection. + * + * This returns one of the constants above. + */ + public static int getDownloadOverMeteredSetting(final Context context) { + final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context); + final int setting = prefs.getInt(DOWNLOAD_OVER_METERED_SETTING_PREFS_KEY, + DOWNLOAD_OVER_METERED_SETTING_UNKNOWN); + return setting; + } + + /** + * Download latest metadata from the server through DownloadManager for all known clients + * @param context The context for retrieving resources + * @return true if an update successfully started, false otherwise. + */ + public static boolean tryUpdate(final Context context) { + // TODO: loop through all clients instead of only doing the default one. + final TreeSet uris = new TreeSet<>(); + final Cursor cursor = MetadataDbHelper.queryClientIds(context); + if (null == cursor) return false; + try { + if (!cursor.moveToFirst()) return false; + do { + final String clientId = cursor.getString(0); + final String metadataUri = + MetadataDbHelper.getMetadataUriAsString(context, clientId); + PrivateLog.log("Update for clientId " + DebugLogUtils.s(clientId)); + DebugLogUtils.l("Update for clientId", clientId, " which uses URI ", metadataUri); + uris.add(metadataUri); + } while (cursor.moveToNext()); + } finally { + cursor.close(); + } + boolean started = false; + for (final String metadataUri : uris) { + if (!TextUtils.isEmpty(metadataUri)) { + // If the metadata URI is empty, that means we should never update it at all. + // It should not be possible to come here with a null metadata URI, because + // it should have been rejected at the time of client registration; if there + // is a bug and it happens anyway, doing nothing is the right thing to do. + // For more information, {@see DictionaryProvider#insert(Uri, ContentValues)}. + updateClientsWithMetadataUri(context, metadataUri); + started = true; + } + } + return started; + } + + /** + * Download latest metadata from the server through DownloadManager for all relevant clients + * + * @param context The context for retrieving resources + * @param metadataUri The client to update + */ + private static void updateClientsWithMetadataUri( + final Context context, final String metadataUri) { + Log.i(TAG, "updateClientsWithMetadataUri() : MetadataUri = " + metadataUri); + // 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) + ".json"; + final Request metadataRequest = new Request(Uri.parse(metadataUri + disambiguator)); + DebugLogUtils.l("Request =", metadataRequest); + + final Resources res = context.getResources(); + metadataRequest.setAllowedNetworkTypes(Request.NETWORK_WIFI | Request.NETWORK_MOBILE); + metadataRequest.setTitle(res.getString(R.string.download_description)); + // Do not show the notification when downloading the metadata. + metadataRequest.setNotificationVisibility(Request.VISIBILITY_HIDDEN); + metadataRequest.setVisibleInDownloadsUi( + res.getBoolean(R.bool.metadata_downloads_visible_in_download_UI)); + + final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); + if (maybeCancelUpdateAndReturnIfStillRunning(context, metadataUri, manager, + DictionaryService.NO_CANCEL_DOWNLOAD_PERIOD_MILLIS)) { + // We already have a recent download in progress. Don't register a new download. + return; + } + final long downloadId; + synchronized (sSharedIdProtector) { + downloadId = manager.enqueue(metadataRequest); + DebugLogUtils.l("Metadata download requested with id", downloadId); + // If there is still a download in progress, it's been there for a while and + // there is probably something wrong with download manager. It's best to just + // overwrite the id and request it again. If the old one happens to finish + // anyway, we don't know about its ID any more, so the downloadFinished + // method will ignore it. + writeMetadataDownloadId(context, metadataUri, downloadId); + } + Log.i(TAG, "updateClientsWithMetadataUri() : DownloadId = " + downloadId); + } + + /** + * Cancels downloading a file if there is one for this URI and it's too long. + * + * If we are not currently downloading the file at this URI, this is a no-op. + * + * @param context the context to open the database on + * @param metadataUri the URI to cancel + * @param manager an wrapped instance of DownloadManager + * @param graceTime if there was a download started less than this many milliseconds, don't + * cancel and return true + * @return whether the download is still active + */ + private static boolean maybeCancelUpdateAndReturnIfStillRunning(final Context context, + final String metadataUri, final DownloadManagerWrapper manager, final long graceTime) { + synchronized (sSharedIdProtector) { + final DownloadIdAndStartDate metadataDownloadIdAndStartDate = + MetadataDbHelper.getMetadataDownloadIdAndStartDateForURI(context, metadataUri); + if (null == metadataDownloadIdAndStartDate) return false; + if (NOT_AN_ID == metadataDownloadIdAndStartDate.mId) return false; + if (metadataDownloadIdAndStartDate.mStartDate + graceTime + > System.currentTimeMillis()) { + return true; + } + manager.remove(metadataDownloadIdAndStartDate.mId); + writeMetadataDownloadId(context, metadataUri, NOT_AN_ID); + } + // Consider a cancellation as a failure. As such, inform listeners that the download + // has failed. + for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) { + listener.downloadedMetadata(false); + } + return false; + } + + /** + * Cancels a pending update for this client, if there is one. + * + * If we are not currently updating metadata for this client, this is a no-op. This is a helper + * method that gets the download manager service and the metadata URI for this client. + * + * @param context the context, to get an instance of DownloadManager + * @param clientId the ID of the client we want to cancel the update of + */ + public static void cancelUpdate(final Context context, final String clientId) { + final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); + final String metadataUri = MetadataDbHelper.getMetadataUriAsString(context, clientId); + maybeCancelUpdateAndReturnIfStillRunning(context, metadataUri, manager, 0 /* graceTime */); + } + + /** + * Registers a download request and flags it as downloading in the metadata table. + * + * This is a helper method that exists to avoid race conditions where DownloadManager might + * finish downloading the file before the data is committed to the database. + * It registers the request with the DownloadManager service and also updates the metadata + * database directly within a synchronized section. + * This method has no intelligence about the data it commits to the database aside from the + * download request id, which is not known before submitting the request to the download + * manager. Hence, it only updates the relevant line. + * + * @param manager a wrapped download manager service to register the request with. + * @param request the request to register. + * @param db the metadata database. + * @param id the id of the word list. + * @param version the version of the word list. + * @return the download id returned by the download manager. + */ + public static long registerDownloadRequest(final DownloadManagerWrapper manager, + final Request request, final SQLiteDatabase db, final String id, final int version) { + Log.i(TAG, "registerDownloadRequest() : Id = " + id + " : Version = " + version); + final long downloadId; + synchronized (sSharedIdProtector) { + downloadId = manager.enqueue(request); + Log.i(TAG, "registerDownloadRequest() : DownloadId = " + downloadId); + MetadataDbHelper.markEntryAsDownloading(db, id, version, downloadId); + } + return downloadId; + } + + /** + * Retrieve information about a specific download from DownloadManager. + */ + private static CompletedDownloadInfo getCompletedDownloadInfo( + final DownloadManagerWrapper manager, final long downloadId) { + final Query query = new Query().setFilterById(downloadId); + final Cursor cursor = manager.query(query); + + if (null == cursor) { + return new CompletedDownloadInfo(null, downloadId, DownloadManager.STATUS_FAILED); + } + try { + final String uri; + final int status; + if (cursor.moveToNext()) { + final int columnStatus = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS); + final int columnError = cursor.getColumnIndex(DownloadManager.COLUMN_REASON); + final int columnUri = cursor.getColumnIndex(DownloadManager.COLUMN_URI); + final int error = cursor.getInt(columnError); + status = cursor.getInt(columnStatus); + final String uriWithAnchor = cursor.getString(columnUri); + int anchorIndex = uriWithAnchor.indexOf('#'); + if (anchorIndex != -1) { + uri = uriWithAnchor.substring(0, anchorIndex); + } else { + uri = uriWithAnchor; + } + if (DownloadManager.STATUS_SUCCESSFUL != status) { + Log.e(TAG, "Permanent failure of download " + downloadId + + " with error code: " + error); + } + } else { + uri = null; + status = DownloadManager.STATUS_FAILED; + } + return new CompletedDownloadInfo(uri, downloadId, status); + } finally { + cursor.close(); + } + } + + private static ArrayList getDownloadRecordsForCompletedDownloadInfo( + final Context context, final CompletedDownloadInfo downloadInfo) { + // Get and check the ID of the file we are waiting for, compare them to downloaded ones + synchronized(sSharedIdProtector) { + final ArrayList downloadRecords = + MetadataDbHelper.getDownloadRecordsForDownloadId(context, + downloadInfo.mDownloadId); + // If any of these is metadata, we should update the DB + boolean hasMetadata = false; + for (DownloadRecord record : downloadRecords) { + if (record.isMetadata()) { + hasMetadata = true; + break; + } + } + if (hasMetadata) { + writeMetadataDownloadId(context, downloadInfo.mUri, NOT_AN_ID); + MetadataDbHelper.saveLastUpdateTimeOfUri(context, downloadInfo.mUri); + } + return downloadRecords; + } + } + + /** + * Take appropriate action after a download finished, in success or in error. + * + * This is called by the system upon broadcast from the DownloadManager that a file + * has been downloaded successfully. + * After a simple check that this is actually the file we are waiting for, this + * method basically coordinates the parsing and comparison of metadata, and fires + * the computation of the list of actions that should be taken then executes them. + * + * @param context The context for this action. + * @param intent The intent from the DownloadManager containing details about the download. + */ + /* package */ static void downloadFinished(final Context context, final Intent intent) { + // Get and check the ID of the file that was downloaded + final long fileId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, NOT_AN_ID); + Log.i(TAG, "downloadFinished() : DownloadId = " + fileId); + if (NOT_AN_ID == fileId) return; // Spurious wake-up: ignore + + final DownloadManagerWrapper manager = new DownloadManagerWrapper(context); + final CompletedDownloadInfo downloadInfo = getCompletedDownloadInfo(manager, fileId); + + final ArrayList recordList = + getDownloadRecordsForCompletedDownloadInfo(context, downloadInfo); + if (null == recordList) return; // It was someone else's download. + DebugLogUtils.l("Received result for download ", fileId); + + // TODO: handle gracefully a null pointer here. This is practically impossible because + // we come here only when DownloadManager explicitly called us when it ended a + // download, so we are pretty sure it's alive. It's theoretically possible that it's + // disabled right inbetween the firing of the intent and the control reaching here. + + for (final DownloadRecord record : recordList) { + // downloadSuccessful is not final because we may still have exceptions from now on + boolean downloadSuccessful = false; + try { + if (downloadInfo.wasSuccessful()) { + downloadSuccessful = handleDownloadedFile(context, record, manager, fileId); + Log.i(TAG, "downloadFinished() : Success = " + downloadSuccessful); + } + } finally { + final String resultMessage = downloadSuccessful ? "Success" : "Failure"; + if (record.isMetadata()) { + Log.i(TAG, "downloadFinished() : Metadata " + resultMessage); + publishUpdateMetadataCompleted(context, downloadSuccessful); + } else { + Log.i(TAG, "downloadFinished() : WordList " + resultMessage); + final SQLiteDatabase db = MetadataDbHelper.getDb(context, record.mClientId); + publishUpdateWordListCompleted(context, downloadSuccessful, fileId, + db, record.mAttributes, record.mClientId); + } + } + } + // Now that we're done using it, we can remove this download from DLManager + manager.remove(fileId); + } + + /** + * Sends a broadcast informing listeners that the dictionaries were updated. + * + * This will call all local listeners through the UpdateEventListener#downloadedMetadata + * callback (for example, the dictionary provider interface uses this to stop the Loading + * animation) and send a broadcast about the metadata having been updated. For a client of + * the dictionary pack like Latin IME, this means it should re-query the dictionary pack + * for any relevant new data. + * + * @param context the context, to send the broadcast. + * @param downloadSuccessful whether the download of the metadata was successful or not. + */ + public static void publishUpdateMetadataCompleted(final Context context, + final boolean downloadSuccessful) { + // We need to warn all listeners of what happened. But some listeners may want to + // remove themselves or re-register something in response. Hence we should take a + // snapshot of the listener list and warn them all. This also prevents any + // concurrent modification problem of the static list. + for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) { + listener.downloadedMetadata(downloadSuccessful); + } + publishUpdateCycleCompletedEvent(context); + } + + private static void publishUpdateWordListCompleted(final Context context, + final boolean downloadSuccessful, final long fileId, + final SQLiteDatabase db, final ContentValues downloadedFileRecord, + final String clientId) { + synchronized(sSharedIdProtector) { + if (downloadSuccessful) { + final ActionBatch actions = new ActionBatch(); + actions.add(new ActionBatch.InstallAfterDownloadAction(clientId, + downloadedFileRecord)); + actions.execute(context, new LogProblemReporter(TAG)); + } else { + MetadataDbHelper.deleteDownloadingEntry(db, fileId); + } + } + // See comment above about #linkedCopyOfLists + for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) { + listener.wordListDownloadFinished(downloadedFileRecord.getAsString( + MetadataDbHelper.WORDLISTID_COLUMN), downloadSuccessful); + } + publishUpdateCycleCompletedEvent(context); + } + + private static void publishUpdateCycleCompletedEvent(final Context context) { + // Even if this is not successful, we have to publish the new state. + PrivateLog.log("Publishing update cycle completed event"); + DebugLogUtils.l("Publishing update cycle completed event"); + for (UpdateEventListener listener : linkedCopyOfList(sUpdateEventListeners)) { + listener.updateCycleCompleted(); + } + signalNewDictionaryState(context); + } + + private static boolean handleDownloadedFile(final Context context, + final DownloadRecord downloadRecord, final DownloadManagerWrapper manager, + final long fileId) { + try { + // {@link handleWordList(Context,InputStream,ContentValues)}. + // Handle the downloaded file according to its type + if (downloadRecord.isMetadata()) { + DebugLogUtils.l("Data D/L'd is metadata for", downloadRecord.mClientId); + // #handleMetadata() closes its InputStream argument + handleMetadata(context, new ParcelFileDescriptor.AutoCloseInputStream( + manager.openDownloadedFile(fileId)), downloadRecord.mClientId); + } else { + DebugLogUtils.l("Data D/L'd is a word list"); + final int wordListStatus = downloadRecord.mAttributes.getAsInteger( + MetadataDbHelper.STATUS_COLUMN); + if (MetadataDbHelper.STATUS_DOWNLOADING == wordListStatus) { + // #handleWordList() closes its InputStream argument + handleWordList(context, new ParcelFileDescriptor.AutoCloseInputStream( + manager.openDownloadedFile(fileId)), downloadRecord); + } else { + Log.e(TAG, "Spurious download ended. Maybe a cancelled download?"); + } + } + return true; + } catch (FileNotFoundException e) { + Log.e(TAG, "A file was downloaded but it can't be opened", e); + } catch (IOException e) { + // Can't read the file... disk damage? + Log.e(TAG, "Can't read a file", e); + // TODO: Check with UX how we should warn the user. + } catch (IllegalStateException e) { + // The format of the downloaded file is incorrect. We should maybe report upstream? + Log.e(TAG, "Incorrect data received", e); + } catch (BadFormatException e) { + // The format of the downloaded file is incorrect. We should maybe report upstream? + Log.e(TAG, "Incorrect data received", e); + } + return false; + } + + /** + * Returns a copy of the specified list, with all elements copied. + * + * This returns a linked list. + */ + private static List linkedCopyOfList(final List src) { + // Instantiation of a parameterized type is not possible in Java, so it's not possible to + // return the same type of list that was passed - probably the same reason why Collections + // does not do it. So we need to decide statically which concrete type to return. + return new LinkedList<>(src); + } + + /** + * Warn Android Keyboard that the state of dictionaries changed and it should refresh its data. + */ + private static void signalNewDictionaryState(final Context context) { + // TODO: Also provide the locale of the updated dictionary so that the LatinIme + // does not have to reset if it is a different locale. + final Intent newDictBroadcast = + new Intent(DictionaryPackConstants.NEW_DICTIONARY_INTENT_ACTION); + context.sendBroadcast(newDictBroadcast); + } + + /** + * Parse metadata and take appropriate action (that is, upgrade dictionaries). + * @param context the context to read settings. + * @param stream an input stream pointing to the downloaded data. May not be null. + * Will be closed upon finishing. + * @param clientId the ID of the client to update + * @throws BadFormatException if the metadata is not in a known format. + * @throws IOException if the downloaded file can't be read from the disk + */ + public static void handleMetadata(final Context context, final InputStream stream, + final String clientId) throws IOException, BadFormatException { + DebugLogUtils.l("Entering handleMetadata"); + final List newMetadata; + final InputStreamReader reader = new InputStreamReader(stream); + try { + // According to the doc InputStreamReader buffers, so no need to add a buffering layer + newMetadata = MetadataHandler.readMetadata(reader); + } finally { + reader.close(); + } + + DebugLogUtils.l("Downloaded metadata :", newMetadata); + PrivateLog.log("Downloaded metadata\n" + newMetadata); + + final ActionBatch actions = computeUpgradeTo(context, clientId, newMetadata); + // TODO: Check with UX how we should report to the user + // TODO: add an action to close the database + actions.execute(context, new LogProblemReporter(TAG)); + } + + /** + * Handle a word list: put it in its right place, and update the passed content values. + * @param context the context for opening files. + * @param inputStream an input stream pointing to the downloaded data. May not be null. + * Will be closed upon finishing. + * @param downloadRecord the content values to fill the file name in. + * @throws IOException if files can't be read or written. + * @throws BadFormatException if the md5 checksum doesn't match the metadata. + */ + private static void handleWordList(final Context context, + final InputStream inputStream, final DownloadRecord downloadRecord) + throws IOException, BadFormatException { + + // DownloadManager does not have the ability to put the file directly where we want + // it, so we had it download to a temporary place. Now we move it. It will be deleted + // automatically by DownloadManager. + DebugLogUtils.l("Downloaded a new word list :", downloadRecord.mAttributes.getAsString( + MetadataDbHelper.DESCRIPTION_COLUMN), "for", downloadRecord.mClientId); + PrivateLog.log("Downloaded a new word list with description : " + + downloadRecord.mAttributes.getAsString(MetadataDbHelper.DESCRIPTION_COLUMN) + + " for " + downloadRecord.mClientId); + + final String locale = + downloadRecord.mAttributes.getAsString(MetadataDbHelper.LOCALE_COLUMN); + final String destinationFile = getTempFileName(context, locale); + downloadRecord.mAttributes.put(MetadataDbHelper.LOCAL_FILENAME_COLUMN, destinationFile); + + FileOutputStream outputStream = null; + try { + outputStream = context.openFileOutput(destinationFile, Context.MODE_PRIVATE); + copyFile(inputStream, outputStream); + } finally { + inputStream.close(); + if (outputStream != null) { + outputStream.close(); + } + } + + // TODO: Consolidate this MD5 calculation with file copying above. + // We need to reopen the file because the inputstream bytes have been consumed, and there + // is nothing in InputStream to reopen or rewind the stream + FileInputStream copiedFile = null; + final String md5sum; + try { + copiedFile = context.openFileInput(destinationFile); + md5sum = MD5Calculator.checksum(copiedFile); + } finally { + if (copiedFile != null) { + copiedFile.close(); + } + } + if (TextUtils.isEmpty(md5sum)) { + return; // We can't compute the checksum anyway, so return and hope for the best + } + if (!md5sum.equals(downloadRecord.mAttributes.getAsString( + MetadataDbHelper.CHECKSUM_COLUMN))) { + context.deleteFile(destinationFile); + throw new BadFormatException("MD5 checksum check failed : \"" + md5sum + "\" <> \"" + + downloadRecord.mAttributes.getAsString(MetadataDbHelper.CHECKSUM_COLUMN) + + "\""); + } + } + + /** + * Copies in to out using FileChannels. + * + * This tries to use channels for fast copying. If it doesn't work, fall back to + * copyFileFallBack below. + * + * @param in the stream to copy from. + * @param out the stream to copy to. + * @throws IOException if both the normal and fallback methods raise exceptions. + */ + private static void copyFile(final InputStream in, final OutputStream out) + throws IOException { + DebugLogUtils.l("Copying files"); + if (!(in instanceof FileInputStream) || !(out instanceof FileOutputStream)) { + DebugLogUtils.l("Not the right types"); + copyFileFallback(in, out); + } else { + try { + final FileChannel sourceChannel = ((FileInputStream) in).getChannel(); + final FileChannel destinationChannel = ((FileOutputStream) out).getChannel(); + sourceChannel.transferTo(0, Integer.MAX_VALUE, destinationChannel); + } catch (IOException e) { + // Can't work with channels, or something went wrong. Copy by hand. + DebugLogUtils.l("Won't work"); + copyFileFallback(in, out); + } + } + } + + /** + * Copies in to out with read/write methods, not FileChannels. + * + * @param in the stream to copy from. + * @param out the stream to copy to. + * @throws IOException if a read or a write fails. + */ + private static void copyFileFallback(final InputStream in, final OutputStream out) + throws IOException { + DebugLogUtils.l("Falling back to slow copy"); + final byte[] buffer = new byte[FILE_COPY_BUFFER_SIZE]; + for (int readBytes = in.read(buffer); readBytes >= 0; readBytes = in.read(buffer)) + out.write(buffer, 0, readBytes); + } + + /** + * Creates and returns a new file to store a dictionary + * @param context the context to use to open the file. + * @param locale the locale for this dictionary, to make the file name more readable. + * @return the file name, or throw an exception. + * @throws IOException if the file cannot be created. + */ + private static String getTempFileName(final Context context, final String locale) + throws IOException { + DebugLogUtils.l("Entering openTempFileOutput"); + final File dir = context.getFilesDir(); + final File f = File.createTempFile(locale + TEMP_DICT_FILE_SUB, DICT_FILE_SUFFIX, dir); + DebugLogUtils.l("File name is", f.getName()); + return f.getName(); + } + + /** + * Compare metadata (collections of word lists). + * + * This method takes whole metadata sets directly and compares them, matching the wordlists in + * each of them on the id. It creates an ActionBatch object that can be .execute()'d to perform + * the actual upgrade from `from' to `to'. + * + * @param context the context to open databases on. + * @param clientId the id of the client. + * @param from the dictionary descriptor (as a list of wordlists) to upgrade from. + * @param to the dictionary descriptor (as a list of wordlists) to upgrade to. + * @return an ordered list of runnables to be called to upgrade. + */ + private static ActionBatch compareMetadataForUpgrade(final Context context, + final String clientId, @Nullable final List from, + @Nullable final List to) { + final ActionBatch actions = new ActionBatch(); + // Upgrade existing word lists + DebugLogUtils.l("Comparing dictionaries"); + final Set wordListIds = new TreeSet<>(); + // TODO: Can these be null? + final List fromList = (from == null) ? new ArrayList() + : from; + final List toList = (to == null) ? new ArrayList() + : to; + for (WordListMetadata wlData : fromList) wordListIds.add(wlData.mId); + for (WordListMetadata wlData : toList) wordListIds.add(wlData.mId); + for (String id : wordListIds) { + final WordListMetadata currentInfo = MetadataHandler.findWordListById(fromList, id); + final WordListMetadata metadataInfo = MetadataHandler.findWordListById(toList, id); + // TODO: Remove the following unnecessary check, since we are now doing the filtering + // inside findWordListById. + final WordListMetadata newInfo = null == metadataInfo + || metadataInfo.mFormatVersion > MAXIMUM_SUPPORTED_FORMAT_VERSION + ? null : metadataInfo; + DebugLogUtils.l("Considering updating ", id, "currentInfo =", currentInfo); + + if (null == currentInfo && null == newInfo) { + // This may happen if a new word list appeared that we can't handle. + if (null == metadataInfo) { + // What happened? Bug in Set<>? + Log.e(TAG, "Got an id for a wordlist that is neither in from nor in to"); + } else { + // We may come here if there is a new word list that we can't handle. + Log.i(TAG, "Can't handle word list with id '" + id + "' because it has format" + + " version " + metadataInfo.mFormatVersion + " and the maximum version" + + " we can handle is " + MAXIMUM_SUPPORTED_FORMAT_VERSION); + } + continue; + } else if (null == currentInfo) { + // This is the case where a new list that we did not know of popped on the server. + // Make it available. + actions.add(new ActionBatch.MakeAvailableAction(clientId, newInfo)); + } else if (null == newInfo) { + // This is the case where an old list we had is not in the server data any more. + // Pass false to ForgetAction: this may be installed and we still want to apply + // a forget-like action (remove the URL) if it is, so we want to turn off the + // status == AVAILABLE check. If it's DELETING, this is the right thing to do, + // as we want to leave the record as long as Android Keyboard has not deleted it ; + // the record will be removed when the file is actually deleted. + actions.add(new ActionBatch.ForgetAction(clientId, currentInfo, false)); + } else { + final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId); + if (newInfo.mVersion == currentInfo.mVersion) { + if (TextUtils.equals(newInfo.mRemoteFilename, currentInfo.mRemoteFilename)) { + // If the dictionary url hasn't changed, we should preserve the retryCount. + newInfo.mRetryCount = currentInfo.mRetryCount; + } + // If it's the same id/version, we update the DB with the new values. + // It doesn't matter too much if they didn't change. + actions.add(new ActionBatch.UpdateDataAction(clientId, newInfo)); + } else if (newInfo.mVersion > currentInfo.mVersion) { + // If it's a new version, it's a different entry in the database. Make it + // available, and if it's installed, also start the download. + final ContentValues values = MetadataDbHelper.getContentValuesByWordListId(db, + currentInfo.mId, currentInfo.mVersion); + final int status = values.getAsInteger(MetadataDbHelper.STATUS_COLUMN); + actions.add(new ActionBatch.MakeAvailableAction(clientId, newInfo)); + if (status == MetadataDbHelper.STATUS_INSTALLED + || status == MetadataDbHelper.STATUS_DISABLED) { + actions.add(new ActionBatch.StartDownloadAction(clientId, newInfo)); + } else { + // Pass true to ForgetAction: this is indeed an update to a non-installed + // word list, so activate status == AVAILABLE check + // In case the status is DELETING, this is the right thing to do. It will + // leave the entry as DELETING and remove its URL so that Android Keyboard + // can delete it the next time it starts up. + actions.add(new ActionBatch.ForgetAction(clientId, currentInfo, true)); + } + } else if (DEBUG) { + Log.i(TAG, "Not updating word list " + id + + " : current list timestamp is " + currentInfo.mLastUpdate + + " ; new list timestamp is " + newInfo.mLastUpdate); + } + } + } + return actions; + } + + /** + * Computes an upgrade from the current state of the dictionaries to some desired state. + * @param context the context for reading settings and files. + * @param clientId the id of the client. + * @param newMetadata the state we want to upgrade to. + * @return the upgrade from the current state to the desired state, ready to be executed. + */ + public static ActionBatch computeUpgradeTo(final Context context, final String clientId, + final List newMetadata) { + final List currentMetadata = + MetadataHandler.getCurrentMetadata(context, clientId); + return compareMetadataForUpgrade(context, clientId, currentMetadata, newMetadata); + } + + /** + * Shows the notification that informs the user a dictionary is available. + * + * When this notification is clicked, the dialog for downloading the dictionary + * over a metered connection is shown. + */ + private static void showDictionaryAvailableNotification(final Context context, + final String clientId, final ContentValues installCandidate) { + final String localeString = installCandidate.getAsString(MetadataDbHelper.LOCALE_COLUMN); + final Intent intent = new Intent(); + intent.setClass(context, DownloadOverMeteredDialog.class); + intent.putExtra(DownloadOverMeteredDialog.CLIENT_ID_KEY, clientId); + intent.putExtra(DownloadOverMeteredDialog.WORDLIST_TO_DOWNLOAD_KEY, + installCandidate.getAsString(MetadataDbHelper.WORDLISTID_COLUMN)); + intent.putExtra(DownloadOverMeteredDialog.SIZE_KEY, + installCandidate.getAsInteger(MetadataDbHelper.FILESIZE_COLUMN)); + intent.putExtra(DownloadOverMeteredDialog.LOCALE_KEY, localeString); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + final PendingIntent notificationIntent = PendingIntent.getActivity(context, + 0 /* requestCode */, intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_ONE_SHOT); + final NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + // None of those are expected to happen, but just in case... + if (null == notificationIntent || null == notificationManager) return; + + final String language = (null == localeString) ? "" + : LocaleUtils.constructLocaleFromString(localeString).getDisplayLanguage(); + final String titleFormat = context.getString(R.string.dict_available_notification_title); + final String notificationTitle = String.format(titleFormat, language); + final Notification.Builder builder = new Notification.Builder(context) + .setAutoCancel(true) + .setContentIntent(notificationIntent) + .setContentTitle(notificationTitle) + .setContentText(context.getString(R.string.dict_available_notification_description)) + .setTicker(notificationTitle) + .setOngoing(false) + .setOnlyAlertOnce(true) + .setSmallIcon(R.drawable.ic_notify_dictionary); + NotificationCompatUtils.setColor(builder, + context.getResources().getColor(R.color.notification_accent_color)); + NotificationCompatUtils.setPriorityToLow(builder); + NotificationCompatUtils.setVisibilityToSecret(builder); + NotificationCompatUtils.setCategoryToRecommendation(builder); + final Notification notification = NotificationCompatUtils.build(builder); + notificationManager.notify(DICT_AVAILABLE_NOTIFICATION_ID, notification); + } + + /** + * Installs a word list if it has never been requested. + * + * This is called when a word list is requested, and is available but not installed. It checks + * the conditions for auto-installation: if the dictionary is a main dictionary for this + * language, and it has never been opted out through the dictionary interface, then we start + * installing it. For the user who enables a language and uses it for the first time, the + * dictionary should magically start being used a short time after they start typing. + * The mayPrompt argument indicates whether we should prompt the user for a decision to + * download or not, in case we decide we are in the case where we should download - this + * roughly happens when the current connectivity is 3G. See + * DictionaryProvider#getDictionaryWordListsForContentUri for details. + */ + // As opposed to many other methods, this method does not need the version of the word + // list because it may only install the latest version we know about for this specific + // word list ID / client ID combination. + public static void installIfNeverRequested(final Context context, final String clientId, + final String wordlistId) { + Log.i(TAG, "installIfNeverRequested() : ClientId = " + clientId + + " : WordListId = " + wordlistId); + final String[] idArray = wordlistId.split(DictionaryProvider.ID_CATEGORY_SEPARATOR); + // If we have a new-format dictionary id (category:manual_id), then use the + // specified category. Otherwise, it is a main dictionary, so force the + // MAIN category upon it. + final String category = 2 == idArray.length ? idArray[0] : MAIN_DICTIONARY_CATEGORY; + if (!MAIN_DICTIONARY_CATEGORY.equals(category)) { + // Not a main dictionary. We only auto-install main dictionaries, so we can return now. + return; + } + if (CommonPreferences.getCommonPreferences(context).contains(wordlistId)) { + // If some kind of settings has been done in the past for this specific id, then + // this is not a candidate for auto-install. Because it already is either true, + // in which case it may be installed or downloading or whatever, and we don't + // need to care about it because it's already handled or being handled, or it's false + // in which case it means the user explicitely turned it off and don't want to have + // it installed. So we quit right away. + return; + } + + final SQLiteDatabase db = MetadataDbHelper.getDb(context, clientId); + final ContentValues installCandidate = + MetadataDbHelper.getContentValuesOfLatestAvailableWordlistById(db, wordlistId); + if (MetadataDbHelper.STATUS_AVAILABLE + != installCandidate.getAsInteger(MetadataDbHelper.STATUS_COLUMN)) { + // If it's not "AVAILABLE", we want to stop now. Because candidates for auto-install + // are lists that we know are available, but we also know have never been installed. + // It does obviously not concern already installed lists, or downloading lists, + // or those that have been disabled, flagged as deleting... So anything else than + // AVAILABLE means we don't auto-install. + return; + } + + // We decided against prompting the user for a decision. This may be because we were + // explicitly asked not to, or because we are currently on wi-fi anyway, or because we + // already know the answer to the question. We'll enqueue a request ; StartDownloadAction + // knows to use the correct type of network according to the current settings. + + // Also note that once it's auto-installed, a word list will be marked as INSTALLED. It will + // thus receive automatic updates if there are any, which is what we want. If the user does + // not want this word list, they will have to go to the settings and change them, which will + // change the shared preferences. So there is no way for a word list that has been + // auto-installed once to get auto-installed again, and that's what we want. + final ActionBatch actions = new ActionBatch(); + WordListMetadata metadata = WordListMetadata.createFromContentValues(installCandidate); + actions.add(new ActionBatch.StartDownloadAction(clientId, metadata)); + final String localeString = installCandidate.getAsString(MetadataDbHelper.LOCALE_COLUMN); + + // We are in a content provider: we can't do any UI at all. We have to defer the displaying + // itself to the service. Also, we only display this when the user does not have a + // dictionary for this language already. During setup wizard, however, this UI is + // suppressed. + final boolean deviceProvisioned = Settings.Global.getInt(context.getContentResolver(), + Settings.Global.DEVICE_PROVISIONED, 0) != 0; + if (deviceProvisioned) { + final Intent intent = new Intent(); + intent.setClass(context, DictionaryService.class); + intent.setAction(DictionaryService.SHOW_DOWNLOAD_TOAST_INTENT_ACTION); + intent.putExtra(DictionaryService.LOCALE_INTENT_ARGUMENT, localeString); + context.startService(intent); + } else { + Log.i(TAG, "installIfNeverRequested() : Don't show download toast"); + } + + Log.i(TAG, "installIfNeverRequested() : StartDownloadAction for " + metadata); + actions.execute(context, new LogProblemReporter(TAG)); + } + + /** + * Marks the word list with the passed id as used. + * + * This will download/install the list as required. The action will see that the destination + * word list is a valid list, and take appropriate action - in this case, mark it as used. + * @see ActionBatch.Action#execute + * + * @param context the context for using action batches. + * @param clientId the id of the client. + * @param wordlistId the id of the word list to mark as installed. + * @param version the version of the word list to mark as installed. + * @param status the current status of the word list. + * @param allowDownloadOnMeteredData whether to download even on metered data connection + */ + // The version argument is not used yet, because we don't need it to retrieve the information + // we need. However, the pair (id, version) being the primary key to a word list in the database + // it feels better for consistency to pass it, and some methods retrieving information about a + // word list need it so we may need it in the future. + public static void markAsUsed(final Context context, final String clientId, + final String wordlistId, final int version, + final int status, final boolean allowDownloadOnMeteredData) { + final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( + context, clientId, wordlistId, version); + + if (null == wordListMetaData) return; + + final ActionBatch actions = new ActionBatch(); + if (MetadataDbHelper.STATUS_DISABLED == status + || MetadataDbHelper.STATUS_DELETING == status) { + actions.add(new ActionBatch.EnableAction(clientId, wordListMetaData)); + } else if (MetadataDbHelper.STATUS_AVAILABLE == status) { + actions.add(new ActionBatch.StartDownloadAction(clientId, wordListMetaData)); + } else { + Log.e(TAG, "Unexpected state of the word list for markAsUsed : " + status); + } + actions.execute(context, new LogProblemReporter(TAG)); + signalNewDictionaryState(context); + } + + /** + * Marks the word list with the passed id as unused. + * + * This leaves the file on the disk for ulterior use. The action will see that the destination + * word list is null, and take appropriate action - in this case, mark it as unused. + * @see ActionBatch.Action#execute + * + * @param context the context for using action batches. + * @param clientId the id of the client. + * @param wordlistId the id of the word list to mark as installed. + * @param version the version of the word list to mark as installed. + * @param status the current status of the word list. + */ + // The version and status arguments are not used yet, but this method matches its interface to + // markAsUsed for consistency. + public static void markAsUnused(final Context context, final String clientId, + final String wordlistId, final int version, final int status) { + + final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( + context, clientId, wordlistId, version); + + if (null == wordListMetaData) return; + final ActionBatch actions = new ActionBatch(); + actions.add(new ActionBatch.DisableAction(clientId, wordListMetaData)); + actions.execute(context, new LogProblemReporter(TAG)); + signalNewDictionaryState(context); + } + + /** + * Marks the word list with the passed id as deleting. + * + * This basically means that on the next chance there is (right away if Android Keyboard + * happens to be up, or the next time it gets up otherwise) the dictionary pack will + * supply an empty dictionary to it that will replace whatever dictionary is installed. + * This allows to release the space taken by a dictionary (except for the few bytes the + * empty dictionary takes up), and override a built-in default dictionary so that we + * can fake delete a built-in dictionary. + * + * @param context the context to open the database on. + * @param clientId the id of the client. + * @param wordlistId the id of the word list to mark as deleted. + * @param version the version of the word list to mark as deleted. + * @param status the current status of the word list. + */ + public static void markAsDeleting(final Context context, final String clientId, + final String wordlistId, final int version, final int status) { + + final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( + context, clientId, wordlistId, version); + + if (null == wordListMetaData) return; + final ActionBatch actions = new ActionBatch(); + actions.add(new ActionBatch.DisableAction(clientId, wordListMetaData)); + actions.add(new ActionBatch.StartDeleteAction(clientId, wordListMetaData)); + actions.execute(context, new LogProblemReporter(TAG)); + signalNewDictionaryState(context); + } + + /** + * Marks the word list with the passed id as actually deleted. + * + * This reverts to available status or deletes the row as appropriate. + * + * @param context the context to open the database on. + * @param clientId the id of the client. + * @param wordlistId the id of the word list to mark as deleted. + * @param version the version of the word list to mark as deleted. + * @param status the current status of the word list. + */ + public static void markAsDeleted(final Context context, final String clientId, + final String wordlistId, final int version, final int status) { + final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( + context, clientId, wordlistId, version); + + if (null == wordListMetaData) return; + + final ActionBatch actions = new ActionBatch(); + actions.add(new ActionBatch.FinishDeleteAction(clientId, wordListMetaData)); + actions.execute(context, new LogProblemReporter(TAG)); + signalNewDictionaryState(context); + } + + /** + * Checks whether the word list should be downloaded again; in which case an download & + * installation attempt is made. Otherwise the word list is marked broken. + * + * @param context the context to open the database on. + * @param clientId the id of the client. + * @param wordlistId the id of the word list which is broken. + * @param version the version of the broken word list. + */ + public static void markAsBrokenOrRetrying(final Context context, final String clientId, + final String wordlistId, final int version) { + boolean isRetryPossible = MetadataDbHelper.maybeMarkEntryAsRetrying( + MetadataDbHelper.getDb(context, clientId), wordlistId, version); + + if (isRetryPossible) { + if (DEBUG) { + Log.d(TAG, "Attempting to download & install the wordlist again."); + } + final WordListMetadata wordListMetaData = MetadataHandler.getCurrentMetadataForWordList( + context, clientId, wordlistId, version); + if (wordListMetaData == null) { + return; + } + + final ActionBatch actions = new ActionBatch(); + actions.add(new ActionBatch.StartDownloadAction(clientId, wordListMetaData)); + actions.execute(context, new LogProblemReporter(TAG)); + } else { + if (DEBUG) { + Log.d(TAG, "Retries for wordlist exhausted, deleting the wordlist from table."); + } + MetadataDbHelper.deleteEntry(MetadataDbHelper.getDb(context, clientId), + wordlistId, version); + } + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/WordListMetadata.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/WordListMetadata.java new file mode 100644 index 00000000..a0999c6a --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/WordListMetadata.java @@ -0,0 +1,135 @@ +/* + * 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 javax.annotation.Nonnull; + +/** + * The metadata for a single word list. + * + * Instances of this class are always immutable. + */ +public class WordListMetadata { + + public final String mId; + public final int mType; // Type, as of MetadataDbHelper#TYPE_* + public final String mDescription; + public final long mLastUpdate; + public final long mFileSize; + public final String mRawChecksum; + public final String mChecksum; + public final String mLocalFilename; + public final String mRemoteFilename; + public final int mVersion; // version of this word list + public final int mFlags; // Always 0 in this version, reserved for future use + public int mRetryCount; + + // The locale is matched against the locale requested by the client. The matching algorithm + // is a standard locale matching with fallback; it is implemented in + // DictionaryProvider#getDictionaryFileForContentUri. + public final String mLocale; + + + // Version number of the format. + // This implementation of the DictionaryDataService knows how to handle format 1 only. + // This is only for forward compatibility, to be able to upgrade the format without + // breaking old implementations. + public final int mFormatVersion; + + public WordListMetadata(final String id, final int type, + final String description, final long lastUpdate, final long fileSize, + final String rawChecksum, final String checksum, final int retryCount, + final String localFilename, final String remoteFilename, + final int version, final int formatVersion, + final int flags, final String locale) { + mId = id; + mType = type; + mDescription = description; + mLastUpdate = lastUpdate; // In milliseconds + mFileSize = fileSize; + mRawChecksum = rawChecksum; + mChecksum = checksum; + mRetryCount = retryCount; + mLocalFilename = localFilename; + mRemoteFilename = remoteFilename; + mVersion = version; + mFormatVersion = formatVersion; + mFlags = flags; + mLocale = locale; + } + + /** + * Create a WordListMetadata from the contents of a ContentValues. + * + * If this lacks any required field, IllegalArgumentException is thrown. + */ + public static WordListMetadata createFromContentValues(@Nonnull final ContentValues values) { + final String id = values.getAsString(MetadataDbHelper.WORDLISTID_COLUMN); + final Integer type = values.getAsInteger(MetadataDbHelper.TYPE_COLUMN); + final String description = values.getAsString(MetadataDbHelper.DESCRIPTION_COLUMN); + final Long lastUpdate = values.getAsLong(MetadataDbHelper.DATE_COLUMN); + final Long fileSize = values.getAsLong(MetadataDbHelper.FILESIZE_COLUMN); + final String rawChecksum = values.getAsString(MetadataDbHelper.RAW_CHECKSUM_COLUMN); + final String checksum = values.getAsString(MetadataDbHelper.CHECKSUM_COLUMN); + final int retryCount = values.getAsInteger(MetadataDbHelper.RETRY_COUNT_COLUMN); + final String localFilename = values.getAsString(MetadataDbHelper.LOCAL_FILENAME_COLUMN); + final String remoteFilename = values.getAsString(MetadataDbHelper.REMOTE_FILENAME_COLUMN); + final Integer version = values.getAsInteger(MetadataDbHelper.VERSION_COLUMN); + final Integer formatVersion = values.getAsInteger(MetadataDbHelper.FORMATVERSION_COLUMN); + final Integer flags = values.getAsInteger(MetadataDbHelper.FLAGS_COLUMN); + final String locale = values.getAsString(MetadataDbHelper.LOCALE_COLUMN); + if (null == id + || null == type + || null == description + || null == lastUpdate + || null == fileSize + || null == checksum + || null == localFilename + || null == remoteFilename + || null == version + || null == formatVersion + || null == flags + || null == locale) { + throw new IllegalArgumentException(); + } + return new WordListMetadata(id, type, description, lastUpdate, fileSize, rawChecksum, + checksum, retryCount, localFilename, remoteFilename, version, formatVersion, + flags, locale); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder(WordListMetadata.class.getSimpleName()); + sb.append(" : ").append(mId); + sb.append("\nType : ").append(mType); + sb.append("\nDescription : ").append(mDescription); + sb.append("\nLastUpdate : ").append(mLastUpdate); + sb.append("\nFileSize : ").append(mFileSize); + sb.append("\nRawChecksum : ").append(mRawChecksum); + sb.append("\nChecksum : ").append(mChecksum); + sb.append("\nRetryCount: ").append(mRetryCount); + sb.append("\nLocalFilename : ").append(mLocalFilename); + sb.append("\nRemoteFilename : ").append(mRemoteFilename); + sb.append("\nVersion : ").append(mVersion); + sb.append("\nFormatVersion : ").append(mFormatVersion); + sb.append("\nFlags : ").append(mFlags); + sb.append("\nLocale : ").append(mLocale); + return sb.toString(); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/WordListPreference.java b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/WordListPreference.java new file mode 100644 index 00000000..1b93d7b5 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/dictionarypack/WordListPreference.java @@ -0,0 +1,310 @@ +/** + * 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; +import android.preference.Preference; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.ListView; +import android.widget.TextView; + +import org.dslul.openboard.inputmethod.latin.R; + +import java.util.Locale; + +/** + * A preference for one word list. + * + * This preference refers to a single word list, as available in the dictionary + * pack. Upon being pressed, it displays a menu to allow the user to install, disable, + * enable or delete it as appropriate for the current state of the word list. + */ +public final class WordListPreference extends Preference { + private static final String TAG = WordListPreference.class.getSimpleName(); + + // What to display in the "status" field when we receive unknown data as a status from + // the content provider. Empty string sounds sensible. + private static final String NO_STATUS_MESSAGE = ""; + + /// Actions + private static final int ACTION_UNKNOWN = 0; + private static final int ACTION_ENABLE_DICT = 1; + private static final int ACTION_DISABLE_DICT = 2; + private static final int ACTION_DELETE_DICT = 3; + + // Members + // The metadata word list id and version of this word list. + public final String mWordlistId; + public final int mVersion; + public final Locale mLocale; + public final String mDescription; + + // The id of the client for which this preference is. + private final String mClientId; + // The status + private int mStatus; + // The size of the dictionary file + private final int mFilesize; + + private final DictionaryListInterfaceState mInterfaceState; + + public WordListPreference(final Context context, + final DictionaryListInterfaceState dictionaryListInterfaceState, final String clientId, + final String wordlistId, final int version, final Locale locale, + final String description, final int status, final int filesize) { + super(context, null); + mInterfaceState = dictionaryListInterfaceState; + mClientId = clientId; + mVersion = version; + mWordlistId = wordlistId; + mFilesize = filesize; + mLocale = locale; + mDescription = description; + + setLayoutResource(R.layout.dictionary_line); + + setTitle(description); + setStatus(status); + setKey(wordlistId); + } + + public void setStatus(final int status) { + if (status == mStatus) return; + mStatus = status; + setSummary(getSummary(status)); + } + + public boolean hasStatus(final int status) { + return status == mStatus; + } + + @Override + public View onCreateView(final ViewGroup parent) { + final View orphanedView = mInterfaceState.findFirstOrphanedView(); + if (null != orphanedView) return orphanedView; // Will be sent to onBindView + final View newView = super.onCreateView(parent); + return mInterfaceState.addToCacheAndReturnView(newView); + } + + public boolean hasPriorityOver(final int otherPrefStatus) { + // Both of these should be one of MetadataDbHelper.STATUS_* + return mStatus > otherPrefStatus; + } + + private String getSummary(final int status) { + final Context context = getContext(); + switch (status) { + // If we are deleting the word list, for the user it's like it's already deleted. + // It should be reinstallable. Exposing to the user the whole complexity of + // the delayed deletion process between the dictionary pack and Android Keyboard + // would only be confusing. + case MetadataDbHelper.STATUS_DELETING: + case MetadataDbHelper.STATUS_AVAILABLE: + return context.getString(R.string.dictionary_available); + case MetadataDbHelper.STATUS_DOWNLOADING: + return context.getString(R.string.dictionary_downloading); + case MetadataDbHelper.STATUS_INSTALLED: + return context.getString(R.string.dictionary_installed); + case MetadataDbHelper.STATUS_DISABLED: + return context.getString(R.string.dictionary_disabled); + default: + return NO_STATUS_MESSAGE; + } + } + + // The table below needs to be kept in sync with MetadataDbHelper.STATUS_* since it uses + // the values as indices. + private static final int sStatusActionList[][] = { + // MetadataDbHelper.STATUS_UNKNOWN + {}, + // MetadataDbHelper.STATUS_AVAILABLE + { ButtonSwitcher.STATUS_INSTALL, ACTION_ENABLE_DICT }, + // MetadataDbHelper.STATUS_DOWNLOADING + { ButtonSwitcher.STATUS_CANCEL, ACTION_DISABLE_DICT }, + // MetadataDbHelper.STATUS_INSTALLED + { ButtonSwitcher.STATUS_DELETE, ACTION_DELETE_DICT }, + // MetadataDbHelper.STATUS_DISABLED + { ButtonSwitcher.STATUS_DELETE, ACTION_DELETE_DICT }, + // MetadataDbHelper.STATUS_DELETING + // We show 'install' because the file is supposed to be deleted. + // The user may reinstall it. + { ButtonSwitcher.STATUS_INSTALL, ACTION_ENABLE_DICT } + }; + + static int getButtonSwitcherStatus(final int status) { + if (status >= sStatusActionList.length) { + Log.e(TAG, "Unknown status " + status); + return ButtonSwitcher.STATUS_NO_BUTTON; + } + return sStatusActionList[status][0]; + } + + static int getActionIdFromStatusAndMenuEntry(final int status) { + if (status >= sStatusActionList.length) { + Log.e(TAG, "Unknown status " + status); + return ACTION_UNKNOWN; + } + return sStatusActionList[status][1]; + } + + private void disableDict() { + final Context context = getContext(); + final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context); + CommonPreferences.disable(prefs, mWordlistId); + UpdateHandler.markAsUnused(context, mClientId, mWordlistId, mVersion, mStatus); + if (MetadataDbHelper.STATUS_DOWNLOADING == mStatus) { + setStatus(MetadataDbHelper.STATUS_AVAILABLE); + } else if (MetadataDbHelper.STATUS_INSTALLED == mStatus) { + // Interface-wise, we should no longer be able to come here. However, this is still + // the right thing to do if we do come here. + setStatus(MetadataDbHelper.STATUS_DISABLED); + } else { + Log.e(TAG, "Unexpected state of the word list for disabling " + mStatus); + } + } + + private void enableDict() { + final Context context = getContext(); + final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context); + CommonPreferences.enable(prefs, mWordlistId); + // Explicit enabling by the user : allow downloading on metered data connection. + UpdateHandler.markAsUsed(context, mClientId, mWordlistId, mVersion, mStatus, true); + if (MetadataDbHelper.STATUS_AVAILABLE == mStatus) { + setStatus(MetadataDbHelper.STATUS_DOWNLOADING); + } else if (MetadataDbHelper.STATUS_DISABLED == mStatus + || MetadataDbHelper.STATUS_DELETING == mStatus) { + // If the status is DELETING, it means Android Keyboard + // has not deleted the word list yet, so we can safely + // turn it to 'installed'. The status DISABLED is still supported internally to + // avoid breaking older installations and all but there should not be a way to + // disable a word list through the interface any more. + setStatus(MetadataDbHelper.STATUS_INSTALLED); + } else { + Log.e(TAG, "Unexpected state of the word list for enabling " + mStatus); + } + } + + private void deleteDict() { + final Context context = getContext(); + final SharedPreferences prefs = CommonPreferences.getCommonPreferences(context); + CommonPreferences.disable(prefs, mWordlistId); + setStatus(MetadataDbHelper.STATUS_DELETING); + UpdateHandler.markAsDeleting(context, mClientId, mWordlistId, mVersion, mStatus); + } + + @Override + protected void onBindView(final View view) { + super.onBindView(view); + ((ViewGroup)view).setLayoutTransition(null); + + final DictionaryDownloadProgressBar progressBar = + (DictionaryDownloadProgressBar)view.findViewById(R.id.dictionary_line_progress_bar); + final TextView status = (TextView)view.findViewById(android.R.id.summary); + progressBar.setIds(mClientId, mWordlistId); + progressBar.setMax(mFilesize); + final boolean showProgressBar = (MetadataDbHelper.STATUS_DOWNLOADING == mStatus); + setSummary(getSummary(mStatus)); + status.setVisibility(showProgressBar ? View.INVISIBLE : View.VISIBLE); + progressBar.setVisibility(showProgressBar ? View.VISIBLE : View.INVISIBLE); + + final ButtonSwitcher buttonSwitcher = (ButtonSwitcher)view.findViewById( + R.id.wordlist_button_switcher); + // We need to clear the state of the button switcher, because we reuse views; if we didn't + // reset it would animate from whatever its old state was. + buttonSwitcher.reset(mInterfaceState); + if (mInterfaceState.isOpen(mWordlistId)) { + // The button is open. + final int previousStatus = mInterfaceState.getStatus(mWordlistId); + buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(previousStatus)); + if (previousStatus != mStatus) { + // We come here if the status has changed since last time. We need to animate + // the transition. + buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(mStatus)); + mInterfaceState.setOpen(mWordlistId, mStatus); + } + } else { + // The button is closed. + buttonSwitcher.setStatusAndUpdateVisuals(ButtonSwitcher.STATUS_NO_BUTTON); + } + buttonSwitcher.setInternalOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + onActionButtonClicked(); + } + }); + view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(final View v) { + onWordListClicked(v); + } + }); + } + + void onWordListClicked(final View v) { + // Note : v is the preference view + final ViewParent parent = v.getParent(); + // Just in case something changed in the framework, test for the concrete class + if (!(parent instanceof ListView)) return; + final ListView listView = (ListView)parent; + final int indexToOpen; + // Close all first, we'll open back any item that needs to be open. + final boolean wasOpen = mInterfaceState.isOpen(mWordlistId); + mInterfaceState.closeAll(); + if (wasOpen) { + // This button being shown. Take note that we don't want to open any button in the + // loop below. + indexToOpen = -1; + } else { + // This button was not being shown. Open it, and remember the index of this + // child as the one to open in the following loop. + mInterfaceState.setOpen(mWordlistId, mStatus); + indexToOpen = listView.indexOfChild(v); + } + final int lastDisplayedIndex = + listView.getLastVisiblePosition() - listView.getFirstVisiblePosition(); + // The "lastDisplayedIndex" is actually displayed, hence the <= + for (int i = 0; i <= lastDisplayedIndex; ++i) { + final ButtonSwitcher buttonSwitcher = (ButtonSwitcher)listView.getChildAt(i) + .findViewById(R.id.wordlist_button_switcher); + if (i == indexToOpen) { + buttonSwitcher.setStatusAndUpdateVisuals(getButtonSwitcherStatus(mStatus)); + } else { + buttonSwitcher.setStatusAndUpdateVisuals(ButtonSwitcher.STATUS_NO_BUTTON); + } + } + } + + void onActionButtonClicked() { + switch (getActionIdFromStatusAndMenuEntry(mStatus)) { + case ACTION_ENABLE_DICT: + enableDict(); + break; + case ACTION_DISABLE_DICT: + disableDict(); + break; + case ACTION_DELETE_DICT: + deleteDict(); + break; + default: + Log.e(TAG, "Unknown menu item pressed"); + } + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/event/Combiner.java b/app/src/main/java/org/dslul/openboard/inputmethod/event/Combiner.java new file mode 100644 index 00000000..ee14a59e --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/event/Combiner.java @@ -0,0 +1,51 @@ +/* + * 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.event; + +import java.util.ArrayList; + +import javax.annotation.Nonnull; + +/** + * A generic interface for combiners. Combiners are objects that transform chains of input events + * into committable strings and manage feedback to show to the user on the combining state. + */ +public interface Combiner { + /** + * Process an event, possibly combining it with the existing state and return the new event. + * + * If this event does not result in any new event getting passed down the chain, this method + * returns null. It may also modify the previous event list if appropriate. + * + * @param previousEvents the previous events in this composition. + * @param event the event to combine with the existing state. + * @return the resulting event. + */ + @Nonnull + Event processEvent(ArrayList previousEvents, Event event); + + /** + * Get the feedback that should be shown to the user for the current state of this combiner. + * @return A CharSequence representing the feedback to show users. It may include styles. + */ + CharSequence getCombiningStateFeedback(); + + /** + * Reset the state of this combiner, for example when the cursor was moved. + */ + void reset(); +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/event/CombinerChain.java b/app/src/main/java/org/dslul/openboard/inputmethod/event/CombinerChain.java new file mode 100644 index 00000000..f7e4f9eb --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/event/CombinerChain.java @@ -0,0 +1,137 @@ +/* + * 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.event; + +import android.text.SpannableStringBuilder; +import android.text.TextUtils; + +import org.dslul.openboard.inputmethod.latin.common.Constants; + +import java.util.ArrayList; + +import javax.annotation.Nonnull; + +/** + * This class implements the logic chain between receiving events and generating code points. + * + * Event sources are multiple. It may be a hardware keyboard, a D-PAD, a software keyboard, + * or any exotic input source. + * This class will orchestrate the composing chain that starts with an event as its input. Each + * composer will be given turns one after the other. + * The output is composed of two sequences of code points: the first, representing the already + * finished combining part, will be shown normally as the composing string, while the second is + * feedback on the composing state and will typically be shown with different styling such as + * a colored background. + */ +public class CombinerChain { + // The already combined text, as described above + private StringBuilder mCombinedText; + // The feedback on the composing state, as described above + private SpannableStringBuilder mStateFeedback; + private final ArrayList mCombiners; + + /** + * Create an combiner chain. + * + * The combiner chain takes events as inputs and outputs code points and combining state. + * For example, if the input language is Japanese, the combining chain will typically perform + * kana conversion. This takes a string for initial text, taken to be present before the + * cursor: we'll start after this. + * + * @param initialText The text that has already been combined so far. + */ + public CombinerChain(final String initialText) { + mCombiners = new ArrayList<>(); + // The dead key combiner is always active, and always first + mCombiners.add(new DeadKeyCombiner()); + mCombinedText = new StringBuilder(initialText); + mStateFeedback = new SpannableStringBuilder(); + } + + public void reset() { + mCombinedText.setLength(0); + mStateFeedback.clear(); + for (final Combiner c : mCombiners) { + c.reset(); + } + } + + private void updateStateFeedback() { + mStateFeedback.clear(); + for (int i = mCombiners.size() - 1; i >= 0; --i) { + mStateFeedback.append(mCombiners.get(i).getCombiningStateFeedback()); + } + } + + /** + * Process an event through the combining chain, and return a processed event to apply. + * @param previousEvents the list of previous events in this composition + * @param newEvent the new event to process + * @return the processed event. It may be the same event, or a consumed event, or a completely + * new event. However it may never be null. + */ + @Nonnull + public Event processEvent(final ArrayList previousEvents, + @Nonnull final Event newEvent) { + final ArrayList modifiablePreviousEvents = new ArrayList<>(previousEvents); + Event event = newEvent; + for (final Combiner combiner : mCombiners) { + // A combiner can never return more than one event; it can return several + // code points, but they should be encapsulated within one event. + event = combiner.processEvent(modifiablePreviousEvents, event); + if (event.isConsumed()) { + // If the event is consumed, then we don't pass it to subsequent combiners: + // they should not see it at all. + break; + } + } + updateStateFeedback(); + return event; + } + + /** + * Apply a processed event. + * @param event the event to be applied + */ + public void applyProcessedEvent(final Event event) { + if (null != event) { + // TODO: figure out the generic way of doing this + if (Constants.CODE_DELETE == event.mKeyCode) { + final int length = mCombinedText.length(); + if (length > 0) { + final int lastCodePoint = mCombinedText.codePointBefore(length); + mCombinedText.delete(length - Character.charCount(lastCodePoint), length); + } + } else { + final CharSequence textToCommit = event.getTextToCommit(); + if (!TextUtils.isEmpty(textToCommit)) { + mCombinedText.append(textToCommit); + } + } + } + updateStateFeedback(); + } + + /** + * Get the char sequence that should be displayed as the composing word. It may include + * styling spans. + */ + public CharSequence getComposingWordWithCombiningFeedback() { + final SpannableStringBuilder s = new SpannableStringBuilder(mCombinedText); + return s.append(mStateFeedback); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/event/DeadKeyCombiner.java b/app/src/main/java/org/dslul/openboard/inputmethod/event/DeadKeyCombiner.java new file mode 100644 index 00000000..7843e066 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/event/DeadKeyCombiner.java @@ -0,0 +1,303 @@ +/* + * 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.event; + +import android.text.TextUtils; +import android.util.SparseIntArray; + +import org.dslul.openboard.inputmethod.latin.common.Constants; + +import java.text.Normalizer; +import java.util.ArrayList; + +import javax.annotation.Nonnull; + +/** + * A combiner that handles dead keys. + */ +public class DeadKeyCombiner implements Combiner { + + private static class Data { + // This class data taken from KeyCharacterMap.java. + + /* Characters used to display placeholders for dead keys. */ + private static final int ACCENT_ACUTE = '\u00B4'; + private static final int ACCENT_BREVE = '\u02D8'; + private static final int ACCENT_CARON = '\u02C7'; + private static final int ACCENT_CEDILLA = '\u00B8'; + private static final int ACCENT_CIRCUMFLEX = '\u02C6'; + private static final int ACCENT_COMMA_ABOVE = '\u1FBD'; + private static final int ACCENT_COMMA_ABOVE_RIGHT = '\u02BC'; + private static final int ACCENT_DOT_ABOVE = '\u02D9'; + private static final int ACCENT_DOT_BELOW = Constants.CODE_PERIOD; // approximate + private static final int ACCENT_DOUBLE_ACUTE = '\u02DD'; + private static final int ACCENT_GRAVE = '\u02CB'; + private static final int ACCENT_HOOK_ABOVE = '\u02C0'; + private static final int ACCENT_HORN = Constants.CODE_SINGLE_QUOTE; // approximate + private static final int ACCENT_MACRON = '\u00AF'; + private static final int ACCENT_MACRON_BELOW = '\u02CD'; + private static final int ACCENT_OGONEK = '\u02DB'; + private static final int ACCENT_REVERSED_COMMA_ABOVE = '\u02BD'; + private static final int ACCENT_RING_ABOVE = '\u02DA'; + private static final int ACCENT_STROKE = Constants.CODE_DASH; // approximate + private static final int ACCENT_TILDE = '\u02DC'; + private static final int ACCENT_TURNED_COMMA_ABOVE = '\u02BB'; + private static final int ACCENT_UMLAUT = '\u00A8'; + private static final int ACCENT_VERTICAL_LINE_ABOVE = '\u02C8'; + private static final int ACCENT_VERTICAL_LINE_BELOW = '\u02CC'; + + /* Legacy dead key display characters used in previous versions of the API (before L) + * We still support these characters by mapping them to their non-legacy version. */ + private static final int ACCENT_GRAVE_LEGACY = Constants.CODE_GRAVE_ACCENT; + private static final int ACCENT_CIRCUMFLEX_LEGACY = Constants.CODE_CIRCUMFLEX_ACCENT; + private static final int ACCENT_TILDE_LEGACY = Constants.CODE_TILDE; + + /** + * Maps Unicode combining diacritical to display-form dead key. + */ + static final SparseIntArray sCombiningToAccent = new SparseIntArray(); + static final SparseIntArray sAccentToCombining = new SparseIntArray(); + static { + // U+0300: COMBINING GRAVE ACCENT + addCombining('\u0300', ACCENT_GRAVE); + // U+0301: COMBINING ACUTE ACCENT + addCombining('\u0301', ACCENT_ACUTE); + // U+0302: COMBINING CIRCUMFLEX ACCENT + addCombining('\u0302', ACCENT_CIRCUMFLEX); + // U+0303: COMBINING TILDE + addCombining('\u0303', ACCENT_TILDE); + // U+0304: COMBINING MACRON + addCombining('\u0304', ACCENT_MACRON); + // U+0306: COMBINING BREVE + addCombining('\u0306', ACCENT_BREVE); + // U+0307: COMBINING DOT ABOVE + addCombining('\u0307', ACCENT_DOT_ABOVE); + // U+0308: COMBINING DIAERESIS + addCombining('\u0308', ACCENT_UMLAUT); + // U+0309: COMBINING HOOK ABOVE + addCombining('\u0309', ACCENT_HOOK_ABOVE); + // U+030A: COMBINING RING ABOVE + addCombining('\u030A', ACCENT_RING_ABOVE); + // U+030B: COMBINING DOUBLE ACUTE ACCENT + addCombining('\u030B', ACCENT_DOUBLE_ACUTE); + // U+030C: COMBINING CARON + addCombining('\u030C', ACCENT_CARON); + // U+030D: COMBINING VERTICAL LINE ABOVE + addCombining('\u030D', ACCENT_VERTICAL_LINE_ABOVE); + // U+030E: COMBINING DOUBLE VERTICAL LINE ABOVE + //addCombining('\u030E', ACCENT_DOUBLE_VERTICAL_LINE_ABOVE); + // U+030F: COMBINING DOUBLE GRAVE ACCENT + //addCombining('\u030F', ACCENT_DOUBLE_GRAVE); + // U+0310: COMBINING CANDRABINDU + //addCombining('\u0310', ACCENT_CANDRABINDU); + // U+0311: COMBINING INVERTED BREVE + //addCombining('\u0311', ACCENT_INVERTED_BREVE); + // U+0312: COMBINING TURNED COMMA ABOVE + addCombining('\u0312', ACCENT_TURNED_COMMA_ABOVE); + // U+0313: COMBINING COMMA ABOVE + addCombining('\u0313', ACCENT_COMMA_ABOVE); + // U+0314: COMBINING REVERSED COMMA ABOVE + addCombining('\u0314', ACCENT_REVERSED_COMMA_ABOVE); + // U+0315: COMBINING COMMA ABOVE RIGHT + addCombining('\u0315', ACCENT_COMMA_ABOVE_RIGHT); + // U+031B: COMBINING HORN + addCombining('\u031B', ACCENT_HORN); + // U+0323: COMBINING DOT BELOW + addCombining('\u0323', ACCENT_DOT_BELOW); + // U+0326: COMBINING COMMA BELOW + //addCombining('\u0326', ACCENT_COMMA_BELOW); + // U+0327: COMBINING CEDILLA + addCombining('\u0327', ACCENT_CEDILLA); + // U+0328: COMBINING OGONEK + addCombining('\u0328', ACCENT_OGONEK); + // U+0329: COMBINING VERTICAL LINE BELOW + addCombining('\u0329', ACCENT_VERTICAL_LINE_BELOW); + // U+0331: COMBINING MACRON BELOW + addCombining('\u0331', ACCENT_MACRON_BELOW); + // U+0335: COMBINING SHORT STROKE OVERLAY + addCombining('\u0335', ACCENT_STROKE); + // U+0342: COMBINING GREEK PERISPOMENI + //addCombining('\u0342', ACCENT_PERISPOMENI); + // U+0344: COMBINING GREEK DIALYTIKA TONOS + //addCombining('\u0344', ACCENT_DIALYTIKA_TONOS); + // U+0345: COMBINING GREEK YPOGEGRAMMENI + //addCombining('\u0345', ACCENT_YPOGEGRAMMENI); + + // One-way mappings to equivalent preferred accents. + // U+0340: COMBINING GRAVE TONE MARK + sCombiningToAccent.append('\u0340', ACCENT_GRAVE); + // U+0341: COMBINING ACUTE TONE MARK + sCombiningToAccent.append('\u0341', ACCENT_ACUTE); + // U+0343: COMBINING GREEK KORONIS + sCombiningToAccent.append('\u0343', ACCENT_COMMA_ABOVE); + + // One-way legacy mappings to preserve compatibility with older applications. + // U+0300: COMBINING GRAVE ACCENT + sAccentToCombining.append(ACCENT_GRAVE_LEGACY, '\u0300'); + // U+0302: COMBINING CIRCUMFLEX ACCENT + sAccentToCombining.append(ACCENT_CIRCUMFLEX_LEGACY, '\u0302'); + // U+0303: COMBINING TILDE + sAccentToCombining.append(ACCENT_TILDE_LEGACY, '\u0303'); + } + + private static void addCombining(int combining, int accent) { + sCombiningToAccent.append(combining, accent); + sAccentToCombining.append(accent, combining); + } + + // Caution! This may only contain chars, not supplementary code points. It's unlikely + // it will ever need to, but if it does we'll have to change this + private static final SparseIntArray sNonstandardDeadCombinations = new SparseIntArray(); + static { + // Non-standard decompositions. + // Stroke modifier for Finnish multilingual keyboard and others. + // U+0110: LATIN CAPITAL LETTER D WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'D', '\u0110'); + // U+01E4: LATIN CAPITAL LETTER G WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'G', '\u01e4'); + // U+0126: LATIN CAPITAL LETTER H WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'H', '\u0126'); + // U+0197: LATIN CAPITAL LETTER I WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'I', '\u0197'); + // U+0141: LATIN CAPITAL LETTER L WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'L', '\u0141'); + // U+00D8: LATIN CAPITAL LETTER O WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'O', '\u00d8'); + // U+0166: LATIN CAPITAL LETTER T WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'T', '\u0166'); + // U+0111: LATIN SMALL LETTER D WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'd', '\u0111'); + // U+01E5: LATIN SMALL LETTER G WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'g', '\u01e5'); + // U+0127: LATIN SMALL LETTER H WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'h', '\u0127'); + // U+0268: LATIN SMALL LETTER I WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'i', '\u0268'); + // U+0142: LATIN SMALL LETTER L WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'l', '\u0142'); + // U+00F8: LATIN SMALL LETTER O WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 'o', '\u00f8'); + // U+0167: LATIN SMALL LETTER T WITH STROKE + addNonStandardDeadCombination(ACCENT_STROKE, 't', '\u0167'); + } + + private static void addNonStandardDeadCombination(final int deadCodePoint, + final int spacingCodePoint, final int result) { + final int combination = (deadCodePoint << 16) | spacingCodePoint; + sNonstandardDeadCombinations.put(combination, result); + } + + public static final int NOT_A_CHAR = 0; + public static final int BITS_TO_SHIFT_DEAD_CODE_POINT_FOR_NON_STANDARD_COMBINATION = 16; + // Get a non-standard combination + public static char getNonstandardCombination(final int deadCodePoint, + final int spacingCodePoint) { + final int combination = spacingCodePoint | + (deadCodePoint << BITS_TO_SHIFT_DEAD_CODE_POINT_FOR_NON_STANDARD_COMBINATION); + return (char)sNonstandardDeadCombinations.get(combination, NOT_A_CHAR); + } + } + + // TODO: make this a list of events instead + final StringBuilder mDeadSequence = new StringBuilder(); + + @Nonnull + private static Event createEventChainFromSequence(final @Nonnull CharSequence text, + @Nonnull final Event originalEvent) { + int index = text.length(); + if (index <= 0) { + return originalEvent; + } + Event lastEvent = null; + do { + final int codePoint = Character.codePointBefore(text, index); + lastEvent = Event.createHardwareKeypressEvent(codePoint, + originalEvent.mKeyCode, lastEvent, false /* isKeyRepeat */); + index -= Character.charCount(codePoint); + } while (index > 0); + return lastEvent; + } + + @Override + @Nonnull + public Event processEvent(final ArrayList previousEvents, final Event event) { + if (TextUtils.isEmpty(mDeadSequence)) { + // No dead char is currently being tracked: this is the most common case. + if (event.isDead()) { + // The event was a dead key. Start tracking it. + mDeadSequence.appendCodePoint(event.mCodePoint); + return Event.createConsumedEvent(event); + } + // Regular keystroke when not keeping track of a dead key. Simply said, there are + // no dead keys at all in the current input, so this combiner has nothing to do and + // simply returns the event as is. The majority of events will go through this path. + return event; + } + if (Character.isWhitespace(event.mCodePoint) + || event.mCodePoint == mDeadSequence.codePointBefore(mDeadSequence.length())) { + // When whitespace or twice the same dead key, we should output the dead sequence as is. + final Event resultEvent = createEventChainFromSequence(mDeadSequence.toString(), + event); + mDeadSequence.setLength(0); + return resultEvent; + } + if (event.isFunctionalKeyEvent()) { + if (Constants.CODE_DELETE == event.mKeyCode) { + // Remove the last code point + final int trimIndex = mDeadSequence.length() - Character.charCount( + mDeadSequence.codePointBefore(mDeadSequence.length())); + mDeadSequence.setLength(trimIndex); + return Event.createConsumedEvent(event); + } + return event; + } + if (event.isDead()) { + mDeadSequence.appendCodePoint(event.mCodePoint); + return Event.createConsumedEvent(event); + } + // Combine normally. + final StringBuilder sb = new StringBuilder(); + sb.appendCodePoint(event.mCodePoint); + int codePointIndex = 0; + while (codePointIndex < mDeadSequence.length()) { + final int deadCodePoint = mDeadSequence.codePointAt(codePointIndex); + final char replacementSpacingChar = + Data.getNonstandardCombination(deadCodePoint, event.mCodePoint); + if (Data.NOT_A_CHAR != replacementSpacingChar) { + sb.setCharAt(0, replacementSpacingChar); + } else { + final int combining = Data.sAccentToCombining.get(deadCodePoint); + sb.appendCodePoint(0 == combining ? deadCodePoint : combining); + } + codePointIndex += Character.isSupplementaryCodePoint(deadCodePoint) ? 2 : 1; + } + final String normalizedString = Normalizer.normalize(sb, Normalizer.Form.NFC); + final Event resultEvent = createEventChainFromSequence(normalizedString, event); + mDeadSequence.setLength(0); + return resultEvent; + } + + @Override + public void reset() { + mDeadSequence.setLength(0); + } + + @Override + public CharSequence getCombiningStateFeedback() { + return mDeadSequence; + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/event/Event.java b/app/src/main/java/org/dslul/openboard/inputmethod/event/Event.java new file mode 100644 index 00000000..4cfcd23b --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/event/Event.java @@ -0,0 +1,319 @@ +/* + * 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.event; + +import org.dslul.openboard.inputmethod.annotations.ExternallyReferenced; +import org.dslul.openboard.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import org.dslul.openboard.inputmethod.latin.common.Constants; +import org.dslul.openboard.inputmethod.latin.common.StringUtils; + +import javax.annotation.Nonnull; + +/** + * Class representing a generic input event as handled by Latin IME. + * + * This contains information about the origin of the event, but it is generalized and should + * represent a software keypress, hardware keypress, or d-pad move alike. + * Very importantly, this does not necessarily result in inputting one character, or even anything + * at all - it may be a dead key, it may be a partial input, it may be a special key on the + * keyboard, it may be a cancellation of a keypress (e.g. in a soft keyboard the finger of the + * user has slid out of the key), etc. It may also be a batch input from a gesture or handwriting + * for example. + * The combiner should figure out what to do with this. + */ +public class Event { + // Should the types below be represented by separate classes instead? It would be cleaner + // but probably a bit too much + // An event we don't handle in Latin IME, for example pressing Ctrl on a hardware keyboard. + final public static int EVENT_TYPE_NOT_HANDLED = 0; + // A key press that is part of input, for example pressing an alphabetic character on a + // hardware qwerty keyboard. It may be part of a sequence that will be re-interpreted later + // through combination. + final public static int EVENT_TYPE_INPUT_KEYPRESS = 1; + // A toggle event is triggered by a key that affects the previous character. An example would + // be a numeric key on a 10-key keyboard, which would toggle between 1 - a - b - c with + // repeated presses. + final public static int EVENT_TYPE_TOGGLE = 2; + // A mode event instructs the combiner to change modes. The canonical example would be the + // hankaku/zenkaku key on a Japanese keyboard, or even the caps lock key on a qwerty keyboard + // if handled at the combiner level. + final public static int EVENT_TYPE_MODE_KEY = 3; + // An event corresponding to a gesture. + final public static int EVENT_TYPE_GESTURE = 4; + // An event corresponding to the manual pick of a suggestion. + final public static int EVENT_TYPE_SUGGESTION_PICKED = 5; + // An event corresponding to a string generated by some software process. + final public static int EVENT_TYPE_SOFTWARE_GENERATED_STRING = 6; + // An event corresponding to a cursor move + final public static int EVENT_TYPE_CURSOR_MOVE = 7; + + // 0 is a valid code point, so we use -1 here. + final public static int NOT_A_CODE_POINT = -1; + // -1 is a valid key code, so we use 0 here. + final public static int NOT_A_KEY_CODE = 0; + + final private static int FLAG_NONE = 0; + // This event is a dead character, usually input by a dead key. Examples include dead-acute + // or dead-abovering. + final private static int FLAG_DEAD = 0x1; + // This event is coming from a key repeat, software or hardware. + final private static int FLAG_REPEAT = 0x2; + // This event has already been consumed. + final private static int FLAG_CONSUMED = 0x4; + + final private int mEventType; // The type of event - one of the constants above + // The code point associated with the event, if relevant. This is a unicode code point, and + // has nothing to do with other representations of the key. It is only relevant if this event + // is of KEYPRESS type, but for a mode key like hankaku/zenkaku or ctrl, there is no code point + // associated so this should be NOT_A_CODE_POINT to avoid unintentional use of its value when + // it's not relevant. + final public int mCodePoint; + + // If applicable, this contains the string that should be input. + final public CharSequence mText; + + // The key code associated with the event, if relevant. This is relevant whenever this event + // has been triggered by a key press, but not for a gesture for example. This has conceptually + // no link to the code point, although keys that enter a straight code point may often set + // this to be equal to mCodePoint for convenience. If this is not a key, this must contain + // NOT_A_KEY_CODE. + final public int mKeyCode; + + // Coordinates of the touch event, if relevant. If useful, we may want to replace this with + // a MotionEvent or something in the future. This is only relevant when the keypress is from + // a software keyboard obviously, unless there are touch-sensitive hardware keyboards in the + // future or some other awesome sauce. + final public int mX; + final public int mY; + + // Some flags that can't go into the key code. It's a bit field of FLAG_* + final private int mFlags; + + // If this is of type EVENT_TYPE_SUGGESTION_PICKED, this must not be null (and must be null in + // other cases). + final public SuggestedWordInfo mSuggestedWordInfo; + + // The next event, if any. Null if there is no next event yet. + final public Event mNextEvent; + + // This method is private - to create a new event, use one of the create* utility methods. + private Event(final int type, final CharSequence text, final int codePoint, final int keyCode, + final int x, final int y, final SuggestedWordInfo suggestedWordInfo, final int flags, + final Event next) { + mEventType = type; + mText = text; + mCodePoint = codePoint; + mKeyCode = keyCode; + mX = x; + mY = y; + mSuggestedWordInfo = suggestedWordInfo; + mFlags = flags; + mNextEvent = next; + // Sanity checks + // mSuggestedWordInfo is non-null if and only if the type is SUGGESTION_PICKED + if (EVENT_TYPE_SUGGESTION_PICKED == mEventType) { + if (null == mSuggestedWordInfo) { + throw new RuntimeException("Wrong event: SUGGESTION_PICKED event must have a " + + "non-null SuggestedWordInfo"); + } + } else { + if (null != mSuggestedWordInfo) { + throw new RuntimeException("Wrong event: only SUGGESTION_PICKED events may have " + + "a non-null SuggestedWordInfo"); + } + } + } + + @Nonnull + public static Event createSoftwareKeypressEvent(final int codePoint, final int keyCode, + final int x, final int y, final boolean isKeyRepeat) { + return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, keyCode, x, y, + null /* suggestedWordInfo */, isKeyRepeat ? FLAG_REPEAT : FLAG_NONE, null); + } + + @Nonnull + public static Event createHardwareKeypressEvent(final int codePoint, final int keyCode, + final Event next, final boolean isKeyRepeat) { + return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, keyCode, + Constants.EXTERNAL_KEYBOARD_COORDINATE, Constants.EXTERNAL_KEYBOARD_COORDINATE, + null /* suggestedWordInfo */, isKeyRepeat ? FLAG_REPEAT : FLAG_NONE, next); + } + + // This creates an input event for a dead character. @see {@link #FLAG_DEAD} + @ExternallyReferenced + @Nonnull + public static Event createDeadEvent(final int codePoint, final int keyCode, final Event next) { + // TODO: add an argument or something if we ever create a software layout with dead keys. + return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, keyCode, + Constants.EXTERNAL_KEYBOARD_COORDINATE, Constants.EXTERNAL_KEYBOARD_COORDINATE, + null /* suggestedWordInfo */, FLAG_DEAD, next); + } + + /** + * Create an input event with nothing but a code point. This is the most basic possible input + * event; it contains no information on many things the IME requires to function correctly, + * so avoid using it unless really nothing is known about this input. + * @param codePoint the code point. + * @return an event for this code point. + */ + @Nonnull + public static Event createEventForCodePointFromUnknownSource(final int codePoint) { + // TODO: should we have a different type of event for this? After all, it's not a key press. + return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, NOT_A_KEY_CODE, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, + null /* suggestedWordInfo */, FLAG_NONE, null /* next */); + } + + /** + * Creates an input event with a code point and x, y coordinates. This is typically used when + * resuming a previously-typed word, when the coordinates are still known. + * @param codePoint the code point to input. + * @param x the X coordinate. + * @param y the Y coordinate. + * @return an event for this code point and coordinates. + */ + @Nonnull + public static Event createEventForCodePointFromAlreadyTypedText(final int codePoint, + final int x, final int y) { + // TODO: should we have a different type of event for this? After all, it's not a key press. + return new Event(EVENT_TYPE_INPUT_KEYPRESS, null /* text */, codePoint, NOT_A_KEY_CODE, + x, y, null /* suggestedWordInfo */, FLAG_NONE, null /* next */); + } + + /** + * Creates an input event representing the manual pick of a suggestion. + * @return an event for this suggestion pick. + */ + @Nonnull + public static Event createSuggestionPickedEvent(final SuggestedWordInfo suggestedWordInfo) { + return new Event(EVENT_TYPE_SUGGESTION_PICKED, suggestedWordInfo.mWord, + NOT_A_CODE_POINT, NOT_A_KEY_CODE, + Constants.SUGGESTION_STRIP_COORDINATE, Constants.SUGGESTION_STRIP_COORDINATE, + suggestedWordInfo, FLAG_NONE, null /* next */); + } + + /** + * Creates an input event with a CharSequence. This is used by some software processes whose + * output is a string, possibly with styling. Examples include press on a multi-character key, + * or combination that outputs a string. + * @param text the CharSequence associated with this event. + * @param keyCode the key code, or NOT_A_KEYCODE if not applicable. + * @return an event for this text. + */ + @Nonnull + public static Event createSoftwareTextEvent(final CharSequence text, final int keyCode) { + return new Event(EVENT_TYPE_SOFTWARE_GENERATED_STRING, text, NOT_A_CODE_POINT, keyCode, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, + null /* suggestedWordInfo */, FLAG_NONE, null /* next */); + } + + /** + * Creates an input event representing the manual pick of a punctuation suggestion. + * @return an event for this suggestion pick. + */ + @Nonnull + public static Event createPunctuationSuggestionPickedEvent( + final SuggestedWordInfo suggestedWordInfo) { + final int primaryCode = suggestedWordInfo.mWord.charAt(0); + return new Event(EVENT_TYPE_SUGGESTION_PICKED, suggestedWordInfo.mWord, primaryCode, + NOT_A_KEY_CODE, Constants.SUGGESTION_STRIP_COORDINATE, + Constants.SUGGESTION_STRIP_COORDINATE, suggestedWordInfo, FLAG_NONE, + null /* next */); + } + + /** + * Creates an input event representing moving the cursor. The relative move amount is stored + * in mX. + * @param moveAmount the relative move amount. + * @return an event for this cursor move. + */ + @Nonnull + public static Event createCursorMovedEvent(final int moveAmount) { + return new Event(EVENT_TYPE_CURSOR_MOVE, null, NOT_A_CODE_POINT, NOT_A_KEY_CODE, + moveAmount, Constants.NOT_A_COORDINATE, null, FLAG_NONE, null); + } + + /** + * Creates an event identical to the passed event, but that has already been consumed. + * @param source the event to copy the properties of. + * @return an identical event marked as consumed. + */ + @Nonnull + public static Event createConsumedEvent(final Event source) { + // A consumed event should not input any text at all, so we pass the empty string as text. + return new Event(source.mEventType, source.mText, source.mCodePoint, source.mKeyCode, + source.mX, source.mY, source.mSuggestedWordInfo, source.mFlags | FLAG_CONSUMED, + source.mNextEvent); + } + + @Nonnull + public static Event createNotHandledEvent() { + return new Event(EVENT_TYPE_NOT_HANDLED, null /* text */, NOT_A_CODE_POINT, NOT_A_KEY_CODE, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, + null /* suggestedWordInfo */, FLAG_NONE, null); + } + + // Returns whether this is a function key like backspace, ctrl, settings... as opposed to keys + // that result in input like letters or space. + public boolean isFunctionalKeyEvent() { + // This logic may need to be refined in the future + return NOT_A_CODE_POINT == mCodePoint; + } + + // Returns whether this event is for a dead character. @see {@link #FLAG_DEAD} + public boolean isDead() { + return 0 != (FLAG_DEAD & mFlags); + } + + public boolean isKeyRepeat() { + return 0 != (FLAG_REPEAT & mFlags); + } + + public boolean isConsumed() { return 0 != (FLAG_CONSUMED & mFlags); } + + public boolean isGesture() { return EVENT_TYPE_GESTURE == mEventType; } + + // Returns whether this is a fake key press from the suggestion strip. This happens with + // punctuation signs selected from the suggestion strip. + public boolean isSuggestionStripPress() { + return EVENT_TYPE_SUGGESTION_PICKED == mEventType; + } + + public boolean isHandled() { + return EVENT_TYPE_NOT_HANDLED != mEventType; + } + + public CharSequence getTextToCommit() { + if (isConsumed()) { + return ""; // A consumed event should input no text. + } + switch (mEventType) { + case EVENT_TYPE_MODE_KEY: + case EVENT_TYPE_NOT_HANDLED: + case EVENT_TYPE_TOGGLE: + case EVENT_TYPE_CURSOR_MOVE: + return ""; + case EVENT_TYPE_INPUT_KEYPRESS: + return StringUtils.newSingleCodePointString(mCodePoint); + case EVENT_TYPE_GESTURE: + case EVENT_TYPE_SOFTWARE_GENERATED_STRING: + case EVENT_TYPE_SUGGESTION_PICKED: + return mText; + } + throw new RuntimeException("Unknown event type: " + mEventType); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/event/EventDecoder.java b/app/src/main/java/org/dslul/openboard/inputmethod/event/EventDecoder.java new file mode 100644 index 00000000..d5c6ae1a --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/event/EventDecoder.java @@ -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.event; + +/** + * A generic interface for event decoders. + */ +public interface EventDecoder { + +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/event/HardwareEventDecoder.java b/app/src/main/java/org/dslul/openboard/inputmethod/event/HardwareEventDecoder.java new file mode 100644 index 00000000..2d8c8db0 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/event/HardwareEventDecoder.java @@ -0,0 +1,26 @@ +/* + * 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.event; + +import android.view.KeyEvent; + +/** + * An event decoder for hardware events. + */ +public interface HardwareEventDecoder extends EventDecoder { + public Event decodeHardwareKey(final KeyEvent keyEvent); +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/event/HardwareKeyboardEventDecoder.java b/app/src/main/java/org/dslul/openboard/inputmethod/event/HardwareKeyboardEventDecoder.java new file mode 100644 index 00000000..4bba8d93 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/event/HardwareKeyboardEventDecoder.java @@ -0,0 +1,81 @@ +/* + * 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.event; + +import android.view.KeyCharacterMap; +import android.view.KeyEvent; + +import org.dslul.openboard.inputmethod.latin.common.Constants; + +/** + * A hardware event decoder for a hardware qwerty-ish keyboard. + * + * The events are always hardware keypresses, but they can be key down or key up events, they + * can be dead keys, they can be meta keys like shift or ctrl... This does not deal with + * 10-key like keyboards; a different decoder is used for this. + */ +public class HardwareKeyboardEventDecoder implements HardwareEventDecoder { + final int mDeviceId; + + public HardwareKeyboardEventDecoder(final int deviceId) { + mDeviceId = deviceId; + // TODO: get the layout for this hardware keyboard + } + + @Override + public Event decodeHardwareKey(final KeyEvent keyEvent) { + // KeyEvent#getUnicodeChar() does not exactly returns a unicode char, but rather a value + // that includes both the unicode char in the lower 21 bits and flags in the upper bits, + // hence the name "codePointAndFlags". {@see KeyEvent#getUnicodeChar()} for more info. + final int codePointAndFlags = keyEvent.getUnicodeChar(); + // The keyCode is the abstraction used by the KeyEvent to represent different keys that + // do not necessarily map to a unicode character. This represents a physical key, like + // the key for 'A' or Space, but also Backspace or Ctrl or Caps Lock. + final int keyCode = keyEvent.getKeyCode(); + final boolean isKeyRepeat = (0 != keyEvent.getRepeatCount()); + if (KeyEvent.KEYCODE_DEL == keyCode) { + return Event.createHardwareKeypressEvent(Event.NOT_A_CODE_POINT, Constants.CODE_DELETE, + null /* next */, isKeyRepeat); + } + if (keyEvent.isPrintingKey() || KeyEvent.KEYCODE_SPACE == keyCode + || KeyEvent.KEYCODE_ENTER == keyCode) { + if (0 != (codePointAndFlags & KeyCharacterMap.COMBINING_ACCENT)) { + // A dead key. + return Event.createDeadEvent( + codePointAndFlags & KeyCharacterMap.COMBINING_ACCENT_MASK, keyCode, + null /* next */); + } + if (KeyEvent.KEYCODE_ENTER == keyCode) { + // The Enter key. If the Shift key is not being pressed, this should send a + // CODE_ENTER to trigger the action if any, or a carriage return otherwise. If the + // Shift key is being pressed, this should send a CODE_SHIFT_ENTER and let + // Latin IME decide what to do with it. + if (keyEvent.isShiftPressed()) { + return Event.createHardwareKeypressEvent(Event.NOT_A_CODE_POINT, + Constants.CODE_SHIFT_ENTER, null /* next */, isKeyRepeat); + } + return Event.createHardwareKeypressEvent(Constants.CODE_ENTER, keyCode, + null /* next */, isKeyRepeat); + } + // If not Enter, then this is just a regular keypress event for a normal character + // that can be committed right away, taking into account the current state. + return Event.createHardwareKeypressEvent(codePointAndFlags, keyCode, null /* next */, + isKeyRepeat); + } + return Event.createNotHandledEvent(); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/event/InputTransaction.java b/app/src/main/java/org/dslul/openboard/inputmethod/event/InputTransaction.java new file mode 100644 index 00000000..38af8212 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/event/InputTransaction.java @@ -0,0 +1,116 @@ +/* + * 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.event; + +import org.dslul.openboard.inputmethod.latin.settings.SettingsValues; + +/** + * An object encapsulating a single transaction for input. + */ +public class InputTransaction { + // UPDATE_LATER is stronger than UPDATE_NOW. The reason for this is, if we have to update later, + // it's because something will change that we can't evaluate now, which means that even if we + // re-evaluate now we'll have to do it again later. The only case where that wouldn't apply + // would be if we needed to update now to find out the new state right away, but then we + // can't do it with this deferred mechanism anyway. + public static final int SHIFT_NO_UPDATE = 0; + public static final int SHIFT_UPDATE_NOW = 1; + public static final int SHIFT_UPDATE_LATER = 2; + + // Initial conditions + public final SettingsValues mSettingsValues; + public final Event mEvent; + public final long mTimestamp; + public final int mSpaceState; + public final int mShiftState; + + // Outputs + private int mRequiredShiftUpdate = SHIFT_NO_UPDATE; + private boolean mRequiresUpdateSuggestions = false; + private boolean mDidAffectContents = false; + private boolean mDidAutoCorrect = false; + + public InputTransaction(final SettingsValues settingsValues, final Event event, + final long timestamp, final int spaceState, final int shiftState) { + mSettingsValues = settingsValues; + mEvent = event; + mTimestamp = timestamp; + mSpaceState = spaceState; + mShiftState = shiftState; + } + + /** + * Indicate that this transaction requires some type of shift update. + * @param updateType What type of shift update this requires. + */ + public void requireShiftUpdate(final int updateType) { + mRequiredShiftUpdate = Math.max(mRequiredShiftUpdate, updateType); + } + + /** + * Gets what type of shift update this transaction requires. + * @return The shift update type. + */ + public int getRequiredShiftUpdate() { + return mRequiredShiftUpdate; + } + + /** + * Indicate that this transaction requires updating the suggestions. + */ + public void setRequiresUpdateSuggestions() { + mRequiresUpdateSuggestions = true; + } + + /** + * Find out whether this transaction requires updating the suggestions. + * @return Whether this transaction requires updating the suggestions. + */ + public boolean requiresUpdateSuggestions() { + return mRequiresUpdateSuggestions; + } + + /** + * Indicate that this transaction affected the contents of the editor. + */ + public void setDidAffectContents() { + mDidAffectContents = true; + } + + /** + * Find out whether this transaction affected contents of the editor. + * @return Whether this transaction affected contents of the editor. + */ + public boolean didAffectContents() { + return mDidAffectContents; + } + + /** + * Indicate that this transaction performed an auto-correction. + */ + public void setDidAutoCorrect() { + mDidAutoCorrect = true; + } + + /** + * Find out whether this transaction performed an auto-correction. + * @return Whether this transaction performed an auto-correction. + */ + public boolean didAutoCorrect() { + return mDidAutoCorrect; + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Key.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Key.java new file mode 100644 index 00000000..266d35e9 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Key.java @@ -0,0 +1,1022 @@ +/* + * Copyright (C) 2010 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.keyboard; + +import static org.dslul.openboard.inputmethod.keyboard.internal.KeyboardIconsSet.ICON_UNDEFINED; +import static org.dslul.openboard.inputmethod.latin.common.Constants.CODE_OUTPUT_TEXT; +import static org.dslul.openboard.inputmethod.latin.common.Constants.CODE_SHIFT; +import static org.dslul.openboard.inputmethod.latin.common.Constants.CODE_SWITCH_ALPHA_SYMBOL; +import static org.dslul.openboard.inputmethod.latin.common.Constants.CODE_UNSPECIFIED; + +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.text.TextUtils; + +import org.dslul.openboard.inputmethod.keyboard.internal.KeyDrawParams; +import org.dslul.openboard.inputmethod.keyboard.internal.KeySpecParser; +import org.dslul.openboard.inputmethod.keyboard.internal.KeyStyle; +import org.dslul.openboard.inputmethod.keyboard.internal.KeyVisualAttributes; +import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardIconsSet; +import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams; +import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardRow; +import org.dslul.openboard.inputmethod.keyboard.internal.MoreKeySpec; +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.Arrays; +import java.util.Locale; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Class for describing the position and characteristics of a single key in the keyboard. + */ +public class Key implements Comparable { + /** + * The key code (unicode or custom code) that this key generates. + */ + private final int mCode; + + /** Label to display */ + private final String mLabel; + /** Hint label to display on the key in conjunction with the label */ + private final String mHintLabel; + /** Flags of the label */ + private final int mLabelFlags; + private static final int LABEL_FLAGS_ALIGN_HINT_LABEL_TO_BOTTOM = 0x02; + private static final int LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM = 0x04; + private static final int LABEL_FLAGS_ALIGN_LABEL_OFF_CENTER = 0x08; + // Font typeface specification. + private static final int LABEL_FLAGS_FONT_MASK = 0x30; + private static final int LABEL_FLAGS_FONT_NORMAL = 0x10; + private static final int LABEL_FLAGS_FONT_MONO_SPACE = 0x20; + private static final int LABEL_FLAGS_FONT_DEFAULT = 0x30; + // Start of key text ratio enum values + private static final int LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK = 0x1C0; + private static final int LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO = 0x40; + private static final int LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO = 0x80; + private static final int LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO = 0xC0; + private static final int LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO = 0x140; + // End of key text ratio mask enum values + private static final int LABEL_FLAGS_HAS_POPUP_HINT = 0x200; + private static final int LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT = 0x400; + private static final int LABEL_FLAGS_HAS_HINT_LABEL = 0x800; + // The bit to calculate the ratio of key label width against key width. If autoXScale bit is on + // and autoYScale bit is off, the key label may be shrunk only for X-direction. + // If both autoXScale and autoYScale bits are on, the key label text size may be auto scaled. + private static final int LABEL_FLAGS_AUTO_X_SCALE = 0x4000; + private static final int LABEL_FLAGS_AUTO_Y_SCALE = 0x8000; + private static final int LABEL_FLAGS_AUTO_SCALE = LABEL_FLAGS_AUTO_X_SCALE + | LABEL_FLAGS_AUTO_Y_SCALE; + private static final int LABEL_FLAGS_PRESERVE_CASE = 0x10000; + private static final int LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED = 0x20000; + private static final int LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL = 0x40000; + private static final int LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR = 0x80000; + private static final int LABEL_FLAGS_KEEP_BACKGROUND_ASPECT_RATIO = 0x100000; + private static final int LABEL_FLAGS_DISABLE_HINT_LABEL = 0x40000000; + private static final int LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS = 0x80000000; + + /** Icon to display instead of a label. Icon takes precedence over a label */ + private final int mIconId; + + /** Width of the key, excluding the gap */ + private final int mWidth; + /** Height of the key, excluding the gap */ + private final int mHeight; + /** + * The combined width in pixels of the horizontal gaps belonging to this key, both to the left + * and to the right. I.e., mWidth + mHorizontalGap = total width belonging to the key. + */ + private final int mHorizontalGap; + /** + * The combined height in pixels of the vertical gaps belonging to this key, both above and + * below. I.e., mHeight + mVerticalGap = total height belonging to the key. + */ + private final int mVerticalGap; + /** X coordinate of the top-left corner of the key in the keyboard layout, excluding the gap. */ + private final int mX; + /** Y coordinate of the top-left corner of the key in the keyboard layout, excluding the gap. */ + private final int mY; + /** Hit bounding box of the key */ + @Nonnull + private final Rect mHitBox = new Rect(); + + /** More keys. It is guaranteed that this is null or an array of one or more elements */ + @Nullable + private final MoreKeySpec[] mMoreKeys; + /** More keys column number and flags */ + private final int mMoreKeysColumnAndFlags; + private static final int MORE_KEYS_COLUMN_NUMBER_MASK = 0x000000ff; + // If this flag is specified, more keys keyboard should have the specified number of columns. + // Otherwise more keys keyboard should have less than or equal to the specified maximum number + // of columns. + private static final int MORE_KEYS_FLAGS_FIXED_COLUMN = 0x00000100; + // If this flag is specified, the order of more keys is determined by the order in the more + // keys' specification. Otherwise the order of more keys is automatically determined. + private static final int MORE_KEYS_FLAGS_FIXED_ORDER = 0x00000200; + private static final int MORE_KEYS_MODE_MAX_COLUMN_WITH_AUTO_ORDER = 0; + private static final int MORE_KEYS_MODE_FIXED_COLUMN_WITH_AUTO_ORDER = + MORE_KEYS_FLAGS_FIXED_COLUMN; + private static final int MORE_KEYS_MODE_FIXED_COLUMN_WITH_FIXED_ORDER = + (MORE_KEYS_FLAGS_FIXED_COLUMN | MORE_KEYS_FLAGS_FIXED_ORDER); + private static final int MORE_KEYS_FLAGS_HAS_LABELS = 0x40000000; + private static final int MORE_KEYS_FLAGS_NEEDS_DIVIDERS = 0x20000000; + private static final int MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY = 0x10000000; + // TODO: Rename these specifiers to !autoOrder! and !fixedOrder! respectively. + private static final String MORE_KEYS_AUTO_COLUMN_ORDER = "!autoColumnOrder!"; + private static final String MORE_KEYS_FIXED_COLUMN_ORDER = "!fixedColumnOrder!"; + private static final String MORE_KEYS_HAS_LABELS = "!hasLabels!"; + private static final String MORE_KEYS_NEEDS_DIVIDERS = "!needsDividers!"; + private static final String MORE_KEYS_NO_PANEL_AUTO_MORE_KEY = "!noPanelAutoMoreKey!"; + + /** Background type that represents different key background visual than normal one. */ + private final int mBackgroundType; + public static final int BACKGROUND_TYPE_EMPTY = 0; + public static final int BACKGROUND_TYPE_NORMAL = 1; + public static final int BACKGROUND_TYPE_FUNCTIONAL = 2; + public static final int BACKGROUND_TYPE_STICKY_OFF = 3; + public static final int BACKGROUND_TYPE_STICKY_ON = 4; + public static final int BACKGROUND_TYPE_ACTION = 5; + public static final int BACKGROUND_TYPE_SPACEBAR = 6; + + private final int mActionFlags; + private static final int ACTION_FLAGS_IS_REPEATABLE = 0x01; + private static final int ACTION_FLAGS_NO_KEY_PREVIEW = 0x02; + private static final int ACTION_FLAGS_ALT_CODE_WHILE_TYPING = 0x04; + private static final int ACTION_FLAGS_ENABLE_LONG_PRESS = 0x08; + + @Nullable + private final KeyVisualAttributes mKeyVisualAttributes; + @Nullable + private final OptionalAttributes mOptionalAttributes; + + private static final class OptionalAttributes { + /** Text to output when pressed. This can be multiple characters, like ".com" */ + public final String mOutputText; + public final int mAltCode; + /** Icon for disabled state */ + public final int mDisabledIconId; + /** The visual insets */ + public final int mVisualInsetsLeft; + public final int mVisualInsetsRight; + + private OptionalAttributes(final String outputText, final int altCode, + final int disabledIconId, final int visualInsetsLeft, final int visualInsetsRight) { + mOutputText = outputText; + mAltCode = altCode; + mDisabledIconId = disabledIconId; + mVisualInsetsLeft = visualInsetsLeft; + mVisualInsetsRight = visualInsetsRight; + } + + @Nullable + public static OptionalAttributes newInstance(final String outputText, final int altCode, + final int disabledIconId, final int visualInsetsLeft, final int visualInsetsRight) { + if (outputText == null && altCode == CODE_UNSPECIFIED + && disabledIconId == ICON_UNDEFINED && visualInsetsLeft == 0 + && visualInsetsRight == 0) { + return null; + } + return new OptionalAttributes(outputText, altCode, disabledIconId, visualInsetsLeft, + visualInsetsRight); + } + } + + private final int mHashCode; + + /** The current pressed state of this key */ + private boolean mPressed; + /** Key is enabled and responds on press */ + private boolean mEnabled = true; + + /** + * Constructor for a key on MoreKeyKeyboard, on MoreSuggestions, + * and in a . + */ + public Key(@Nullable final String label, final int iconId, final int code, + @Nullable final String outputText, @Nullable final String hintLabel, + final int labelFlags, final int backgroundType, final int x, final int y, + final int width, final int height, final int horizontalGap, final int verticalGap) { + mWidth = width - horizontalGap; + mHeight = height - verticalGap; + mHorizontalGap = horizontalGap; + mVerticalGap = verticalGap; + mHintLabel = hintLabel; + mLabelFlags = labelFlags; + mBackgroundType = backgroundType; + // TODO: Pass keyActionFlags as an argument. + mActionFlags = ACTION_FLAGS_NO_KEY_PREVIEW; + mMoreKeys = null; + mMoreKeysColumnAndFlags = 0; + mLabel = label; + mOptionalAttributes = OptionalAttributes.newInstance(outputText, CODE_UNSPECIFIED, + ICON_UNDEFINED, 0 /* visualInsetsLeft */, 0 /* visualInsetsRight */); + mCode = code; + mEnabled = (code != CODE_UNSPECIFIED); + mIconId = iconId; + // Horizontal gap is divided equally to both sides of the key. + mX = x + mHorizontalGap / 2; + mY = y; + mHitBox.set(x, y, x + width + 1, y + height); + mKeyVisualAttributes = null; + + mHashCode = computeHashCode(this); + } + + /** + * Create a key with the given top-left coordinate and extract its attributes from a key + * specification string, Key attribute array, key style, and etc. + * + * @param keySpec the key specification. + * @param keyAttr the Key XML attributes array. + * @param style the {@link KeyStyle} of this key. + * @param params the keyboard building parameters. + * @param row the row that this key belongs to. row's x-coordinate will be the right edge of + * this key. + */ + public Key(@Nullable final String keySpec, @Nonnull final TypedArray keyAttr, + @Nonnull final KeyStyle style, @Nonnull final KeyboardParams params, + @Nonnull final KeyboardRow row) { + mHorizontalGap = isSpacer() ? 0 : params.mHorizontalGap; + mVerticalGap = params.mVerticalGap; + + final float horizontalGapFloat = mHorizontalGap; + final int rowHeight = row.getRowHeight(); + mHeight = rowHeight - mVerticalGap; + + final float keyXPos = row.getKeyX(keyAttr); + final float keyWidth = row.getKeyWidth(keyAttr, keyXPos); + final int keyYPos = row.getKeyY(); + + // Horizontal gap is divided equally to both sides of the key. + mX = Math.round(keyXPos + horizontalGapFloat / 2); + mY = keyYPos; + mWidth = Math.round(keyWidth - horizontalGapFloat); + mHitBox.set(Math.round(keyXPos), keyYPos, Math.round(keyXPos + keyWidth) + 1, + keyYPos + rowHeight); + // Update row to have current x coordinate. + row.setXPos(keyXPos + keyWidth); + + mBackgroundType = style.getInt(keyAttr, + R.styleable.Keyboard_Key_backgroundType, row.getDefaultBackgroundType()); + + final int baseWidth = params.mBaseWidth; + final int visualInsetsLeft = Math.round(keyAttr.getFraction( + R.styleable.Keyboard_Key_visualInsetsLeft, baseWidth, baseWidth, 0)); + final int visualInsetsRight = Math.round(keyAttr.getFraction( + R.styleable.Keyboard_Key_visualInsetsRight, baseWidth, baseWidth, 0)); + + mLabelFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyLabelFlags) + | row.getDefaultKeyLabelFlags(); + final boolean needsToUpcase = needsToUpcase(mLabelFlags, params.mId.mElementId); + final Locale localeForUpcasing = params.mId.getLocale(); + int actionFlags = style.getFlags(keyAttr, R.styleable.Keyboard_Key_keyActionFlags); + String[] moreKeys = style.getStringArray(keyAttr, R.styleable.Keyboard_Key_moreKeys); + + // Get maximum column order number and set a relevant mode value. + int moreKeysColumnAndFlags = MORE_KEYS_MODE_MAX_COLUMN_WITH_AUTO_ORDER + | style.getInt(keyAttr, R.styleable.Keyboard_Key_maxMoreKeysColumn, + params.mMaxMoreKeysKeyboardColumn); + int value; + if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) { + // Override with fixed column order number and set a relevant mode value. + moreKeysColumnAndFlags = MORE_KEYS_MODE_FIXED_COLUMN_WITH_AUTO_ORDER + | (value & MORE_KEYS_COLUMN_NUMBER_MASK); + } + if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) { + // Override with fixed column order number and set a relevant mode value. + moreKeysColumnAndFlags = MORE_KEYS_MODE_FIXED_COLUMN_WITH_FIXED_ORDER + | (value & MORE_KEYS_COLUMN_NUMBER_MASK); + } + if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) { + moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_HAS_LABELS; + } + if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) { + moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_NEEDS_DIVIDERS; + } + if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) { + moreKeysColumnAndFlags |= MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY; + } + mMoreKeysColumnAndFlags = moreKeysColumnAndFlags; + + final String[] additionalMoreKeys; + if ((mLabelFlags & LABEL_FLAGS_DISABLE_ADDITIONAL_MORE_KEYS) != 0) { + additionalMoreKeys = null; + } else { + additionalMoreKeys = style.getStringArray(keyAttr, + R.styleable.Keyboard_Key_additionalMoreKeys); + } + moreKeys = MoreKeySpec.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys); + if (moreKeys != null) { + actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS; + mMoreKeys = new MoreKeySpec[moreKeys.length]; + for (int i = 0; i < moreKeys.length; i++) { + mMoreKeys[i] = new MoreKeySpec(moreKeys[i], needsToUpcase, localeForUpcasing); + } + } else { + mMoreKeys = null; + } + mActionFlags = actionFlags; + + mIconId = KeySpecParser.getIconId(keySpec); + final int disabledIconId = KeySpecParser.getIconId(style.getString(keyAttr, + R.styleable.Keyboard_Key_keyIconDisabled)); + + final int code = KeySpecParser.getCode(keySpec); + if ((mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0) { + mLabel = params.mId.mCustomActionLabel; + } else if (code >= Character.MIN_SUPPLEMENTARY_CODE_POINT) { + // This is a workaround to have a key that has a supplementary code point in its label. + // Because we can put a string in resource neither as a XML entity of a supplementary + // code point nor as a surrogate pair. + mLabel = new StringBuilder().appendCodePoint(code).toString(); + } else { + final String label = KeySpecParser.getLabel(keySpec); + mLabel = needsToUpcase + ? StringUtils.toTitleCaseOfKeyLabel(label, localeForUpcasing) + : label; + } + if ((mLabelFlags & LABEL_FLAGS_DISABLE_HINT_LABEL) != 0) { + mHintLabel = null; + } else { + final String hintLabel = style.getString( + keyAttr, R.styleable.Keyboard_Key_keyHintLabel); + mHintLabel = needsToUpcase + ? StringUtils.toTitleCaseOfKeyLabel(hintLabel, localeForUpcasing) + : hintLabel; + } + String outputText = KeySpecParser.getOutputText(keySpec); + if (needsToUpcase) { + outputText = StringUtils.toTitleCaseOfKeyLabel(outputText, localeForUpcasing); + } + // Choose the first letter of the label as primary code if not specified. + if (code == CODE_UNSPECIFIED && TextUtils.isEmpty(outputText) + && !TextUtils.isEmpty(mLabel)) { + if (StringUtils.codePointCount(mLabel) == 1) { + // Use the first letter of the hint label if shiftedLetterActivated flag is + // specified. + if (hasShiftedLetterHint() && isShiftedLetterActivated()) { + mCode = mHintLabel.codePointAt(0); + } else { + mCode = mLabel.codePointAt(0); + } + } else { + // In some locale and case, the character might be represented by multiple code + // points, such as upper case Eszett of German alphabet. + outputText = mLabel; + mCode = CODE_OUTPUT_TEXT; + } + } else if (code == CODE_UNSPECIFIED && outputText != null) { + if (StringUtils.codePointCount(outputText) == 1) { + mCode = outputText.codePointAt(0); + outputText = null; + } else { + mCode = CODE_OUTPUT_TEXT; + } + } else { + mCode = needsToUpcase ? StringUtils.toTitleCaseOfKeyCode(code, localeForUpcasing) + : code; + } + final int altCodeInAttr = KeySpecParser.parseCode( + style.getString(keyAttr, R.styleable.Keyboard_Key_altCode), CODE_UNSPECIFIED); + final int altCode = needsToUpcase + ? StringUtils.toTitleCaseOfKeyCode(altCodeInAttr, localeForUpcasing) + : altCodeInAttr; + mOptionalAttributes = OptionalAttributes.newInstance(outputText, altCode, + disabledIconId, visualInsetsLeft, visualInsetsRight); + mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr); + mHashCode = computeHashCode(this); + } + + /** + * Copy constructor for DynamicGridKeyboard.GridKey. + * + * @param key the original key. + */ + protected Key(@Nonnull final Key key) { + this(key, key.mMoreKeys); + } + + private Key(@Nonnull final Key key, @Nullable final MoreKeySpec[] moreKeys) { + // Final attributes. + mCode = key.mCode; + mLabel = key.mLabel; + mHintLabel = key.mHintLabel; + mLabelFlags = key.mLabelFlags; + mIconId = key.mIconId; + mWidth = key.mWidth; + mHeight = key.mHeight; + mHorizontalGap = key.mHorizontalGap; + mVerticalGap = key.mVerticalGap; + mX = key.mX; + mY = key.mY; + mHitBox.set(key.mHitBox); + mMoreKeys = moreKeys; + mMoreKeysColumnAndFlags = key.mMoreKeysColumnAndFlags; + mBackgroundType = key.mBackgroundType; + mActionFlags = key.mActionFlags; + mKeyVisualAttributes = key.mKeyVisualAttributes; + mOptionalAttributes = key.mOptionalAttributes; + mHashCode = key.mHashCode; + // Key state. + mPressed = key.mPressed; + mEnabled = key.mEnabled; + } + + @Nonnull + public static Key removeRedundantMoreKeys(@Nonnull final Key key, + @Nonnull final MoreKeySpec.LettersOnBaseLayout lettersOnBaseLayout) { + final MoreKeySpec[] moreKeys = key.getMoreKeys(); + final MoreKeySpec[] filteredMoreKeys = MoreKeySpec.removeRedundantMoreKeys( + moreKeys, lettersOnBaseLayout); + return (filteredMoreKeys == moreKeys) ? key : new Key(key, filteredMoreKeys); + } + + private static boolean needsToUpcase(final int labelFlags, final int keyboardElementId) { + if ((labelFlags & LABEL_FLAGS_PRESERVE_CASE) != 0) return false; + switch (keyboardElementId) { + case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: + case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: + case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: + case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: + return true; + default: + return false; + } + } + + private static int computeHashCode(final Key key) { + return Arrays.hashCode(new Object[] { + key.mX, + key.mY, + key.mWidth, + key.mHeight, + key.mCode, + key.mLabel, + key.mHintLabel, + key.mIconId, + key.mBackgroundType, + Arrays.hashCode(key.mMoreKeys), + key.getOutputText(), + key.mActionFlags, + key.mLabelFlags, + // Key can be distinguishable without the following members. + // key.mOptionalAttributes.mAltCode, + // key.mOptionalAttributes.mDisabledIconId, + // key.mOptionalAttributes.mPreviewIconId, + // key.mHorizontalGap, + // key.mVerticalGap, + // key.mOptionalAttributes.mVisualInsetLeft, + // key.mOptionalAttributes.mVisualInsetRight, + // key.mMaxMoreKeysColumn, + }); + } + + private boolean equalsInternal(final Key o) { + if (this == o) return true; + return o.mX == mX + && o.mY == mY + && o.mWidth == mWidth + && o.mHeight == mHeight + && o.mCode == mCode + && TextUtils.equals(o.mLabel, mLabel) + && TextUtils.equals(o.mHintLabel, mHintLabel) + && o.mIconId == mIconId + && o.mBackgroundType == mBackgroundType + && Arrays.equals(o.mMoreKeys, mMoreKeys) + && TextUtils.equals(o.getOutputText(), getOutputText()) + && o.mActionFlags == mActionFlags + && o.mLabelFlags == mLabelFlags; + } + + @Override + public int compareTo(Key o) { + if (equalsInternal(o)) return 0; + if (mHashCode > o.mHashCode) return 1; + return -1; + } + + @Override + public int hashCode() { + return mHashCode; + } + + @Override + public boolean equals(final Object o) { + return o instanceof Key && equalsInternal((Key)o); + } + + @Override + public String toString() { + return toShortString() + " " + getX() + "," + getY() + " " + getWidth() + "x" + getHeight(); + } + + public String toShortString() { + final int code = getCode(); + if (code == Constants.CODE_OUTPUT_TEXT) { + return getOutputText(); + } + return Constants.printableCode(code); + } + + public String toLongString() { + final int iconId = getIconId(); + final String topVisual = (iconId == KeyboardIconsSet.ICON_UNDEFINED) + ? KeyboardIconsSet.PREFIX_ICON + KeyboardIconsSet.getIconName(iconId) : getLabel(); + final String hintLabel = getHintLabel(); + final String visual = (hintLabel == null) ? topVisual : topVisual + "^" + hintLabel; + return toString() + " " + visual + "/" + backgroundName(mBackgroundType); + } + + private static String backgroundName(final int backgroundType) { + switch (backgroundType) { + case BACKGROUND_TYPE_EMPTY: return "empty"; + case BACKGROUND_TYPE_NORMAL: return "normal"; + case BACKGROUND_TYPE_FUNCTIONAL: return "functional"; + case BACKGROUND_TYPE_STICKY_OFF: return "stickyOff"; + case BACKGROUND_TYPE_STICKY_ON: return "stickyOn"; + case BACKGROUND_TYPE_ACTION: return "action"; + case BACKGROUND_TYPE_SPACEBAR: return "spacebar"; + default: return null; + } + } + + public int getCode() { + return mCode; + } + + @Nullable + public String getLabel() { + return mLabel; + } + + @Nullable + public String getHintLabel() { + return mHintLabel; + } + + @Nullable + public MoreKeySpec[] getMoreKeys() { + return mMoreKeys; + } + + public void markAsLeftEdge(final KeyboardParams params) { + mHitBox.left = params.mLeftPadding; + } + + public void markAsRightEdge(final KeyboardParams params) { + mHitBox.right = params.mOccupiedWidth - params.mRightPadding; + } + + public void markAsTopEdge(final KeyboardParams params) { + mHitBox.top = params.mTopPadding; + } + + public void markAsBottomEdge(final KeyboardParams params) { + mHitBox.bottom = params.mOccupiedHeight + params.mBottomPadding; + } + + public final boolean isSpacer() { + return this instanceof Spacer; + } + + public final boolean isActionKey() { + return mBackgroundType == BACKGROUND_TYPE_ACTION; + } + + public final boolean isShift() { + return mCode == CODE_SHIFT; + } + + public final boolean isModifier() { + return mCode == CODE_SHIFT || mCode == CODE_SWITCH_ALPHA_SYMBOL; + } + + public final boolean isRepeatable() { + return (mActionFlags & ACTION_FLAGS_IS_REPEATABLE) != 0; + } + + public final boolean noKeyPreview() { + return (mActionFlags & ACTION_FLAGS_NO_KEY_PREVIEW) != 0; + } + + public final boolean altCodeWhileTyping() { + return (mActionFlags & ACTION_FLAGS_ALT_CODE_WHILE_TYPING) != 0; + } + + public final boolean isLongPressEnabled() { + // We need not start long press timer on the key which has activated shifted letter. + return (mActionFlags & ACTION_FLAGS_ENABLE_LONG_PRESS) != 0 + && (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) == 0; + } + + public KeyVisualAttributes getVisualAttributes() { + return mKeyVisualAttributes; + } + + @Nonnull + public final Typeface selectTypeface(final KeyDrawParams params) { + switch (mLabelFlags & LABEL_FLAGS_FONT_MASK) { + case LABEL_FLAGS_FONT_NORMAL: + return Typeface.DEFAULT; + case LABEL_FLAGS_FONT_MONO_SPACE: + return Typeface.MONOSPACE; + case LABEL_FLAGS_FONT_DEFAULT: + default: + // The type-face is specified by keyTypeface attribute. + return params.mTypeface; + } + } + + public final int selectTextSize(final KeyDrawParams params) { + switch (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_TEXT_RATIO_MASK) { + case LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO: + return params.mLetterSize; + case LABEL_FLAGS_FOLLOW_KEY_LARGE_LETTER_RATIO: + return params.mLargeLetterSize; + case LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO: + return params.mLabelSize; + case LABEL_FLAGS_FOLLOW_KEY_HINT_LABEL_RATIO: + return params.mHintLabelSize; + default: // No follow key ratio flag specified. + return StringUtils.codePointCount(mLabel) == 1 ? params.mLetterSize : params.mLabelSize; + } + } + + public final int selectTextColor(final KeyDrawParams params) { + if ((mLabelFlags & LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR) != 0) { + return params.mFunctionalTextColor; + } + return isShiftedLetterActivated() ? params.mTextInactivatedColor : params.mTextColor; + } + + public final int selectHintTextSize(final KeyDrawParams params) { + if (hasHintLabel()) { + return params.mHintLabelSize; + } + if (hasShiftedLetterHint()) { + return params.mShiftedLetterHintSize; + } + return params.mHintLetterSize; + } + + public final int selectHintTextColor(final KeyDrawParams params) { + if (hasHintLabel()) { + return params.mHintLabelColor; + } + if (hasShiftedLetterHint()) { + return isShiftedLetterActivated() ? params.mShiftedLetterHintActivatedColor + : params.mShiftedLetterHintInactivatedColor; + } + return params.mHintLetterColor; + } + + public final int selectMoreKeyTextSize(final KeyDrawParams params) { + return hasLabelsInMoreKeys() ? params.mLabelSize : params.mLetterSize; + } + + public final String getPreviewLabel() { + return isShiftedLetterActivated() ? mHintLabel : mLabel; + } + + private boolean previewHasLetterSize() { + return (mLabelFlags & LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO) != 0 + || StringUtils.codePointCount(getPreviewLabel()) == 1; + } + + public final int selectPreviewTextSize(final KeyDrawParams params) { + if (previewHasLetterSize()) { + return params.mPreviewTextSize; + } + return params.mLetterSize; + } + + @Nonnull + public Typeface selectPreviewTypeface(final KeyDrawParams params) { + if (previewHasLetterSize()) { + return selectTypeface(params); + } + return Typeface.DEFAULT_BOLD; + } + + public final boolean isAlignHintLabelToBottom(final int defaultFlags) { + return ((mLabelFlags | defaultFlags) & LABEL_FLAGS_ALIGN_HINT_LABEL_TO_BOTTOM) != 0; + } + + public final boolean isAlignIconToBottom() { + return (mLabelFlags & LABEL_FLAGS_ALIGN_ICON_TO_BOTTOM) != 0; + } + + public final boolean isAlignLabelOffCenter() { + return (mLabelFlags & LABEL_FLAGS_ALIGN_LABEL_OFF_CENTER) != 0; + } + + public final boolean hasPopupHint() { + return (mLabelFlags & LABEL_FLAGS_HAS_POPUP_HINT) != 0; + } + + public final boolean hasShiftedLetterHint() { + return (mLabelFlags & LABEL_FLAGS_HAS_SHIFTED_LETTER_HINT) != 0 + && !TextUtils.isEmpty(mHintLabel); + } + + public final boolean hasHintLabel() { + return (mLabelFlags & LABEL_FLAGS_HAS_HINT_LABEL) != 0; + } + + public final boolean needsAutoXScale() { + return (mLabelFlags & LABEL_FLAGS_AUTO_X_SCALE) != 0; + } + + public final boolean needsAutoScale() { + return (mLabelFlags & LABEL_FLAGS_AUTO_SCALE) == LABEL_FLAGS_AUTO_SCALE; + } + + public final boolean needsToKeepBackgroundAspectRatio(final int defaultFlags) { + return ((mLabelFlags | defaultFlags) & LABEL_FLAGS_KEEP_BACKGROUND_ASPECT_RATIO) != 0; + } + + public final boolean hasCustomActionLabel() { + return (mLabelFlags & LABEL_FLAGS_FROM_CUSTOM_ACTION_LABEL) != 0; + } + + private final boolean isShiftedLetterActivated() { + return (mLabelFlags & LABEL_FLAGS_SHIFTED_LETTER_ACTIVATED) != 0 + && !TextUtils.isEmpty(mHintLabel); + } + + public final int getMoreKeysColumnNumber() { + return mMoreKeysColumnAndFlags & MORE_KEYS_COLUMN_NUMBER_MASK; + } + + public final boolean isMoreKeysFixedColumn() { + return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_FIXED_COLUMN) != 0; + } + + public final boolean isMoreKeysFixedOrder() { + return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_FIXED_ORDER) != 0; + } + + public final boolean hasLabelsInMoreKeys() { + return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_HAS_LABELS) != 0; + } + + public final int getMoreKeyLabelFlags() { + final int labelSizeFlag = hasLabelsInMoreKeys() + ? LABEL_FLAGS_FOLLOW_KEY_LABEL_RATIO + : LABEL_FLAGS_FOLLOW_KEY_LETTER_RATIO; + return labelSizeFlag | LABEL_FLAGS_AUTO_X_SCALE; + } + + public final boolean needsDividersInMoreKeys() { + return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NEEDS_DIVIDERS) != 0; + } + + public final boolean hasNoPanelAutoMoreKey() { + return (mMoreKeysColumnAndFlags & MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY) != 0; + } + + @Nullable + public final String getOutputText() { + final OptionalAttributes attrs = mOptionalAttributes; + return (attrs != null) ? attrs.mOutputText : null; + } + + public final int getAltCode() { + final OptionalAttributes attrs = mOptionalAttributes; + return (attrs != null) ? attrs.mAltCode : CODE_UNSPECIFIED; + } + + public int getIconId() { + return mIconId; + } + + @Nullable + public Drawable getIcon(final KeyboardIconsSet iconSet, final int alpha) { + final OptionalAttributes attrs = mOptionalAttributes; + final int disabledIconId = (attrs != null) ? attrs.mDisabledIconId : ICON_UNDEFINED; + final int iconId = mEnabled ? getIconId() : disabledIconId; + final Drawable icon = iconSet.getIconDrawable(iconId); + if (icon != null) { + icon.setAlpha(alpha); + } + return icon; + } + + @Nullable + public Drawable getPreviewIcon(final KeyboardIconsSet iconSet) { + return iconSet.getIconDrawable(getIconId()); + } + + /** + * Gets the width of the key in pixels, excluding the gap. + * @return The width of the key in pixels, excluding the gap. + */ + public int getWidth() { + return mWidth; + } + + /** + * Gets the height of the key in pixels, excluding the gap. + * @return The height of the key in pixels, excluding the gap. + */ + public int getHeight() { + return mHeight; + } + + /** + * The combined width in pixels of the horizontal gaps belonging to this key, both above and + * below. I.e., getWidth() + getHorizontalGap() = total width belonging to the key. + * @return Horizontal gap belonging to this key. + */ + public int getHorizontalGap() { + return mHorizontalGap; + } + + /** + * The combined height in pixels of the vertical gaps belonging to this key, both above and + * below. I.e., getHeight() + getVerticalGap() = total height belonging to the key. + * @return Vertical gap belonging to this key. + */ + public int getVerticalGap() { + return mVerticalGap; + } + + /** + * Gets the x-coordinate of the top-left corner of the key in pixels, excluding the gap. + * @return The x-coordinate of the top-left corner of the key in pixels, excluding the gap. + */ + public int getX() { + return mX; + } + + /** + * Gets the y-coordinate of the top-left corner of the key in pixels, excluding the gap. + * @return The y-coordinate of the top-left corner of the key in pixels, excluding the gap. + */ + public int getY() { + return mY; + } + + public final int getDrawX() { + final int x = getX(); + final OptionalAttributes attrs = mOptionalAttributes; + return (attrs == null) ? x : x + attrs.mVisualInsetsLeft; + } + + public final int getDrawWidth() { + final OptionalAttributes attrs = mOptionalAttributes; + return (attrs == null) ? mWidth + : mWidth - attrs.mVisualInsetsLeft - attrs.mVisualInsetsRight; + } + + /** + * Informs the key that it has been pressed, in case it needs to change its appearance or + * state. + * @see #onReleased() + */ + public void onPressed() { + mPressed = true; + } + + /** + * Informs the key that it has been released, in case it needs to change its appearance or + * state. + * @see #onPressed() + */ + public void onReleased() { + mPressed = false; + } + + public final boolean isEnabled() { + return mEnabled; + } + + public void setEnabled(final boolean enabled) { + mEnabled = enabled; + } + + @Nonnull + public Rect getHitBox() { + return mHitBox; + } + + /** + * Detects if a point falls on this key. + * @param x the x-coordinate of the point + * @param y the y-coordinate of the point + * @return whether or not the point falls on the key. If the key is attached to an edge, it + * will assume that all points between the key and the edge are considered to be on the key. + * @see #markAsLeftEdge(KeyboardParams) etc. + */ + public boolean isOnKey(final int x, final int y) { + return mHitBox.contains(x, y); + } + + /** + * Returns the square of the distance to the nearest edge of the key and the given point. + * @param x the x-coordinate of the point + * @param y the y-coordinate of the point + * @return the square of the distance of the point from the nearest edge of the key + */ + public int squaredDistanceToEdge(final int x, final int y) { + final int left = getX(); + final int right = left + mWidth; + final int top = getY(); + final int bottom = top + mHeight; + final int edgeX = x < left ? left : (x > right ? right : x); + final int edgeY = y < top ? top : (y > bottom ? bottom : y); + final int dx = x - edgeX; + final int dy = y - edgeY; + return dx * dx + dy * dy; + } + + static class KeyBackgroundState { + private final int[] mReleasedState; + private final int[] mPressedState; + + private KeyBackgroundState(final int ... attrs) { + mReleasedState = attrs; + mPressedState = Arrays.copyOf(attrs, attrs.length + 1); + mPressedState[attrs.length] = android.R.attr.state_pressed; + } + + public int[] getState(final boolean pressed) { + return pressed ? mPressedState : mReleasedState; + } + + public static final KeyBackgroundState[] STATES = { + // 0: BACKGROUND_TYPE_EMPTY + new KeyBackgroundState(android.R.attr.state_empty), + // 1: BACKGROUND_TYPE_NORMAL + new KeyBackgroundState(), + // 2: BACKGROUND_TYPE_FUNCTIONAL + new KeyBackgroundState(), + // 3: BACKGROUND_TYPE_STICKY_OFF + new KeyBackgroundState(android.R.attr.state_checkable), + // 4: BACKGROUND_TYPE_STICKY_ON + new KeyBackgroundState(android.R.attr.state_checkable, android.R.attr.state_checked), + // 5: BACKGROUND_TYPE_ACTION + new KeyBackgroundState(android.R.attr.state_active), + // 6: BACKGROUND_TYPE_SPACEBAR + new KeyBackgroundState(), + }; + } + + /** + * Returns the background drawable for the key, based on the current state and type of the key. + * @return the background drawable of the key. + * @see android.graphics.drawable.StateListDrawable#setState(int[]) + */ + @Nonnull + public final Drawable selectBackgroundDrawable(@Nonnull final Drawable keyBackground, + @Nonnull final Drawable functionalKeyBackground, + @Nonnull final Drawable spacebarBackground) { + final Drawable background; + if (mBackgroundType == BACKGROUND_TYPE_FUNCTIONAL) { + background = functionalKeyBackground; + } else if (mBackgroundType == BACKGROUND_TYPE_SPACEBAR) { + background = spacebarBackground; + } else { + background = keyBackground; + } + final int[] state = KeyBackgroundState.STATES[mBackgroundType].getState(mPressed); + background.setState(state); + return background; + } + + public static class Spacer extends Key { + public Spacer(final TypedArray keyAttr, final KeyStyle keyStyle, + final KeyboardParams params, final KeyboardRow row) { + super(null /* keySpec */, keyAttr, keyStyle, params, row); + } + + /** + * This constructor is being used only for divider in more keys keyboard. + */ + protected Spacer(final KeyboardParams params, final int x, final int y, final int width, + final int height) { + super(null /* label */, ICON_UNDEFINED, CODE_UNSPECIFIED, null /* outputText */, + null /* hintLabel */, 0 /* labelFlags */, BACKGROUND_TYPE_EMPTY, x, y, width, + height, params.mHorizontalGap, params.mVerticalGap); + } + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyDetector.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyDetector.java new file mode 100644 index 00000000..58bd3992 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyDetector.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2010 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.keyboard; + +/** + * This class handles key detection. + */ +public class KeyDetector { + private final int mKeyHysteresisDistanceSquared; + private final int mKeyHysteresisDistanceForSlidingModifierSquared; + + private Keyboard mKeyboard; + private int mCorrectionX; + private int mCorrectionY; + + public KeyDetector() { + this(0.0f /* keyHysteresisDistance */, 0.0f /* keyHysteresisDistanceForSlidingModifier */); + } + + /** + * Key detection object constructor with key hysteresis distances. + * + * @param keyHysteresisDistance if the pointer movement distance is smaller than this, the + * movement will not be handled as meaningful movement. The unit is pixel. + * @param keyHysteresisDistanceForSlidingModifier the same parameter for sliding input that + * starts from a modifier key such as shift and symbols key. + */ + public KeyDetector(final float keyHysteresisDistance, + final float keyHysteresisDistanceForSlidingModifier) { + mKeyHysteresisDistanceSquared = (int)(keyHysteresisDistance * keyHysteresisDistance); + mKeyHysteresisDistanceForSlidingModifierSquared = (int)( + keyHysteresisDistanceForSlidingModifier * keyHysteresisDistanceForSlidingModifier); + } + + public void setKeyboard(final Keyboard keyboard, final float correctionX, + final float correctionY) { + if (keyboard == null) { + throw new NullPointerException(); + } + mCorrectionX = (int)correctionX; + mCorrectionY = (int)correctionY; + mKeyboard = keyboard; + } + + public int getKeyHysteresisDistanceSquared(final boolean isSlidingFromModifier) { + return isSlidingFromModifier + ? mKeyHysteresisDistanceForSlidingModifierSquared : mKeyHysteresisDistanceSquared; + } + + public int getTouchX(final int x) { + return x + mCorrectionX; + } + + // TODO: Remove vertical correction. + public int getTouchY(final int y) { + return y + mCorrectionY; + } + + public Keyboard getKeyboard() { + return mKeyboard; + } + + public boolean alwaysAllowsKeySelectionByDraggingFinger() { + return false; + } + + /** + * Detect the key whose hitbox the touch point is in. + * + * @param x The x-coordinate of a touch point + * @param y The y-coordinate of a touch point + * @return the key that the touch point hits. + */ + public Key detectHitKey(final int x, final int y) { + if (mKeyboard == null) { + return null; + } + final int touchX = getTouchX(x); + final int touchY = getTouchY(y); + + int minDistance = Integer.MAX_VALUE; + Key primaryKey = null; + for (final Key key: mKeyboard.getNearestKeys(touchX, touchY)) { + // An edge key always has its enlarged hitbox to respond to an event that occurred in + // the empty area around the key. (@see Key#markAsLeftEdge(KeyboardParams)} etc.) + if (!key.isOnKey(touchX, touchY)) { + continue; + } + final int distance = key.squaredDistanceToEdge(touchX, touchY); + if (distance > minDistance) { + continue; + } + // To take care of hitbox overlaps, we compare key's code here too. + if (primaryKey == null || distance < minDistance + || key.getCode() > primaryKey.getCode()) { + minDistance = distance; + primaryKey = key; + } + } + return primaryKey; + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Keyboard.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Keyboard.java new file mode 100644 index 00000000..e64794cf --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/Keyboard.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2010 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.keyboard; + +import android.util.SparseArray; + +import org.dslul.openboard.inputmethod.keyboard.internal.KeyVisualAttributes; +import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardIconsSet; +import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams; +import org.dslul.openboard.inputmethod.latin.common.Constants; +import org.dslul.openboard.inputmethod.latin.common.CoordinateUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Loads an XML description of a keyboard and stores the attributes of the keys. A keyboard + * consists of rows of keys. + *

The layout file for a keyboard contains XML that looks like the following snippet:

+ *
+ * <Keyboard
+ *         latin:keyWidth="10%p"
+ *         latin:rowHeight="50px"
+ *         latin:horizontalGap="2%p"
+ *         latin:verticalGap="2%p" >
+ *     <Row latin:keyWidth="10%p" >
+ *         <Key latin:keyLabel="A" />
+ *         ...
+ *     </Row>
+ *     ...
+ * </Keyboard>
+ * 
+ */ +public class Keyboard { + @Nonnull + public final KeyboardId mId; + public final int mThemeId; + + /** Total height of the keyboard, including the padding and keys */ + public final int mOccupiedHeight; + /** Total width of the keyboard, including the padding and keys */ + public final int mOccupiedWidth; + + /** Base height of the keyboard, used to calculate rows' height */ + public final int mBaseHeight; + /** Base width of the keyboard, used to calculate keys' width */ + public final int mBaseWidth; + + /** The padding above the keyboard */ + public final int mTopPadding; + /** Default gap between rows */ + public final int mVerticalGap; + + /** Per keyboard key visual parameters */ + public final KeyVisualAttributes mKeyVisualAttributes; + + public final int mMostCommonKeyHeight; + public final int mMostCommonKeyWidth; + + /** More keys keyboard template */ + public final int mMoreKeysTemplate; + + /** Maximum column for more keys keyboard */ + public final int mMaxMoreKeysKeyboardColumn; + + /** List of keys in this keyboard */ + @Nonnull + private final List mSortedKeys; + @Nonnull + public final List mShiftKeys; + @Nonnull + public final List mAltCodeKeysWhileTyping; + @Nonnull + public final KeyboardIconsSet mIconsSet; + + private final SparseArray mKeyCache = new SparseArray<>(); + + @Nonnull + private final ProximityInfo mProximityInfo; + @Nonnull + private final KeyboardLayout mKeyboardLayout; + + private final boolean mProximityCharsCorrectionEnabled; + + public Keyboard(@Nonnull final KeyboardParams params) { + mId = params.mId; + mThemeId = params.mThemeId; + mOccupiedHeight = params.mOccupiedHeight; + mOccupiedWidth = params.mOccupiedWidth; + mBaseHeight = params.mBaseHeight; + mBaseWidth = params.mBaseWidth; + mMostCommonKeyHeight = params.mMostCommonKeyHeight; + mMostCommonKeyWidth = params.mMostCommonKeyWidth; + mMoreKeysTemplate = params.mMoreKeysTemplate; + mMaxMoreKeysKeyboardColumn = params.mMaxMoreKeysKeyboardColumn; + mKeyVisualAttributes = params.mKeyVisualAttributes; + mTopPadding = params.mTopPadding; + mVerticalGap = params.mVerticalGap; + + mSortedKeys = Collections.unmodifiableList(new ArrayList<>(params.mSortedKeys)); + mShiftKeys = Collections.unmodifiableList(params.mShiftKeys); + mAltCodeKeysWhileTyping = Collections.unmodifiableList(params.mAltCodeKeysWhileTyping); + mIconsSet = params.mIconsSet; + + mProximityInfo = new ProximityInfo(params.GRID_WIDTH, params.GRID_HEIGHT, + mOccupiedWidth, mOccupiedHeight, mMostCommonKeyWidth, mMostCommonKeyHeight, + mSortedKeys, params.mTouchPositionCorrection); + mProximityCharsCorrectionEnabled = params.mProximityCharsCorrectionEnabled; + mKeyboardLayout = KeyboardLayout.newKeyboardLayout(mSortedKeys, mMostCommonKeyWidth, + mMostCommonKeyHeight, mOccupiedWidth, mOccupiedHeight); + } + + protected Keyboard(@Nonnull final Keyboard keyboard) { + mId = keyboard.mId; + mThemeId = keyboard.mThemeId; + mOccupiedHeight = keyboard.mOccupiedHeight; + mOccupiedWidth = keyboard.mOccupiedWidth; + mBaseHeight = keyboard.mBaseHeight; + mBaseWidth = keyboard.mBaseWidth; + mMostCommonKeyHeight = keyboard.mMostCommonKeyHeight; + mMostCommonKeyWidth = keyboard.mMostCommonKeyWidth; + mMoreKeysTemplate = keyboard.mMoreKeysTemplate; + mMaxMoreKeysKeyboardColumn = keyboard.mMaxMoreKeysKeyboardColumn; + mKeyVisualAttributes = keyboard.mKeyVisualAttributes; + mTopPadding = keyboard.mTopPadding; + mVerticalGap = keyboard.mVerticalGap; + + mSortedKeys = keyboard.mSortedKeys; + mShiftKeys = keyboard.mShiftKeys; + mAltCodeKeysWhileTyping = keyboard.mAltCodeKeysWhileTyping; + mIconsSet = keyboard.mIconsSet; + + mProximityInfo = keyboard.mProximityInfo; + mProximityCharsCorrectionEnabled = keyboard.mProximityCharsCorrectionEnabled; + mKeyboardLayout = keyboard.mKeyboardLayout; + } + + public boolean hasProximityCharsCorrection(final int code) { + if (!mProximityCharsCorrectionEnabled) { + return false; + } + // Note: The native code has the main keyboard layout only at this moment. + // TODO: Figure out how to handle proximity characters information of all layouts. + final boolean canAssumeNativeHasProximityCharsInfoOfAllKeys = ( + mId.mElementId == KeyboardId.ELEMENT_ALPHABET + || mId.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED); + return canAssumeNativeHasProximityCharsInfoOfAllKeys || Character.isLetter(code); + } + + @Nonnull + public ProximityInfo getProximityInfo() { + return mProximityInfo; + } + + @Nonnull + public KeyboardLayout getKeyboardLayout() { + return mKeyboardLayout; + } + + /** + * Return the sorted list of keys of this keyboard. + * The keys are sorted from top-left to bottom-right order. + * The list may contain {@link Key.Spacer} object as well. + * @return the sorted unmodifiable list of {@link Key}s of this keyboard. + */ + @Nonnull + public List getSortedKeys() { + return mSortedKeys; + } + + @Nullable + public Key getKey(final int code) { + if (code == Constants.CODE_UNSPECIFIED) { + return null; + } + synchronized (mKeyCache) { + final int index = mKeyCache.indexOfKey(code); + if (index >= 0) { + return mKeyCache.valueAt(index); + } + + for (final Key key : getSortedKeys()) { + if (key.getCode() == code) { + mKeyCache.put(code, key); + return key; + } + } + mKeyCache.put(code, null); + return null; + } + } + + public boolean hasKey(@Nonnull final Key aKey) { + if (mKeyCache.indexOfValue(aKey) >= 0) { + return true; + } + + for (final Key key : getSortedKeys()) { + if (key == aKey) { + mKeyCache.put(key.getCode(), key); + return true; + } + } + return false; + } + + @Override + public String toString() { + return mId.toString(); + } + + /** + * Returns the array of the keys that are closest to the given point. + * @param x the x-coordinate of the point + * @param y the y-coordinate of the point + * @return the list of the nearest keys to the given point. If the given + * point is out of range, then an array of size zero is returned. + */ + @Nonnull + public List getNearestKeys(final int x, final int y) { + // Avoid dead pixels at edges of the keyboard + final int adjustedX = Math.max(0, Math.min(x, mOccupiedWidth - 1)); + final int adjustedY = Math.max(0, Math.min(y, mOccupiedHeight - 1)); + return mProximityInfo.getNearestKeys(adjustedX, adjustedY); + } + + @Nonnull + public int[] getCoordinates(@Nonnull final int[] codePoints) { + final int length = codePoints.length; + final int[] coordinates = CoordinateUtils.newCoordinateArray(length); + for (int i = 0; i < length; ++i) { + final Key key = getKey(codePoints[i]); + if (null != key) { + CoordinateUtils.setXYInArray(coordinates, i, + key.getX() + key.getWidth() / 2, key.getY() + key.getHeight() / 2); + } else { + CoordinateUtils.setXYInArray(coordinates, i, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); + } + } + return coordinates; + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardActionListener.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardActionListener.java new file mode 100644 index 00000000..9dd60fed --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardActionListener.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2010 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.keyboard; + +import org.dslul.openboard.inputmethod.latin.common.Constants; +import org.dslul.openboard.inputmethod.latin.common.InputPointers; + +public interface KeyboardActionListener { + /** + * Called when the user presses a key. This is sent before the {@link #onCodeInput} is called. + * For keys that repeat, this is only called once. + * + * @param primaryCode the unicode of the key being pressed. If the touch is not on a valid key, + * the value will be zero. + * @param repeatCount how many times the key was repeated. Zero if it is the first press. + * @param isSinglePointer true if pressing has occurred while no other key is being pressed. + */ + public void onPressKey(int primaryCode, int repeatCount, boolean isSinglePointer); + + /** + * Called when the user releases a key. This is sent after the {@link #onCodeInput} is called. + * For keys that repeat, this is only called once. + * + * @param primaryCode the code of the key that was released + * @param withSliding true if releasing has occurred because the user slid finger from the key + * to other key without releasing the finger. + */ + public void onReleaseKey(int primaryCode, boolean withSliding); + + /** + * Send a key code to the listener. + * + * @param primaryCode this is the code of the key that was pressed + * @param x x-coordinate pixel of touched event. If {@link #onCodeInput} is not called by + * {@link PointerTracker} or so, the value should be + * {@link Constants#NOT_A_COORDINATE}. If it's called on insertion from the + * suggestion strip, it should be {@link Constants#SUGGESTION_STRIP_COORDINATE}. + * @param y y-coordinate pixel of touched event. If {@link #onCodeInput} is not called by + * {@link PointerTracker} or so, the value should be + * {@link Constants#NOT_A_COORDINATE}.If it's called on insertion from the + * suggestion strip, it should be {@link Constants#SUGGESTION_STRIP_COORDINATE}. + * @param isKeyRepeat true if this is a key repeat, false otherwise + */ + // TODO: change this to send an Event object instead + public void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat); + + /** + * Sends a string of characters to the listener. + * + * @param text the string of characters to be registered. + */ + public void onTextInput(String text); + + /** + * Called when user started batch input. + */ + public void onStartBatchInput(); + + /** + * Sends the ongoing batch input points data. + * @param batchPointers the batch input points representing the user input + */ + public void onUpdateBatchInput(InputPointers batchPointers); + + /** + * Sends the final batch input points data. + * + * @param batchPointers the batch input points representing the user input + */ + public void onEndBatchInput(InputPointers batchPointers); + + public void onCancelBatchInput(); + + /** + * Called when user released a finger outside any key. + */ + public void onCancelInput(); + + /** + * Called when user finished sliding key input. + */ + public void onFinishSlidingInput(); + + /** + * Send a non-"code input" custom request to the listener. + * @return true if the request has been consumed, false otherwise. + */ + public boolean onCustomRequest(int requestCode); + + public static final KeyboardActionListener EMPTY_LISTENER = new Adapter(); + + public static class Adapter implements KeyboardActionListener { + @Override + public void onPressKey(int primaryCode, int repeatCount, boolean isSinglePointer) {} + @Override + public void onReleaseKey(int primaryCode, boolean withSliding) {} + @Override + public void onCodeInput(int primaryCode, int x, int y, boolean isKeyRepeat) {} + @Override + public void onTextInput(String text) {} + @Override + public void onStartBatchInput() {} + @Override + public void onUpdateBatchInput(InputPointers batchPointers) {} + @Override + public void onEndBatchInput(InputPointers batchPointers) {} + @Override + public void onCancelBatchInput() {} + @Override + public void onCancelInput() {} + @Override + public void onFinishSlidingInput() {} + @Override + public boolean onCustomRequest(int requestCode) { + return false; + } + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardId.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardId.java new file mode 100644 index 00000000..df9afd7d --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardId.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2015 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.keyboard; + +import static org.dslul.openboard.inputmethod.latin.common.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET; + +import android.text.InputType; +import android.text.TextUtils; +import android.view.inputmethod.EditorInfo; + +import org.dslul.openboard.inputmethod.compat.EditorInfoCompatUtils; +import org.dslul.openboard.inputmethod.latin.RichInputMethodSubtype; +import org.dslul.openboard.inputmethod.latin.utils.InputTypeUtils; + +import java.util.Arrays; +import java.util.Locale; + +/** + * Unique identifier for each keyboard type. + */ +public final class KeyboardId { + public static final int MODE_TEXT = 0; + public static final int MODE_URL = 1; + public static final int MODE_EMAIL = 2; + public static final int MODE_IM = 3; + public static final int MODE_PHONE = 4; + public static final int MODE_NUMBER = 5; + public static final int MODE_DATE = 6; + public static final int MODE_TIME = 7; + public static final int MODE_DATETIME = 8; + + public static final int ELEMENT_ALPHABET = 0; + public static final int ELEMENT_ALPHABET_MANUAL_SHIFTED = 1; + public static final int ELEMENT_ALPHABET_AUTOMATIC_SHIFTED = 2; + public static final int ELEMENT_ALPHABET_SHIFT_LOCKED = 3; + public static final int ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED = 4; + public static final int ELEMENT_SYMBOLS = 5; + public static final int ELEMENT_SYMBOLS_SHIFTED = 6; + public static final int ELEMENT_PHONE = 7; + public static final int ELEMENT_PHONE_SYMBOLS = 8; + public static final int ELEMENT_NUMBER = 9; + public static final int ELEMENT_EMOJI_RECENTS = 10; + public static final int ELEMENT_EMOJI_CATEGORY1 = 11; + public static final int ELEMENT_EMOJI_CATEGORY2 = 12; + public static final int ELEMENT_EMOJI_CATEGORY3 = 13; + public static final int ELEMENT_EMOJI_CATEGORY4 = 14; + public static final int ELEMENT_EMOJI_CATEGORY5 = 15; + public static final int ELEMENT_EMOJI_CATEGORY6 = 16; + public static final int ELEMENT_EMOJI_CATEGORY7 = 17; + public static final int ELEMENT_EMOJI_CATEGORY8 = 18; + public static final int ELEMENT_EMOJI_CATEGORY9 = 19; + public static final int ELEMENT_EMOJI_CATEGORY10 = 20; + public static final int ELEMENT_EMOJI_CATEGORY11 = 21; + public static final int ELEMENT_EMOJI_CATEGORY12 = 22; + public static final int ELEMENT_EMOJI_CATEGORY13 = 23; + public static final int ELEMENT_EMOJI_CATEGORY14 = 24; + public static final int ELEMENT_EMOJI_CATEGORY15 = 25; + public static final int ELEMENT_EMOJI_CATEGORY16 = 26; + + public final RichInputMethodSubtype mSubtype; + public final int mWidth; + public final int mHeight; + public final int mMode; + public final int mElementId; + public final EditorInfo mEditorInfo; + public final boolean mClobberSettingsKey; + public final boolean mLanguageSwitchKeyEnabled; + public final String mCustomActionLabel; + public final boolean mHasShortcutKey; + public final boolean mIsSplitLayout; + + private final int mHashCode; + + public KeyboardId(final int elementId, final KeyboardLayoutSet.Params params) { + mSubtype = params.mSubtype; + mWidth = params.mKeyboardWidth; + mHeight = params.mKeyboardHeight; + mMode = params.mMode; + mElementId = elementId; + mEditorInfo = params.mEditorInfo; + mClobberSettingsKey = params.mNoSettingsKey; + mLanguageSwitchKeyEnabled = params.mLanguageSwitchKeyEnabled; + mCustomActionLabel = (mEditorInfo.actionLabel != null) + ? mEditorInfo.actionLabel.toString() : null; + mHasShortcutKey = params.mVoiceInputKeyEnabled; + mIsSplitLayout = params.mIsSplitLayoutEnabled; + + mHashCode = computeHashCode(this); + } + + private static int computeHashCode(final KeyboardId id) { + return Arrays.hashCode(new Object[] { + id.mElementId, + id.mMode, + id.mWidth, + id.mHeight, + id.passwordInput(), + id.mClobberSettingsKey, + id.mHasShortcutKey, + id.mLanguageSwitchKeyEnabled, + id.isMultiLine(), + id.imeAction(), + id.mCustomActionLabel, + id.navigateNext(), + id.navigatePrevious(), + id.mSubtype, + id.mIsSplitLayout + }); + } + + private boolean equals(final KeyboardId other) { + if (other == this) + return true; + return other.mElementId == mElementId + && other.mMode == mMode + && other.mWidth == mWidth + && other.mHeight == mHeight + && other.passwordInput() == passwordInput() + && other.mClobberSettingsKey == mClobberSettingsKey + && other.mHasShortcutKey == mHasShortcutKey + && other.mLanguageSwitchKeyEnabled == mLanguageSwitchKeyEnabled + && other.isMultiLine() == isMultiLine() + && other.imeAction() == imeAction() + && TextUtils.equals(other.mCustomActionLabel, mCustomActionLabel) + && other.navigateNext() == navigateNext() + && other.navigatePrevious() == navigatePrevious() + && other.mSubtype.equals(mSubtype) + && other.mIsSplitLayout == mIsSplitLayout; + } + + private static boolean isAlphabetKeyboard(final int elementId) { + return elementId < ELEMENT_SYMBOLS; + } + + public boolean isAlphabetKeyboard() { + return isAlphabetKeyboard(mElementId); + } + + public boolean navigateNext() { + return (mEditorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0 + || imeAction() == EditorInfo.IME_ACTION_NEXT; + } + + public boolean navigatePrevious() { + return (mEditorInfo.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0 + || imeAction() == EditorInfo.IME_ACTION_PREVIOUS; + } + + public boolean passwordInput() { + final int inputType = mEditorInfo.inputType; + return InputTypeUtils.isPasswordInputType(inputType) + || InputTypeUtils.isVisiblePasswordInputType(inputType); + } + + public boolean isMultiLine() { + return (mEditorInfo.inputType & InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0; + } + + public int imeAction() { + return InputTypeUtils.getImeOptionsActionIdFromEditorInfo(mEditorInfo); + } + + public Locale getLocale() { + return mSubtype.getLocale(); + } + + @Override + public boolean equals(final Object other) { + return other instanceof KeyboardId && equals((KeyboardId) other); + } + + @Override + public int hashCode() { + return mHashCode; + } + + @Override + public String toString() { + return String.format(Locale.ROOT, "[%s %s:%s %dx%d %s %s%s%s%s%s%s%s%s%s]", + elementIdToName(mElementId), + mSubtype.getLocale(), + mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET), + mWidth, mHeight, + modeName(mMode), + actionName(imeAction()), + (navigateNext() ? " navigateNext" : ""), + (navigatePrevious() ? " navigatePrevious" : ""), + (mClobberSettingsKey ? " clobberSettingsKey" : ""), + (passwordInput() ? " passwordInput" : ""), + (mHasShortcutKey ? " hasShortcutKey" : ""), + (mLanguageSwitchKeyEnabled ? " languageSwitchKeyEnabled" : ""), + (isMultiLine() ? " isMultiLine" : ""), + (mIsSplitLayout ? " isSplitLayout" : "") + ); + } + + public static boolean equivalentEditorInfoForKeyboard(final EditorInfo a, final EditorInfo b) { + if (a == null && b == null) return true; + if (a == null || b == null) return false; + return a.inputType == b.inputType + && a.imeOptions == b.imeOptions + && TextUtils.equals(a.privateImeOptions, b.privateImeOptions); + } + + public static String elementIdToName(final int elementId) { + switch (elementId) { + case ELEMENT_ALPHABET: return "alphabet"; + case ELEMENT_ALPHABET_MANUAL_SHIFTED: return "alphabetManualShifted"; + case ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: return "alphabetAutomaticShifted"; + case ELEMENT_ALPHABET_SHIFT_LOCKED: return "alphabetShiftLocked"; + case ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: return "alphabetShiftLockShifted"; + case ELEMENT_SYMBOLS: return "symbols"; + case ELEMENT_SYMBOLS_SHIFTED: return "symbolsShifted"; + case ELEMENT_PHONE: return "phone"; + case ELEMENT_PHONE_SYMBOLS: return "phoneSymbols"; + case ELEMENT_NUMBER: return "number"; + case ELEMENT_EMOJI_RECENTS: return "emojiRecents"; + case ELEMENT_EMOJI_CATEGORY1: return "emojiCategory1"; + case ELEMENT_EMOJI_CATEGORY2: return "emojiCategory2"; + case ELEMENT_EMOJI_CATEGORY3: return "emojiCategory3"; + case ELEMENT_EMOJI_CATEGORY4: return "emojiCategory4"; + case ELEMENT_EMOJI_CATEGORY5: return "emojiCategory5"; + case ELEMENT_EMOJI_CATEGORY6: return "emojiCategory6"; + case ELEMENT_EMOJI_CATEGORY7: return "emojiCategory7"; + case ELEMENT_EMOJI_CATEGORY8: return "emojiCategory8"; + case ELEMENT_EMOJI_CATEGORY9: return "emojiCategory9"; + case ELEMENT_EMOJI_CATEGORY10: return "emojiCategory10"; + case ELEMENT_EMOJI_CATEGORY11: return "emojiCategory11"; + case ELEMENT_EMOJI_CATEGORY12: return "emojiCategory12"; + case ELEMENT_EMOJI_CATEGORY13: return "emojiCategory13"; + case ELEMENT_EMOJI_CATEGORY14: return "emojiCategory14"; + case ELEMENT_EMOJI_CATEGORY15: return "emojiCategory15"; + case ELEMENT_EMOJI_CATEGORY16: return "emojiCategory16"; + default: return null; + } + } + + public static String modeName(final int mode) { + switch (mode) { + case MODE_TEXT: return "text"; + case MODE_URL: return "url"; + case MODE_EMAIL: return "email"; + case MODE_IM: return "im"; + case MODE_PHONE: return "phone"; + case MODE_NUMBER: return "number"; + case MODE_DATE: return "date"; + case MODE_TIME: return "time"; + case MODE_DATETIME: return "datetime"; + default: return null; + } + } + + public static String actionName(final int actionId) { + return (actionId == InputTypeUtils.IME_ACTION_CUSTOM_LABEL) ? "actionCustomLabel" + : EditorInfoCompatUtils.imeActionName(actionId); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardLayout.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardLayout.java new file mode 100644 index 00000000..abc54ec9 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardLayout.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2015 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.keyboard; + +import org.dslul.openboard.inputmethod.annotations.UsedForTesting; + +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.Nonnull; + +/** + * KeyboardLayout maintains the keyboard layout information. + */ +public class KeyboardLayout { + + private final int[] mKeyCodes; + + private final int[] mKeyXCoordinates; + private final int[] mKeyYCoordinates; + + private final int[] mKeyWidths; + private final int[] mKeyHeights; + + public final int mMostCommonKeyWidth; + public final int mMostCommonKeyHeight; + + public final int mKeyboardWidth; + public final int mKeyboardHeight; + + public KeyboardLayout(ArrayList layoutKeys, int mostCommonKeyWidth, + int mostCommonKeyHeight, int keyboardWidth, int keyboardHeight) { + mMostCommonKeyWidth = mostCommonKeyWidth; + mMostCommonKeyHeight = mostCommonKeyHeight; + mKeyboardWidth = keyboardWidth; + mKeyboardHeight = keyboardHeight; + + mKeyCodes = new int[layoutKeys.size()]; + mKeyXCoordinates = new int[layoutKeys.size()]; + mKeyYCoordinates = new int[layoutKeys.size()]; + mKeyWidths = new int[layoutKeys.size()]; + mKeyHeights = new int[layoutKeys.size()]; + + for (int i = 0; i < layoutKeys.size(); i++) { + Key key = layoutKeys.get(i); + mKeyCodes[i] = Character.toLowerCase(key.getCode()); + mKeyXCoordinates[i] = key.getX(); + mKeyYCoordinates[i] = key.getY(); + mKeyWidths[i] = key.getWidth(); + mKeyHeights[i] = key.getHeight(); + } + } + + @UsedForTesting + public int[] getKeyCodes() { + return mKeyCodes; + } + + /** + * The x-coordinate for the top-left corner of the keys. + * + */ + public int[] getKeyXCoordinates() { + return mKeyXCoordinates; + } + + /** + * The y-coordinate for the top-left corner of the keys. + */ + public int[] getKeyYCoordinates() { + return mKeyYCoordinates; + } + + /** + * The widths of the keys which are smaller than the true hit-area due to the gaps + * between keys. The mostCommonKey(Width/Height) represents the true key width/height + * including the gaps. + */ + public int[] getKeyWidths() { + return mKeyWidths; + } + + /** + * The heights of the keys which are smaller than the true hit-area due to the gaps + * between keys. The mostCommonKey(Width/Height) represents the true key width/height + * including the gaps. + */ + public int[] getKeyHeights() { + return mKeyHeights; + } + + /** + * Factory method to create {@link KeyboardLayout} objects. + */ + public static KeyboardLayout newKeyboardLayout(@Nonnull final List sortedKeys, + int mostCommonKeyWidth, int mostCommonKeyHeight, + int occupiedWidth, int occupiedHeight) { + final ArrayList layoutKeys = new ArrayList(); + for (final Key key : sortedKeys) { + if (!ProximityInfo.needsProximityInfo(key)) { + continue; + } + if (key.getCode() != ',') { + layoutKeys.add(key); + } + } + return new KeyboardLayout(layoutKeys, mostCommonKeyWidth, + mostCommonKeyHeight, occupiedWidth, occupiedHeight); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardLayoutSet.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardLayoutSet.java new file mode 100644 index 00000000..f881eb19 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardLayoutSet.java @@ -0,0 +1,508 @@ +/* + * 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.keyboard; + +import static org.dslul.openboard.inputmethod.latin.common.Constants.ImeOption.FORCE_ASCII; +import static org.dslul.openboard.inputmethod.latin.common.Constants.ImeOption.NO_SETTINGS_KEY; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.text.InputType; +import android.util.Log; +import android.util.SparseArray; +import android.util.Xml; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodSubtype; + +import org.dslul.openboard.inputmethod.compat.EditorInfoCompatUtils; +import org.dslul.openboard.inputmethod.compat.InputMethodSubtypeCompatUtils; +import org.dslul.openboard.inputmethod.compat.UserManagerCompatUtils; +import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardBuilder; +import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardParams; +import org.dslul.openboard.inputmethod.keyboard.internal.UniqueKeysCache; +import org.dslul.openboard.inputmethod.latin.InputAttributes; +import org.dslul.openboard.inputmethod.latin.R; +import org.dslul.openboard.inputmethod.latin.RichInputMethodSubtype; +import org.dslul.openboard.inputmethod.latin.define.DebugFlags; +import org.dslul.openboard.inputmethod.latin.utils.InputTypeUtils; +import org.dslul.openboard.inputmethod.latin.utils.ScriptUtils; +import org.dslul.openboard.inputmethod.latin.utils.SubtypeLocaleUtils; +import org.dslul.openboard.inputmethod.latin.utils.XmlParseUtils; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.lang.ref.SoftReference; +import java.util.HashMap; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * This class represents a set of keyboard layouts. Each of them represents a different keyboard + * specific to a keyboard state, such as alphabet, symbols, and so on. Layouts in the same + * {@link KeyboardLayoutSet} are related to each other. + * A {@link KeyboardLayoutSet} needs to be created for each + * {@link android.view.inputmethod.EditorInfo}. + */ +public final class KeyboardLayoutSet { + private static final String TAG = KeyboardLayoutSet.class.getSimpleName(); + private static final boolean DEBUG_CACHE = false; + + private static final String TAG_KEYBOARD_SET = "KeyboardLayoutSet"; + private static final String TAG_ELEMENT = "Element"; + private static final String TAG_FEATURE = "Feature"; + + private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_"; + + private final Context mContext; + @Nonnull + private final Params mParams; + + // How many layouts we forcibly keep in cache. This only includes ALPHABET (default) and + // ALPHABET_AUTOMATIC_SHIFTED layouts - other layouts may stay in memory in the map of + // soft-references, but we forcibly cache this many alphabetic/auto-shifted layouts. + private static final int FORCIBLE_CACHE_SIZE = 4; + // By construction of soft references, anything that is also referenced somewhere else + // will stay in the cache. So we forcibly keep some references in an array to prevent + // them from disappearing from sKeyboardCache. + private static final Keyboard[] sForcibleKeyboardCache = new Keyboard[FORCIBLE_CACHE_SIZE]; + private static final HashMap> sKeyboardCache = + new HashMap<>(); + @Nonnull + private static final UniqueKeysCache sUniqueKeysCache = UniqueKeysCache.newInstance(); + private final static HashMap sScriptIdsForSubtypes = + new HashMap<>(); + + @SuppressWarnings("serial") + public static final class KeyboardLayoutSetException extends RuntimeException { + public final KeyboardId mKeyboardId; + + public KeyboardLayoutSetException(final Throwable cause, final KeyboardId keyboardId) { + super(cause); + mKeyboardId = keyboardId; + } + } + + private static final class ElementParams { + int mKeyboardXmlId; + boolean mProximityCharsCorrectionEnabled; + boolean mSupportsSplitLayout; + boolean mAllowRedundantMoreKeys; + public ElementParams() {} + } + + public static final class Params { + String mKeyboardLayoutSetName; + int mMode; + boolean mDisableTouchPositionCorrectionDataForTest; + // TODO: Use {@link InputAttributes} instead of these variables. + EditorInfo mEditorInfo; + boolean mIsPasswordField; + boolean mVoiceInputKeyEnabled; + boolean mNoSettingsKey; + boolean mLanguageSwitchKeyEnabled; + RichInputMethodSubtype mSubtype; + boolean mIsSpellChecker; + int mKeyboardWidth; + int mKeyboardHeight; + int mScriptId = ScriptUtils.SCRIPT_LATIN; + // Indicates if the user has enabled the split-layout preference + // and the required ProductionFlags are enabled. + boolean mIsSplitLayoutEnabledByUser; + // Indicates if split layout is actually enabled, taking into account + // whether the user has enabled it, and the keyboard layout supports it. + boolean mIsSplitLayoutEnabled; + // Sparse array of KeyboardLayoutSet element parameters indexed by element's id. + final SparseArray mKeyboardLayoutSetElementIdToParamsMap = + new SparseArray<>(); + } + + public static void onSystemLocaleChanged() { + clearKeyboardCache(); + } + + public static void onKeyboardThemeChanged() { + clearKeyboardCache(); + } + + private static void clearKeyboardCache() { + sKeyboardCache.clear(); + sUniqueKeysCache.clear(); + } + + public static int getScriptId(final Resources resources, + @Nonnull final InputMethodSubtype subtype) { + final Integer value = sScriptIdsForSubtypes.get(subtype); + if (null == value) { + final int scriptId = Builder.readScriptId(resources, subtype); + sScriptIdsForSubtypes.put(subtype, scriptId); + return scriptId; + } + return value; + } + + KeyboardLayoutSet(final Context context, @Nonnull final Params params) { + mContext = context; + mParams = params; + } + + @Nonnull + public Keyboard getKeyboard(final int baseKeyboardLayoutSetElementId) { + final int keyboardLayoutSetElementId; + switch (mParams.mMode) { + case KeyboardId.MODE_PHONE: + if (baseKeyboardLayoutSetElementId == KeyboardId.ELEMENT_SYMBOLS) { + keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE_SYMBOLS; + } else { + keyboardLayoutSetElementId = KeyboardId.ELEMENT_PHONE; + } + break; + case KeyboardId.MODE_NUMBER: + case KeyboardId.MODE_DATE: + case KeyboardId.MODE_TIME: + case KeyboardId.MODE_DATETIME: + keyboardLayoutSetElementId = KeyboardId.ELEMENT_NUMBER; + break; + default: + keyboardLayoutSetElementId = baseKeyboardLayoutSetElementId; + break; + } + + ElementParams elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get( + keyboardLayoutSetElementId); + if (elementParams == null) { + elementParams = mParams.mKeyboardLayoutSetElementIdToParamsMap.get( + KeyboardId.ELEMENT_ALPHABET); + } + // Note: The keyboard for each shift state, and mode are represented as an elementName + // attribute in a keyboard_layout_set XML file. Also each keyboard layout XML resource is + // specified as an elementKeyboard attribute in the file. + // The KeyboardId is an internal key for a Keyboard object. + + mParams.mIsSplitLayoutEnabled = mParams.mIsSplitLayoutEnabledByUser + && elementParams.mSupportsSplitLayout; + final KeyboardId id = new KeyboardId(keyboardLayoutSetElementId, mParams); + try { + return getKeyboard(elementParams, id); + } catch (final RuntimeException e) { + Log.e(TAG, "Can't create keyboard: " + id, e); + throw new KeyboardLayoutSetException(e, id); + } + } + + @Nonnull + private Keyboard getKeyboard(final ElementParams elementParams, final KeyboardId id) { + final SoftReference ref = sKeyboardCache.get(id); + final Keyboard cachedKeyboard = (ref == null) ? null : ref.get(); + if (cachedKeyboard != null) { + if (DEBUG_CACHE) { + Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": HIT id=" + id); + } + return cachedKeyboard; + } + + final KeyboardBuilder builder = + new KeyboardBuilder<>(mContext, new KeyboardParams(sUniqueKeysCache)); + sUniqueKeysCache.setEnabled(id.isAlphabetKeyboard()); + builder.setAllowRedundantMoreKes(elementParams.mAllowRedundantMoreKeys); + final int keyboardXmlId = elementParams.mKeyboardXmlId; + builder.load(keyboardXmlId, id); + if (mParams.mDisableTouchPositionCorrectionDataForTest) { + builder.disableTouchPositionCorrectionDataForTest(); + } + builder.setProximityCharsCorrectionEnabled(elementParams.mProximityCharsCorrectionEnabled); + final Keyboard keyboard = builder.build(); + sKeyboardCache.put(id, new SoftReference<>(keyboard)); + if ((id.mElementId == KeyboardId.ELEMENT_ALPHABET + || id.mElementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) + && !mParams.mIsSpellChecker) { + // We only forcibly cache the primary, "ALPHABET", layouts. + for (int i = sForcibleKeyboardCache.length - 1; i >= 1; --i) { + sForcibleKeyboardCache[i] = sForcibleKeyboardCache[i - 1]; + } + sForcibleKeyboardCache[0] = keyboard; + if (DEBUG_CACHE) { + Log.d(TAG, "forcing caching of keyboard with id=" + id); + } + } + if (DEBUG_CACHE) { + Log.d(TAG, "keyboard cache size=" + sKeyboardCache.size() + ": " + + ((ref == null) ? "LOAD" : "GCed") + " id=" + id); + } + return keyboard; + } + + public int getScriptId() { + return mParams.mScriptId; + } + + public static final class Builder { + private final Context mContext; + private final String mPackageName; + private final Resources mResources; + + private final Params mParams = new Params(); + + private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo(); + + public Builder(final Context context, @Nullable final EditorInfo ei) { + mContext = context; + mPackageName = context.getPackageName(); + mResources = context.getResources(); + final Params params = mParams; + + final EditorInfo editorInfo = (ei != null) ? ei : EMPTY_EDITOR_INFO; + params.mMode = getKeyboardMode(editorInfo); + // TODO: Consolidate those with {@link InputAttributes}. + params.mEditorInfo = editorInfo; + params.mIsPasswordField = InputTypeUtils.isPasswordInputType(editorInfo.inputType); + params.mNoSettingsKey = InputAttributes.inPrivateImeOptions( + mPackageName, NO_SETTINGS_KEY, editorInfo); + + // When the device is still unlocked, features like showing the IME setting app need to + // be locked down. + // TODO: Switch to {@code UserManagerCompat.isUserUnlocked()} in the support-v4 library + // when it becomes publicly available. + @UserManagerCompatUtils.LockState + final int lockState = UserManagerCompatUtils.getUserLockState(context); + if (lockState == UserManagerCompatUtils.LOCK_STATE_LOCKED) { + params.mNoSettingsKey = true; + } + } + + public Builder setKeyboardGeometry(final int keyboardWidth, final int keyboardHeight) { + mParams.mKeyboardWidth = keyboardWidth; + mParams.mKeyboardHeight = keyboardHeight; + return this; + } + + public Builder setSubtype(@Nonnull final RichInputMethodSubtype subtype) { + final boolean asciiCapable = InputMethodSubtypeCompatUtils.isAsciiCapable(subtype); + // TODO: Consolidate with {@link InputAttributes}. + @SuppressWarnings("deprecation") + final boolean deprecatedForceAscii = InputAttributes.inPrivateImeOptions( + mPackageName, FORCE_ASCII, mParams.mEditorInfo); + final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii( + mParams.mEditorInfo.imeOptions) + || deprecatedForceAscii; + final RichInputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable) + ? RichInputMethodSubtype.getNoLanguageSubtype() + : subtype; + mParams.mSubtype = keyboardSubtype; + mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX + + keyboardSubtype.getKeyboardLayoutSetName(); + return this; + } + + public Builder setIsSpellChecker(final boolean isSpellChecker) { + mParams.mIsSpellChecker = isSpellChecker; + return this; + } + + public Builder setVoiceInputKeyEnabled(final boolean enabled) { + mParams.mVoiceInputKeyEnabled = enabled; + return this; + } + + public Builder setLanguageSwitchKeyEnabled(final boolean enabled) { + mParams.mLanguageSwitchKeyEnabled = enabled; + return this; + } + + public Builder disableTouchPositionCorrectionData() { + mParams.mDisableTouchPositionCorrectionDataForTest = true; + return this; + } + + public Builder setSplitLayoutEnabledByUser(final boolean enabled) { + mParams.mIsSplitLayoutEnabledByUser = enabled; + return this; + } + + // Super redux version of reading the script ID for some subtype from Xml. + static int readScriptId(final Resources resources, final InputMethodSubtype subtype) { + final String layoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX + + SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype); + final int xmlId = getXmlId(resources, layoutSetName); + final XmlResourceParser parser = resources.getXml(xmlId); + try { + while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { + // Bovinate through the XML stupidly searching for TAG_FEATURE, and read + // the script Id from it. + parser.next(); + final String tag = parser.getName(); + if (TAG_FEATURE.equals(tag)) { + return readScriptIdFromTagFeature(resources, parser); + } + } + } catch (final IOException | XmlPullParserException e) { + throw new RuntimeException(e.getMessage() + " in " + layoutSetName, e); + } finally { + parser.close(); + } + // If the tag is not found, then the default script is Latin. + return ScriptUtils.SCRIPT_LATIN; + } + + private static int readScriptIdFromTagFeature(final Resources resources, + final XmlPullParser parser) throws IOException, XmlPullParserException { + final TypedArray featureAttr = resources.obtainAttributes(Xml.asAttributeSet(parser), + R.styleable.KeyboardLayoutSet_Feature); + try { + final int scriptId = + featureAttr.getInt(R.styleable.KeyboardLayoutSet_Feature_supportedScript, + ScriptUtils.SCRIPT_UNKNOWN); + XmlParseUtils.checkEndTag(TAG_FEATURE, parser); + return scriptId; + } finally { + featureAttr.recycle(); + } + } + + public KeyboardLayoutSet build() { + if (mParams.mSubtype == null) + throw new RuntimeException("KeyboardLayoutSet subtype is not specified"); + final int xmlId = getXmlId(mResources, mParams.mKeyboardLayoutSetName); + try { + parseKeyboardLayoutSet(mResources, xmlId); + } catch (final IOException | XmlPullParserException e) { + throw new RuntimeException(e.getMessage() + " in " + mParams.mKeyboardLayoutSetName, + e); + } + return new KeyboardLayoutSet(mContext, mParams); + } + + private static int getXmlId(final Resources resources, final String keyboardLayoutSetName) { + final String packageName = resources.getResourcePackageName( + R.xml.keyboard_layout_set_qwerty); + return resources.getIdentifier(keyboardLayoutSetName, "xml", packageName); + } + + private void parseKeyboardLayoutSet(final Resources res, final int resId) + throws XmlPullParserException, IOException { + final XmlResourceParser parser = res.getXml(resId); + try { + while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { + final int event = parser.next(); + if (event == XmlPullParser.START_TAG) { + final String tag = parser.getName(); + if (TAG_KEYBOARD_SET.equals(tag)) { + parseKeyboardLayoutSetContent(parser); + } else { + throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET); + } + } + } + } finally { + parser.close(); + } + } + + private void parseKeyboardLayoutSetContent(final XmlPullParser parser) + throws XmlPullParserException, IOException { + while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { + final int event = parser.next(); + if (event == XmlPullParser.START_TAG) { + final String tag = parser.getName(); + if (TAG_ELEMENT.equals(tag)) { + parseKeyboardLayoutSetElement(parser); + } else if (TAG_FEATURE.equals(tag)) { + mParams.mScriptId = readScriptIdFromTagFeature(mResources, parser); + } else { + throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET); + } + } else if (event == XmlPullParser.END_TAG) { + final String tag = parser.getName(); + if (TAG_KEYBOARD_SET.equals(tag)) { + break; + } + throw new XmlParseUtils.IllegalEndTag(parser, tag, TAG_KEYBOARD_SET); + } + } + } + + private void parseKeyboardLayoutSetElement(final XmlPullParser parser) + throws XmlPullParserException, IOException { + final TypedArray a = mResources.obtainAttributes(Xml.asAttributeSet(parser), + R.styleable.KeyboardLayoutSet_Element); + try { + XmlParseUtils.checkAttributeExists(a, + R.styleable.KeyboardLayoutSet_Element_elementName, "elementName", + TAG_ELEMENT, parser); + XmlParseUtils.checkAttributeExists(a, + R.styleable.KeyboardLayoutSet_Element_elementKeyboard, "elementKeyboard", + TAG_ELEMENT, parser); + XmlParseUtils.checkEndTag(TAG_ELEMENT, parser); + + final ElementParams elementParams = new ElementParams(); + final int elementName = a.getInt( + R.styleable.KeyboardLayoutSet_Element_elementName, 0); + elementParams.mKeyboardXmlId = a.getResourceId( + R.styleable.KeyboardLayoutSet_Element_elementKeyboard, 0); + elementParams.mProximityCharsCorrectionEnabled = a.getBoolean( + R.styleable.KeyboardLayoutSet_Element_enableProximityCharsCorrection, + false); + elementParams.mSupportsSplitLayout = a.getBoolean( + R.styleable.KeyboardLayoutSet_Element_supportsSplitLayout, false); + elementParams.mAllowRedundantMoreKeys = a.getBoolean( + R.styleable.KeyboardLayoutSet_Element_allowRedundantMoreKeys, true); + mParams.mKeyboardLayoutSetElementIdToParamsMap.put(elementName, elementParams); + } finally { + a.recycle(); + } + } + + private static int getKeyboardMode(final EditorInfo editorInfo) { + final int inputType = editorInfo.inputType; + final int variation = inputType & InputType.TYPE_MASK_VARIATION; + + switch (inputType & InputType.TYPE_MASK_CLASS) { + case InputType.TYPE_CLASS_NUMBER: + return KeyboardId.MODE_NUMBER; + case InputType.TYPE_CLASS_DATETIME: + switch (variation) { + case InputType.TYPE_DATETIME_VARIATION_DATE: + return KeyboardId.MODE_DATE; + case InputType.TYPE_DATETIME_VARIATION_TIME: + return KeyboardId.MODE_TIME; + default: // InputType.TYPE_DATETIME_VARIATION_NORMAL + return KeyboardId.MODE_DATETIME; + } + case InputType.TYPE_CLASS_PHONE: + return KeyboardId.MODE_PHONE; + case InputType.TYPE_CLASS_TEXT: + if (InputTypeUtils.isEmailVariation(variation)) { + return KeyboardId.MODE_EMAIL; + } else if (variation == InputType.TYPE_TEXT_VARIATION_URI) { + return KeyboardId.MODE_URL; + } else if (variation == InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE) { + return KeyboardId.MODE_IM; + } else if (variation == InputType.TYPE_TEXT_VARIATION_FILTER) { + return KeyboardId.MODE_TEXT; + } else { + return KeyboardId.MODE_TEXT; + } + default: + return KeyboardId.MODE_TEXT; + } + } + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardSwitcher.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardSwitcher.java new file mode 100644 index 00000000..c58b0a8f --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardSwitcher.java @@ -0,0 +1,503 @@ +/* + * Copyright (C) 2008 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.keyboard; + +import android.content.Context; +import android.content.res.Resources; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.inputmethod.EditorInfo; + +import org.dslul.openboard.inputmethod.compat.InputMethodServiceCompatUtils; +import org.dslul.openboard.inputmethod.event.Event; +import org.dslul.openboard.inputmethod.keyboard.KeyboardLayoutSet.KeyboardLayoutSetException; +import org.dslul.openboard.inputmethod.keyboard.emoji.EmojiPalettesView; +import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardState; +import org.dslul.openboard.inputmethod.keyboard.internal.KeyboardTextsSet; +import org.dslul.openboard.inputmethod.latin.InputView; +import org.dslul.openboard.inputmethod.latin.LatinIME; +import org.dslul.openboard.inputmethod.latin.R; +import org.dslul.openboard.inputmethod.latin.RichInputMethodManager; +import org.dslul.openboard.inputmethod.latin.WordComposer; +import org.dslul.openboard.inputmethod.latin.define.ProductionFlags; +import org.dslul.openboard.inputmethod.latin.settings.Settings; +import org.dslul.openboard.inputmethod.latin.settings.SettingsValues; +import org.dslul.openboard.inputmethod.latin.utils.CapsModeUtils; +import org.dslul.openboard.inputmethod.latin.utils.LanguageOnSpacebarUtils; +import org.dslul.openboard.inputmethod.latin.utils.RecapitalizeStatus; +import org.dslul.openboard.inputmethod.latin.utils.ResourceUtils; +import org.dslul.openboard.inputmethod.latin.utils.ScriptUtils; + +import javax.annotation.Nonnull; + +public final class KeyboardSwitcher implements KeyboardState.SwitchActions { + private static final String TAG = KeyboardSwitcher.class.getSimpleName(); + + private InputView mCurrentInputView; + private View mMainKeyboardFrame; + private MainKeyboardView mKeyboardView; + private EmojiPalettesView mEmojiPalettesView; + private LatinIME mLatinIME; + private RichInputMethodManager mRichImm; + private boolean mIsHardwareAcceleratedDrawingEnabled; + + private KeyboardState mState; + + private KeyboardLayoutSet mKeyboardLayoutSet; + // TODO: The following {@link KeyboardTextsSet} should be in {@link KeyboardLayoutSet}. + private final KeyboardTextsSet mKeyboardTextsSet = new KeyboardTextsSet(); + + private KeyboardTheme mKeyboardTheme; + private Context mThemeContext; + + private static final KeyboardSwitcher sInstance = new KeyboardSwitcher(); + + public static KeyboardSwitcher getInstance() { + return sInstance; + } + + private KeyboardSwitcher() { + // Intentional empty constructor for singleton. + } + + public static void init(final LatinIME latinIme) { + sInstance.initInternal(latinIme); + } + + private void initInternal(final LatinIME latinIme) { + mLatinIME = latinIme; + mRichImm = RichInputMethodManager.getInstance(); + mState = new KeyboardState(this); + mIsHardwareAcceleratedDrawingEnabled = + InputMethodServiceCompatUtils.enableHardwareAcceleration(mLatinIME); + } + + public void updateKeyboardTheme() { + final boolean themeUpdated = updateKeyboardThemeAndContextThemeWrapper( + mLatinIME, KeyboardTheme.getKeyboardTheme(mLatinIME /* context */)); + if (themeUpdated && mKeyboardView != null) { + mLatinIME.setInputView(onCreateInputView(mIsHardwareAcceleratedDrawingEnabled)); + } + } + + private boolean updateKeyboardThemeAndContextThemeWrapper(final Context context, + final KeyboardTheme keyboardTheme) { + if (mThemeContext == null || !keyboardTheme.equals(mKeyboardTheme)) { + mKeyboardTheme = keyboardTheme; + mThemeContext = new ContextThemeWrapper(context, keyboardTheme.mStyleId); + KeyboardLayoutSet.onKeyboardThemeChanged(); + return true; + } + return false; + } + + public void loadKeyboard(final EditorInfo editorInfo, final SettingsValues settingsValues, + final int currentAutoCapsState, final int currentRecapitalizeState) { + final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder( + mThemeContext, editorInfo); + final Resources res = mThemeContext.getResources(); + final int keyboardWidth = ResourceUtils.getDefaultKeyboardWidth(res); + final int keyboardHeight = ResourceUtils.getKeyboardHeight(res, settingsValues); + builder.setKeyboardGeometry(keyboardWidth, keyboardHeight); + builder.setSubtype(mRichImm.getCurrentSubtype()); + builder.setVoiceInputKeyEnabled(settingsValues.mShowsVoiceInputKey); + builder.setLanguageSwitchKeyEnabled(mLatinIME.shouldShowLanguageSwitchKey()); + builder.setSplitLayoutEnabledByUser(ProductionFlags.IS_SPLIT_KEYBOARD_SUPPORTED + && settingsValues.mIsSplitKeyboardEnabled); + mKeyboardLayoutSet = builder.build(); + try { + mState.onLoadKeyboard(currentAutoCapsState, currentRecapitalizeState); + mKeyboardTextsSet.setLocale(mRichImm.getCurrentSubtypeLocale(), mThemeContext); + } catch (KeyboardLayoutSetException e) { + Log.w(TAG, "loading keyboard failed: " + e.mKeyboardId, e.getCause()); + } + } + + public void saveKeyboardState() { + if (getKeyboard() != null || isShowingEmojiPalettes()) { + mState.onSaveKeyboardState(); + } + } + + public void onHideWindow() { + if (mKeyboardView != null) { + mKeyboardView.onHideWindow(); + } + } + + private void setKeyboard( + @Nonnull final int keyboardId, + @Nonnull final KeyboardSwitchState toggleState) { + // Make {@link MainKeyboardView} visible and hide {@link EmojiPalettesView}. + final SettingsValues currentSettingsValues = Settings.getInstance().getCurrent(); + setMainKeyboardFrame(currentSettingsValues, toggleState); + // TODO: pass this object to setKeyboard instead of getting the current values. + final MainKeyboardView keyboardView = mKeyboardView; + final Keyboard oldKeyboard = keyboardView.getKeyboard(); + final Keyboard newKeyboard = mKeyboardLayoutSet.getKeyboard(keyboardId); + keyboardView.setKeyboard(newKeyboard); + mCurrentInputView.setKeyboardTopPadding(newKeyboard.mTopPadding); + keyboardView.setKeyPreviewPopupEnabled( + currentSettingsValues.mKeyPreviewPopupOn, + currentSettingsValues.mKeyPreviewPopupDismissDelay); + keyboardView.setKeyPreviewAnimationParams( + currentSettingsValues.mHasCustomKeyPreviewAnimationParams, + currentSettingsValues.mKeyPreviewShowUpStartXScale, + currentSettingsValues.mKeyPreviewShowUpStartYScale, + currentSettingsValues.mKeyPreviewShowUpDuration, + currentSettingsValues.mKeyPreviewDismissEndXScale, + currentSettingsValues.mKeyPreviewDismissEndYScale, + currentSettingsValues.mKeyPreviewDismissDuration); + keyboardView.updateShortcutKey(mRichImm.isShortcutImeReady()); + final boolean subtypeChanged = (oldKeyboard == null) + || !newKeyboard.mId.mSubtype.equals(oldKeyboard.mId.mSubtype); + final int languageOnSpacebarFormatType = LanguageOnSpacebarUtils + .getLanguageOnSpacebarFormatType(newKeyboard.mId.mSubtype); + final boolean hasMultipleEnabledIMEsOrSubtypes = mRichImm + .hasMultipleEnabledIMEsOrSubtypes(true /* shouldIncludeAuxiliarySubtypes */); + keyboardView.startDisplayLanguageOnSpacebar(subtypeChanged, languageOnSpacebarFormatType, + hasMultipleEnabledIMEsOrSubtypes); + } + + public Keyboard getKeyboard() { + if (mKeyboardView != null) { + return mKeyboardView.getKeyboard(); + } + return null; + } + + // TODO: Remove this method. Come up with a more comprehensive way to reset the keyboard layout + // when a keyboard layout set doesn't get reloaded in LatinIME.onStartInputViewInternal(). + public void resetKeyboardStateToAlphabet(final int currentAutoCapsState, + final int currentRecapitalizeState) { + mState.onResetKeyboardStateToAlphabet(currentAutoCapsState, currentRecapitalizeState); + } + + public void onPressKey(final int code, final boolean isSinglePointer, + final int currentAutoCapsState, final int currentRecapitalizeState) { + mState.onPressKey(code, isSinglePointer, currentAutoCapsState, currentRecapitalizeState); + } + + public void onReleaseKey(final int code, final boolean withSliding, + final int currentAutoCapsState, final int currentRecapitalizeState) { + mState.onReleaseKey(code, withSliding, currentAutoCapsState, currentRecapitalizeState); + } + + public void onFinishSlidingInput(final int currentAutoCapsState, + final int currentRecapitalizeState) { + mState.onFinishSlidingInput(currentAutoCapsState, currentRecapitalizeState); + } + + // Implements {@link KeyboardState.SwitchActions}. + @Override + public void setAlphabetKeyboard() { + if (DEBUG_ACTION) { + Log.d(TAG, "setAlphabetKeyboard"); + } + setKeyboard(KeyboardId.ELEMENT_ALPHABET, KeyboardSwitchState.OTHER); + } + + // Implements {@link KeyboardState.SwitchActions}. + @Override + public void setAlphabetManualShiftedKeyboard() { + if (DEBUG_ACTION) { + Log.d(TAG, "setAlphabetManualShiftedKeyboard"); + } + setKeyboard(KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED, KeyboardSwitchState.OTHER); + } + + // Implements {@link KeyboardState.SwitchActions}. + @Override + public void setAlphabetAutomaticShiftedKeyboard() { + if (DEBUG_ACTION) { + Log.d(TAG, "setAlphabetAutomaticShiftedKeyboard"); + } + setKeyboard(KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED, KeyboardSwitchState.OTHER); + } + + // Implements {@link KeyboardState.SwitchActions}. + @Override + public void setAlphabetShiftLockedKeyboard() { + if (DEBUG_ACTION) { + Log.d(TAG, "setAlphabetShiftLockedKeyboard"); + } + setKeyboard(KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED, KeyboardSwitchState.OTHER); + } + + // Implements {@link KeyboardState.SwitchActions}. + @Override + public void setAlphabetShiftLockShiftedKeyboard() { + if (DEBUG_ACTION) { + Log.d(TAG, "setAlphabetShiftLockShiftedKeyboard"); + } + setKeyboard(KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED, KeyboardSwitchState.OTHER); + } + + // Implements {@link KeyboardState.SwitchActions}. + @Override + public void setSymbolsKeyboard() { + if (DEBUG_ACTION) { + Log.d(TAG, "setSymbolsKeyboard"); + } + setKeyboard(KeyboardId.ELEMENT_SYMBOLS, KeyboardSwitchState.OTHER); + } + + // Implements {@link KeyboardState.SwitchActions}. + @Override + public void setSymbolsShiftedKeyboard() { + if (DEBUG_ACTION) { + Log.d(TAG, "setSymbolsShiftedKeyboard"); + } + setKeyboard(KeyboardId.ELEMENT_SYMBOLS_SHIFTED, KeyboardSwitchState.SYMBOLS_SHIFTED); + } + + public boolean isImeSuppressedByHardwareKeyboard( + @Nonnull final SettingsValues settingsValues, + @Nonnull final KeyboardSwitchState toggleState) { + return settingsValues.mHasHardwareKeyboard && toggleState == KeyboardSwitchState.HIDDEN; + } + + private void setMainKeyboardFrame( + @Nonnull final SettingsValues settingsValues, + @Nonnull final KeyboardSwitchState toggleState) { + final int visibility = isImeSuppressedByHardwareKeyboard(settingsValues, toggleState) + ? View.GONE : View.VISIBLE; + mKeyboardView.setVisibility(visibility); + // The visibility of {@link #mKeyboardView} must be aligned with {@link #MainKeyboardFrame}. + // @see #getVisibleKeyboardView() and + // @see LatinIME#onComputeInset(android.inputmethodservice.InputMethodService.Insets) + mMainKeyboardFrame.setVisibility(visibility); + mEmojiPalettesView.setVisibility(View.GONE); + mEmojiPalettesView.stopEmojiPalettes(); + } + + // Implements {@link KeyboardState.SwitchActions}. + @Override + public void setEmojiKeyboard() { + if (DEBUG_ACTION) { + Log.d(TAG, "setEmojiKeyboard"); + } + final Keyboard keyboard = mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET); + mMainKeyboardFrame.setVisibility(View.GONE); + // The visibility of {@link #mKeyboardView} must be aligned with {@link #MainKeyboardFrame}. + // @see #getVisibleKeyboardView() and + // @see LatinIME#onComputeInset(android.inputmethodservice.InputMethodService.Insets) + mKeyboardView.setVisibility(View.GONE); + mEmojiPalettesView.startEmojiPalettes( + mKeyboardTextsSet.getText(KeyboardTextsSet.SWITCH_TO_ALPHA_KEY_LABEL), + mKeyboardView.getKeyVisualAttribute(), keyboard.mIconsSet); + mEmojiPalettesView.setVisibility(View.VISIBLE); + } + + public enum KeyboardSwitchState { + HIDDEN(-1), + SYMBOLS_SHIFTED(KeyboardId.ELEMENT_SYMBOLS_SHIFTED), + EMOJI(KeyboardId.ELEMENT_EMOJI_RECENTS), + OTHER(-1); + + final int mKeyboardId; + + KeyboardSwitchState(int keyboardId) { + mKeyboardId = keyboardId; + } + } + + public KeyboardSwitchState getKeyboardSwitchState() { + boolean hidden = !isShowingEmojiPalettes() + && (mKeyboardLayoutSet == null + || mKeyboardView == null + || !mKeyboardView.isShown()); + KeyboardSwitchState state; + if (hidden) { + return KeyboardSwitchState.HIDDEN; + } else if (isShowingEmojiPalettes()) { + return KeyboardSwitchState.EMOJI; + } else if (isShowingKeyboardId(KeyboardId.ELEMENT_SYMBOLS_SHIFTED)) { + return KeyboardSwitchState.SYMBOLS_SHIFTED; + } + return KeyboardSwitchState.OTHER; + } + + public void onToggleKeyboard(@Nonnull final KeyboardSwitchState toggleState) { + KeyboardSwitchState currentState = getKeyboardSwitchState(); + Log.w(TAG, "onToggleKeyboard() : Current = " + currentState + " : Toggle = " + toggleState); + if (currentState == toggleState) { + mLatinIME.stopShowingInputView(); + mLatinIME.hideWindow(); + setAlphabetKeyboard(); + } else { + mLatinIME.startShowingInputView(true); + if (toggleState == KeyboardSwitchState.EMOJI) { + setEmojiKeyboard(); + } else { + mEmojiPalettesView.stopEmojiPalettes(); + mEmojiPalettesView.setVisibility(View.GONE); + + mMainKeyboardFrame.setVisibility(View.VISIBLE); + mKeyboardView.setVisibility(View.VISIBLE); + setKeyboard(toggleState.mKeyboardId, toggleState); + } + } + } + + // Future method for requesting an updating to the shift state. + @Override + public void requestUpdatingShiftState(final int autoCapsFlags, final int recapitalizeMode) { + if (DEBUG_ACTION) { + Log.d(TAG, "requestUpdatingShiftState: " + + " autoCapsFlags=" + CapsModeUtils.flagsToString(autoCapsFlags) + + " recapitalizeMode=" + RecapitalizeStatus.modeToString(recapitalizeMode)); + } + mState.onUpdateShiftState(autoCapsFlags, recapitalizeMode); + } + + // Implements {@link KeyboardState.SwitchActions}. + @Override + public void startDoubleTapShiftKeyTimer() { + if (DEBUG_TIMER_ACTION) { + Log.d(TAG, "startDoubleTapShiftKeyTimer"); + } + final MainKeyboardView keyboardView = getMainKeyboardView(); + if (keyboardView != null) { + keyboardView.startDoubleTapShiftKeyTimer(); + } + } + + // Implements {@link KeyboardState.SwitchActions}. + @Override + public void cancelDoubleTapShiftKeyTimer() { + if (DEBUG_TIMER_ACTION) { + Log.d(TAG, "setAlphabetKeyboard"); + } + final MainKeyboardView keyboardView = getMainKeyboardView(); + if (keyboardView != null) { + keyboardView.cancelDoubleTapShiftKeyTimer(); + } + } + + // Implements {@link KeyboardState.SwitchActions}. + @Override + public boolean isInDoubleTapShiftKeyTimeout() { + if (DEBUG_TIMER_ACTION) { + Log.d(TAG, "isInDoubleTapShiftKeyTimeout"); + } + final MainKeyboardView keyboardView = getMainKeyboardView(); + return keyboardView != null && keyboardView.isInDoubleTapShiftKeyTimeout(); + } + + /** + * Updates state machine to figure out when to automatically switch back to the previous mode. + */ + public void onEvent(final Event event, final int currentAutoCapsState, + final int currentRecapitalizeState) { + mState.onEvent(event, currentAutoCapsState, currentRecapitalizeState); + } + + public boolean isShowingKeyboardId(@Nonnull int... keyboardIds) { + if (mKeyboardView == null || !mKeyboardView.isShown()) { + return false; + } + int activeKeyboardId = mKeyboardView.getKeyboard().mId.mElementId; + for (int keyboardId : keyboardIds) { + if (activeKeyboardId == keyboardId) { + return true; + } + } + return false; + } + + public boolean isShowingEmojiPalettes() { + return mEmojiPalettesView != null && mEmojiPalettesView.isShown(); + } + + public boolean isShowingMoreKeysPanel() { + if (isShowingEmojiPalettes()) { + return false; + } + return mKeyboardView.isShowingMoreKeysPanel(); + } + + public View getVisibleKeyboardView() { + if (isShowingEmojiPalettes()) { + return mEmojiPalettesView; + } + return mKeyboardView; + } + + public MainKeyboardView getMainKeyboardView() { + return mKeyboardView; + } + + public void deallocateMemory() { + if (mKeyboardView != null) { + mKeyboardView.cancelAllOngoingEvents(); + mKeyboardView.deallocateMemory(); + } + if (mEmojiPalettesView != null) { + mEmojiPalettesView.stopEmojiPalettes(); + } + } + + public View onCreateInputView(final boolean isHardwareAcceleratedDrawingEnabled) { + if (mKeyboardView != null) { + mKeyboardView.closing(); + } + + updateKeyboardThemeAndContextThemeWrapper( + mLatinIME, KeyboardTheme.getKeyboardTheme(mLatinIME /* context */)); + mCurrentInputView = (InputView)LayoutInflater.from(mThemeContext).inflate( + R.layout.input_view, null); + mMainKeyboardFrame = mCurrentInputView.findViewById(R.id.main_keyboard_frame); + mEmojiPalettesView = (EmojiPalettesView)mCurrentInputView.findViewById( + R.id.emoji_palettes_view); + + mKeyboardView = (MainKeyboardView) mCurrentInputView.findViewById(R.id.keyboard_view); + mKeyboardView.setHardwareAcceleratedDrawingEnabled(isHardwareAcceleratedDrawingEnabled); + mKeyboardView.setKeyboardActionListener(mLatinIME); + mEmojiPalettesView.setHardwareAcceleratedDrawingEnabled( + isHardwareAcceleratedDrawingEnabled); + mEmojiPalettesView.setKeyboardActionListener(mLatinIME); + return mCurrentInputView; + } + + public int getKeyboardShiftMode() { + final Keyboard keyboard = getKeyboard(); + if (keyboard == null) { + return WordComposer.CAPS_MODE_OFF; + } + switch (keyboard.mId.mElementId) { + case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED: + case KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED: + return WordComposer.CAPS_MODE_MANUAL_SHIFT_LOCKED; + case KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED: + return WordComposer.CAPS_MODE_MANUAL_SHIFTED; + case KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED: + return WordComposer.CAPS_MODE_AUTO_SHIFTED; + default: + return WordComposer.CAPS_MODE_OFF; + } + } + + public int getCurrentKeyboardScriptId() { + if (null == mKeyboardLayoutSet) { + return ScriptUtils.SCRIPT_UNKNOWN; + } + return mKeyboardLayoutSet.getScriptId(); + } +} diff --git a/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardTheme.java b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardTheme.java new file mode 100644 index 00000000..8d58acc8 --- /dev/null +++ b/app/src/main/java/org/dslul/openboard/inputmethod/keyboard/KeyboardTheme.java @@ -0,0 +1,223 @@ +/* + * 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.keyboard; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.preference.PreferenceManager; +import android.util.Log; + +import org.dslul.openboard.inputmethod.compat.BuildCompatUtils; +import org.dslul.openboard.inputmethod.latin.R; + +import java.util.ArrayList; +import java.util.Arrays; + +public final class KeyboardTheme implements Comparable { + private static final String TAG = KeyboardTheme.class.getSimpleName(); + + static final String KLP_KEYBOARD_THEME_KEY = "pref_keyboard_layout_20110916"; + static final String LXX_KEYBOARD_THEME_KEY = "pref_keyboard_theme_20140509"; + + // These should be aligned with Keyboard.themeId and Keyboard.Case.keyboardTheme + // attributes' values in attrs.xml. + public static final int THEME_ID_ICS = 0; + public static final int THEME_ID_KLP = 2; + public static final int THEME_ID_LXX_LIGHT = 3; + public static final int THEME_ID_LXX_DARK = 4; + public static final int THEME_ID_LIGHT_BORDER = 5; + public static final int THEME_ID_DARK_BORDER = 6; + public static final int DEFAULT_THEME_ID = THEME_ID_LIGHT_BORDER; + + private static KeyboardTheme[] AVAILABLE_KEYBOARD_THEMES; + + /* package private for testing */ + static final KeyboardTheme[] KEYBOARD_THEMES = { + new KeyboardTheme(THEME_ID_ICS, "ICS", R.style.KeyboardTheme_ICS, + // This has never been selected because we support ICS or later. + VERSION_CODES.BASE), + new KeyboardTheme(THEME_ID_KLP, "KLP", R.style.KeyboardTheme_KLP, + // Default theme for ICS, JB, and KLP. + VERSION_CODES.ICE_CREAM_SANDWICH), + new KeyboardTheme(THEME_ID_LXX_LIGHT, "LXXLight", R.style.KeyboardTheme_LXX_Light, + // Default theme for LXX. + VERSION_CODES.BASE), + new KeyboardTheme(THEME_ID_LXX_DARK, "LXXDark", R.style.KeyboardTheme_LXX_Dark, + // This has never been selected as default theme. + VERSION_CODES.BASE), + new KeyboardTheme(THEME_ID_LIGHT_BORDER, "LXXLightBorder", R.style.KeyboardTheme_LXX_Light_Border, + // This has never been selected as default theme. + Build.VERSION_CODES.LOLLIPOP), + new KeyboardTheme(THEME_ID_DARK_BORDER, "LXXDarkBorder", R.style.KeyboardTheme_LXX_Dark_Border, + // This has never been selected as default theme. + VERSION_CODES.BASE), + }; + + static { + // Sort {@link #KEYBOARD_THEME} by descending order of {@link #mMinApiVersion}. + Arrays.sort(KEYBOARD_THEMES); + } + + public final int mThemeId; + public final int mStyleId; + public final String mThemeName; + public final int mMinApiVersion; + + // Note: The themeId should be aligned with "themeId" attribute of Keyboard style + // in values/themes- + + diff --git a/app/src/main/res/values-v18/emoji-categories.xml b/app/src/main/res/values-v18/emoji-categories.xml new file mode 100644 index 00000000..2ea08159 --- /dev/null +++ b/app/src/main/res/values-v18/emoji-categories.xml @@ -0,0 +1,909 @@ + + + + + + + + 1f436 + 1f429 + + 1f431 + + + 1f42d + 1f439 + 1f422 + + 1f430 + + 1f414 + 1f423 + 1f424 + 1f425 + 1f426 + + 1f411 + + 1f43a + + + + 1f42e + 1f434 + 1f417 + + 1f437 + 1f43d + 1f438 + 1f40d + 1f43c + 1f427 + 1f418 + 1f428 + 1f412 + 1f435 + + 1f42f + 1f43b + 1f42b + + + 1f433 + + 1f41f + 1f420 + 1f421 + 1f419 + 1f41a + 1f42c + 1f40c + 1f41b + 1f41c + 1f41d + 1f41e + 1f432 + + 1f43e + 1f378 + 1f37a + 1f37b + 1f377 + 1f379 + 1f376 + + 1f375 + + 1f374 + 1f368 + 1f367 + 1f366 + 1f369 + 1f370 + 1f36a + 1f36b + 1f36c + 1f36d + 1f36e + 1f36f + 1f373 + 1f354 + 1f35f + 1f35d + 1f355 + 1f356 + 1f357 + 1f364 + 1f363 + 1f371 + 1f35e + 1f35c + 1f359 + 1f35a + 1f35b + 1f372 + 1f365 + 1f362 + 1f361 + 1f358 + 1f360 + 1f34c + 1f34e + 1f34f + 1f34a + + 1f344 + 1f345 + 1f346 + 1f347 + 1f348 + 1f349 + + 1f351 + 1f352 + 1f353 + 1f34d + 1f330 + 1f331 + + + 1f334 + 1f335 + 1f337 + 1f338 + 1f339 + 1f340 + 1f341 + 1f342 + 1f343 + 1f33a + 1f33b + 1f33c + 1f33d + 1f33e + 1f33f + 2600 + 1f308 + 26c5 + 2601 + 1f301 + 1f302 + + 1f4a7 + 26a1 + 1f300 + 2744 + 26c4 + 1f319 + + + + 1f31b + + 1f311 + + 1f313 + 1f314 + 1f315 + + + + 1f391 + 1f304 + 1f305 + 1f307 + 1f306 + 1f303 + 1f30c + 1f309 + 1f30a + 1f30b + + 1f30f + + + + + + + + + + + + + + + + + 1f51d + 1f519 + 1f51b + 1f51c + 1f51a + 23f3 + 231b + 23f0 + 2648 + 2649 + 264a + 264b + 264c + 264d + 264e + 264f + 2650 + 2651 + 2652 + 2653 + 26ce + 1f531 + 1f52f + 1f6bb + + + + + 1f170 + 1f171 + 1f18e + 1f17e + 1f4ae + 1f4af + 1f520 + 1f521 + 1f522 + 1f523 + 1f524 + 27bf + 1f4f6 + 1f4f3 + 1f4f4 + + 1f6b9 + 1f6ba + 1f6bc + 267f + 267b + 1f6ad + 1f6a9 + 26a0 + 1f201 + 1f51e + 26d4 + 1f192 + 1f197 + 1f195 + 1f198 + 1f199 + 1f193 + 1f196 + 1f19a + 1f232 + 1f233 + 1f234 + 1f235 + 1f236 + 1f237 + 1f238 + 1f239 + 1f202 + 1f23a + 1f250 + 1f251 + 3299 + 00ae + 00a9 + 2122 + 1f21a + 1f22f + 3297 + 2b55 + 274c + 274e + 2139 + 1f6ab + 2705 + 2714 + 1f517 + 2734 + 2733 + 2795 + 2796 + 2716 + 2797 + 1f4a0 + 1f4a1 + 1f4a4 + 1f4a2 + 1f525 + 1f4a5 + 1f4a8 + 1f4a6 + 1f4ab + 1f55b + + 1f550 + + 1f551 + + 1f552 + + 1f553 + + 1f554 + + 1f555 + + 1f556 + + 1f557 + + 1f558 + + 1f559 + + 1f55a + + 2195 + 2b06 + 2197 + 27a1 + 2198 + 2b07 + 2199 + 2b05 + 2196 + 2194 + 2934 + 2935 + 23ea + 23eb + 23ec + 23e9 + 25c0 + 25b6 + 1f53d + 1f53c + 2747 + 2728 + 1f534 + 1f535 + 26aa + 26ab + 1f533 + 1f532 + 2b50 + 1f31f + 1f320 + 25ab + 25aa + 25fd + 25fe + 25fb + 25fc + 2b1c + 2b1b + 1f538 + 1f539 + 1f536 + 1f537 + 1f53a + 1f53b + 2754 + 2753 + 2755 + 2757 + 203c + 2049 + 3030 + 27b0 + 2660 + 2665 + 2663 + 2666 + 1f194 + 1f511 + 21a9 + 1f191 + 1f50d + 1f512 + 1f513 + 21aa + 1f510 + + 1f518 + 1f50e + 1f516 + 1f50f + 1f503 + + + + + 1f4e7 + + + + + + 1f50a + + + 263a + 1f60a + + 1f601 + 1f602 + 1f603 + 1f604 + 1f605 + 1f606 + + + 1f609 + + + + + 1f620 + + 1f621 + 1f622 + + + 1f623 + 1f624 + 1f625 + + + 1f628 + 1f629 + 1f630 + + 1f631 + 1f632 + 1f633 + 1f635 + + 1f637 + 1f61e + 1f612 + 1f60d + + 1f61c + 1f61d + 1f60b + + + 1f618 + 1f61a + + 1f62d + 1f60c + 1f616 + 1f614 + 1f62a + 1f60f + 1f613 + 1f62b + 1f64b + 1f64c + 1f64d + 1f645 + 1f646 + 1f647 + 1f64e + 1f64f + 1f63a + 1f63c + 1f638 + 1f639 + 1f63b + 1f63d + 1f63f + 1f63e + 1f640 + 1f648 + 1f649 + 1f64a + 1f4a9 + 1f476 + 1f466 + 1f467 + 1f468 + 1f469 + 1f474 + 1f475 + 1f48f + 1f491 + 1f46a + 1f46b + + + 1f464 + + 1f46e + 1f477 + 1f481 + 1f482 + 1f46f + 1f470 + 1f478 + 1f385 + 1f47c + + + + 1f483 + 1f486 + 1f487 + 1f485 + 1f47b + 1f479 + 1f47a + 1f47d + 1f47e + 1f47f + 1f480 + 1f4aa + 1f440 + 1f442 + 1f443 + 1f463 + 1f444 + 1f445 + 1f48b + 2764 + 1f499 + 1f49a + 1f49b + 1f49c + 1f493 + 1f494 + 1f495 + 1f496 + 1f497 + 1f498 + 1f49d + 1f49e + 1f49f + 1f44d + 1f44e + 1f44c + 270a + 270c + 270b + 1f44a + + 1f446 + 1f447 + 1f448 + 1f449 + 1f44b + 1f44f + + + + 1f530 + 1f484 + 1f45e + 1f45f + 1f451 + 1f452 + 1f3a9 + 1f393 + 1f453 + 231a + 1f454 + 1f455 + 1f456 + 1f457 + 1f458 + 1f459 + 1f460 + 1f461 + 1f462 + 1f45a + 1f45c + 1f4bc + 1f392 + 1f45d + 1f45b + 1f4b0 + 1f4b3 + 1f4b2 + 1f4b5 + 1f4b4 + + + 1f4b8 + 1f4b1 + 1f4b9 + 1f52b + 1f52a + 1f4a3 + 1f489 + 1f48a + 1f6ac + 1f514 + + 1f6aa + + + 1f52e + 1f526 + 1f50b + 1f50c + 1f4dc + 1f4d7 + 1f4d8 + 1f4d9 + 1f4da + 1f4d4 + 1f4d2 + 1f4d1 + 1f4d3 + 1f4d5 + 1f4d6 + 1f4f0 + 1f4db + 1f383 + 1f384 + 1f380 + 1f381 + 1f382 + 1f388 + 1f386 + 1f387 + 1f389 + 1f38a + 1f38d + 1f38f + 1f38c + 1f390 + 1f38b + 1f38e + 1f4f1 + 1f4f2 + 1f4df + 260e + 1f4de + 1f4e0 + 1f4e6 + 2709 + 1f4e8 + 1f4e9 + 1f4ea + 1f4eb + + + 1f4ee + 1f4e4 + 1f4e5 + + 1f4e2 + 1f4e3 + 1f4e1 + 1f4ac + + 2712 + 270f + 1f4dd + 1f4cf + 1f4d0 + 1f4cd + 1f4cc + 1f4ce + 2702 + 1f4ba + 1f4bb + 1f4bd + 1f4be + 1f4bf + 1f4c6 + 1f4c5 + 1f4c7 + 1f4cb + 1f4c1 + 1f4c2 + 1f4c3 + 1f4c4 + 1f4ca + 1f4c8 + 1f4c9 + 26fa + 1f3a1 + 1f3a2 + 1f3a0 + 1f3aa + 1f3a8 + 1f3ac + 1f3a5 + 1f4f7 + 1f4f9 + 1f3a6 + 1f3ad + 1f3ab + 1f3ae + 1f3b2 + 1f3b0 + 1f0cf + 1f3b4 + 1f004 + 1f3af + 1f4fa + 1f4fb + 1f4c0 + 1f4fc + 1f3a7 + 1f3a4 + 1f3b5 + 1f3b6 + 1f3bc + 1f3bb + 1f3b9 + 1f3b7 + 1f3ba + 1f3b8 + 303d + + + 1f3e0 + 1f3e1 + 1f3e2 + 1f3e3 + + 1f3e5 + 1f3e6 + 1f3e7 + 1f3e8 + 1f3e9 + 1f3ea + 1f3eb + 26ea + 26f2 + 1f3ec + 1f3ef + 1f3f0 + 1f3ed + 1f5fb + 1f5fc + 1f5fd + 1f5fe + 1f5ff + 2693 + 1f3ee + 1f488 + 1f527 + 1f528 + 1f529 + + + 1f6c0 + 1f6bd + 1f6be + 1f3bd + 1f3a3 + 1f3b1 + 1f3b3 + 26be + 26f3 + 1f3be + 26bd + 1f3bf + 1f3c0 + 1f3c1 + 1f3c2 + 1f3c3 + 1f3c4 + 1f3c6 + + 1f40e + 1f3c8 + + 1f3ca + + 1f683 + 1f684 + 1f685 + + 1f687 + 24c2 + + + + 1f68c + + + 1f68f + + 1f691 + 1f692 + 1f693 + + 1f695 + + 1f697 + + 1f699 + + + + + + + + + 1f6a2 + + + 2708 + + + + + 26f5 + 1f6b2 + + + + + + 1f689 + 1f680 + 1f6a4 + 1f6b6 + 26fd + 1f17f + 1f6a5 + + 1f6a7 + 1f6a8 + 2668 + 1f48c + 1f48d + 1f48e + 1f490 + 1f492 + fe4e5|1f1ef,1f1f5 + fe4e6|1f1fa,1f1f8 + fe4e7|1f1eb,1f1f7 + fe4e8|1f1e9,1f1ea + fe4e9|1f1ee,1f1f9 + fe4ea|1f1ec,1f1e7 + fe4eb|1f1ea,1f1f8 + fe4ec|1f1f7,1f1fa + fe4ed|1f1e8,1f1f3 + fe4ee|1f1f0,1f1f7 + + + =-O + :-P + ;-) + :-( + :-) + :-! + :-$ + B-) + :O + :-* + :-D + :\'( + :-\\ + O:-) + :-[ + + diff --git a/app/src/main/res/values-v19/emoji-categories.xml b/app/src/main/res/values-v19/emoji-categories.xml new file mode 100644 index 00000000..50063ea5 --- /dev/null +++ b/app/src/main/res/values-v19/emoji-categories.xml @@ -0,0 +1,5445 @@ + + + + + + + 1f415 + 1f436 + 1f429 + 1f408 + 1f431 + 1f400 + 1f401 + 1f42d + 1f439 + 1f422 + 1f407 + 1f430 + 1f413 + 1f414 + 1f423 + 1f424 + 1f425 + 1f426 + 1f40f + 1f411 + 1f410 + 1f43a + 1f403 + 1f402 + 1f404 + 1f42e + 1f434 + 1f417 + 1f416 + 1f437 + 1f43d + 1f438 + 1f40d + 1f43c + 1f427 + 1f418 + 1f428 + 1f412 + 1f435 + 1f406 + 1f42f + 1f43b + 1f42b + 1f42a + 1f40a + 1f433 + 1f40b + 1f41f + 1f420 + 1f421 + 1f419 + 1f41a + 1f42c + 1f40c + 1f41b + 1f41c + 1f41d + 1f41e + 1f432 + 1f409 + 1f43e + 1f378 + 1f37a + 1f37b + 1f377 + 1f379 + 1f376 + 2615 + 1f375 + 1f37c + 1f374 + 1f368 + 1f367 + 1f366 + 1f369 + 1f370 + 1f36a + 1f36b + 1f36c + 1f36d + 1f36e + 1f36f + 1f373 + 1f354 + 1f35f + 1f35d + 1f355 + 1f356 + 1f357 + 1f364 + 1f363 + 1f371 + 1f35e + 1f35c + 1f359 + 1f35a + 1f35b + 1f372 + 1f365 + 1f362 + 1f361 + 1f358 + 1f360 + 1f34c + 1f34e + 1f34f + 1f34a + 1f34b + 1f344 + 1f345 + 1f346 + 1f347 + 1f348 + 1f349 + 1f350 + 1f351 + 1f352 + 1f353 + 1f34d + 1f330 + 1f331 + 1f332 + 1f333 + 1f334 + 1f335 + 1f337 + 1f338 + 1f339 + 1f340 + 1f341 + 1f342 + 1f343 + 1f33a + 1f33b + 1f33c + 1f33d + 1f33e + 1f33f + 2600 + 1f308 + 26c5 + 2601 + 1f301 + 1f302 + 2614 + 1f4a7 + 26a1 + 1f300 + 2744 + 26c4 + 1f319 + 1f31e + 1f31d + 1f31a + 1f31b + 1f31c + 1f311 + 1f312 + 1f313 + 1f314 + 1f315 + 1f316 + 1f317 + 1f318 + 1f391 + 1f304 + 1f305 + 1f307 + 1f306 + 1f303 + 1f30c + 1f309 + 1f30a + 1f30b + 1f30e + 1f30f + 1f30d + 1f310 + + + fe82e|0031,20e3|99 + fe82f|0032,20e3|99 + fe830|0033,20e3|99 + fe831|0034,20e3|99 + fe832|0035,20e3|99 + fe833|0036,20e3|99 + fe834|0037,20e3|99 + fe835|0038,20e3|99 + fe836|0039,20e3|99 + fe837|0030,20e3|99 + 1f51f||99 + fe82c|0023,20e3|99 + 1f51d + 1f519 + 1f51b + 1f51c + 1f51a + 23f3 + 231b + 23f0 + 2648 + 2649 + 264a + 264b + 264c + 264d + 264e + 264f + 2650 + 2651 + 2652 + 2653 + 26ce + 1f531 + 1f52f + 1f6bb + 1f6ae + 1f6af + 1f6b0 + 1f6b1 + 1f170 + 1f171 + 1f18e + 1f17e + 1f4ae + 1f4af + 1f520 + 1f521 + 1f522 + 1f523 + 1f524 + 27bf + 1f4f6 + 1f4f3 + 1f4f4 + 1f4f5 + 1f6b9 + 1f6ba + 1f6bc + 267f + 267b + 1f6ad + 1f6a9 + 26a0 + 1f201 + 1f51e + 26d4 + 1f192 + 1f197 + 1f195 + 1f198 + 1f199 + 1f193 + 1f196 + 1f19a + 1f232 + 1f233 + 1f234 + 1f235 + 1f236 + 1f237 + 1f238 + 1f239 + 1f202 + 1f23a + 1f250 + 1f251 + 3299 + 00ae + 00a9 + 2122 + 1f21a + 1f22f + 3297 + 2b55 + 274c + 274e + 2139 + 1f6ab + 2705 + 2714 + 1f517 + 2734 + 2733 + 2795 + 2796 + 2716 + 2797 + 1f4a0 + 1f4a1 + 1f4a4 + 1f4a2 + 1f525 + 1f4a5 + 1f4a8 + 1f4a6 + 1f4ab + 1f55b + 1f567 + 1f550 + 1f55c + 1f551 + 1f55d + 1f552 + 1f55e + 1f553 + 1f55f + 1f554 + 1f560 + 1f555 + 1f561 + 1f556 + 1f562 + 1f557 + 1f563 + 1f558 + 1f564 + 1f559 + 1f565 + 1f55a + 1f566 + 2195 + 2b06 + 2197 + 27a1 + 2198 + 2b07 + 2199 + 2b05 + 2196 + 2194 + 2934 + 2935 + 23ea + 23eb + 23ec + 23e9 + 25c0 + 25b6 + 1f53d + 1f53c + 2747 + 2728 + 1f534 + 1f535 + 26aa + 26ab + 1f533 + 1f532 + 2b50 + 1f31f + 1f320 + 25ab + 25aa + 25fd + 25fe + 25fb + 25fc + 2b1c + 2b1b + 1f538 + 1f539 + 1f536 + 1f537 + 1f53a + 1f53b + 2754 + 2753 + 2755 + 2757 + 203c + 2049 + 3030 + 27b0 + 2660 + 2665 + 2663 + 2666 + 1f194 + 1f511 + 21a9 + 1f191 + 1f50d + 1f512 + 1f513 + 21aa + 1f510 + 2611 + 1f518 + 1f50e + 1f516 + 1f50f + 1f503 + 1f500 + 1f501 + 1f502 + 1f504 + 1f4e7 + 1f505 + 1f506 + 1f507 + 1f508 + 1f509 + 1f50a + 1f5a4 + 1f6d1 + + + 263a + 1f60a + 1f600 + 1f601 + 1f602 + 1f603 + 1f604 + 1f605 + 1f606 + 1f607 + 1f608 + 1f609 + 1f62f + 1f610 + 1f611 + 1f615 + 1f620 + 1f62c + 1f621 + 1f622 + 1f634 + 1f62e + 1f623 + 1f624 + 1f625 + 1f626 + 1f627 + 1f628 + 1f629 + 1f630 + 1f61f + 1f631 + 1f632 + 1f633 + 1f635 + 1f636 + 1f637 + 1f61e + 1f612 + 1f60d + 1f61b + 1f61c + 1f61d + 1f60b + 1f617 + 1f619 + 1f618 + 1f61a + 1f60e + 1f62d + 1f60c + 1f616 + 1f614 + 1f62a + 1f60f + 1f613 + 1f62b + 1f64b + 1f64c + 1f64d + 1f645 + 1f646 + 1f647 + 1f64e + 1f64f + 1f63a + 1f63c + 1f638 + 1f639 + 1f63b + 1f63d + 1f63f + 1f63e + 1f640 + 1f648 + 1f649 + 1f64a + 1f4a9 + 1f476 + 1f466 + 1f467 + 1f468 + 1f469 + 1f474 + 1f475 + 1f48f + 1f491 + 1f46a + 1f46b + 1f46c + 1f46d + 1f464 + 1f465 + 1f46e + 1f477 + 1f481 + 1f482 + 1f46f + 1f470 + 1f478 + 1f385 + 1f47c + 1f471 + 1f472 + 1f473 + 1f483 + 1f486 + 1f487 + 1f485 + 1f47b + 1f479 + 1f47a + 1f47d + 1f47e + 1f47f + 1f480 + 1f4aa + 1f440 + 1f442 + 1f443 + 1f463 + 1f444 + 1f445 + 1f48b + 2764 + 1f499 + 1f49a + 1f49b + 1f49c + 1f493 + + + + 1f923 + 1f924 + + 1f926 + + 1f930 + 1f933 + 1f934 + 1f935 + 1f936 + 1f937 + 1f494 + 1f495 + 1f496 + 1f497 + 1f498 + 1f49d + 1f49e + 1f49f + 1f44d + 1f44e + 1f44c + 270a + 270c + 270b + 1f44a + 261d + 1f446 + 1f447 + 1f448 + 1f449 + 1f44b + 1f44f + 1f450 + 1f919 + 1f91a + 1f91b + 1f91c + 1f91d + 1f91e + + + 1f530 + 1f484 + 1f45e + 1f45f + 1f451 + 1f452 + 1f3a9 + 1f393 + 1f453 + 231a + 1f454 + 1f455 + 1f456 + 1f457 + 1f458 + 1f459 + 1f460 + 1f461 + 1f462 + 1f45a + 1f45c + 1f4bc + 1f392 + 1f45d + 1f45b + 1f4b0 + 1f4b3 + 1f4b2 + 1f4b5 + 1f4b4 + 1f4b6 + 1f4b7 + 1f4b8 + 1f4b1 + 1f4b9 + 1f52b + 1f52a + 1f4a3 + 1f489 + 1f48a + 1f6ac + 1f514 + 1f515 + 1f6aa + 1f52c + 1f52d + 1f52e + 1f526 + 1f50b + 1f50c + 1f4dc + 1f4d7 + 1f4d8 + 1f4d9 + 1f4da + 1f4d4 + 1f4d2 + 1f4d1 + 1f4d3 + 1f4d5 + 1f4d6 + 1f4f0 + 1f4db + 1f383 + 1f384 + 1f380 + 1f381 + 1f382 + 1f388 + 1f386 + 1f387 + 1f389 + 1f38a + 1f38d + 1f38f + 1f38c + 1f390 + 1f38b + 1f38e + 1f4f1 + 1f4f2 + 1f4df + 260e + 1f4de + 1f4e0 + 1f4e6 + 2709 + 1f4e8 + 1f4e9 + 1f4ea + 1f4eb + 1f4ed + 1f4ec + 1f4ee + 1f4e4 + 1f4e5 + 1f4ef + 1f4e2 + 1f4e3 + 1f4e1 + 1f4ac + 1f4ad + 2712 + 270f + 1f4dd + 1f4cf + 1f4d0 + 1f4cd + 1f4cc + 1f4ce + 2702 + 1f4ba + 1f4bb + 1f4bd + 1f4be + 1f4bf + 1f4c6 + 1f4c5 + 1f4c7 + 1f4cb + 1f4c1 + 1f4c2 + 1f4c3 + 1f4c4 + 1f4ca + 1f4c8 + 1f4c9 + 26fa + 1f3a1 + 1f3a2 + 1f3a0 + 1f3aa + 1f3a8 + 1f3ac + 1f3a5 + 1f4f7 + 1f4f9 + 1f3a6 + 1f3ad + 1f3ab + 1f3ae + 1f3b2 + 1f3b0 + 1f0cf + 1f3b4 + 1f004 + 1f3af + 1f4fa + 1f4fb + 1f4c0 + 1f4fc + 1f3a7 + 1f3a4 + 1f3b5 + 1f3b6 + 1f3bc + 1f3bb + 1f3b9 + 1f3b7 + 1f3ba + 1f3b8 + 303d + 1f940 + 1f941 + 1f942 + 1f943 + 1f944 + 1f945 + 1f946 + 1f947 + 1f948 + 1f949 + 1f94a + 1f94b + + + 1f3e0 + 1f3e1 + 1f3e2 + 1f3e3 + 1f3e4 + 1f3e5 + 1f3e6 + 1f3e7 + 1f3e8 + 1f3e9 + 1f3ea + 1f3eb + 26ea + 26f2 + 1f3ec + 1f3ef + 1f3f0 + 1f3ed + 1f5fb + 1f5fc + 1f5fd + 1f5fe + 1f5ff + 2693 + 1f3ee + 1f488 + 1f527 + 1f528 + 1f529 + 1f6bf + 1f6c1 + 1f6c0 + 1f6bd + 1f6be + 1f3bd + 1f3a3 + 1f3b1 + 1f3b3 + 26be + 26f3 + 1f3be + 26bd + 1f3bf + 1f3c0 + 1f3c1 + 1f3c2 + 1f3c3 + 1f3c4 + 1f3c6 + 1f3c7 + 1f40e + 1f3c8 + 1f3c9 + 1f3ca + 1f682 + 1f683 + 1f684 + 1f685 + 1f686 + 1f687 + 24c2 + 1f688 + 1f68a + 1f68b + 1f68c + 1f68d + 1f68e + 1f68f + 1f690 + 1f691 + 1f692 + 1f693 + 1f694 + 1f695 + 1f696 + 1f697 + 1f698 + 1f699 + 1f69a + 1f69b + 1f69c + 1f69d + 1f69e + 1f69f + 1f6a0 + 1f6a1 + 1f6a2 + 1f6a3 + 1f681 + 2708 + 1f6c2 + 1f6c3 + 1f6c4 + 1f6c5 + 26f5 + 1f6b2 + 1f6b3 + 1f6b4 + 1f6b5 + 1f6b7 + 1f6b8 + 1f689 + 1f680 + 1f6a4 + 1f6b6 + 26fd + 1f17f + 1f6a5 + 1f6a6 + 1f6a7 + 1f6a8 + 2668 + 1f48c + 1f48d + 1f48e + 1f490 + 1f492 + fe4e5|1f1ef,1f1f5|99 + fe4e6|1f1fa,1f1f8|99 + fe4e7|1f1eb,1f1f7|99 + fe4e8|1f1e9,1f1ea|99 + fe4e9|1f1ee,1f1f9|99 + fe4ea|1f1ec,1f1e7|99 + fe4eb|1f1ea,1f1f8|99 + fe4ec|1f1f7,1f1fa|99 + fe4ed|1f1e8,1f1f3|99 + fe4ee|1f1f0,1f1f7|99 + + + 1f1e6,1f1e9 + 1f1e6,1f1ea + 1f1e6,1f1eb + 1f1e6,1f1ec + 1f1e6,1f1ee + 1f1e6,1f1f1 + 1f1e6,1f1f2 + 1f1e6,1f1f4 + 1f1e6,1f1f7 + 1f1e6,1f1f8 + 1f1e6,1f1f9 + 1f1e6,1f1fa + 1f1e6,1f1fc + 1f1e6,1f1fd + 1f1e6,1f1ff + 1f1e7,1f1e6 + 1f1e7,1f1e7 + 1f1e7,1f1e9 + 1f1e7,1f1ea + 1f1e7,1f1eb + 1f1e7,1f1ec + 1f1e7,1f1ed + 1f1e7,1f1ee + 1f1e7,1f1ef + 1f1e7,1f1f2 + 1f1e7,1f1f3 + 1f1e7,1f1f4 + 1f1e7,1f1f7 + 1f1e7,1f1f8 + 1f1e7,1f1f9 + 1f1e7,1f1fc + 1f1e7,1f1fe + 1f1e7,1f1ff + 1f1e8,1f1e6 + 1f1e8,1f1e8 + 1f1e8,1f1e9 + 1f1e8,1f1eb + 1f1e8,1f1ec + 1f1e8,1f1ed + 1f1e8,1f1ee + 1f1e8,1f1f0 + 1f1e8,1f1f1 + 1f1e8,1f1f2 + 1f1e8,1f1f3 + 1f1e8,1f1f4 + 1f1e8,1f1f7 + 1f1e8,1f1fa + 1f1e8,1f1fb + 1f1e8,1f1fc + 1f1e8,1f1fd + 1f1e8,1f1fe + 1f1e8,1f1ff + 1f1e9,1f1ea + 1f1e9,1f1ef + 1f1e9,1f1f0 + 1f1e9,1f1f2 + 1f1e9,1f1f4 + 1f1e9,1f1ff + 1f1ea,1f1e8 + 1f1ea,1f1ea + 1f1ea,1f1ec + 1f1ea,1f1f7 + 1f1ea,1f1f8 + 1f1ea,1f1f9 + 1f1ea,1f1fa + 1f1eb,1f1ee + 1f1eb,1f1ef + 1f1eb,1f1f2 + 1f1eb,1f1f4 + 1f1eb,1f1f7 + 1f1ec,1f1e6 + 1f1ec,1f1e7 + 1f1ec,1f1e9 + 1f1ec,1f1ea + 1f1ec,1f1ec + 1f1ec,1f1ed + 1f1ec,1f1ee + 1f1ec,1f1f1 + 1f1ec,1f1f2 + 1f1ec,1f1f3 + 1f1ec,1f1f6 + 1f1ec,1f1f7 + 1f1ec,1f1f9 + 1f1ec,1f1fa + 1f1ec,1f1fc + 1f1ec,1f1fe + 1f1ed,1f1f0 + 1f1ed,1f1f3 + 1f1ed,1f1f7 + 1f1ed,1f1f9 + 1f1ed,1f1fa + 1f1ee,1f1e9 + 1f1ee,1f1ea + 1f1ee,1f1f1 + 1f1ee,1f1f2 + 1f1ee,1f1f3 + 1f1ee,1f1f4 + 1f1ee,1f1f6 + 1f1ee,1f1f7 + 1f1ee,1f1f8 + 1f1ee,1f1f9 + 1f1ef,1f1ea + 1f1ef,1f1f2 + 1f1ef,1f1f4 + 1f1ef,1f1f5 + 1f1f0,1f1ea + 1f1f0,1f1ec + 1f1f0,1f1ed + 1f1f0,1f1ee + 1f1f0,1f1f2 + 1f1f0,1f1f3 + 1f1f0,1f1f5 + 1f1f0,1f1f7 + 1f1f0,1f1fc + 1f1f0,1f1fe + 1f1f0,1f1ff + 1f1f1,1f1e6 + 1f1f1,1f1e7 + 1f1f1,1f1e8 + 1f1f1,1f1ee + 1f1f1,1f1f0 + 1f1f1,1f1f7 + 1f1f1,1f1f8 + 1f1f1,1f1f9 + 1f1f1,1f1fa + 1f1f1,1f1fb + 1f1f1,1f1fe + 1f1f2,1f1e6 + 1f1f2,1f1e8 + 1f1f2,1f1e9 + 1f1f2,1f1ea + 1f1f2,1f1ec + 1f1f2,1f1ed + 1f1f2,1f1f0 + 1f1f2,1f1f1 + 1f1f2,1f1f2 + 1f1f2,1f1f3 + 1f1f2,1f1f4 + 1f1f2,1f1f5 + 1f1f2,1f1f7 + 1f1f2,1f1f8 + 1f1f2,1f1f9 + 1f1f2,1f1fa + 1f1f2,1f1fb + 1f1f2,1f1fc + 1f1f2,1f1fd + 1f1f2,1f1fe + 1f1f2,1f1ff + 1f1f3,1f1e6 + 1f1f3,1f1ea + 1f1f3,1f1eb + 1f1f3,1f1ec + 1f1f3,1f1ee + 1f1f3,1f1f1 + 1f1f3,1f1f4 + 1f1f3,1f1f5 + 1f1f3,1f1f7 + 1f1f3,1f1fa + 1f1f3,1f1ff + 1f1f4,1f1f2 + 1f1f5,1f1e6 + 1f1f5,1f1ea + 1f1f5,1f1eb + 1f1f5,1f1ec + 1f1f5,1f1ed + 1f1f5,1f1f0 + 1f1f5,1f1f1 + 1f1f5,1f1f3 + 1f1f5,1f1f7 + 1f1f5,1f1f8 + 1f1f5,1f1f9 + 1f1f5,1f1fc + 1f1f5,1f1fe + 1f1f6,1f1e6 + 1f1f7,1f1f4 + 1f1f7,1f1f8 + 1f1f7,1f1fa + 1f1f7,1f1fc + 1f1f8,1f1e6 + 1f1f8,1f1e7 + 1f1f8,1f1e8 + 1f1f8,1f1e9 + 1f1f8,1f1ea + 1f1f8,1f1ec + 1f1f8,1f1ee + 1f1f8,1f1f0 + 1f1f8,1f1f1 + 1f1f8,1f1f2 + 1f1f8,1f1f3 + 1f1f8,1f1f4 + 1f1f8,1f1f7 + 1f1f8,1f1f8 + 1f1f8,1f1f9 + 1f1f8,1f1fb + 1f1f8,1f1fd + 1f1f8,1f1fe + 1f1f8,1f1ff + 1f1f9,1f1e8 + 1f1f9,1f1e9 + 1f1f9,1f1ec + 1f1f9,1f1ed + 1f1f9,1f1ef + 1f1f9,1f1f0 + 1f1f9,1f1f1 + 1f1f9,1f1f2 + 1f1f9,1f1f3 + 1f1f9,1f1f4 + 1f1f9,1f1f7 + 1f1f9,1f1f9 + 1f1f9,1f1fb + 1f1f9,1f1fc + 1f1f9,1f1ff + 1f1fa,1f1e6 + 1f1fa,1f1ec + 1f1fa,1f1f8 + 1f1fa,1f1fe + 1f1fa,1f1ff + 1f1fb,1f1e6 + 1f1fb,1f1e8 + 1f1fb,1f1ea + 1f1fb,1f1ec + 1f1fb,1f1ee + 1f1fb,1f1f3 + 1f1fb,1f1fa + 1f1fc,1f1f8 + 1f1fe,1f1ea + 1f1ff,1f1e6 + 1f1ff,1f1f2 + 1f1ff,1f1fc + + + + + + (╯° + □°) + ╯︵ + ┻━┻ + ¯\\_ + (ツ) + _/¯ + ┬─┬ + ︵ /( + .□.\\ + + + + 1F3E7 + 1F6AE + 1F6B0 + 267F + 1F6B9 + 1F6BA + 1F6BB + 1F6BC + 1F6BE + 1F6C2 + 1F6C3 + 1F6C4 + 1F6C5 + + 26A0 + 1F6B8 + 26D4 + 1F6AB + 1F6B3 + 1F6AD + 1F6AF + 1F6B1 + 1F6B7 + 1F4F5 + 1F51E + 2622 + 2623 + + 2B06 + 2197 + 27A1 + 2198 + 2B07 + 2199 + 2B05 + 2196 + 2195 + 2194 + 21A9 + 21AA + 2934 + 2935 + 1F503 + 1F504 + 1F519 + 1F51A + 1F51B + 1F51C + 1F51D + + 1F6D0 + 269B + 1F549 + 2721 + 2638 + 262F + 271D + 2626 + 262A + 262E + 1F54E + 1F52F + + 2648 + 2649 + 264A + 264B + 264C + 264D + 264E + 264F + 2650 + 2651 + 2652 + 2653 + 26CE + + 1F500 + 1F501 + 1F502 + 25B6 + 23E9 + 23ED + 23EF + 25C0 + 23EA + 23EE + 1F53C + 23EB + 1F53D + 23EC + 23F8 + 23F9 + 23FA + 23CF + 1F3A6 + 1F505 + 1F506 + 1F4F6 + 1F4F3 + 1F4F4 + + 2640 + 2642 + 2695 + 267B + 269C + 1F531 + 1F4DB + 1F530 + 2B55 + 2705 + 2611 + 2714 + 2716 + 274C + 274E + 2795 + 2796 + 2797 + 27B0 + 27BF + 303D + 2733 + 2734 + 2747 + 203C + 2049 + 2753 + 2754 + 2755 + 2757 + 3030 + 00A9 + 00AE + 2122 + + 0023,FE0F,20E3 + 002A,FE0F,20E3 + 0030,FE0F,20E3 + 0031,FE0F,20E3 + 0032,FE0F,20E3 + 0033,FE0F,20E3 + 0034,FE0F,20E3 + 0035,FE0F,20E3 + 0036,FE0F,20E3 + 0037,FE0F,20E3 + 0038,FE0F,20E3 + 0039,FE0F,20E3 + 1F51F + + 1F4AF + 1F520 + 1F521 + 1F522 + 1F523 + 1F524 + 1F170 + 1F18E + 1F171 + 1F191 + 1F192 + 1F193 + 2139 + 1F194 + 24C2 + 1F195 + 1F196 + 1F17E + 1F197 + 1F17F + 1F198 + 1F199 + 1F19A + 1F201 + 1F202 + 1F237 + 1F236 + 1F22F + 1F250 + 1F239 + 1F21A + 1F232 + 1F251 + 1F238 + 1F234 + 1F233 + 3297 + 3299 + 1F23A + 1F235 + + 25AA + 25AB + 25FB + 25FC + 25FD + 25FE + 2B1B + 2B1C + 1F536 + 1F537 + 1F538 + 1F539 + 1F53A + 1F53B + 1F4A0 + 1F518 + 1F532 + 1F533 + 26AA + 26AB + 1F534 + 1F535 + + + + 1F435 + 1F412 + 1F98D + 1F436 + 1F415 + 1F429 + 1F43A + 1F98A + 1F431 + 1F408 + 1F981 + 1F42F + 1F405 + 1F406 + 1F434 + 1F40E + 1F984 + 1F993 + 1F98C + 1F42E + 1F402 + 1F403 + 1F404 + 1F437 + 1F416 + 1F417 + 1F43D + 1F40F + 1F411 + 1F410 + 1F42A + 1F42B + 1F992 + 1F418 + 1F98F + 1F42D + 1F401 + 1F400 + 1F439 + 1F430 + 1F407 + 1F43F + 1F994 + 1F987 + 1F43B + 1F428 + 1F43C + 1F43E + + 1F983 + 1F414 + 1F413 + 1F423 + 1F424 + 1F425 + 1F426 + 1F427 + 1F54A + 1F985 + 1F986 + 1F989 + + 1F438 + + 1F40A + 1F422 + 1F98E + 1F40D + 1F432 + 1F409 + 1F995 + 1F996 + + 1F433 + 1F40B + 1F42C + 1F41F + 1F420 + 1F421 + 1F988 + 1F419 + 1F41A + 1F980 + 1F990 + 1F991 + + 1F40C + 1F98B + 1F41B + 1F41C + 1F41D + 1F41E + 1F997 + 1F577 + 1F578 + 1F982 + + 1F490 + 1F338 + 1F4AE + 1F3F5 + 1F339 + 1F940 + 1F33A + 1F33B + 1F33C + 1F337 + + 1F331 + 1F332 + 1F333 + 1F334 + 1F335 + 1F33E + 1F33F + 2618 + 1F340 + 1F341 + 1F342 + 1F343 + + + + 1F600 + 1F601 + 1F602 + 1F923 + 1F603 + 1F604 + 1F605 + 1F606 + 1F609 + 1F60A + 1F60B + 1F60E + 1F60D + 1F618 + 1F617 + 1F619 + 1F61A + 263A + 1F642 + 1F917 + 1F929 + + 1F914 + 1F928 + 1F610 + 1F611 + 1F636 + 1F644 + 1F60F + 1F623 + 1F625 + 1F62E + 1F910 + 1F62F + 1F62A + 1F62B + 1F634 + 1F60C + 1F61B + 1F61C + 1F61D + 1F924 + 1F612 + 1F613 + 1F614 + 1F615 + 1F643 + 1F911 + 1F632 + + 2639 + 1F641 + 1F616 + 1F61E + 1F61F + 1F624 + 1F622 + 1F62D + 1F626 + 1F627 + 1F628 + 1F629 + 1F92F + 1F62C + 1F630 + 1F631 + 1F633 + 1F92A + 1F635 + 1F621 + 1F620 + 1F92C + + 1F637 + 1F912 + 1F915 + 1F922 + 1F92E + 1F927 + + 1F607 + 1F920 + 1F921 + 1F925 + 1F92B + 1F92D + 1F9D0 + 1F913 + + 1F608 + 1F47F + 1F479 + 1F47A + 1F480 + 2620 + 1F47B + 1F47D + 1F47E + 1F916 + 1F4A9 + + 1F63A + 1F638 + 1F639 + 1F63B + 1F63C + 1F63D + 1F640 + 1F63F + 1F63E + + 1F648 + 1F649 + 1F64A + + 1F476 + 1F476,1F3FB + 1F476,1F3FC + 1F476,1F3FD + 1F476,1F3FE + 1F476,1F3FF + 1F9D2 + 1F9D2,1F3FB + 1F9D2,1F3FC + 1F9D2,1F3FD + 1F9D2,1F3FE + 1F9D2,1F3FF + 1F466 + 1F466,1F3FB + 1F466,1F3FC + 1F466,1F3FD + 1F466,1F3FE + 1F466,1F3FF + 1F467 + 1F467,1F3FB + 1F467,1F3FC + 1F467,1F3FD + 1F467,1F3FE + 1F467,1F3FF + 1F9D1 + 1F9D1,1F3FB + 1F9D1,1F3FC + 1F9D1,1F3FD + 1F9D1,1F3FE + 1F9D1,1F3FF + 1F468 + 1F468,1F3FB + 1F468,1F3FC + 1F468,1F3FD + 1F468,1F3FE + 1F468,1F3FF + 1F469 + 1F469,1F3FB + 1F469,1F3FC + 1F469,1F3FD + 1F469,1F3FE + 1F469,1F3FF + 1F9D3 + 1F9D3,1F3FB + 1F9D3,1F3FC + 1F9D3,1F3FD + 1F9D3,1F3FE + 1F9D3,1F3FF + 1F474 + 1F474,1F3FB + 1F474,1F3FC + 1F474,1F3FD + 1F474,1F3FE + 1F474,1F3FF + 1F475 + 1F475,1F3FB + 1F475,1F3FC + 1F475,1F3FD + 1F475,1F3FE + 1F475,1F3FF + + 1F468,200D,2695,FE0F + 1F468,1F3FB,200D,2695,FE0F + 1F468,1F3FC,200D,2695,FE0F + 1F468,1F3FD,200D,2695,FE0F + 1F468,1F3FE,200D,2695,FE0F + 1F468,1F3FF,200D,2695,FE0F + 1F469,200D,2695,FE0F + 1F469,1F3FB,200D,2695,FE0F + 1F469,1F3FC,200D,2695,FE0F + 1F469,1F3FD,200D,2695,FE0F + 1F469,1F3FE,200D,2695,FE0F + 1F469,1F3FF,200D,2695,FE0F + 1F468,200D,1F393 + 1F468,1F3FB,200D,1F393 + 1F468,1F3FC,200D,1F393 + 1F468,1F3FD,200D,1F393 + 1F468,1F3FE,200D,1F393 + 1F468,1F3FF,200D,1F393 + 1F469,200D,1F393 + 1F469,1F3FB,200D,1F393 + 1F469,1F3FC,200D,1F393 + 1F469,1F3FD,200D,1F393 + 1F469,1F3FE,200D,1F393 + 1F469,1F3FF,200D,1F393 + 1F468,200D,1F3EB + 1F468,1F3FB,200D,1F3EB + 1F468,1F3FC,200D,1F3EB + 1F468,1F3FD,200D,1F3EB + 1F468,1F3FE,200D,1F3EB + 1F468,1F3FF,200D,1F3EB + 1F469,200D,1F3EB + 1F469,1F3FB,200D,1F3EB + 1F469,1F3FC,200D,1F3EB + 1F469,1F3FD,200D,1F3EB + 1F469,1F3FE,200D,1F3EB + 1F469,1F3FF,200D,1F3EB + 1F468,200D,2696,FE0F + 1F468,1F3FB,200D,2696,FE0F + 1F468,1F3FC,200D,2696,FE0F + 1F468,1F3FD,200D,2696,FE0F + 1F468,1F3FE,200D,2696,FE0F + 1F468,1F3FF,200D,2696,FE0F + 1F469,200D,2696,FE0F + 1F469,1F3FB,200D,2696,FE0F + 1F469,1F3FC,200D,2696,FE0F + 1F469,1F3FD,200D,2696,FE0F + 1F469,1F3FE,200D,2696,FE0F + 1F469,1F3FF,200D,2696,FE0F + 1F468,200D,1F33E + 1F468,1F3FB,200D,1F33E + 1F468,1F3FC,200D,1F33E + 1F468,1F3FD,200D,1F33E + 1F468,1F3FE,200D,1F33E + 1F468,1F3FF,200D,1F33E + 1F469,200D,1F33E + 1F469,1F3FB,200D,1F33E + 1F469,1F3FC,200D,1F33E + 1F469,1F3FD,200D,1F33E + 1F469,1F3FE,200D,1F33E + 1F469,1F3FF,200D,1F33E + 1F468,200D,1F373 + 1F468,1F3FB,200D,1F373 + 1F468,1F3FC,200D,1F373 + 1F468,1F3FD,200D,1F373 + 1F468,1F3FE,200D,1F373 + 1F468,1F3FF,200D,1F373 + 1F469,200D,1F373 + 1F469,1F3FB,200D,1F373 + 1F469,1F3FC,200D,1F373 + 1F469,1F3FD,200D,1F373 + 1F469,1F3FE,200D,1F373 + 1F469,1F3FF,200D,1F373 + 1F468,200D,1F527 + 1F468,1F3FB,200D,1F527 + 1F468,1F3FC,200D,1F527 + 1F468,1F3FD,200D,1F527 + 1F468,1F3FE,200D,1F527 + 1F468,1F3FF,200D,1F527 + 1F469,200D,1F527 + 1F469,1F3FB,200D,1F527 + 1F469,1F3FC,200D,1F527 + 1F469,1F3FD,200D,1F527 + 1F469,1F3FE,200D,1F527 + 1F469,1F3FF,200D,1F527 + 1F468,200D,1F3ED + 1F468,1F3FB,200D,1F3ED + 1F468,1F3FC,200D,1F3ED + 1F468,1F3FD,200D,1F3ED + 1F468,1F3FE,200D,1F3ED + 1F468,1F3FF,200D,1F3ED + 1F469,200D,1F3ED + 1F469,1F3FB,200D,1F3ED + 1F469,1F3FC,200D,1F3ED + 1F469,1F3FD,200D,1F3ED + 1F469,1F3FE,200D,1F3ED + 1F469,1F3FF,200D,1F3ED + 1F468,200D,1F4BC + 1F468,1F3FB,200D,1F4BC + 1F468,1F3FC,200D,1F4BC + 1F468,1F3FD,200D,1F4BC + 1F468,1F3FE,200D,1F4BC + 1F468,1F3FF,200D,1F4BC + 1F469,200D,1F4BC + 1F469,1F3FB,200D,1F4BC + 1F469,1F3FC,200D,1F4BC + 1F469,1F3FD,200D,1F4BC + 1F469,1F3FE,200D,1F4BC + 1F469,1F3FF,200D,1F4BC + 1F468,200D,1F52C + 1F468,1F3FB,200D,1F52C + 1F468,1F3FC,200D,1F52C + 1F468,1F3FD,200D,1F52C + 1F468,1F3FE,200D,1F52C + 1F468,1F3FF,200D,1F52C + 1F469,200D,1F52C + 1F469,1F3FB,200D,1F52C + 1F469,1F3FC,200D,1F52C + 1F469,1F3FD,200D,1F52C + 1F469,1F3FE,200D,1F52C + 1F469,1F3FF,200D,1F52C + 1F468,200D,1F4BB + 1F468,1F3FB,200D,1F4BB + 1F468,1F3FC,200D,1F4BB + 1F468,1F3FD,200D,1F4BB + 1F468,1F3FE,200D,1F4BB + 1F468,1F3FF,200D,1F4BB + 1F469,200D,1F4BB + 1F469,1F3FB,200D,1F4BB + 1F469,1F3FC,200D,1F4BB + 1F469,1F3FD,200D,1F4BB + 1F469,1F3FE,200D,1F4BB + 1F469,1F3FF,200D,1F4BB + 1F468,200D,1F3A4 + 1F468,1F3FB,200D,1F3A4 + 1F468,1F3FC,200D,1F3A4 + 1F468,1F3FD,200D,1F3A4 + 1F468,1F3FE,200D,1F3A4 + 1F468,1F3FF,200D,1F3A4 + 1F469,200D,1F3A4 + 1F469,1F3FB,200D,1F3A4 + 1F469,1F3FC,200D,1F3A4 + 1F469,1F3FD,200D,1F3A4 + 1F469,1F3FE,200D,1F3A4 + 1F469,1F3FF,200D,1F3A4 + 1F468,200D,1F3A8 + 1F468,1F3FB,200D,1F3A8 + 1F468,1F3FC,200D,1F3A8 + 1F468,1F3FD,200D,1F3A8 + 1F468,1F3FE,200D,1F3A8 + 1F468,1F3FF,200D,1F3A8 + 1F469,200D,1F3A8 + 1F469,1F3FB,200D,1F3A8 + 1F469,1F3FC,200D,1F3A8 + 1F469,1F3FD,200D,1F3A8 + 1F469,1F3FE,200D,1F3A8 + 1F469,1F3FF,200D,1F3A8 + 1F468,200D,2708,FE0F + 1F468,1F3FB,200D,2708,FE0F + 1F468,1F3FC,200D,2708,FE0F + 1F468,1F3FD,200D,2708,FE0F + 1F468,1F3FE,200D,2708,FE0F + 1F468,1F3FF,200D,2708,FE0F + 1F469,200D,2708,FE0F + 1F469,1F3FB,200D,2708,FE0F + 1F469,1F3FC,200D,2708,FE0F + 1F469,1F3FD,200D,2708,FE0F + 1F469,1F3FE,200D,2708,FE0F + 1F469,1F3FF,200D,2708,FE0F + 1F468,200D,1F680 + 1F468,1F3FB,200D,1F680 + 1F468,1F3FC,200D,1F680 + 1F468,1F3FD,200D,1F680 + 1F468,1F3FE,200D,1F680 + 1F468,1F3FF,200D,1F680 + 1F469,200D,1F680 + 1F469,1F3FB,200D,1F680 + 1F469,1F3FC,200D,1F680 + 1F469,1F3FD,200D,1F680 + 1F469,1F3FE,200D,1F680 + 1F469,1F3FF,200D,1F680 + 1F468,200D,1F692 + 1F468,1F3FB,200D,1F692 + 1F468,1F3FC,200D,1F692 + 1F468,1F3FD,200D,1F692 + 1F468,1F3FE,200D,1F692 + 1F468,1F3FF,200D,1F692 + 1F469,200D,1F692 + 1F469,1F3FB,200D,1F692 + 1F469,1F3FC,200D,1F692 + 1F469,1F3FD,200D,1F692 + 1F469,1F3FE,200D,1F692 + 1F469,1F3FF,200D,1F692 + 1F46E + 1F46E,1F3FB + 1F46E,1F3FC + 1F46E,1F3FD + 1F46E,1F3FE + 1F46E,1F3FF + 1F46E,200D,2642,FE0F + 1F46E,1F3FB,200D,2642,FE0F + 1F46E,1F3FC,200D,2642,FE0F + 1F46E,1F3FD,200D,2642,FE0F + 1F46E,1F3FE,200D,2642,FE0F + 1F46E,1F3FF,200D,2642,FE0F + 1F46E,200D,2640,FE0F + 1F46E,1F3FB,200D,2640,FE0F + 1F46E,1F3FC,200D,2640,FE0F + 1F46E,1F3FD,200D,2640,FE0F + 1F46E,1F3FE,200D,2640,FE0F + 1F46E,1F3FF,200D,2640,FE0F + 1F575 + 1F575,1F3FB + 1F575,1F3FC + 1F575,1F3FD + 1F575,1F3FE + 1F575,1F3FF + 1F575,FE0F,200D,2642,FE0F + 1F575,1F3FB,200D,2642,FE0F + 1F575,1F3FC,200D,2642,FE0F + 1F575,1F3FD,200D,2642,FE0F + 1F575,1F3FE,200D,2642,FE0F + 1F575,1F3FF,200D,2642,FE0F + 1F575,FE0F,200D,2640,FE0F + 1F575,1F3FB,200D,2640,FE0F + 1F575,1F3FC,200D,2640,FE0F + 1F575,1F3FD,200D,2640,FE0F + 1F575,1F3FE,200D,2640,FE0F + 1F575,1F3FF,200D,2640,FE0F + 1F482 + 1F482,1F3FB + 1F482,1F3FC + 1F482,1F3FD + 1F482,1F3FE + 1F482,1F3FF + 1F482,200D,2642,FE0F + 1F482,1F3FB,200D,2642,FE0F + 1F482,1F3FC,200D,2642,FE0F + 1F482,1F3FD,200D,2642,FE0F + 1F482,1F3FE,200D,2642,FE0F + 1F482,1F3FF,200D,2642,FE0F + 1F482,200D,2640,FE0F + 1F482,1F3FB,200D,2640,FE0F + 1F482,1F3FC,200D,2640,FE0F + 1F482,1F3FD,200D,2640,FE0F + 1F482,1F3FE,200D,2640,FE0F + 1F482,1F3FF,200D,2640,FE0F + 1F477 + 1F477,1F3FB + 1F477,1F3FC + 1F477,1F3FD + 1F477,1F3FE + 1F477,1F3FF + 1F477,200D,2642,FE0F + 1F477,1F3FB,200D,2642,FE0F + 1F477,1F3FC,200D,2642,FE0F + 1F477,1F3FD,200D,2642,FE0F + 1F477,1F3FE,200D,2642,FE0F + 1F477,1F3FF,200D,2642,FE0F + 1F477,200D,2640,FE0F + 1F477,1F3FB,200D,2640,FE0F + 1F477,1F3FC,200D,2640,FE0F + 1F477,1F3FD,200D,2640,FE0F + 1F477,1F3FE,200D,2640,FE0F + 1F477,1F3FF,200D,2640,FE0F + 1F934 + 1F934,1F3FB + 1F934,1F3FC + 1F934,1F3FD + 1F934,1F3FE + 1F934,1F3FF + 1F478 + 1F478,1F3FB + 1F478,1F3FC + 1F478,1F3FD + 1F478,1F3FE + 1F478,1F3FF + 1F473 + 1F473,1F3FB + 1F473,1F3FC + 1F473,1F3FD + 1F473,1F3FE + 1F473,1F3FF + 1F473,200D,2642,FE0F + 1F473,1F3FB,200D,2642,FE0F + 1F473,1F3FC,200D,2642,FE0F + 1F473,1F3FD,200D,2642,FE0F + 1F473,1F3FE,200D,2642,FE0F + 1F473,1F3FF,200D,2642,FE0F + 1F473,200D,2640,FE0F + 1F473,1F3FB,200D,2640,FE0F + 1F473,1F3FC,200D,2640,FE0F + 1F473,1F3FD,200D,2640,FE0F + 1F473,1F3FE,200D,2640,FE0F + 1F473,1F3FF,200D,2640,FE0F + 1F472 + 1F472,1F3FB + 1F472,1F3FC + 1F472,1F3FD + 1F472,1F3FE + 1F472,1F3FF + 1F9D5 + 1F9D5,1F3FB + 1F9D5,1F3FC + 1F9D5,1F3FD + 1F9D5,1F3FE + 1F9D5,1F3FF + 1F9D4 + 1F9D4,1F3FB + 1F9D4,1F3FC + 1F9D4,1F3FD + 1F9D4,1F3FE + 1F9D4,1F3FF + 1F471 + 1F471,1F3FB + 1F471,1F3FC + 1F471,1F3FD + 1F471,1F3FE + 1F471,1F3FF + 1F471,200D,2642,FE0F + 1F471,1F3FB,200D,2642,FE0F + 1F471,1F3FC,200D,2642,FE0F + 1F471,1F3FD,200D,2642,FE0F + 1F471,1F3FE,200D,2642,FE0F + 1F471,1F3FF,200D,2642,FE0F + 1F471,200D,2640,FE0F + 1F471,1F3FB,200D,2640,FE0F + 1F471,1F3FC,200D,2640,FE0F + 1F471,1F3FD,200D,2640,FE0F + 1F471,1F3FE,200D,2640,FE0F + 1F471,1F3FF,200D,2640,FE0F + 1F935 + 1F935,1F3FB + 1F935,1F3FC + 1F935,1F3FD + 1F935,1F3FE + 1F935,1F3FF + 1F470 + 1F470,1F3FB + 1F470,1F3FC + 1F470,1F3FD + 1F470,1F3FE + 1F470,1F3FF + 1F930 + 1F930,1F3FB + 1F930,1F3FC + 1F930,1F3FD + 1F930,1F3FE + 1F930,1F3FF + 1F931 + 1F931,1F3FB + 1F931,1F3FC + 1F931,1F3FD + 1F931,1F3FE + 1F931,1F3FF + + 1F47C + 1F47C,1F3FB + 1F47C,1F3FC + 1F47C,1F3FD + 1F47C,1F3FE + 1F47C,1F3FF + 1F385 + 1F385,1F3FB + 1F385,1F3FC + 1F385,1F3FD + 1F385,1F3FE + 1F385,1F3FF + 1F936 + 1F936,1F3FB + 1F936,1F3FC + 1F936,1F3FD + 1F936,1F3FE + 1F936,1F3FF + 1F9D9 + 1F9D9,1F3FB + 1F9D9,1F3FC + 1F9D9,1F3FD + 1F9D9,1F3FE + 1F9D9,1F3FF + 1F9D9,200D,2640,FE0F + 1F9D9,1F3FB,200D,2640,FE0F + 1F9D9,1F3FC,200D,2640,FE0F + 1F9D9,1F3FD,200D,2640,FE0F + 1F9D9,1F3FE,200D,2640,FE0F + 1F9D9,1F3FF,200D,2640,FE0F + 1F9D9,200D,2642,FE0F + 1F9D9,1F3FB,200D,2642,FE0F + 1F9D9,1F3FC,200D,2642,FE0F + 1F9D9,1F3FD,200D,2642,FE0F + 1F9D9,1F3FE,200D,2642,FE0F + 1F9D9,1F3FF,200D,2642,FE0F + 1F9DA + 1F9DA,1F3FB + 1F9DA,1F3FC + 1F9DA,1F3FD + 1F9DA,1F3FE + 1F9DA,1F3FF + 1F9DA,200D,2640,FE0F + 1F9DA,1F3FB,200D,2640,FE0F + 1F9DA,1F3FC,200D,2640,FE0F + 1F9DA,1F3FD,200D,2640,FE0F + 1F9DA,1F3FE,200D,2640,FE0F + 1F9DA,1F3FF,200D,2640,FE0F + 1F9DA,200D,2642,FE0F + 1F9DA,1F3FB,200D,2642,FE0F + 1F9DA,1F3FC,200D,2642,FE0F + 1F9DA,1F3FD,200D,2642,FE0F + 1F9DA,1F3FE,200D,2642,FE0F + 1F9DA,1F3FF,200D,2642,FE0F + 1F9DB + 1F9DB,1F3FB + 1F9DB,1F3FC + 1F9DB,1F3FD + 1F9DB,1F3FE + 1F9DB,1F3FF + 1F9DB,200D,2640,FE0F + 1F9DB,1F3FB,200D,2640,FE0F + 1F9DB,1F3FC,200D,2640,FE0F + 1F9DB,1F3FD,200D,2640,FE0F + 1F9DB,1F3FE,200D,2640,FE0F + 1F9DB,1F3FF,200D,2640,FE0F + 1F9DB,200D,2642,FE0F + 1F9DB,1F3FB,200D,2642,FE0F + 1F9DB,1F3FC,200D,2642,FE0F + 1F9DB,1F3FD,200D,2642,FE0F + 1F9DB,1F3FE,200D,2642,FE0F + 1F9DB,1F3FF,200D,2642,FE0F + 1F9DC + 1F9DC,1F3FB + 1F9DC,1F3FC + 1F9DC,1F3FD + 1F9DC,1F3FE + 1F9DC,1F3FF + 1F9DC,200D,2640,FE0F + 1F9DC,1F3FB,200D,2640,FE0F + 1F9DC,1F3FC,200D,2640,FE0F + 1F9DC,1F3FD,200D,2640,FE0F + 1F9DC,1F3FE,200D,2640,FE0F + 1F9DC,1F3FF,200D,2640,FE0F + 1F9DC,200D,2642,FE0F + 1F9DC,1F3FB,200D,2642,FE0F + 1F9DC,1F3FC,200D,2642,FE0F + 1F9DC,1F3FD,200D,2642,FE0F + 1F9DC,1F3FE,200D,2642,FE0F + 1F9DC,1F3FF,200D,2642,FE0F + 1F9DD + 1F9DD,1F3FB + 1F9DD,1F3FC + 1F9DD,1F3FD + 1F9DD,1F3FE + 1F9DD,1F3FF + 1F9DD,200D,2640,FE0F + 1F9DD,1F3FB,200D,2640,FE0F + 1F9DD,1F3FC,200D,2640,FE0F + 1F9DD,1F3FD,200D,2640,FE0F + 1F9DD,1F3FE,200D,2640,FE0F + 1F9DD,1F3FF,200D,2640,FE0F + 1F9DD,200D,2642,FE0F + 1F9DD,1F3FB,200D,2642,FE0F + 1F9DD,1F3FC,200D,2642,FE0F + 1F9DD,1F3FD,200D,2642,FE0F + 1F9DD,1F3FE,200D,2642,FE0F + 1F9DD,1F3FF,200D,2642,FE0F + 1F9DE + 1F9DE,200D,2640,FE0F + 1F9DE,200D,2642,FE0F + 1F9DF + 1F9DF,200D,2640,FE0F + 1F9DF,200D,2642,FE0F + + 1F64D + 1F64D,1F3FB + 1F64D,1F3FC + 1F64D,1F3FD + 1F64D,1F3FE + 1F64D,1F3FF + 1F64D,200D,2642,FE0F + 1F64D,1F3FB,200D,2642,FE0F + 1F64D,1F3FC,200D,2642,FE0F + 1F64D,1F3FD,200D,2642,FE0F + 1F64D,1F3FE,200D,2642,FE0F + 1F64D,1F3FF,200D,2642,FE0F + 1F64D,200D,2640,FE0F + 1F64D,1F3FB,200D,2640,FE0F + 1F64D,1F3FC,200D,2640,FE0F + 1F64D,1F3FD,200D,2640,FE0F + 1F64D,1F3FE,200D,2640,FE0F + 1F64D,1F3FF,200D,2640,FE0F + 1F64E + 1F64E,1F3FB + 1F64E,1F3FC + 1F64E,1F3FD + 1F64E,1F3FE + 1F64E,1F3FF + 1F64E,200D,2642,FE0F + 1F64E,1F3FB,200D,2642,FE0F + 1F64E,1F3FC,200D,2642,FE0F + 1F64E,1F3FD,200D,2642,FE0F + 1F64E,1F3FE,200D,2642,FE0F + 1F64E,1F3FF,200D,2642,FE0F + 1F64E,200D,2640,FE0F + 1F64E,1F3FB,200D,2640,FE0F + 1F64E,1F3FC,200D,2640,FE0F + 1F64E,1F3FD,200D,2640,FE0F + 1F64E,1F3FE,200D,2640,FE0F + 1F64E,1F3FF,200D,2640,FE0F + 1F645 + 1F645,1F3FB + 1F645,1F3FC + 1F645,1F3FD + 1F645,1F3FE + 1F645,1F3FF + 1F645,200D,2642,FE0F + 1F645,1F3FB,200D,2642,FE0F + 1F645,1F3FC,200D,2642,FE0F + 1F645,1F3FD,200D,2642,FE0F + 1F645,1F3FE,200D,2642,FE0F + 1F645,1F3FF,200D,2642,FE0F + 1F645,200D,2640,FE0F + 1F645,1F3FB,200D,2640,FE0F + 1F645,1F3FC,200D,2640,FE0F + 1F645,1F3FD,200D,2640,FE0F + 1F645,1F3FE,200D,2640,FE0F + 1F645,1F3FF,200D,2640,FE0F + 1F646 + 1F646,1F3FB + 1F646,1F3FC + 1F646,1F3FD + 1F646,1F3FE + 1F646,1F3FF + 1F646,200D,2642,FE0F + 1F646,1F3FB,200D,2642,FE0F + 1F646,1F3FC,200D,2642,FE0F + 1F646,1F3FD,200D,2642,FE0F + 1F646,1F3FE,200D,2642,FE0F + 1F646,1F3FF,200D,2642,FE0F + 1F646,200D,2640,FE0F + 1F646,1F3FB,200D,2640,FE0F + 1F646,1F3FC,200D,2640,FE0F + 1F646,1F3FD,200D,2640,FE0F + 1F646,1F3FE,200D,2640,FE0F + 1F646,1F3FF,200D,2640,FE0F + 1F481 + 1F481,1F3FB + 1F481,1F3FC + 1F481,1F3FD + 1F481,1F3FE + 1F481,1F3FF + 1F481,200D,2642,FE0F + 1F481,1F3FB,200D,2642,FE0F + 1F481,1F3FC,200D,2642,FE0F + 1F481,1F3FD,200D,2642,FE0F + 1F481,1F3FE,200D,2642,FE0F + 1F481,1F3FF,200D,2642,FE0F + 1F481,200D,2640,FE0F + 1F481,1F3FB,200D,2640,FE0F + 1F481,1F3FC,200D,2640,FE0F + 1F481,1F3FD,200D,2640,FE0F + 1F481,1F3FE,200D,2640,FE0F + 1F481,1F3FF,200D,2640,FE0F + 1F64B + 1F64B,1F3FB + 1F64B,1F3FC + 1F64B,1F3FD + 1F64B,1F3FE + 1F64B,1F3FF + 1F64B,200D,2642,FE0F + 1F64B,1F3FB,200D,2642,FE0F + 1F64B,1F3FC,200D,2642,FE0F + 1F64B,1F3FD,200D,2642,FE0F + 1F64B,1F3FE,200D,2642,FE0F + 1F64B,1F3FF,200D,2642,FE0F + 1F64B,200D,2640,FE0F + 1F64B,1F3FB,200D,2640,FE0F + 1F64B,1F3FC,200D,2640,FE0F + 1F64B,1F3FD,200D,2640,FE0F + 1F64B,1F3FE,200D,2640,FE0F + 1F64B,1F3FF,200D,2640,FE0F + 1F647 + 1F647,1F3FB + 1F647,1F3FC + 1F647,1F3FD + 1F647,1F3FE + 1F647,1F3FF + 1F647,200D,2642,FE0F + 1F647,1F3FB,200D,2642,FE0F + 1F647,1F3FC,200D,2642,FE0F + 1F647,1F3FD,200D,2642,FE0F + 1F647,1F3FE,200D,2642,FE0F + 1F647,1F3FF,200D,2642,FE0F + 1F647,200D,2640,FE0F + 1F647,1F3FB,200D,2640,FE0F + 1F647,1F3FC,200D,2640,FE0F + 1F647,1F3FD,200D,2640,FE0F + 1F647,1F3FE,200D,2640,FE0F + 1F647,1F3FF,200D,2640,FE0F + 1F926 + 1F926,1F3FB + 1F926,1F3FC + 1F926,1F3FD + 1F926,1F3FE + 1F926,1F3FF + 1F926,200D,2642,FE0F + 1F926,1F3FB,200D,2642,FE0F + 1F926,1F3FC,200D,2642,FE0F + 1F926,1F3FD,200D,2642,FE0F + 1F926,1F3FE,200D,2642,FE0F + 1F926,1F3FF,200D,2642,FE0F + 1F926,200D,2640,FE0F + 1F926,1F3FB,200D,2640,FE0F + 1F926,1F3FC,200D,2640,FE0F + 1F926,1F3FD,200D,2640,FE0F + 1F926,1F3FE,200D,2640,FE0F + 1F926,1F3FF,200D,2640,FE0F + 1F937 + 1F937,1F3FB + 1F937,1F3FC + 1F937,1F3FD + 1F937,1F3FE + 1F937,1F3FF + 1F937,200D,2642,FE0F + 1F937,1F3FB,200D,2642,FE0F + 1F937,1F3FC,200D,2642,FE0F + 1F937,1F3FD,200D,2642,FE0F + 1F937,1F3FE,200D,2642,FE0F + 1F937,1F3FF,200D,2642,FE0F + 1F937,200D,2640,FE0F + 1F937,1F3FB,200D,2640,FE0F + 1F937,1F3FC,200D,2640,FE0F + 1F937,1F3FD,200D,2640,FE0F + 1F937,1F3FE,200D,2640,FE0F + 1F937,1F3FF,200D,2640,FE0F + + 1F486 + 1F486,1F3FB + 1F486,1F3FC + 1F486,1F3FD + 1F486,1F3FE + 1F486,1F3FF + 1F486,200D,2642,FE0F + 1F486,1F3FB,200D,2642,FE0F + 1F486,1F3FC,200D,2642,FE0F + 1F486,1F3FD,200D,2642,FE0F + 1F486,1F3FE,200D,2642,FE0F + 1F486,1F3FF,200D,2642,FE0F + 1F486,200D,2640,FE0F + 1F486,1F3FB,200D,2640,FE0F + 1F486,1F3FC,200D,2640,FE0F + 1F486,1F3FD,200D,2640,FE0F + 1F486,1F3FE,200D,2640,FE0F + 1F486,1F3FF,200D,2640,FE0F + 1F487 + 1F487,1F3FB + 1F487,1F3FC + 1F487,1F3FD + 1F487,1F3FE + 1F487,1F3FF + 1F487,200D,2642,FE0F + 1F487,1F3FB,200D,2642,FE0F + 1F487,1F3FC,200D,2642,FE0F + 1F487,1F3FD,200D,2642,FE0F + 1F487,1F3FE,200D,2642,FE0F + 1F487,1F3FF,200D,2642,FE0F + 1F487,200D,2640,FE0F + 1F487,1F3FB,200D,2640,FE0F + 1F487,1F3FC,200D,2640,FE0F + 1F487,1F3FD,200D,2640,FE0F + 1F487,1F3FE,200D,2640,FE0F + 1F487,1F3FF,200D,2640,FE0F + 1F6B6 + 1F6B6,1F3FB + 1F6B6,1F3FC + 1F6B6,1F3FD + 1F6B6,1F3FE + 1F6B6,1F3FF + 1F6B6,200D,2642,FE0F + 1F6B6,1F3FB,200D,2642,FE0F + 1F6B6,1F3FC,200D,2642,FE0F + 1F6B6,1F3FD,200D,2642,FE0F + 1F6B6,1F3FE,200D,2642,FE0F + 1F6B6,1F3FF,200D,2642,FE0F + 1F6B6,200D,2640,FE0F + 1F6B6,1F3FB,200D,2640,FE0F + 1F6B6,1F3FC,200D,2640,FE0F + 1F6B6,1F3FD,200D,2640,FE0F + 1F6B6,1F3FE,200D,2640,FE0F + 1F6B6,1F3FF,200D,2640,FE0F + 1F3C3 + 1F3C3,1F3FB + 1F3C3,1F3FC + 1F3C3,1F3FD + 1F3C3,1F3FE + 1F3C3,1F3FF + 1F3C3,200D,2642,FE0F + 1F3C3,1F3FB,200D,2642,FE0F + 1F3C3,1F3FC,200D,2642,FE0F + 1F3C3,1F3FD,200D,2642,FE0F + 1F3C3,1F3FE,200D,2642,FE0F + 1F3C3,1F3FF,200D,2642,FE0F + 1F3C3,200D,2640,FE0F + 1F3C3,1F3FB,200D,2640,FE0F + 1F3C3,1F3FC,200D,2640,FE0F + 1F3C3,1F3FD,200D,2640,FE0F + 1F3C3,1F3FE,200D,2640,FE0F + 1F3C3,1F3FF,200D,2640,FE0F + 1F483 + 1F483,1F3FB + 1F483,1F3FC + 1F483,1F3FD + 1F483,1F3FE + 1F483,1F3FF + 1F57A + 1F57A,1F3FB + 1F57A,1F3FC + 1F57A,1F3FD + 1F57A,1F3FE + 1F57A,1F3FF + 1F46F + 1F46F,200D,2642,FE0F + 1F46F,200D,2640,FE0F + 1F9D6 + 1F9D6,1F3FB + 1F9D6,1F3FC + 1F9D6,1F3FD + 1F9D6,1F3FE + 1F9D6,1F3FF + 1F9D6,200D,2640,FE0F + 1F9D6,1F3FB,200D,2640,FE0F + 1F9D6,1F3FC,200D,2640,FE0F + 1F9D6,1F3FD,200D,2640,FE0F + 1F9D6,1F3FE,200D,2640,FE0F + 1F9D6,1F3FF,200D,2640,FE0F + 1F9D6,200D,2642,FE0F + 1F9D6,1F3FB,200D,2642,FE0F + 1F9D6,1F3FC,200D,2642,FE0F + 1F9D6,1F3FD,200D,2642,FE0F + 1F9D6,1F3FE,200D,2642,FE0F + 1F9D6,1F3FF,200D,2642,FE0F + 1F9D7 + 1F9D7,1F3FB + 1F9D7,1F3FC + 1F9D7,1F3FD + 1F9D7,1F3FE + 1F9D7,1F3FF + 1F9D7,200D,2640,FE0F + 1F9D7,1F3FB,200D,2640,FE0F + 1F9D7,1F3FC,200D,2640,FE0F + 1F9D7,1F3FD,200D,2640,FE0F + 1F9D7,1F3FE,200D,2640,FE0F + 1F9D7,1F3FF,200D,2640,FE0F + 1F9D7,200D,2642,FE0F + 1F9D7,1F3FB,200D,2642,FE0F + 1F9D7,1F3FC,200D,2642,FE0F + 1F9D7,1F3FD,200D,2642,FE0F + 1F9D7,1F3FE,200D,2642,FE0F + 1F9D7,1F3FF,200D,2642,FE0F + 1F9D8 + 1F9D8,1F3FB + 1F9D8,1F3FC + 1F9D8,1F3FD + 1F9D8,1F3FE + 1F9D8,1F3FF + 1F9D8,200D,2640,FE0F + 1F9D8,1F3FB,200D,2640,FE0F + 1F9D8,1F3FC,200D,2640,FE0F + 1F9D8,1F3FD,200D,2640,FE0F + 1F9D8,1F3FE,200D,2640,FE0F + 1F9D8,1F3FF,200D,2640,FE0F + 1F9D8,200D,2642,FE0F + 1F9D8,1F3FB,200D,2642,FE0F + 1F9D8,1F3FC,200D,2642,FE0F + 1F9D8,1F3FD,200D,2642,FE0F + 1F9D8,1F3FE,200D,2642,FE0F + 1F9D8,1F3FF,200D,2642,FE0F + 1F6C0 + 1F6C0,1F3FB + 1F6C0,1F3FC + 1F6C0,1F3FD + 1F6C0,1F3FE + 1F6C0,1F3FF + 1F6CC + 1F6CC,1F3FB + 1F6CC,1F3FC + 1F6CC,1F3FD + 1F6CC,1F3FE + 1F6CC,1F3FF + 1F574 + 1F574,1F3FB + 1F574,1F3FC + 1F574,1F3FD + 1F574,1F3FE + 1F574,1F3FF + 1F5E3 + 1F464 + 1F465 + + 1F93A + 1F3C7 + 1F3C7,1F3FB + 1F3C7,1F3FC + 1F3C7,1F3FD + 1F3C7,1F3FE + 1F3C7,1F3FF + 26F7 + 1F3C2 + 1F3C2,1F3FB + 1F3C2,1F3FC + 1F3C2,1F3FD + 1F3C2,1F3FE + 1F3C2,1F3FF + 1F3CC + 1F3CC,1F3FB + 1F3CC,1F3FC + 1F3CC,1F3FD + 1F3CC,1F3FE + 1F3CC,1F3FF + 1F3CC,FE0F,200D,2642,FE0F + 1F3CC,1F3FB,200D,2642,FE0F + 1F3CC,1F3FC,200D,2642,FE0F + 1F3CC,1F3FD,200D,2642,FE0F + 1F3CC,1F3FE,200D,2642,FE0F + 1F3CC,1F3FF,200D,2642,FE0F + 1F3CC,FE0F,200D,2640,FE0F + 1F3CC,1F3FB,200D,2640,FE0F + 1F3CC,1F3FC,200D,2640,FE0F + 1F3CC,1F3FD,200D,2640,FE0F + 1F3CC,1F3FE,200D,2640,FE0F + 1F3CC,1F3FF,200D,2640,FE0F + 1F3C4 + 1F3C4,1F3FB + 1F3C4,1F3FC + 1F3C4,1F3FD + 1F3C4,1F3FE + 1F3C4,1F3FF + 1F3C4,200D,2642,FE0F + 1F3C4,1F3FB,200D,2642,FE0F + 1F3C4,1F3FC,200D,2642,FE0F + 1F3C4,1F3FD,200D,2642,FE0F + 1F3C4,1F3FE,200D,2642,FE0F + 1F3C4,1F3FF,200D,2642,FE0F + 1F3C4,200D,2640,FE0F + 1F3C4,1F3FB,200D,2640,FE0F + 1F3C4,1F3FC,200D,2640,FE0F + 1F3C4,1F3FD,200D,2640,FE0F + 1F3C4,1F3FE,200D,2640,FE0F + 1F3C4,1F3FF,200D,2640,FE0F + 1F6A3 + 1F6A3,1F3FB + 1F6A3,1F3FC + 1F6A3,1F3FD + 1F6A3,1F3FE + 1F6A3,1F3FF + 1F6A3,200D,2642,FE0F + 1F6A3,1F3FB,200D,2642,FE0F + 1F6A3,1F3FC,200D,2642,FE0F + 1F6A3,1F3FD,200D,2642,FE0F + 1F6A3,1F3FE,200D,2642,FE0F + 1F6A3,1F3FF,200D,2642,FE0F + 1F6A3,200D,2640,FE0F + 1F6A3,1F3FB,200D,2640,FE0F + 1F6A3,1F3FC,200D,2640,FE0F + 1F6A3,1F3FD,200D,2640,FE0F + 1F6A3,1F3FE,200D,2640,FE0F + 1F6A3,1F3FF,200D,2640,FE0F + 1F3CA + 1F3CA,1F3FB + 1F3CA,1F3FC + 1F3CA,1F3FD + 1F3CA,1F3FE + 1F3CA,1F3FF + 1F3CA,200D,2642,FE0F + 1F3CA,1F3FB,200D,2642,FE0F + 1F3CA,1F3FC,200D,2642,FE0F + 1F3CA,1F3FD,200D,2642,FE0F + 1F3CA,1F3FE,200D,2642,FE0F + 1F3CA,1F3FF,200D,2642,FE0F + 1F3CA,200D,2640,FE0F + 1F3CA,1F3FB,200D,2640,FE0F + 1F3CA,1F3FC,200D,2640,FE0F + 1F3CA,1F3FD,200D,2640,FE0F + 1F3CA,1F3FE,200D,2640,FE0F + 1F3CA,1F3FF,200D,2640,FE0F + 26F9 + 26F9,1F3FB + 26F9,1F3FC + 26F9,1F3FD + 26F9,1F3FE + 26F9,1F3FF + 26F9,FE0F,200D,2642,FE0F + 26F9,1F3FB,200D,2642,FE0F + 26F9,1F3FC,200D,2642,FE0F + 26F9,1F3FD,200D,2642,FE0F + 26F9,1F3FE,200D,2642,FE0F + 26F9,1F3FF,200D,2642,FE0F + 26F9,FE0F,200D,2640,FE0F + 26F9,1F3FB,200D,2640,FE0F + 26F9,1F3FC,200D,2640,FE0F + 26F9,1F3FD,200D,2640,FE0F + 26F9,1F3FE,200D,2640,FE0F + 26F9,1F3FF,200D,2640,FE0F + 1F3CB + 1F3CB,1F3FB + 1F3CB,1F3FC + 1F3CB,1F3FD + 1F3CB,1F3FE + 1F3CB,1F3FF + 1F3CB,FE0F,200D,2642,FE0F + 1F3CB,1F3FB,200D,2642,FE0F + 1F3CB,1F3FC,200D,2642,FE0F + 1F3CB,1F3FD,200D,2642,FE0F + 1F3CB,1F3FE,200D,2642,FE0F + 1F3CB,1F3FF,200D,2642,FE0F + 1F3CB,FE0F,200D,2640,FE0F + 1F3CB,1F3FB,200D,2640,FE0F + 1F3CB,1F3FC,200D,2640,FE0F + 1F3CB,1F3FD,200D,2640,FE0F + 1F3CB,1F3FE,200D,2640,FE0F + 1F3CB,1F3FF,200D,2640,FE0F + 1F6B4 + 1F6B4,1F3FB + 1F6B4,1F3FC + 1F6B4,1F3FD + 1F6B4,1F3FE + 1F6B4,1F3FF + 1F6B4,200D,2642,FE0F + 1F6B4,1F3FB,200D,2642,FE0F + 1F6B4,1F3FC,200D,2642,FE0F + 1F6B4,1F3FD,200D,2642,FE0F + 1F6B4,1F3FE,200D,2642,FE0F + 1F6B4,1F3FF,200D,2642,FE0F + 1F6B4,200D,2640,FE0F + 1F6B4,1F3FB,200D,2640,FE0F + 1F6B4,1F3FC,200D,2640,FE0F + 1F6B4,1F3FD,200D,2640,FE0F + 1F6B4,1F3FE,200D,2640,FE0F + 1F6B4,1F3FF,200D,2640,FE0F + 1F6B5 + 1F6B5,1F3FB + 1F6B5,1F3FC + 1F6B5,1F3FD + 1F6B5,1F3FE + 1F6B5,1F3FF + 1F6B5,200D,2642,FE0F + 1F6B5,1F3FB,200D,2642,FE0F + 1F6B5,1F3FC,200D,2642,FE0F + 1F6B5,1F3FD,200D,2642,FE0F + 1F6B5,1F3FE,200D,2642,FE0F + 1F6B5,1F3FF,200D,2642,FE0F + 1F6B5,200D,2640,FE0F + 1F6B5,1F3FB,200D,2640,FE0F + 1F6B5,1F3FC,200D,2640,FE0F + 1F6B5,1F3FD,200D,2640,FE0F + 1F6B5,1F3FE,200D,2640,FE0F + 1F6B5,1F3FF,200D,2640,FE0F + 1F3CE + 1F3CD + 1F938 + 1F938,1F3FB + 1F938,1F3FC + 1F938,1F3FD + 1F938,1F3FE + 1F938,1F3FF + 1F938,200D,2642,FE0F + 1F938,1F3FB,200D,2642,FE0F + 1F938,1F3FC,200D,2642,FE0F + 1F938,1F3FD,200D,2642,FE0F + 1F938,1F3FE,200D,2642,FE0F + 1F938,1F3FF,200D,2642,FE0F + 1F938,200D,2640,FE0F + 1F938,1F3FB,200D,2640,FE0F + 1F938,1F3FC,200D,2640,FE0F + 1F938,1F3FD,200D,2640,FE0F + 1F938,1F3FE,200D,2640,FE0F + 1F938,1F3FF,200D,2640,FE0F + 1F93C + 1F93C,200D,2642,FE0F + 1F93C,200D,2640,FE0F + 1F93D + 1F93D,1F3FB + 1F93D,1F3FC + 1F93D,1F3FD + 1F93D,1F3FE + 1F93D,1F3FF + 1F93D,200D,2642,FE0F + 1F93D,1F3FB,200D,2642,FE0F + 1F93D,1F3FC,200D,2642,FE0F + 1F93D,1F3FD,200D,2642,FE0F + 1F93D,1F3FE,200D,2642,FE0F + 1F93D,1F3FF,200D,2642,FE0F + 1F93D,200D,2640,FE0F + 1F93D,1F3FB,200D,2640,FE0F + 1F93D,1F3FC,200D,2640,FE0F + 1F93D,1F3FD,200D,2640,FE0F + 1F93D,1F3FE,200D,2640,FE0F + 1F93D,1F3FF,200D,2640,FE0F + 1F93E + 1F93E,1F3FB + 1F93E,1F3FC + 1F93E,1F3FD + 1F93E,1F3FE + 1F93E,1F3FF + 1F93E,200D,2642,FE0F + 1F93E,1F3FB,200D,2642,FE0F + 1F93E,1F3FC,200D,2642,FE0F + 1F93E,1F3FD,200D,2642,FE0F + 1F93E,1F3FE,200D,2642,FE0F + 1F93E,1F3FF,200D,2642,FE0F + 1F93E,200D,2640,FE0F + 1F93E,1F3FB,200D,2640,FE0F + 1F93E,1F3FC,200D,2640,FE0F + 1F93E,1F3FD,200D,2640,FE0F + 1F93E,1F3FE,200D,2640,FE0F + 1F93E,1F3FF,200D,2640,FE0F + 1F939 + 1F939,1F3FB + 1F939,1F3FC + 1F939,1F3FD + 1F939,1F3FE + 1F939,1F3FF + 1F939,200D,2642,FE0F + 1F939,1F3FB,200D,2642,FE0F + 1F939,1F3FC,200D,2642,FE0F + 1F939,1F3FD,200D,2642,FE0F + 1F939,1F3FE,200D,2642,FE0F + 1F939,1F3FF,200D,2642,FE0F + 1F939,200D,2640,FE0F + 1F939,1F3FB,200D,2640,FE0F + 1F939,1F3FC,200D,2640,FE0F + 1F939,1F3FD,200D,2640,FE0F + 1F939,1F3FE,200D,2640,FE0F + 1F939,1F3FF,200D,2640,FE0F + + 1F46B + 1F46C + 1F46D + 1F48F + 1F469,200D,2764,FE0F,200D,1F48B,200D,1F468 + 1F468,200D,2764,FE0F,200D,1F48B,200D,1F468 + 1F469,200D,2764,FE0F,200D,1F48B,200D,1F469 + 1F491 + 1F469,200D,2764,FE0F,200D,1F468 + 1F468,200D,2764,FE0F,200D,1F468 + 1F469,200D,2764,FE0F,200D,1F469 + 1F46A + 1F468,200D,1F469,200D,1F466 + 1F468,200D,1F469,200D,1F467 + 1F468,200D,1F469,200D,1F467,200D,1F466 + 1F468,200D,1F469,200D,1F466,200D,1F466 + 1F468,200D,1F469,200D,1F467,200D,1F467 + 1F468,200D,1F468,200D,1F466 + 1F468,200D,1F468,200D,1F467 + 1F468,200D,1F468,200D,1F467,200D,1F466 + 1F468,200D,1F468,200D,1F466,200D,1F466 + 1F468,200D,1F468,200D,1F467,200D,1F467 + 1F469,200D,1F469,200D,1F466 + 1F469,200D,1F469,200D,1F467 + 1F469,200D,1F469,200D,1F467,200D,1F466 + 1F469,200D,1F469,200D,1F466,200D,1F466 + 1F469,200D,1F469,200D,1F467,200D,1F467 + 1F468,200D,1F466 + 1F468,200D,1F466,200D,1F466 + 1F468,200D,1F467 + 1F468,200D,1F467,200D,1F466 + 1F468,200D,1F467,200D,1F467 + 1F469,200D,1F466 + 1F469,200D,1F466,200D,1F466 + 1F469,200D,1F467 + 1F469,200D,1F467,200D,1F466 + 1F469,200D,1F467,200D,1F467 + + 1F933 + 1F933,1F3FB + 1F933,1F3FC + 1F933,1F3FD + 1F933,1F3FE + 1F933,1F3FF + 1F4AA + 1F4AA,1F3FB + 1F4AA,1F3FC + 1F4AA,1F3FD + 1F4AA,1F3FE + 1F4AA,1F3FF + 1F448 + 1F448,1F3FB + 1F448,1F3FC + 1F448,1F3FD + 1F448,1F3FE + 1F448,1F3FF + 1F449 + 1F449,1F3FB + 1F449,1F3FC + 1F449,1F3FD + 1F449,1F3FE + 1F449,1F3FF + 261D + 261D,1F3FB + 261D,1F3FC + 261D,1F3FD + 261D,1F3FE + 261D,1F3FF + 1F446 + 1F446,1F3FB + 1F446,1F3FC + 1F446,1F3FD + 1F446,1F3FE + 1F446,1F3FF + 1F595 + 1F595,1F3FB + 1F595,1F3FC + 1F595,1F3FD + 1F595,1F3FE + 1F595,1F3FF + 1F447 + 1F447,1F3FB + 1F447,1F3FC + 1F447,1F3FD + 1F447,1F3FE + 1F447,1F3FF + 270C + 270C,1F3FB + 270C,1F3FC + 270C,1F3FD + 270C,1F3FE + 270C,1F3FF + 1F91E + 1F91E,1F3FB + 1F91E,1F3FC + 1F91E,1F3FD + 1F91E,1F3FE + 1F91E,1F3FF + 1F596 + 1F596,1F3FB + 1F596,1F3FC + 1F596,1F3FD + 1F596,1F3FE + 1F596,1F3FF + 1F918 + 1F918,1F3FB + 1F918,1F3FC + 1F918,1F3FD + 1F918,1F3FE + 1F918,1F3FF + 1F919 + 1F919,1F3FB + 1F919,1F3FC + 1F919,1F3FD + 1F919,1F3FE + 1F919,1F3FF + 1F590 + 1F590,1F3FB + 1F590,1F3FC + 1F590,1F3FD + 1F590,1F3FE + 1F590,1F3FF + 270B + 270B,1F3FB + 270B,1F3FC + 270B,1F3FD + 270B,1F3FE + 270B,1F3FF + 1F44C + 1F44C,1F3FB + 1F44C,1F3FC + 1F44C,1F3FD + 1F44C,1F3FE + 1F44C,1F3FF + 1F44D + 1F44D,1F3FB + 1F44D,1F3FC + 1F44D,1F3FD + 1F44D,1F3FE + 1F44D,1F3FF + 1F44E + 1F44E,1F3FB + 1F44E,1F3FC + 1F44E,1F3FD + 1F44E,1F3FE + 1F44E,1F3FF + 270A + 270A,1F3FB + 270A,1F3FC + 270A,1F3FD + 270A,1F3FE + 270A,1F3FF + 1F44A + 1F44A,1F3FB + 1F44A,1F3FC + 1F44A,1F3FD + 1F44A,1F3FE + 1F44A,1F3FF + 1F91B + 1F91B,1F3FB + 1F91B,1F3FC + 1F91B,1F3FD + 1F91B,1F3FE + 1F91B,1F3FF + 1F91C + 1F91C,1F3FB + 1F91C,1F3FC + 1F91C,1F3FD + 1F91C,1F3FE + 1F91C,1F3FF + 1F91A + 1F91A,1F3FB + 1F91A,1F3FC + 1F91A,1F3FD + 1F91A,1F3FE + 1F91A,1F3FF + 1F44B + 1F44B,1F3FB + 1F44B,1F3FC + 1F44B,1F3FD + 1F44B,1F3FE + 1F44B,1F3FF + 1F91F + 1F91F,1F3FB + 1F91F,1F3FC + 1F91F,1F3FD + 1F91F,1F3FE + 1F91F,1F3FF + 270D + 270D,1F3FB + 270D,1F3FC + 270D,1F3FD + 270D,1F3FE + 270D,1F3FF + 1F44F + 1F44F,1F3FB + 1F44F,1F3FC + 1F44F,1F3FD + 1F44F,1F3FE + 1F44F,1F3FF + 1F450 + 1F450,1F3FB + 1F450,1F3FC + 1F450,1F3FD + 1F450,1F3FE + 1F450,1F3FF + 1F64C + 1F64C,1F3FB + 1F64C,1F3FC + 1F64C,1F3FD + 1F64C,1F3FE + 1F64C,1F3FF + 1F932 + 1F932,1F3FB + 1F932,1F3FC + 1F932,1F3FD + 1F932,1F3FE + 1F932,1F3FF + 1F64F + 1F64F,1F3FB + 1F64F,1F3FC + 1F64F,1F3FD + 1F64F,1F3FE + 1F64F,1F3FF + 1F91D + 1F485 + 1F485,1F3FB + 1F485,1F3FC + 1F485,1F3FD + 1F485,1F3FE + 1F485,1F3FF + 1F442 + 1F442,1F3FB + 1F442,1F3FC + 1F442,1F3FD + 1F442,1F3FE + 1F442,1F3FF + 1F443 + 1F443,1F3FB + 1F443,1F3FC + 1F443,1F3FD + 1F443,1F3FE + 1F443,1F3FF + 1F463 + 1F440 + 1F441 + 1F441,FE0F,200D,1F5E8,FE0F + 1F9E0 + 1F445 + 1F444 + + 1F48B + 1F498 + 2764 + 1F493 + 1F494 + 1F495 + 1F496 + 1F497 + 1F499 + 1F49A + 1F49B + 1F9E1 + 1F49C + 1F5A4 + 1F49D + 1F49E + 1F49F + 2763 + 1F48C + 1F4A4 + 1F4A2 + 1F4A3 + 1F4A5 + 1F4A6 + 1F4A8 + 1F4AB + 1F4AC + 1F5E8 + 1F5EF + 1F4AD + 1F573 + + 1F453 + 1F576 + 1F454 + 1F455 + 1F456 + 1F9E3 + 1F9E4 + 1F9E5 + 1F9E6 + 1F457 + 1F458 + 1F459 + 1F45A + 1F45B + 1F45C + 1F45D + 1F6CD + 1F392 + 1F45E + 1F45F + 1F460 + 1F461 + 1F462 + 1F451 + 1F452 + 1F3A9 + 1F393 + 1F9E2 + 26D1 + 1F4FF + 1F484 + 1F48D + 1F48E + + + + 1F600 + 1F601 + 1F602 + 1F923 + 1F603 + 1F604 + 1F605 + 1F606 + 1F609 + 1F60A + 1F60B + 1F60E + 1F60D + 1F618 + 1F617 + 1F619 + 1F61A + 263A + 1F642 + 1F917 + 1F929 + + 1F914 + 1F928 + 1F610 + 1F611 + 1F636 + 1F644 + 1F60F + 1F623 + 1F625 + 1F62E + 1F910 + 1F62F + 1F62A + 1F62B + 1F634 + 1F60C + 1F61B + 1F61C + 1F61D + 1F924 + 1F612 + 1F613 + 1F614 + 1F615 + 1F643 + 1F911 + 1F632 + + 2639 + 1F641 + 1F616 + 1F61E + 1F61F + 1F624 + 1F622 + 1F62D + 1F626 + 1F627 + 1F628 + 1F629 + 1F92F + 1F62C + 1F630 + 1F631 + 1F633 + 1F92A + 1F635 + 1F621 + 1F620 + 1F92C + + 1F637 + 1F912 + 1F915 + 1F922 + 1F92E + 1F927 + + 1F607 + 1F920 + 1F921 + 1F925 + 1F92B + 1F92D + 1F9D0 + 1F913 + + 1F608 + 1F47F + 1F479 + 1F47A + 1F480 + 2620 + 1F47B + 1F47D + 1F47E + 1F916 + 1F4A9 + + 1F63A + 1F638 + 1F639 + 1F63B + 1F63C + 1F63D + 1F640 + 1F63F + 1F63E + + 1F648 + 1F649 + 1F64A + + 1F476 + 1F476,1F3FB + 1F476,1F3FC + 1F476,1F3FD + 1F476,1F3FE + 1F476,1F3FF + 1F9D2 + 1F9D2,1F3FB + 1F9D2,1F3FC + 1F9D2,1F3FD + 1F9D2,1F3FE + 1F9D2,1F3FF + 1F466 + 1F466,1F3FB + 1F466,1F3FC + 1F466,1F3FD + 1F466,1F3FE + 1F466,1F3FF + 1F467 + 1F467,1F3FB + 1F467,1F3FC + 1F467,1F3FD + 1F467,1F3FE + 1F467,1F3FF + 1F9D1 + 1F9D1,1F3FB + 1F9D1,1F3FC + 1F9D1,1F3FD + 1F9D1,1F3FE + 1F9D1,1F3FF + 1F468 + 1F468,1F3FB + 1F468,1F3FC + 1F468,1F3FD + 1F468,1F3FE + 1F468,1F3FF + 1F469 + 1F469,1F3FB + 1F469,1F3FC + 1F469,1F3FD + 1F469,1F3FE + 1F469,1F3FF + 1F9D3 + 1F9D3,1F3FB + 1F9D3,1F3FC + 1F9D3,1F3FD + 1F9D3,1F3FE + 1F9D3,1F3FF + 1F474 + 1F474,1F3FB + 1F474,1F3FC + 1F474,1F3FD + 1F474,1F3FE + 1F474,1F3FF + 1F475 + 1F475,1F3FB + 1F475,1F3FC + 1F475,1F3FD + 1F475,1F3FE + 1F475,1F3FF + + 1F468,200D,2695,FE0F + 1F468,1F3FB,200D,2695,FE0F + 1F468,1F3FC,200D,2695,FE0F + 1F468,1F3FD,200D,2695,FE0F + 1F468,1F3FE,200D,2695,FE0F + 1F468,1F3FF,200D,2695,FE0F + 1F469,200D,2695,FE0F + 1F469,1F3FB,200D,2695,FE0F + 1F469,1F3FC,200D,2695,FE0F + 1F469,1F3FD,200D,2695,FE0F + 1F469,1F3FE,200D,2695,FE0F + 1F469,1F3FF,200D,2695,FE0F + 1F468,200D,1F393 + 1F468,1F3FB,200D,1F393 + 1F468,1F3FC,200D,1F393 + 1F468,1F3FD,200D,1F393 + 1F468,1F3FE,200D,1F393 + 1F468,1F3FF,200D,1F393 + 1F469,200D,1F393 + 1F469,1F3FB,200D,1F393 + 1F469,1F3FC,200D,1F393 + 1F469,1F3FD,200D,1F393 + 1F469,1F3FE,200D,1F393 + 1F469,1F3FF,200D,1F393 + 1F468,200D,1F3EB + 1F468,1F3FB,200D,1F3EB + 1F468,1F3FC,200D,1F3EB + 1F468,1F3FD,200D,1F3EB + 1F468,1F3FE,200D,1F3EB + 1F468,1F3FF,200D,1F3EB + 1F469,200D,1F3EB + 1F469,1F3FB,200D,1F3EB + 1F469,1F3FC,200D,1F3EB + 1F469,1F3FD,200D,1F3EB + 1F469,1F3FE,200D,1F3EB + 1F469,1F3FF,200D,1F3EB + 1F468,200D,2696,FE0F + 1F468,1F3FB,200D,2696,FE0F + 1F468,1F3FC,200D,2696,FE0F + 1F468,1F3FD,200D,2696,FE0F + 1F468,1F3FE,200D,2696,FE0F + 1F468,1F3FF,200D,2696,FE0F + 1F469,200D,2696,FE0F + 1F469,1F3FB,200D,2696,FE0F + 1F469,1F3FC,200D,2696,FE0F + 1F469,1F3FD,200D,2696,FE0F + 1F469,1F3FE,200D,2696,FE0F + 1F469,1F3FF,200D,2696,FE0F + 1F468,200D,1F33E + 1F468,1F3FB,200D,1F33E + 1F468,1F3FC,200D,1F33E + 1F468,1F3FD,200D,1F33E + 1F468,1F3FE,200D,1F33E + 1F468,1F3FF,200D,1F33E + 1F469,200D,1F33E + 1F469,1F3FB,200D,1F33E + 1F469,1F3FC,200D,1F33E + 1F469,1F3FD,200D,1F33E + 1F469,1F3FE,200D,1F33E + 1F469,1F3FF,200D,1F33E + 1F468,200D,1F373 + 1F468,1F3FB,200D,1F373 + 1F468,1F3FC,200D,1F373 + 1F468,1F3FD,200D,1F373 + 1F468,1F3FE,200D,1F373 + 1F468,1F3FF,200D,1F373 + 1F469,200D,1F373 + 1F469,1F3FB,200D,1F373 + 1F469,1F3FC,200D,1F373 + 1F469,1F3FD,200D,1F373 + 1F469,1F3FE,200D,1F373 + 1F469,1F3FF,200D,1F373 + 1F468,200D,1F527 + 1F468,1F3FB,200D,1F527 + 1F468,1F3FC,200D,1F527 + 1F468,1F3FD,200D,1F527 + 1F468,1F3FE,200D,1F527 + 1F468,1F3FF,200D,1F527 + 1F469,200D,1F527 + 1F469,1F3FB,200D,1F527 + 1F469,1F3FC,200D,1F527 + 1F469,1F3FD,200D,1F527 + 1F469,1F3FE,200D,1F527 + 1F469,1F3FF,200D,1F527 + 1F468,200D,1F3ED + 1F468,1F3FB,200D,1F3ED + 1F468,1F3FC,200D,1F3ED + 1F468,1F3FD,200D,1F3ED + 1F468,1F3FE,200D,1F3ED + 1F468,1F3FF,200D,1F3ED + 1F469,200D,1F3ED + 1F469,1F3FB,200D,1F3ED + 1F469,1F3FC,200D,1F3ED + 1F469,1F3FD,200D,1F3ED + 1F469,1F3FE,200D,1F3ED + 1F469,1F3FF,200D,1F3ED + 1F468,200D,1F4BC + 1F468,1F3FB,200D,1F4BC + 1F468,1F3FC,200D,1F4BC + 1F468,1F3FD,200D,1F4BC + 1F468,1F3FE,200D,1F4BC + 1F468,1F3FF,200D,1F4BC + 1F469,200D,1F4BC + 1F469,1F3FB,200D,1F4BC + 1F469,1F3FC,200D,1F4BC + 1F469,1F3FD,200D,1F4BC + 1F469,1F3FE,200D,1F4BC + 1F469,1F3FF,200D,1F4BC + 1F468,200D,1F52C + 1F468,1F3FB,200D,1F52C + 1F468,1F3FC,200D,1F52C + 1F468,1F3FD,200D,1F52C + 1F468,1F3FE,200D,1F52C + 1F468,1F3FF,200D,1F52C + 1F469,200D,1F52C + 1F469,1F3FB,200D,1F52C + 1F469,1F3FC,200D,1F52C + 1F469,1F3FD,200D,1F52C + 1F469,1F3FE,200D,1F52C + 1F469,1F3FF,200D,1F52C + 1F468,200D,1F4BB + 1F468,1F3FB,200D,1F4BB + 1F468,1F3FC,200D,1F4BB + 1F468,1F3FD,200D,1F4BB + 1F468,1F3FE,200D,1F4BB + 1F468,1F3FF,200D,1F4BB + 1F469,200D,1F4BB + 1F469,1F3FB,200D,1F4BB + 1F469,1F3FC,200D,1F4BB + 1F469,1F3FD,200D,1F4BB + 1F469,1F3FE,200D,1F4BB + 1F469,1F3FF,200D,1F4BB + 1F468,200D,1F3A4 + 1F468,1F3FB,200D,1F3A4 + 1F468,1F3FC,200D,1F3A4 + 1F468,1F3FD,200D,1F3A4 + 1F468,1F3FE,200D,1F3A4 + 1F468,1F3FF,200D,1F3A4 + 1F469,200D,1F3A4 + 1F469,1F3FB,200D,1F3A4 + 1F469,1F3FC,200D,1F3A4 + 1F469,1F3FD,200D,1F3A4 + 1F469,1F3FE,200D,1F3A4 + 1F469,1F3FF,200D,1F3A4 + 1F468,200D,1F3A8 + 1F468,1F3FB,200D,1F3A8 + 1F468,1F3FC,200D,1F3A8 + 1F468,1F3FD,200D,1F3A8 + 1F468,1F3FE,200D,1F3A8 + 1F468,1F3FF,200D,1F3A8 + 1F469,200D,1F3A8 + 1F469,1F3FB,200D,1F3A8 + 1F469,1F3FC,200D,1F3A8 + 1F469,1F3FD,200D,1F3A8 + 1F469,1F3FE,200D,1F3A8 + 1F469,1F3FF,200D,1F3A8 + 1F468,200D,2708,FE0F + 1F468,1F3FB,200D,2708,FE0F + 1F468,1F3FC,200D,2708,FE0F + 1F468,1F3FD,200D,2708,FE0F + 1F468,1F3FE,200D,2708,FE0F + 1F468,1F3FF,200D,2708,FE0F + 1F469,200D,2708,FE0F + 1F469,1F3FB,200D,2708,FE0F + 1F469,1F3FC,200D,2708,FE0F + 1F469,1F3FD,200D,2708,FE0F + 1F469,1F3FE,200D,2708,FE0F + 1F469,1F3FF,200D,2708,FE0F + 1F468,200D,1F680 + 1F468,1F3FB,200D,1F680 + 1F468,1F3FC,200D,1F680 + 1F468,1F3FD,200D,1F680 + 1F468,1F3FE,200D,1F680 + 1F468,1F3FF,200D,1F680 + 1F469,200D,1F680 + 1F469,1F3FB,200D,1F680 + 1F469,1F3FC,200D,1F680 + 1F469,1F3FD,200D,1F680 + 1F469,1F3FE,200D,1F680 + 1F469,1F3FF,200D,1F680 + 1F468,200D,1F692 + 1F468,1F3FB,200D,1F692 + 1F468,1F3FC,200D,1F692 + 1F468,1F3FD,200D,1F692 + 1F468,1F3FE,200D,1F692 + 1F468,1F3FF,200D,1F692 + 1F469,200D,1F692 + 1F469,1F3FB,200D,1F692 + 1F469,1F3FC,200D,1F692 + 1F469,1F3FD,200D,1F692 + 1F469,1F3FE,200D,1F692 + 1F469,1F3FF,200D,1F692 + 1F46E + 1F46E,1F3FB + 1F46E,1F3FC + 1F46E,1F3FD + 1F46E,1F3FE + 1F46E,1F3FF + 1F46E,200D,2642,FE0F + 1F46E,1F3FB,200D,2642,FE0F + 1F46E,1F3FC,200D,2642,FE0F + 1F46E,1F3FD,200D,2642,FE0F + 1F46E,1F3FE,200D,2642,FE0F + 1F46E,1F3FF,200D,2642,FE0F + 1F46E,200D,2640,FE0F + 1F46E,1F3FB,200D,2640,FE0F + 1F46E,1F3FC,200D,2640,FE0F + 1F46E,1F3FD,200D,2640,FE0F + 1F46E,1F3FE,200D,2640,FE0F + 1F46E,1F3FF,200D,2640,FE0F + 1F575 + 1F575,1F3FB + 1F575,1F3FC + 1F575,1F3FD + 1F575,1F3FE + 1F575,1F3FF + 1F575,FE0F,200D,2642,FE0F + 1F575,1F3FB,200D,2642,FE0F + 1F575,1F3FC,200D,2642,FE0F + 1F575,1F3FD,200D,2642,FE0F + 1F575,1F3FE,200D,2642,FE0F + 1F575,1F3FF,200D,2642,FE0F + 1F575,FE0F,200D,2640,FE0F + 1F575,1F3FB,200D,2640,FE0F + 1F575,1F3FC,200D,2640,FE0F + 1F575,1F3FD,200D,2640,FE0F + 1F575,1F3FE,200D,2640,FE0F + 1F575,1F3FF,200D,2640,FE0F + 1F482 + 1F482,1F3FB + 1F482,1F3FC + 1F482,1F3FD + 1F482,1F3FE + 1F482,1F3FF + 1F482,200D,2642,FE0F + 1F482,1F3FB,200D,2642,FE0F + 1F482,1F3FC,200D,2642,FE0F + 1F482,1F3FD,200D,2642,FE0F + 1F482,1F3FE,200D,2642,FE0F + 1F482,1F3FF,200D,2642,FE0F + 1F482,200D,2640,FE0F + 1F482,1F3FB,200D,2640,FE0F + 1F482,1F3FC,200D,2640,FE0F + 1F482,1F3FD,200D,2640,FE0F + 1F482,1F3FE,200D,2640,FE0F + 1F482,1F3FF,200D,2640,FE0F + 1F477 + 1F477,1F3FB + 1F477,1F3FC + 1F477,1F3FD + 1F477,1F3FE + 1F477,1F3FF + 1F477,200D,2642,FE0F + 1F477,1F3FB,200D,2642,FE0F + 1F477,1F3FC,200D,2642,FE0F + 1F477,1F3FD,200D,2642,FE0F + 1F477,1F3FE,200D,2642,FE0F + 1F477,1F3FF,200D,2642,FE0F + 1F477,200D,2640,FE0F + 1F477,1F3FB,200D,2640,FE0F + 1F477,1F3FC,200D,2640,FE0F + 1F477,1F3FD,200D,2640,FE0F + 1F477,1F3FE,200D,2640,FE0F + 1F477,1F3FF,200D,2640,FE0F + 1F934 + 1F934,1F3FB + 1F934,1F3FC + 1F934,1F3FD + 1F934,1F3FE + 1F934,1F3FF + 1F478 + 1F478,1F3FB + 1F478,1F3FC + 1F478,1F3FD + 1F478,1F3FE + 1F478,1F3FF + 1F473 + 1F473,1F3FB + 1F473,1F3FC + 1F473,1F3FD + 1F473,1F3FE + 1F473,1F3FF + 1F473,200D,2642,FE0F + 1F473,1F3FB,200D,2642,FE0F + 1F473,1F3FC,200D,2642,FE0F + 1F473,1F3FD,200D,2642,FE0F + 1F473,1F3FE,200D,2642,FE0F + 1F473,1F3FF,200D,2642,FE0F + 1F473,200D,2640,FE0F + 1F473,1F3FB,200D,2640,FE0F + 1F473,1F3FC,200D,2640,FE0F + 1F473,1F3FD,200D,2640,FE0F + 1F473,1F3FE,200D,2640,FE0F + 1F473,1F3FF,200D,2640,FE0F + 1F472 + 1F472,1F3FB + 1F472,1F3FC + 1F472,1F3FD + 1F472,1F3FE + 1F472,1F3FF + 1F9D5 + 1F9D5,1F3FB + 1F9D5,1F3FC + 1F9D5,1F3FD + 1F9D5,1F3FE + 1F9D5,1F3FF + 1F9D4 + 1F9D4,1F3FB + 1F9D4,1F3FC + 1F9D4,1F3FD + 1F9D4,1F3FE + 1F9D4,1F3FF + 1F471 + 1F471,1F3FB + 1F471,1F3FC + 1F471,1F3FD + 1F471,1F3FE + 1F471,1F3FF + 1F471,200D,2642,FE0F + 1F471,1F3FB,200D,2642,FE0F + 1F471,1F3FC,200D,2642,FE0F + 1F471,1F3FD,200D,2642,FE0F + 1F471,1F3FE,200D,2642,FE0F + 1F471,1F3FF,200D,2642,FE0F + 1F471,200D,2640,FE0F + 1F471,1F3FB,200D,2640,FE0F + 1F471,1F3FC,200D,2640,FE0F + 1F471,1F3FD,200D,2640,FE0F + 1F471,1F3FE,200D,2640,FE0F + 1F471,1F3FF,200D,2640,FE0F + 1F935 + 1F935,1F3FB + 1F935,1F3FC + 1F935,1F3FD + 1F935,1F3FE + 1F935,1F3FF + 1F470 + 1F470,1F3FB + 1F470,1F3FC + 1F470,1F3FD + 1F470,1F3FE + 1F470,1F3FF + 1F930 + 1F930,1F3FB + 1F930,1F3FC + 1F930,1F3FD + 1F930,1F3FE + 1F930,1F3FF + 1F931 + 1F931,1F3FB + 1F931,1F3FC + 1F931,1F3FD + 1F931,1F3FE + 1F931,1F3FF + + 1F47C + 1F47C,1F3FB + 1F47C,1F3FC + 1F47C,1F3FD + 1F47C,1F3FE + 1F47C,1F3FF + 1F385 + 1F385,1F3FB + 1F385,1F3FC + 1F385,1F3FD + 1F385,1F3FE + 1F385,1F3FF + 1F936 + 1F936,1F3FB + 1F936,1F3FC + 1F936,1F3FD + 1F936,1F3FE + 1F936,1F3FF + 1F9D9 + 1F9D9,1F3FB + 1F9D9,1F3FC + 1F9D9,1F3FD + 1F9D9,1F3FE + 1F9D9,1F3FF + 1F9D9,200D,2640,FE0F + 1F9D9,1F3FB,200D,2640,FE0F + 1F9D9,1F3FC,200D,2640,FE0F + 1F9D9,1F3FD,200D,2640,FE0F + 1F9D9,1F3FE,200D,2640,FE0F + 1F9D9,1F3FF,200D,2640,FE0F + 1F9D9,200D,2642,FE0F + 1F9D9,1F3FB,200D,2642,FE0F + 1F9D9,1F3FC,200D,2642,FE0F + 1F9D9,1F3FD,200D,2642,FE0F + 1F9D9,1F3FE,200D,2642,FE0F + 1F9D9,1F3FF,200D,2642,FE0F + 1F9DA + 1F9DA,1F3FB + 1F9DA,1F3FC + 1F9DA,1F3FD + 1F9DA,1F3FE + 1F9DA,1F3FF + 1F9DA,200D,2640,FE0F + 1F9DA,1F3FB,200D,2640,FE0F + 1F9DA,1F3FC,200D,2640,FE0F + 1F9DA,1F3FD,200D,2640,FE0F + 1F9DA,1F3FE,200D,2640,FE0F + 1F9DA,1F3FF,200D,2640,FE0F + 1F9DA,200D,2642,FE0F + 1F9DA,1F3FB,200D,2642,FE0F + 1F9DA,1F3FC,200D,2642,FE0F + 1F9DA,1F3FD,200D,2642,FE0F + 1F9DA,1F3FE,200D,2642,FE0F + 1F9DA,1F3FF,200D,2642,FE0F + 1F9DB + 1F9DB,1F3FB + 1F9DB,1F3FC + 1F9DB,1F3FD + 1F9DB,1F3FE + 1F9DB,1F3FF + 1F9DB,200D,2640,FE0F + 1F9DB,1F3FB,200D,2640,FE0F + 1F9DB,1F3FC,200D,2640,FE0F + 1F9DB,1F3FD,200D,2640,FE0F + 1F9DB,1F3FE,200D,2640,FE0F + 1F9DB,1F3FF,200D,2640,FE0F + 1F9DB,200D,2642,FE0F + 1F9DB,1F3FB,200D,2642,FE0F + 1F9DB,1F3FC,200D,2642,FE0F + 1F9DB,1F3FD,200D,2642,FE0F + 1F9DB,1F3FE,200D,2642,FE0F + 1F9DB,1F3FF,200D,2642,FE0F + 1F9DC + 1F9DC,1F3FB + 1F9DC,1F3FC + 1F9DC,1F3FD + 1F9DC,1F3FE + 1F9DC,1F3FF + 1F9DC,200D,2640,FE0F + 1F9DC,1F3FB,200D,2640,FE0F + 1F9DC,1F3FC,200D,2640,FE0F + 1F9DC,1F3FD,200D,2640,FE0F + 1F9DC,1F3FE,200D,2640,FE0F + 1F9DC,1F3FF,200D,2640,FE0F + 1F9DC,200D,2642,FE0F + 1F9DC,1F3FB,200D,2642,FE0F + 1F9DC,1F3FC,200D,2642,FE0F + 1F9DC,1F3FD,200D,2642,FE0F + 1F9DC,1F3FE,200D,2642,FE0F + 1F9DC,1F3FF,200D,2642,FE0F + 1F9DD + 1F9DD,1F3FB + 1F9DD,1F3FC + 1F9DD,1F3FD + 1F9DD,1F3FE + 1F9DD,1F3FF + 1F9DD,200D,2640,FE0F + 1F9DD,1F3FB,200D,2640,FE0F + 1F9DD,1F3FC,200D,2640,FE0F + 1F9DD,1F3FD,200D,2640,FE0F + 1F9DD,1F3FE,200D,2640,FE0F + 1F9DD,1F3FF,200D,2640,FE0F + 1F9DD,200D,2642,FE0F + 1F9DD,1F3FB,200D,2642,FE0F + 1F9DD,1F3FC,200D,2642,FE0F + 1F9DD,1F3FD,200D,2642,FE0F + 1F9DD,1F3FE,200D,2642,FE0F + 1F9DD,1F3FF,200D,2642,FE0F + 1F9DE + 1F9DE,200D,2640,FE0F + 1F9DE,200D,2642,FE0F + 1F9DF + 1F9DF,200D,2640,FE0F + 1F9DF,200D,2642,FE0F + + 1F64D + 1F64D,1F3FB + 1F64D,1F3FC + 1F64D,1F3FD + 1F64D,1F3FE + 1F64D,1F3FF + 1F64D,200D,2642,FE0F + 1F64D,1F3FB,200D,2642,FE0F + 1F64D,1F3FC,200D,2642,FE0F + 1F64D,1F3FD,200D,2642,FE0F + 1F64D,1F3FE,200D,2642,FE0F + 1F64D,1F3FF,200D,2642,FE0F + 1F64D,200D,2640,FE0F + 1F64D,1F3FB,200D,2640,FE0F + 1F64D,1F3FC,200D,2640,FE0F + 1F64D,1F3FD,200D,2640,FE0F + 1F64D,1F3FE,200D,2640,FE0F + 1F64D,1F3FF,200D,2640,FE0F + 1F64E + 1F64E,1F3FB + 1F64E,1F3FC + 1F64E,1F3FD + 1F64E,1F3FE + 1F64E,1F3FF + 1F64E,200D,2642,FE0F + 1F64E,1F3FB,200D,2642,FE0F + 1F64E,1F3FC,200D,2642,FE0F + 1F64E,1F3FD,200D,2642,FE0F + 1F64E,1F3FE,200D,2642,FE0F + 1F64E,1F3FF,200D,2642,FE0F + 1F64E,200D,2640,FE0F + 1F64E,1F3FB,200D,2640,FE0F + 1F64E,1F3FC,200D,2640,FE0F + 1F64E,1F3FD,200D,2640,FE0F + 1F64E,1F3FE,200D,2640,FE0F + 1F64E,1F3FF,200D,2640,FE0F + 1F645 + 1F645,1F3FB + 1F645,1F3FC + 1F645,1F3FD + 1F645,1F3FE + 1F645,1F3FF + 1F645,200D,2642,FE0F + 1F645,1F3FB,200D,2642,FE0F + 1F645,1F3FC,200D,2642,FE0F + 1F645,1F3FD,200D,2642,FE0F + 1F645,1F3FE,200D,2642,FE0F + 1F645,1F3FF,200D,2642,FE0F + 1F645,200D,2640,FE0F + 1F645,1F3FB,200D,2640,FE0F + 1F645,1F3FC,200D,2640,FE0F + 1F645,1F3FD,200D,2640,FE0F + 1F645,1F3FE,200D,2640,FE0F + 1F645,1F3FF,200D,2640,FE0F + 1F646 + 1F646,1F3FB + 1F646,1F3FC + 1F646,1F3FD + 1F646,1F3FE + 1F646,1F3FF + 1F646,200D,2642,FE0F + 1F646,1F3FB,200D,2642,FE0F + 1F646,1F3FC,200D,2642,FE0F + 1F646,1F3FD,200D,2642,FE0F + 1F646,1F3FE,200D,2642,FE0F + 1F646,1F3FF,200D,2642,FE0F + 1F646,200D,2640,FE0F + 1F646,1F3FB,200D,2640,FE0F + 1F646,1F3FC,200D,2640,FE0F + 1F646,1F3FD,200D,2640,FE0F + 1F646,1F3FE,200D,2640,FE0F + 1F646,1F3FF,200D,2640,FE0F + 1F481 + 1F481,1F3FB + 1F481,1F3FC + 1F481,1F3FD + 1F481,1F3FE + 1F481,1F3FF + 1F481,200D,2642,FE0F + 1F481,1F3FB,200D,2642,FE0F + 1F481,1F3FC,200D,2642,FE0F + 1F481,1F3FD,200D,2642,FE0F + 1F481,1F3FE,200D,2642,FE0F + 1F481,1F3FF,200D,2642,FE0F + 1F481,200D,2640,FE0F + 1F481,1F3FB,200D,2640,FE0F + 1F481,1F3FC,200D,2640,FE0F + 1F481,1F3FD,200D,2640,FE0F + 1F481,1F3FE,200D,2640,FE0F + 1F481,1F3FF,200D,2640,FE0F + 1F64B + 1F64B,1F3FB + 1F64B,1F3FC + 1F64B,1F3FD + 1F64B,1F3FE + 1F64B,1F3FF + 1F64B,200D,2642,FE0F + 1F64B,1F3FB,200D,2642,FE0F + 1F64B,1F3FC,200D,2642,FE0F + 1F64B,1F3FD,200D,2642,FE0F + 1F64B,1F3FE,200D,2642,FE0F + 1F64B,1F3FF,200D,2642,FE0F + 1F64B,200D,2640,FE0F + 1F64B,1F3FB,200D,2640,FE0F + 1F64B,1F3FC,200D,2640,FE0F + 1F64B,1F3FD,200D,2640,FE0F + 1F64B,1F3FE,200D,2640,FE0F + 1F64B,1F3FF,200D,2640,FE0F + 1F647 + 1F647,1F3FB + 1F647,1F3FC + 1F647,1F3FD + 1F647,1F3FE + 1F647,1F3FF + 1F647,200D,2642,FE0F + 1F647,1F3FB,200D,2642,FE0F + 1F647,1F3FC,200D,2642,FE0F + 1F647,1F3FD,200D,2642,FE0F + 1F647,1F3FE,200D,2642,FE0F + 1F647,1F3FF,200D,2642,FE0F + 1F647,200D,2640,FE0F + 1F647,1F3FB,200D,2640,FE0F + 1F647,1F3FC,200D,2640,FE0F + 1F647,1F3FD,200D,2640,FE0F + 1F647,1F3FE,200D,2640,FE0F + 1F647,1F3FF,200D,2640,FE0F + 1F926 + 1F926,1F3FB + 1F926,1F3FC + 1F926,1F3FD + 1F926,1F3FE + 1F926,1F3FF + 1F926,200D,2642,FE0F + 1F926,1F3FB,200D,2642,FE0F + 1F926,1F3FC,200D,2642,FE0F + 1F926,1F3FD,200D,2642,FE0F + 1F926,1F3FE,200D,2642,FE0F + 1F926,1F3FF,200D,2642,FE0F + 1F926,200D,2640,FE0F + 1F926,1F3FB,200D,2640,FE0F + 1F926,1F3FC,200D,2640,FE0F + 1F926,1F3FD,200D,2640,FE0F + 1F926,1F3FE,200D,2640,FE0F + 1F926,1F3FF,200D,2640,FE0F + 1F937 + 1F937,1F3FB + 1F937,1F3FC + 1F937,1F3FD + 1F937,1F3FE + 1F937,1F3FF + 1F937,200D,2642,FE0F + 1F937,1F3FB,200D,2642,FE0F + 1F937,1F3FC,200D,2642,FE0F + 1F937,1F3FD,200D,2642,FE0F + 1F937,1F3FE,200D,2642,FE0F + 1F937,1F3FF,200D,2642,FE0F + 1F937,200D,2640,FE0F + 1F937,1F3FB,200D,2640,FE0F + 1F937,1F3FC,200D,2640,FE0F + 1F937,1F3FD,200D,2640,FE0F + 1F937,1F3FE,200D,2640,FE0F + 1F937,1F3FF,200D,2640,FE0F + + 1F486 + 1F486,1F3FB + 1F486,1F3FC + 1F486,1F3FD + 1F486,1F3FE + 1F486,1F3FF + 1F486,200D,2642,FE0F + 1F486,1F3FB,200D,2642,FE0F + 1F486,1F3FC,200D,2642,FE0F + 1F486,1F3FD,200D,2642,FE0F + 1F486,1F3FE,200D,2642,FE0F + 1F486,1F3FF,200D,2642,FE0F + 1F486,200D,2640,FE0F + 1F486,1F3FB,200D,2640,FE0F + 1F486,1F3FC,200D,2640,FE0F + 1F486,1F3FD,200D,2640,FE0F + 1F486,1F3FE,200D,2640,FE0F + 1F486,1F3FF,200D,2640,FE0F + 1F487 + 1F487,1F3FB + 1F487,1F3FC + 1F487,1F3FD + 1F487,1F3FE + 1F487,1F3FF + 1F487,200D,2642,FE0F + 1F487,1F3FB,200D,2642,FE0F + 1F487,1F3FC,200D,2642,FE0F + 1F487,1F3FD,200D,2642,FE0F + 1F487,1F3FE,200D,2642,FE0F + 1F487,1F3FF,200D,2642,FE0F + 1F487,200D,2640,FE0F + 1F487,1F3FB,200D,2640,FE0F + 1F487,1F3FC,200D,2640,FE0F + 1F487,1F3FD,200D,2640,FE0F + 1F487,1F3FE,200D,2640,FE0F + 1F487,1F3FF,200D,2640,FE0F + 1F6B6 + 1F6B6,1F3FB + 1F6B6,1F3FC + 1F6B6,1F3FD + 1F6B6,1F3FE + 1F6B6,1F3FF + 1F6B6,200D,2642,FE0F + 1F6B6,1F3FB,200D,2642,FE0F + 1F6B6,1F3FC,200D,2642,FE0F + 1F6B6,1F3FD,200D,2642,FE0F + 1F6B6,1F3FE,200D,2642,FE0F + 1F6B6,1F3FF,200D,2642,FE0F + 1F6B6,200D,2640,FE0F + 1F6B6,1F3FB,200D,2640,FE0F + 1F6B6,1F3FC,200D,2640,FE0F + 1F6B6,1F3FD,200D,2640,FE0F + 1F6B6,1F3FE,200D,2640,FE0F + 1F6B6,1F3FF,200D,2640,FE0F + 1F3C3 + 1F3C3,1F3FB + 1F3C3,1F3FC + 1F3C3,1F3FD + 1F3C3,1F3FE + 1F3C3,1F3FF + 1F3C3,200D,2642,FE0F + 1F3C3,1F3FB,200D,2642,FE0F + 1F3C3,1F3FC,200D,2642,FE0F + 1F3C3,1F3FD,200D,2642,FE0F + 1F3C3,1F3FE,200D,2642,FE0F + 1F3C3,1F3FF,200D,2642,FE0F + 1F3C3,200D,2640,FE0F + 1F3C3,1F3FB,200D,2640,FE0F + 1F3C3,1F3FC,200D,2640,FE0F + 1F3C3,1F3FD,200D,2640,FE0F + 1F3C3,1F3FE,200D,2640,FE0F + 1F3C3,1F3FF,200D,2640,FE0F + 1F483 + 1F483,1F3FB + 1F483,1F3FC + 1F483,1F3FD + 1F483,1F3FE + 1F483,1F3FF + 1F57A + 1F57A,1F3FB + 1F57A,1F3FC + 1F57A,1F3FD + 1F57A,1F3FE + 1F57A,1F3FF + 1F46F + 1F46F,200D,2642,FE0F + 1F46F,200D,2640,FE0F + 1F9D6 + 1F9D6,1F3FB + 1F9D6,1F3FC + 1F9D6,1F3FD + 1F9D6,1F3FE + 1F9D6,1F3FF + 1F9D6,200D,2640,FE0F + 1F9D6,1F3FB,200D,2640,FE0F + 1F9D6,1F3FC,200D,2640,FE0F + 1F9D6,1F3FD,200D,2640,FE0F + 1F9D6,1F3FE,200D,2640,FE0F + 1F9D6,1F3FF,200D,2640,FE0F + 1F9D6,200D,2642,FE0F + 1F9D6,1F3FB,200D,2642,FE0F + 1F9D6,1F3FC,200D,2642,FE0F + 1F9D6,1F3FD,200D,2642,FE0F + 1F9D6,1F3FE,200D,2642,FE0F + 1F9D6,1F3FF,200D,2642,FE0F + 1F9D7 + 1F9D7,1F3FB + 1F9D7,1F3FC + 1F9D7,1F3FD + 1F9D7,1F3FE + 1F9D7,1F3FF + 1F9D7,200D,2640,FE0F + 1F9D7,1F3FB,200D,2640,FE0F + 1F9D7,1F3FC,200D,2640,FE0F + 1F9D7,1F3FD,200D,2640,FE0F + 1F9D7,1F3FE,200D,2640,FE0F + 1F9D7,1F3FF,200D,2640,FE0F + 1F9D7,200D,2642,FE0F + 1F9D7,1F3FB,200D,2642,FE0F + 1F9D7,1F3FC,200D,2642,FE0F + 1F9D7,1F3FD,200D,2642,FE0F + 1F9D7,1F3FE,200D,2642,FE0F + 1F9D7,1F3FF,200D,2642,FE0F + 1F9D8 + 1F9D8,1F3FB + 1F9D8,1F3FC + 1F9D8,1F3FD + 1F9D8,1F3FE + 1F9D8,1F3FF + 1F9D8,200D,2640,FE0F + 1F9D8,1F3FB,200D,2640,FE0F + 1F9D8,1F3FC,200D,2640,FE0F + 1F9D8,1F3FD,200D,2640,FE0F + 1F9D8,1F3FE,200D,2640,FE0F + 1F9D8,1F3FF,200D,2640,FE0F + 1F9D8,200D,2642,FE0F + 1F9D8,1F3FB,200D,2642,FE0F + 1F9D8,1F3FC,200D,2642,FE0F + 1F9D8,1F3FD,200D,2642,FE0F + 1F9D8,1F3FE,200D,2642,FE0F + 1F9D8,1F3FF,200D,2642,FE0F + 1F6C0 + 1F6C0,1F3FB + 1F6C0,1F3FC + 1F6C0,1F3FD + 1F6C0,1F3FE + 1F6C0,1F3FF + 1F6CC + 1F6CC,1F3FB + 1F6CC,1F3FC + 1F6CC,1F3FD + 1F6CC,1F3FE + 1F6CC,1F3FF + 1F574 + 1F574,1F3FB + 1F574,1F3FC + 1F574,1F3FD + 1F574,1F3FE + 1F574,1F3FF + 1F5E3 + 1F464 + 1F465 + + 1F93A + 1F3C7 + 1F3C7,1F3FB + 1F3C7,1F3FC + 1F3C7,1F3FD + 1F3C7,1F3FE + 1F3C7,1F3FF + 26F7 + 1F3C2 + 1F3C2,1F3FB + 1F3C2,1F3FC + 1F3C2,1F3FD + 1F3C2,1F3FE + 1F3C2,1F3FF + 1F3CC + 1F3CC,1F3FB + 1F3CC,1F3FC + 1F3CC,1F3FD + 1F3CC,1F3FE + 1F3CC,1F3FF + 1F3CC,FE0F,200D,2642,FE0F + 1F3CC,1F3FB,200D,2642,FE0F + 1F3CC,1F3FC,200D,2642,FE0F + 1F3CC,1F3FD,200D,2642,FE0F + 1F3CC,1F3FE,200D,2642,FE0F + 1F3CC,1F3FF,200D,2642,FE0F + 1F3CC,FE0F,200D,2640,FE0F + 1F3CC,1F3FB,200D,2640,FE0F + 1F3CC,1F3FC,200D,2640,FE0F + 1F3CC,1F3FD,200D,2640,FE0F + 1F3CC,1F3FE,200D,2640,FE0F + 1F3CC,1F3FF,200D,2640,FE0F + 1F3C4 + 1F3C4,1F3FB + 1F3C4,1F3FC + 1F3C4,1F3FD + 1F3C4,1F3FE + 1F3C4,1F3FF + 1F3C4,200D,2642,FE0F + 1F3C4,1F3FB,200D,2642,FE0F + 1F3C4,1F3FC,200D,2642,FE0F + 1F3C4,1F3FD,200D,2642,FE0F + 1F3C4,1F3FE,200D,2642,FE0F + 1F3C4,1F3FF,200D,2642,FE0F + 1F3C4,200D,2640,FE0F + 1F3C4,1F3FB,200D,2640,FE0F + 1F3C4,1F3FC,200D,2640,FE0F + 1F3C4,1F3FD,200D,2640,FE0F + 1F3C4,1F3FE,200D,2640,FE0F + 1F3C4,1F3FF,200D,2640,FE0F + 1F6A3 + 1F6A3,1F3FB + 1F6A3,1F3FC + 1F6A3,1F3FD + 1F6A3,1F3FE + 1F6A3,1F3FF + 1F6A3,200D,2642,FE0F + 1F6A3,1F3FB,200D,2642,FE0F + 1F6A3,1F3FC,200D,2642,FE0F + 1F6A3,1F3FD,200D,2642,FE0F + 1F6A3,1F3FE,200D,2642,FE0F + 1F6A3,1F3FF,200D,2642,FE0F + 1F6A3,200D,2640,FE0F + 1F6A3,1F3FB,200D,2640,FE0F + 1F6A3,1F3FC,200D,2640,FE0F + 1F6A3,1F3FD,200D,2640,FE0F + 1F6A3,1F3FE,200D,2640,FE0F + 1F6A3,1F3FF,200D,2640,FE0F + 1F3CA + 1F3CA,1F3FB + 1F3CA,1F3FC + 1F3CA,1F3FD + 1F3CA,1F3FE + 1F3CA,1F3FF + 1F3CA,200D,2642,FE0F + 1F3CA,1F3FB,200D,2642,FE0F + 1F3CA,1F3FC,200D,2642,FE0F + 1F3CA,1F3FD,200D,2642,FE0F + 1F3CA,1F3FE,200D,2642,FE0F + 1F3CA,1F3FF,200D,2642,FE0F + 1F3CA,200D,2640,FE0F + 1F3CA,1F3FB,200D,2640,FE0F + 1F3CA,1F3FC,200D,2640,FE0F + 1F3CA,1F3FD,200D,2640,FE0F + 1F3CA,1F3FE,200D,2640,FE0F + 1F3CA,1F3FF,200D,2640,FE0F + 26F9 + 26F9,1F3FB + 26F9,1F3FC + 26F9,1F3FD + 26F9,1F3FE + 26F9,1F3FF + 26F9,FE0F,200D,2642,FE0F + 26F9,1F3FB,200D,2642,FE0F + 26F9,1F3FC,200D,2642,FE0F + 26F9,1F3FD,200D,2642,FE0F + 26F9,1F3FE,200D,2642,FE0F + 26F9,1F3FF,200D,2642,FE0F + 26F9,FE0F,200D,2640,FE0F + 26F9,1F3FB,200D,2640,FE0F + 26F9,1F3FC,200D,2640,FE0F + 26F9,1F3FD,200D,2640,FE0F + 26F9,1F3FE,200D,2640,FE0F + 26F9,1F3FF,200D,2640,FE0F + 1F3CB + 1F3CB,1F3FB + 1F3CB,1F3FC + 1F3CB,1F3FD + 1F3CB,1F3FE + 1F3CB,1F3FF + 1F3CB,FE0F,200D,2642,FE0F + 1F3CB,1F3FB,200D,2642,FE0F + 1F3CB,1F3FC,200D,2642,FE0F + 1F3CB,1F3FD,200D,2642,FE0F + 1F3CB,1F3FE,200D,2642,FE0F + 1F3CB,1F3FF,200D,2642,FE0F + 1F3CB,FE0F,200D,2640,FE0F + 1F3CB,1F3FB,200D,2640,FE0F + 1F3CB,1F3FC,200D,2640,FE0F + 1F3CB,1F3FD,200D,2640,FE0F + 1F3CB,1F3FE,200D,2640,FE0F + 1F3CB,1F3FF,200D,2640,FE0F + 1F6B4 + 1F6B4,1F3FB + 1F6B4,1F3FC + 1F6B4,1F3FD + 1F6B4,1F3FE + 1F6B4,1F3FF + 1F6B4,200D,2642,FE0F + 1F6B4,1F3FB,200D,2642,FE0F + 1F6B4,1F3FC,200D,2642,FE0F + 1F6B4,1F3FD,200D,2642,FE0F + 1F6B4,1F3FE,200D,2642,FE0F + 1F6B4,1F3FF,200D,2642,FE0F + 1F6B4,200D,2640,FE0F + 1F6B4,1F3FB,200D,2640,FE0F + 1F6B4,1F3FC,200D,2640,FE0F + 1F6B4,1F3FD,200D,2640,FE0F + 1F6B4,1F3FE,200D,2640,FE0F + 1F6B4,1F3FF,200D,2640,FE0F + 1F6B5 + 1F6B5,1F3FB + 1F6B5,1F3FC + 1F6B5,1F3FD + 1F6B5,1F3FE + 1F6B5,1F3FF + 1F6B5,200D,2642,FE0F + 1F6B5,1F3FB,200D,2642,FE0F + 1F6B5,1F3FC,200D,2642,FE0F + 1F6B5,1F3FD,200D,2642,FE0F + 1F6B5,1F3FE,200D,2642,FE0F + 1F6B5,1F3FF,200D,2642,FE0F + 1F6B5,200D,2640,FE0F + 1F6B5,1F3FB,200D,2640,FE0F + 1F6B5,1F3FC,200D,2640,FE0F + 1F6B5,1F3FD,200D,2640,FE0F + 1F6B5,1F3FE,200D,2640,FE0F + 1F6B5,1F3FF,200D,2640,FE0F + 1F3CE + 1F3CD + 1F938 + 1F938,1F3FB + 1F938,1F3FC + 1F938,1F3FD + 1F938,1F3FE + 1F938,1F3FF + 1F938,200D,2642,FE0F + 1F938,1F3FB,200D,2642,FE0F + 1F938,1F3FC,200D,2642,FE0F + 1F938,1F3FD,200D,2642,FE0F + 1F938,1F3FE,200D,2642,FE0F + 1F938,1F3FF,200D,2642,FE0F + 1F938,200D,2640,FE0F + 1F938,1F3FB,200D,2640,FE0F + 1F938,1F3FC,200D,2640,FE0F + 1F938,1F3FD,200D,2640,FE0F + 1F938,1F3FE,200D,2640,FE0F + 1F938,1F3FF,200D,2640,FE0F + 1F93C + 1F93C,200D,2642,FE0F + 1F93C,200D,2640,FE0F + 1F93D + 1F93D,1F3FB + 1F93D,1F3FC + 1F93D,1F3FD + 1F93D,1F3FE + 1F93D,1F3FF + 1F93D,200D,2642,FE0F + 1F93D,1F3FB,200D,2642,FE0F + 1F93D,1F3FC,200D,2642,FE0F + 1F93D,1F3FD,200D,2642,FE0F + 1F93D,1F3FE,200D,2642,FE0F + 1F93D,1F3FF,200D,2642,FE0F + 1F93D,200D,2640,FE0F + 1F93D,1F3FB,200D,2640,FE0F + 1F93D,1F3FC,200D,2640,FE0F + 1F93D,1F3FD,200D,2640,FE0F + 1F93D,1F3FE,200D,2640,FE0F + 1F93D,1F3FF,200D,2640,FE0F + 1F93E + 1F93E,1F3FB + 1F93E,1F3FC + 1F93E,1F3FD + 1F93E,1F3FE + 1F93E,1F3FF + 1F93E,200D,2642,FE0F + 1F93E,1F3FB,200D,2642,FE0F + 1F93E,1F3FC,200D,2642,FE0F + 1F93E,1F3FD,200D,2642,FE0F + 1F93E,1F3FE,200D,2642,FE0F + 1F93E,1F3FF,200D,2642,FE0F + 1F93E,200D,2640,FE0F + 1F93E,1F3FB,200D,2640,FE0F + 1F93E,1F3FC,200D,2640,FE0F + 1F93E,1F3FD,200D,2640,FE0F + 1F93E,1F3FE,200D,2640,FE0F + 1F93E,1F3FF,200D,2640,FE0F + 1F939 + 1F939,1F3FB + 1F939,1F3FC + 1F939,1F3FD + 1F939,1F3FE + 1F939,1F3FF + 1F939,200D,2642,FE0F + 1F939,1F3FB,200D,2642,FE0F + 1F939,1F3FC,200D,2642,FE0F + 1F939,1F3FD,200D,2642,FE0F + 1F939,1F3FE,200D,2642,FE0F + 1F939,1F3FF,200D,2642,FE0F + 1F939,200D,2640,FE0F + 1F939,1F3FB,200D,2640,FE0F + 1F939,1F3FC,200D,2640,FE0F + 1F939,1F3FD,200D,2640,FE0F + 1F939,1F3FE,200D,2640,FE0F + 1F939,1F3FF,200D,2640,FE0F + + 1F46B + 1F46C + 1F46D + 1F48F + 1F469,200D,2764,FE0F,200D,1F48B,200D,1F468 + 1F468,200D,2764,FE0F,200D,1F48B,200D,1F468 + 1F469,200D,2764,FE0F,200D,1F48B,200D,1F469 + 1F491 + 1F469,200D,2764,FE0F,200D,1F468 + 1F468,200D,2764,FE0F,200D,1F468 + 1F469,200D,2764,FE0F,200D,1F469 + 1F46A + 1F468,200D,1F469,200D,1F466 + 1F468,200D,1F469,200D,1F467 + 1F468,200D,1F469,200D,1F467,200D,1F466 + 1F468,200D,1F469,200D,1F466,200D,1F466 + 1F468,200D,1F469,200D,1F467,200D,1F467 + 1F468,200D,1F468,200D,1F466 + 1F468,200D,1F468,200D,1F467 + 1F468,200D,1F468,200D,1F467,200D,1F466 + 1F468,200D,1F468,200D,1F466,200D,1F466 + 1F468,200D,1F468,200D,1F467,200D,1F467 + 1F469,200D,1F469,200D,1F466 + 1F469,200D,1F469,200D,1F467 + 1F469,200D,1F469,200D,1F467,200D,1F466 + 1F469,200D,1F469,200D,1F466,200D,1F466 + 1F469,200D,1F469,200D,1F467,200D,1F467 + 1F468,200D,1F466 + 1F468,200D,1F466,200D,1F466 + 1F468,200D,1F467 + 1F468,200D,1F467,200D,1F466 + 1F468,200D,1F467,200D,1F467 + 1F469,200D,1F466 + 1F469,200D,1F466,200D,1F466 + 1F469,200D,1F467 + 1F469,200D,1F467,200D,1F466 + 1F469,200D,1F467,200D,1F467 + + 1F933 + 1F933,1F3FB + 1F933,1F3FC + 1F933,1F3FD + 1F933,1F3FE + 1F933,1F3FF + 1F4AA + 1F4AA,1F3FB + 1F4AA,1F3FC + 1F4AA,1F3FD + 1F4AA,1F3FE + 1F4AA,1F3FF + 1F448 + 1F448,1F3FB + 1F448,1F3FC + 1F448,1F3FD + 1F448,1F3FE + 1F448,1F3FF + 1F449 + 1F449,1F3FB + 1F449,1F3FC + 1F449,1F3FD + 1F449,1F3FE + 1F449,1F3FF + 261D + 261D,1F3FB + 261D,1F3FC + 261D,1F3FD + 261D,1F3FE + 261D,1F3FF + 1F446 + 1F446,1F3FB + 1F446,1F3FC + 1F446,1F3FD + 1F446,1F3FE + 1F446,1F3FF + 1F595 + 1F595,1F3FB + 1F595,1F3FC + 1F595,1F3FD + 1F595,1F3FE + 1F595,1F3FF + 1F447 + 1F447,1F3FB + 1F447,1F3FC + 1F447,1F3FD + 1F447,1F3FE + 1F447,1F3FF + 270C + 270C,1F3FB + 270C,1F3FC + 270C,1F3FD + 270C,1F3FE + 270C,1F3FF + 1F91E + 1F91E,1F3FB + 1F91E,1F3FC + 1F91E,1F3FD + 1F91E,1F3FE + 1F91E,1F3FF + 1F596 + 1F596,1F3FB + 1F596,1F3FC + 1F596,1F3FD + 1F596,1F3FE + 1F596,1F3FF + 1F918 + 1F918,1F3FB + 1F918,1F3FC + 1F918,1F3FD + 1F918,1F3FE + 1F918,1F3FF + 1F919 + 1F919,1F3FB + 1F919,1F3FC + 1F919,1F3FD + 1F919,1F3FE + 1F919,1F3FF + 1F590 + 1F590,1F3FB + 1F590,1F3FC + 1F590,1F3FD + 1F590,1F3FE + 1F590,1F3FF + 270B + 270B,1F3FB + 270B,1F3FC + 270B,1F3FD + 270B,1F3FE + 270B,1F3FF + 1F44C + 1F44C,1F3FB + 1F44C,1F3FC + 1F44C,1F3FD + 1F44C,1F3FE + 1F44C,1F3FF + 1F44D + 1F44D,1F3FB + 1F44D,1F3FC + 1F44D,1F3FD + 1F44D,1F3FE + 1F44D,1F3FF + 1F44E + 1F44E,1F3FB + 1F44E,1F3FC + 1F44E,1F3FD + 1F44E,1F3FE + 1F44E,1F3FF + 270A + 270A,1F3FB + 270A,1F3FC + 270A,1F3FD + 270A,1F3FE + 270A,1F3FF + 1F44A + 1F44A,1F3FB + 1F44A,1F3FC + 1F44A,1F3FD + 1F44A,1F3FE + 1F44A,1F3FF + 1F91B + 1F91B,1F3FB + 1F91B,1F3FC + 1F91B,1F3FD + 1F91B,1F3FE + 1F91B,1F3FF + 1F91C + 1F91C,1F3FB + 1F91C,1F3FC + 1F91C,1F3FD + 1F91C,1F3FE + 1F91C,1F3FF + 1F91A + 1F91A,1F3FB + 1F91A,1F3FC + 1F91A,1F3FD + 1F91A,1F3FE + 1F91A,1F3FF + 1F44B + 1F44B,1F3FB + 1F44B,1F3FC + 1F44B,1F3FD + 1F44B,1F3FE + 1F44B,1F3FF + 1F91F + 1F91F,1F3FB + 1F91F,1F3FC + 1F91F,1F3FD + 1F91F,1F3FE + 1F91F,1F3FF + 270D + 270D,1F3FB + 270D,1F3FC + 270D,1F3FD + 270D,1F3FE + 270D,1F3FF + 1F44F + 1F44F,1F3FB + 1F44F,1F3FC + 1F44F,1F3FD + 1F44F,1F3FE + 1F44F,1F3FF + 1F450 + 1F450,1F3FB + 1F450,1F3FC + 1F450,1F3FD + 1F450,1F3FE + 1F450,1F3FF + 1F64C + 1F64C,1F3FB + 1F64C,1F3FC + 1F64C,1F3FD + 1F64C,1F3FE + 1F64C,1F3FF + 1F932 + 1F932,1F3FB + 1F932,1F3FC + 1F932,1F3FD + 1F932,1F3FE + 1F932,1F3FF + 1F64F + 1F64F,1F3FB + 1F64F,1F3FC + 1F64F,1F3FD + 1F64F,1F3FE + 1F64F,1F3FF + 1F91D + 1F485 + 1F485,1F3FB + 1F485,1F3FC + 1F485,1F3FD + 1F485,1F3FE + 1F485,1F3FF + 1F442 + 1F442,1F3FB + 1F442,1F3FC + 1F442,1F3FD + 1F442,1F3FE + 1F442,1F3FF + 1F443 + 1F443,1F3FB + 1F443,1F3FC + 1F443,1F3FD + 1F443,1F3FE + 1F443,1F3FF + 1F463 + 1F440 + 1F441 + 1F441,FE0F,200D,1F5E8,FE0F + 1F9E0 + 1F445 + 1F444 + + 1F48B + 1F498 + 2764 + 1F493 + 1F494 + 1F495 + 1F496 + 1F497 + 1F499 + 1F49A + 1F49B + 1F9E1 + 1F49C + 1F5A4 + 1F49D + 1F49E + 1F49F + 2763 + 1F48C + 1F4A4 + 1F4A2 + 1F4A3 + 1F4A5 + 1F4A6 + 1F4A8 + 1F4AB + 1F4AC + 1F5E8 + 1F5EF + 1F4AD + 1F573 + + 1F453 + 1F576 + 1F454 + 1F455 + 1F456 + 1F9E3 + 1F9E4 + 1F9E5 + 1F9E6 + 1F457 + 1F458 + 1F459 + 1F45A + 1F45B + 1F45C + 1F45D + 1F6CD + 1F392 + 1F45E + 1F45F + 1F460 + 1F461 + 1F462 + 1F451 + 1F452 + 1F3A9 + 1F393 + 1F9E2 + 26D1 + 1F4FF + 1F484 + 1F48D + 1F48E + + + + + 1F347 + 1F348 + 1F349 + 1F34A + 1F34B + 1F34C + 1F34D + 1F34E + 1F34F + 1F350 + 1F351 + 1F352 + 1F353 + 1F95D + 1F345 + 1F965 + + 1F951 + 1F346 + 1F954 + 1F955 + 1F33D + 1F336 + 1F952 + 1F966 + 1F344 + 1F95C + 1F330 + + 1F35E + 1F950 + 1F956 + 1F968 + 1F95E + 1F9C0 + 1F356 + 1F357 + 1F969 + 1F953 + 1F354 + 1F35F + 1F355 + 1F32D + 1F96A + 1F32E + 1F32F + 1F959 + 1F95A + 1F373 + 1F958 + 1F372 + 1F963 + 1F957 + 1F37F + 1F96B + + 1F371 + 1F358 + 1F359 + 1F35A + 1F35B + 1F35C + 1F35D + 1F360 + 1F362 + 1F363 + 1F364 + 1F365 + 1F361 + 1F95F + 1F960 + 1F961 + + 1F366 + 1F367 + 1F368 + 1F369 + 1F36A + 1F382 + 1F370 + 1F967 + 1F36B + 1F36C + 1F36D + 1F36E + 1F36F + + 1F37C + 1F95B + 2615 + 1F375 + 1F376 + 1F37E + 1F377 + 1F378 + 1F379 + 1F37A + 1F37B + 1F942 + 1F943 + 1F964 + + 1F962 + 1F37D + 1F374 + 1F944 + 1F52A + 1F3FA + + + + 1F507 + 1F508 + 1F509 + 1F50A + 1F4E2 + 1F4E3 + 1F4EF + 1F514 + 1F515 + + 1F3BC + 1F3B5 + 1F3B6 + 1F399 + 1F39A + 1F39B + 1F3A4 + 1F3A7 + 1F4FB + + 1F3B7 + 1F3B8 + 1F3B9 + 1F3BA + 1F3BB + 1F941 + + 1F4F1 + 1F4F2 + 260E + 1F4DE + 1F4DF + 1F4E0 + + 1F50B + 1F50C + 1F4BB + 1F5A5 + 1F5A8 + 2328 + 1F5B1 + 1F5B2 + 1F4BD + 1F4BE + 1F4BF + 1F4C0 + + 1F3A5 + 1F39E + 1F4FD + 1F3AC + 1F4FA + 1F4F7 + 1F4F8 + 1F4F9 + 1F4FC + 1F50D + 1F50E + 1F52C + 1F52D + 1F4E1 + 1F56F + 1F4A1 + 1F526 + 1F3EE + + 1F4D4 + 1F4D5 + 1F4D6 + 1F4D7 + 1F4D8 + 1F4D9 + 1F4DA + 1F4D3 + 1F4D2 + 1F4C3 + 1F4DC + 1F4C4 + 1F4F0 + 1F5DE + 1F4D1 + 1F516 + 1F3F7 + + 1F4B0 + 1F4B4 + 1F4B5 + 1F4B6 + 1F4B7 + 1F4B8 + 1F4B3 + 1F4B9 + 1F4B1 + 1F4B2 + + 2709 + 1F4E7 + 1F4E8 + 1F4E9 + 1F4E4 + 1F4E5 + 1F4E6 + 1F4EB + 1F4EA + 1F4EC + 1F4ED + 1F4EE + 1F5F3 + + 270F + 2712 + 1F58B + 1F58A + 1F58C + 1F58D + 1F4DD + + 1F4BC + 1F4C1 + 1F4C2 + 1F5C2 + 1F4C5 + 1F4C6 + 1F5D2 + 1F5D3 + 1F4C7 + 1F4C8 + 1F4C9 + 1F4CA + 1F4CB + 1F4CC + 1F4CD + 1F4CE + 1F587 + 1F4CF + 1F4D0 + 2702 + 1F5C3 + 1F5C4 + 1F5D1 + + 1F512 + 1F513 + 1F50F + 1F510 + 1F511 + 1F5DD + + 1F528 + 26CF + 2692 + 1F6E0 + 1F5E1 + 2694 + 1F52B + 1F3F9 + 1F6E1 + 1F527 + 1F529 + 2699 + 1F5DC + 2697 + 2696 + 1F517 + 26D3 + + 1F489 + 1F48A + + 1F6AC + 26B0 + 26B1 + 1F5FF + 1F6E2 + 1F52E + 1F6D2 + + + + 1F383 + 1F384 + 1F386 + 1F387 + 2728 + 1F388 + 1F389 + 1F38A + 1F38B + 1F38D + 1F38E + 1F38F + 1F390 + 1F391 + 1F380 + 1F381 + 1F397 + 1F39F + 1F3AB + + 1F396 + 1F3C6 + 1F3C5 + 1F947 + 1F948 + 1F949 + + 26BD + 26BE + 1F3C0 + 1F3D0 + 1F3C8 + 1F3C9 + 1F3BE + 1F3B1 + 1F3B3 + 1F3CF + 1F3D1 + 1F3D2 + 1F3D3 + 1F3F8 + 1F94A + 1F94B + 1F945 + 1F3AF + 26F3 + 26F8 + 1F3A3 + 1F3BD + 1F3BF + 1F6F7 + 1F94C + + 1F3AE + 1F579 + 1F3B2 + 2660 + 2665 + 2666 + 2663 + 1F0CF + 1F004 + 1F3B4 + + + + 1F30D + 1F30E + 1F30F + 1F310 + 1F5FA + 1F5FE + + 1F3D4 + 26F0 + 1F30B + 1F5FB + 1F3D5 + 1F3D6 + 1F3DC + 1F3DD + 1F3DE + + 1F3DF + 1F3DB + 1F3D7 + 1F3D8 + 1F3D9 + 1F3DA + 1F3E0 + 1F3E1 + 1F3E2 + 1F3E3 + 1F3E4 + 1F3E5 + 1F3E6 + 1F3E8 + 1F3E9 + 1F3EA + 1F3EB + 1F3EC + 1F3ED + 1F3EF + 1F3F0 + 1F492 + 1F5FC + 1F5FD + + 26EA + 1F54C + 1F54D + 26E9 + 1F54B + + 26F2 + 26FA + 1F301 + 1F303 + 1F304 + 1F305 + 1F306 + 1F307 + 1F309 + 2668 + 1F30C + 1F3A0 + 1F3A1 + 1F3A2 + 1F488 + 1F3AA + 1F3AD + 1F5BC + 1F3A8 + 1F3B0 + + 1F682 + 1F683 + 1F684 + 1F685 + 1F686 + 1F687 + 1F688 + 1F689 + 1F68A + 1F69D + 1F69E + 1F68B + 1F68C + 1F68D + 1F68E + 1F690 + 1F691 + 1F692 + 1F693 + 1F694 + 1F695 + 1F696 + 1F697 + 1F698 + 1F699 + 1F69A + 1F69B + 1F69C + 1F6B2 + 1F6F4 + 1F6F5 + 1F68F + 1F6E3 + 1F6E4 + 26FD + 1F6A8 + 1F6A5 + 1F6A6 + 1F6A7 + 1F6D1 + + 2693 + 26F5 + 1F6F6 + 1F6A4 + 1F6F3 + 26F4 + 1F6E5 + 1F6A2 + + 2708 + 1F6E9 + 1F6EB + 1F6EC + 1F4BA + 1F681 + 1F69F + 1F6A0 + 1F6A1 + 1F6F0 + 1F680 + 1F6F8 + + 1F6CE + 1F6AA + 1F6CF + 1F6CB + 1F6BD + 1F6BF + 1F6C1 + + 231B + 23F3 + 231A + 23F0 + 23F1 + 23F2 + 1F570 + 1F55B + 1F567 + 1F550 + 1F55C + 1F551 + 1F55D + 1F552 + 1F55E + 1F553 + 1F55F + 1F554 + 1F560 + 1F555 + 1F561 + 1F556 + 1F562 + 1F557 + 1F563 + 1F558 + 1F564 + 1F559 + 1F565 + 1F55A + 1F566 + + 1F311 + 1F312 + 1F313 + 1F314 + 1F315 + 1F316 + 1F317 + 1F318 + 1F319 + 1F31A + 1F31B + 1F31C + 1F321 + 2600 + 1F31D + 1F31E + 2B50 + 1F31F + 1F320 + 2601 + 26C5 + 26C8 + 1F324 + 1F325 + 1F326 + 1F327 + 1F328 + 1F329 + 1F32A + 1F32B + 1F32C + 1F300 + 1F308 + 1F302 + 2602 + 2614 + 26F1 + 26A1 + 2744 + 2603 + 26C4 + 2604 + 1F525 + 1F4A7 + 1F30A + + + 1F3C1 + 1F6A9 + 1F38C + 1F3F4 + 1F3F3 + 1F3F3,FE0F,200D,1F308 + + 1F1E6,1F1E8 + 1F1E6,1F1E9 + 1F1E6,1F1EA + 1F1E6,1F1EB + 1F1E6,1F1EC + 1F1E6,1F1EE + 1F1E6,1F1F1 + 1F1E6,1F1F2 + 1F1E6,1F1F4 + 1F1E6,1F1F6 + 1F1E6,1F1F7 + 1F1E6,1F1F8 + 1F1E6,1F1F9 + 1F1E6,1F1FA + 1F1E6,1F1FC + 1F1E6,1F1FD + 1F1E6,1F1FF + 1F1E7,1F1E6 + 1F1E7,1F1E7 + 1F1E7,1F1E9 + 1F1E7,1F1EA + 1F1E7,1F1EB + 1F1E7,1F1EC + 1F1E7,1F1ED + 1F1E7,1F1EE + 1F1E7,1F1EF + 1F1E7,1F1F1 + 1F1E7,1F1F2 + 1F1E7,1F1F3 + 1F1E7,1F1F4 + 1F1E7,1F1F6 + 1F1E7,1F1F7 + 1F1E7,1F1F8 + 1F1E7,1F1F9 + 1F1E7,1F1FB + 1F1E7,1F1FC + 1F1E7,1F1FE + 1F1E7,1F1FF + 1F1E8,1F1E6 + 1F1E8,1F1E8 + 1F1E8,1F1E9 + 1F1E8,1F1EB + 1F1E8,1F1EC + 1F1E8,1F1ED + 1F1E8,1F1EE + 1F1E8,1F1F0 + 1F1E8,1F1F1 + 1F1E8,1F1F2 + 1F1E8,1F1F3 + 1F1E8,1F1F4 + 1F1E8,1F1F5 + 1F1E8,1F1F7 + 1F1E8,1F1FA + 1F1E8,1F1FB + 1F1E8,1F1FC + 1F1E8,1F1FD + 1F1E8,1F1FE + 1F1E8,1F1FF + 1F1E9,1F1EA + 1F1E9,1F1EC + 1F1E9,1F1EF + 1F1E9,1F1F0 + 1F1E9,1F1F2 + 1F1E9,1F1F4 + 1F1E9,1F1FF + 1F1EA,1F1E6 + 1F1EA,1F1E8 + 1F1EA,1F1EA + 1F1EA,1F1EC + 1F1EA,1F1ED + 1F1EA,1F1F7 + 1F1EA,1F1F8 + 1F1EA,1F1F9 + 1F1EA,1F1FA + 1F1EB,1F1EE + 1F1EB,1F1EF + 1F1EB,1F1F0 + 1F1EB,1F1F2 + 1F1EB,1F1F4 + 1F1EB,1F1F7 + 1F1EC,1F1E6 + 1F1EC,1F1E7 + 1F1EC,1F1E9 + 1F1EC,1F1EA + 1F1EC,1F1EB + 1F1EC,1F1EC + 1F1EC,1F1ED + 1F1EC,1F1EE + 1F1EC,1F1F1 + 1F1EC,1F1F2 + 1F1EC,1F1F3 + 1F1EC,1F1F5 + 1F1EC,1F1F6 + 1F1EC,1F1F7 + 1F1EC,1F1F8 + 1F1EC,1F1F9 + 1F1EC,1F1FA + 1F1EC,1F1FC + 1F1EC,1F1FE + 1F1ED,1F1F0 + 1F1ED,1F1F2 + 1F1ED,1F1F3 + 1F1ED,1F1F7 + 1F1ED,1F1F9 + 1F1ED,1F1FA + 1F1EE,1F1E8 + 1F1EE,1F1E9 + 1F1EE,1F1EA + 1F1EE,1F1F1 + 1F1EE,1F1F2 + 1F1EE,1F1F3 + 1F1EE,1F1F4 + 1F1EE,1F1F6 + 1F1EE,1F1F7 + 1F1EE,1F1F8 + 1F1EE,1F1F9 + 1F1EF,1F1EA + 1F1EF,1F1F2 + 1F1EF,1F1F4 + 1F1EF,1F1F5 + 1F1F0,1F1EA + 1F1F0,1F1EC + 1F1F0,1F1ED + 1F1F0,1F1EE + 1F1F0,1F1F2 + 1F1F0,1F1F3 + 1F1F0,1F1F5 + 1F1F0,1F1F7 + 1F1F0,1F1FC + 1F1F0,1F1FE + 1F1F0,1F1FF + 1F1F1,1F1E6 + 1F1F1,1F1E7 + 1F1F1,1F1E8 + 1F1F1,1F1EE + 1F1F1,1F1F0 + 1F1F1,1F1F7 + 1F1F1,1F1F8 + 1F1F1,1F1F9 + 1F1F1,1F1FA + 1F1F1,1F1FB + 1F1F1,1F1FE + 1F1F2,1F1E6 + 1F1F2,1F1E8 + 1F1F2,1F1E9 + 1F1F2,1F1EA + 1F1F2,1F1EB + 1F1F2,1F1EC + 1F1F2,1F1ED + 1F1F2,1F1F0 + 1F1F2,1F1F1 + 1F1F2,1F1F2 + 1F1F2,1F1F3 + 1F1F2,1F1F4 + 1F1F2,1F1F5 + 1F1F2,1F1F6 + 1F1F2,1F1F7 + 1F1F2,1F1F8 + 1F1F2,1F1F9 + 1F1F2,1F1FA + 1F1F2,1F1FB + 1F1F2,1F1FC + 1F1F2,1F1FD + 1F1F2,1F1FE + 1F1F2,1F1FF + 1F1F3,1F1E6 + 1F1F3,1F1E8 + 1F1F3,1F1EA + 1F1F3,1F1EB + 1F1F3,1F1EC + 1F1F3,1F1EE + 1F1F3,1F1F1 + 1F1F3,1F1F4 + 1F1F3,1F1F5 + 1F1F3,1F1F7 + 1F1F3,1F1FA + 1F1F3,1F1FF + 1F1F4,1F1F2 + 1F1F5,1F1E6 + 1F1F5,1F1EA + 1F1F5,1F1EB + 1F1F5,1F1EC + 1F1F5,1F1ED + 1F1F5,1F1F0 + 1F1F5,1F1F1 + 1F1F5,1F1F2 + 1F1F5,1F1F3 + 1F1F5,1F1F7 + 1F1F5,1F1F8 + 1F1F5,1F1F9 + 1F1F5,1F1FC + 1F1F5,1F1FE + 1F1F6,1F1E6 + 1F1F7,1F1EA + 1F1F7,1F1F4 + 1F1F7,1F1F8 + 1F1F7,1F1FA + 1F1F7,1F1FC + 1F1F8,1F1E6 + 1F1F8,1F1E7 + 1F1F8,1F1E8 + 1F1F8,1F1E9 + 1F1F8,1F1EA + 1F1F8,1F1EC + 1F1F8,1F1ED + 1F1F8,1F1EE + 1F1F8,1F1EF + 1F1F8,1F1F0 + 1F1F8,1F1F1 + 1F1F8,1F1F2 + 1F1F8,1F1F3 + 1F1F8,1F1F4 + 1F1F8,1F1F7 + 1F1F8,1F1F8 + 1F1F8,1F1F9 + 1F1F8,1F1FB + 1F1F8,1F1FD + 1F1F8,1F1FE + 1F1F8,1F1FF + 1F1F9,1F1E6 + 1F1F9,1F1E8 + 1F1F9,1F1E9 + 1F1F9,1F1EB + 1F1F9,1F1EC + 1F1F9,1F1ED + 1F1F9,1F1EF + 1F1F9,1F1F0 + 1F1F9,1F1F1 + 1F1F9,1F1F2 + 1F1F9,1F1F3 + 1F1F9,1F1F4 + 1F1F9,1F1F7 + 1F1F9,1F1F9 + 1F1F9,1F1FB + 1F1F9,1F1FC + 1F1F9,1F1FF + 1F1FA,1F1E6 + 1F1FA,1F1EC + 1F1FA,1F1F2 + 1F1FA,1F1F3 + 1F1FA,1F1F8 + 1F1FA,1F1FE + 1F1FA,1F1FF + 1F1FB,1F1E6 + 1F1FB,1F1E8 + 1F1FB,1F1EA + 1F1FB,1F1EC + 1F1FB,1F1EE + 1F1FB,1F1F3 + 1F1FB,1F1FA + 1F1FC,1F1EB + 1F1FC,1F1F8 + 1F1FD,1F1F0 + 1F1FE,1F1EA + 1F1FE,1F1F9 + 1F1FF,1F1E6 + 1F1FF,1F1F2 + 1F1FF,1F1FC + + 1F3F4,E0067,E0062,E0065,E006E,E0067,E007F + 1F3F4,E0067,E0062,E0073,E0063,E0074,E007F + 1F3F4,E0067,E0062,E0077,E006C,E0073,E007F + + diff --git a/app/src/main/res/values-v19/spinner-style.xml b/app/src/main/res/values-v19/spinner-style.xml new file mode 100644 index 00000000..a6999057 --- /dev/null +++ b/app/src/main/res/values-v19/spinner-style.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/app/src/main/res/values-v21/platform-theme.xml b/app/src/main/res/values-v21/platform-theme.xml new file mode 100644 index 00000000..58763fbd --- /dev/null +++ b/app/src/main/res/values-v21/platform-theme.xml @@ -0,0 +1,25 @@ + + + + + + diff --git a/app/src/main/res/values-vi/bools.xml b/app/src/main/res/values-vi/bools.xml new file mode 100644 index 00000000..840d20c2 --- /dev/null +++ b/app/src/main/res/values-vi/bools.xml @@ -0,0 +1,24 @@ + + + + + true + diff --git a/app/src/main/res/values-vi/strings-action-keys.xml b/app/src/main/res/values-vi/strings-action-keys.xml new file mode 100644 index 00000000..3c406088 --- /dev/null +++ b/app/src/main/res/values-vi/strings-action-keys.xml @@ -0,0 +1,31 @@ + + + + + "Tìm" + "Tiếp" + "Trước" + "Xong" + "Gửi" + "Tìm kiếm" + "Tdừng" + "Đợi" + diff --git a/app/src/main/res/values-vi/strings-config-important-notice.xml b/app/src/main/res/values-vi/strings-config-important-notice.xml new file mode 100644 index 00000000..828f0b60 --- /dev/null +++ b/app/src/main/res/values-vi/strings-config-important-notice.xml @@ -0,0 +1,24 @@ + + + + + "Tìm hiểu từ thư từ trao đổi và dữ liệu đã nhập của bạn để cải tiến đề xuất" + diff --git a/app/src/main/res/values-vi/strings-emoji-descriptions.xml b/app/src/main/res/values-vi/strings-emoji-descriptions.xml new file mode 100644 index 00000000..b7397574 --- /dev/null +++ b/app/src/main/res/values-vi/strings-emoji-descriptions.xml @@ -0,0 +1,1767 @@ + + + + + + + "Biểu tượng bản quyền" + "Biểu tượng đã đăng ký" + "Hai dấu chấm than" + "Dấu chấm than dấu hỏi" + "Biểu tượng nhãn hiệu thương mại" + "Nguồn thông tin" + "Mũi tên trái phải" + "Mũi tên lên xuống" + "Mũi tên tây bắc" + "Mũi tên đông bắc" + "Mũi tên đông nam" + "Mũi tên tây nam" + "Mũi tên sang trái có móc" + "Mũi tên sang phải có móc" + "Đồng hồ" + "Đồng hồ cát" + + + + + "Hai tam giác màu đen trỏ sang phải" + "Hai tam giác màu đen trỏ sang trái" + "Hai tam giác màu đen trỏ lên" + "Hai tam giác màu đen trỏ xuống" + + + + + + + "Đồng hồ báo thức" + + + + + "Đồng hồ cát với cát đang chảy" + + + + + + + "Chữ m hoa trong vòng tròn" + "Hình vuông đen nhỏ" + "Hình vuông trắng nhỏ" + "Hình tam giác đen trỏ sang phải" + "Hình tam giác đen trỏ sang trái" + "Hình vuông trắng trung bình" + "Hình vuông đen trung bình" + "Hình vuông trắng nhỏ trung bình" + "Hình vuông đen nhỏ trung bình" + "Mặt trời màu đen với các tia" + "Mây" + + + + + + + "Điện thoại màu đen" + "Hộp phiếu có dấu kiểm" + "Ô với giọt mưa" + "Đồ uống nóng" + + + "Ngón trỏ chỉ lên màu trắng" + + + + + + + + + + + + + + + + + + + "Mặt cười màu trắng" + "Cung Bạch dương" + "Cung Kim ngưu" + "Cung Song sinh" + "Cung Cự giải" + "Cung Sư tử" + "Cung Xử nữ" + "Cung Thiên bình" + "Cung Bọ cạp" + "Cung Nhân mã" + "Cung Ma kết" + "Cung Bảo bình" + "Cung Song ngư" + "Quân bích đen" + "Quân tép đen" + "Quân cơ đen" + "Quân rô đen" + "Suối nước nóng" + "Biểu tượng tái chế chung màu đen" + "Biểu tượng xe lăn" + + + "Mỏ neo" + + + + + + + + + + + + + "Biển báo nguy hiểm" + "Biểu tượng điện cao thế" + "Vòng tròn trắng trung bình" + "Vòng tròn đen trung bình" + + + + + "Quả bóng đá" + "Bóng chày" + "Người tuyết không có tuyết" + "Mặt trời phía sau đám mây" + + + "Chòm sao" + + + + + + + "Cấm vào" + + + "Nhà thờ" + + + + + "Vòi phun nước" + "Cờ trong lỗ" + + + "Thuyền buồm" + + + + + + + "Lều" + "Bơm nhiên liệu" + "Kéo đen" + "Dấu kiểm trắng đậm" + "Máy bay" + "Phong bì" + "Giơ quả đấm" + "Giơ tay" + "Tay chữ V" + + + "Bút chì" + "Đầu ngòi bút đen" + "Dấu kiểm đậm" + "Dấu nhân x đậm" + + + + + "Tia lửa" + "Dấu hoa thị 8 que" + "Sao màu đen 8 que" + "Bông tuyết" + "Tia lửa" + "Dấu gạch chéo" + "Biểu tượng dấu gạch chéo hình vuông" + "Hình trang trí dấu hỏi màu đen" + "Hình trang trí dấu hỏi màu trắng" + "Hình trang trí dấu cảm thán màu trắng" + "Biểu tượng dấu cảm thán đậm" + + + "Trái tim buồn phiền" + "Dấu cộng đậm" + "Dấu trừ đậm" + "Dấu chia đậm" + "Mũi tên sang phải màu đen" + "Vòng xoắn" + "Hai vòng xoắn" + "Mũi tên trỏ sang phải rồi uốn cong lên trên" + "Mũi tên trỏ sang phải rồi uốn cong xuống dưới" + "Mũi tên đen trỏ sang trái" + "Mũi tên đen trỏ lên" + "Mũi tên đen trỏ xuống" + "Hình vuông lớn màu đen" + "Hình vuông lớn màu trắng" + "Sao trắng trung bình" + "Vòng tròn đậm lớn" + "Nét lượn sóng" + "Dấu biến thiên từng phần" + "Biểu tượng chữ tượng hình \"Chúc mừng\" vòng tròn" + "Biểu tượng chữ tượng hình \"Bí mật\" vòng tròn" + "Rồng đỏ xếp tầng Mahjong" + "Chơi bài phăng teo đen" + "Nhóm máu A" + "Nhóm máu B" + "Nhóm máu O" + "Bãi đỗ xe" + "Nhóm máu AB" + "Biểu tượng CL hình vuông" + "Biểu tượng \"Cool\" hình vuông" + "Biểu tượng \"Free\" hình vuông" + "Biểu tượng \"ID\" hình vuông" + "Biểu tượng \"New\" hình vuông" + "Biểu tượng \"N G\" hình vuông" + "Biểu tượng \"OK\" hình vuông" + "Biểu tượng \"SOS\" hình vuông" + "Biểu tượng \"Up!\" hình vuông" + "Biểu tượng \"VS\" hình vuông" + "Biểu tượng \"Here\" (theo kiểu katakana) hình vuông" + "Biểu tượng \"Service\" (theo kiểu katakana) hình vuông" + "Biểu tượng chữ tượng hình \"Miễn phí\" hình vuông" + "Biểu tượng chữ tượng hình \"Chỗ ngồi đặt trước\" hình vuông" + "Biểu tượng chữ tượng hình \"Cấm\" hình vuông" + "Biểu tượng chữ tượng hình \"Còn trống\" hình vuông" + "Biểu tượng chữ tượng hình \"Chấp nhận\" hình vuông" + "Biểu tượng chữ tượng hình \"Hết chỗ\" hình vuông" + "Biểu tượng chữ tượng hình \"Trả tiền\" hình vuông" + "Biểu tượng chữ tượng hình \"Hàng tháng\" hình vuông" + "Biểu tượng chữ tượng hình \"Đơn đăng ký\" hình vuông" + "Biểu tượng chữ tượng hình \"Giảm giá\" hình vuông" + "Biểu tượng chữ tượng hình \"Doanh nghiệp\" hình vuông" + "Biểu tượng chữ tượng hình \"Thuận lợi\" hình tròn" + "Biểu tượng chữ tượng hình \"Chấp nhận\" hình tròn" + "Gió xoáy" + "Sương mù" + "Ô đóng" + "Đêm đầy sao" + "Bình minh trên núi" + "Bình minh" + "Cảnh quan thành phố khi trời tối" + "Bình minh trên các tòa nhà" + "Cầu vồng" + "Cầu vào ban đêm" + "Sóng nước" + "Núi lửa" + "Ngân hà" + "Châu Âu-Châu Phi trên quả địa cầu" + "Châu Mỹ trên quả địa cầu" + "Châu Á-Châu Úc trên quả địa cầu" + "Quả cầu có kinh tuyến" + "Biểu tượng trăng non" + "Biểu tượng trăng lưỡi liềm" + "Biểu tượng trăng thượng tuần" + "Biểu tượng trăng khuyết" + "Biểu tượng trăng tròn" + "Biểu tượng trăng khuyết cuối tháng" + "Biểu tượng trăng hạ tuần" + "Biểu tượng trăng khuyết" + "Trăng lưỡi liềm" + "Trăng non hình khuôn mặt" + "Trăng thượng tuần hình khuôn mặt" + "Trăng hạ tuần hình khuôn mặt" + "Trăng tròn hình khuôn mặt" + "Mặt trời hình khuôn mặt" + "Sao lấp lánh" + "Sao băng" + + + + + + + + + + + + + + + + + + + + + + + + + + + "Cây hạt dẻ" + "Cây giống con" + "Cây thường xanh" + "Cây lá rộng" + "Cây cọ" + "Cây xương rồng" + + + "Hoa tulip" + "Hoa anh đào" + "Hoa hồng" + "Hoa dâm bụt" + "Hoa hướng dương" + "Hoa nở" + "Bắp ngô" + "Bông lúa" + "Thảo mộc" + "Cỏ bốn lá" + "Lá phong đỏ" + "Lá rơi" + "Lá đu đưa trong gió" + "Nấm" + "Cà chua" + "Cà tím" + "Nho" + "Quả dưa" + "Dưa hấu" + "Quả quýt" + "Chanh" + "Quả chuối" + "Quả dứa" + "Táo đỏ" + "Táo xanh" + "Lê" + "Đào" + "Quả anh đào" + "Quả dâu" + "Hamburger" + "Lát pizza" + "Thịt trên xương" + "Chân gia cầm" + "Bánh gạo" + "Nắm gạo" + "Cơm" + "Bột ca ri và gạo" + "Bát bốc hơi" + "Mì ống" + "Bánh mì" + "Khoai tây chiên kiểu Pháp" + "Khoai lang nướng" + "Bánh đoàn tử" + "Oden" + "Sushi" + "Tôm chiên" + "Bánh cá hình cuộn sóng" + "Kem mềm" + "Đá bào" + "Kem" + "Bánh rán" + "Bánh quy" + "Thanh sôcôla" + "Kẹo" + "Lollipop" + "Món sữa trứng" + "Mắt ong" + "Bánh bơ giòn" + "Hộp Bento" + "Nồi thức ăn" + "Nấu ăn" + "Dao và nĩa" + "Chèn trà không có tay cầm" + "Cốc và chai sake" + "Cốc rượu" + "Cốc cocktail" + "Đồ uống kiểu nhiệt đới" + "Cốc bia" + "Chạm cốc bia" + "Chai nhỏ" + + + + + + + "Chiếc nơ" + "Món quà được bọc" + "Bánh sinh nhật" + "Đèn ma" + "Cây Nô-en" + "Ông già Nô-en" + "Pháo hoa" + "Nến pháo hoa" + "Bóng bay" + "Pháo hoa kim tuyến" + "Quả cầu hoa giấy" + "Cây Tanabata" + "Cờ đan chéo" + "Trang trí bằng quả thông" + "Búp bê Nhật Bản" + "Cờ cá chép" + "Gió thổi" + "Khai mạc lễ ngắm trăng" + "Cặp đi học" + "Mũ tốt nghiệp" + + + + + + + + + + + + + + + "Vòng quay ngựa gỗ" + "Vòng đu quay" + "Đường sắt cho tàu trượt" + "Cần câu cá và cá" + "Micrô" + "Máy quay phim" + "Rạp chiếu phim" + "Tai nghe" + "Bảng màu họa sĩ" + "Mũ chóp cao" + "Lều trại" + "Vé" + "Bảng phân cảnh" + "Nghệ thuật biểu diễn" + "Trò chơi điện tử" + "Mũi tên trúng đích" + "Máy bán hàng tự động" + "Bi-a" + "Xúc xắc" + "Trò chơi lăn bóng gỗ" + "Chơi bài hoa" + "Nốt nhạc" + "Nhiều nốt nhạc" + "Saxophone" + "Guitar" + "Bàn phím nhạc" + "Kèn Trompet" + "Vĩ cầm" + "Điểm âm nhạc" + "Áo thun có khăn quàng cổ chạy" + "Vợt tennis và bóng" + "Trượt tuyết và ủng trượt tuyết" + "Bóng rổ và vòng" + "Cờ có kẻ ô vuông" + "Người trượt tuyết bằng ván" + "Người chạy" + "Người lướt sóng" + + + "Cúp" + "Đua ngựa" + "Bóng đá Mỹ" + "Bóng bầu dục" + "Người bơi" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "Xây nhà" + "Nhà có vườn" + "Tòa nhà văn phòng" + "Bưu điện ở Nhật Bản" + "Bưu điện ở Châu Âu" + "Bệnh viện" + "Ngân hàng" + "Máy rút tiền tự động" + "Khách sạn" + "Khách sạn tình yêu" + "Cửa hàng tiện ích" + "Trường học" + "Cửa hàng bách hóa" + "Nhà máy" + "Đèn lồng Izakaya" + "Lâu đài ở Nhật Bản" + "Lâu đài ở Châu Âu" + + + + + + + + + + + + + + + + + + + + + + + + + "Chuột cống" + "Chuột" + "Bò đực" + "Con trâu" + "Bò cái" + + + "Con báo" + "Con thỏ" + "Mèo" + "Rồng" + "Cá sấu" + "Cá voi" + "Ốc sên" + "Rắn" + "Ngựa" + "Cừu đực" + "Dê" + "Cừu" + "Khỉ" + "Gà trống" + "Gà con" + "Cún" + "Lợn" + "Lợn lòi đực" + "Voi" + "Bạch tuộc" + "Vỏ xoắn" + "Con rệp" + "Kiến" + "Ong mật" + "Bọ cánh cứng" + "Cá" + "Cá nhiệt đới" + "Cá nóc" + "Rùa" + "Gà ấp trứng" + "Gà con" + "Gà con mặt trước" + "Chim" + "Chim cánh cụt" + "Gấu túi" + "Chó xù" + "Lạc đà một bướu" + "Lạc đà hai bướu" + "Cá heo" + "Mặt chuột" + "Mặt bò đực" + "Mặt hổ" + "Mặt thỏ" + "Mặt mèo" + "Mặt rồng" + "Cá voi phun tia" + "Mặt ngựa" + "Mặt khỉ" + "Mặt cún" + "Mặt lợn" + "Mặt ếch" + "Mặt chuột đồng" + "Mặt sói" + "Mặt gấu" + "Mặt gấu trúc" + "Mũi lợn" + "Vết móng" + + + "Đôi mắt" + + + "Tai" + "Mũi" + "Miệng" + "Lưỡi" + "Ngón trỏ chỉ lên trên" + "Ngón trỏ chỉ xuống dưới" + "Ngón trỏ chỉ sang trái" + "Ngón trỏ chỉ sang phải" + "Biểu tượng quả đấm" + "Biểu tượng tay vẫy" + "Biểu tượng bàn tay ra dấu Ok" + "Biểu tượng ngón tay cái giơ lên" + "Biểu tượng ngón tay cái quay xuống" + "Biểu tượng vỗ tay" + "Biểu tượng đôi bàn tay mở" + "Vương miện" + "Mũ nữ" + "Mắt kính" + "Ca vát" + "Áo sơ mi" + "Quần bò" + "Váy" + "Kimono" + "Bộ bikini" + "Quần áo nữ" + "Ví" + "Túi xách" + "Túi đựng" + "Giày nam" + "Giày điền kinh" + "Giày cao gót" + "Xăng đan nữ" + "Giày ống nữ" + "Dấu chân" + "Hình bóng nửa người" + "Hình bóng nửa người" + "Cậu bé" + "Cô bé" + "Nam" + "Nữ" + "Gia đình" + "Đôi nam nữ nắm tay" + "Hai người đàn ông nắm tay" + "Hai người phụ nữ nắm tay" + "Cảnh sát" + "Người phụ nữ có tai thỏ" + "Cô dâu mang khăn che mặt" + "Người có tóc vàng hoe" + "Người đàn ông đội mũ gua pi mao" + "Người đàn ông đội khăn xếp" + "Người đàn ông già hơn" + "Người phụ nữ già hơn" + "Đứa trẻ" + "Công nhân xây dựng" + "Công chúa" + "Yêu quái Nhật Bản" + "Yêu tinh Nhật Bản" + "Ma" + "Thiên thần nhỏ" + "Người ngoài hành tinh" + "Quỷ ngoài hành tinh" + "Tiểu yêu" + "Sọ" + "Nhân viên tiếp tân" + "Nhân viên bảo vệ" + "Vũ công" + "Son môi" + "Sơn móng tay" + "Xoa bóp mặt" + "Cắt tóc" + "Thợ cắt tóc" + "Ống tiêm" + "Viên thuốc" + "Dấu hôn" + "Thư tình" + "Chuông" + "Ngọc" + "Nụ hôn" + "Bó hoa" + "Cặp đôi trái tim" + "Lễ cưới" + "Trái tim đập" + "Trái tim tan vỡ" + "Hai trái tim" + "Trái tim lấp lánh" + "Trái tím lớn lên" + "Trái tim có mũi tên" + "Trái tim màu xanh lam" + "Trái tim màu xanh lục" + "Trái tim màu vàng" + "Trái tim màu tía" + "Trái tim có dải băng" + "Trái tim xoay vòng" + "Trang trí trái tim" + "Hình kim cương có dấu chấm bên trong" + "Đèn điện" + "Biểu tượng giận dữ" + "Bom" + "Biểu tượng đang ngủ" + "Biểu tượng va chạm" + "Biểu trưng giọt mồ hôi vã ra" + "Giọt nhỏ" + "Biểu tượng nét gạch" + "Một đống" + "Bắp tay uốn cong" + "Biểu tượng hoa mắt" + "Bóng lời nói" + "Bóng suy nghĩ" + "Hoa trắng" + "Biểu tượng một trăm điểm" + "Túi tiền" + "Hối đoái" + "Biểu tượng đồng đô la mạnh" + "Thẻ tín dụng" + "Tiền giấy có ký hiệu đồng yên" + "Tiền giấy có ký hiệu đô la" + "Tiền giấy có ký hiệu đồng Euro" + "Tiền giấy có ký hiệu bảng Anh" + "Tiền có đôi cánh" + "Biểu đồ có xu hướng đi lên và ký hiệu yen" + "Chỗ ngồi" + "Máy tính cá nhân" + "Cặp tài liệu" + "Đĩa nhỏ" + "Đĩa mềm" + "Đĩa quang" + "Dvd" + "Thư mục tệp" + "Mở thư mục tệp" + "Trang giấy uốn quăn" + "Trang ngửa lên" + "Lịch" + "Lịch xé" + "Chỉ mục thẻ" + "Biểu đồ có xu hướng đi lên" + "Biểu đồ có xu hướng đi xuống" + "Biểu đồ thanh" + "Khay nhớ tạm" + "Đinh ghim" + "Đinh ghim tròn" + "Kẹp giấy" + "Thước kẻ thẳng" + "Thước kẻ hình chữ nhật" + "Thanh dấu trang" + "Sổ cái" + "Sổ ghi chú" + "Sổ ghi chú có bìa trang trí" + "Sách đóng" + "Sách mở" + "Sách xanh lục" + "Sách xanh lam" + "Sách cam" + "Sách" + "Huy hiệu tên" + "Cuộn" + "Bản ghi nhớ" + "Bộ nhận điện thoại" + "Máy nhắn tin" + "Máy fax" + "Ăng-ten vệ tinh" + "Loa phóng thanh" + "Loa cổ động" + "Khay hộp thư đi" + "Khay hộp thư đến" + "Gói" + "Biểu tượng email" + "Phong bì thư đến" + "Phong bì có mũi tên hướng xuống dưới ở trên" + "Hộp thư đóng với cờ được hạ xuống" + "Hộp thư đóng với cờ được giương lên" + "Hộp thư mở với cờ được giương lên" + "Hộp thư mở với cờ được hạ xuống" + "Hòm thư" + "Còi bưu chính" + "Báo" + "Điện thoại di động" + "Điện thoại di động có mũi tên về phía phải ở bên trái" + "Chế độ rung" + "Tắt điện thoại di động" + "Cấm điện thoại di động" + "Ăng-ten có các vạch" + "Máy ảnh" + + + "Máy quay video" + "TV" + "Đài" + "Videocassette" + + + + + "Mũi tên xoắn về bên phải" + "Mũi tên vòng tròn mở về bên phải và về bên trái theo chiều kim đồng hồ" + "Mũi tên vòng tròn mở về bên phải và bên bên trái theo chiều kim đồng hồ có lớp phủ 1 hình tròn" + "Mũi tên hình tròn mở hướng xuống dưới và hướng lên trên theo chiều kim đồng hồ" + "Mũi tên hình tròn mở hướng xuống dưới và hướng lên trên ngược chiều kim đồng hồ" + "Biểu tượng độ sáng thấp" + "Biểu tượng độ sáng cao" + "Loa có tiếng gõ hủy" + "Loa" + "Loa có một sóng âm thanh" + "Loa có ba sóng âm thanh" + "Pin" + "Đầu cắm điện" + "Kính lúp trỏ sang trái" + "Kính lúp trỏ sang phải" + "Khóa có bút mực" + "Khóa đóng có chìa" + "Chìa khóa" + "Khóa" + "Khóa mở" + "Chuông" + "Chuông có tiếng gõ hủy" + "Dấu trang" + "Biểu tượng liên kết" + "Nút đài" + "Biểu tượng \"BACK\" có mũi tên sang trái ở trên" + "Biểu tượng \"END\" có mũi tên sang trái ở trên" + "Biểu tượng \"ON!\" có mũi tên trái phải ở trên" + "Biểu tượng \"SOON\" với mũi tên sang phải ở trên" + "Biểu tượng \"TOP\" với mũi tên hướng lên trên ở trên" + "Biểu tượng cấm người dưới 18" + "Mũ phím 10" + "Biểu tượng nhập cho chữ cái hoa la tinh" + "Biểu tượng nhập các chữ cái nhỏ la tinh" + "Biểu tượng nhập cho số" + "Nhập ký hiệu cho ký hiệu" + "Biểu tượng nhập cho chữ cái la tinh" + "Lửa" + "Đuốc điện" + "Cờ lê" + "Búa" + "Đai ốc và bu lông" + "Hocho" + "Súng lục" + "Kính hiển vi" + "Kính thiên văn" + "Quả cầu pha lê" + "Ngôi sao sáu cánh có chấm ở giữa" + "Biểu tượng tiếng Nhật cho người mới bắt đầu" + "Biểu tượng đinh ba" + "Nút hình vuông màu đen" + "Nút hình vuông màu trắng" + "Vòng tròn đỏ lớn" + "Vòng tròn xanh lam lớn" + "Hình thoi màu cam lớn" + "Hình thoi màu xanh lam lớn" + "Hình thoi màu cam nhỏ" + "Hình thoi màu xanh lam nhỏ" + "Hình tam giác đỏ một đỉnh trỏ lên" + "Tam giác đỏ một đỉnh trỏ xuống" + "Tam giác đỏ nhỏ một đỉnh trỏ lên" + "Tam giác đỏ nhỏ một đỉnh trỏ xuống" + + + + + + + + + + + + + "Mặt đồng hồ chỉ 1 giờ" + "Mặt đồng hồ chỉ 2 giờ" + "Mặt đồng hồ chỉ 3 giờ" + "Mặt đồng hồ chỉ 4 giờ" + "Mặt đồng hồ chỉ 5 giờ" + "Mặt đồng hồ chỉ 6 giờ" + "Mặt đồng hồ chỉ 7 giờ" + "Mặt đồng hồ chỉ 8 giờ" + "Mặt đồng hồ chỉ 9 giờ" + "Mặt đồng hồ chỉ 10 giờ" + "Mặt đồng hồ chỉ 11 giờ" + "Mặt đồng hồ chỉ 12 giờ" + "Mặt đồng hồ chỉ 1 giờ 30 phút" + "Mặt đồng hồ chỉ 2 giờ 30 phút" + "Mặt đồng hồ chỉ 3 giờ 30 phút" + "Mặt đồng hồ chỉ 4 giờ 30 phút" + "Mặt đồng hồ chỉ 5 giờ 30 phút" + "Mặt đồng hồ chỉ 6 giờ 30 phút" + "Mặt đồng hồ chỉ 7 giờ 30 phút" + "Mặt đồng hồ chỉ 8 giờ 30 phút" + "Mặt đồng hồ chỉ 9 giờ 30 phút" + "Mặt đồng hồ chỉ 10 giờ 30 phút" + "Mặt đồng hồ chỉ 11 giờ 30 phút" + "Mặt đồng hồ chỉ 12 giờ 30 phút" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "Đỉnh fuji" + "Tháp Tokyo" + "Tượng nữ thần tự do" + "Hình bóng của Nhật Bản" + "Moyai" + "Mặt cười" + "Khuôn mặt cười toe toét với đôi mắt cười" + "Khuôn mặt với giọt nước mắt vui mừng" + "Mặt cười với miệng há" + "Mặt cười với miệng há và đôi mắt cười" + "Mặt cười với miệng há và toát mồ hôi" + "Mặt cười với miệng há và đôi mắt nhắm chặt" + "Mặt cười với vầng hào quang" + "Mặt có sừng cười" + "Mặt nháy mắt" + "Mặt cười với đôi mắt cười" + "Mặt nhấm nháp thức ăn ngon" + "Mặt thanh thản" + "Mặt cười với đôi mắt hình trái tim" + "Mặt cười với kính râm" + "Mặt cười giả tạo" + "Mặt trung lập" + "Mặt vô cảm" + "Mặt buồn rầu" + "Mặt toát mồ hôi" + "Mặt trầm ngâm" + "Mặt bối rối" + "Mặt bực bội" + "Mặt đang hôn" + "Mặt đang gửi một nụ hôn" + "Mặt đang hôn với đôi mắt cười" + "Mặt đang hôn với đôi mắt nhắm" + "Mặt lè lưỡi" + "Mặt lè lưỡi và nháy mắt" + "Mặt lè lưỡi và đôi mắt nhắm chặt" + "Mặt thất vọng" + "Mặt lo lắng" + "Mặt giận dữ" + "Mặt hờn dỗi" + "Mặt khóc" + "Mặt kiên nhẫn" + "Mặt giận dữ" + "Mặt thất vọng nhưng thanh thản" + "Mặt cau có với miệng há" + "Khuôn mặt đau khổ" + "Mặt sợ hãi" + "Mặt mệt lử" + "Mặt buồn ngủ" + "Mặt mệt mỏi" + "Mặt nhăn nhó" + "Mặt khóc òa" + "Mặt há miệng" + "Mặt ngơ ngác" + "Mặt há miệng và toát mồ hôi" + "Mặt la hét vì sợ hãi" + "Mặt ngạc nhiên" + "Mặt xúc động" + "Mặt buồn ngủ" + "Mặt choáng váng" + "Khuôn mặt không có miệng" + "Khuôn mặt đeo khẩu trang y tế" + "Khuôn mặt mèo rạng rỡ với đôi mắt cười" + "Khuôn mặt với giọt nước mắt vui mừng" + "Khuôn mặt mèo cười với miệng há" + "Mặt mèo cười với đôi mắt hình trái tim" + "Mặt mèo với nụ cười gượng gạo" + "Khuôn mặt mèo đang hôn với mắt nhắm" + "Mặt mèo hờn dỗi" + "Mặt mèo khóc" + "Mặt mèo mệt lử" + + + + + + + + + "Khuôn mặt với cử chỉ tồi" + "Khuôn mặt với cử chỉ tốt" + "Người cúi gập mình" + "Khỉ che mắt" + "Khỉ bịt tai" + "Khỉ bịt miệng" + "Người vui vẻ giơ một tay" + "Người giơ hai tay chúc mừng" + "Người mặt cau có" + "Người mặt hờn dỗi" + "Người với hai tay gập" + "Tên lửa" + "Máy bay trực thăng" + "Đầu máy hơi nước" + "Ô tô ray" + "Tàu cao tốc" + "Tàu cao tốc có mũi hình viên đạn" + "Tàu" + "Xe điện ngầm" + "Đường sắt loại nhẹ" + "Ga" + "Xe điện" + "Tàu điện" + "Xe buýt" + "Xe buýt sắp đến" + "Ô tô điện" + "Bến xe buýt" + "Xe buýt nhỏ" + "Xe cứu thương" + "Xe chữa cháy" + "Ô tô cảnh sát" + "Ô tô cảnh sát sắp đến" + "Taxi" + "Taxi sắp đến" + "Ô tô" + "Ô tô sắp đến" + "Phương tiện giải trí" + "Xe chở hàng" + "Xe tải nối khớp" + "Máy kéo" + "Đường một ray" + "Đường sắt trên núi" + "Đường xe lửa trên cao" + "Đường cáp qua núi" + "Cáp treo" + "Tàu thủy" + "Xuồng" + "Thuyền tốc độ" + "Đèn giao thông theo chiều ngang" + "Đèn giao thông theo chiều dọc" + "Biểu tượng xây dựng" + "Ô tô cảnh sát có đèn xoay" + "Cờ tam giác trên cột trụ" + "Cửa" + "Biểu tượng cấm vào" + "Biểu tượng hút thuốc" + "Biểu tượng không hút thuốc" + "Biểu tượng vứt rác vào nơi quy định" + "Biểu tượng không vứt rác" + "Biểu tượng nước uống được" + "Biểu tượng nước không uống được" + "Xe đạp" + "Cấm xe đap" + "Người đi xe đạp" + "Người đi xe đạp leo núi" + "Người đi bộ" + "Cấm người đi bộ" + "Trẻ em qua đường" + "Biểu tượng đàn ông" + "Biểu tượng phụ nữ" + "Phòng vệ sinh" + "Biểu tượng trẻ em" + "Nhà vệ sinh" + "Nhà vệ sinh" + "Vòi hoa sen" + "Tắm" + "Bồn tắm" + "Kiểm soát hộ chiếu" + "Hải quan" + "Xác nhận hành lý" + "Hành lý bên trái" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-vi/strings-letter-descriptions.xml b/app/src/main/res/values-vi/strings-letter-descriptions.xml new file mode 100644 index 00000000..74f199f0 --- /dev/null +++ b/app/src/main/res/values-vi/strings-letter-descriptions.xml @@ -0,0 +1,208 @@ + + + + + + + "Chỉ báo thứ tự giống cái" + "Ký hiệu micrô" + "Chỉ báo thứ tự giống đực" + "Sharp S" + "A, dấu huyền" + "A, dấu sắc" + "A, dấu mũ" + "A, dấu ngã" + "A, dấu tách đôi" + "A, dấu nhẫn phía trên" + "A, E, dấu gạch nối" + "C, dấu móc dưới" + "E, dấu huyền" + "E, dấu sắc" + "E, dấu mũ" + "E, dấu tách đôi" + "I, dấu huyền" + "I, dấu sắc" + "I, dấu mũ" + "I, dấu tách đôi" + "Eth" + "N, dấu ngã" + "O, dấu huyền" + "O, dấu sắc" + "O, dấu mũ" + "O, dấu ngã" + "O, dấu tách đôi" + "O, nét gạch" + "U, dấu huyền" + "U, dấu sắc" + "U, dấu mũ" + "U, dấu tách đôi" + "Y, dấu sắc" + "Dấu móc" + "Y, dấu tách đôi" + "A, sự làm dấu" + "A, dấu ngân" + "A, một cái đuôi nhỏ" + "C, dấu sắc" + "C, dấu mũ" + "C, dấu chấm phía trên" + "C, dấu mũ ngược" + "D, dấu mũ ngược" + "D, nét gạch" + "E, sự làm dấu" + "E, dấu ngân" + "E, dấu chấm phía trên" + "E, một cái đuôi nhỏ" + "E, dấu mũ ngược" + "G, dấu mũ" + "G, dấu ngân" + "G, dấu chấm phía trên" + "G, dấu móc dưới" + "H, dấu mũ" + "H, nét gạch" + "I, dấu ngã" + "I, sự làm dấu" + "I, dấu ngân" + "I, một cái đuôi nhỏ" + "I không dấu chấm" + "I, J, dấu gạch nối" + "J, dấu mũ" + "K, dấu móc dưới" + "Kra" + "L, dấu sắc" + "L, dấu móc dưới" + "L, dấu mũ ngược" + "L, chấm ở giữa" + "L, nét gạch" + "N, dấu sắc" + "N, dấu móc dưới" + "N, dấu mũ ngược" + "N, theo sau dấu móc lửng" + "Eng" + "O, sự làm dấu" + "O, dấu ngân" + "O, hai dấu sắc" + "O, E, dấu gạch nối" + "R, dấu sắc" + "R, dấu móc dưới" + "R, dấu mũ ngược" + "S, dấu sắc" + "S, dấu mũ" + "S, dấu móc dưới" + "S, dấu mũ ngược" + "T, dấu móc dưới" + "T, dấu mũ ngược" + "T, nét gạch" + "U, dấu ngã" + "U, sự làm dấu" + "U, dấu ngân" + "U, dấu nhẫn ở trên" + "U, hai dấu sắc" + "U, một cái đuôi nhỏ" + "W, dấu mũ" + "Y, dấu mũ" + "Z, dấu sắc" + "Z, dấu chấm phía trên" + "Z, dấu mũ ngược" + "S dài" + "O, dấu móc" + "U, dấu móc" + "S, dấu phẩy phía dưới" + "T, dấu phẩy phía dưới" + "Âm Schwa" + "A, dấu chấm phía dưới" + "A, dấu móc phía trên" + "A, dấu mũ và dấu sắc" + "A, dấu mũ và dấu huyền" + "A, dấu mũ và móc phía trên" + "A, dấu mũ và dấu ngã" + "A, dấu mũ và dấu chấm phía dưới" + "A, dấu ngân và dấu sắc" + "A, dấu ngân và dấu huyền" + "A, dấu ngân và móc phía trên" + "A, dấu ngân và dấu ngã" + "A, dấu ngân và dấu chấm phía dưới" + "E, dấu chấm phía dưới" + "E, móc phía trên" + "E, dấu ngã" + "E, dấu mũ và dấu sắc" + "E, dấu mũ và dấu huyền" + "E, dấu mũ và móc phía trên" + "E, dấu mũ và dấu ngã" + "E, dấu mũ và dấu chấm phía dưới" + "I, móc phía trên" + "I, dấu chấm phía dưới" + "O, dấu chấm phía dưới" + "O, móc phía trên" + "O, dấu mũ và dấu sắc" + "O, dấu mũ và dấu huyền" + "O, dấu mũ và móc phía trên" + "O, dấu mũ và dấu ngã" + "O, dấu mũ và dấu chấm phía dưới" + "O, dấu móc và dấu sắc" + "O, dấu móc và dấu huyền" + "O, dấu móc và móc phía trên" + "O, dấu móc và dấu ngã" + "O, dấu móc và dấu chấm phía dưới" + "U, dấu chấm phía dưới" + "U, móc phía trên" + "U, dấu móc và dấu sắc" + "U, dấu móc và dấu huyền" + "U, dấu móc và móc phía trên" + "U, dấu móc và dấu ngã" + "U, dấu móc và dấu chấm phía dưới" + "Y, dấu huyền" + "Y, dấu chấm phía dưới" + "Y, móc phía trên" + "Y, dấu ngã" + "Dấu cảm thán đảo ngược" + "Dấu ngoặc kép mở" + "Dấu chấm ở giữa" + "Chỉ số trên một" + "Dấu ngoặc kép đóng" + "Dấu hỏi đảo ngược" + "Dấu ngoặc đơn mở" + "Dấu ngoặc đơn đóng" + "Dấu trích dẫn đơn" + "Dấu ngoặc kép mở" + "Dấu ngoặc kép đóng" + "Dấu chữ thập" + "Dấu chữ thập kép" + "Dấu cho mỗi ngàn" + "Dấu phẩy" + "Hai phẩy" + "Dấu ngoặc đơn mở" + "Dấu ngoặc đơn đóng" + "Chỉ số trên bốn" + "Chỉ số trên của n thường" + "Ký hiệu đồng Pêsô" + "Dấu phần trăm" + "Mũi tên về phía phải" + "Mũi tên xuống dưới" + "Tập rỗng" + "Tăng dần" + "Nhỏ hơn hoặc bằng" + "Lớn hơn hoặc bằng" + "Sao đen" + diff --git a/app/src/main/res/values-vi/strings-talkback-descriptions.xml b/app/src/main/res/values-vi/strings-talkback-descriptions.xml new file mode 100644 index 00000000..2e530569 --- /dev/null +++ b/app/src/main/res/values-vi/strings-talkback-descriptions.xml @@ -0,0 +1,96 @@ + + + + + "Cắm tai nghe để nghe mật khẩu được đọc to." + "Ký tự hiện tại là %s" + "Không có ký tự nào được nhập" + "%1$s sửa %2$s thành %3$s" + "%1$s tự động sửa" + "Không có đề xuất nào" + "Ký tự không xác định" + "Shift" + "Biểu tượng khác" + "Shift" + "Biểu tượng" + "Shift" + "Xóa" + "Biểu tượng" + "Chữ cái" + "Số" + "Cài đặt" + "Tab" + "Dấu cách" + "Nhập bằng giọng nói" + "Biểu tượng cảm xúc" + "Quay lại" + "Tìm kiếm" + "Dấu chấm" + "Chuyển ngôn ngữ" + "Tiếp theo" + "Trước" + "Đã bật Shift" + "Đã bật Caps lock" + "Chế độ biểu tượng" + "Chế độ biểu tượng khác" + "Chế độ chữ cái" + "Chế độ điện thoại" + "Chế độ biểu tượng điện thoại" + "Bàn phím bị ẩn" + "Hiển thị bàn phím %s" + "ngày" + "ngày và giờ" + "email" + "nhắn tin" + "số" + "điện thoại" + "văn bản" + "giờ" + "URL" + "Gần đây" + "Con người" + "Đồ vật" + "Tự nhiên" + "Địa điểm" + "Biểu tượng" + "Cờ" + "Mặt cười và mọi người" + "Động vật và thiên nhiên" + "Thực phẩm và đồ uống" + "Du lịch và địa danh" + "Hoạt động" + "Biểu tượng cảm xúc" + "%s hoa" + "I hoa" + "I hoa, dấu chấm phía trên" + "Ký hiệu không xác định" + "Biểu tượng cảm xúc không xác định" + "Mặt buồn chán" + "Mặt bối rối" + "Mặt đeo kính râm" + "Mặt ngạc nhiên" + "Mặt đang hôn" + "Mặt cau mày" + "Có ký tự thay thế" + "Ký tự thay thế đã bị loại bỏ" + "Có đề xuất thay thế" + "Đề xuất thay thế đã bị loại bỏ" + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml new file mode 100644 index 00000000..46636297 --- /dev/null +++ b/app/src/main/res/values-vi/strings.xml @@ -0,0 +1,212 @@ + + + + + "Tùy chọn nhập" + "Tra cứu tên liên hệ" + "Trình kiểm tra chính tả sử dụng các mục nhập từ danh bạ của bạn" + "Rung khi nhấn phím" + "Phát âm thanh khi nhấn phím" + "Phông chữ lớn hơn bật lên khi nhấn phím" + "Tùy chọn" + "Tài khoản và bảo mật" + "Giao diện và bố cục" + "Nhập bằng cử chỉ" + "Sửa văn bản" + "Nâng cao" + "Chủ đề" + "Bật chia bàn phím" + "Đồng bộ hóa Bàn phím Google" + "Đã bật tính năng đồng bộ hóa" + "Đồng bộ hóa từ điển cá nhân của bạn trên các thiết bị" + "Đồng bộ hóa ngay bây giờ" + "Xóa dữ liệu đám mây" + "Xóa dữ liệu đã được đồng bộ hóa của bạn khỏi Google" + "Dữ liệu đã đồng bộ hóa của bạn sẽ bị xóa khỏi đám mây. Bạn có chắc chắn không?" + "Xóa" + "Hủy" + "Từ điển cá nhân của bạn sẽ được đồng bộ hóa và sao lưu vào máy chủ Google. Chúng tôi có thể thu thập thông tin thống kê về tần suất của từ để giúp cải thiện sản phẩm của mình. Việc thu thập và sử dụng tất cả thông tin sẽ tuân thủ ""Chính sách quyền riêng tư của Google""." + "Vui lòng thêm một tài khoản Google vào thiết bị này để bật tính năng này" + "Tính năng đồng bộ hóa không khả dụng cho các thiết bị có tài khoản Google Apps for Business" + "Phương thức nhập khác" + "Khóa chuyển ngôn ngữ bao gồm cả các phương thức nhập liệu khác" + "Phím chuyển đổi ngôn ngữ" + "Hiển thị khi nhiều ngôn ngữ đầu vào được bật" + "Độ trễ ẩn phím bật lên" + "Không trễ" + "Mặc định" + "%s mili giây" + "Theo mặc định của hệ thống" + "Đề xuất tên liên hệ" + "Sử dụng tên từ Danh bạ cho các đề xuất và chỉnh sửa" + "Đề xuất được cá nhân hóa" + "Cải thiện %s" + "Thêm dấu chấm khi nhấn hai phím cách" + "Nhấn đúp phím cách sẽ chèn một dấu chấm, theo sau là dấu cách" + "Tự động viết hoa" + "Viết hoa chữ đầu tiên của mỗi câu" + "Từ điển cá nhân" + "Thêm từ điển" + "Từ điển chính" + "Hiển thị gợi ý sửa" + "Hiển thị từ được đề xuất khi nhập" + "Chặn các từ phản cảm" + "Không đề xuất các từ có thể gây xúc phạm" + "Tự động sửa" + "Phím cách và dấu câu tự động sửa từ nhập sai" + "Tắt" + "Đơn giản" + "Linh hoạt" + "Rất linh hoạt" + "Đề xuất từ tiếp theo" + "Sử dụng từ trước đó khi đưa ra đề xuất" + "Bật nhập bằng cử chỉ" + "Nhập từ bằng cách trượt qua các chữ cái" + "Hiển thị vệt cử chỉ" + "Xem trước nổi động" + "Xem từ được đề xuất trong khi dùng cử chỉ" + "Cử chỉ nhập cụm từ" + "Nhập dấu cách khi thực hiện cử chỉ bằng cách trượt tới phím cách" + "Phím nhập liệu bằng giọng nói" + "Không có phương thức nhập bằng giọng nói nào được bật. Kiểm tra cài đặt Ngôn ngữ và phương thức nhập." + "Định cấu hình phương thức nhập" + "Ngôn ngữ" + "Trợ giúp và phản hồi" + "Ngôn ngữ" + "Nhấn lại để lưu" + "Nhấn vào đây để lưu" + "Có sẵn từ điển" + "Chủ đề bàn phím" + "Chuyển đổi tài khoản" + "Chưa chọn tài khoản nào" + "Hiện đang sử dụng %1$s" + "OK" + "Hủy" + "Đăng xuất" + "Chọn một tài khoản để sử dụng" + "Tiếng Anh (Anh)" + "Tiếng Anh (Mỹ)" + "Tiếng Tây Ban Nha (Mỹ)" + "Tiếng Anh-Hindi" + "Tiếng Serbia (La tinh)" + "Tiếng Anh (Anh) (%s)" + "Tiếng Anh (Mỹ) (%s)" + "Tiếng Tây Ban Nha (Mỹ) (%s)" + "Tiếng Anh-Hindi (%s)" + "Tiếng Serbia (%s)" + "%s (Truyền thống)" + "%s (Viết tắt)" + "Không ngôn ngữ nào (Bảng chữ cái)" + "Bảng chữ cái (QWERTY)" + "Bảng chữ cái (QWERTZ)" + "Bảng chữ cái (AZERTY)" + "Bảng chữ cái (Dvorak)" + "Bảng chữ cái (Colemak)" + "Bảng chữ cái (PC)" + "Biểu tượng cảm xúc" + "Chủ đề bàn phím" + "Kiểu nhập tùy chỉnh" + "Thêm kiểu" + "Thêm" + "Xóa" + "Lưu" + "Ngôn ngữ" + "Bố cục" + "Bạn cần phải bật kiểu nhập tùy chỉnh trước khi bắt đầu sử dụng nó. Bạn có muốn bật kiểu nhập tùy chỉnh ngay bây giờ không?" + "Bật" + "Để sau" + "Đã tồn tại kiểu nhập tương tự: %s" + "Thời gian rung khi nhấn phím" + "Âm lượng khi nhấn phím" + "Thời gian nhấn và giữ phím" + "Biểu tượng cảm xúc cho bàn phím vật lý" + "Phím Alt thực hiển thị bảng biểu tượng cảm xúc" + "Mặc định" + "Chào mừng bạn đến với %s" + "với Nhập bằng cử chỉ" + "Bắt đầu" + "Bước tiếp theo" + "Thiết lập %s" + "Bật %s" + "Hãy chọn \"%s\" trong cài đặt Ngôn ngữ và phương thức nhập. Thao tác này sẽ cho phép ứng dụng chạy trên thiết bị của bạn." + "%s đã được bật trong cài đặt Ngôn ngữ và phương thức nhập, do đó bước này đã hoàn tất. Hãy chuyển sang bước tiếp theo!" + "Bật trong Cài đặt" + "Chuyển sang %s" + "Tiếp theo, chọn \"%s\" làm phương thức nhập văn bản hoạt động của bạn." + "Chuyển phương thức nhập" + "Xin chúc mừng, bạn đã cài đặt xong!" + "Bây giờ bạn có thể nhập vào tất cả ứng dụng yêu thích của mình với %s." + "Định cấu hình các ngôn ngữ khác" + "Đã xong" + "Hiển thị biểu tượng ứng dụng" + "Hiển thị biểu tượng ứng dụng trong trình chạy" + "Nhà cung cấp từ điển" + "Nhà cung cấp từ điển" + "Dịch vụ từ điển" + "Thông tin cập nhật từ điển" + "Từ điển bổ sung" + "Từ điển có sẵn" + "Cài đặt cho từ điển" + "Từ điển người dùng" + "Từ điển người dùng" + "Từ điển có sẵn" + "Hiện đang tải xuống" + "Đã cài đặt" + "Đã cài đặt, bị tắt" + "Lỗi kết nối DV từ điển" + "Không có từ điển nào" + "Làm mới" + "Cập nhật lần cuối" + "Đang kiểm tra cập nhật" + "Đang tải..." + "Từ điển chính" + "Hủy" + "Cài đặt" + "Cài đặt" + "Hủy" + "Xóa" + "Ngôn ngữ đã chọn trên thiết bị di động của bạn hiện có từ điển.<br/> Chúng tôi khuyên bạn nên <b>tải xuống</b> từ điển %1$s để cải thiện trải nghiệm nhập của mình.<br/> <br/> Quá trình tải xuống có thể mất vài phút qua 3G. Có thể mất phí nếu bạn không có <b>gói dữ liệu không giới hạn</b>.<br/> Nếu bạn không chắc mình có gói dữ liệu nào, bạn nên tìm kết nối Wi-Fi để bắt đầu tải xuống tự động.<br/> <br/> Mẹo: Bạn có thể tải xuống và xóa từ điển bằng cách chuyển tới <b>Ngôn ngữ và phương thức nhập</b> trong menu <b>Cài đặt</b> trên thiết bị di động của mình." + "Tải xuống bây giờ (%1$.1fMB)" + "Tải xuống qua Wi-Fi" + "Hiện có từ điển cho %1$s" + "Nhấn để xem lại và tải xuống" + "Đang tải xuống: đề xuất cho %1$s sẽ sớm sẵn sàng." + "Phiên bản %1$s" + "Thêm" + "Thêm vào từ điển" + "Cụm từ" + "Thêm tùy chọn" + "Bớt tùy chọn" + "OK" + "Từ:" + "Phím tắt:" + "Ngôn ngữ:" + "Nhập từ" + "Phím tắt tùy chọn" + "Chỉnh sửa từ" + "Chỉnh sửa" + "Xóa" + "Bạn không có bất kỳ từ nào trong từ điển người dùng. Để thêm từ, hãy nhấn vào nút Thêm (+)." + "Cho tất cả ngôn ngữ" + "Ngôn ngữ khác…" + "Xóa" + " ABCDEFGHIJKLMNOPQRSTUVWXYZ" + diff --git a/app/src/main/res/values-zh-rCN/strings-action-keys.xml b/app/src/main/res/values-zh-rCN/strings-action-keys.xml new file mode 100644 index 00000000..de00e833 --- /dev/null +++ b/app/src/main/res/values-zh-rCN/strings-action-keys.xml @@ -0,0 +1,31 @@ + + + + + "前往" + "下个" + "上个" + "完成" + "发送" + "搜索" + "暂停" + "等待" + diff --git a/app/src/main/res/values-zh-rCN/strings-config-important-notice.xml b/app/src/main/res/values-zh-rCN/strings-config-important-notice.xml new file mode 100644 index 00000000..6013af78 --- /dev/null +++ b/app/src/main/res/values-zh-rCN/strings-config-important-notice.xml @@ -0,0 +1,24 @@ + + + + + "根据您的通信记录和以往输入的数据来完善建议" + diff --git a/app/src/main/res/values-zh-rCN/strings-emoji-descriptions.xml b/app/src/main/res/values-zh-rCN/strings-emoji-descriptions.xml new file mode 100644 index 00000000..9ab0be59 --- /dev/null +++ b/app/src/main/res/values-zh-rCN/strings-emoji-descriptions.xml @@ -0,0 +1,1767 @@ + + + + + + + "版权符号" + "注册符号" + "双叹号" + "感叹号和问号" + "商标符号" + "信息来源" + "左右箭头" + "上下箭头" + "西北箭头" + "东北箭头" + "东南箭头" + "西南箭头" + "有钩的向左箭头" + "有钩的向右箭头" + "手表" + "沙漏" + + + + + "实心右指双三角" + "实心左指双三角" + "实心上指双三角" + "实心下指双三角" + + + + + + + "闹钟" + + + + + "沙粒流动中的沙漏" + + + + + + + "带圆圈的拉丁文大写字母M" + "实心小方形" + "空心小方形" + "实心右指三角形" + "实心左指三角形" + "空心中等方形" + "实心中等方形" + "空心中小方形" + "实心中小方形" + "放射光线的实心太阳" + "云朵" + + + + + + + "实心电话标志" + "带对钩的选框" + "挡着雨点的雨伞" + "热饮料" + + + "向上指的食指" + + + + + + + + + + + + + + + + + + + "轻松的笑脸" + "白羊座" + "金牛座" + "双子座" + "巨蟹座" + "狮子座" + "处女座" + "天秤座" + "天蝎座" + "人马座" + "摩羯座" + "水瓶座" + "双鱼座" + "实心黑桃" + "实心梅花" + "实心红桃" + "实心方块" + "温泉" + "实心通用再循环符号" + "轮椅符号" + + + "锚" + + + + + + + + + + + + + "警告符号" + "高压符号" + "中等空心圆圈" + "中等实心圆圈" + + + + + "足球" + "棒球" + "雪人" + "被云遮挡的太阳" + + + "蛇夫座" + + + + + + + "禁止通行" + + + "教堂" + + + + + "喷泉" + "插在高尔夫球洞中的旗" + + + "帆船" + + + + + + + "帐篷" + "油泵" + "实心剪刀" + "空心加粗对钩标志" + "飞机" + "信封" + "举着的拳头" + "举着的手" + "胜利手势" + + + "铅笔" + "实心钢笔尖" + "加粗对钩标志" + "加粗乘号 x" + + + + + "火花" + "八轮辐星号" + "实心八角星" + "雪花" + "火花" + "十字线" + "带方框的反白十字线" + "实心问号装饰" + "空心问号装饰" + "空心叹号装饰" + "加粗叹号" + + + "实心的心形" + "加粗加号" + "加粗减号" + "加粗除号" + "实心向右箭头" + "卷曲环" + "双卷曲环" + "指向右侧然后向上弯曲的箭头" + "指向右侧然后向下弯曲的箭头" + "向左实心箭头" + "向上实心箭头" + "向下实心箭头" + "实心大正方形" + "空心大正方形" + "空心中等星形" + "加粗大圆圈" + "波形长划" + "部分替换标记" + "带圆圈的汉字祝" + "带圆圈的汉字秘" + "麻将牌红中" + "扑克牌小王" + "A型血" + "B型血" + "O型血" + "停车场" + "AB型血" + "带方框的CL" + "带方框的COOL" + "带方框的FREE" + "带方框的ID" + "带方框的NEW" + "带方框的NG" + "带方框的OK" + "带方框的SOS" + "带方框的UP和感叹号" + "带方框的VS" + "带方框的片假名ココ" + "带方框的片假名サ" + "带方框的汉字無" + "带方框的汉字指" + "带方框的汉字禁" + "带方框的汉字空" + "带方框的汉字合" + "带方框的汉字満" + "带方框的汉字有" + "带方框的汉字月" + "带方框的汉字申" + "带方框的汉字割" + "带方框的汉字営" + "带圆圈的汉字得" + "带圆圈的汉字可" + "旋风" + "有雾" + "合上的雨伞" + "有星星的夜晚" + "越过山顶的日出" + "日出" + "黄昏的城市风景" + "越过建筑物的日落" + "彩虹" + "夜间的大桥" + "水波" + "火山" + "银河系" + "地球欧洲-非洲" + "地球美洲" + "地球亚洲-澳大利亚" + "带子午线的球形" + "新月符号" + "新月娥眉月符号" + "上弦月符号" + "盈凸月符号" + "满月符号" + "亏凸月符号" + "下弦月符号" + "残月娥眉月符号" + "娥眉月" + "带一张脸的新月" + "带一张脸的上弦月" + "带一张脸的下弦月" + "带一张脸的满月" + "带一张脸的太阳" + "发光的星星" + "流星" + + + + + + + + + + + + + + + + + + + + + + + + + + + "板栗" + "幼苗" + "常青树" + "落叶树" + "棕榈树" + "仙人掌" + + + "郁金香" + "樱花" + "玫瑰" + "芙蓉" + "向日葵" + "花蕾" + "玉米穗" + "稻穗" + "香草" + "四叶草" + "枫叶" + "落叶" + "在风中飘扬的树叶" + "蘑菇" + "番茄" + "茄子" + "葡萄" + "哈密瓜" + "西瓜" + "柑橘" + "柠檬" + "香蕉" + "菠萝" + "红苹果" + "青苹果" + "梨" + "桃" + "樱桃" + "草莓" + "汉堡" + "披萨片" + "连骨肉" + "鸡腿" + "米饼" + "饭团" + "白米饭" + "咖喱饭" + "热气腾腾的碗" + "意大利面" + "面包" + "炸薯条" + "烤地瓜" + "团子" + "关东煮" + "寿司" + "炸虾" + "漩涡图案鱼饼" + "软冰淇淋" + "刨冰" + "冰淇淋" + "甜甜圈" + "饼干" + "巧克力块" + "糖果" + "棒棒糖" + "奶油蛋羹" + "蜜罐" + "水果蛋糕" + "便当盒" + "盛满食物的锅" + "烹调" + "刀叉" + "无柄茶杯" + "日本酒瓶和杯子" + "葡萄酒杯" + "鸡尾酒杯" + "热带风情鸡尾酒" + "啤酒杯" + "碰在一起的啤酒杯" + "婴儿奶瓶" + + + + + + + "绸带" + "包好的礼物" + "生日蛋糕" + "南瓜灯" + "圣诞树" + "圣诞老人" + "焰火" + "烟花焰火" + "气球" + "礼花" + "五彩纸屑球" + "七夕树" + "交叉旗帜" + "门松" + "日本人偶" + "鲤鱼旗" + "风铃" + "赏月仪式" + "书包" + "毕业帽" + + + + + + + + + + + + + + + "旋转木马" + "摩天轮" + "过山车" + "鱼竿和鱼" + "麦克风" + "摄像机" + "电影院" + "耳机" + "画家调色板" + "礼帽" + "马戏团帐篷" + "门票" + "场记板" + "表演艺术" + "电子游戏机" + "命中靶心" + "老虎机" + "台球" + "游戏骰子" + "保龄球" + "花牌" + "音符" + "多个音符" + "萨克斯管" + "吉他" + "音乐键盘" + "小号" + "小提琴" + "乐谱" + "带饰带的运动衫" + "网球拍和网球" + "滑雪板和滑雪靴" + "篮球和篮框" + "格子旗" + "单板滑雪者" + "跑步者" + "冲浪者" + + + "奖杯" + "赛马" + "美式橄榄球" + "英式橄榄球" + "游泳者" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "房屋建筑" + "带花园的房子" + "办公楼" + "日本邮局" + "欧洲邮局" + "医院" + "银行" + "自动取款机" + "酒店" + "情侣酒店" + "便利店" + "学校" + "百货公司" + "工厂" + "居酒屋灯笼" + "日本城堡" + "欧洲城堡" + + + + + + + + + + + + + + + + + + + + + + + + + "大老鼠" + "小老鼠" + "牛" + "水牛" + "奶牛" + + + "豹" + "兔子" + "猫" + "龙" + "鳄鱼" + "鲸" + "蜗牛" + "蛇" + "马" + "公羊" + "山羊" + "绵羊" + "猴子" + "公鸡" + "母鸡" + "狗" + "猪" + "野猪" + "大象" + "章鱼" + "螺蛳" + "虫子" + "蚂蚁" + "蜜蜂" + "瓢虫" + "鱼" + "热带鱼" + "河豚" + "乌龟" + "破壳而出的小鸡" + "小鸡" + "面朝前方的小鸡" + "鸟" + "企鹅" + "考拉" + "贵宾狗" + "单峰驼" + "双峰驼" + "海豚" + "老鼠脸" + "牛脸" + "老虎脸" + "兔脸" + "猫脸" + "龙脸" + "喷水的鲸" + "马脸" + "猴脸" + "狗脸" + "猪脸" + "青蛙脸" + "仓鼠脸" + "狼脸" + "熊脸" + "熊猫脸" + "猪鼻子" + "掌印" + + + "眼睛" + + + "耳朵" + "鼻子" + "嘴" + "舌头" + "反手向上指的食指" + "反手向下指的食指" + "反手向左指的食指" + "反手向右指的食指" + "握拳手势" + "挥手手势" + "OK手势" + "拇指朝上手势" + "拇指朝下手势" + "拍手手势" + "张开双手手势" + "王冠" + "女式帽子" + "眼镜" + "领带" + "T恤衫" + "牛仔裤" + "礼服" + "和服" + "比基尼" + "女装" + "女用小提包" + "手提包" + "小包包" + "男鞋" + "运动鞋" + "高跟鞋" + "女式凉鞋" + "女靴" + "脚印" + "半身人像轮廓" + "双半身人像轮廓" + "男孩" + "女孩" + "男人" + "女人" + "家庭" + "男人和女人手牵着手" + "握住手的两位男性" + "握住手的两位女性" + "警官" + "兔耳朵装扮的女性" + "戴面纱的新娘" + "金发人" + "戴瓜皮帽的男性" + "戴头巾的男性" + "年长的男性" + "年长的女性" + "婴儿" + "建筑工人" + "公主" + "日本鬼" + "日本天狗" + "幽灵" + "天使宝贝" + "外星人" + "外星怪物" + "小鬼" + "头骨" + "信息服务台人员" + "卫兵" + "舞者" + "口红" + "指甲油" + "脸部按摩" + "理发" + "理发店门前的旋转彩柱" + "注射器" + "药丸" + "吻痕" + "情书" + "戒指" + "宝石" + "接吻" + "花束" + "夫妻连心" + "婚礼" + "跳动的心" + "破碎的心" + "两颗心" + "闪亮的心" + "成长的心" + "被箭射中的心" + "蓝色的心" + "绿色的心" + "黄色的心" + "紫色的心" + "带丝带的心" + "环绕旋转的心" + "心形装饰" + "代表花卉的内部带点的菱形" + "电灯泡" + "愤怒标志" + "炸弹" + "睡觉标志" + "碰撞标志" + "汗水飞溅" + "水滴" + "快速奔跑标志" + "便便堆" + "弯曲二头肌" + "眩晕符号" + "对话泡泡框" + "思考泡泡框" + "白花" + "一百分符号" + "钱袋" + "货币兑换" + "加粗美元符号" + "信用卡" + "带日元符号的钞票" + "带美元符号的钞票" + "带欧元符号的钞票" + "带英镑符号的钞票" + "长翅膀的钱" + "带向上趋势和日元符号的图表" + "座椅" + "个人计算机" + "公文包" + "微型碟片" + "软盘" + "光盘" + "DVD" + "文件夹" + "打开的文件夹" + "下端卷起的纸页" + "正面朝上的纸页" + "日历" + "撕页日历" + "卡片索引" + "带向上趋势的图表" + "带向下趋势的图表" + "条形图" + "带夹写字板" + "图钉" + "圆图钉" + "曲别针" + "直尺" + "三角尺" + "书签贴纸" + "帐簿" + "笔记本" + "带装饰封面的笔记本" + "合上的书" + "打开的书" + "绿皮书" + "蓝皮书" + "橙皮书" + "图书" + "姓名牌" + "文卷" + "便笺" + "电话听筒" + "寻呼机" + "传真机" + "卫星天线" + "公共广播喇叭" + "啦啦队喇叭筒" + "发件箱" + "收件箱" + "包裹" + "电子邮件符号" + "来函" + "上方带有向下箭头的信封" + "带有降下的旗帜的关闭信箱" + "带有升起的旗帜的关闭信箱" + "带有升起的旗帜的打开信箱" + "带有降下的旗帜的打开信箱" + "信箱" + "邮政号角" + "报纸" + "手机" + "左侧带向右箭头的手机" + "振动模式" + "手机关机" + "禁止使用手机" + "天线信号格" + "相机" + + + "摄像机" + "电视" + "收音机" + "录像带" + + + + + "相交的向右箭头" + "顺时针向右和向左绕成开放式圆圈的箭头" + "顺时针向右和向左绕成开放式圆圈的箭头,左下角圆圈中标有数字1" + "顺时针向下和向上绕成开放式圆圈的箭头" + "逆时针向下和向上绕成开放式圆圈的箭头" + "低亮度符号" + "高亮度符号" + "带删除斜线的扬声器" + "扬声器" + "带一个声波的扬声器" + "带三个声波的扬声器" + "电池" + "电源插头" + "朝向左边的放大镜" + "朝向右边的放大镜" + "带有墨水笔的锁" + "带有钥匙的闭合锁" + "钥匙" + "锁" + "打开的锁" + "铃" + "带删除斜线的铃" + "书签" + "链接符号" + "单选按钮" + "上带向左箭头的BACK" + "上带向左箭头的END" + "上带左右箭头的ON和感叹号" + "上带向右箭头的SOON" + "上带向上箭头的TOP" + "未满18岁禁止符号" + "键帽10" + "大写拉丁文字母输入符号" + "小写拉丁文字母输入符号" + "数字输入符号" + "符号的输入符号" + "拉丁文字母输入符号" + "火" + "手电筒" + "扳手" + "锤子" + "螺母和螺栓" + "日式厨刀" + "手枪" + "显微镜" + "望远镜" + "水晶球" + "中间带点的六角星" + "日本的新手标志" + "三叉戟徽章" + "实心方形按钮" + "空心方形按钮" + "红色大圆" + "蓝色大圆" + "橙色大菱形" + "蓝色大菱形" + "橙色小菱形" + "蓝色小菱形" + "尖朝上的红色三角形" + "尖朝下的红色三角形" + "尖朝上的红色小三角形" + "尖朝下的红色小三角形" + + + + + + + + + + + + + "显示为一点的钟面" + "显示为两点的钟面" + "显示为三点的钟面" + "显示为四点的钟面" + "显示为五点的钟面" + "显示为六点的钟面" + "显示为七点的钟面" + "显示为八点的钟面" + "显示为九点的钟面" + "显示为十点的钟面" + "显示为十一点的钟面" + "显示为十二点的钟面" + "显示为一点半的钟面" + "显示为两点半的钟面" + "显示为三点半的钟面" + "显示为四点半的钟面" + "显示为五点半的钟面" + "显示为六点半的钟面" + "显示为七点半的钟面" + "显示为八点半的钟面" + "显示为九点半的钟面" + "显示为十点半的钟面" + "显示为十一点半的钟面" + "显示为十二点半的钟面" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "富士山" + "东京塔" + "自由女神像" + "日本地图轮廓" + "摩艾石像" + "露齿而笑的表情" + "咧嘴眉开眼笑的表情" + "喜极而泣的表情" + "咧嘴笑的表情" + "咧嘴且眯眼笑的表情" + "咧嘴笑且冒冷汗的表情" + "咧嘴笑且眼睛紧闭的表情" + "带光环的笑脸" + "带恶魔角的笑脸" + "眨眼微笑的表情" + "眼睛笑眯眯的表情" + "品尝到美食的表情" + "如释重负的表情" + "眼睛呈心形的笑脸" + "戴墨镜的笑脸" + "得意的笑脸" + "无表情的脸" + "无动于衷的脸" + "面带不快的脸" + "冒冷汗的脸" + "忧虑的表情" + "困惑的表情" + "不知所措的表情" + "为亲吻而嘟着嘴的脸" + "拋飞吻的脸" + "为亲吻而嘟着嘴的笑眯眯的脸" + "为亲吻而嘟着嘴的闭起眼的脸" + "吐舌头的表情" + "眨眼吐舌头的表情" + "闭眼吐舌头的表情" + "失望的表情" + "担心的表情" + "生气的表情" + "噘嘴的表情" + "哭泣的表情" + "忍着哭的表情" + "不可一世的表情" + "失望但如释重负的表情" + "张嘴皱眉的表情" + "苦恼的表情" + "害怕的表情" + "厌烦的表情" + "打瞌睡的表情" + "疲惫的表情" + "做鬼脸的表情" + "嚎啕大哭的表情" + "张大嘴的表情" + "噤声的表情" + "张嘴冒冷汗的表情" + "恐惧尖叫的表情" + "惊讶的表情" + "脸红的表情" + "熟睡的表情" + "头晕眼花的表情" + "没有嘴的脸" + "戴医用口罩的脸" + "露齿笑眯眯的猫脸" + "喜极而泣的猫脸" + "张嘴微笑的猫脸" + "眼睛呈心形的笑猫脸" + "坏笑的猫脸" + "为亲吻而嘟着嘴的闭起眼的猫脸" + "噘嘴的猫脸" + "哭泣的猫脸" + "厌倦的猫脸" + + + + + + + + + "做禁止手势的人" + "做OK手势的人" + "跪伏的人" + "非礼勿视的猴子" + "非礼勿听的猴子" + "非礼勿言的猴子" + "开心地举起一只手的人" + "高举双手庆祝的人" + "皱眉的人" + "噘嘴的人" + "双手合十的人" + "火箭" + "直升飞机" + "蒸汽火车" + "火车车厢" + "高铁列车" + "子弹头高铁列车" + "火车" + "地铁" + "轻轨" + "车站" + "有轨电车" + "电车车厢" + "公交车" + "迎面而来的公交车" + "无轨电车" + "公交车站牌" + "小型公共汽车" + "救护车" + "消防车" + "警车" + "迎面而来的警车" + "出租车" + "迎面而来的出租车" + "汽车" + "迎面而来的汽车" + "野营旅游车" + "送货车" + "铰接式卡车" + "拖拉机" + "单轨列车" + "山区铁路" + "高架铁路" + "山地索道" + "高空旋转缆车" + "轮船" + "小船" + "快艇" + "横向红绿灯" + "竖向红绿灯" + "施工标志" + "旋转的警车警示灯" + "旗杆上的三角旗" + "门" + "禁止通行标志" + "吸烟标志" + "禁止吸烟标志" + "正确丢弃垃圾标志" + "请勿乱丢垃圾标志" + "饮用水标志" + "非饮用水标志" + "自行车" + "禁止自行车通行" + "骑自行车的人" + "骑山地自行车的人" + "行人" + "禁止行人通行" + "注意儿童穿越马路" + "男士标志" + "女士标志" + "洗手间" + "婴儿标志" + "卫生间" + "厕所" + "淋浴喷头" + "洗浴" + "浴缸" + "入境护照检查" + "海关" + "行李领取处" + "行李寄存处" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-zh-rCN/strings-letter-descriptions.xml b/app/src/main/res/values-zh-rCN/strings-letter-descriptions.xml new file mode 100644 index 00000000..269f6c4a --- /dev/null +++ b/app/src/main/res/values-zh-rCN/strings-letter-descriptions.xml @@ -0,0 +1,208 @@ + + + + + + + "阴性顺序指示符" + "微单位标记" + "阳性顺序指示符" + "清音S" + "带抑音符的拉丁文小写字母a" + "带锐音符的拉丁文小写字母a" + "带扬抑符的拉丁文小写字母a" + "带颚化符的拉丁文小写字母a" + "带分音符的拉丁文小写字母a" + "带上圆圈的拉丁文小写字母a" + "拉丁文小写字母连字ae" + "带软音符的拉丁文小写字母c" + "带抑音符的拉丁文小写字母e" + "带锐音符的拉丁文小写字母e" + "带扬抑符的拉丁文小写字母e" + "带分音符的拉丁文小写字母e" + "带抑音符的拉丁文小写字母i" + "带锐音符的拉丁文小写字母i" + "带扬抑符的拉丁文小写字母i" + "带分音符的拉丁文小写字母i" + "拉丁文小写字母eth" + "带颚化符的拉丁文小写字母n" + "带抑音符的拉丁文小写字母o" + "带锐音符的拉丁文小写字母o" + "带扬抑符的拉丁文小写字母o" + "带颚化符的拉丁文小写字母o" + "带分音符的拉丁文小写字母o" + "带斜线的拉丁文小写字母o" + "带抑音符的拉丁文小写字母u" + "带锐音符的拉丁文小写字母u" + "带扬抑符的拉丁文小写字母u" + "带分音符的拉丁文小写字母u" + "带锐音符的拉丁文小写字母y" + "拉丁文小写字母thorn" + "带分音符的拉丁文小写字母y" + "带长音符的拉丁文小写字母a" + "带短音符的拉丁文小写字母a" + "带反尾形符的拉丁文小写字母a" + "带锐音符的拉丁文小写字母c" + "带扬抑符的拉丁文小写字母c" + "带上点的拉丁文小写字母c" + "带抑扬符的拉丁文小写字母c" + "带抑扬符的拉丁文小写字母d" + "带粗线的拉丁文小写字母d" + "带长音符的拉丁文小写字母e" + "带短音符的拉丁文小写字母e" + "带上点的拉丁文小写字母e" + "带反尾形符的拉丁文小写字母e" + "带抑扬符的拉丁文小写字母e" + "带扬抑符的拉丁文小写字母g" + "带短音符的拉丁文小写字母g" + "带上点的拉丁文小写字母g" + "带软音符的拉丁文小写字母g" + "带扬抑符的拉丁文小写字母h" + "带粗线的拉丁文小写字母h" + "带颚化符的拉丁文小写字母i" + "带长音符的拉丁文小写字母i" + "带短音符的拉丁文小写字母i" + "带反尾形符的拉丁文小写字母i" + "拉丁文小写字母无点i" + "拉丁文小写字母连字ij" + "带扬抑符的拉丁文小写字母j" + "带软音符的拉丁文小写字母k" + "拉丁文小写字母kra" + "带锐音符的拉丁文小写字母l" + "带软音符的拉丁文小写字母l" + "带抑扬符的拉丁文小写字母l" + "带中间点的拉丁文小写字母l" + "带斜线的拉丁文小写字母l" + "带锐音符的拉丁文小写字母n" + "带软音符的拉丁文小写字母n" + "带抑扬符的拉丁文小写字母n" + "前带撇号的拉丁文小写字母n" + "拉丁文小写字母eng" + "带长音符的拉丁文小写字母o" + "带短音符的拉丁文小写字母o" + "带双锐音符的拉丁文小写字母o" + "拉丁文小写字母连字oe" + "带锐音符的拉丁文小写字母r" + "带软音符的拉丁文小写字母r" + "带抑扬符的拉丁文小写字母r" + "带锐音符的拉丁文小写字母s" + "带扬抑符的拉丁文小写字母s" + "带软音符的拉丁文小写字母s" + "带抑扬符的拉丁文小写字母s" + "带软音符的拉丁文小写字母t" + "带抑扬符的拉丁文小写字母t" + "带粗线的拉丁文小写字母t" + "带颚化符的拉丁文小写字母u" + "带长音符的拉丁文小写字母u" + "带短音符的拉丁文小写字母u" + "带上圆圈的拉丁文小写字母u" + "带双锐音符的拉丁文小写字母u" + "带反尾形符的拉丁文小写字母u" + "带扬抑符的拉丁文小写字母w" + "带扬抑符的拉丁文小写字母y" + "带锐音符的拉丁文小写字母z" + "带上点的拉丁文小写字母z" + "带抑扬符的拉丁文小写字母z" + "拉丁文小写字母长s" + "带触角的拉丁文小写字母o" + "带触角的拉丁文小写字母u" + "带下逗号的拉丁文小写字母s" + "带下逗号的拉丁文小写字母t" + "拉丁文小写字母schwa" + "带下点的拉丁文小写字母a" + "带上钩的拉丁文小写字母a" + "带扬抑符和锐音符的拉丁文小写字母a" + "带扬抑符和抑音符的拉丁文小写字母a" + "带扬抑符和上钩的拉丁文小写字母a" + "带扬抑符和颚化符的拉丁文小写字母a" + "带扬抑符和下点的拉丁文小写字母a" + "带短音符和锐音符的拉丁文小写字母a" + "带短音符和抑音符的拉丁文小写字母a" + "带短音符和上钩的拉丁文小写字母a" + "带短音符和颚化符的拉丁文小写字母a" + "带短音符和下点的拉丁文小写字母a" + "带下点的拉丁文小写字母e" + "带上钩的拉丁文小写字母e" + "带颚化符的拉丁文小写字母e" + "带扬抑符和锐音符的拉丁文小写字母e" + "带扬抑符和抑音符的拉丁文小写字母e" + "带扬抑符和上钩的拉丁文小写字母e" + "带扬抑符和颚化符的拉丁文小写字母e" + "带扬抑符和下点的拉丁文小写字母e" + "带上钩的拉丁文小写字母i" + "带下点的拉丁文小写字母i" + "带下点的拉丁文小写字母o" + "带上钩的拉丁文小写字母o" + "带扬抑符和锐音符的拉丁文小写字母o" + "带扬抑符和抑音符的拉丁文小写字母o" + "带扬抑符和上钩的拉丁文小写字母o" + "带扬抑符和颚化符的拉丁文小写字母o" + "带扬抑符和下点的拉丁文小写字母o" + "带触角和锐音符的拉丁文小写字母o" + "带触角和抑音符的拉丁文小写字母o" + "带触角和上钩的拉丁文小写字母o" + "带触角和颚化符的拉丁文小写字母o" + "带触角和下点的拉丁文小写字母o" + "带下点的拉丁文小写字母u" + "带上钩的拉丁文小写字母u" + "带触角和锐音符的拉丁文小写字母u" + "带触角和抑音符的拉丁文小写字母u" + "带触角和上钩的拉丁文小写字母u" + "带触角和颚化符的拉丁文小写字母u" + "带触角和下点的拉丁文小写字母u" + "带抑音符的拉丁文小写字母y" + "带下点的拉丁文小写字母y" + "带上钩的拉丁文小写字母y" + "带颚化符的拉丁文小写字母y" + "竖翻叹号" + "左指双尖引号" + "中间点" + "上标1" + "右指双尖引号" + "竖翻问号" + "左单引号" + "右单引号" + "单下9形引号" + "左双引号" + "右双引号" + "剑号" + "双剑号" + "千分号" + "角分符号" + "角秒符号" + "单左指尖引号" + "单右指尖引号" + "上标4" + "上标拉丁文小写字母n" + "比索符号" + "由...转交" + "向右箭头" + "向下箭头" + "空集" + "增量" + "小于或等于" + "大于或等于" + "实心星" + diff --git a/app/src/main/res/values-zh-rCN/strings-talkback-descriptions.xml b/app/src/main/res/values-zh-rCN/strings-talkback-descriptions.xml new file mode 100644 index 00000000..fec43e84 --- /dev/null +++ b/app/src/main/res/values-zh-rCN/strings-talkback-descriptions.xml @@ -0,0 +1,96 @@ + + + + + "插入耳机可听到输入密码时的按键提示音。" + "当前文字为%s" + "未输入文字" + "按%1$s键可将%2$s更正为%3$s" + "按%1$s键可进行自动更正" + "没有任何建议" + "未知字符" + "Shift" + "更多符号" + "Shift键" + "符号" + "Shift键" + "删除" + "符号" + "字母" + "数字" + "设置" + "Tab" + "空格" + "语音输入" + "表情符号" + "回车" + "搜索" + "点" + "切换语言" + "下一个" + "上一个" + "已开启Shift模式" + "已锁定大写模式" + "符号模式" + "更多符号模式" + "字母模式" + "电话模式" + "电话符号模式" + "键盘已隐藏" + "当前显示的是%s键盘" + "日期" + "日期和时间" + "电子邮件地址" + "消息" + "数字" + "电话号码" + "文字" + "时间" + "网址" + "最近用过" + "人物" + "物件" + "自然" + "地点" + "符号" + "旗帜" + "表情符号与人物" + "动物与大自然" + "美食" + "旅行与地点" + "活动" + "表情图标" + "拉丁文大写字母%s" + "拉丁文大写字母I" + "带上点的拉丁文大写字母I" + "未知符号" + "未知表情符号" + "无聊" + "尴尬" + "戴墨镜" + "惊讶" + "亲吻" + "皱眉" + "有可用的替代字符" + "已关闭替代字符" + "有可用的其他建议字词" + "已关闭其他建议字词" + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 00000000..dfa4f8d7 --- /dev/null +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,212 @@ + + + + + "输入选项" + "查找联系人姓名" + "拼写检查工具会使用您的联系人列表中的条目" + "按键振动" + "按键音效" + "按键时弹出显示字符" + "偏好设置" + "帐号和隐私权" + "外观和布局" + "滑行输入" + "文字更正" + "高级" + "主题背景" + "使用分离式键盘" + "Google 键盘同步" + "同步功能已开启" + "在设备间同步您的个人字典" + "立即同步" + "删除云端数据" + "将您已同步的数据从 Google 删除" + "您已同步的数据将会从云端删除。确定要执行此操作吗?" + "删除" + "取消" + "您的个人字典将会同步并备份到 Google 服务器。系统可能会收集字词出现频率的统计信息,来帮助改进我们的产品。所有信息的收集和使用都会遵守 ""Google 隐私权政策""。" + "要启用此功能,请将 Google 帐号添加到此设备中" + "在登录了 Google Apps for Business 帐号的设备上无法使用同步功能" + "切换到其他输入法" + "语言切换键也可用于切换其他输入法" + "语言切换键" + "启用了多种输入语言时显示" + "弹出字符隐藏延迟" + "无延迟" + "默认" + "%s毫秒" + "系统默认值" + "联系人姓名建议" + "根据通讯录中的姓名提供建议和更正" + "个性化建议" + "改进%s" + "双击空格插入句号" + "双击空格键可插入句号并后跟空格" + "自动大写" + "句首字词大写" + "个人字典" + "附加字典" + "主字典" + "显示更正建议" + "键入时显示建议的字词" + "屏蔽不文明的字词" + "屏蔽可能不文明的字词" + "自动更正" + "按空格键和标点可自动更正错别字" + "关闭" + "小改" + "大幅改动" + "极大幅度改动" + "后续字词建议" + "根据上一个字词提供建议" + "启用滑行输入" + "以滑行方式写出字词中字母来输入字词" + "显示滑行输入轨迹" + "动态漂浮预览" + "在滑行输入过程中显示建议字词" + "词组滑行输入" + "滑行输入时,滑过空格键即可输入空格" + "语音输入键" + "未启用任何语音输入法。请检查“语言和输入法”设置。" + "配置输入法" + "语言" + "帮助和反馈" + "语言" + "再次点按即可保存" + "点按此处即可保存" + "有可用字典" + "键盘主题" + "切换帐号" + "未选择任何帐号" + "当前使用的是%1$s" + "确定" + "取消" + "退出帐号" + "选择要使用的帐号" + "英语(英国)" + "英语(美国)" + "西班牙语(美国)" + "印地英语" + "塞尔维亚语(拉丁语布局)" + "英式英语(%s)" + "美式英语(%s)" + "美式西班牙语(%s)" + "印地英语(%s)" + "塞尔维亚语(%s布局)" + "%s(传统)" + "%s(紧凑型)" + "无语言(字母)" + "字母 (QWERTY)" + "字母 (QWERTZ)" + "字母 (AZERTY)" + "字母 (Dvorak)" + "字母 (Colemak)" + "字母 (PC)" + "表情符号" + "键盘主题" + "自定义输入样式" + "添加样式" + "添加" + "删除" + "保存" + "语言" + "布局" + "您需要先启用自定义输入样式,然后才能开始使用。要立即启用该样式吗?" + "启用" + "以后再说" + "已经存在相同的输入样式:%s" + "按键振动时长" + "按键音量" + "按键长按延迟" + "通过实体键盘启用表情符号" + "按实体键盘上的 Alt 键可显示表情符号面板" + "默认" + "欢迎使用 %s" + "体验顺畅的滑行输入体验" + "开始" + "下一步" + "设置 %s" + "启用 %s" + "请在“语言和输入法”设置中选中“%s”,授权该应用在您的设备上运行。" + "您已在“语言和输入法”中启用了%s,因此这一步骤已完成。继续下一步吧!" + "在设置中启用" + "切换到 %s" + "接下来,请选择“%s”作为您要使用的文字输入法。" + "切换输入法" + "恭喜,您已完成了设置!" + "现在,您可以在自己喜欢的所有应用中使用 %s来输入内容了。" + "配置其他语言" + "完成" + "显示应用图标" + "在启动器中显示应用图标" + "字典提供程序" + "字典提供程序" + "字典服务" + "字典更新信息" + "附加字典" + "字典可供下载" + "字典设置" + "用户字典" + "用户字典" + "字典可供下载" + "目前正在下载" + "已安装" + "已安装,但目前已停用" + "连接到字典服务时发生问题" + "没有可供下载的字典" + "刷新" + "上次更新时间" + "正在检查更新" + "正在加载…" + "主字典" + "取消" + "设置" + "安装" + "取消" + "删除" + "您在移动设备上选择的语言有可用的字典可供下载。<br/>建议您<b>下载</b>该%1$s字典,以便获得更好的输入体验。<br/> <br/>通过 3G 网络下载可能需要一两分钟的时间。如果您使用的不是<b>无流量限制的套餐</b>,则可能需要额外支付流量费用。<br/>如果您不确定自己使用的是哪种流量套餐,建议您连接到 WLAN 网络后再自动开始下载。<br/> <br/>提示:您可以在移动设备上转到<b>语言和输入法</b>(位于<b>设置</b>菜单中)来下载及移除字典。" + "立即下载 (%1$.1fMB)" + "通过WLAN网络下载" + "有一个%1$s字典可供下载" + "按此通知即可查看和下载" + "正在下载:%1$s输入建议功能马上就可以使用了!" + "版本:%1$s" + "添加" + "添加到字典" + "词组" + "更多选项" + "隐藏部分选项" + "确定" + "字词:" + "快捷键:" + "语言:" + "输入字词" + "快捷键(选填)" + "修改字词" + "修改" + "删除" + "用户字典中没有任何字词。要添加字词,请点按“添加”(+) 按钮。" + "所有语言" + "更多语言…" + "删除" + " ABCDEFGHIJKLMNOPQRSTUVWXYZ" + diff --git a/app/src/main/res/values-zh-rHK/strings-action-keys.xml b/app/src/main/res/values-zh-rHK/strings-action-keys.xml new file mode 100644 index 00000000..500cd5f6 --- /dev/null +++ b/app/src/main/res/values-zh-rHK/strings-action-keys.xml @@ -0,0 +1,31 @@ + + + + + "前往" + "下一步" + "上一步" + "完成" + "傳送" + "搜尋" + "暫停" + "等候" + diff --git a/app/src/main/res/values-zh-rHK/strings-config-important-notice.xml b/app/src/main/res/values-zh-rHK/strings-config-important-notice.xml new file mode 100644 index 00000000..a21b2438 --- /dev/null +++ b/app/src/main/res/values-zh-rHK/strings-config-important-notice.xml @@ -0,0 +1,24 @@ + + + + + "根據您的通訊記錄和已輸入的資料改善建議" + diff --git a/app/src/main/res/values-zh-rHK/strings-emoji-descriptions.xml b/app/src/main/res/values-zh-rHK/strings-emoji-descriptions.xml new file mode 100644 index 00000000..a743e38c --- /dev/null +++ b/app/src/main/res/values-zh-rHK/strings-emoji-descriptions.xml @@ -0,0 +1,1767 @@ + + + + + + + "版權符號" + "註冊符號" + "雙感嘆號" + "感嘆和問號" + "商標符號" + "資料來源" + "向左向右箭嘴" + "向上向下箭嘴" + "西北方箭嘴" + "東北方箭嘴" + "東南方箭嘴" + "西南方箭嘴" + "有鉤向左箭嘴" + "有鉤向右箭嘴" + "手錶" + "沙漏" + + + + + "黑色向右雙重三角形" + "黑色向左雙重三角形" + "黑色向上雙重三角形" + "黑色向下雙重三角形" + + + + + + + "鬧鐘" + + + + + "流動的沙漏" + + + + + + + "圓圈中的大寫字母 M" + "小型黑色正方形" + "小型白色正方形" + "黑色向右三角形" + "黑色向左三角形" + "中型白色正方形" + "中型黑色正方形" + "中小型白色正方形" + "中小型黑色正方形" + "有光芒的太陽" + "雲朵" + + + + + + + "黑色電話" + "有勾選記號的投票箱" + "有雨滴的傘" + "熱飲" + + + "指向上的白色食指" + + + + + + + + + + + + + + + + + + + "白色笑臉" + "白羊座" + "金牛座" + "雙子座" + "巨蟹座" + "獅子座" + "處女座" + "天秤座" + "天蠍座" + "射手座" + "摩羯座" + "水瓶座" + "雙魚座" + "黑色葵扇紙牌" + "黑色梅花紙牌" + "黑色紅心紙牌" + "黑色階磚紙牌" + "溫泉" + "黑色的國際通用資源回收符號" + "輪椅符號" + + + "錨" + + + + + + + + + + + + + "警告標誌" + "高電壓標誌" + "中型白圓點" + "中型黑圓點" + + + + + "足球" + "棒球" + "四周沒有下雪的雪人" + "雲遮日" + + + "蛇夫座" + + + + + + + "禁止進入" + + + "教堂" + + + + + "噴泉" + "插在洞中的旗子" + + + "帆船" + + + + + + + "帳篷" + "燃料泵" + "黑剪刀" + "白色加粗的剔號" + "飛機" + "信封" + "高舉拳頭" + "舉起的手" + "勝利手勢" + + + "鉛筆" + "黑色筆尖" + "加粗的剔號" + "加粗的乘法符號" + + + + + "三個火花" + "八角星號" + "黑色八角星形" + "雪花" + "火花" + "打叉符號" + "反白方框交叉符號" + "黑色問號裝飾" + "白色問號裝飾" + "白色感嘆號裝飾" + "加粗的感嘆號符號" + + + "粗黑的愛心" + "加粗的加號" + "加粗的減號" + "加粗的除號" + "黑色向右箭嘴" + "捲曲循環線" + "雙捲曲循環線" + "先向右再往上彎曲的箭嘴" + "先向右再往下彎曲的箭嘴" + "黑色向左箭嘴" + "黑色向上箭嘴" + "黑色向下箭嘴" + "大型黑色正方形" + "大型白色正方形" + "中型白色星星" + "加粗的大圓形" + "波折線" + "專輯交替標記" + "圓形的表意文字「祝賀」" + "圓形的表意文字「秘密」" + "麻雀牌紅中" + "黑色鬼牌" + "A 型血" + "B 型血" + "O 型血" + "停車場" + "AB 型血" + "方框 CL" + "方框 COOL" + "方框 FREE" + "方框 ID" + "方框 NEW" + "方框 NG" + "方框 OK" + "方框 SOS" + "方框 UP 與感嘆號" + "方框 VS" + "方框片假名「這裡」" + "方框片假名「服務」" + "方框表意文字「免費」" + "方框表意文字「預留座位」" + "方框表意文字「禁止」" + "方框表意文字「空位」" + "方框表意文字「同意」" + "方框表意文字「已滿」" + "方框表意文字「收費」" + "方框表意文字「每月」" + "方框表意文字「申請」" + "方框表意文字「折扣」" + "方框表意文字「營業中」" + "圓圈表意文字「特惠」" + "圓圈表意文字「接受」" + "氣旋" + "有霧" + "未打開的雨傘" + "有星空的夜晚" + "山頂日出" + "日出" + "暮光傾城" + "夕陽下的城市森林" + "彩虹" + "橋下夜色" + "水波" + "火山" + "銀河" + "地球 (歐洲到非洲)" + "地球 (美洲)" + "地球 (亞洲到澳洲)" + "有經線的地球" + "新月符號" + "眉月符號" + "上弦月符號" + "盈凸月符號" + "滿月符號" + "虧凸月符號" + "下弦月符號" + "殘月符號" + "新月" + "有臉孔的新月" + "有臉孔的上弦月" + "有臉孔的下弦月" + "有臉孔的滿月" + "有臉孔的太陽" + "閃耀的星星" + "流星" + + + + + + + + + + + + + + + + + + + + + + + + + + + "栗子" + "幼苗" + "長青樹" + "落葉樹" + "棕櫚樹" + "仙人掌" + + + "鬱金香" + "櫻花" + "玫瑰" + "大紅花" + "向日葵" + "花朵" + "玉米穗" + "稻穗" + "香草" + "四葉草" + "楓葉" + "落葉" + "隨風飛舞的葉子" + "蘑菇" + "番茄" + "茄子" + "葡萄" + "蜜瓜" + "西瓜" + "橘子" + "檸檬" + "香蕉" + "菠蘿" + "紅蘋果" + "青蘋果" + "梨" + "桃" + "車厘子" + "士多啤梨" + "漢堡包" + "一片薄餅" + "連骨的肉" + "家禽腿肉" + "米餅" + "飯糰" + "煮熟的飯" + "咖哩飯" + "冒著熱氣的碗" + "意粉" + "麵包" + "薯條" + "烤蕃薯" + "糯米丸子" + "關東煮" + "壽司" + "炸蝦" + "魚餅" + "軟雪糕" + "刨冰" + "雪糕" + "冬甩" + "曲奇" + "朱古力條" + "糖果" + "波板糖" + "奶黄/吉士" + "蜜糖罐" + "酥餅" + "便當" + "鍋物" + "烹飪" + "刀叉" + "無柄茶杯" + "清酒瓶與杯" + "酒杯" + "雞尾酒杯" + "熱帶飲料" + "啤酒杯" + "乾杯" + "奶瓶" + + + + + + + "絲帶" + "包裝好的禮物" + "生日蛋糕" + "南瓜燈" + "聖誕樹" + "聖誕老人" + "煙花" + "仙女棒" + "汽球" + "單線拉炮" + "五彩碎紙球" + "七夕許願樹" + "交叉的旗子" + "松果裝飾品" + "日本娃娃" + "鯉魚旗" + "風鈴" + "賞月" + "書包" + "學士帽" + + + + + + + + + + + + + + + "旋轉木馬" + "摩天輪" + "過山車" + "釣竿與魚" + "咪高峰" + "電影攝影機" + "戲院" + "耳機" + "調色盤" + "大禮帽" + "馬戲團帳篷" + "門票" + "場記牌" + "表演藝術" + "電子遊戲" + "正中紅心" + "老虎機" + "桌球" + "遊戲骰子" + "保齡球" + "花牌" + "音符" + "許多音符" + "色士風" + "結他" + "鍵盤琴" + "小號" + "小提琴" + "樂譜" + "有飾帶的運動衣" + "網球拍與球" + "滑雪與滑雪靴" + "籃球與球框" + "賽車方格旗" + "滑雪的人" + "跑步者" + "衝浪者" + + + "獎盃" + "賽馬" + "美式足球" + "橄欖球" + "游泳者" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "住宅建築" + "有庭院的房子" + "辦公大樓" + "日本郵局" + "歐洲郵局" + "醫院" + "銀行" + "自動提款機" + "酒店" + "汽車旅館" + "便利店" + "學校" + "百貨公司" + "工廠" + "居酒屋燈籠" + "日本城堡" + "歐式城堡" + + + + + + + + + + + + + + + + + + + + + + + + + "大型家鼠" + "老鼠" + "牛" + "水牛" + "母牛" + + + "豹" + "兔子" + "貓" + "龍" + "鱷魚" + "鯨魚" + "蝸牛" + "蛇" + "馬" + "公羊" + "山羊" + "綿羊" + "猴子" + "公雞" + "雞" + "狗" + "豬" + "野豬" + "大象" + "章魚" + "貝殼" + "蟲子" + "螞蟻" + "蜜蜂" + "甲蟲" + "魚" + "熱帶魚" + "河豚" + "烏龜" + "剛孵化的小雞" + "小雞" + "正面的小雞" + "鳥" + "企鵝" + "無尾熊" + "捲毛狗" + "單峰駱駝" + "雙峰駱駝" + "海豚" + "老鼠臉" + "牛臉" + "老虎臉" + "兔子臉" + "貓臉" + "龍臉" + "噴水鯨魚" + "馬臉" + "猴子臉" + "狗臉" + "豬臉" + "青蛙臉" + "倉鼠臉" + "狼臉" + "熊臉" + "熊貓臉" + "豬鼻" + "動物足印" + + + "眼睛" + + + "耳朵" + "鼻子" + "嘴巴" + "舌頭" + "白色食指向上反手" + "白色食指向下反手" + "白色食指向左反手" + "白色食指向右反手" + "握拳符號" + "揮手符號" + "OK 手勢符號" + "豎起拇指符號" + "拇指朝下符號" + "鼓掌符號" + "張開雙手符號" + "皇冠" + "女裝帽" + "眼鏡" + "領帶" + "T 恤" + "牛仔褲" + "連身裙" + "和服" + "比基尼" + "女裝" + "皮包" + "手提包" + "小袋子" + "男裝鞋" + "運動鞋" + "高跟鞋" + "女裝涼鞋" + "女裝長靴" + "腳印" + "半身剪影" + "多個半身剪影" + "男孩" + "女孩" + "男人" + "女人" + "家庭" + "手牽手的男女" + "手牽手的男人" + "手牽手的女人" + "警察" + "兔女郎" + "戴著面紗的新娘" + "金髮的人" + "戴瓜皮帽的男人" + "戴著頭巾的男性" + "男性長者" + "女性長者" + "嬰兒" + "建築工人" + "公主" + "日本魔鬼" + "日本天狗" + "鬼魂" + "嬰兒天使" + "外星人" + "外星怪獸" + "小惡魔" + "骷髏頭" + "服務台人員" + "衛兵" + "舞者" + "唇膏" + "指甲油" + "臉部按摩" + "理髮" + "理髮店招牌" + "注射器" + "藥丸" + "唇印" + "情書" + "戒指" + "寶石" + "親吻" + "花束" + "情侶加上愛心" + "婚禮" + "跳動的心" + "破碎的心" + "兩顆心" + "閃耀的心" + "日益增大的心" + "一箭穿心" + "藍色的心" + "綠色的心" + "黃色的心" + "紫色的心" + "有彩帶的心" + "旋轉的心" + "心形裝飾品" + "中間有一點的鑽石圖案" + "電燈泡" + "生氣符號" + "炸彈" + "睡覺中符號" + "衝突符號" + "噴汗符號" + "小水滴" + "衝刺符號" + "一坨便便" + "展示二頭肌" + "暈眩符號" + "對話框" + "思考框" + "白色的花" + "一百分符號" + "錢包" + "貨幣換算" + "粗黑貨幣符號" + "信用卡" + "有日圓記號的鈔票" + "有美元記號的鈔票" + "有歐元記號的鈔票" + "有英鎊記號的鈔票" + "有翅膀的貨幣" + "有日圓記號且走勢向上的圖表" + "座位" + "個人電腦" + "公事包" + "迷你光碟" + "磁碟" + "光碟" + "DVD" + "檔案資料夾" + "開啟文件夾" + "捲曲的頁面" + "頁面朝上" + "日曆" + "撕頁式日曆" + "卡片索引" + "走勢向上的圖表" + "走勢向下的圖表" + "棒型圖" + "剪貼簿" + "圖釘" + "圓頂圖釘" + "萬字夾" + "直尺" + "三角尺" + "便利貼書籤" + "總帳" + "筆記本" + "有裝飾封面的筆記本" + "合起來的書" + "打開的書" + "綠色的書" + "藍色的書" + "橙色的書" + "圖書" + "名牌" + "捲軸" + "備忘標籤" + "電話聽筒" + "傳呼機" + "傳真機" + "衛星天線" + "大型擴音器" + "傳聲筒加油棒" + "送件匣" + "收件匣" + "包裹" + "電郵符號" + "寄來的信封" + "上方有向下箭嘴的信封" + "關閉而旗幟倒下的信箱" + "關閉而旗幟拉起的信箱" + "開啟而旗幟拉起的信箱" + "開啟而旗幟倒下的信箱" + "郵筒" + "急件喇叭" + "報紙" + "手機" + "左方有向右箭嘴的手機" + "振動模式" + "手機關機" + "禁止使用手機" + "含訊號格的天線" + "相機" + + + "攝影機" + "電視" + "無線電" + "錄影帶" + + + + + "扭轉向右的兩個箭嘴" + "順時針向右和向左繞成開放式圓圈的箭嘴" + "順時針向右和向左繞成開放式圓圈的箭嘴,上面有在圓圈內的 1 字" + "順時針向下和向上繞成開放式圓圈的箭嘴" + "逆時針向下和向上繞成開放式圓圈的箭嘴" + "低亮度符號" + "高亮度符號" + "加上刪除斜線的揚聲器" + "揚聲器" + "有一個聲波符號的揚聲器" + "有三個聲波符號的揚聲器" + "電池" + "電源插頭" + "向左的放大鏡" + "向右的放大鏡" + "墨水筆和鎖" + "鎖與鑰匙" + "鑰匙" + "鎖" + "打開了的鎖" + "鈴噹" + "加上刪除斜線的鈴鐺" + "書籤" + "連結符號" + "圓形按鈕" + "上方有向左箭嘴的「BACK」" + "上方有向左箭嘴的「END」" + "上方有向左、向右箭嘴的「ON」和感嘆號" + "上方有向右箭嘴的「SOON」" + "上方有向上箭嘴的「TOP」" + "18 禁符號" + "鍵帽 10" + "大寫拉丁字母輸入符號" + "小寫拉丁字母輸入符號" + "數字輸入符號" + "符號輸入符號" + "拉丁字母輸入符號" + "火" + "電筒" + "士巴拿" + "鐵鎚" + "螺帽與螺栓" + "日式廚刀" + "手槍" + "顯微鏡" + "望遠鏡" + "水晶球" + "中間有一點的六星形" + "日本的新手符號" + "三叉戟圖騰" + "黑色方形按鈕" + "白色方形按鈕" + "大型紅色圓圈" + "大型藍色圓圈" + "大型橙色菱形" + "大型藍色菱形" + "小型橙色菱形" + "小型藍色菱形" + "往上的紅色三角形" + "往下的紅色三角形" + "往上的小型紅色三角形" + "往下的小型紅色三角形" + + + + + + + + + + + + + "顯示為一點的時鐘鐘面" + "顯示為兩點的時鐘鐘面" + "顯示為三點的時鐘鐘面" + "顯示為四點的時鐘鐘面" + "顯示為五點的時鐘鐘面" + "顯示為六點的時鐘鐘面" + "顯示為七點的時鐘鐘面" + "顯示為八點的時鐘鐘面" + "顯示為九點的時鐘鐘面" + "顯示為十點的時鐘鐘面" + "顯示為十一點的時鐘鐘面" + "顯示為十二點的時鐘鐘面" + "顯示為一點辦的時鐘鐘面" + "顯示為兩點半的時鐘鐘面" + "顯示為三點半的時鐘鐘面" + "顯示為四點半的時鐘鐘面" + "顯示為五點半的時鐘鐘面" + "顯示為六點半的時鐘鐘面" + "顯示為七點半的時鐘鐘面" + "顯示為八點半的時鐘鐘面" + "顯示為九點半的時鐘鐘面" + "顯示為十點半的時鐘鐘面" + "顯示為十一點半的時鐘鐘面" + "顯示為十二點半的時鐘鐘面" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "富士山" + "東京鐵塔" + "自由女神像" + "日本的剪影" + "澀谷仿摩艾石像" + "露齒而笑的笑臉" + "帶著微笑眼睛且露齒而笑的笑臉" + "流下開心淚水的笑臉" + "張嘴的笑臉" + "張嘴且帶著笑意的笑臉" + "張嘴且冒著冷汗的笑臉" + "張嘴閉眼的笑臉" + "頭頂光環的笑臉" + "頭上長角的笑臉" + "眨眼睛的表情" + "眼帶笑意的笑臉" + "嚐到美食的表情" + "安心的表情" + "眼睛呈現心形的笑臉" + "戴著太陽眼鏡的笑臉" + "嘻笑的臉" + "無表情" + "面無表情的臉" + "不帶一絲愉悅的表情" + "冒冷汗的表情" + "沈思的表情" + "困惑的表情" + "狼狽的表情" + "親吻的表情" + "送出飛吻的表情" + "笑咪咪親吻的表情" + "閉眼親吻的表情" + "伸舌的表情" + "伸舌眨眼的表情" + "伸舌閉眼的表情" + "失望的表情" + "擔心的表情" + "生氣的表情" + "面露不悅的表情" + "嚎啕大哭" + "固執己見的表情" + "洋洋得意的表情" + "失望卻又安心的表情" + "皺眉張嘴的表情" + "苦惱的表情" + "害怕的表情" + "精疲力竭的表情" + "想睡的表情" + "疲倦的表情" + "做鬼臉" + "大聲哭泣的表情" + "張嘴的表情" + "噤聲的表情" + "張嘴且冒著冷汗的表情" + "因害怕而尖叫的表情" + "驚訝的表情" + "興奮的表情" + "熟睡的表情" + "頭暈目眩的表情" + "沒有嘴巴的臉" + "帶著口罩的臉" + "眼睛微笑且露齒而笑的貓臉" + "喜極而泣的貓臉" + "露齒而笑的貓臉" + "微笑時眼睛呈心形的貓臉" + "帶著諷刺微笑的貓臉" + "閉著眼睛親吻的貓臉" + "生氣的貓臉" + "正在哭泣的貓臉" + "精疲力竭的貓臉" + + + + + + + + + "帶有「不好」手勢的臉" + "帶有 OK 手勢的臉" + "深深鞠躬的人" + "非禮勿視的猴子" + "非禮勿聽的猴子" + "非禮勿言的猴子" + "快樂且高舉單手的人" + "高舉雙手慶祝的人" + "皺眉的人" + "面露不悅的人" + "雙手交叉的人" + "火箭" + "直升機" + "蒸汽火車頭" + "火車車廂" + "高鐵列車" + "子彈火車" + "火車" + "地鐵" + "輕鐵" + "車站" + "電車" + "電車車廂" + "巴士" + "迎面駛來的巴士" + "無軌電車" + "巴士站" + "小巴" + "救護車" + "消防車" + "警車" + "迎面駛來的警車" + "的士" + "迎面駛來的的士" + "汽車" + "迎面駛來的汽車" + "露營車" + "運輸貨車" + "拖斗貨車" + "拖拉機" + "單軌鐵路" + "山區火車" + "懸掛式鐵路" + "山區纜車" + "架空纜車" + "船隻" + "划艇" + "快艇" + "橫向紅綠燈" + "垂直式紅綠燈" + "施工標誌" + "旋轉的警車警示燈" + "郵筒上的三角旗" + "門" + "禁止進入標誌" + "吸煙區符號" + "禁煙符號" + "正確捨棄垃圾標誌" + "請勿亂丟垃圾標誌" + "飲用水標誌" + "非飲用水標誌" + "單車" + "禁止單車" + "單車騎士" + "越野單車騎士" + "行人" + "禁止行人通過" + "注意兒童穿越馬路" + "男性符號" + "女性符號" + "廁所" + "嬰兒符號" + "廁所" + "盥洗室" + "淋浴間" + "浸浴" + "浴缸" + "護照檢查處" + "海關" + "行李領取處" + "行李寄存處" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-zh-rHK/strings-letter-descriptions.xml b/app/src/main/res/values-zh-rHK/strings-letter-descriptions.xml new file mode 100644 index 00000000..83ea28bf --- /dev/null +++ b/app/src/main/res/values-zh-rHK/strings-letter-descriptions.xml @@ -0,0 +1,208 @@ + + + + + + + "雌性符號" + "Micro 符號" + "雄性符號" + "清音 S" + "抑音符 A" + "銳音符 A" + "抑揚符 A" + "波浪號 A" + "分音符 A" + "帶上圓圈的 A" + "連字 AE" + "下尾符 C" + "抑音符 E" + "銳音符 E" + "抑揚符 E" + "分音符 E" + "抑音符 I" + "銳音符 I" + "抑揚符 I" + "分音符 I" + "Eth 符號" + "波浪號 N" + "抑音符 O" + "銳音符 O" + "抑揚符 O" + "波浪號 O" + "分音符 O" + "帶斜線的 O" + "抑音符 U" + "銳音符 U" + "抑揚符 U" + "分音符 U" + "銳音符 Y" + "Thorn 符號" + "分音符 Y" + "長音符 A" + "短音符 A" + "反下尾符 A" + "銳音符 C" + "抑揚符 C" + "上點 C" + "上勾符 C" + "上勾符 D" + "帶斜線的 D" + "長音符 E" + "短音符 E" + "上點 E" + "反下尾符 E" + "上勾符 E" + "抑揚符 G" + "短音符 G" + "上點 G" + "下尾符 G" + "抑揚符 H" + "帶斜線的 H" + "波浪號 I" + "長音符 I" + "短音符 I" + "反下尾符 I" + "無點 I" + "連字 IJ" + "抑揚符 J" + "下尾符 K" + "Kra 符號" + "銳音符 L" + "下尾符 L" + "上勾符 L" + "中點 L" + "帶斜線的 L" + "銳音符 N" + "下尾符 N" + "上勾符 N" + "前接縮寫符號的 N" + "Eng 符號" + "長音符 O" + "短音符 O" + "雙銳音符 O" + "連字 OE" + "銳音符 R" + "下尾符 R" + "上勾符 R" + "銳音符 S" + "抑揚符 S" + "下尾符 S" + "上勾符 S" + "下尾符 T" + "上勾符 T" + "帶斜線的 T" + "波浪號 U" + "長音符 U" + "短音符 U" + "帶上圓圈的 U" + "雙銳音符 U" + "反下尾符 U" + "抑揚符 W" + "抑揚符 Y" + "銳音符 Z" + "上點 Z" + "上勾符 Z" + "長 S" + "帶尖角的 O" + "帶尖角的 U" + "下加逗號的 S" + "下加逗號的 T" + "中性母音符號" + "下點 A" + "上方加勾的 A" + "帶抑揚符和銳音符的 A" + "帶抑揚符和抑音符的 A" + "帶抑揚符和上方加勾的 A" + "帶抑揚符和波浪號的 A" + "帶抑揚符和下點的 A" + "帶短音符和銳音符的 A" + "帶短音符和抑音符的 A" + "帶短音符和上方加勾的 A" + "帶短音符和波浪號的 A" + "帶短音符和下點的 A" + "下點 E" + "上方加勾的 E" + "波浪號 E" + "帶抑揚符和銳音符的 E" + "帶抑揚符和抑音符的 E" + "帶抑揚符和上方加勾的 E" + "帶抑揚符和波浪號的 E" + "帶抑揚符和下點的 E" + "上方加勾的 I" + "下點 I" + "下點 O" + "上方加勾的 O" + "帶抑揚符和銳音符的 O" + "帶抑揚符和抑音符的 O" + "帶抑揚符和上方加勾的 O" + "帶抑揚符和波浪號的 O" + "帶抑揚符和下點的 O" + "帶尖角和銳音符的 O" + "帶尖角和抑音符的 O" + "帶尖角和上方加勾的 O" + "帶尖角和波浪號的 O" + "帶尖角和下點的 O" + "下點 U" + "上方加勾的 U" + "帶尖角和銳音符的 U" + "帶尖角和抑音符的 U" + "帶尖角和上方加勾的 U" + "帶尖角和波浪號的 U" + "帶尖角和下點的 U" + "抑音符 Y" + "下點 Y" + "上方加勾的 Y" + "波浪號 Y" + "倒置的驚嘆號" + "左雙角括號" + "中間點" + "上標字 1" + "右雙角括號" + "倒問號" + "左單引號" + "右單引號" + "Low-9 單引號" + "左雙引號" + "右雙引號" + "單劍註釋符號" + "雙劍註釋符號" + "千分比符號" + "單撇" + "雙撇" + "左單角括號" + "右單角括號" + "上標 4" + "上標拉丁小寫字母 n" + "比索符號" + "百分比" + "向右箭頭" + "向下箭頭" + "空集合" + "增量" + "小於或等於" + "大於或等於" + "黑星" + diff --git a/app/src/main/res/values-zh-rHK/strings-talkback-descriptions.xml b/app/src/main/res/values-zh-rHK/strings-talkback-descriptions.xml new file mode 100644 index 00000000..3930dd3e --- /dev/null +++ b/app/src/main/res/values-zh-rHK/strings-talkback-descriptions.xml @@ -0,0 +1,96 @@ + + + + + "插上耳機即可聽到系統讀出密碼鍵。" + "目前文字為 %s" + "未輸入文字" + "按「%1$s」可將「%2$s」修正為「%3$s」" + "按「%1$s」可自動修正" + "沒有建議" + "未知的字元" + "Shift 鍵" + "更多符號" + "Shift 鍵" + "符號" + "Shift 鍵" + "刪除" + "符號" + "字母" + "數字" + "設定" + "Tab 鍵" + "空白鍵" + "語音輸入" + "表情圖案" + "Return 鍵" + "搜尋" + "分點符號" + "切換語言" + "下一個" + "上一個" + "Shift 鍵已啟用" + "大寫鎖定已啟用" + "符號模式" + "更多符號模式" + "字母模式" + "撥號模式" + "符號撥號模式" + "鍵盤已隱藏" + "目前顯示的是%s鍵盤" + "日期" + "日期和時間" + "電郵" + "短訊" + "數字" + "電話" + "文字" + "時間" + "網址" + "最近使用過" + "人物" + "物件" + "大自然" + "地點" + "符號" + "旗幟" + "笑臉和人物" + "動物和大自然" + "飲食" + "旅遊和地方" + "活動" + "表情符號" + "大寫 %s" + "大寫 I" + "上方加點的大寫 I" + "未知的符號" + "未知的表情符號" + "無聊" + "尷尬" + "戴著墨鏡" + "驚訝" + "親吻" + "皺眉" + "有可用的替代字元" + "已關閉替代字元" + "有可用的建議字詞" + "已關閉其他建議字詞" + diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml new file mode 100644 index 00000000..f5b7c949 --- /dev/null +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -0,0 +1,212 @@ + + + + + "輸入選項" + "查找聯絡人姓名" + "拼字檢查程式使用您的聯絡人名單中的各項記錄" + "按鍵時震動" + "按鍵時播放音效" + "按鍵時顯示彈出式視窗" + "偏好設定" + "帳戶與私隱權" + "外觀和版面配置" + "手勢輸入" + "文字更正" + "進階" + "主題背景" + "啟用分離式鍵盤" + "Google 鍵盤同步" + "同步功能已啟用" + "同步處理各裝置之間的個人字典" + "立即同步" + "刪除雲端資料" + "刪除您來自 Google 的同步處理資料" + "您的同步處理資料將從雲端刪除,要確定嗎?" + "刪除" + "取消" + "您的個人字典將同步處理並備份至 Google 伺服器。系統可能會收集字詞頻率的統計資料,以協助改善產品。所有資訊的收集和使用均遵循《""Google 私隱權政策""》。" + "請在這部裝置加入 Google 帳戶,以加入此功能。" + "擁有 Google Apps for Business 帳戶的裝置無法使用同步功能" + "切換至其他輸入法" + "語言切換鍵包括其他輸入法" + "語言切換鍵" + "在啟用多種輸入語言時顯示" + "關閉彈出式鍵盤的延遲時間" + "不延遲" + "預設" + "%s 毫秒" + "系統預設" + "建議聯絡人名稱" + "使用「聯絡人」的名稱提供建議與修正" + "個人化建議" + "強化 %s" + "連按兩下空白鍵插入句號" + "只要連續輕按兩下空白鍵,即可插入句號並在後面加入空格" + "自動大寫" + "每句首個字詞大寫" + "個人字典" + "附加字典" + "主要字典" + "顯示更正建議" + "輸入時顯示建議字詞" + "封鎖令人反感的字詞" + "不建議可能令人反感的字詞" + "自動更正" + "自動插入空白鍵和標點符號鍵盤,以修正拼字錯誤" + "關閉" + "普通模式" + "加強模式" + "極度加強模式" + "建議下一個字詞" + "根據上一個字詞提供建議" + "啟用手勢輸入功能" + "透過在字母上滑動輸入字詞" + "顯示觸控蹤跡" + "動態浮動預覽" + "在啟用手勢輸入時顯示建議的字詞" + "詞組手勢" + "在手勢輸入過程中,滑過空白鍵即可輸入空格" + "語音輸入鍵" + "尚未啟用語音輸入法,請檢查「語言和輸入」設定。" + "設定輸入法" + "語言" + "說明和意見反映" + "語言" + "再次輕按即可儲存" + "輕按這裡即可儲存" + "可使用字典" + "鍵盤主題" + "切換帳戶" + "未選取任何帳戶" + "目前使用的是 %1$s" + "確定" + "取消" + "登出" + "選取要使用的帳戶" + "英文 (英國)" + "英文 (美國)" + "西班牙文 (美國)" + "印度英文" + "塞爾維亞文 (拉丁文)" + "英文 (英國) (%s)" + "英文 (美國) (%s)" + "西班牙文 (美國) (%s)" + "印度英文 (%s)" + "塞爾維亞文 (%s)" + "%s (傳統)" + "%s (精簡版)" + "無語言 (字母)" + "字母 (QWERTY)" + "字母 (QWERTZ)" + "字母 (AZERTY)" + "字母 (Dvorak)" + "字母 (Colemak)" + "字母 (PC)" + "Emoji" + "鍵盤主題" + "自訂輸入樣式" + "新增樣式" + "新增" + "移除" + "儲存" + "語言" + "配置" + "您必須先啟用自訂輸入樣式,才能開始使用。您要立即啟用嗎?" + "啟用" + "暫時不要" + "已存在相同的輸入樣式:%s" + "按鍵震動時間" + "按鍵音量" + "長按鍵延遲" + "實體鍵盤上使用表情圖案" + "以實體 Alt 鍵顯示表情圖案選項" + "預設" + "歡迎使用「%s」" + "配備手勢輸入功能" + "開始" + "下一步" + "設定「%s」" + "啟用「%s」" + "請在「語言和輸入」設定中勾選「%s」。這將允許裝置執行此應用程式。" + "您已在「語言和輸入」設定中啟用「%s」。因此這個步驟已完成,請繼續執行下一個步驟!" + "在設定中啟用" + "切換至「%s」" + "接著,請選取「%s」作為目前使用的文字輸入方法。" + "切換輸入方法" + "恭喜,一切就緒!" + "現在,您可以在所有最愛的應用程式中使用「%s」輸入文字。" + "設定其他語言" + "完成" + "顯示應用程式圖示" + "在啟動器中顯示圖示" + "字典供應商" + "字典供應商" + "字典服務" + "字典更新資訊" + "附加字典" + "可使用字典" + "字典設定" + "使用者字典" + "使用者字典" + "可使用字典" + "目前下載中" + "已安裝" + "已安裝,但已停用" + "連線至字典服務時發生問題" + "沒有可用的字典" + "重新整理" + "上次更新日期" + "正在檢查更新" + "正在載入…" + "主要字典" + "取消" + "設定" + "安裝" + "取消" + "刪除" + "您流動裝置上所選的語言有字典可供使用。<br/> 我們建議您<b>下載</b>%1$s字典以改善文字輸入體驗。<br/> <br/> 透過 3G 下載可能需時一至兩分鐘。如果您並非使用<b>無限數據計劃</b>,可能需要支付數據費用。<br/> 如果您不清楚您的流動數據計劃內容,建議您連線至 Wi-Fi 網絡以開始自動下載。<br/> <br/> 提示:您可以前往流動裝置「設定」選單中的「語言和輸入」,下載並移除字典。<b></b><b></b>" + "立即下載 (%1$.1f MB)" + "經由 Wi-Fi 下載" + "可使用%1$s字典" + "按下即可查看並下載" + "下載中:%1$s字詞建議服務即將啟用。" + "版本 %1$s" + "新增" + "加入字典" + "詞組" + "更多選項" + "較少選項" + "確定" + "字詞:" + "快速鍵:" + "語言:" + "輸入字詞" + "自選快速鍵" + "編輯字詞" + "編輯" + "刪除" + "您的用戶字典中沒有任何字詞。如要新增字詞,請輕按 [新增] (+) 按鈕。" + "所有語言" + "更多語言..." + "刪除" + " ABCDEFGHIJKLMNOPQRSTUVWXYZ" + diff --git a/app/src/main/res/values-zh-rTW/strings-action-keys.xml b/app/src/main/res/values-zh-rTW/strings-action-keys.xml new file mode 100644 index 00000000..b56dedef --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings-action-keys.xml @@ -0,0 +1,31 @@ + + + + + "執行" + "繼續" + "返回" + "完成" + "傳送" + "搜尋" + "暫停" + "等待" + diff --git a/app/src/main/res/values-zh-rTW/strings-config-important-notice.xml b/app/src/main/res/values-zh-rTW/strings-config-important-notice.xml new file mode 100644 index 00000000..4c94b06f --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings-config-important-notice.xml @@ -0,0 +1,24 @@ + + + + + "根據你的通訊記錄和以往輸入的資料改善建議項目" + diff --git a/app/src/main/res/values-zh-rTW/strings-emoji-descriptions.xml b/app/src/main/res/values-zh-rTW/strings-emoji-descriptions.xml new file mode 100644 index 00000000..fe8056d3 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings-emoji-descriptions.xml @@ -0,0 +1,1767 @@ + + + + + + + "版權符號" + "註冊符號" + "雙驚嘆號" + "驚嘆號和問號" + "商標符號" + "資訊來源" + "左右箭頭" + "上下箭頭" + "西北方箭頭" + "東北方箭頭" + "東南方箭頭" + "西南方箭頭" + "鉤狀向左箭頭" + "鉤狀向右箭頭" + "手錶" + "沙漏" + + + + + "指向右邊的兩個黑色三角形" + "指向左邊的兩個黑色三角形" + "指向上方的兩個黑色三角形" + "指向下方的兩個黑色三角形" + + + + + + + "鬧鐘" + + + + + "沙粒流動中的沙漏" + + + + + + + "圓圈中的大寫字母 M" + "小型黑色正方形" + "小型白色正方形" + "指向右邊的黑色三角形" + "指向左邊的黑色三角形" + "中型白色正方形" + "中型黑色正方形" + "中小型白色正方形" + "中小型黑色正方形" + "黑色的太陽和光線" + "雲朵" + + + + + + + "黑色電話" + "打勾的黑色方塊" + "雨傘和雨滴" + "熱飲" + + + "往上指的白色食指" + + + + + + + + + + + + + + + + + + + "白色笑臉" + "牡羊座" + "金牛座" + "雙子座" + "巨蟹座" + "獅子座" + "處女座" + "天秤座" + "天蠍座" + "射手座" + "摩羯座" + "水瓶座" + "雙魚座" + "黑色的黑桃紙牌花色" + "黑色的梅花紙牌花色" + "黑色的紅心紙牌花色" + "黑色的方塊紙牌花色" + "溫泉" + "黑色的國際通用資源回收符號" + "輪椅符號" + + + "船錨" + + + + + + + + + + + + + "警告標誌" + "高壓電標誌" + "中型白色圓形" + "中型黑色圓形" + + + + + "足球" + "棒球" + "四周沒有下雪的雪人" + "雲後的太陽" + + + "蛇夫座" + + + + + + + "禁止進入" + + + "教堂" + + + + + "噴泉" + "球洞中的旗子" + + + "帆船" + + + + + + + "帳篷" + "燃油泵" + "黑色剪刀" + "加粗的白色打勾記號" + "飛機" + "信封" + "舉起的拳頭" + "舉起的手" + "勝利手勢" + + + "鉛筆" + "黑色鋼筆筆尖" + "加粗的打勾記號" + "加粗的乘法記號" + + + + + "閃光" + "八角星芒" + "黑色八角星形" + "雪花" + "火花" + "交叉記號" + "反白方框打叉記號" + "黑色問號裝飾" + "白色問號裝飾" + "白色驚嘆號裝飾" + "加粗的驚嘆號符號" + + + "加粗的黑色愛心" + "加粗的加號" + "加粗的減號" + "加粗的除號" + "黑色向右箭頭" + "捲曲線" + "雙捲曲線" + "先向右再往上彎曲的箭頭" + "先向右再往下彎曲的箭頭" + "黑色向左箭頭" + "黑色向上箭頭" + "黑色向下箭頭" + "大型黑色正方形" + "大型白色正方形" + "中型白色星星" + "加粗的大型圓形" + "連續波浪號" + "歌謠標記" + "圓圈形的表意字元「祝賀」" + "圓圈形的表意字元「秘密」" + "麻將牌紅中" + "黑色鬼牌" + "血型 A 型" + "血型 B 型" + "血型 O 型" + "停車場" + "血型 AB 型" + "方框 CL" + "方框 COOL" + "方框 FREE" + "方框 ID" + "方框 NEW" + "方框 NG" + "方框 OK" + "方框 SOS" + "方框 UP 和驚嘆號" + "方框 VS" + "方框片假名「這裡」" + "方框片假名「服務」" + "方框表意字元「免費」" + "方框表意字元「對號座」" + "方框表意字元「禁止」" + "方框表意字元「空位」" + "方框表意字元「同意」" + "方框表意字元「已滿」" + "方框表意字元「收費」" + "方框表意字元「每月」" + "方框表意字元「申請」" + "方框表意字元「折扣」" + "方框表意字元「營業中」" + "圓圈形的表意字元「特惠」" + "圓圈形的表意字元「接受」" + "氣旋" + "起霧" + "收起的雨傘" + "星夜" + "山頂日出" + "日出" + "城市暮色" + "高樓日落" + "彩虹" + "夜晚的橋" + "水波" + "火山" + "銀河" + "地球 (歐洲到非洲)" + "地球 (美洲)" + "地球 (亞洲到澳洲)" + "有經線的地球" + "新月符號" + "娥眉月符號" + "上弦月符號" + "盈凸月符號" + "滿月符號" + "虧凸月符號" + "下弦月符號" + "殘月符號" + "月牙" + "有臉的新月" + "有臉的上弦月" + "有臉的下弦月" + "有臉的滿月" + "有臉的太陽" + "發光的星星" + "流星" + + + + + + + + + + + + + + + + + + + + + + + + + + + "栗子" + "樹苗" + "常綠植物" + "落葉植物" + "棕櫚樹" + "仙人掌" + + + "鬱金香" + "櫻花" + "玫瑰" + "扶桑花" + "向日葵" + "花朵" + "玉米" + "稻米" + "香草" + "四葉酢醬草" + "楓葉" + "落葉" + "風中落葉" + "蘑菇" + "番茄" + "茄子" + "葡萄" + "香瓜" + "西瓜" + "柑橘" + "檸檬" + "香蕉" + "鳳梨" + "紅蘋果" + "綠蘋果" + "梨子" + "桃子" + "櫻桃" + "草莓" + "漢堡" + "披薩切片" + "帶骨肉" + "家禽腿肉" + "米脆餅" + "飯糰" + "米飯" + "咖哩飯" + "熱食" + "義大利麵" + "麵包" + "薯條" + "烤地瓜" + "日式糯米丸子" + "關東煮" + "壽司" + "炸蝦" + "鳴門卷" + "霜淇淋" + "刨冰" + "冰淇淋" + "甜甜圈" + "餅乾" + "巧克力棒" + "糖果" + "Lollipop" + "卡士達" + "蜂蜜罐" + "水果蛋糕" + "便當" + "火鍋料理" + "烹飪" + "刀叉" + "茶碗" + "清酒和酒杯" + "葡萄酒杯" + "雞尾酒杯" + "熱帶風情雞尾酒" + "啤酒杯" + "乾杯" + "奶瓶" + + + + + + + "緞帶" + "包裝好的禮物" + "生日蛋糕" + "萬聖節南瓜燈籠" + "聖誕樹" + "聖誕老人" + "煙火" + "仙女棒" + "氣球" + "拉炮" + "五彩碎紙球" + "七夕許願樹" + "交叉的旗子" + "門松" + "日本女兒節人偶" + "鯉魚旗" + "風鈴" + "賞月" + "書包" + "學士帽" + + + + + + + + + + + + + + + "旋轉木馬" + "摩天輪" + "雲霄飛車" + "釣竿和魚" + "麥克風" + "電影攝影機" + "電影" + "耳機" + "調色盤" + "禮帽" + "馬戲團帳篷" + "門票" + "場記板" + "表演藝術" + "電玩遊戲" + "正中紅心" + "吃角子老虎機" + "撞球" + "遊戲骰子" + "保齡球" + "日本花牌" + "音符" + "許多音符" + "薩克斯風" + "吉他" + "鍵盤樂器" + "小號" + "小提琴" + "樂譜" + "有飾帶的跑步運動服" + "網球拍和球" + "滑雪和滑雪靴" + "籃球和籃框" + "賽車方格旗" + "滑雪板" + "跑者" + "衝浪客" + + + "獎盃" + "賽馬" + "美式足球" + "橄欖球" + "游泳者" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "住家建築" + "有庭院的房子" + "辦公建築" + "日本郵局" + "歐洲郵局" + "醫院" + "銀行" + "自動櫃員機" + "飯店" + "賓館" + "便利商店" + "學校" + "百貨公司" + "工廠" + "居酒屋燈籠" + "日式宮殿" + "歐式城堡" + + + + + + + + + + + + + + + + + + + + + + + + + "大型家鼠" + "老鼠" + "牛" + "水牛" + "母牛" + + + "豹" + "兔子" + "貓" + "龍" + "鱷魚" + "鯨魚" + "蝸牛" + "蛇" + "馬" + "公羊" + "山羊" + "綿羊" + "猴子" + "公雞" + "雞" + "狗" + "豬" + "野豬" + "大象" + "章魚" + "螺" + "蟲子" + "螞蟻" + "蜜蜂" + "瓢蟲" + "魚" + "熱帶魚" + "河豚" + "烏龜" + "剛孵化的小雞" + "小雞" + "面向前方的小雞" + "鳥" + "企鵝" + "無尾熊" + "貴賓狗" + "單峰駱駝" + "雙峰駱駝" + "海豚" + "老鼠臉" + "母牛臉" + "老虎臉" + "兔子臉" + "貓臉" + "龍臉" + "噴水的鯨魚" + "馬臉" + "猴子臉" + "狗臉" + "豬臉" + "青蛙臉" + "倉鼠臉" + "狼臉" + "熊臉" + "貓熊臉" + "豬鼻子" + "腳掌印" + + + "眼睛" + + + "耳朵" + "鼻子" + "嘴巴" + "舌頭" + "手背朝前往上指的白色食指" + "手背朝前往下指的白色食指" + "手背朝前往左指的白色食指" + "手背朝前往右指的白色食指" + "握拳符號" + "揮手符號" + "OK 手勢符號" + "拇指朝上符號" + "拇指朝下符號" + "鼓掌符號" + "張開雙手符號" + "皇冠" + "女帽" + "眼鏡" + "領帶" + "T 恤" + "牛仔褲" + "洋裝" + "和服" + "比基尼" + "女裝" + "錢包" + "手提包" + "小袋子" + "男鞋" + "運動鞋" + "高跟鞋" + "女用涼鞋" + "女靴" + "腳印" + "半身像輪廓" + "多個半身像輪廓" + "男孩" + "女孩" + "男人" + "女人" + "家庭" + "一男一女手牽手" + "兩個男人手牽手" + "兩個女人手牽手" + "警察" + "兔耳女郎" + "戴頭紗的新娘" + "金髮的人" + "戴瓜皮帽的男人" + "戴頭巾的男人" + "男性長者" + "女性長者" + "嬰兒" + "建築工人" + "公主" + "日本妖怪" + "日本天狗" + "幽靈" + "小天使" + "外星人" + "外星怪物" + "小惡魔" + "骷髏" + "服務台人員" + "衛兵" + "舞者" + "口紅" + "指甲油" + "臉部按摩" + "剪髮" + "理髮店彩色旋轉燈筒招牌" + "注射器" + "藥丸" + "唇印" + "情書" + "戒指" + "寶石" + "親吻" + "花束" + "情侶和愛心" + "婚禮" + "跳動的心" + "碎裂的心" + "兩顆愛心" + "閃閃發光的愛心" + "變大的愛心" + "一箭穿心" + "藍色愛心" + "綠色愛心" + "黃色愛心" + "紫色愛心" + "綁上緞帶的愛心" + "旋轉的愛心" + "心形裝飾" + "中間有一個點的鑽石形狀" + "電燈泡" + "生氣符號" + "炸彈" + "想睡符號" + "碰撞符號" + "噴汗符號" + "水滴" + "衝刺符號" + "一坨便便" + "股起的二頭肌" + "暈眩符號" + "對話框" + "思考泡泡框" + "白花" + "一百分符號" + "錢袋" + "貨幣兌換" + "大型錢幣符號" + "信用卡" + "有日幣符號的鈔票" + "有美元符號的鈔票" + "有歐元符號的鈔票" + "有英鎊符號的鈔票" + "長翅膀的錢" + "有日圓符號且走勢往上的圖表" + "座位" + "個人電腦" + "公事包" + "迷你光碟" + "磁碟片" + "光碟" + "DVD" + "文件夾" + "開啟的文件夾" + "一角捲起的紙頁" + "正面朝上的紙頁" + "日曆" + "撕頁式日曆" + "卡片索引" + "走勢往上的圖表" + "走勢往下的圖表" + "長條圖" + "剪貼簿" + "圖釘" + "圓頭圖釘" + "迴紋針" + "直尺" + "三角板" + "書籤標籤" + "分類帳" + "筆記本" + "有裝飾性封面的筆記本" + "闔上的書" + "翻開的書" + "綠色的書" + "藍色的書" + "橘色的書" + "書籍" + "識別證" + "卷軸" + "筆記" + "電話話筒" + "呼叫器" + "傳真機" + "衛星天線" + "大聲公" + "傳聲筒加油棒" + "寄件匣" + "收件匣" + "包裹" + "電子郵件符號" + "寄來的信封" + "上方有向下箭頭的信封" + "關閉且已將旗子拉下的信箱" + "關閉且已將旗子升起的信箱" + "開啟且已將旗子升起的信箱" + "開啟且已將旗子降下的信箱" + "郵筒" + "急件喇叭" + "報紙" + "手機" + "左方有向右箭頭的手機" + "振動模式" + "手機關機" + "禁止使用手機" + "含訊號格的天線" + "相機" + + + "攝影機" + "電視" + "收音機" + "錄影帶" + + + + + "扭轉向右的兩個箭頭" + "順時針向右和向左繞成開放式圓圈的箭頭" + "順時針向右和向左繞成開放式圓圈的箭頭,上面有圈起來的 1" + "順時針向下和向上繞成開放式圓圈的箭頭" + "逆時針向下和向上繞成開放式圓圈的箭頭" + "低亮度符號" + "高亮度符號" + "加上刪除斜線的喇叭" + "喇叭" + "有一個聲波符號的喇叭" + "有三個聲波符號的喇叭" + "電池" + "插頭" + "朝向左邊的放大鏡" + "朝向右邊的放大鏡" + "鎖頭和鋼筆" + "關上的鎖頭和鑰匙" + "鑰匙" + "鎖頭" + "打開的鎖頭" + "響鈴" + "加上刪除斜線的響鈴" + "書籤" + "連結符號" + "圓形按鈕" + "上方有向左箭頭的 BACK" + "上方有向左箭頭的 END" + "上方有向左箭頭的 ON 和驚嘆號" + "上面有向右箭頭的 SOON" + "上面有向上箭頭的 TOP" + "18 禁符號" + "鍵帽 10" + "大寫拉丁字母輸入符號" + "小寫拉丁字母輸入符號" + "數字輸入符號" + "符號輸入符號" + "拉丁字母輸入符號" + "火" + "手電筒" + "扳手" + "鐵鎚" + "螺帽與螺栓" + "日式廚刀" + "手槍" + "顯微鏡" + "望遠鏡" + "水晶球" + "中間有一個點的六角星" + "日本的新手符號" + "三插戟圖騰" + "黑色方形按鈕" + "白色方形按鈕" + "大型紅色圓圈" + "大型藍色圓圈" + "大型橘色菱形" + "大型藍色菱形" + "小型橘色菱形" + "小型藍色菱形" + "往上的紅色三角形" + "往下的紅色三角形" + "往上的小型紅色三角形" + "往下的小型紅色三角形" + + + + + + + + + + + + + "顯示為一點的時鐘鐘面" + "顯示為兩點的時鐘鐘面" + "顯示為三點的時鐘鐘面" + "顯示為四點的時鐘鐘面" + "顯示為五點的時鐘鐘面" + "顯示為六點的時鐘鐘面" + "顯示為七點的時鐘鐘面" + "顯示為八點的時鐘鐘面" + "顯示為九點的時鐘鐘面" + "顯示為十點的時鐘鐘面" + "顯示為十一點的時鐘鐘面" + "顯示為十二點的時鐘鐘面" + "顯示為一點半的時鐘鐘面" + "顯示為兩點半的時鐘鐘面" + "顯示為三點半的時鐘鐘面" + "顯示為四點半的時鐘鐘面" + "顯示為五點半的時鐘鐘面" + "顯示為六點半的時鐘鐘面" + "顯示為七點半的時鐘鐘面" + "顯示為八點半的時鐘鐘面" + "顯示為九點半的時鐘鐘面" + "顯示為十點半的時鐘鐘面" + "顯示為十一點半的時鐘鐘面" + "顯示為十二點半的時鐘鐘面" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "富士山" + "東京鐵塔" + "自由女神像" + "日本地圖輪廓" + "澀谷仿摩艾石像" + "露齒而笑的表情" + "眉開眼笑的表情" + "喜極而泣的表情" + "張嘴微笑的表情" + "張嘴眉開眼笑的表情" + "張嘴微笑冒冷汗的表情" + "張嘴微笑眼睛緊閉的表情" + "有光圈的笑臉" + "長角的笑臉" + "眨眼微笑的表情" + "眼睛笑眯眯的表情" + "嚐到美食的表情" + "如釋重負的表情" + "眼睛變成愛心的笑臉" + "戴墨鏡微笑的表情" + "得意的表情" + "心平氣和的表情" + "面無表情的臉" + "無精打采的表情" + "冒冷汗的表情" + "憂慮的表情" + "困惑的表情" + "困惑的表情" + "親吻的表情" + "飛吻的表情" + "笑咪咪親吻的表情" + "眨眼親吻的表情" + "吐舌頭的表情" + "眨眼吐舌頭的表情" + "緊閉眼睛吐舌頭的表情" + "失望的表情" + "擔心的表情" + "生氣的表情" + "噘嘴的表情" + "哭泣的表情" + "堅持的表情" + "洋洋得意的表情" + "失望又鬆了一口氣的表情" + "皺眉張嘴的表情" + "苦惱的表情" + "害怕的表情" + "厭倦的表情" + "想睡的表情" + "疲累的表情" + "扭曲的表情" + "嚎啕大哭的表情" + "張大嘴的表情" + "噤聲的表情" + "張嘴冒冷汗的表情" + "驚恐尖叫的表情" + "吃驚的表情" + "臉紅的表情" + "想睡的表情" + "頭暈眼花的表情" + "沒有嘴巴的臉" + "戴著醫療口罩的臉" + "貓咪眉開眼笑的表情" + "貓咪喜極而泣的表情" + "貓咪張嘴微笑的表情" + "貓咪眼睛變成愛心的笑臉" + "貓咪狡猾微笑的表情" + "貓咪眨眼親吻的表情" + "貓咪噘嘴的表情" + "貓咪哭泣的表情" + "貓咪厭倦的表情" + + + + + + + + + "比出拒絕手勢的人" + "比出 OK 手勢的人" + "深深鞠躬的人" + "代表非禮勿視的猴子" + "代表非禮勿聽的猴子" + "代表非禮勿言的猴子" + "開心地舉起一隻手的人" + "舉起雙手慶祝的人" + "皺眉的人" + "噘嘴的人" + "雙手合十的人" + "火箭" + "直昇機" + "蒸汽火車頭" + "火車車廂" + "高速鐵路列車" + "子彈頭高速鐵路列車" + "火車" + "地下鐵路" + "輕軌鐵路" + "車站" + "電車" + "電車車廂" + "公車" + "迎面駛來的公車" + "無軌電車" + "公車站牌" + "小型巴士" + "救護車" + "消防車" + "警車" + "迎面駛來的警車" + "計程車" + "迎面駛來的計程車" + "汽車" + "迎面駛來的汽車" + "休旅露營車" + "運貨車" + "鉸接式卡車" + "拖拉機" + "單軌鐵路" + "山區火車" + "懸掛式鐵路" + "山區纜車" + "空中纜車" + "船隻" + "划槳船" + "快艇" + "水平式紅綠燈" + "垂直式紅綠燈" + "施工標誌" + "旋轉的警車警示燈" + "旗杆上的三角旗" + "門" + "禁止進入標誌" + "吸菸標誌" + "禁菸標誌" + "請正確丟棄垃圾標誌" + "請勿亂丟垃圾標誌" + "飲用水標誌" + "非飲用水標誌" + "腳踏車" + "禁帶腳踏車" + "腳踏車騎士" + "越野自行車騎士" + "行人" + "禁止行人通行" + "注意兒童穿越馬路" + "男性符號" + "女性符號" + "洗手間" + "嬰兒符號" + "馬桶" + "廁所" + "淋浴間" + "浴室" + "浴缸" + "護照查驗處" + "海關" + "行李領取處" + "行李寄存處" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-zh-rTW/strings-letter-descriptions.xml b/app/src/main/res/values-zh-rTW/strings-letter-descriptions.xml new file mode 100644 index 00000000..c38aa38e --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings-letter-descriptions.xml @@ -0,0 +1,208 @@ + + + + + + + "雌性符號" + "Micro 符號" + "雄性符號" + "清音 S" + "抑音符 A" + "銳音符 A" + "揚抑符 A" + "顎化符 A" + "分音符 A" + "帶上圓圈的 A" + "連字 AE" + "下加符 C" + "抑音符 E" + "銳音符 E" + "揚抑符 E" + "分音符 E" + "抑音符 I" + "銳音符 I" + "揚抑符 I" + "分音符 I" + "濁齒擦音符號" + "顎化符 N" + "抑音符 O" + "銳音符 O" + "揚抑符 O" + "顎化符 O" + "分音符 O" + "帶斜線的 O" + "抑音符 U" + "銳音符 U" + "揚抑符 U" + "分音符 U" + "銳音符 Y" + "刺形符號" + "分音符 Y" + "長音符 A" + "短音符 A" + "反下加符 A" + "銳音符 C" + "揚抑符 C" + "上點 C" + "倒折音符 C" + "倒折音符 D" + "帶斜線的 D" + "長音符 E" + "短音符 E" + "上點 E" + "反下加符 E" + "倒折音符 E" + "揚抑符 G" + "短音符 G" + "上點 G" + "下加符 G" + "揚抑符 H" + "帶斜線的 H" + "顎化符 I" + "長音符 I" + "短音符 I" + "反下加符 I" + "無點 I" + "連字 IJ" + "揚抑符 J" + "下加符 K" + "Kra 符號" + "銳音符 L" + "下加符 L" + "倒折音符 L" + "中點 L" + "帶斜線的 L" + "銳音符 N" + "下加符 N" + "倒折音符 N" + "前接單引號的 N" + "軟顎鼻音" + "長音符 O" + "短音符 O" + "雙銳音符 O" + "連字 OE" + "銳音符 R" + "下加符 R" + "倒折音符 R" + "銳音符 S" + "揚抑符 S" + "下加符 S" + "倒折音符 S" + "下加符 T" + "倒折音符 T" + "帶斜線的 T" + "顎化符 U" + "長音符 U" + "短音符 U" + "帶上圓圈的 U" + "雙銳音符 U" + "反下加符 U" + "揚抑符 W" + "揚抑符 Y" + "銳音符 Z" + "上點 Z" + "倒折音符 Z" + "長音 S" + "帶觸角的 O" + "帶觸角的 U" + "下加逗號的 S" + "下加逗號的 T" + "中性母音符號" + "下點 A" + "上方加勾的 A" + "帶揚抑符和銳音符的 A" + "帶揚抑符和抑音符的 A" + "帶揚抑符和上勾的 A" + "帶揚抑符和顎化符的 A" + "帶揚抑符和下點的 A" + "帶短音符和銳音符的 A" + "帶短音符和抑音符的 A" + "帶短音符和上勾的 A" + "帶短音符和顎化符的 A" + "帶短音符和下點的 A" + "下點 E" + "上方加勾的 E" + "顎化符 E" + "帶揚抑符和銳音符的 E" + "帶揚抑符和抑音符的 E" + "帶揚抑符和上勾的 E" + "帶揚抑符和顎化符的 E" + "帶揚抑符和下點的 E" + "上方加勾的 I" + "下點 I" + "下點 O" + "上方加勾的 O" + "帶揚抑符和銳音符的 O" + "帶揚抑符和抑音符的 O" + "帶揚抑符和上勾的 O" + "帶揚抑符和顎化符的 O" + "帶揚抑符和下點的 O" + "帶觸角和銳音符的 O" + "帶觸角和抑音符的 O" + "帶觸角和上勾的 O" + "帶觸角和顎化符的 O" + "帶觸角和下點的 O" + "下點 U" + "上方加勾的 U" + "帶觸角和銳音符的 U" + "帶觸角和抑音符的 U" + "帶觸角和上勾的 U" + "帶觸角和顎化符的 U" + "帶觸角和下點的 U" + "抑音符 Y" + "下點 Y" + "上方加勾的 Y" + "顎化符 Y" + "倒置的驚嘆號" + "左雙角括號" + "中間點" + "上標字 1" + "右雙角括號" + "倒置的問號" + "左單引號" + "右單引號" + "Low-9 單引號" + "左雙引號" + "右雙引號" + "單劍註釋符號" + "雙劍註釋符號" + "千分比符號" + "角分符號" + "角秒符號" + "左英文單引號" + "右英文單引號" + "上標 4" + "上標小寫拉丁字母 N" + "披索符號" + "轉交符號" + "向右箭頭" + "向下箭頭" + "空集合" + "遞增" + "小於或等於" + "大於或等於" + "黑星" + diff --git a/app/src/main/res/values-zh-rTW/strings-talkback-descriptions.xml b/app/src/main/res/values-zh-rTW/strings-talkback-descriptions.xml new file mode 100644 index 00000000..3a722237 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings-talkback-descriptions.xml @@ -0,0 +1,96 @@ + + + + + "連接耳機即可聽取系統朗讀密碼按鍵。" + "目前文字為 %s" + "未輸入文字" + "按下「%1$s」可將「%2$s」修正為「%3$s」" + "按下「%1$s」可執行自動修正" + "沒有任何建議" + "未知的字元" + "Shift 鍵" + "更多符號" + "Shift 鍵" + "符號" + "Shift 鍵" + "刪除" + "符號" + "字母" + "數字" + "設定" + "Tab 鍵" + "空格鍵" + "語音輸入" + "表情符號" + "返回" + "搜尋" + "圓點符號" + "切換語言" + "下一個" + "上一個" + "Shift 鍵已啟用" + "大寫鎖定已啟用" + "符號模式" + "更多符號模式" + "字母模式" + "撥號模式" + "撥號符號模式" + "鍵盤已隱藏" + "目前顯示的是%s鍵盤" + "日期" + "日期和時間" + "電子郵件" + "簡訊" + "數字" + "電話號碼" + "文字" + "時間" + "網址" + "最近使用過" + "人物" + "物體" + "自然" + "地點" + "符號" + "旗幟" + "表情符號與人物" + "動物與大自然" + "美食佳飲" + "旅行與地點" + "活動" + "表情" + "大寫 %s" + "大寫 I" + "上方加點的大寫 I" + "未知的符號" + "未知的表情符號" + "無聊" + "尷尬" + "戴墨鏡" + "驚訝" + "親吻" + "皺眉" + "有可用的替代字元" + "已關閉替代字元" + "有可用的建議字詞" + "已關閉其他建議字詞" + diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 00000000..2ef81f60 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,212 @@ + + + + + "輸入選項" + "查詢聯絡人姓名" + "拼字檢查程式使用你的聯絡人清單項目" + "按鍵時震動" + "按鍵聲音" + "按鍵時顯示大型字體" + "偏好設定" + "帳戶與隱私權" + "外觀與版面配置" + "手勢輸入" + "文字修正" + "進階" + "主題" + "啟用分離式鍵盤" + "Google 鍵盤同步處理" + "同步處理功能已開啟" + "在各裝置上同步處理你的個人字典" + "立即同步處理" + "刪除雲端資料" + "將已同步處理的資料從 Google 刪除" + "已同步處理的資料將從雲端中刪除。你確定嗎?" + "刪除" + "取消" + "系統將會同步處理你的個人字典並備份至 Google 伺服器,同時也會收集字詞頻率的統計資訊,藉此改善我們的產品品質。所有資訊的收集和使用方式都會遵守《""Google 隱私權政策""》的規定。" + "請將 Google 帳戶新增到這個裝置以啟用這項功能" + "採用 Google Apps for Business 帳戶的裝置無法使用同步處理功能" + "切換到其他輸入法" + "使語言切換鍵包含其他輸入法" + "語言切換鍵" + "有多種輸入語言可選用時顯示切換鍵" + "關閉彈出式鍵盤的延遲時間" + "不延遲" + "預設" + "%s 毫秒" + "系統預設" + "聯絡人姓名建議" + "根據「聯絡人」名稱提供建議與修正" + "個人化建議" + "協助改善 %s" + "輕觸兩下空格鍵即插入句號" + "輕觸兩下空格鍵可插入句號另加一個空格" + "自動大寫" + "句首字詞大寫" + "個人字典" + "外掛字典" + "主要字典" + "顯示修正建議" + "輸入時顯示建議字詞" + "封鎖令人反感的字詞" + "不建議可能令人反感的字詞" + "自動修正" + "按空白鍵或標點符號時,自動修正前面的錯字" + "關閉" + "更正範圍小" + "大幅更正" + "極大幅度更正" + "建議下一個字詞" + "根據前一個字詞提供建議" + "啟用滑行輸入" + "以滑動方式寫出字詞中字母來輸入字詞" + "顯示手勢軌跡" + "動態浮動預覽" + "使用滑行輸入時顯示建議字詞" + "詞組手勢" + "手勢輸入時,滑過空格鍵即可輸入空格" + "語音輸入按鍵" + "尚未啟用語音輸入法,請檢查語言與輸入設定。" + "設定輸入法" + "語言" + "說明與意見回饋" + "語言" + "再次輕觸即可儲存" + "輕觸這裡即可儲存" + "可用的字典" + "鍵盤主題" + "切換帳戶" + "尚未選取帳戶" + "目前使用的帳戶是 %1$s" + "確定" + "取消" + "登出" + "選取要使用的帳戶" + "英文 (英國)" + "英文 (美國)" + "西班牙文 (美國)" + "印度英文" + "塞爾維亞文 (拉丁文)" + "英文 (英國) (%s)" + "英文 (美國) (%s)" + "西班牙文 (美國) (%s)" + "印度英文 (%s)" + "塞爾維亞文 (%s)" + "%s (傳統)" + "%s (精簡)" + "無語言 (字母)" + "字母 (QWERTY)" + "字母 (QWERTZ)" + "字母 (AZERTY)" + "字母 (Dvorak)" + "字母 (Colemak)" + "字母 (PC)" + "表情符號" + "鍵盤主題" + "自訂輸入樣式" + "新增樣式" + "新增" + "移除" + "儲存" + "語言" + "配置" + "你必須先啟用自訂輸入樣式,才能開始使用。你要立即啟用嗎?" + "啟用" + "暫時不要" + "已存在相同的輸入樣式:%s" + "按鍵震動持續時間" + "按鍵音量" + "按鍵長按延遲" + "透過實體鍵盤啟用表情符號" + "按下實體 Alt 鍵即顯示表情符號面板" + "預設" + "歡迎使用 %s" + "含滑行輸入功能" + "開始設定" + "下一步" + "正在設定「%s」" + "啟用「%s」" + "請在 [語言與輸入設定] 中勾選 [%s],授權該應用程式在你的裝置上執行。" + "你已在 [語言與輸入設定] 中啟用 %s,因此這個步驟已經完成。請進行下一個步驟!" + "在設定中啟用" + "切換至「%s」" + "接著,請選取「%s」做為目前使用的文字輸入法。" + "切換輸入法" + "恭喜,你已完成設定!" + "現在,你可以在自己喜愛的所有應用程式中使用「%s」輸入文字。" + "設定其他語言" + "完成" + "顯示應用程式圖示" + "在啟動器中顯示應用程式圖示" + "字典提供者" + "字典提供者" + "字典服務" + "字典更新資訊" + "外掛字典" + "可用的字典" + "字典設定" + "使用者字典" + "使用者字典" + "可用的字典" + "正在下載" + "已安裝" + "已安裝但目前停用" + "連線至字典服務時發生問題" + "沒有可用的字典" + "重新整理" + "上次更新時間:" + "正在檢查更新" + "載入中…" + "主要字典" + "取消" + "設定" + "安裝" + "取消" + "刪除" + "你在行動裝置上選用的語言現有可用的字典。<br/> 建議你<b>下載</b>%1$s字典,藉此強化輸入功能。<br/> <br/> 透過 3G 網路下載約需一兩分鐘。如果你未使用<b>無限行動上網資費方案</b>,可能必須另外付費。<br/> 假使你不確定自己的行動上網資費方案為何,建議你連上 Wi-Fi 網路後再自動下載。<br/> <br/> 提示:前往行動裝置的 [設定]<b></b> 選單並選擇 [語言與輸入設定]<b></b>,即可下載及移除字典。" + "立即下載 (%1$.1fMB)" + "透過 Wi-Fi 下載" + "支援%1$s字典" + "按下即可查看並下載" + "下載中:即將啟用%1$s字詞建議服務。" + "版本 %1$s" + "新增" + "加入字典" + "詞組" + "更多選項" + "較少選項" + "確定" + "字詞:" + "快速鍵:" + "語言:" + "輸入字詞" + "可選用的快速鍵" + "編輯字詞" + "編輯" + "刪除" + "使用者字典中沒有任何字詞。如要新增字詞,請輕觸「新增」(+) 按鈕。" + "所有語言" + "更多語言…" + "刪除" + " ABCDEFGHIJKLMNOPQRSTUVWXYZ" + diff --git a/app/src/main/res/values-zu/bools.xml b/app/src/main/res/values-zu/bools.xml new file mode 100644 index 00000000..840d20c2 --- /dev/null +++ b/app/src/main/res/values-zu/bools.xml @@ -0,0 +1,24 @@ + + + + + true + diff --git a/app/src/main/res/values-zu/strings-action-keys.xml b/app/src/main/res/values-zu/strings-action-keys.xml new file mode 100644 index 00000000..fe36678e --- /dev/null +++ b/app/src/main/res/values-zu/strings-action-keys.xml @@ -0,0 +1,31 @@ + + + + + "Hamba" + "Okulandelayo" + "Okwedlule" + "Kwenziwe" + "Thumela" + "Sesha" + "Misa isikhashana" + "Linda" + diff --git a/app/src/main/res/values-zu/strings-config-important-notice.xml b/app/src/main/res/values-zu/strings-config-important-notice.xml new file mode 100644 index 00000000..0d352006 --- /dev/null +++ b/app/src/main/res/values-zu/strings-config-important-notice.xml @@ -0,0 +1,24 @@ + + + + + "Funda kusukela kwezokuxhumana zakho nedatha ethayiphiwe ukuze uthuthukise iziphakamiso" + diff --git a/app/src/main/res/values-zu/strings-emoji-descriptions.xml b/app/src/main/res/values-zu/strings-emoji-descriptions.xml new file mode 100644 index 00000000..1dfed844 --- /dev/null +++ b/app/src/main/res/values-zu/strings-emoji-descriptions.xml @@ -0,0 +1,1767 @@ + + + + + + + "Uphawu le-copyright" + "Uphawu olubhalisiwe" + "Isababazo kabili" + "Uphawu lesibabazo mbuzo" + "Uphawu lokuhweba" + "Umthombo wolwazi" + "Umcibisholo ongakwesokunxele-nakwesokudla" + "Umcibisholo obheke phezulu naphansi" + "Umcibisholo wenyakatho nentshonalanga" + "Umcibisholo wenyakatho nempumalanga" + "Umcibisholo weningizimu nempumalanga" + "Umcibisholo weningizimu nentshonalanga" + "Umcibisholo ongakwesokunxele nehhuku" + "Umcibisholo ongakwesokudla nehhuku" + "Buka" + "Ingilasi yesikhathi" + + + + + "Unxande okabili omnyama obheke ngakwesokudla" + "Unxande okabili omnyama obheke ngakwesokunxele" + "Unxande okabili omnyama obheke ngaphezulu" + "Unxande okabili omnyama obheke ngezansi" + + + + + + + "Iwashi le-alamu" + + + + + "Ingilasi yesikhathi egeleza isihlabathi" + + + + + + + "Igama elingu-m lofeleba oyingiliziwe" + "Isikwele esincane esimnyama" + "Isikwele esincane esimhlophe" + "Unxande omnyama obheke ngakwesokudla" + "Unxande omnyama obheke ngakwesokunxele" + "Isikwele esimhlophe esimaphakathi" + "Isikwele esimnyama esimaphakathi" + "Isikwele esincane esimaphakathi esimhlophe" + "Isikwele esincane esimaphakathi esimnyama" + "Ilanga elimnyama elinemisebe" + "Ifu" + + + + + + + "Ucingo olumnyama" + "Ibhokisi lokuvota nesheke" + "Isambulela esinamaconsi emvula" + "Isiphuzo esishisayo" + + + "Inkomba emhlophe ebheke phezulu" + + + + + + + + + + + + + + + + + + + "Ubuso obumhlophe obumomothekayo" + "I-Aries" + "I-Taurus" + "I-Gemini" + "I-Cancer" + "I-Leo" + "I-Virgo" + "I-Libra" + "I-Scorpius" + "I-Sagittarius" + "I-Capricorn" + "I-Aquarius" + "I-Pisces" + "Isudi yesipedi esimnyama" + "Isudi yeklabhu emnyama" + "Isudi yenhliziyo emnyama" + "Isudi yedayimane elimnyama" + "Intwasanhlobo eshisayo" + "Uphawu olumnyama lokusebenzisa kabusha jikelele" + "Uphawu lesihlalo sabakhubazekile" + + + "Ihange" + + + + + + + + + + + + + "Uphawu lesexwayiso" + "Uphawu lesilinganiso sikagesi esiphezulu" + "Isiyingi esimhlophe okumaphakathi" + "Isiyingi esimaphakathi esimnyama" + + + + + "Ibhola lezinyawo" + "I-Baseball" + "Indoda yeqhwa engenalo iqhwa" + "Ilanga ngemva kwefu" + + + "I-Ophiuchus" + + + + + + + "Akungenwa" + + + "Isonto" + + + + + "Umthombo" + "Ifulegi kwimbobo" + + + "Isikebhe" + + + + + + + "Itende" + "Isifutho sophethiloli" + "Izikele ezimnyama" + "Uphawu lokuhlola olusindayo olumhlophe" + "Indiza" + "Imvilophi" + "Ingqindi ephakanyisiwe" + "Isandla esiphakamisiwe" + "Isandla sokunqoba" + + + "Umsizi" + "Inibhu emnyama" + "Uphawu lokuhlola olusindayo" + "U-x wokuphinda osindayo" + + + + + "Ukucwebezela" + "Izinkanyezi ezihlanganisiwe eziyisishiyagalombili" + "Inkanyezi emnyama yamaphoyinti ayisishiyagalombili" + "Iqhwa" + "Ukukhazimula" + "Uphawu lokushayisana" + "Uphawu lokushayisana lesikwele esingesihle" + "Umhlobiso wophawu lombuzo olumnyama" + "Umhlobiso wophawu lombuzo omhlophe" + "Umhlobisi wesibabazo esimhlophe" + "Uphawu lesibabazo olusindayo" + + + "Inhliziyo emnyama esindayo" + "Uphawu lokuhlanganisa olusindayo" + "Uphawu lokususa olusindayo" + "Uphawu lokwehlukanisa olusindayo" + "Umcibisholo ongakwesokudla omnyama" + "Umumo otshekile" + "Umumo otshekile kabili" + "Umcibisholo obheke ngakwesokudla bese watsheka ngaphezulu" + "Umcibisholo obheke ngakwesokudla bese watsheka ngezansi" + "Umcibisholo omnyama ongakwesokunxele" + "Umncibisholo omnyama ongaphezulu" + "Umcibisholo omnyama obheke phansi" + "Isikwele esikhulu esimnyama" + "Isikwele esikhulu esimhlophe" + "Inkanyezi emaphakathi emhlophe" + "Isiyingi esikhulu esisindayo" + "I-Wavy dash" + "Olunye uphawu lwengxenye" + "Ukuhalalisa i-ideograph eyisiyingi" + "Imfihlo ye-ideograph eyisilingi" + "Isilo esibomvu sethayili ye-Mahjong" + "Ukudlala ikhadi le-joker omnyama" + "Uhlobo legazi A" + "Uhlobo legazi B" + "Uhlobo legazi O" + "Indawo yokupaka" + "Uhlobo legazi AB" + "I-CL eyisikwele" + "Okupholile okuyisikwele" + "Okuyisikwele kwamahhala" + "I-ID eyisikwele" + "Okuyisikwele okusha" + "I-N G eyisikwele" + "U-OK oyisikwele" + "I-SOS eyisikwele" + "Kuhlanganiswe nophawu lesibabazo" + "Okuyisikwele okumelene" + "I-katakana eyisikwele lapha" + "Isevisi ye-katakana eyisikwele" + "Inani lamahhala le-ideograph eyisikwele" + "Isihlalo esibekiwe se-ideograph eyisikwele" + "Ukuvinjelwa kwe-ideograph eyisikwele" + "Isikhala se-ideography eyisikwele" + "Ukwamukelwa kwe-ideograph eyisikwele" + "Ukusetshenziswa ngokugcwele kwe-ideograph eyisikwele" + "I-Ideograph eyisikwele ekhokhelwe" + "I-Ideography eyisikwele yanyanga zonke" + "Uhlelo lokusebenza le-ideograph eyisikwele" + "Isaphulelo se-ideograph eyisikwele" + "I-Ideograph eyisikwele kwezamabhizinisi" + "Inzuzo ye-ideograph eyisiyingi" + "Ukwamukela i-ideograph eyisiyingi" + "Isihlwithi" + "Nenkungu" + "Isambulela esivaliwe" + "Ubusuku obunezinkanyezi" + "Ukuphuma kwelanga ezintabeni" + "Ukuphuma kwelanga" + "Ukubonakala kwedolobha lapho kuhlwa" + "Ukushona kwelanga emabhilidini" + "Uthingo lwenkosazane" + "Ibhuloho ebusuku" + "Igagasi lamanzi" + "Intabamlilo" + "Umthamo wezinkanyezi" + "Imbulunga yomhlaba waseyurophu ne-afrika" + "I-Amelika yembulunga yomhlaba" + "Imbulunga yomhlaba we-asia-australia" + "Imbulunga ye-meridians" + "Uphawu lenyanga entsha" + "Uphawu locwezana lenyanga egcwele" + "Uphawu lenyanga yekota yokuqala" + "Uphawu lenyanga egcwele ye-gibbous" + "Uphawu lenyanga egcwele" + "Uphawu lenyanga ye-gibbous eyehlayo" + "Uphawu lenyanga yekota yokugcina" + "Uphawu lenyanga yocwezana olwehlayo" + "Ucwezana lenyanga" + "Inyanga entsha enobuso" + "Inyanga yekota yokuqala enobuso" + "Inyanga yekota yokugcina enobuso" + "Inyanga egcwele enobuso" + "Ilanga elinobuso" + "Inkanyezi ekhazimulayo" + "Inkanyezi entwizayo" + + + + + + + + + + + + + + + + + + + + + + + + + + + "I-Chestnut" + "Isithombo" + "Isihlahla esihlale siluhlaza" + "Isihlahla esibunayo" + "Isundu" + "Umdolofiya" + + + "I-Tulip" + "Isithundu samathanduluka" + "Irozi" + "I-Hibiscus" + "Ujikanelanga" + "Imbali" + "Indlebe yombila" + "Indlebe yelayisi" + "Isithombo" + "I-Clover yamacembe amane" + "Icembe le-maple" + "Icembe eliwile" + "Icembe eliphephezela emoyeni" + "Inkowankowane" + "Utamatisi" + "I-Aubergine" + "Amagilebhisi" + "Ikhabe" + "Ikhabe" + "I-Tangerine" + "Ilamuni" + "Ubhanana" + "Uphayinaphu" + "I-Apula elibomvu" + "I-Apula eliluhlaza okotshani" + "Iganandoda" + "Ipentshisi" + "Amasheri" + "Ijikijolo" + "I-Hamburger" + "Ucezu le-pizza" + "Inyama enethambo" + "Umlenze wenkukhu" + "Ikhekhe lelayisi" + "Ilayisi eliyibhola" + "Ilayisi eliphekiwe" + "Inani nelayisi" + "Isitsha sokubilisa" + "I-Spaghetti" + "Isinkwa" + "Amazambane athosiwe" + "Ubhatata oyosiwe" + "I-Dango" + "I-Oden" + "I-Sushi" + "Imbaza ethosiwe" + "Ikhekhe lenhlanzi elidizayinwe ngendlela eyaluzayo" + "I-Asikhilimu ethambile" + "Iqhwa eliphuciwe" + "U-Ayisikhilimu" + "Idonathi" + "Ikhukhi" + "Ibha yoshokholethi" + "Uswidi" + "I-Lollipop" + "Ukhastadi" + "Ibhodwe loju" + "Okusaswidi" + "Ibhokisi le-Bento" + "Imbiza yokudla" + "Ukupheka" + "Imfoloko nommese" + "Inkomishi engenaso isibambo" + "Ibhodlela le-Sake nenkomishi" + "Ingilasi yewayini" + "Ingilasi ye-cocktail" + "Isiphuzo esinoshukela" + "Inkomishi yobhiya" + "Ukuqhafaza izinkomishi zobhiya" + "Ibhodlela lomntwana" + + + + + + + "Iribhoni" + "Isipho esigoqiwe" + "Ikhekhe losuku lokuzalwa" + "I-Jack-o-lantern" + "Isihlahla sikakhisimusi" + "Indoda yokhisimusi" + "Iziqhumane" + "Iziqhumane ezibanikayo" + "Ibhamuza" + "Umuntu oqeda ubumnandi" + "Ibhola le-confetti" + "Isihlahla se-Tanabata" + "Amafulegi ashayisanayo" + "Umhlobiso wephayini" + "Onodoli besi-Japanese" + "I-Carp streamer" + "Ushimula" + "Umkhosi wokubuka inyanga" + "Isikhwama sesikole" + "Isigqoko sokuthweswa iziqu" + + + + + + + + + + + + + + + "Umbukiso wamahhashi" + "Isondo lasepaki lokuzijabulisa" + "I-Roller coaster" + "Induku yokudoba nenhlanzi" + "Imakrofoni" + "Ikhamela ye-movie" + "Ibhayisikobho" + "Ihedfoni" + "Iseceshana sompendi sokufaka upende" + "Isigqoko esiphezulu" + "Itende lesekisi" + "Ithikithi" + "Idivayisi yokwenza ifilimu" + "Ukwenza ubuciko" + "Igeyimu yevidiyo" + "Ukushaya ngokuqondile" + "Umshini wokugembula" + "Ama-Billiard" + "Igeyimu yokufa" + "I-Bowling" + "Imbali yokudlala amakhadi" + "Inothi yomculo" + "Amanothi omculo amaningi" + "Icilongo" + "Isiginci" + "Ikhibhodi yomculo" + "Ithrampethi" + "Ivayolini" + "Isikolo somculo" + "Ihembe lokugijima elinebhande" + "Irakhethi yethenisi nebhola" + "Ukusheleleza eqhweni nezicathulo zokusheleleza" + "I-Basketball ne-hoop" + "Ifulegi lemibala eminingi" + "Ibhodi lokusheleleza eqhweni" + "Umgijimi" + "Umtshuzi" + + + "Indondo" + "Umjaho wamahhashi" + "Ibhola lase-America" + "Ibhola lombhoxo" + "Umbhukudi" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "Isakhiwo sendlu" + "Indlu enengadi" + "Ibhilidi lehhovisi" + "Iposi lase-Japanese" + "Iposi lase-European" + "Isibhedlela" + "Ibhange" + "Umshini ozenzakalelayo womthengisi" + "Ihotela" + "Ihotela lothando" + "Isitolo esiphethe zonke izinto" + "Isikole" + "Isitolo somnyango" + "Imboni" + "I-Izakaya lantern" + "Inqaba yesi-Japanese" + "Inqaba yesi-European" + + + + + + + + + + + + + + + + + + + + + + + + + "Igundane" + "Igundane" + "Inkabi" + "Izinyathi" + "Inkomo" + + + "Ingwe" + "Unogwaja" + "Ikati" + "Isilo" + "Ingwenya" + "Umkhomo" + "Umnenke" + "Inyoka" + "Ihhashi" + "Inyamazane yenduna" + "Imbuzi" + "Imvu" + "Inkawu" + "Iqhude" + "Inkukhu" + "Inja" + "Ingulube" + "Izingulube" + "Indlovu" + "Ingwane" + "I-Shell Spiral" + "Imbungulu" + "Intuthwane" + "Inyosi" + "Omanqulwane" + "Inhlanzi" + "Inhlanzi yasendaweni eshisayo" + "I-Blowfish" + "Ufudu" + "Ukuchamisela ichwane" + "Ichwane" + "Ichwane elibheke ngaphambili" + "Inyoni" + "Ihlengethwa" + "I-Koala" + "Uhlobo ou" + "Ikamela le-Dromedary" + "Ikamela le-Bactrian" + "Ihlengethwa" + "Ubuso begundane" + "Ubuso benkomo" + "Ingwe" + "Ubuso bonogwaja" + "Ubuso bekati" + "Ubuso besilo" + "Umkhomo otshuzayo" + "Ubuso behhashi" + "Ubuso bonogwaja" + "Ubuso benja" + "Ubuso bengulube" + "Ubuso bexoxo" + "Ubuso be-hamster" + "Ubuso bempisi" + "Ubuso bebhele" + "Ubuso be-Panda" + "Ikhala lengulube" + "Imikhondo yesidladla" + + + "Amehlo" + + + "Indlebe" + "Ikhala" + "Umlomo" + "Ulimi" + "Okumhlophe okubheke inkomba yesandla sasemuva" + "Okumhlophe okuphasni okubheke inkomba yesandla sasemuva" + "Okumhlophe kwesokunxele okubheke inkomba yesandla sasemuva" + "Okumhlophe kwesokudla okubheke inkomba yesandla sasemuva" + "Uphawu lwesandla esiyingqindi" + "Uphawu lwesandla esiphephezelayo" + "Uphawu lwesandla esibonisa ukuthi kulungile" + "Uphawu lokushaphu" + "Uphawu lokungekho shaphu" + "Uphawu lokushaya izandla" + "Uphawu lwezandla ezivulekile" + "Umqhele" + "Isigqoko sowesifazane" + "Izibuko zamehlo" + "Uthayi" + "Isikibha" + "Obhokhathi" + "Ingubo" + "I-Kimono" + "Ibhikhini" + "Izingubo zabesifazane" + "Isikhwama semali" + "Isikhwama" + "Isikhwama esincane" + "Izicathulo zabesilisa" + "Izicathulo zabadlali" + "Isicathulo sesithende esiphezulu" + "Izicathulo ezivulekile zabesifazane" + "Amabhuthi abesifazane" + "Izinyathelo" + "Isithombe esibonisa ikhanda namahlombe" + "Isithombe esibonisa ikhanda namahlombe" + "Umfana" + "Intombazane" + "Indoda" + "Owesifazane" + "Umndeni" + "Indoda nowesifazane ababambene izandla" + "Amadoda amabili abambene izandla" + "Abesifazane ababili ababambene izandla" + "Ihhovisi" + "Owesifazane onezindlebe ezindizayo" + "Umakoti ofake umgubuzelo" + "Umuntu onezinwele ezimhloshana" + "Indoda ene-gua pi mao" + "Indoda enomshuqulo" + "Indoda esekhulile" + "Owesifazane osekhulile" + "Umntwana" + "Isisebenzi sezokwakha" + "Inkosazana" + "Isilwane esisabekayo sesi-Japanese" + "Isichwe sesi-Japanese" + "Isipoki" + "Ingelosi yomntwana" + "I-Alien esemkhathini" + "I-Alien esabekayo" + "Idemoni" + "Ukhakhayi" + "Umuntu wendawo yosizo" + "Ibutho" + "Umdansi" + "Iluji" + "Ipholishi yenzipho" + "Ukubhucunga ubuso" + "Ukugunda izinwele" + "Uphawu lasendaweni yokugunda" + "Isichatha" + "Iphilisi" + "Uphawu lokuqabula" + "Incwadi yothando" + "Khalisa" + "Itshe eliyigugu" + "Qabula" + "Umqulu wezimbali" + "Izithandani ezinenhliziyo" + "Umshado" + "Inhliziyo eshayayo" + "Inhliziyo ephukile" + "Izinhliziyo ezimbili" + "Inhliziyo ekhazimulayo" + "Inhliziyo ekhulayo" + "Inhliziyo enomcibisholo" + "Inhliziyo eluhlaza okwesibhakabha" + "Inhliziyo eluhlaza okotshani" + "Inhliziyo eliphuzi" + "Inhliziyo ebumsomi" + "Inhliziyo eneribhoni" + "Izinhliziyo ezijikelezayo" + "Umhlobiso wenhliziyo" + "Umumo wedayimane onechashazi ngaphakathi" + "Igilobhu yogesi yokukhanyisa" + "Uphawu lentukuthelo" + "Ibhomu" + "Uphawu lokulala" + "Uphawu lokushayisana" + "Uphawu lokukhipha umjuluko" + "Iconsi" + "Uphawu lodwi" + "Inqwaba yendle" + "Izinkonyane ezigobile" + "Uphawu lwesiyezi" + "Ibhamuza lenkulumo" + "Ibhamuza lomncabango" + "Imbali emhlophe" + "Uphawu lamaphoyinti ayikhulu" + "Isikhwama semali" + "Ukushintshwa kohlobo lemali" + "Uphawu lwedola esindayo" + "Ikhadi lesikweletu" + "Uphawu lemali ye-yen" + "Imali enophawu ledola" + "Uphawu lemali ye-euro" + "Uphawu lemali yephawundi" + "Imali enamaphiko" + "Ishadi elinomkhuba obheke phezulu nophawu le-yen" + "Isihlalo" + "Ikhompyutha yokomuntu siqu" + "Isikhwama" + "Idiski encane" + "Iflophidiski" + "Idiski ebonakalayo" + "I-DVD" + "Ifolda yefayela" + "Vula ifolda yefayela" + "Ikhasi eligoqekile" + "Ikhasi elibheke phezulu" + "Ikhalenda" + "Ikhalenda yokukhipha" + "Inkomba yekhadi" + "Ishadi enomkhuba obheke phezulu" + "Ishadi elinomkhuba obheke phasni" + "Ishadi lebha" + "Ibhodi lokumanathisela" + "Iphini" + "Iphini eyindilinga" + "Okokubamba iphepha" + "Umbusi oqondile" + "Umbusi ongunxantathu" + "Amathebhu ebhukhimakhi" + "Ileja" + "Inothibhuku" + "Inothibhuku enekhava ehlotshisiwe" + "Ibhuku elivaliwe" + "Vula ibhuku" + "Ibhuku eliluhlaza okotshani" + "Ibhuku eliluhlaza okwesibhakabhaka" + "Ibhuku elisawonlintshi" + "Amabhuku" + "Ibheji legama" + "Phenya" + "Imemo" + "Isamukeli socingo" + "Ipheyi" + "Umshini wefeksi" + "I-Antenna yesethalaythi" + "Isandisizwi sekheli elisesidlangalaleni" + "I-Megaphone yenjabulo" + "Itreyi lokuphumayo" + "Itreyi lebhokisi lokungenayo" + "Iphakeji" + "Uphawu lwe-imeyili" + "Imvilophi yokungenayo" + "Imvilophi engenhla enomcibisholo obheke phansi" + "Ibhokisi lemeyili elivaliwe elinefulegi eyehlisiwe" + "Ibhokisi lemeyili elivaliwe elinefulegi ekhushuliwe" + "Vula ibhokisi lemeyili elinefulegi ekhushuliwe" + "Vula ibhokisi lemeyili elinefulegi eyehlisiwe" + "Isigxobo seposi" + "Uphondo lweposi" + "Iphephandaba" + "Iselula" + "Iselula yomcibisholo wokwesokudla ngakwesokunxele" + "Imodi yokudlidliza" + "Iselula icishiwe" + "Awekho amaselula" + "I-Antenna enamabha" + "Ikhamela" + + + "Ikhamela yevidiyo" + "Umabonakude" + "Irediyo" + "Ikhasethi yevidiyo" + + + + + "Imicibisholo ejikiswe ngakwesokudla" + "Imicibisholo yendilinga evulwe ngakwesokudla nangakwesokunxele" + "Imicibisholo yendilinga evulwe ngakwesokudla nangakwesokunxele enombondela eyodwa ezungezile" + "Imicibisholo yendilinga evulwe ngezansi nangaphezulu ehamba njengewashi" + "Imicibisholo yendilinga evulwe ngezansi nangaphezulu ehamba ngokuphambene newashi" + "Uphawu lokukhanya okuphansi" + "Uphawu lokukhanya okuphezulu" + "Isipikha esinomudwa wokukhansela" + "Isipikha" + "Isipikha esinegagasi lomsindo elilodwa" + "Isipikha esinamagagasi omsindo amathathu" + "Ibhethri" + "Ipulagi kagesi" + "Isibonisakhulu esikhombe ngakwesokunxele" + "Isibonisakhulu esikhombe ngakwesokudla" + "Khiya ngepeni loyinki" + "Ilokhi evalwe ngokhiye" + "Ukhiye" + "Khiya" + "Vula ilokhi" + "Isikhalisi" + "Isikhalisi esinomudwa wokukhansela" + "Ibhukhimakhi" + "Isixhumanisi sophawu" + "Inkinobho yerediyo" + "Ngemuva ngomcibisholo ongakwesokunxele ngenhla" + "Phetha ngomcibisholo ongakwesokunxele ngenhla" + "Kuvulwe ngesibabazo esinomcibisholo ongakwesokunxele nesokudla ngenhla" + "Maduze ngomcibisholo ongakwesokudla ngenhla" + "Ngaphezulu ngomcibisholo opheke phezulu ngenhla" + "Akekho ongaphansi kophawu leshumi nesishiyagalombili" + "I-Keycap yeshumi" + "Uphawu lokokufaka lamagama ofeleba be-latin" + "Uphawu lokokufaka lamagama amancane e-latin" + "Uphawu lokokufaka lezinombolo" + "Uphawu lokokufaka lezimpawu" + "Uphawu lokokufaka lamagama e-latin" + "Umlilo" + "Ithoshi logesi" + "Ukudonsa" + "Isando" + "Insinjana nomshudo" + "I-Hocho" + "Iphistoli" + "Isibonakhulu" + "Isibonakude" + "Ibhola le-Crystal" + "Inkanyezi enamaphoyinti ayisithupha nechashazi eliphakathi nendawo" + "Uphawu lesi-Japanese lomqali" + "Isibonakaliso se-Trident" + "Inkinobho emnyama eyisikwele" + "Inkinobho emhlophe eyisikwele" + "Isiyingi esikhulu esibomvu" + "Isiyingi esikhulu esiluhlaza okwesibhakabhaka" + "Idayimane enkulu esawolintshi" + "Idayimane elikhulu eliluhlaza okwesibhakabhaka" + "Idayimane elincane elisawolintshi" + "Idayimane elincane eliluhlaza okwesibhakabhaka" + "Unxande obomvu obheke phezulu" + "Unxande obomvu obheke phansi" + "Unxande omncane obomvu obheke phezulu" + "Unxande omncane obomvu obheke phansi" + + + + + + + + + + + + + "Ubuso bewashi lehora lokuqala ezimpondweni" + "Ubuso bewashi lehora lesibili ezimpondweni" + "Ubuso bewashi lehora lesithathu ezimpondweni" + "Ubuso bewashi lehora lesine ezimpondweni" + "Ubuso bewashi lehora lesithathu ezimpondweni" + "Ubuso bewashi lehora lesithupha ezimpondweni" + "Ubuso bewashi lehora lesikhombisa ezimpondweni" + "Ubuso bewashi lehora lesishiyagalombili ezimpondweni" + "Ubuso bewashi lehora lesishiyagalolunye ezimpondweni" + "Ubuso bewashi lehora leshumi ezimpondweni" + "Ubuso bewashi lehora leshumi nanye ezimpondweni" + "Ubuso bewashi lehora leshumi nambili ezimpondweni" + "Ubuso bewashi bamashumi amathathu nanye" + "Ubuso bewashi bamashumi amathathu nambili" + "Ubuso bewashi bamashumi amathathu nantathu" + "Ubuso bewashi bamashumi amathathu nane" + "Ubuso bewashi bamashumi amathathu nanhlanu" + "Ubuso bewashi bamashumi amathathu nesithupha" + "Ubuso bewashi bamashumi amathathu nesikhombisa" + "Ubuso bewashi bamashumi amathathu nesishiyagalombili" + "Ubuso bewashi bamashumi amathathu nesishiyagalolunye" + "Ubuso bewashi bamashumi amathathu neshumi" + "Ubuso bewashi bamashumi amathathu neshumi nanye" + "Ubuso bewashi bamashumi amathathu neshumi nambili" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + "I-Mount fuji" + "Inqaba yase-Tokyo" + "Isithombe senkululeko" + "I-Silhouette yase-japan" + "I-Moyai" + "Ubuso obuhlinile" + "Ubuso obuhlinile namehlo amomothekayo" + "Ubuso obunezinyembezi zenjabulo" + "Ubuso obumomothekayo nomlomo ovuliwe" + "Ubuso obumomothekayo nomlomo ovuliwe namehlo amomothekyo" + "Ubuso obumomothekayo nomlomo ovulekile nomjuluko obandayo" + "Ubuso obumomothekayo nomlomo ovulekile namehlo avalwe kakhulu" + "Umlomo obumomothekayo obingelelayo" + "Ubuso obumomothekayo obunezimpondo" + "Ubuso obucimezayo" + "Ubuso obumomothekayo namehlo amomothekayo" + "Ubuso obujabulela ukudla okumnandi" + "Ubuso obukhululekile" + "Ubuso obumomothekayo namehla amise okwenhliziyo" + "Ubuso obumomothekayo nezibuko zelanga" + "Ubuso obusinekayo" + "Ubuso obungathathi hlangothi" + "Ubuso obungabonisi mizwa" + "Ubuso obungajabulile" + "Ubuso obunomjuluko obondayo" + "Ubuso obujulayo" + "Ubuso obudidekile" + "Ubuso bokugcizelela" + "Ubuso bokuqabula" + "Ubuso obufutha ukuqabula" + "Ubuso obuqabulayo namehlo amomothekayo" + "Ubuso obuqabulayo namehlo avaliwe" + "Ubuso bolimi obukhishwe ngaphandle" + "Ubuso bolimi olukhishwe ngaphandle nehlo elicwayizayo" + "Ubuso nolimi olukhishwe ngaphandle namehlo avalwe kakhulu" + "Ubuso obudumele" + "Ubuso obukhathazekile" + "Ubuso obucasukile" + "Ubuso bomlomo ophukuliwe" + "Ubuso obukhalayo" + "Ubuso obubekezelayo" + "Ubuso bokunqoba" + "Ubuso obuphoxekile kodwa obukhululekile" + "Ubuso obuhlinayo nomlomo ovuliwe" + "Ubuso obukhathazekile" + "Ubuso obethukile" + "Ubuso obukhathele" + "Ubuso obuzelayo" + "Ubuso obukhathele" + "Ubuso obuhlinayo" + "Ubuso obukhalela phezulu" + "Ubuso bomlomo ovulekile" + "Ubuso obugcwele usizi" + "Ubuso bomlomo ovulekile nomjuluko obandayo" + "Ubuso obumemezayo ngenxa yokwesaba" + "Ubuso obumangele" + "Ubuso obucobekile" + "Ubuso obulele" + "Ubuso obunesiyezi" + "Ubuso obungenawo umlomo" + "Ubuso obunesembozo sezempilo" + "Ubuso bekati elihlinile namehlo amomothekayo" + "Ubuso bekati nezinyembezi zenjabulo" + "Ubuso bekati nomlomo ovulekile" + "Ubuso bekati obumomothekayo namehlo amise okwenhliziyo" + "Ubuso bekati nokumomotheka obubhuqayo" + "Ubuso bekati bokuqabula namehlo avaliwe" + "Ubuso bekati bokuphukula" + "Ubuso bekati obukhalayo" + "Ubuso bekati obukhathele" + + + + + + + + + "Ubuso obungenaso isenzo esihle" + "Ubuso besenzo esilungile" + "Umuntu ogobe ngokujulile" + "Ukungaboni unogwaja omubi" + "Ukungezwa unogwaja omubi" + "Ungakhulumi unogwaja omubi" + "Umuntu ojabulile ophakamisa isandla esisodwa" + "Umuntu ophakamise izandla ezimbili ngenxa yenjabulo" + "Umuntu ohlinayo" + "Umuntu onobuso obuphukulayo" + "Umuntu nezandla ezigoqiwe" + "Irokhethi" + "Indiza" + "Intuthu yesitimela" + "Imoto yojantshi" + "Isitimela sejubane elisheshayo" + "Isitimela sejubane elisheshayo nekhala elicijile" + "Isitimela" + "I-Metro" + "Ujantshi olula" + "Isiteshi" + "Ithilamu" + "Imoto yethilamu" + "Ibhasi" + "Ibhasi ezayo" + "Ibhasi yenqola" + "Isitobhi sebhasi" + "Ikhumbi" + "I-Ambulensi" + "Injini yomlilo" + "Imoto yamaphoyisa" + "Imoto yamaphoyisa ezayo" + "Itekisi" + "Itekisi ezayo" + "Izimoto" + "Izimoto ezizayo" + "Imoto yokuzithokozisa" + "Iloli yokuthumela" + "Iloli enezingxenye ezimbili" + "Ugandaganda" + "Ujantshi omncane" + "Ujantshi wasentabeni" + "Ukumiswa kojantshi" + "Imoto yekhubula yasentabeni" + "Indlela yethilamu ye-Aerial" + "Umkhumbi" + "Isikebhe somdlalo" + "Isikebhe esigijimayo" + "Ilobhothi elivundlile" + "Ilobhothi emile" + "Uphawu lesakhiwo" + "Izimoto zamaphoyisa ezikhanyisayo" + "Ifulegi engunxantathu eposini" + "Umnyango" + "Alukho uphawu lokungena" + "Uphawu lokubhema" + "Uphawu lokungabhemi" + "Uphawu lokubeka udoti endaweni yawo" + "Uphawu lokungalahleli phansi" + "Uphawu lamanzi aphuzwayo" + "Uphawu lamanzi angaphuzwa" + "Ibhayisikili" + "Awekho amabhayisikili" + "Umqhubi webhayisikili" + "Umqhubi webhayisikili entabeni" + "Abahamba ngezinyawo" + "Abavumelekile abahamba ngezinyawo" + "Indawo yokuhamba izingane" + "Uphawu lamadoda" + "Uphawu labesifazane" + "Indawo yangasese" + "Uphawu labantwana" + "Indlu yangasese" + "Ikhosambana lamanzi" + "Ishawa" + "Ubhavu" + "Ubhavu" + "Ukulawula iphasiphothi" + "Umnyango wezimpahla" + "Ukulanda umthwalo" + "Umthwalo oshiyiwe" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-zu/strings-letter-descriptions.xml b/app/src/main/res/values-zu/strings-letter-descriptions.xml new file mode 100644 index 00000000..54eb6e5f --- /dev/null +++ b/app/src/main/res/values-zu/strings-letter-descriptions.xml @@ -0,0 +1,208 @@ + + + + + + + "Isikhombi sobesifazane" + "Uphawu olukhulu" + "Isikhombi sowesilisa" + "Sharp S" + "A, grave" + "A, acute" + "A, circumflex" + "A, tilde" + "A, diaeresis" + "A, ring ngenhla" + "A, E, ligature" + "C, cedilla" + "E, grave" + "E, acute" + "E, circumflex" + "E, diaeresis" + "I, grave" + "I, acute" + "I, circumflex" + "I, diaeresis" + "Eth" + "N, tilde" + "O, grave" + "O, acute" + "O, circumflex" + "O, tilde" + "O, diaeresis" + "O, stroke" + "U, grave" + "U, acute" + "U, circumflex" + "U, diaeresis" + "Y, acute" + "Thorn" + "Y, diaeresis" + "A, macron" + "A, breve" + "A, ogonek" + "C, acute" + "C, circumflex" + "C, icashazi ngenhla" + "C, caron" + "D, caron" + "D, stroke" + "E, macron" + "E, breve" + "E, icashazi ngenhla" + "E, ogonek" + "E, caron" + "G, circumflex" + "G, breve" + "G, icashazi ngenhla" + "G, cedilla" + "H, circumflex" + "H, stroke" + "I, tilde" + "I, macron" + "I, breve" + "I, ogonek" + "Dotless I" + "I, J, ligature" + "J, circumflex" + "K, cedilla" + "Kra" + "L, acute" + "L, cedilla" + "L, caron" + "L, icashazi eliphakathi" + "L, stroke" + "N, acute" + "N, cedilla" + "N, caron" + "N, kulandele i-apostrophe" + "Eng" + "O, macron" + "O, breve" + "O, double acute" + "O, E, ligature" + "R, acute" + "R, cedilla" + "R, caron" + "S, acute" + "S, circumflex" + "S, cedilla" + "S, caron" + "T, cedilla" + "T, caron" + "T, stroke" + "U, tilde" + "U, macron" + "U, breve" + "U, indingilizi ngenhla" + "U, double acute" + "U, ogonek" + "W, circumflex" + "Y, circumflex" + "Z, acute" + "Z, icashazi ngenhla" + "Z, caron" + "Long S" + "O, horn" + "U, horn" + "S, ukhefana ngezansi" + "T, ukhefana ngezansi" + "Schwa" + "A, icashazi ngenhla" + "A, ihhuku ngenhla" + "A, circumflex ne-acute" + "A, circumflex ne-grave" + "A, circumflex nehhuku ngenhla" + "A, circumflex ne-tilde" + "A, circumflex necashazi ngenhla" + "A, breve ne-acute" + "A, breve ne-grave" + "A, breve nehhuku ngenhla" + "A, breve ne-tilde" + "A, breve necashazi ngezansi" + "E, icashazi ngezansi" + "E, ihhuku ngenhla" + "E, tilde" + "E, circumflex ne-acute" + "E, circumflex ne-grave" + "E, circumflex nehhuku ngenhla" + "E, circumflex ne-tilde" + "E, circumflex necashazi ngezansi" + "I, ihhuku ngenhla" + "I, icashazi ngezansi" + "O, icashazi ngezansi" + "O, ihhuku ngenhla" + "O, circumflex ne-acute" + "O, circumflex ne-grave" + "O, circumflex nehhuku ngenhla" + "O, circumflex ne-tilde" + "O, circumflex necashazi ngezansi" + "O, horn ne-acute" + "O, horn ne-grave" + "O, horn nehhuku ngenhla" + "O, horn ne-tilde" + "O, horn necashazi ngezansi" + "U, icashazi ngezansi" + "U, uhhuku ngehla" + "U, horn ne-acute" + "U, horn ne-grave" + "U, horn nehhuku ngenhla" + "U, horn ne-tilde" + "U, horn necashazi ngezansi" + "Y, grave" + "Y, icashazi ngezansi" + "Y, ihhuku ngenhla" + "Y, tilde" + "Umbabazi okokushiwo" + "Umaki wokukopisha onama-engeli amabili okhombe ngakwesokunxele" + "Icashazi eliphakathi" + "I-Superscript one" + "Umaki wokukopisha onama-engeli amabili okhombe ngakwesokudla" + "Umbuzi okokushiwo" + "Umaki wokukopisha oyedwa ongakwesokunxele" + "Umaki wokukopisha oyedwa ongakwesokudla" + "Umaki wokukopisha owodwa ophansi 9" + "Umaki wokukopisha ongakubili ongakwekunxele" + "Umaki wokukopisha ongakubili ongakwekudla" + "Dagger" + "Double dagger" + "Per mille sign" + "Prime" + "Double prime" + "Umaki wokukopisha oyedwa ngokhombe ngakwesokunxele" + "Umaki wokukopisha oyedwa ngokhombe ngakwesokudla" + "Superscript four" + "I-Superscript latin uhlambu oluncane u-n" + "Uphawu lwe-Peso" + "Ukunakekela ko" + "Umcibisholo wangakwesokudla" + "Umcibisholo oyaphansi" + "Isethi engenalutho" + "Nciphisa" + "Okuncane noma okulinganayo" + "Okukhulu noma okulinganayo" + "Inkanyezi emnyama" + diff --git a/app/src/main/res/values-zu/strings-talkback-descriptions.xml b/app/src/main/res/values-zu/strings-talkback-descriptions.xml new file mode 100644 index 00000000..a2951ca1 --- /dev/null +++ b/app/src/main/res/values-zu/strings-talkback-descriptions.xml @@ -0,0 +1,96 @@ + + + + + "Plaka ku-headset ukuze uzwe okhiye bephasiwedi ezindlebeni zakho bezwakala kakhulu." + "Umbhalo wamanje ngu %s" + "Awukho umbhalo ofakiwe" + "I-%1$s ilungisa i-%2$s kube yi-%3$s" + "I-%1$s yenza ukulungisa okuzenzakalelayo" + "Asikho isiphakamiso" + "Uhlamvu olungaziwa" + "U-Shift" + "Amasimbuli amaningi" + "U-Shift" + "Amasimbuli" + "U-Shift" + "Susa" + "Amasimbuli" + "Izinhlamvu" + "Izinombolo" + "Izilungiselelo" + "Ithebhu" + "Isikhala" + "Okungenayo kwezwi" + "I-Emoji" + "Buyela" + "Sesha" + "Icashazi" + "Shintsha ulimi" + "Okulandelayo" + "Okwangaphambilini" + "U-Shift uvunyelwe" + "Ofeleba bavunyelwe" + "Imodi yezimpawu" + "Imodi yamasimbuli amaningi" + "Imodi yezinhlamvu" + "Imodi yefoni" + "Imodi yezimpawu zefoni" + "Ikhibhodi ifihliwe" + "Ibonisa ikhibhodi ye-%s" + "idethi" + "idethi nesikhathi" + "i-imeyili" + "imilayezo" + "inombolo" + "ifoni" + "umbhalo" + "isikhathi" + "I-URL" + "Okwakamuva" + "Abantu" + "Izinto" + "Indalo" + "Izindawo" + "Amasimbuli" + "Ukuhlaba umkhosi" + "Ismayili nabantu" + "Izilwane nemvelo" + "Awokudla neziphuzo" + "Ukuhamba nezindawo" + "Umsebenzi" + "Izithombe-mzwelo" + "Ufeleba %s" + "Ufeleba I" + "Ufeleba I, icashazi ngenhla" + "Uphawu olungaziwa" + "I-emoji engaziwa" + "Ubuso obunesithukuthezi" + "Ubuso obuphoxekile" + "Ubuso obugqoke izibuko" + "Ubuso obumangele" + "Ukubso bokuqabula" + "Ubuso obuswacile" + "Ezinye izinhlamvu ziyatholakala" + "Ezinye izinhlamvu ziyalahlwa" + "Ezinye iziphakamiso ziyatholakala" + "Ezinye iziphakamiso ziyalahlwa" + diff --git a/app/src/main/res/values-zu/strings.xml b/app/src/main/res/values-zu/strings.xml new file mode 100644 index 00000000..956d50ad --- /dev/null +++ b/app/src/main/res/values-zu/strings.xml @@ -0,0 +1,212 @@ + + + + + "Okukhethwa kukho kokungenayo" + "Bheka amagama woxhumana nabo" + "Isihloli sokupela sisebenzisa okungenayo kusuka kuhlu lalabo oxhumana nabo" + "Dlidlizelisa ngokucindezela inkinobho" + "Umsindo wokucindezela ukhiye" + "Ugaxekile ngokucindezela ukhiye" + "Izintandokazi" + "Ama-akhawunti nobumfihlo" + "Ukubonakala nezakhiwo" + "Ukuthayipha ngokuthinta" + "Ukulungisa umbhalo" + "Okuthuthukisiwe" + "Itimu" + "Nika amandla ikhibhodi yokuhlukanisa" + "Ukuvumlanisa kwekhibhodi ye-Google" + "Ukuvumelanisa kuvuliwe" + "Vumelanisa isichazamazwi sakho kuwo wonke amadivayisi wakho" + "Vumelanisa manje" + "Susa idatha yamafu" + "Susa idatha yakho evumelanisiwe esuka ku-Google" + "Idatha yakho evumelanisiwe izosuswa emafini. Ingabe uqinisekile?" + "Susa" + "Khansela" + "Isichazamazwi sakho somuntu siqu sizovumelaniswa siphinde senziwe isipele kumaseva we-Google. Ulwazi lezibalo lemvamisa yegama lingaqoqwa ukusiza ukuthuthukisa imikhiqizo yethu. Ukuqoqwa nokusetshenziswa kwalo lonke ulwazi kuzohambisana ne-""Inqubomgomo Yobumfihlo ye-Google""." + "Sicela ungeze i-akhawunti ye-Google kule divayisi ukuze unike amandla lesi sici" + "Ukuvumelanisa akutholakali kumadivayisi anezinhlelo zokusebenza ze-Google zama-akhawunti webhizinisi" + "Shintshela kwezinye izindlela zokungena" + "Ukhiye wokushintsha ulimi ubandakanya ezinye izindlela zokungenayo" + "Ukhiye wokushintsha ullimi" + "Bonisa uma izilimi zokufaka zinikwe amandla" + "Ukuvela kokhiye cashisa ukulibazisa" + "Cha ukulibazisa" + "Okuzenzakalelayo" + "%sms" + "Okuzenzakalelayo kwesistimu" + "Sikisela amagama Othintana nabo" + "Amagama abasebenzisi kusuka Kothintana nabo bokusikisela nokulungisa" + "Iziphakamiso ezenziwe okomuntu siqu" + "Thuthukisa i-%s" + "Isikhathi se-Double-space" + "Ukuthepha kabili kubha yesikhala kufaka isikhathi esilandelwa yisikhala" + "Ukwenza ofeleba okuzenzakalelayo" + "Yenza ufeleba wegama lokuqala lomusho ngamunye" + "Isichazamazwi somuntu" + "Faka izichazamazwi" + "Isichazamazwi sakho ngqangi" + "Bonisa ukusikesela kokulungisa" + "Bonisa amagama aphakamisiwe ngenkathi uthayipha" + "Vimba amagama ahlaselayo" + "Ungaphakamisi amagama angaba nokuhlaselayo" + "Ukulungisa okuzenzakalelayo" + "Ibha yesikhala nokubhala ngamagama amakhulu kulungisa amaphutha amagama athayiphwe kabi" + "Valiwe" + "Thobekile" + "Bukhali" + "Bukhali kakhulu" + "Iziphakamiso zegama elilandelayo" + "Sebenzisa igama langaphambilini ekwenzeni iziphakamiso" + "Nika amandla okuthayipha ngokuthinta" + "Faka igama ngokushelelisa ezinhlamvini" + "Bonisa i-trail yokuthinta" + "Ukuhlola kuqala okuntantayo okunamandla" + "Bona igama eliphakanyisiwe ngenkathi uthinta" + "Igama lokuthinta" + "Faka izikhala ngesikhathi sokuthinta ngokushelelela kukhiye wesikhala" + "Inkinobho yokufaka izwi" + "Azikho izindlela zokufaka zezwi ezinikwe amandla. Hlola izilungiselelo zolimi kanye nezokufaka." + "Misa izindlela zokufakwayo" + "Izilimi" + "Usizo nempendulo" + "Izilimi" + "Thepha futhi ukuze ulondoloze" + "Thepha lapha ukuze ulondoloze" + "Isichazamazwi siyatholakala" + "Indikimba yekhibhodi" + "Shintshanisa ama-akhawunti" + "Awekho ama-akhawunti akhethiwe" + "Okwamanje isebenzisa i-%1$s" + "KULUNGILE" + "Khansela" + "Phuma ngemvume" + "Khetha i-akhawunti ezosetshenziswa" + "i-English(UK)" + "i-English (US)" + "I-Spanish (US)" + "I-Hinglish" + "Isi-Serbian (Latin)" + "I-English (UK) ( %s )" + "I-English (US) ( %s )" + "Isi-Spanish (US) ( %s )" + "I-Hinglish (%s)" + "Isi-Serbian (%s)" + "Isi-%s (Tradition)" + "%s (Okuqoqene ndawonye)" + "Alikho ulimi (Alfabhethi)" + "Alfabhethi (QWERTY)" + "Alfabhethi (QWERTZ)" + "Alfabhethi (I-AZERTY)" + "Alfabhethi (Dvorak)" + "Alfabhethi (Colemak)" + "Alfabhethi (PC)" + "I-Emoji" + "Itimu yekhibhodi" + "Izitayela zokufaka ngokwezifiso" + "Engeza isitayela" + "Engeza" + "Khipha" + "Londoloza" + "Ulimi" + "Isakhiwo" + "Isitayela sakho sokufakwayo sokwenza ngokwezifiso sidinga ukunikwa amandla ngaphambi kokusisebenzisa. Ufuna ukusinika amandla manje?" + "Nika amandla" + "Hhayi manje" + "Isitayela sokufaka esifanayo sesivele sikhona: %s" + "Ubude besikhathi sokudlidliza ukucindezela ukhiye" + "Ivolumu yomsindo wokucindezela ukhiye" + "Ukulibazisa ukucindezela isikhashana ukhiye" + "I-Emoji wekhibhodi yangempela" + "Ukhiye we-Alt woqobo ubonisa i-emoji palette" + "Okuzenzakalelayo" + "Siyakwamukela ku-%s" + "nokuthayipha ngokuthinta" + "Qalisa" + "Isinyathelo esilandelayo" + "Kusethwa i-%s" + "Nika amandla i-%s" + "Sicela uhlole i-\"%s\" kulimi lwakho nezilungiselelo zokokufaka. Lokhu kuzoyigunyaza ukuthi isebenze kudivayisi yakho." + "I-%s isivele inikwe amandla kulimi lwakho nakuzilungiselelo zokufaka, ngakho-ke lesi sinyathelo senziwe. Qhubekela kwesilandelayo!" + "Nika amandla kuzilungiselelo" + "Shintshela ku-%s." + "Okulandelayo, khetha i-\"%s\" njengendlela yakho yokufaka umbhalo osebenzayo." + "Shintsha izindlela zokufaka" + "Siyakuhalalisela, usumi ngomumo!" + "Manje usungathayipha ngokufaka zonke izinhlelo zokusebenza eziyizintandokazi zakho nge-%s." + "Lungiselela izilimi ezingeziwe" + "Iqedile" + "Bonisa isithonjana sohlelo lokusebenza" + "Bonisa isithonjana sohlelo lokusebenza kusiqalisi" + "Umhlinzeki wesichazamazwi" + "Umhlinzeki wesichazamazwi" + "Isevisi yesichazamazwi" + "Ulwazi lokubuyekeza isichazamazwi" + "Izichazamazwi ezingezwayo" + "Isichazamazwi siyatholakala" + "Izilungiselelo zezichazamazwi" + "Izichazamazwi zomsebenzisi" + "Isichazamazwi somsebenzisi" + "Isichazamazwi siyatholakala" + "Okwamanje iyalanda" + "Kufakiwe" + "Kufakiwe, kukhutshaziwe" + "Inkinga yokuxhumanisa kusevisi yesichazamazwi" + "Azikho izachazimazwi ezitholakalayo" + "Qala kabusha" + "Igcine ukulungiswa" + "Ihlola izibuyekezo" + "Iyalayisha..." + "Isichazamazwi sakho esisemqoka" + "Khansela" + "Izilungiselelo" + "Faka" + "Khansela" + "Susa" + "Ulimi olukhethiwe kudivayisi yakho yeselula lunesichazamazwi.<br/> Sincoma ukuthi <b>ulande</b> isichazamazwi se-%1$s ukuze uthuthukise umuzwa wakho wokuthayipha.<br/> <br/> Ukulanda kungathatha iminithi noma amabili nge-3G. Izindleko zingasebenza uma ungenalo <b>icebo ledatha elingakhawulelwe</b>.<br/> Uma ungenasiqiniseko sokuthi iliphi icebo ledatha onalo, sincoma ukuthi uthole uxhumo lwe-Wi-Fi ukuze uqale ukulanda ngokuzenzakalela.<br/> <br/> Ithiphu: Ungalanda futhi ususe izichazamazwi ngokuya kokuthi <b>Izilimi nokufaka</b> kumenyu <b>izilungiselelo</b> kudivayisi yakho yeselula." + "Landa manje (%1$.1fMB)" + "Landa nge-Wi-Fi" + "Isichazamazwi sitholakalela i-%1$s" + "Cindezela ukuze ubuyekeze uphinde ulande" + "Ukulanda: iziphakamiso ze-%1$s zizolunga maduze." + "Inguqulo engu-%1$s" + "Engeza" + "Faka kusichazamazwi" + "Umshwana" + "Izinketho eziningi" + "Izinketho ezincane" + "KULUNGILE" + "Igama:" + "Isinqamulelo:" + "Ulimi:" + "Thayipha igama" + "Ukunqamulela okukhethekayo" + "Hlela igama" + "Hlela" + "Susa" + "Awunawo amagama kwisichazamazwi somsebenzisi. Ukuze ungeze igama, thepha inkinobho (+)." + "Okwazo zonke izilimi" + "Izilimi eziningi…" + "Susa" + " ABCDEFGHIJKLMNOPQRSTUVWXYZ" + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 00000000..dc617f9e --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,604 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/bools.xml b/app/src/main/res/values/bools.xml new file mode 100644 index 00000000..10d21798 --- /dev/null +++ b/app/src/main/res/values/bools.xml @@ -0,0 +1,24 @@ + + + + + false + diff --git a/app/src/main/res/values/cm_strings.xml b/app/src/main/res/values/cm_strings.xml new file mode 100644 index 00000000..513dbd5b --- /dev/null +++ b/app/src/main/res/values/cm_strings.xml @@ -0,0 +1,29 @@ + + + + + Alphabet (Bépo) + Hungarian (QWERTY) + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..5453d51c --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,87 @@ + + + + + + + #FF33B5E5 + #D833B5E5 + #B233B5E5 + #9933B5E5 + @android:color/white + #66E0E4E5 + #80000000 + #A0FFFFFF + #66E0E4E5 + @android:color/white + #FFC0C0C0 + #80000000 + #C0000000 + #111111 + + #FFF0F0F0 + #D8F0F0F0 + #B2F0F0F0 + #99E0E0E0 + + #37474F + #CC37474F + #B337474F + #B337474F + #B337474F + #4DB6AC + #37474F + #D937474F + #B337474F + #4DB6AC + #B34DB6AC + #ECEFF1 + #2637474F + #2637474F + #E6ECEFF1 + #E4E7E9 + + #CCFFFFFF + #CCFFFFFF + #80FFFFFF + #80FFFFFF + #B3FFFFFF + #80CBC4 + #FFFFFF + #D9FFFFFF + #B3FFFFFF + #80CBC4 + #B380CBC4 + #263238 + #19FFFFFF + #19FFFFFF + #E621272B + #21272B + + #FFEBEBEB + #FF707070 + @android:color/holo_blue_light + @android:color/background_light + #FFCCCCCC + + #FF607D8B + diff --git a/app/src/main/res/values/config-additional-features.xml b/app/src/main/res/values/config-additional-features.xml new file mode 100644 index 00000000..47eb7724 --- /dev/null +++ b/app/src/main/res/values/config-additional-features.xml @@ -0,0 +1,24 @@ + + + + + + false + diff --git a/app/src/main/res/values/config-auto-correction-thresholds.xml b/app/src/main/res/values/config-auto-correction-thresholds.xml new file mode 100644 index 00000000..fc701c7f --- /dev/null +++ b/app/src/main/res/values/config-auto-correction-thresholds.xml @@ -0,0 +1,63 @@ + + + + + + + + floatMaxValue + + 0.185 + + 0.067 + + floatNegativeInfinity + + + + 0.065 + + + 0 + 1 + 2 + 3 + + + + @string/auto_correction_threshold_mode_index_off + @string/auto_correction_threshold_mode_index_modest + @string/auto_correction_threshold_mode_index_aggressive + @string/auto_correction_threshold_mode_index_very_aggressive + + + + @string/auto_correction_threshold_mode_off + @string/auto_correction_threshold_mode_modest + @string/auto_correction_threshold_mode_aggressive + @string/auto_correction_threshold_mode_very_aggressive + + diff --git a/app/src/main/res/values/config-common.xml b/app/src/main/res/values/config-common.xml new file mode 100644 index 00000000..fb20f7c3 --- /dev/null +++ b/app/src/main/res/values/config-common.xml @@ -0,0 +1,147 @@ + + + + + true + + true + + 100 + 1100 + + 400 + 50 + + 350 + + 17 + 53 + 98% + 94% + + 70 + + 1000 + + true + 100 + + 300 + 700 + 100 + 10 + 3000 + 5 + 0 + 100 + + + 1200 + + + 8.0dp + + + 100 + -1 + 8.0dp + + 128 + 1dp + + 200 + 100 + 800 + 20 + + 500 + + 150% + + 450 + + 300 + 20 + + 600% + 50% + + 16.6666% + + 100 + 100 + 550% + + 32 + 16 + 12.6dp + 40 + + + 0%p + 0%p + 0.0dp + + + 2dp + + + 3 + 36% + 100 + 300 + + + 12dp + 6dp + 32.0dp + 18% + + + + 9.6dp + + 15 + + 16.0dp + + 6 + 10.0dp + 2.5dp + + + 100 + -1 + + + 2dp + + + 8dp + + 48 + + + + -1 + diff --git a/app/src/main/res/values/config-dictionary-pack.xml b/app/src/main/res/values/config-dictionary-pack.xml new file mode 100644 index 00000000..bf52de88 --- /dev/null +++ b/app/src/main/res/values/config-dictionary-pack.xml @@ -0,0 +1,30 @@ + + + + + + + false + false + false + false + true + true + diff --git a/app/src/main/res/values/config-per-form-factor.xml b/app/src/main/res/values/config-per-form-factor.xml new file mode 100644 index 00000000..67fc7513 --- /dev/null +++ b/app/src/main/res/values/config-per-form-factor.xml @@ -0,0 +1,32 @@ + + + + + + true + + true + false + true + true + + false + diff --git a/app/src/main/res/values/config-screen-metrics.xml b/app/src/main/res/values/config-screen-metrics.xml new file mode 100644 index 00000000..99629944 --- /dev/null +++ b/app/src/main/res/values/config-screen-metrics.xml @@ -0,0 +1,24 @@ + + + + + + 0 + diff --git a/app/src/main/res/values/config-spellchecker-thresholds.xml b/app/src/main/res/values/config-spellchecker-thresholds.xml new file mode 100644 index 00000000..e99ba662 --- /dev/null +++ b/app/src/main/res/values/config-spellchecker-thresholds.xml @@ -0,0 +1,25 @@ + + + + + + 0.11 + diff --git a/app/src/main/res/values/config.xml b/app/src/main/res/values/config.xml new file mode 100644 index 00000000..33bcb060 --- /dev/null +++ b/app/src/main/res/values/config.xml @@ -0,0 +1,112 @@ + + + + + + false + + 8.0dp + + + + 205.6dp + 46%p + -61.8%p + + 52.8dp + + + 63.36dp + 8dp + + 2.335%p + 4.669%p + 6.127%p + 1.739%p + + -26.4dp + 8.0dp + 80dp + 50.0dp + 122dp + 82% + 55% + 55% + 65% + 90% + 34% + 34% + 25% + 25% + 44% + 30% + 35% + 35% + 33.735% + 1dp + 2dp + + + 3.20%p + 55% + 41% + + 40dp + 36dp + 36dp + 40dp + 6 + 90% + 44dp + 6dp + 18dp + 27dp + + + 24dp + 73dp + 24dp + 16dp + 2dp + + + 14.2857%p + 33%p + 68%p + 21 + + + + 57,16 + + + + + 58,32 + + diff --git a/app/src/main/res/values/dictionary-pack.xml b/app/src/main/res/values/dictionary-pack.xml new file mode 100644 index 00000000..66101afa --- /dev/null +++ b/app/src/main/res/values/dictionary-pack.xml @@ -0,0 +1,27 @@ + + + + org.dslul.openboard.inputmethod.latin + + org.dslul.openboard.inputmethod.dictionarypack.DictionarySettingsActivity + org.dslul.openboard.inputmethod.dictionarypack.aosp + + metadata.json + diff --git a/app/src/main/res/values/donottranslate-config-spacing-and-punctuations.xml b/app/src/main/res/values/donottranslate-config-spacing-and-punctuations.xml new file mode 100644 index 00000000..06144d02 --- /dev/null +++ b/app/src/main/res/values/donottranslate-config-spacing-and-punctuations.xml @@ -0,0 +1,46 @@ + + + + + + !,?,\\,,:,;,\",(,),\',-,/,@,_ + + ([{& + + .,;:!?)]}& + + + + + "  "()[]{}*&<>+=|.,;:!?/_\" + + \'- + + + 46 + + + 46 + + .?! + + true + diff --git a/app/src/main/res/values/donottranslate-debug-settings.xml b/app/src/main/res/values/donottranslate-debug-settings.xml new file mode 100644 index 00000000..9df73ae4 --- /dev/null +++ b/app/src/main/res/values/donottranslate-debug-settings.xml @@ -0,0 +1,50 @@ + + + + + Android Keyboard Debug settings + Debug Mode + Force non-distinct multitouch + Show LXX suggestion UI + + Show slide indicator + + Display visual cue while sliding from Shift or Symbol keys + + Customize key preview animation + + Key popup show up duration + + Key popup dismiss duration + + Key popup show up start X scale + + Key popup show up start Y scale + + Key popup dismiss end X scale + + Key popup dismiss end Y scale + + Enable keyboard resizing + + Keyboard height scale + + Dump dictionary + diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml new file mode 100644 index 00000000..1e543d82 --- /dev/null +++ b/app/src/main/res/values/donottranslate.xml @@ -0,0 +1,129 @@ + + + + + 0 + + + + en_US + en_GB + es_US + hi_ZZ + hu_ZZ + sr_ZZ + + + + + hi_ZZ + sr_ZZ + + Hinglish + Srpski + + + %s + + + + qwerty + qwertz + azerty + dvorak + colemak + bepo + pcqwerty + + + + QWERTY + QWERTZ + AZERTY + Dvorak + Colemak + Bépo + PC + + + %s (QWERTY) + %s (QWERTZ) + %s (AZERTY) + %s (Dvorak) + %s (Colemak) + %s (Bépo) + %s (PC) + + + %s (BDS) + + + + en_US:TrySuppressingImeSwitcher,AsciiCapable,SupportTouchPositionCorrection,EmojiCapable + qwerty + en_GB:TrySuppressingImeSwitcher,AsciiCapable,SupportTouchPositionCorrection,EmojiCapable + qwerty + ar:SupportTouchPositionCorrection,EmojiCapable + arabic + cs:AsciiCapable,SupportTouchPositionCorrection,EmojiCapable + qwertz + da:AsciiCapable,SupportTouchPositionCorrection,EmojiCapable + nordic + de:AsciiCapable,SupportTouchPositionCorrection,EmojiCapable + qwertz + es:AsciiCapable,SupportTouchPositionCorrection,EmojiCapable + spanish + fi:AsciiCapable,SupportTouchPositionCorrection,EmojiCapable + nordic + fr:AsciiCapable,SupportTouchPositionCorrection,EmojiCapable + azerty + fr_CA:AsciiCapable,SupportTouchPositionCorrection,EmojiCapable + qwerty + hr:AsciiCapable,SupportTouchPositionCorrection,EmojiCapable + qwertz + hu:AsciiCapable,SupportTouchPositionCorrection,EmojiCapable + qwertz + it:AsciiCapable,SupportTouchPositionCorrection,EmojiCapable + qwerty + iw:SupportTouchPositionCorrection,EmojiCapable + hebrew + nb:AsciiCapable,SupportTouchPositionCorrection,EmojiCapable + nordic + nl:AsciiCapable,SupportTouchPositionCorrection,EmojiCapable + qwerty + pl:AsciiCapable,SupportTouchPositionCorrection,EmojiCapable + qwerty + ru:SupportTouchPositionCorrection,EmojiCapable + east_slavic + sr:SupportTouchPositionCorrection,EmojiCapable + south_slavic + sv:AsciiCapable,SupportTouchPositionCorrection,EmojiCapable + nordic + tr:AsciiCapable,SupportTouchPositionCorrection,EmojiCapable + qwerty + + diff --git a/app/src/main/res/values/emoji-categories.xml b/app/src/main/res/values/emoji-categories.xml new file mode 100644 index 00000000..ce82a8b4 --- /dev/null +++ b/app/src/main/res/values/emoji-categories.xml @@ -0,0 +1,127 @@ + + + + + + + + + 30 + 31 + + + 2744 + + + 2460 + 2461 + 2462 + 2463 + 2464 + 2465 + 2466 + 2467 + 2468 + 2469 + 00ae + 00a9 + 2122 + 2734 + 2733 + 2716 + 2195 + 2197 + 27a1 + 2198 + 2199 + 2196 + 2194 + 25c0 + 25b6 + 2747 + 25aa + 203c + 2660 + 2665 + 2663 + 2666 + 21a9 + 21aa + + + 270C + 2764 + + + 260e + 2709 + 2712 + 270f + 2702 + 2669 + 266a + 266c + + + 2708 + 2668 + + + =-O + :-P + ;-) + :-( + :-) + :-! + :-$ + B-) + :O + :-* + :-D + :\'( + :-\\ + O:-) + :-[ + + diff --git a/app/src/main/res/values/gesture-input.xml b/app/src/main/res/values/gesture-input.xml new file mode 100644 index 00000000..235616fb --- /dev/null +++ b/app/src/main/res/values/gesture-input.xml @@ -0,0 +1,22 @@ + + + + false + diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 00000000..0a3cb8ac --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #10B19B + \ No newline at end of file diff --git a/app/src/main/res/values/important_notice_strings.xml b/app/src/main/res/values/important_notice_strings.xml new file mode 100644 index 00000000..b1f3fc13 --- /dev/null +++ b/app/src/main/res/values/important_notice_strings.xml @@ -0,0 +1,21 @@ + + + + + + Suggest contact names? Touch for info. + \ No newline at end of file diff --git a/app/src/main/res/values/keyboard-heights.xml b/app/src/main/res/values/keyboard-heights.xml new file mode 100644 index 00000000..12dd51d9 --- /dev/null +++ b/app/src/main/res/values/keyboard-heights.xml @@ -0,0 +1,37 @@ + + + + + + + + + HARDWARE=sholes,227.0167 + + HARDWARE=mahimahi,217.5932 + + HARDWARE=herring,200.8554 + + HARDWARE=tuna,202.5869 + + + HARDWARE=stingray,283.1337 + + diff --git a/app/src/main/res/values/keyboard-icons-holo.xml b/app/src/main/res/values/keyboard-icons-holo.xml new file mode 100644 index 00000000..f5484bf4 --- /dev/null +++ b/app/src/main/res/values/keyboard-icons-holo.xml @@ -0,0 +1,41 @@ + + + + + + diff --git a/app/src/main/res/values/keyboard-icons-lxx-dark.xml b/app/src/main/res/values/keyboard-icons-lxx-dark.xml new file mode 100644 index 00000000..2e2fd0ab --- /dev/null +++ b/app/src/main/res/values/keyboard-icons-lxx-dark.xml @@ -0,0 +1,46 @@ + + + + + + diff --git a/app/src/main/res/values/keyboard-icons-lxx-light.xml b/app/src/main/res/values/keyboard-icons-lxx-light.xml new file mode 100644 index 00000000..099a706f --- /dev/null +++ b/app/src/main/res/values/keyboard-icons-lxx-light.xml @@ -0,0 +1,48 @@ + + + + + + diff --git a/app/src/main/res/values/keyboard-themes.xml b/app/src/main/res/values/keyboard-themes.xml new file mode 100644 index 00000000..3c2a42a7 --- /dev/null +++ b/app/src/main/res/values/keyboard-themes.xml @@ -0,0 +1,40 @@ + + + + + + @string/keyboard_theme_light_border + @string/keyboard_theme_dark_border + @string/keyboard_theme_material_light + @string/keyboard_theme_material_dark + @string/keyboard_theme_holo_white + @string/keyboard_theme_holo_blue + + + + 5 + 6 + 3 + 4 + 2 + 0 + + diff --git a/app/src/main/res/values/keypress-vibration-durations.xml b/app/src/main/res/values/keypress-vibration-durations.xml new file mode 100644 index 00000000..032b5fd9 --- /dev/null +++ b/app/src/main/res/values/keypress-vibration-durations.xml @@ -0,0 +1,63 @@ + + + + + + + MODEL=Nexus S:BRAND=google,5 + + MODEL=Galaxy Nexus:BRAND=google,5 + + MODEL=Nexus 4:BRAND=google,8 + + MODEL=Nexus 10:BRAND=google,16 + + MODEL=GT-I(9100[GMPT]?|9108|9210T?):MANUFACTURER=samsung,8 + MODEL=SGH-(I9[27]7R?|I927|T989D?):MANUFACTURER=samsung,8 + MODEL=SHW-M250[KLS]?|SPH-D710|SCH-R760:MANUFACTURER=samsung,8 + MODEL=ISW11SC|SC-02C:MANUFACTURER=samsung,8 + + MODEL=(SAMSUNG-)?GT-I(930[05][NT]?|9308):MANUFACTURER=samsung,8 + MODEL=(SAMSUNG-)?SGH-(T999[V]?|I747[M]?|N064|N035):MANUFACTURER=samsung,8 + MODEL=(SAMSUNG-)?SCH-(J021|R530|I535|I939):MANUFACTURER=samsung,8 + MODEL=(SAMSUNG-)?(SCL21|SC-06D|SC-03E):MANUFACTURER=samsung,8 + MODEL=(SAMSUNG-)?(SHV-210[KLS]?|SPH-L710):MANUFACTURER=samsung,8 + + MODEL=(SAMSUNG-)?GT-I(950[0258][G]?):MANUFACTURER=samsung,7 + MODEL=(SAMSUNG-)?SGH-(I337|M919|N045):MANUFACTURER=samsung,7 + MODEL=(SAMSUNG-)?SCH-(I545|I959|R970):MANUFACTURER=samsung,7 + MODEL=(SAMSUNG-)?SPH-(L720):MANUFACTURER=samsung,7 + MODEL=(SAMSUNG-)?(SC-04E):MANUFACTURER=samsung,7 + MODEL=(SAMSUNG-)?(SHV-E300[KLS]?):MANUFACTURER=samsung,7 + + MODEL=LG-E97[013]|LS970|L-01E:MANUFACTURER=LGE,15 + + MODEL=HTC One X:MANUFACTURER=HTC,20 + + MODEL=HTC One:MANUFACTURER=HTC,15 + MODEL=HTL22:MANUFACTURER=HTC,15 + + MODEL=XT907:MANUFACTURER=motorola,30 + + MODEL=XT1035:MANUFACTURER=motorola,18 + + MODEL=C6603|C6806:MANUFACTURER=Sony,35 + + diff --git a/app/src/main/res/values/keypress-volumes.xml b/app/src/main/res/values/keypress-volumes.xml new file mode 100644 index 00000000..074581d0 --- /dev/null +++ b/app/src/main/res/values/keypress-volumes.xml @@ -0,0 +1,30 @@ + + + + + + HARDWARE=herring,0.5f + HARDWARE=tuna,0.5f + HARDWARE=stingray,0.4f + HARDWARE=grouper,0.3f + HARDWARE=mako,0.3f + HARDWARE=manta,0.2f + + diff --git a/app/src/main/res/values/phantom-sudden-move-event-device-list.xml b/app/src/main/res/values/phantom-sudden-move-event-device-list.xml new file mode 100644 index 00000000..d982af6c --- /dev/null +++ b/app/src/main/res/values/phantom-sudden-move-event-device-list.xml @@ -0,0 +1,27 @@ + + + + + + + HARDWARE=stingray,true + + diff --git a/app/src/main/res/values/platform-theme.xml b/app/src/main/res/values/platform-theme.xml new file mode 100644 index 00000000..2854ff72 --- /dev/null +++ b/app/src/main/res/values/platform-theme.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/setup-styles.xml b/app/src/main/res/values/setup-styles.xml new file mode 100644 index 00000000..c968b2fc --- /dev/null +++ b/app/src/main/res/values/setup-styles.xml @@ -0,0 +1,24 @@ + + + + + + diff --git a/app/src/main/res/values/strings-action-keys.xml b/app/src/main/res/values/strings-action-keys.xml new file mode 100644 index 00000000..96b2e7dd --- /dev/null +++ b/app/src/main/res/values/strings-action-keys.xml @@ -0,0 +1,38 @@ + + + + + + Go + + Next + + Prev + + Done + + Send + + Search + + Pause + + Wait + diff --git a/app/src/main/res/values/strings-config-important-notice.xml b/app/src/main/res/values/strings-config-important-notice.xml new file mode 100644 index 00000000..de8d6fc5 --- /dev/null +++ b/app/src/main/res/values/strings-config-important-notice.xml @@ -0,0 +1,24 @@ + + + + + + Learn from your communications and typed data to improve suggestions + diff --git a/app/src/main/res/values/strings-emoji-descriptions.xml b/app/src/main/res/values/strings-emoji-descriptions.xml new file mode 100644 index 00000000..3799a23d --- /dev/null +++ b/app/src/main/res/values/strings-emoji-descriptions.xml @@ -0,0 +1,2587 @@ + + + + + + Copyright sign + + Registered sign + + Double exclamation mark + + Exclamation question mark + + Trade mark sign + + Information source + + Left right arrow + + Up down arrow + + North west arrow + + North east arrow + + South east arrow + + South west arrow + + Leftwards arrow with hook + + Rightwards arrow with hook + + Watch + + Hourglass + + Keyboard + + Eject symbol + + Black right-pointing double triangle + + Black left-pointing double triangle + + Black up-pointing double triangle + + Black down-pointing double triangle + + Black right-pointing double triangle with vertical bar + + Black left-pointing double triangle with vertical bar + + Black right-pointing triangle with double vertical bar + + Alarm clock + + Stopwatch + + Timer clock + + Hourglass with flowing sand + + Double vertical bar + + Black square for stop + + Black circle for record + + Circled latin capital letter m + + Black small square + + White small square + + Black right-pointing triangle + + Black left-pointing triangle + + White medium square + + Black medium square + + White medium small square + + Black medium small square + + Black sun with rays + + Cloud + + Umbrella + + Snowman + + Comet + + Black telephone + + Ballot box with check + + Umbrella with rain drops + + Hot beverage + + Shamrock + + White up pointing index + + Skull and crossbones + + Radioactive sign + + Biohazard sign + + Orthodox cross + + Star and crescent + + Peace symbol + + Yin yang + + Wheel of dharma + + White frowning face + + White smiling face + + Aries + + Taurus + + Gemini + + Cancer + + Leo + + Virgo + + Libra + + Scorpius + + Sagittarius + + Capricorn + + Aquarius + + Pisces + + Black spade suit + + Black club suit + + Black heart suit + + Black diamond suit + + Hot springs + + Black universal recycling symbol + + Wheelchair symbol + + Hammer and pick + + Anchor + + Crossed swords + + Scales + + Alembic + + Gear + + Atom symbol + + Fleur-de-lis + + Warning sign + + High voltage sign + + Medium white circle + + Medium black circle + + Coffin + + Funeral urn + + Soccer ball + + Baseball + + Snowman without snow + + Sun behind cloud + + Thunder cloud and rain + + Ophiuchus + + Pick + + Helmet with white cross + + Chains + + No entry + + Shinto shrine + + Church + + Mountain + + Umbrella on ground + + Fountain + + Flag in hole + + Ferry + + Sailboat + + Skier + + Ice skate + + Person with ball + + Tent + + Fuel pump + + Black scissors + + White heavy check mark + + Airplane + + Envelope + + Raised fist + + Raised hand + + Victory hand + + Writing hand + + Pencil + + Black nib + + Heavy check mark + + Heavy multiplication x + + Latin cross + + Star of David + + Sparkles + + Eight spoked asterisk + + Eight pointed black star + + Snowflake + + Sparkle + + Cross mark + + Negative squared cross mark + + Black question mark ornament + + White question mark ornament + + White exclamation mark ornament + + Heavy exclamation mark symbol + + Heavy heart exclamation mark ornament + + Heavy black heart + + Heavy plus sign + + Heavy minus sign + + Heavy division sign + + Black rightwards arrow + + Curly loop + + Double curly loop + + Arrow pointing rightwards then curving upwards + + Arrow pointing rightwards then curving downwards + + Leftwards black arrow + + Upwards black arrow + + Downwards black arrow + + Black large square + + White large square + + White medium star + + Heavy large circle + + Wavy dash + + Part alternation mark + + Circled ideograph congratulation + + Circled ideograph secret + + Mahjong tile red dragon + + Playing card black joker + + Blood type A + + Blood type B + + Blood type O + + Parking lot + + Blood type AB + + Squared CL + + Squared cool + + Squared free + + Squared ID + + Squared new + + Squared N G + + Squared OK + + Squared SOS + + Squared up with exclamation mark + + Squared vs + + Squared katakana here + + Squared katakana service + + Squared ideograph charge-free + + Squared ideograph reserved-seat + + Squared ideograph prohibitation + + Squared ideograph vacancy + + Squared ideograph acceptance + + Squared ideograph full occupancy + + Squared ideograph paid + + Squared ideograph monthly + + Squared ideograph application + + Squared ideograph discount + + Squared ideograph in business + + Circled ideograph advantage + + Circled ideograph accept + + Cyclone + + Foggy + + Closed umbrella + + Night with stars + + Sunrise over mountains + + Sunrise + + Cityscape at dusk + + Sunset over buildings + + Rainbow + + Bridge at night + + Water wave + + Volcano + + Milky way + + Earth globe europe-africa + + Earth globe americas + + Earth globe asia-australia + + Globe with meridians + + New moon symbol + + Waxing crescent moon symbol + + First quarter moon symbol + + Waxing gibbous moon symbol + + Full moon symbol + + Waning gibbous moon symbol + + Last quarter moon symbol + + Waning crescent moon symbol + + Crescent moon + + New moon with face + + First quarter moon with face + + Last quarter moon with face + + Full moon with face + + Sun with face + + Glowing star + + Shooting star + + Thermometer + + White sun with small cloud + + White sun behind cloud + + White sun behind cloud with rain + + Cloud with rain + + Cloud with snow + + Cloud with lightning + + Cloud with tornado + + Fog + + Wind blowing face + + Hot dog + + Taco + + Burrito + + Chestnut + + Seedling + + Evergreen tree + + Deciduous tree + + Palm tree + + Cactus + + Hot pepper + + Tulip + + Cherry blossom + + Rose + + Hibiscus + + Sunflower + + Blossom + + Ear of maize + + Ear of rice + + Herb + + Four leaf clover + + Maple leaf + + Fallen leaf + + Leaf fluttering in wind + + Mushroom + + Tomato + + Aubergine + + Grapes + + Melon + + Watermelon + + Tangerine + + Lemon + + Banana + + Pineapple + + Red apple + + Green apple + + Pear + + Peach + + Cherries + + Strawberry + + Hamburger + + Slice of pizza + + Meat on bone + + Poultry leg + + Rice cracker + + Rice ball + + Cooked rice + + Curry and rice + + Steaming bowl + + Spaghetti + + Bread + + French fries + + Roasted sweet potato + + Dango + + Oden + + Sushi + + Fried shrimp + + Fish cake with swirl design + + Soft ice cream + + Shaved ice + + Ice cream + + Doughnut + + Cookie + + Chocolate bar + + Candy + + Lollipop + + Custard + + Honey pot + + Shortcake + + Bento box + + Pot of food + + Cooking + + Fork and knife + + Teacup without handle + + Sake bottle and cup + + Wine glass + + Cocktail glass + + Tropical drink + + Beer mug + + Clinking beer mugs + + Baby bottle + + Fork and knife with plate + + Bottle with popping cork + + Popcorn + + Ribbon + + Wrapped present + + Birthday cake + + Jack-o-lantern + + Christmas tree + + Father christmas + + Fireworks + + Firework sparkler + + Balloon + + Party popper + + Confetti ball + + Tanabata tree + + Crossed flags + + Pine decoration + + Japanese dolls + + Carp streamer + + Wind chime + + Moon viewing ceremony + + School satchel + + Graduation cap + + Military medal + + Reminder ribbon + + Studio microphone + + Level slider + + Control knobs + + Film frames + + Admission tickets + + Carousel horse + + Ferris wheel + + Roller coaster + + Fishing pole and fish + + Microphone + + Movie camera + + Cinema + + Headphone + + Artist palette + + Top hat + + Circus tent + + Ticket + + Clapper board + + Performing arts + + Video game + + Direct hit + + Slot machine + + Billiards + + Game die + + Bowling + + Flower playing cards + + Musical note + + Multiple musical notes + + Saxophone + + Guitar + + Musical keyboard + + Trumpet + + Violin + + Musical score + + Running shirt with sash + + Tennis racquet and ball + + Ski and ski boot + + Basketball and hoop + + Chequered flag + + Snowboarder + + Runner + + Surfer + + Sports medal + + Trophy + + Horse racing + + American football + + Rugby football + + Swimmer + + Weight lifter + + Golfer + + Racing motorcycle + + Racing car + + Cricket bat and ball + + Volleyball + + Field hockey stick and ball + + Ice hockey stick and puck + + Table tennis paddle and ball + + Snow capped mountain + + Camping + + Beach with umbrella + + Building construction + + House buildings + + Cityscape + + Derelict house building + + Classical building + + Desert + + Desert island + + National park + + Stadium + + House building + + House with garden + + Office building + + Japanese post office + + European post office + + Hospital + + Bank + + Automated teller machine + + Hotel + + Love hotel + + Convenience store + + School + + Department store + + Factory + + Izakaya lantern + + Japanese castle + + European castle + + Waving white flag + + Waving black flag + + Rosette + + Label + + Badminton racquet and shuttlecock + + Bow and arrow + + Amphora + + Emoji modifier Fitzpatrick type-1-2 + + Emoji modifier Fitzpatrick type-3 + + Emoji modifier Fitzpatrick type-4 + + Emoji modifier Fitzpatrick type-5 + + Emoji modifier Fitzpatrick type-6 + + Rat + + Mouse + + Ox + + Water buffalo + + Cow + + Tiger + + Leopard + + Rabbit + + Cat + + Dragon + + Crocodile + + Whale + + Snail + + Snake + + Horse + + Ram + + Goat + + Sheep + + Monkey + + Rooster + + Chicken + + Dog + + Pig + + Boar + + Elephant + + Octopus + + Spiral shell + + Bug + + Ant + + Honeybee + + Lady beetle + + Fish + + Tropical fish + + Blowfish + + Turtle + + Hatching chick + + Baby chick + + Front-facing baby chick + + Bird + + Penguin + + Koala + + Poodle + + Dromedary camel + + Bactrian camel + + Dolphin + + Mouse face + + Cow face + + Tiger face + + Rabbit face + + Cat face + + Dragon face + + Spouting whale + + Horse face + + Monkey face + + Dog face + + Pig face + + Frog face + + Hamster face + + Wolf face + + Bear face + + Panda face + + Pig nose + + Paw prints + + Chipmunk + + Eyes + + Eye + + Ear + + Nose + + Mouth + + Tongue + + White up pointing backhand index + + White down pointing backhand index + + White left pointing backhand index + + White right pointing backhand index + + Fisted hand sign + + Waving hand sign + + Ok hand sign + + Thumbs up sign + + Thumbs down sign + + Clapping hands sign + + Open hands sign + + Crown + + Womans hat + + Eyeglasses + + Necktie + + T-shirt + + Jeans + + Dress + + Kimono + + Bikini + + Womans clothes + + Purse + + Handbag + + Pouch + + Mans shoe + + Athletic shoe + + High-heeled shoe + + Womans sandal + + Womans boots + + Footprints + + Bust in silhouette + + Busts in silhouette + + Boy + + Girl + + Man + + Woman + + Family + + Man and woman holding hands + + Two men holding hands + + Two women holding hands + + Police officer + + Woman with bunny ears + + Bride with veil + + Person with blond hair + + Man with gua pi mao + + Man with turban + + Older man + + Older woman + + Baby + + Construction worker + + Princess + + Japanese ogre + + Japanese goblin + + Ghost + + Baby angel + + Extraterrestrial alien + + Alien monster + + Imp + + Skull + + Information desk person + + Guardsman + + Dancer + + Lipstick + + Nail polish + + Face massage + + Haircut + + Barber pole + + Syringe + + Pill + + Kiss mark + + Love letter + + Ring + + Gem stone + + Kiss + + Bouquet + + Couple with heart + + Wedding + + Beating heart + + Broken heart + + Two hearts + + Sparkling heart + + Growing heart + + Heart with arrow + + Blue heart + + Green heart + + Yellow heart + + Purple heart + + Heart with ribbon + + Revolving hearts + + Heart decoration + + Diamond shape with a dot inside + + Electric light bulb + + Anger symbol + + Bomb + + Sleeping symbol + + Collision symbol + + Splashing sweat symbol + + Droplet + + Dash symbol + + Pile of poo + + Flexed biceps + + Dizzy symbol + + Speech balloon + + Thought balloon + + White flower + + Hundred points symbol + + Money bag + + Currency exchange + + Heavy dollar sign + + Credit card + + Banknote with yen sign + + Banknote with dollar sign + + Banknote with euro sign + + Banknote with pound sign + + Money with wings + + Chart with upwards trend and yen sign + + Seat + + Personal computer + + Briefcase + + Minidisc + + Floppy disk + + Optical disc + + Dvd + + File folder + + Open file folder + + Page with curl + + Page facing up + + Calendar + + Tear-off calendar + + Card index + + Chart with upwards trend + + Chart with downwards trend + + Bar chart + + Clipboard + + Pushpin + + Round pushpin + + Paperclip + + Straight ruler + + Triangular ruler + + Bookmark tabs + + Ledger + + Notebook + + Notebook with decorative cover + + Closed book + + Open book + + Green book + + Blue book + + Orange book + + Books + + Name badge + + Scroll + + Memo + + Telephone receiver + + Pager + + Fax machine + + Satellite antenna + + Public address loudspeaker + + Cheering megaphone + + Outbox tray + + Inbox tray + + Package + + E-mail symbol + + Incoming envelope + + Envelope with downwards arrow above + + Closed mailbox with lowered flag + + Closed mailbox with raised flag + + Open mailbox with raised flag + + Open mailbox with lowered flag + + Postbox + + Postal horn + + Newspaper + + Mobile phone + + Mobile phone with rightwards arrow at left + + Vibration mode + + Mobile phone off + + No mobile phones + + Antenna with bars + + Camera + + Camera with flash + + Video camera + + Television + + Radio + + Videocassette + + Film projector + + Prayer beads + + Twisted rightwards arrows + + Clockwise rightwards and leftwards open circle arrows + + Clockwise rightwards and leftwards open circle arrows with circled one overlay + + Clockwise downwards and upwards open circle arrows + + Anticlockwise downwards and upwards open circle arrows + + Low brightness symbol + + High brightness symbol + + Speaker with cancellation stroke + + Speaker + + Speaker with one sound wave + + Speaker with three sound waves + + Battery + + Electric plug + + Left-pointing magnifying glass + + Right-pointing magnifying glass + + Lock with ink pen + + Closed lock with key + + Key + + Lock + + Open lock + + Bell + + Bell with cancellation stroke + + Bookmark + + Link symbol + + Radio button + + Back with leftwards arrow above + + End with leftwards arrow above + + On with exclamation mark with left right arrow above + + Soon with rightwards arrow above + + Top with upwards arrow above + + No one under eighteen symbol + + Keycap ten + + Input symbol for latin capital letters + + Input symbol for latin small letters + + Input symbol for numbers + + Input symbol for symbols + + Input symbol for latin letters + + Fire + + Electric torch + + Wrench + + Hammer + + Nut and bolt + + Hocho + + Pistol + + Microscope + + Telescope + + Crystal ball + + Six pointed star with middle dot + + Japanese symbol for beginner + + Trident emblem + + Black square button + + White square button + + Large red circle + + Large blue circle + + Large orange diamond + + Large blue diamond + + Small orange diamond + + Small blue diamond + + Up-pointing red triangle + + Down-pointing red triangle + + Up-pointing small red triangle + + Down-pointing small red triangle + + Om symbol + + Dove of peace + + Kaaba + + Mosque + + Synagogue + + Menorah with nine branches + + Clock face one oclock + + Clock face two oclock + + Clock face three oclock + + Clock face four oclock + + Clock face five oclock + + Clock face six oclock + + Clock face seven oclock + + Clock face eight oclock + + Clock face nine oclock + + Clock face ten oclock + + Clock face eleven oclock + + Clock face twelve oclock + + Clock face one-thirty + + Clock face two-thirty + + Clock face three-thirty + + Clock face four-thirty + + Clock face five-thirty + + Clock face six-thirty + + Clock face seven-thirty + + Clock face eight-thirty + + Clock face nine-thirty + + Clock face ten-thirty + + Clock face eleven-thirty + + Clock face twelve-thirty + + Candle + + Mantelpiece clock + + Hole + + Man in business suit levitating + + Sleuth or spy + + Dark sunglasses + + Spider + + Spider web + + Joystick + + Linked paperclips + + Lower left ballpoint pen + + Lower left fountain pen + + Lower left paintbrush + + Lower left crayon + + Raised hand with fingers splayed + + Reversed hand with middle finger extended + + Raised hand with part between middle and ring fingers + + Desktop computer + + Printer + + Three button mouse + + Trackball + + Frame with picture + + Card index dividers + + Card file box + + File cabinet + + Wastebasket + + Spiral note pad + + Spiral calendar pad + + Compression + + Old key + + Rolled-up newspaper + + Dagger knife + + Speaking head in silhouette + + Right anger bubble + + Ballot box with ballot + + World map + + Mount fuji + + Tokyo tower + + Statue of liberty + + Silhouette of japan + + Moyai + + Grinning face + + Grinning face with smiling eyes + + Face with tears of joy + + Smiling face with open mouth + + Smiling face with open mouth and smiling eyes + + Smiling face with open mouth and cold sweat + + Smiling face with open mouth and tightly-closed eyes + + Smiling face with halo + + Smiling face with horns + + Winking face + + Smiling face with smiling eyes + + Face savouring delicious food + + Relieved face + + Smiling face with heart-shaped eyes + + Smiling face with sunglasses + + Smirking face + + Neutral face + + Expressionless face + + Unamused face + + Face with cold sweat + + Pensive face + + Confused face + + Confounded face + + Kissing face + + Face throwing a kiss + + Kissing face with smiling eyes + + Kissing face with closed eyes + + Face with stuck-out tongue + + Face with stuck-out tongue and winking eye + + Face with stuck-out tongue and tightly-closed eyes + + Disappointed face + + Worried face + + Angry face + + Pouting face + + Crying face + + Persevering face + + Face with look of triumph + + Disappointed but relieved face + + Frowning face with open mouth + + Anguished face + + Fearful face + + Weary face + + Sleepy face + + Tired face + + Grimacing face + + Loudly crying face + + Face with open mouth + + Hushed face + + Face with open mouth and cold sweat + + Face screaming in fear + + Astonished face + + Flushed face + + Sleeping face + + Dizzy face + + Face without mouth + + Face with medical mask + + Grinning cat face with smiling eyes + + Cat face with tears of joy + + Smiling cat face with open mouth + + Smiling cat face with heart-shaped eyes + + Cat face with wry smile + + Kissing cat face with closed eyes + + Pouting cat face + + Crying cat face + + Weary cat face + + Slightly frowning face + + Slightly smiling face + + Upside-down face + + Face with rolling eyes + + Face with no good gesture + + Face with ok gesture + + Person bowing deeply + + See-no-evil monkey + + Hear-no-evil monkey + + Speak-no-evil monkey + + Happy person raising one hand + + Person raising both hands in celebration + + Person frowning + + Person with pouting face + + Person with folded hands + + Rocket + + Helicopter + + Steam locomotive + + Railway car + + High-speed train + + High-speed train with bullet nose + + Train + + Metro + + Light rail + + Station + + Tram + + Tram car + + Bus + + Oncoming bus + + Trolleybus + + Bus stop + + Minibus + + Ambulance + + Fire engine + + Police car + + Oncoming police car + + Taxi + + Oncoming taxi + + Automobile + + Oncoming automobile + + Recreational vehicle + + Delivery truck + + Articulated lorry + + Tractor + + Monorail + + Mountain railway + + Suspension railway + + Mountain cableway + + Aerial tramway + + Ship + + Rowboat + + Speedboat + + Horizontal traffic light + + Vertical traffic light + + Construction sign + + Police cars revolving light + + Triangular flag on post + + Door + + No entry sign + + Smoking symbol + + No smoking symbol + + Put litter in its place symbol + + Do not litter symbol + + Potable water symbol + + Non-potable water symbol + + Bicycle + + No bicycles + + Bicyclist + + Mountain bicyclist + + Pedestrian + + No pedestrians + + Children crossing + + Mens symbol + + Womens symbol + + Restroom + + Baby symbol + + Toilet + + Water closet + + Shower + + Bath + + Bathtub + + Passport control + + Customs + + Baggage claim + + Left luggage + + Couch and lamp + + Sleeping accommodation + + Shopping bags + + Bellhop bell + + Bed + + Place of worship + + Hammer and wrench + + Shield + + Oil drum + + Motorway + + Railway track + + Motor boat + + Small airplane + + Airplane departure + + Airplane arriving + + Satellite + + Passenger ship + + Zipper-mouth face + + Money-mouth face + + Face with thermometer + + Nerd face + + Thinking face + + Face with head-bandage + + Robot face + + Hugging face + + Sign of the horns + + Crab + + Lion face + + Scorpion + + Turkey + + Unicorn face + + Cheese wedge + + Keycap number sign + + Keycap asterisk + + Keycap digit zero + + Keycap digit one + + Keycap digit two + + Keycap digit three + + Keycap digit four + + Keycap digit five + + Keycap digit six + + Keycap digit seven + + Keycap digit eight + + Keycap digit nine + + Flag for Ascension Island + + Flag for Andorra + + Flag for United Arab Emirates + + Flag for Afghanistan + + Flag for Antigua & Barbuda + + Flag for Anguilla + + Flag for Albania + + Flag for Armenia + + Flag for Angola + + Flag for Antarctica + + Flag for Argentina + + Flag for American Samoa + + Flag for Austria + + Flag for Australia + + Flag for Aruba + + Flag for Åland Islands + + Flag for Azerbaijan + + Flag for Bosnia & Herzegovina + + Flag for Barbados + + Flag for Bangladesh + + Flag for Belgium + + Flag for Burkina Faso + + Flag for Bulgaria + + Flag for Bahrain + + Flag for Burundi + + Flag for Benin + + Flag for St. Barthélemy + + Flag for Bermuda + + Flag for Brunei + + Flag for Bolivia + + Flag for Caribbean Netherlands + + Flag for Brazil + + Flag for Bahamas + + Flag for Bhutan + + Flag for Bouvet Island + + Flag for Botswana + + Flag for Belarus + + Flag for Belize + + Flag for Canada + + Flag for Cocos Islands + + Flag for Congo - Kinshasa + + Flag for Central African Republic + + Flag for Congo - Brazzaville + + Flag for Switzerland + + Flag for Côte d\'Ivoire + + Flag for Cook Islands + + Flag for Chile + + Flag for Cameroon + + Flag for China + + Flag for Colombia + + Flag for Clipperton Island + + Flag for Costa Rica + + Flag for Cuba + + Flag for Cape Verde + + Flag for Curaçao + + Flag for Christmas Island + + Flag for Cyprus + + Flag for Czech Republic + + Flag for Germany + + Flag for Diego Garcia + + Flag for Djibouti + + Flag for Denmark + + Flag for Dominica + + Flag for Dominican Republic + + Flag for Algeria + + Flag for Ceuta & Melilla + + Flag for Ecuador + + Flag for Estonia + + Flag for Egypt + + Flag for Western Sahara + + Flag for Eritrea + + Flag for Spain + + Flag for Ethiopia + + Flag for European Union + + Flag for Finland + + Flag for Fiji + + Flag for Falkland Islands + + Flag for Micronesia + + Flag for Faroe Islands + + Flag for France + + Flag for Gabon + + Flag for United Kingdom + + Flag for Grenada + + Flag for Georgia + + Flag for French Guiana + + Flag for Guernsey + + Flag for Ghana + + Flag for Gibraltar + + Flag for Greenland + + Flag for Gambia + + Flag for Guinea + + Flag for Guadeloupe + + Flag for Equatorial Guinea + + Flag for Greece + + Flag for South Georgia & South Sandwich Islands + + Flag for Guatemala + + Flag for Guam + + Flag for Guinea-Bissau + + Flag for Guyana + + Flag for Hong Kong + + Flag for Heard & McDonald Islands + + Flag for Honduras + + Flag for Croatia + + Flag for Haiti + + Flag for Hungary + + Flag for Canary Islands + + Flag for Indonesia + + Flag for Ireland + + Flag for Israel + + Flag for Isle of Man + + Flag for India + + Flag for British Indian Ocean Territory + + Flag for Iraq + + Flag for Iran + + Flag for Iceland + + Flag for Italy + + Flag for Jersey + + Flag for Jamaica + + Flag for Jordan + + Flag for Japan + + Flag for Kenya + + Flag for Kyrgyzstan + + Flag for Cambodia + + Flag for Kiribati + + Flag for Comoros + + Flag for St. Kitts & Nevis + + Flag for North Korea + + Flag for South Korea + + Flag for Kuwait + + Flag for Cayman Islands + + Flag for Kazakhstan + + Flag for Laos + + Flag for Lebanon + + Flag for St. Lucia + + Flag for Liechtenstein + + Flag for Sri Lanka + + Flag for Liberia + + Flag for Lesotho + + Flag for Lithuania + + Flag for Luxembourg + + Flag for Latvia + + Flag for Libya + + Flag for Morocco + + Flag for Monaco + + Flag for Moldova + + Flag for Montenegro + + Flag for St. Martin + + Flag for Madagascar + + Flag for Marshall Islands + + Flag for Macedonia + + Flag for Mali + + Flag for Myanmar + + Flag for Mongolia + + Flag for Macau + + Flag for Northern Mariana Islands + + Flag for Martinique + + Flag for Mauritania + + Flag for Montserrat + + Flag for Malta + + Flag for Mauritius + + Flag for Maldives + + Flag for Malawi + + Flag for Mexico + + Flag for Malaysia + + Flag for Mozambique + + Flag for Namibia + + Flag for New Caledonia + + Flag for Niger + + Flag for Norfolk Island + + Flag for Nigeria + + Flag for Nicaragua + + Flag for Netherlands + + Flag for Norway + + Flag for Nepal + + Flag for Nauru + + Flag for Niue + + Flag for New Zealand + + Flag for Oman + + Flag for Panama + + Flag for Peru + + Flag for French Polynesia + + Flag for Papua New Guinea + + Flag for Philippines + + Flag for Pakistan + + Flag for Poland + + Flag for St. Pierre & Miquelon + + Flag for Pitcairn Islands + + Flag for Puerto Rico + + Flag for Palestinian Territories + + Flag for Portugal + + Flag for Palau + + Flag for Paraguay + + Flag for Qatar + + Flag for Réunion + + Flag for Romania + + Flag for Serbia + + Flag for Russia + + Flag for Rwanda + + Flag for Saudi Arabia + + Flag for Solomon Islands + + Flag for Seychelles + + Flag for Sudan + + Flag for Sweden + + Flag for Singapore + + Flag for St. Helena + + Flag for Slovenia + + Flag for Svalbard & Jan Mayen + + Flag for Slovakia + + Flag for Sierra Leone + + Flag for San Marino + + Flag for Senegal + + Flag for Somalia + + Flag for Suriname + + Flag for South Sudan + + Flag for São Tomé & Príncipe + + Flag for El Salvador + + Flag for Sint Maarten + + Flag for Syria + + Flag for Swaziland + + Flag for Tristan da Cunha + + Flag for Turks & Caicos Islands + + Flag for Chad + + Flag for French Southern Territories + + Flag for Togo + + Flag for Thailand + + Flag for Tajikistan + + Flag for Tokelau + + Flag for Timor-Leste + + Flag for Turkmenistan + + Flag for Tunisia + + Flag for Tonga + + Flag for Turkey + + Flag for Trinidad & Tobago + + Flag for Tuvalu + + Flag for Taiwan + + Flag for Tanzania + + Flag for Ukraine + + Flag for Uganda + + Flag for U.S. Outlying Islands + + Flag for United States + + Flag for Uruguay + + Flag for Uzbekistan + + Flag for Vatican City + + Flag for St. Vincent & Grenadines + + Flag for Venezuela + + Flag for British Virgin Islands + + Flag for U.S. Virgin Islands + + Flag for Vietnam + + Flag for Vanuatu + + Flag for Wallis & Futuna + + Flag for Samoa + + Flag for Kosovo + + Flag for Yemen + + Flag for Mayotte + + Flag for South Africa + + Flag for Zambia + + Flag for Zimbabwe + diff --git a/app/src/main/res/values/strings-letter-descriptions.xml b/app/src/main/res/values/strings-letter-descriptions.xml new file mode 100644 index 00000000..297b6bed --- /dev/null +++ b/app/src/main/res/values/strings-letter-descriptions.xml @@ -0,0 +1,384 @@ + + + + + + Feminine ordinal indicator + + Micro sign + + Masculine ordinal indicator + + Sharp S + + A, grave + + A, acute + + A, circumflex + + A, tilde + + A, diaeresis + + A, ring above + + A, E, ligature + + C, cedilla + + E, grave + + E, acute + + E, circumflex + + E, diaeresis + + I, grave + + I, acute + + I, circumflex + + I, diaeresis + + Eth + + N, tilde + + O, grave + + O, acute + + O, circumflex + + O, tilde + + O, diaeresis + + O, stroke + + U, grave + + U, acute + + U, circumflex + + U, diaeresis + + Y, acute + + Thorn + + Y, diaeresis + + A, macron + + A, breve + + A, ogonek + + C, acute + + C, circumflex + + C, dot above + + C, caron + + D, caron + + D, stroke + + E, macron + + E, breve + + E, dot above + + E, ogonek + + E, caron + + G, circumflex + + G, breve + + G, dot above + + G, cedilla + + H, circumflex + + H, stroke + + I, tilde + + I, macron + + I, breve + + I, ogonek + + Dotless I + + I, J, ligature + + J, circumflex + + K, cedilla + + Kra + + L, acute + + L, cedilla + + L, caron + + L, middle dot + + L, stroke + + N, acute + + N, cedilla + + N, caron + + N, preceded by apostrophe + + Eng + + O, macron + + O, breve + + O, double acute + + O, E, ligature + + R, acute + + R, cedilla + + R, caron + + S, acute + + S, circumflex + + S, cedilla + + S, caron + + T, cedilla + + T, caron + + T, stroke + + U, tilde + + U, macron + + U, breve + + U, ring above + + U, double acute + + U, ogonek + + W, circumflex + + Y, circumflex + + Z, acute + + Z, dot above + + Z, caron + + Long S + + O, horn + + U, horn + + S, comma below + + T, comma below + + Schwa + + A, dot below + + A, hook above + + A, circumflex and acute + + A, circumflex and grave + + A, circumflex and hook above + + A, circumflex and tilde + + A, circumflex and dot below + + A, breve and acute + + A, breve and grave + + A, breve and hook above + + A, breve and tilde + + A, breve and dot below + + E, dot below + + E, hook above + + E, tilde + + E, circumflex and acute + + E, circumflex and grave + + E, circumflex and hook above + + E, circumflex and tilde + + E, circumflex and dot below + + I, hook above + + I, dot below + + O, dot below + + O, hook above + + O, circumflex and acute + + O, circumflex and grave + + O, circumflex and hook above + + O, circumflex and tilde + + O, circumflex and dot below + + O, horn and acute + + O, horn and grave + + O, horn and hook above + + O, horn and tilde + + O, horn and dot below + + U, dot below + + U, hook above + + U, horn and acute + + U, horn and grave + + U, horn and hook above + + U, horn and tilde + + U, horn and dot below + + Y, grave + + Y, dot below + + Y, hook above + + Y, tilde + + Inverted exclamation mark + + Left-pointing double angle quotation mark + + Middle dot + + Superscript one + + Right-pointing double angle quotation mark + + Inverted question mark + + Left single quotation mark + + Right single quotation mark + + Single low-9 quotation mark + + Left double quotation mark + + Right double quotation mark + + Dagger + + Double dagger + + Per mille sign + + Prime + + Double prime + + Single left-pointing angle quotation mark + + Single right-pointing angle quotation mark + + Superscript four + + Superscript latin small letter n + + Peso sign + + Care of + + Rightwards arrow + + Downwards arrow + + Empty set + + Increment + + Less-than or equal to + + Greater-than or equal to + + Black star + diff --git a/app/src/main/res/values/strings-production.xml b/app/src/main/res/values/strings-production.xml new file mode 100644 index 00000000..8064b994 --- /dev/null +++ b/app/src/main/res/values/strings-production.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/app/src/main/res/values/strings-talkback-descriptions.xml b/app/src/main/res/values/strings-talkback-descriptions.xml new file mode 100644 index 00000000..307cd47a --- /dev/null +++ b/app/src/main/res/values/strings-talkback-descriptions.xml @@ -0,0 +1,179 @@ + + + + + Plug in a headset to hear password keys spoken aloud. + + + Current text is "%s" + + No text entered + + + %1$s corrects %2$s to %3$s + + %1$s performs auto-correction + + + No suggestion + + + Unknown character + + Shift + + More symbols + + Shift + + Symbols + + Shift + + Delete + + Symbols + + Letters + + Numbers + + Settings + + Tab + + Space + + Voice input + + Emoji + + Return + + Search + + Dot + + Switch language + + Next + + Previous + + + Shift enabled + + Caps lock enabled + + Symbols mode + + More symbols mode + + Letters mode + + Phone mode + + Phone symbols mode + + + Keyboard hidden + + Showing %s keyboard + + date + + date and time + + email + + messaging + + number + + phone + + text + + time + + URL + + + Recents + + People + + Objects + + Nature + + Places + + Symbols + + Flags + + Smiley & People + + Animals & Nature + + Food & Drink + + Travel & Places + + Activity + + Emoticons + + + Capital %s + + Capital I + + Capital I, dot above + + Unknown symbol + + Unknown emoji + + Bored face + + Embarrassed face + + Face wearing sunglasses + + Surprised face + + Kissing face + + Frowning face + + + Alternative characters are available + + Alternative characters are dismissed + + + Alternative suggestions are available + + Alternative suggestions are dismissed + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..1b534d77 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,595 @@ + + + + + OpenBoard + + OpenBoard Spell Checker + + OpenBoard Settings + + OpenBoard Spell Checker Settings + + + + Input options + + + Look up contact names + + + Spell checker uses entries from your contact list + + + Vibrate on keypress + + + Sound on keypress + + + Popup on keypress + + + Preferences + + Accounts & Privacy + + Appearance & Layouts + + Gesture Typing + + Text correction + + Advanced + + Theme + + + Enable split keyboard + + + Google Keyboard Sync + + Sync is turned on + + Sync your personal dictionary across devices + + Sync now + + Delete cloud data + + Deletes your synced data from Google + + Your synced data will be deleted from the cloud. Are you sure? + + Delete + + Cancel + + Your personal dictionary will be synced and backed up to + Google servers. The statistical information of word frequency may be collected to help + improve our products. The collection and usage of all the information will be compliant with + Google\'s Privacy Policy. + + + + Please add a Google account to this device to enable this feature + + + + Sync is not available for devices with Google Apps for Business accounts + + + + Switch to other input methods + + Language switch key covers other input methods too + + Language switch key + + Show when multiple input languages are enabled + + + Key popup dismiss delay + + No delay + + Default + + + %sms + + System default + + + Suggest Contact names + + Use names from Contacts for suggestions and corrections + + + Personalized suggestions + + + "Improve %s" + + + Double-space period + + Double tap on spacebar inserts a period followed by a space + + + Auto-capitalization + + Capitalize the first word of each sentence + + + Personal dictionary + + + Add-on dictionaries + + Main dictionary + + + Show correction suggestions + + Display suggested words while typing + + + Block offensive words + + Do not suggest potentially offensive words + + + + Auto-correction + + Spacebar and punctuation automatically correct mistyped words + + Off + + Modest + + Aggressive + + Very aggressive + + + Next-word suggestions + + Use the previous word in making suggestions + + + Enable gesture typing + + Input a word by sliding through the letters + + Show gesture trail + + Dynamic floating preview + + See the suggested word while gesturing + + Phrase gesture + + Input spaces during gestures by gliding to the space key + + + Voice input key + + No voice input methods enabled. Check Languages & input settings. + + + Configure input methods + + + Languages + + + Help & feedback + + + Languages + + + Tap again to save + + Tap here to save + + + Dictionary available + + + Keyboard theme + + + Switch accounts + + No accounts selected + + Currently using %1$s + + OK + + Cancel + + Sign out + + Select an account to use + + + English (UK) + + English (US) + + Spanish (US) + + Hinglish + + Serbian (Latin) + + English (UK) (%s) + + English (US) (%s) + + Spanish (US) (%s) + + Hinglish (%s) + + Serbian (%s) + + %s (Traditional) + + %s (Compact) + + No language (Alphabet) + + Alphabet (QWERTY) + + Alphabet (QWERTZ) + + Alphabet (AZERTY) + + Alphabet (Dvorak) + + Alphabet (Colemak) + + Alphabet (PC) + + Emoji + + + Keyboard theme + + Holo White + + Holo Blue + + Material Dark + + Material Light + Material Dark Border + Material Light Border + + + Custom input styles + + Add style + + Add + + Remove + + Save + + Language + + Layout + + "Your custom input style needs to be enabled before you start using it. Do you want to enable it now?" + + Enable + + Not now + + "The same input style already exists: %s" + + + Keypress vibration duration + + Keypress sound volume + + Key long press delay + + Emoji for physical keyboard + + Physical Alt key shows the emoji palette + + + Default + + + "Welcome to %s" + + with Gesture Typing + + Get started + + Next step + + "Setting up %s" + + 1 + + "Enable %s" + + "Please check \"%s\" in your Languages & input settings. This will authorize it to run on your device." + + "%s is already enabled in your Languages & input settings, so this step is done. On to the next one!" + + Enable in Settings + + 2 + + "Switch to %s" + + "Next, select \"%s\" as your active text-input method." + + Switch input methods + + 3 + + "Congratulations, you're all set!" + + Now you can type in all your favorite apps with %s. + + Configure additional languages + + Finished + + Show app icon + + Display application icon in the launcher + + + Dictionary Provider + + Dictionary Provider + + Dictionary Service + + + Dictionary update information + + + Add-on dictionaries + + Dictionary available + Settings for dictionaries + + User dictionaries + + User dictionary + + Dictionary available + + Currently downloading + + Installed + + Installed, disabled + + + Problem connecting to dictionary service + + No dictionaries available + + + Refresh + + Last updated + + + Checking for updates + + Loading… + + + Main dictionary + + + Cancel + + Settings + + + Install + + Cancel + + Delete + + + The selected language on your mobile device has an available dictionary.<br/> +We recommend <b>downloading</b> the %1$s dictionary to improve your typing experience.<br/> +<br/> +The download could take a minute or two over 3G. Charges may apply if you don\'t have an <b>unlimited data plan</b>.<br/> +If you are not sure which data plan you have, we recommend finding a Wi-Fi connection to start the download automatically.<br/> +<br/> +Tip: You can download and remove dictionaries by going to <b>Languages & input</b> in the <b>Settings</b> menu of your mobile device. + Download now (%1$.1fMB) + Download over Wi-Fi + + A dictionary is available for %1$s + + Press to review and download + + + Downloading: suggestions for %1$s will be ready soon. + + + Version %1$s + + + + "" + + + Add + + + Add to dictionary + + + Phrase + + + More options + + + Less options + + + OK + + + Word: + + + Shortcut: + + + Language: + + + Type a word + + + Optional shortcut + + + Edit word + + + Edit + + + Delete + + + You don\'t have any words in the user dictionary. To add a word, tap the Add (+) button. + + + For all languages + + + More languages… + + + Delete + + + \u0020ABCDEFGHIJKLMNOPQRSTUVWXYZ + diff --git a/app/src/main/res/values/themes-common.xml b/app/src/main/res/values/themes-common.xml new file mode 100644 index 00000000..980244a7 --- /dev/null +++ b/app/src/main/res/values/themes-common.xml @@ -0,0 +1,133 @@ + + + + + + + + diff --git a/app/src/main/res/values/themes-holo.xml b/app/src/main/res/values/themes-holo.xml new file mode 100644 index 00000000..efac6562 --- /dev/null +++ b/app/src/main/res/values/themes-holo.xml @@ -0,0 +1,43 @@ + + + + + + diff --git a/app/src/main/res/values/themes-ics.xml b/app/src/main/res/values/themes-ics.xml new file mode 100644 index 00000000..00e75d97 --- /dev/null +++ b/app/src/main/res/values/themes-ics.xml @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/themes-klp.xml b/app/src/main/res/values/themes-klp.xml new file mode 100644 index 00000000..8d49e736 --- /dev/null +++ b/app/src/main/res/values/themes-klp.xml @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/themes-lxx-dark-border.xml b/app/src/main/res/values/themes-lxx-dark-border.xml new file mode 100644 index 00000000..e221e997 --- /dev/null +++ b/app/src/main/res/values/themes-lxx-dark-border.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/themes-lxx-dark.xml b/app/src/main/res/values/themes-lxx-dark.xml new file mode 100644 index 00000000..8adf045d --- /dev/null +++ b/app/src/main/res/values/themes-lxx-dark.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/themes-lxx-light-border.xml b/app/src/main/res/values/themes-lxx-light-border.xml new file mode 100644 index 00000000..8b00c59a --- /dev/null +++ b/app/src/main/res/values/themes-lxx-light-border.xml @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/themes-lxx-light.xml b/app/src/main/res/values/themes-lxx-light.xml new file mode 100644 index 00000000..8a0fc289 --- /dev/null +++ b/app/src/main/res/values/themes-lxx-light.xml @@ -0,0 +1,157 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/themes-lxx.xml b/app/src/main/res/values/themes-lxx.xml new file mode 100644 index 00000000..463306b4 --- /dev/null +++ b/app/src/main/res/values/themes-lxx.xml @@ -0,0 +1,45 @@ + + + + + + diff --git a/app/src/main/res/values/touch-position-correction.xml b/app/src/main/res/values/touch-position-correction.xml new file mode 100644 index 00000000..1731cdd3 --- /dev/null +++ b/app/src/main/res/values/touch-position-correction.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + 0.0038756 + -0.0005677 + 0.1577026 + + + -0.0236678 + 0.0381731 + 0.1529972 + + + -0.0086827 + 0.0880847 + 0.1522819 + + diff --git a/app/src/main/res/xml-hu-rZZ/kbd_qwerty.xml b/app/src/main/res/xml-hu-rZZ/kbd_qwerty.xml new file mode 100644 index 00000000..432a2fcf --- /dev/null +++ b/app/src/main/res/xml-hu-rZZ/kbd_qwerty.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml-land/kbd_number.xml b/app/src/main/res/xml-land/kbd_number.xml new file mode 100644 index 00000000..cc708033 --- /dev/null +++ b/app/src/main/res/xml-land/kbd_number.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/app/src/main/res/xml-land/kbd_phone.xml b/app/src/main/res/xml-land/kbd_phone.xml new file mode 100644 index 00000000..3128e2d6 --- /dev/null +++ b/app/src/main/res/xml-land/kbd_phone.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/app/src/main/res/xml-land/kbd_phone_symbols.xml b/app/src/main/res/xml-land/kbd_phone_symbols.xml new file mode 100644 index 00000000..9d5877e3 --- /dev/null +++ b/app/src/main/res/xml-land/kbd_phone_symbols.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/app/src/main/res/xml-sw600dp-land/kbd_more_keys_keyboard_template.xml b/app/src/main/res/xml-sw600dp-land/kbd_more_keys_keyboard_template.xml new file mode 100644 index 00000000..b74f939f --- /dev/null +++ b/app/src/main/res/xml-sw600dp-land/kbd_more_keys_keyboard_template.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/app/src/main/res/xml-sw600dp-land/kbd_number.xml b/app/src/main/res/xml-sw600dp-land/kbd_number.xml new file mode 100644 index 00000000..0ecca852 --- /dev/null +++ b/app/src/main/res/xml-sw600dp-land/kbd_number.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/app/src/main/res/xml-sw600dp-land/kbd_phone.xml b/app/src/main/res/xml-sw600dp-land/kbd_phone.xml new file mode 100644 index 00000000..770633e6 --- /dev/null +++ b/app/src/main/res/xml-sw600dp-land/kbd_phone.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/app/src/main/res/xml-sw600dp-land/kbd_phone_symbols.xml b/app/src/main/res/xml-sw600dp-land/kbd_phone_symbols.xml new file mode 100644 index 00000000..d7acbe74 --- /dev/null +++ b/app/src/main/res/xml-sw600dp-land/kbd_phone_symbols.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/app/src/main/res/xml-sw600dp-land/key_space_3kw.xml b/app/src/main/res/xml-sw600dp-land/key_space_3kw.xml new file mode 100644 index 00000000..87f625d2 --- /dev/null +++ b/app/src/main/res/xml-sw600dp-land/key_space_3kw.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp-land/row_qwerty4.xml b/app/src/main/res/xml-sw600dp-land/row_qwerty4.xml new file mode 100644 index 00000000..07a73f26 --- /dev/null +++ b/app/src/main/res/xml-sw600dp-land/row_qwerty4.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp-land/rows_qwerty.xml b/app/src/main/res/xml-sw600dp-land/rows_qwerty.xml new file mode 100644 index 00000000..ecba7616 --- /dev/null +++ b/app/src/main/res/xml-sw600dp-land/rows_qwerty.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/kbd_more_keys_keyboard_template.xml b/app/src/main/res/xml-sw600dp/kbd_more_keys_keyboard_template.xml new file mode 100644 index 00000000..b3cdb9e3 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/kbd_more_keys_keyboard_template.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/app/src/main/res/xml-sw600dp/kbd_number.xml b/app/src/main/res/xml-sw600dp/kbd_number.xml new file mode 100644 index 00000000..cf9f60dd --- /dev/null +++ b/app/src/main/res/xml-sw600dp/kbd_number.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/app/src/main/res/xml-sw600dp/kbd_phone.xml b/app/src/main/res/xml-sw600dp/kbd_phone.xml new file mode 100644 index 00000000..f13696c6 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/kbd_phone.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/app/src/main/res/xml-sw600dp/kbd_phone_symbols.xml b/app/src/main/res/xml-sw600dp/kbd_phone_symbols.xml new file mode 100644 index 00000000..08b175b0 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/kbd_phone_symbols.xml @@ -0,0 +1,29 @@ + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/key_comma.xml b/app/src/main/res/xml-sw600dp/key_comma.xml new file mode 100644 index 00000000..b814f269 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/key_comma.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/key_period.xml b/app/src/main/res/xml-sw600dp/key_period.xml new file mode 100644 index 00000000..57d43720 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/key_period.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/key_settings.xml b/app/src/main/res/xml-sw600dp/key_settings.xml new file mode 100644 index 00000000..18f12952 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/key_settings.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/key_space_3kw.xml b/app/src/main/res/xml-sw600dp/key_space_3kw.xml new file mode 100644 index 00000000..4737aba8 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/key_space_3kw.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/key_space_7kw.xml b/app/src/main/res/xml-sw600dp/key_space_7kw.xml new file mode 100644 index 00000000..6fcc3fb5 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/key_space_7kw.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/key_space_symbols.xml b/app/src/main/res/xml-sw600dp/key_space_symbols.xml new file mode 100644 index 00000000..51125e62 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/key_space_symbols.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/app/src/main/res/xml-sw600dp/key_styles_common.xml b/app/src/main/res/xml-sw600dp/key_styles_common.xml new file mode 100644 index 00000000..cdf79bb9 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/key_styles_common.xml @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/key_styles_enter.xml b/app/src/main/res/xml-sw600dp/key_styles_enter.xml new file mode 100644 index 00000000..0544b870 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/key_styles_enter.xml @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/keys_arabic3_left.xml b/app/src/main/res/xml-sw600dp/keys_arabic3_left.xml new file mode 100644 index 00000000..9b40bb9e --- /dev/null +++ b/app/src/main/res/xml-sw600dp/keys_arabic3_left.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/keys_dvorak_123.xml b/app/src/main/res/xml-sw600dp/keys_dvorak_123.xml new file mode 100644 index 00000000..8f44811f --- /dev/null +++ b/app/src/main/res/xml-sw600dp/keys_dvorak_123.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/keys_exclamation_question.xml b/app/src/main/res/xml-sw600dp/keys_exclamation_question.xml new file mode 100644 index 00000000..a6f1afe0 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/keys_exclamation_question.xml @@ -0,0 +1,32 @@ + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/keys_farsi3_right.xml b/app/src/main/res/xml-sw600dp/keys_farsi3_right.xml new file mode 100644 index 00000000..6bf45344 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/keys_farsi3_right.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/keys_pcqwerty2_right3.xml b/app/src/main/res/xml-sw600dp/keys_pcqwerty2_right3.xml new file mode 100644 index 00000000..90e93e75 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/keys_pcqwerty2_right3.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml-sw600dp/keys_pcqwerty3_right2.xml b/app/src/main/res/xml-sw600dp/keys_pcqwerty3_right2.xml new file mode 100644 index 00000000..63df0753 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/keys_pcqwerty3_right2.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/keys_pcqwerty4_right3.xml b/app/src/main/res/xml-sw600dp/keys_pcqwerty4_right3.xml new file mode 100644 index 00000000..3255396f --- /dev/null +++ b/app/src/main/res/xml-sw600dp/keys_pcqwerty4_right3.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/row_pcqwerty5.xml b/app/src/main/res/xml-sw600dp/row_pcqwerty5.xml new file mode 100644 index 00000000..11b18aaa --- /dev/null +++ b/app/src/main/res/xml-sw600dp/row_pcqwerty5.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/row_qwerty4.xml b/app/src/main/res/xml-sw600dp/row_qwerty4.xml new file mode 100644 index 00000000..4fe4cf0d --- /dev/null +++ b/app/src/main/res/xml-sw600dp/row_qwerty4.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rowkeys_dvorak3.xml b/app/src/main/res/xml-sw600dp/rowkeys_dvorak3.xml new file mode 100644 index 00000000..b8f95759 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rowkeys_dvorak3.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rowkeys_pcqwerty1.xml b/app/src/main/res/xml-sw600dp/rowkeys_pcqwerty1.xml new file mode 100644 index 00000000..1b3592c5 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rowkeys_pcqwerty1.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_arabic.xml b/app/src/main/res/xml-sw600dp/rows_arabic.xml new file mode 100644 index 00000000..3542dc48 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_arabic.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_armenian_phonetic.xml b/app/src/main/res/xml-sw600dp/rows_armenian_phonetic.xml new file mode 100644 index 00000000..39da7a3a --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_armenian_phonetic.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_azerty.xml b/app/src/main/res/xml-sw600dp/rows_azerty.xml new file mode 100644 index 00000000..d2b2a721 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_azerty.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_bengali.xml b/app/src/main/res/xml-sw600dp/rows_bengali.xml new file mode 100644 index 00000000..adcfc45b --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_bengali.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_bengali_akkhor.xml b/app/src/main/res/xml-sw600dp/rows_bengali_akkhor.xml new file mode 100644 index 00000000..172226a3 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_bengali_akkhor.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_bepo.xml b/app/src/main/res/xml-sw600dp/rows_bepo.xml new file mode 100644 index 00000000..d64cdeda --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_bepo.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_bulgarian.xml b/app/src/main/res/xml-sw600dp/rows_bulgarian.xml new file mode 100644 index 00000000..be644b96 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_bulgarian.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_bulgarian_bds.xml b/app/src/main/res/xml-sw600dp/rows_bulgarian_bds.xml new file mode 100644 index 00000000..a3b2e4dd --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_bulgarian_bds.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_colemak.xml b/app/src/main/res/xml-sw600dp/rows_colemak.xml new file mode 100644 index 00000000..29dc4cbb --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_colemak.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_dvorak.xml b/app/src/main/res/xml-sw600dp/rows_dvorak.xml new file mode 100644 index 00000000..e0f87c93 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_dvorak.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_east_slavic.xml b/app/src/main/res/xml-sw600dp/rows_east_slavic.xml new file mode 100644 index 00000000..f574135f --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_east_slavic.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_farsi.xml b/app/src/main/res/xml-sw600dp/rows_farsi.xml new file mode 100644 index 00000000..f0d44da1 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_farsi.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_georgian.xml b/app/src/main/res/xml-sw600dp/rows_georgian.xml new file mode 100644 index 00000000..64d00f1e --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_georgian.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_greek.xml b/app/src/main/res/xml-sw600dp/rows_greek.xml new file mode 100644 index 00000000..578792d1 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_greek.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_hebrew.xml b/app/src/main/res/xml-sw600dp/rows_hebrew.xml new file mode 100644 index 00000000..565da8fe --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_hebrew.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_hindi.xml b/app/src/main/res/xml-sw600dp/rows_hindi.xml new file mode 100644 index 00000000..3d6de315 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_hindi.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_hindi_compact.xml b/app/src/main/res/xml-sw600dp/rows_hindi_compact.xml new file mode 100644 index 00000000..23220835 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_hindi_compact.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_kannada.xml b/app/src/main/res/xml-sw600dp/rows_kannada.xml new file mode 100644 index 00000000..58858412 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_kannada.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_khmer.xml b/app/src/main/res/xml-sw600dp/rows_khmer.xml new file mode 100644 index 00000000..dd843888 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_khmer.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_lao.xml b/app/src/main/res/xml-sw600dp/rows_lao.xml new file mode 100644 index 00000000..1f545da0 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_lao.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_malayalam.xml b/app/src/main/res/xml-sw600dp/rows_malayalam.xml new file mode 100644 index 00000000..d69c04e9 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_malayalam.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_marathi.xml b/app/src/main/res/xml-sw600dp/rows_marathi.xml new file mode 100644 index 00000000..e536845c --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_marathi.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_mongolian.xml b/app/src/main/res/xml-sw600dp/rows_mongolian.xml new file mode 100644 index 00000000..4fdccef2 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_mongolian.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_nepali_romanized.xml b/app/src/main/res/xml-sw600dp/rows_nepali_romanized.xml new file mode 100644 index 00000000..df6ffa9b --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_nepali_romanized.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_nepali_traditional.xml b/app/src/main/res/xml-sw600dp/rows_nepali_traditional.xml new file mode 100644 index 00000000..32d83751 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_nepali_traditional.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_nordic.xml b/app/src/main/res/xml-sw600dp/rows_nordic.xml new file mode 100644 index 00000000..7ac3df56 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_nordic.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_number_normal.xml b/app/src/main/res/xml-sw600dp/rows_number_normal.xml new file mode 100644 index 00000000..57ea5f47 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_number_normal.xml @@ -0,0 +1,163 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_number_password.xml b/app/src/main/res/xml-sw600dp/rows_number_password.xml new file mode 100644 index 00000000..db7378c7 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_number_password.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_pcqwerty.xml b/app/src/main/res/xml-sw600dp/rows_pcqwerty.xml new file mode 100644 index 00000000..fa2e8182 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_pcqwerty.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_phone.xml b/app/src/main/res/xml-sw600dp/rows_phone.xml new file mode 100644 index 00000000..6c3137ab --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_phone.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_qwerty.xml b/app/src/main/res/xml-sw600dp/rows_qwerty.xml new file mode 100644 index 00000000..d02cf53a --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_qwerty.xml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_qwertz.xml b/app/src/main/res/xml-sw600dp/rows_qwertz.xml new file mode 100644 index 00000000..1b77a7f0 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_qwertz.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_serbian_qwertz.xml b/app/src/main/res/xml-sw600dp/rows_serbian_qwertz.xml new file mode 100644 index 00000000..d0a21f53 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_serbian_qwertz.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_sinhala.xml b/app/src/main/res/xml-sw600dp/rows_sinhala.xml new file mode 100644 index 00000000..b5cd32bf --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_sinhala.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_south_slavic.xml b/app/src/main/res/xml-sw600dp/rows_south_slavic.xml new file mode 100644 index 00000000..42746f93 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_south_slavic.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_spanish.xml b/app/src/main/res/xml-sw600dp/rows_spanish.xml new file mode 100644 index 00000000..f12b24b3 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_spanish.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_swiss.xml b/app/src/main/res/xml-sw600dp/rows_swiss.xml new file mode 100644 index 00000000..92742641 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_swiss.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_symbols.xml b/app/src/main/res/xml-sw600dp/rows_symbols.xml new file mode 100644 index 00000000..08d730c8 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_symbols.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_symbols_shift.xml b/app/src/main/res/xml-sw600dp/rows_symbols_shift.xml new file mode 100644 index 00000000..bfaf0556 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_symbols_shift.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_tamil.xml b/app/src/main/res/xml-sw600dp/rows_tamil.xml new file mode 100644 index 00000000..a649508c --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_tamil.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_telugu.xml b/app/src/main/res/xml-sw600dp/rows_telugu.xml new file mode 100644 index 00000000..4de98885 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_telugu.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_thai.xml b/app/src/main/res/xml-sw600dp/rows_thai.xml new file mode 100644 index 00000000..0aa2aced --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_thai.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-sw600dp/rows_uzbek.xml b/app/src/main/res/xml-sw600dp/rows_uzbek.xml new file mode 100644 index 00000000..c674eb85 --- /dev/null +++ b/app/src/main/res/xml-sw600dp/rows_uzbek.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml-v16/keystyle_devanagari_sign_anusvara.xml b/app/src/main/res/xml-v16/keystyle_devanagari_sign_anusvara.xml new file mode 100644 index 00000000..b18ec628 --- /dev/null +++ b/app/src/main/res/xml-v16/keystyle_devanagari_sign_anusvara.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-v16/keystyle_devanagari_sign_candrabindu.xml b/app/src/main/res/xml-v16/keystyle_devanagari_sign_candrabindu.xml new file mode 100644 index 00000000..8e5353a8 --- /dev/null +++ b/app/src/main/res/xml-v16/keystyle_devanagari_sign_candrabindu.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-v16/keystyle_devanagari_sign_nukta.xml b/app/src/main/res/xml-v16/keystyle_devanagari_sign_nukta.xml new file mode 100644 index 00000000..cb0f9040 --- /dev/null +++ b/app/src/main/res/xml-v16/keystyle_devanagari_sign_nukta.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-v16/keystyle_devanagari_sign_virama.xml b/app/src/main/res/xml-v16/keystyle_devanagari_sign_virama.xml new file mode 100644 index 00000000..6ba6c16b --- /dev/null +++ b/app/src/main/res/xml-v16/keystyle_devanagari_sign_virama.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-v16/keystyle_devanagari_sign_visarga.xml b/app/src/main/res/xml-v16/keystyle_devanagari_sign_visarga.xml new file mode 100644 index 00000000..a62a2b2b --- /dev/null +++ b/app/src/main/res/xml-v16/keystyle_devanagari_sign_visarga.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_aa.xml b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_aa.xml new file mode 100644 index 00000000..98323751 --- /dev/null +++ b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_aa.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_ai.xml b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_ai.xml new file mode 100644 index 00000000..c97e8fcf --- /dev/null +++ b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_ai.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_au.xml b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_au.xml new file mode 100644 index 00000000..41e18eca --- /dev/null +++ b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_au.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_candra_e.xml b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_candra_e.xml new file mode 100644 index 00000000..131728ec --- /dev/null +++ b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_candra_e.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_candra_o.xml b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_candra_o.xml new file mode 100644 index 00000000..77455782 --- /dev/null +++ b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_candra_o.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_e.xml b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_e.xml new file mode 100644 index 00000000..ed1fac2b --- /dev/null +++ b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_e.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_i.xml b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_i.xml new file mode 100644 index 00000000..f3e1dd74 --- /dev/null +++ b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_i.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_ii.xml b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_ii.xml new file mode 100644 index 00000000..17555573 --- /dev/null +++ b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_ii.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_o.xml b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_o.xml new file mode 100644 index 00000000..d0a0d0f7 --- /dev/null +++ b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_o.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_u.xml b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_u.xml new file mode 100644 index 00000000..82dbff58 --- /dev/null +++ b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_u.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_uu.xml b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_uu.xml new file mode 100644 index 00000000..34a88319 --- /dev/null +++ b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_uu.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_vocalic_r.xml b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_vocalic_r.xml new file mode 100644 index 00000000..c7357547 --- /dev/null +++ b/app/src/main/res/xml-v16/keystyle_devanagari_vowel_sign_vocalic_r.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/additional_subtype_settings.xml b/app/src/main/res/xml/additional_subtype_settings.xml new file mode 100644 index 00000000..ad280c14 --- /dev/null +++ b/app/src/main/res/xml/additional_subtype_settings.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/app/src/main/res/xml/dictionary_settings.xml b/app/src/main/res/xml/dictionary_settings.xml new file mode 100644 index 00000000..684dfe5b --- /dev/null +++ b/app/src/main/res/xml/dictionary_settings.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_arabic.xml b/app/src/main/res/xml/kbd_arabic.xml new file mode 100644 index 00000000..225174f9 --- /dev/null +++ b/app/src/main/res/xml/kbd_arabic.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_armenian_phonetic.xml b/app/src/main/res/xml/kbd_armenian_phonetic.xml new file mode 100644 index 00000000..06b9c9ef --- /dev/null +++ b/app/src/main/res/xml/kbd_armenian_phonetic.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_azerty.xml b/app/src/main/res/xml/kbd_azerty.xml new file mode 100644 index 00000000..643ee7ee --- /dev/null +++ b/app/src/main/res/xml/kbd_azerty.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_bengali.xml b/app/src/main/res/xml/kbd_bengali.xml new file mode 100644 index 00000000..dd2ee9f6 --- /dev/null +++ b/app/src/main/res/xml/kbd_bengali.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_bengali_akkhor.xml b/app/src/main/res/xml/kbd_bengali_akkhor.xml new file mode 100644 index 00000000..1b8cdc09 --- /dev/null +++ b/app/src/main/res/xml/kbd_bengali_akkhor.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_bepo.xml b/app/src/main/res/xml/kbd_bepo.xml new file mode 100644 index 00000000..3e102925 --- /dev/null +++ b/app/src/main/res/xml/kbd_bepo.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/xml/kbd_bulgarian.xml b/app/src/main/res/xml/kbd_bulgarian.xml new file mode 100644 index 00000000..79210a46 --- /dev/null +++ b/app/src/main/res/xml/kbd_bulgarian.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_bulgarian_bds.xml b/app/src/main/res/xml/kbd_bulgarian_bds.xml new file mode 100644 index 00000000..ad832df5 --- /dev/null +++ b/app/src/main/res/xml/kbd_bulgarian_bds.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_colemak.xml b/app/src/main/res/xml/kbd_colemak.xml new file mode 100644 index 00000000..77156d49 --- /dev/null +++ b/app/src/main/res/xml/kbd_colemak.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_dvorak.xml b/app/src/main/res/xml/kbd_dvorak.xml new file mode 100644 index 00000000..d31dc634 --- /dev/null +++ b/app/src/main/res/xml/kbd_dvorak.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_east_slavic.xml b/app/src/main/res/xml/kbd_east_slavic.xml new file mode 100644 index 00000000..53e3002a --- /dev/null +++ b/app/src/main/res/xml/kbd_east_slavic.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_emoji_category1.xml b/app/src/main/res/xml/kbd_emoji_category1.xml new file mode 100644 index 00000000..3dba4970 --- /dev/null +++ b/app/src/main/res/xml/kbd_emoji_category1.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_emoji_category10.xml b/app/src/main/res/xml/kbd_emoji_category10.xml new file mode 100644 index 00000000..51fb78f3 --- /dev/null +++ b/app/src/main/res/xml/kbd_emoji_category10.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_emoji_category11.xml b/app/src/main/res/xml/kbd_emoji_category11.xml new file mode 100644 index 00000000..abf9771d --- /dev/null +++ b/app/src/main/res/xml/kbd_emoji_category11.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_emoji_category12.xml b/app/src/main/res/xml/kbd_emoji_category12.xml new file mode 100644 index 00000000..b73b0e10 --- /dev/null +++ b/app/src/main/res/xml/kbd_emoji_category12.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_emoji_category13.xml b/app/src/main/res/xml/kbd_emoji_category13.xml new file mode 100644 index 00000000..10330923 --- /dev/null +++ b/app/src/main/res/xml/kbd_emoji_category13.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_emoji_category14.xml b/app/src/main/res/xml/kbd_emoji_category14.xml new file mode 100644 index 00000000..97826f9b --- /dev/null +++ b/app/src/main/res/xml/kbd_emoji_category14.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_emoji_category15.xml b/app/src/main/res/xml/kbd_emoji_category15.xml new file mode 100644 index 00000000..fba57aa3 --- /dev/null +++ b/app/src/main/res/xml/kbd_emoji_category15.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_emoji_category16.xml b/app/src/main/res/xml/kbd_emoji_category16.xml new file mode 100644 index 00000000..f02e66f6 --- /dev/null +++ b/app/src/main/res/xml/kbd_emoji_category16.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_emoji_category2.xml b/app/src/main/res/xml/kbd_emoji_category2.xml new file mode 100644 index 00000000..67dd00be --- /dev/null +++ b/app/src/main/res/xml/kbd_emoji_category2.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_emoji_category3.xml b/app/src/main/res/xml/kbd_emoji_category3.xml new file mode 100644 index 00000000..e461b46c --- /dev/null +++ b/app/src/main/res/xml/kbd_emoji_category3.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_emoji_category4.xml b/app/src/main/res/xml/kbd_emoji_category4.xml new file mode 100644 index 00000000..84202508 --- /dev/null +++ b/app/src/main/res/xml/kbd_emoji_category4.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_emoji_category5.xml b/app/src/main/res/xml/kbd_emoji_category5.xml new file mode 100644 index 00000000..bf34b100 --- /dev/null +++ b/app/src/main/res/xml/kbd_emoji_category5.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_emoji_category6.xml b/app/src/main/res/xml/kbd_emoji_category6.xml new file mode 100644 index 00000000..b9b21f6b --- /dev/null +++ b/app/src/main/res/xml/kbd_emoji_category6.xml @@ -0,0 +1,32 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_emoji_category7.xml b/app/src/main/res/xml/kbd_emoji_category7.xml new file mode 100644 index 00000000..f25aaad5 --- /dev/null +++ b/app/src/main/res/xml/kbd_emoji_category7.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_emoji_category8.xml b/app/src/main/res/xml/kbd_emoji_category8.xml new file mode 100644 index 00000000..c8ca9d05 --- /dev/null +++ b/app/src/main/res/xml/kbd_emoji_category8.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_emoji_category9.xml b/app/src/main/res/xml/kbd_emoji_category9.xml new file mode 100644 index 00000000..af8ef153 --- /dev/null +++ b/app/src/main/res/xml/kbd_emoji_category9.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_emoji_recents.xml b/app/src/main/res/xml/kbd_emoji_recents.xml new file mode 100644 index 00000000..a9b1d7a2 --- /dev/null +++ b/app/src/main/res/xml/kbd_emoji_recents.xml @@ -0,0 +1,32 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_farsi.xml b/app/src/main/res/xml/kbd_farsi.xml new file mode 100644 index 00000000..137f70d5 --- /dev/null +++ b/app/src/main/res/xml/kbd_farsi.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_georgian.xml b/app/src/main/res/xml/kbd_georgian.xml new file mode 100644 index 00000000..0db8ac9d --- /dev/null +++ b/app/src/main/res/xml/kbd_georgian.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_greek.xml b/app/src/main/res/xml/kbd_greek.xml new file mode 100644 index 00000000..3d7f7304 --- /dev/null +++ b/app/src/main/res/xml/kbd_greek.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_hebrew.xml b/app/src/main/res/xml/kbd_hebrew.xml new file mode 100644 index 00000000..cc255246 --- /dev/null +++ b/app/src/main/res/xml/kbd_hebrew.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_hindi.xml b/app/src/main/res/xml/kbd_hindi.xml new file mode 100644 index 00000000..4f0cdd1b --- /dev/null +++ b/app/src/main/res/xml/kbd_hindi.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_hindi_compact.xml b/app/src/main/res/xml/kbd_hindi_compact.xml new file mode 100644 index 00000000..16e5b62e --- /dev/null +++ b/app/src/main/res/xml/kbd_hindi_compact.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_kannada.xml b/app/src/main/res/xml/kbd_kannada.xml new file mode 100644 index 00000000..61c15f1b --- /dev/null +++ b/app/src/main/res/xml/kbd_kannada.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_khmer.xml b/app/src/main/res/xml/kbd_khmer.xml new file mode 100644 index 00000000..c92a4c89 --- /dev/null +++ b/app/src/main/res/xml/kbd_khmer.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_lao.xml b/app/src/main/res/xml/kbd_lao.xml new file mode 100644 index 00000000..2c3a8f63 --- /dev/null +++ b/app/src/main/res/xml/kbd_lao.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_malayalam.xml b/app/src/main/res/xml/kbd_malayalam.xml new file mode 100644 index 00000000..3f06efbb --- /dev/null +++ b/app/src/main/res/xml/kbd_malayalam.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_marathi.xml b/app/src/main/res/xml/kbd_marathi.xml new file mode 100644 index 00000000..b7aacc51 --- /dev/null +++ b/app/src/main/res/xml/kbd_marathi.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_mongolian.xml b/app/src/main/res/xml/kbd_mongolian.xml new file mode 100644 index 00000000..4abf4d74 --- /dev/null +++ b/app/src/main/res/xml/kbd_mongolian.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_more_keys_keyboard_template.xml b/app/src/main/res/xml/kbd_more_keys_keyboard_template.xml new file mode 100644 index 00000000..099562d0 --- /dev/null +++ b/app/src/main/res/xml/kbd_more_keys_keyboard_template.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/app/src/main/res/xml/kbd_nepali_romanized.xml b/app/src/main/res/xml/kbd_nepali_romanized.xml new file mode 100644 index 00000000..373c19f2 --- /dev/null +++ b/app/src/main/res/xml/kbd_nepali_romanized.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_nepali_traditional.xml b/app/src/main/res/xml/kbd_nepali_traditional.xml new file mode 100644 index 00000000..3b13f7a9 --- /dev/null +++ b/app/src/main/res/xml/kbd_nepali_traditional.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_nordic.xml b/app/src/main/res/xml/kbd_nordic.xml new file mode 100644 index 00000000..10b5dd7e --- /dev/null +++ b/app/src/main/res/xml/kbd_nordic.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_number.xml b/app/src/main/res/xml/kbd_number.xml new file mode 100644 index 00000000..0677ffe6 --- /dev/null +++ b/app/src/main/res/xml/kbd_number.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_pcqwerty.xml b/app/src/main/res/xml/kbd_pcqwerty.xml new file mode 100644 index 00000000..76f5bdf2 --- /dev/null +++ b/app/src/main/res/xml/kbd_pcqwerty.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_phone.xml b/app/src/main/res/xml/kbd_phone.xml new file mode 100644 index 00000000..084b09fe --- /dev/null +++ b/app/src/main/res/xml/kbd_phone.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_phone_symbols.xml b/app/src/main/res/xml/kbd_phone_symbols.xml new file mode 100644 index 00000000..f7658d62 --- /dev/null +++ b/app/src/main/res/xml/kbd_phone_symbols.xml @@ -0,0 +1,28 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_qwerty.xml b/app/src/main/res/xml/kbd_qwerty.xml new file mode 100644 index 00000000..432a2fcf --- /dev/null +++ b/app/src/main/res/xml/kbd_qwerty.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_qwertz.xml b/app/src/main/res/xml/kbd_qwertz.xml new file mode 100644 index 00000000..1c0c8b3f --- /dev/null +++ b/app/src/main/res/xml/kbd_qwertz.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_serbian_qwertz.xml b/app/src/main/res/xml/kbd_serbian_qwertz.xml new file mode 100644 index 00000000..934fad47 --- /dev/null +++ b/app/src/main/res/xml/kbd_serbian_qwertz.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/kbd_sinhala.xml b/app/src/main/res/xml/kbd_sinhala.xml new file mode 100644 index 00000000..986cbc59 --- /dev/null +++ b/app/src/main/res/xml/kbd_sinhala.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_south_slavic.xml b/app/src/main/res/xml/kbd_south_slavic.xml new file mode 100644 index 00000000..f1c6d583 --- /dev/null +++ b/app/src/main/res/xml/kbd_south_slavic.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_spanish.xml b/app/src/main/res/xml/kbd_spanish.xml new file mode 100644 index 00000000..36540e8f --- /dev/null +++ b/app/src/main/res/xml/kbd_spanish.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_suggestions_pane_template.xml b/app/src/main/res/xml/kbd_suggestions_pane_template.xml new file mode 100644 index 00000000..2ad64e49 --- /dev/null +++ b/app/src/main/res/xml/kbd_suggestions_pane_template.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/xml/kbd_swiss.xml b/app/src/main/res/xml/kbd_swiss.xml new file mode 100644 index 00000000..9c78a7c1 --- /dev/null +++ b/app/src/main/res/xml/kbd_swiss.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_symbols.xml b/app/src/main/res/xml/kbd_symbols.xml new file mode 100644 index 00000000..09015443 --- /dev/null +++ b/app/src/main/res/xml/kbd_symbols.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_symbols_shift.xml b/app/src/main/res/xml/kbd_symbols_shift.xml new file mode 100644 index 00000000..62c7f6fa --- /dev/null +++ b/app/src/main/res/xml/kbd_symbols_shift.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_tamil.xml b/app/src/main/res/xml/kbd_tamil.xml new file mode 100644 index 00000000..216b6ffe --- /dev/null +++ b/app/src/main/res/xml/kbd_tamil.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_telugu.xml b/app/src/main/res/xml/kbd_telugu.xml new file mode 100644 index 00000000..baa24c38 --- /dev/null +++ b/app/src/main/res/xml/kbd_telugu.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_thai.xml b/app/src/main/res/xml/kbd_thai.xml new file mode 100644 index 00000000..3fb3483b --- /dev/null +++ b/app/src/main/res/xml/kbd_thai.xml @@ -0,0 +1,31 @@ + + + + + + diff --git a/app/src/main/res/xml/kbd_uzbek.xml b/app/src/main/res/xml/kbd_uzbek.xml new file mode 100644 index 00000000..a2c524d5 --- /dev/null +++ b/app/src/main/res/xml/kbd_uzbek.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/key_armenian_sha.xml b/app/src/main/res/xml/key_armenian_sha.xml new file mode 100644 index 00000000..b5db9d10 --- /dev/null +++ b/app/src/main/res/xml/key_armenian_sha.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/app/src/main/res/xml/key_armenian_xeh.xml b/app/src/main/res/xml/key_armenian_xeh.xml new file mode 100644 index 00000000..6b94b732 --- /dev/null +++ b/app/src/main/res/xml/key_armenian_xeh.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/app/src/main/res/xml/key_comma.xml b/app/src/main/res/xml/key_comma.xml new file mode 100644 index 00000000..2c2f2c17 --- /dev/null +++ b/app/src/main/res/xml/key_comma.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/key_emoji.xml b/app/src/main/res/xml/key_emoji.xml new file mode 100644 index 00000000..ab40252a --- /dev/null +++ b/app/src/main/res/xml/key_emoji.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/key_period.xml b/app/src/main/res/xml/key_period.xml new file mode 100644 index 00000000..2e009f6c --- /dev/null +++ b/app/src/main/res/xml/key_period.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/key_space_5kw.xml b/app/src/main/res/xml/key_space_5kw.xml new file mode 100644 index 00000000..4b343b5e --- /dev/null +++ b/app/src/main/res/xml/key_space_5kw.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/key_space_symbols.xml b/app/src/main/res/xml/key_space_symbols.xml new file mode 100644 index 00000000..51c7d689 --- /dev/null +++ b/app/src/main/res/xml/key_space_symbols.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/app/src/main/res/xml/key_styles_actions.xml b/app/src/main/res/xml/key_styles_actions.xml new file mode 100644 index 00000000..e09f1b13 --- /dev/null +++ b/app/src/main/res/xml/key_styles_actions.xml @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/key_styles_common.xml b/app/src/main/res/xml/key_styles_common.xml new file mode 100644 index 00000000..fd69faef --- /dev/null +++ b/app/src/main/res/xml/key_styles_common.xml @@ -0,0 +1,168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/key_styles_currency.xml b/app/src/main/res/xml/key_styles_currency.xml new file mode 100644 index 00000000..d1ea165c --- /dev/null +++ b/app/src/main/res/xml/key_styles_currency.xml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/key_styles_currency_dollar.xml b/app/src/main/res/xml/key_styles_currency_dollar.xml new file mode 100644 index 00000000..ff20025c --- /dev/null +++ b/app/src/main/res/xml/key_styles_currency_dollar.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/key_styles_currency_euro.xml b/app/src/main/res/xml/key_styles_currency_euro.xml new file mode 100644 index 00000000..c8653a16 --- /dev/null +++ b/app/src/main/res/xml/key_styles_currency_euro.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/key_styles_currency_generic.xml b/app/src/main/res/xml/key_styles_currency_generic.xml new file mode 100644 index 00000000..618da09f --- /dev/null +++ b/app/src/main/res/xml/key_styles_currency_generic.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/key_styles_enter.xml b/app/src/main/res/xml/key_styles_enter.xml new file mode 100644 index 00000000..d348bdc3 --- /dev/null +++ b/app/src/main/res/xml/key_styles_enter.xml @@ -0,0 +1,313 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/key_styles_less_greater.xml b/app/src/main/res/xml/key_styles_less_greater.xml new file mode 100644 index 00000000..36078a97 --- /dev/null +++ b/app/src/main/res/xml/key_styles_less_greater.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/key_styles_navigate_more_keys.xml b/app/src/main/res/xml/key_styles_navigate_more_keys.xml new file mode 100644 index 00000000..b40f80a5 --- /dev/null +++ b/app/src/main/res/xml/key_styles_navigate_more_keys.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/key_styles_number.xml b/app/src/main/res/xml/key_styles_number.xml new file mode 100644 index 00000000..cd223a09 --- /dev/null +++ b/app/src/main/res/xml/key_styles_number.xml @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/key_styles_settings.xml b/app/src/main/res/xml/key_styles_settings.xml new file mode 100644 index 00000000..0e1ca429 --- /dev/null +++ b/app/src/main/res/xml/key_styles_settings.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/key_thai_kho_khuat.xml b/app/src/main/res/xml/key_thai_kho_khuat.xml new file mode 100644 index 00000000..15264031 --- /dev/null +++ b/app/src/main/res/xml/key_thai_kho_khuat.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_arabic.xml b/app/src/main/res/xml/keyboard_layout_set_arabic.xml new file mode 100644 index 00000000..1e0e6aab --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_arabic.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_armenian_phonetic.xml b/app/src/main/res/xml/keyboard_layout_set_armenian_phonetic.xml new file mode 100644 index 00000000..4ae7466d --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_armenian_phonetic.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_azerty.xml b/app/src/main/res/xml/keyboard_layout_set_azerty.xml new file mode 100644 index 00000000..2ce6fb29 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_azerty.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_bengali.xml b/app/src/main/res/xml/keyboard_layout_set_bengali.xml new file mode 100644 index 00000000..af890d1e --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_bengali.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_bengali_akkhor.xml b/app/src/main/res/xml/keyboard_layout_set_bengali_akkhor.xml new file mode 100644 index 00000000..15f20e90 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_bengali_akkhor.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_bepo.xml b/app/src/main/res/xml/keyboard_layout_set_bepo.xml new file mode 100644 index 00000000..58a5f1a0 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_bepo.xml @@ -0,0 +1,37 @@ + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_bulgarian.xml b/app/src/main/res/xml/keyboard_layout_set_bulgarian.xml new file mode 100644 index 00000000..296b8f98 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_bulgarian.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_bulgarian_bds.xml b/app/src/main/res/xml/keyboard_layout_set_bulgarian_bds.xml new file mode 100644 index 00000000..3219c0bf --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_bulgarian_bds.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_colemak.xml b/app/src/main/res/xml/keyboard_layout_set_colemak.xml new file mode 100644 index 00000000..d2232ebf --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_colemak.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_dvorak.xml b/app/src/main/res/xml/keyboard_layout_set_dvorak.xml new file mode 100644 index 00000000..a5295ee5 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_dvorak.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_east_slavic.xml b/app/src/main/res/xml/keyboard_layout_set_east_slavic.xml new file mode 100644 index 00000000..7937596e --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_east_slavic.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_emoji.xml b/app/src/main/res/xml/keyboard_layout_set_emoji.xml new file mode 100644 index 00000000..fc4f8916 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_emoji.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_farsi.xml b/app/src/main/res/xml/keyboard_layout_set_farsi.xml new file mode 100644 index 00000000..d0fbaa9f --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_farsi.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_georgian.xml b/app/src/main/res/xml/keyboard_layout_set_georgian.xml new file mode 100644 index 00000000..3418ee16 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_georgian.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_greek.xml b/app/src/main/res/xml/keyboard_layout_set_greek.xml new file mode 100644 index 00000000..f33f8326 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_greek.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_hebrew.xml b/app/src/main/res/xml/keyboard_layout_set_hebrew.xml new file mode 100644 index 00000000..364ccaec --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_hebrew.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_hindi.xml b/app/src/main/res/xml/keyboard_layout_set_hindi.xml new file mode 100644 index 00000000..3703134c --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_hindi.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_hindi_compact.xml b/app/src/main/res/xml/keyboard_layout_set_hindi_compact.xml new file mode 100644 index 00000000..014ae210 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_hindi_compact.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_kannada.xml b/app/src/main/res/xml/keyboard_layout_set_kannada.xml new file mode 100644 index 00000000..a44c1775 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_kannada.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_khmer.xml b/app/src/main/res/xml/keyboard_layout_set_khmer.xml new file mode 100644 index 00000000..86827693 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_khmer.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_lao.xml b/app/src/main/res/xml/keyboard_layout_set_lao.xml new file mode 100644 index 00000000..fd7e9a9f --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_lao.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_malayalam.xml b/app/src/main/res/xml/keyboard_layout_set_malayalam.xml new file mode 100644 index 00000000..249d7010 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_malayalam.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_marathi.xml b/app/src/main/res/xml/keyboard_layout_set_marathi.xml new file mode 100644 index 00000000..6d230393 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_marathi.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_mongolian.xml b/app/src/main/res/xml/keyboard_layout_set_mongolian.xml new file mode 100644 index 00000000..ef5b3862 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_mongolian.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_nepali_romanized.xml b/app/src/main/res/xml/keyboard_layout_set_nepali_romanized.xml new file mode 100644 index 00000000..cc9e3547 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_nepali_romanized.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_nepali_traditional.xml b/app/src/main/res/xml/keyboard_layout_set_nepali_traditional.xml new file mode 100644 index 00000000..31f30923 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_nepali_traditional.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_nordic.xml b/app/src/main/res/xml/keyboard_layout_set_nordic.xml new file mode 100644 index 00000000..48127ab5 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_nordic.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_pcqwerty.xml b/app/src/main/res/xml/keyboard_layout_set_pcqwerty.xml new file mode 100644 index 00000000..2e22abc3 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_pcqwerty.xml @@ -0,0 +1,36 @@ + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_qwerty.xml b/app/src/main/res/xml/keyboard_layout_set_qwerty.xml new file mode 100644 index 00000000..11f3061f --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_qwerty.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_qwertz.xml b/app/src/main/res/xml/keyboard_layout_set_qwertz.xml new file mode 100644 index 00000000..ce0b37ac --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_qwertz.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_serbian_qwertz.xml b/app/src/main/res/xml/keyboard_layout_set_serbian_qwertz.xml new file mode 100644 index 00000000..da5a97fb --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_serbian_qwertz.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/keyboard_layout_set_sinhala.xml b/app/src/main/res/xml/keyboard_layout_set_sinhala.xml new file mode 100644 index 00000000..45d3b1ca --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_sinhala.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_south_slavic.xml b/app/src/main/res/xml/keyboard_layout_set_south_slavic.xml new file mode 100644 index 00000000..bbc4e58a --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_south_slavic.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_spanish.xml b/app/src/main/res/xml/keyboard_layout_set_spanish.xml new file mode 100644 index 00000000..40ac99c2 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_spanish.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_swiss.xml b/app/src/main/res/xml/keyboard_layout_set_swiss.xml new file mode 100644 index 00000000..65ea9d3e --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_swiss.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_tamil.xml b/app/src/main/res/xml/keyboard_layout_set_tamil.xml new file mode 100644 index 00000000..bbd1884e --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_tamil.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_telugu.xml b/app/src/main/res/xml/keyboard_layout_set_telugu.xml new file mode 100644 index 00000000..859b3352 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_telugu.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_thai.xml b/app/src/main/res/xml/keyboard_layout_set_thai.xml new file mode 100644 index 00000000..b653a316 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_thai.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keyboard_layout_set_uzbek.xml b/app/src/main/res/xml/keyboard_layout_set_uzbek.xml new file mode 100644 index 00000000..cd5d04b5 --- /dev/null +++ b/app/src/main/res/xml/keyboard_layout_set_uzbek.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/keys_arabic3_left.xml b/app/src/main/res/xml/keys_arabic3_left.xml new file mode 100644 index 00000000..d892d5eb --- /dev/null +++ b/app/src/main/res/xml/keys_arabic3_left.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/app/src/main/res/xml/keys_curly_brackets.xml b/app/src/main/res/xml/keys_curly_brackets.xml new file mode 100644 index 00000000..df55139f --- /dev/null +++ b/app/src/main/res/xml/keys_curly_brackets.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/app/src/main/res/xml/keys_dvorak_123.xml b/app/src/main/res/xml/keys_dvorak_123.xml new file mode 100644 index 00000000..4223a41c --- /dev/null +++ b/app/src/main/res/xml/keys_dvorak_123.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keys_farsi3_right.xml b/app/src/main/res/xml/keys_farsi3_right.xml new file mode 100644 index 00000000..bfc8cf56 --- /dev/null +++ b/app/src/main/res/xml/keys_farsi3_right.xml @@ -0,0 +1,27 @@ + + + + + + + diff --git a/app/src/main/res/xml/keys_parentheses.xml b/app/src/main/res/xml/keys_parentheses.xml new file mode 100644 index 00000000..ed2848cd --- /dev/null +++ b/app/src/main/res/xml/keys_parentheses.xml @@ -0,0 +1,30 @@ + + + + + + + diff --git a/app/src/main/res/xml/keys_pcqwerty2_right3.xml b/app/src/main/res/xml/keys_pcqwerty2_right3.xml new file mode 100644 index 00000000..94cb97bb --- /dev/null +++ b/app/src/main/res/xml/keys_pcqwerty2_right3.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keys_pcqwerty3_right2.xml b/app/src/main/res/xml/keys_pcqwerty3_right2.xml new file mode 100644 index 00000000..da2cbf7e --- /dev/null +++ b/app/src/main/res/xml/keys_pcqwerty3_right2.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keys_pcqwerty4_right3.xml b/app/src/main/res/xml/keys_pcqwerty4_right3.xml new file mode 100644 index 00000000..b5284a4c --- /dev/null +++ b/app/src/main/res/xml/keys_pcqwerty4_right3.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keys_square_brackets.xml b/app/src/main/res/xml/keys_square_brackets.xml new file mode 100644 index 00000000..92fa821d --- /dev/null +++ b/app/src/main/res/xml/keys_square_brackets.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/app/src/main/res/xml/keystyle_devanagari_sign_anusvara.xml b/app/src/main/res/xml/keystyle_devanagari_sign_anusvara.xml new file mode 100644 index 00000000..60021b32 --- /dev/null +++ b/app/src/main/res/xml/keystyle_devanagari_sign_anusvara.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keystyle_devanagari_sign_candrabindu.xml b/app/src/main/res/xml/keystyle_devanagari_sign_candrabindu.xml new file mode 100644 index 00000000..4b3f955d --- /dev/null +++ b/app/src/main/res/xml/keystyle_devanagari_sign_candrabindu.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keystyle_devanagari_sign_nukta.xml b/app/src/main/res/xml/keystyle_devanagari_sign_nukta.xml new file mode 100644 index 00000000..9277d1de --- /dev/null +++ b/app/src/main/res/xml/keystyle_devanagari_sign_nukta.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keystyle_devanagari_sign_virama.xml b/app/src/main/res/xml/keystyle_devanagari_sign_virama.xml new file mode 100644 index 00000000..86160f0f --- /dev/null +++ b/app/src/main/res/xml/keystyle_devanagari_sign_virama.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keystyle_devanagari_sign_visarga.xml b/app/src/main/res/xml/keystyle_devanagari_sign_visarga.xml new file mode 100644 index 00000000..68d1c6b2 --- /dev/null +++ b/app/src/main/res/xml/keystyle_devanagari_sign_visarga.xml @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/app/src/main/res/xml/keystyle_devanagari_vowel_sign_aa.xml b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_aa.xml new file mode 100644 index 00000000..b4e1a54e --- /dev/null +++ b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_aa.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keystyle_devanagari_vowel_sign_ai.xml b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_ai.xml new file mode 100644 index 00000000..335f640a --- /dev/null +++ b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_ai.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keystyle_devanagari_vowel_sign_au.xml b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_au.xml new file mode 100644 index 00000000..89372e2d --- /dev/null +++ b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_au.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keystyle_devanagari_vowel_sign_candra_e.xml b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_candra_e.xml new file mode 100644 index 00000000..84d8b224 --- /dev/null +++ b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_candra_e.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keystyle_devanagari_vowel_sign_candra_o.xml b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_candra_o.xml new file mode 100644 index 00000000..944ffa68 --- /dev/null +++ b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_candra_o.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keystyle_devanagari_vowel_sign_e.xml b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_e.xml new file mode 100644 index 00000000..59477a41 --- /dev/null +++ b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_e.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keystyle_devanagari_vowel_sign_i.xml b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_i.xml new file mode 100644 index 00000000..262055dc --- /dev/null +++ b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_i.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keystyle_devanagari_vowel_sign_ii.xml b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_ii.xml new file mode 100644 index 00000000..089af4ea --- /dev/null +++ b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_ii.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keystyle_devanagari_vowel_sign_o.xml b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_o.xml new file mode 100644 index 00000000..d9707053 --- /dev/null +++ b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_o.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keystyle_devanagari_vowel_sign_u.xml b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_u.xml new file mode 100644 index 00000000..c48bf3ed --- /dev/null +++ b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_u.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keystyle_devanagari_vowel_sign_uu.xml b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_uu.xml new file mode 100644 index 00000000..63e07d02 --- /dev/null +++ b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_uu.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/keystyle_devanagari_vowel_sign_vocalic_r.xml b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_vocalic_r.xml new file mode 100644 index 00000000..73b3ef93 --- /dev/null +++ b/app/src/main/res/xml/keystyle_devanagari_vowel_sign_vocalic_r.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/method.xml b/app/src/main/res/xml/method.xml new file mode 100644 index 00000000..4c565549 --- /dev/null +++ b/app/src/main/res/xml/method.xml @@ -0,0 +1,835 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/prefs.xml b/app/src/main/res/xml/prefs.xml new file mode 100644 index 00000000..429e2342 --- /dev/null +++ b/app/src/main/res/xml/prefs.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/prefs_screen_accounts.xml b/app/src/main/res/xml/prefs_screen_accounts.xml new file mode 100644 index 00000000..b00393c7 --- /dev/null +++ b/app/src/main/res/xml/prefs_screen_accounts.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/prefs_screen_advanced.xml b/app/src/main/res/xml/prefs_screen_advanced.xml new file mode 100644 index 00000000..326f0b12 --- /dev/null +++ b/app/src/main/res/xml/prefs_screen_advanced.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/prefs_screen_appearance.xml b/app/src/main/res/xml/prefs_screen_appearance.xml new file mode 100644 index 00000000..4372c8b3 --- /dev/null +++ b/app/src/main/res/xml/prefs_screen_appearance.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/app/src/main/res/xml/prefs_screen_correction.xml b/app/src/main/res/xml/prefs_screen_correction.xml new file mode 100644 index 00000000..15f57432 --- /dev/null +++ b/app/src/main/res/xml/prefs_screen_correction.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/prefs_screen_debug.xml b/app/src/main/res/xml/prefs_screen_debug.xml new file mode 100644 index 00000000..ec6c24b2 --- /dev/null +++ b/app/src/main/res/xml/prefs_screen_debug.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/prefs_screen_gesture.xml b/app/src/main/res/xml/prefs_screen_gesture.xml new file mode 100644 index 00000000..38c7eda8 --- /dev/null +++ b/app/src/main/res/xml/prefs_screen_gesture.xml @@ -0,0 +1,40 @@ + + + + + + + + diff --git a/app/src/main/res/xml/prefs_screen_preferences.xml b/app/src/main/res/xml/prefs_screen_preferences.xml new file mode 100644 index 00000000..13085c11 --- /dev/null +++ b/app/src/main/res/xml/prefs_screen_preferences.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/prefs_screen_theme.xml b/app/src/main/res/xml/prefs_screen_theme.xml new file mode 100644 index 00000000..cbdceff4 --- /dev/null +++ b/app/src/main/res/xml/prefs_screen_theme.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/app/src/main/res/xml/row_pcqwerty5.xml b/app/src/main/res/xml/row_pcqwerty5.xml new file mode 100644 index 00000000..fc8b4613 --- /dev/null +++ b/app/src/main/res/xml/row_pcqwerty5.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/row_qwerty4.xml b/app/src/main/res/xml/row_qwerty4.xml new file mode 100644 index 00000000..496fc848 --- /dev/null +++ b/app/src/main/res/xml/row_qwerty4.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/row_symbols4.xml b/app/src/main/res/xml/row_symbols4.xml new file mode 100644 index 00000000..b1d75a75 --- /dev/null +++ b/app/src/main/res/xml/row_symbols4.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/row_symbols_shift4.xml b/app/src/main/res/xml/row_symbols_shift4.xml new file mode 100644 index 00000000..40e108be --- /dev/null +++ b/app/src/main/res/xml/row_symbols_shift4.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_arabic1.xml b/app/src/main/res/xml/rowkeys_arabic1.xml new file mode 100644 index 00000000..ed5a5470 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_arabic1.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_arabic2.xml b/app/src/main/res/xml/rowkeys_arabic2.xml new file mode 100644 index 00000000..132c79f4 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_arabic2.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_arabic3.xml b/app/src/main/res/xml/rowkeys_arabic3.xml new file mode 100644 index 00000000..f390e7a5 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_arabic3.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_armenian_phonetic1.xml b/app/src/main/res/xml/rowkeys_armenian_phonetic1.xml new file mode 100644 index 00000000..81de8e31 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_armenian_phonetic1.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_armenian_phonetic2.xml b/app/src/main/res/xml/rowkeys_armenian_phonetic2.xml new file mode 100644 index 00000000..312217c9 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_armenian_phonetic2.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_armenian_phonetic3.xml b/app/src/main/res/xml/rowkeys_armenian_phonetic3.xml new file mode 100644 index 00000000..1d91c6cb --- /dev/null +++ b/app/src/main/res/xml/rowkeys_armenian_phonetic3.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_armenian_phonetic4.xml b/app/src/main/res/xml/rowkeys_armenian_phonetic4.xml new file mode 100644 index 00000000..95a9bb96 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_armenian_phonetic4.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_azerty1.xml b/app/src/main/res/xml/rowkeys_azerty1.xml new file mode 100644 index 00000000..51a94d3f --- /dev/null +++ b/app/src/main/res/xml/rowkeys_azerty1.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_azerty2.xml b/app/src/main/res/xml/rowkeys_azerty2.xml new file mode 100644 index 00000000..6b2baba8 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_azerty2.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_azerty3.xml b/app/src/main/res/xml/rowkeys_azerty3.xml new file mode 100644 index 00000000..f3717ff9 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_azerty3.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_bengali1.xml b/app/src/main/res/xml/rowkeys_bengali1.xml new file mode 100644 index 00000000..95a3ea36 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_bengali1.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_bengali2.xml b/app/src/main/res/xml/rowkeys_bengali2.xml new file mode 100644 index 00000000..eb76150a --- /dev/null +++ b/app/src/main/res/xml/rowkeys_bengali2.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_bengali3.xml b/app/src/main/res/xml/rowkeys_bengali3.xml new file mode 100644 index 00000000..f17e157d --- /dev/null +++ b/app/src/main/res/xml/rowkeys_bengali3.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_bengali_akkhor1.xml b/app/src/main/res/xml/rowkeys_bengali_akkhor1.xml new file mode 100644 index 00000000..55670af1 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_bengali_akkhor1.xml @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_bengali_akkhor2.xml b/app/src/main/res/xml/rowkeys_bengali_akkhor2.xml new file mode 100644 index 00000000..516cc515 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_bengali_akkhor2.xml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_bengali_akkhor3.xml b/app/src/main/res/xml/rowkeys_bengali_akkhor3.xml new file mode 100644 index 00000000..f3e7aad9 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_bengali_akkhor3.xml @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_bepo1.xml b/app/src/main/res/xml/rowkeys_bepo1.xml new file mode 100644 index 00000000..bb46dd68 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_bepo1.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_bepo2.xml b/app/src/main/res/xml/rowkeys_bepo2.xml new file mode 100644 index 00000000..af587683 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_bepo2.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_bepo3.xml b/app/src/main/res/xml/rowkeys_bepo3.xml new file mode 100644 index 00000000..92941c9d --- /dev/null +++ b/app/src/main/res/xml/rowkeys_bepo3.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_bulgarian1.xml b/app/src/main/res/xml/rowkeys_bulgarian1.xml new file mode 100644 index 00000000..52a0f96f --- /dev/null +++ b/app/src/main/res/xml/rowkeys_bulgarian1.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_bulgarian2.xml b/app/src/main/res/xml/rowkeys_bulgarian2.xml new file mode 100644 index 00000000..20bfdbda --- /dev/null +++ b/app/src/main/res/xml/rowkeys_bulgarian2.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_bulgarian3.xml b/app/src/main/res/xml/rowkeys_bulgarian3.xml new file mode 100644 index 00000000..d74a1fd4 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_bulgarian3.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_bulgarian_bds1.xml b/app/src/main/res/xml/rowkeys_bulgarian_bds1.xml new file mode 100644 index 00000000..c889804c --- /dev/null +++ b/app/src/main/res/xml/rowkeys_bulgarian_bds1.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_bulgarian_bds2.xml b/app/src/main/res/xml/rowkeys_bulgarian_bds2.xml new file mode 100644 index 00000000..f7393dd8 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_bulgarian_bds2.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_bulgarian_bds3.xml b/app/src/main/res/xml/rowkeys_bulgarian_bds3.xml new file mode 100644 index 00000000..3c07b52f --- /dev/null +++ b/app/src/main/res/xml/rowkeys_bulgarian_bds3.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_colemak1.xml b/app/src/main/res/xml/rowkeys_colemak1.xml new file mode 100644 index 00000000..b4d33bf6 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_colemak1.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_colemak2.xml b/app/src/main/res/xml/rowkeys_colemak2.xml new file mode 100644 index 00000000..03c7c493 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_colemak2.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_colemak3.xml b/app/src/main/res/xml/rowkeys_colemak3.xml new file mode 100644 index 00000000..bc039f46 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_colemak3.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_dvorak1.xml b/app/src/main/res/xml/rowkeys_dvorak1.xml new file mode 100644 index 00000000..a2022a7c --- /dev/null +++ b/app/src/main/res/xml/rowkeys_dvorak1.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_dvorak2.xml b/app/src/main/res/xml/rowkeys_dvorak2.xml new file mode 100644 index 00000000..d66345b1 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_dvorak2.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_dvorak3.xml b/app/src/main/res/xml/rowkeys_dvorak3.xml new file mode 100644 index 00000000..62266899 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_dvorak3.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_east_slavic1.xml b/app/src/main/res/xml/rowkeys_east_slavic1.xml new file mode 100644 index 00000000..08feff37 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_east_slavic1.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_east_slavic2.xml b/app/src/main/res/xml/rowkeys_east_slavic2.xml new file mode 100644 index 00000000..ec7a8792 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_east_slavic2.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_east_slavic3.xml b/app/src/main/res/xml/rowkeys_east_slavic3.xml new file mode 100644 index 00000000..ba4b5a98 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_east_slavic3.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_farsi1.xml b/app/src/main/res/xml/rowkeys_farsi1.xml new file mode 100644 index 00000000..86303dab --- /dev/null +++ b/app/src/main/res/xml/rowkeys_farsi1.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_farsi2.xml b/app/src/main/res/xml/rowkeys_farsi2.xml new file mode 100644 index 00000000..77d5a7f2 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_farsi2.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_farsi3.xml b/app/src/main/res/xml/rowkeys_farsi3.xml new file mode 100644 index 00000000..e84b3766 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_farsi3.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_georgian1.xml b/app/src/main/res/xml/rowkeys_georgian1.xml new file mode 100644 index 00000000..24470946 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_georgian1.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_georgian2.xml b/app/src/main/res/xml/rowkeys_georgian2.xml new file mode 100644 index 00000000..f715b08b --- /dev/null +++ b/app/src/main/res/xml/rowkeys_georgian2.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_georgian3.xml b/app/src/main/res/xml/rowkeys_georgian3.xml new file mode 100644 index 00000000..b3c0fd44 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_georgian3.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_greek1.xml b/app/src/main/res/xml/rowkeys_greek1.xml new file mode 100644 index 00000000..5b598e6d --- /dev/null +++ b/app/src/main/res/xml/rowkeys_greek1.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_greek2.xml b/app/src/main/res/xml/rowkeys_greek2.xml new file mode 100644 index 00000000..c1727a21 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_greek2.xml @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_greek3.xml b/app/src/main/res/xml/rowkeys_greek3.xml new file mode 100644 index 00000000..645c1cb3 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_greek3.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_hebrew1.xml b/app/src/main/res/xml/rowkeys_hebrew1.xml new file mode 100644 index 00000000..5d92dfac --- /dev/null +++ b/app/src/main/res/xml/rowkeys_hebrew1.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_hebrew2.xml b/app/src/main/res/xml/rowkeys_hebrew2.xml new file mode 100644 index 00000000..d85a33ca --- /dev/null +++ b/app/src/main/res/xml/rowkeys_hebrew2.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_hebrew3.xml b/app/src/main/res/xml/rowkeys_hebrew3.xml new file mode 100644 index 00000000..4238a03e --- /dev/null +++ b/app/src/main/res/xml/rowkeys_hebrew3.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_hindi1.xml b/app/src/main/res/xml/rowkeys_hindi1.xml new file mode 100644 index 00000000..25d0fe6c --- /dev/null +++ b/app/src/main/res/xml/rowkeys_hindi1.xml @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_hindi2.xml b/app/src/main/res/xml/rowkeys_hindi2.xml new file mode 100644 index 00000000..adf9b4b9 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_hindi2.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_hindi3.xml b/app/src/main/res/xml/rowkeys_hindi3.xml new file mode 100644 index 00000000..fcbf2804 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_hindi3.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_hindi_compact1.xml b/app/src/main/res/xml/rowkeys_hindi_compact1.xml new file mode 100644 index 00000000..49cd4b91 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_hindi_compact1.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_hindi_compact2.xml b/app/src/main/res/xml/rowkeys_hindi_compact2.xml new file mode 100644 index 00000000..d3f7416f --- /dev/null +++ b/app/src/main/res/xml/rowkeys_hindi_compact2.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_hindi_compact3.xml b/app/src/main/res/xml/rowkeys_hindi_compact3.xml new file mode 100644 index 00000000..f76fa16b --- /dev/null +++ b/app/src/main/res/xml/rowkeys_hindi_compact3.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_kannada1.xml b/app/src/main/res/xml/rowkeys_kannada1.xml new file mode 100644 index 00000000..763baf53 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_kannada1.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_kannada2.xml b/app/src/main/res/xml/rowkeys_kannada2.xml new file mode 100644 index 00000000..9d295449 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_kannada2.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_kannada3.xml b/app/src/main/res/xml/rowkeys_kannada3.xml new file mode 100644 index 00000000..64e27ceb --- /dev/null +++ b/app/src/main/res/xml/rowkeys_kannada3.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_khmer1.xml b/app/src/main/res/xml/rowkeys_khmer1.xml new file mode 100644 index 00000000..6e8de465 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_khmer1.xml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_khmer2.xml b/app/src/main/res/xml/rowkeys_khmer2.xml new file mode 100644 index 00000000..1745a25f --- /dev/null +++ b/app/src/main/res/xml/rowkeys_khmer2.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_khmer3.xml b/app/src/main/res/xml/rowkeys_khmer3.xml new file mode 100644 index 00000000..ba912ec1 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_khmer3.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_khmer4.xml b/app/src/main/res/xml/rowkeys_khmer4.xml new file mode 100644 index 00000000..7148d425 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_khmer4.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_lao1.xml b/app/src/main/res/xml/rowkeys_lao1.xml new file mode 100644 index 00000000..0a0ccfef --- /dev/null +++ b/app/src/main/res/xml/rowkeys_lao1.xml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_lao2.xml b/app/src/main/res/xml/rowkeys_lao2.xml new file mode 100644 index 00000000..b148a951 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_lao2.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_lao3.xml b/app/src/main/res/xml/rowkeys_lao3.xml new file mode 100644 index 00000000..30886f73 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_lao3.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_lao4.xml b/app/src/main/res/xml/rowkeys_lao4.xml new file mode 100644 index 00000000..d62c481f --- /dev/null +++ b/app/src/main/res/xml/rowkeys_lao4.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_malayalam1.xml b/app/src/main/res/xml/rowkeys_malayalam1.xml new file mode 100644 index 00000000..bc9512bf --- /dev/null +++ b/app/src/main/res/xml/rowkeys_malayalam1.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_malayalam2.xml b/app/src/main/res/xml/rowkeys_malayalam2.xml new file mode 100644 index 00000000..fa9626d3 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_malayalam2.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_malayalam3.xml b/app/src/main/res/xml/rowkeys_malayalam3.xml new file mode 100644 index 00000000..accde8ea --- /dev/null +++ b/app/src/main/res/xml/rowkeys_malayalam3.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_marathi1.xml b/app/src/main/res/xml/rowkeys_marathi1.xml new file mode 100644 index 00000000..1786cf53 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_marathi1.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_marathi2.xml b/app/src/main/res/xml/rowkeys_marathi2.xml new file mode 100644 index 00000000..544732a4 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_marathi2.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_marathi3.xml b/app/src/main/res/xml/rowkeys_marathi3.xml new file mode 100644 index 00000000..0edc30aa --- /dev/null +++ b/app/src/main/res/xml/rowkeys_marathi3.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_mongolian1.xml b/app/src/main/res/xml/rowkeys_mongolian1.xml new file mode 100644 index 00000000..d8c4f302 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_mongolian1.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_mongolian2.xml b/app/src/main/res/xml/rowkeys_mongolian2.xml new file mode 100644 index 00000000..49b627da --- /dev/null +++ b/app/src/main/res/xml/rowkeys_mongolian2.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_mongolian3.xml b/app/src/main/res/xml/rowkeys_mongolian3.xml new file mode 100644 index 00000000..ad74866e --- /dev/null +++ b/app/src/main/res/xml/rowkeys_mongolian3.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_nepali_romanized1.xml b/app/src/main/res/xml/rowkeys_nepali_romanized1.xml new file mode 100644 index 00000000..89f3ece3 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_nepali_romanized1.xml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_nepali_romanized2.xml b/app/src/main/res/xml/rowkeys_nepali_romanized2.xml new file mode 100644 index 00000000..7f4dc685 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_nepali_romanized2.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_nepali_romanized3.xml b/app/src/main/res/xml/rowkeys_nepali_romanized3.xml new file mode 100644 index 00000000..039717c1 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_nepali_romanized3.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_nepali_traditional1.xml b/app/src/main/res/xml/rowkeys_nepali_traditional1.xml new file mode 100644 index 00000000..becc539c --- /dev/null +++ b/app/src/main/res/xml/rowkeys_nepali_traditional1.xml @@ -0,0 +1,156 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_nepali_traditional2.xml b/app/src/main/res/xml/rowkeys_nepali_traditional2.xml new file mode 100644 index 00000000..d50951bb --- /dev/null +++ b/app/src/main/res/xml/rowkeys_nepali_traditional2.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_nepali_traditional3.xml b/app/src/main/res/xml/rowkeys_nepali_traditional3.xml new file mode 100644 index 00000000..f7240e16 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_nepali_traditional3.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_nordic1.xml b/app/src/main/res/xml/rowkeys_nordic1.xml new file mode 100644 index 00000000..954c23ef --- /dev/null +++ b/app/src/main/res/xml/rowkeys_nordic1.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_nordic2.xml b/app/src/main/res/xml/rowkeys_nordic2.xml new file mode 100644 index 00000000..198444b9 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_nordic2.xml @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_pcqwerty1.xml b/app/src/main/res/xml/rowkeys_pcqwerty1.xml new file mode 100644 index 00000000..93737bcf --- /dev/null +++ b/app/src/main/res/xml/rowkeys_pcqwerty1.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_pcqwerty1_shift.xml b/app/src/main/res/xml/rowkeys_pcqwerty1_shift.xml new file mode 100644 index 00000000..f950f0e6 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_pcqwerty1_shift.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_pcqwerty2.xml b/app/src/main/res/xml/rowkeys_pcqwerty2.xml new file mode 100644 index 00000000..15b30de1 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_pcqwerty2.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_pcqwerty3.xml b/app/src/main/res/xml/rowkeys_pcqwerty3.xml new file mode 100644 index 00000000..dadc8a78 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_pcqwerty3.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_pcqwerty4.xml b/app/src/main/res/xml/rowkeys_pcqwerty4.xml new file mode 100644 index 00000000..f6f8a7a8 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_pcqwerty4.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_qwerty1.xml b/app/src/main/res/xml/rowkeys_qwerty1.xml new file mode 100644 index 00000000..fa0a2ba5 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_qwerty1.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_qwerty1_left5.xml b/app/src/main/res/xml/rowkeys_qwerty1_left5.xml new file mode 100644 index 00000000..cb351caf --- /dev/null +++ b/app/src/main/res/xml/rowkeys_qwerty1_left5.xml @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_qwerty1_right5.xml b/app/src/main/res/xml/rowkeys_qwerty1_right5.xml new file mode 100644 index 00000000..25550d81 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_qwerty1_right5.xml @@ -0,0 +1,48 @@ + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_qwerty2.xml b/app/src/main/res/xml/rowkeys_qwerty2.xml new file mode 100644 index 00000000..465bddf6 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_qwerty2.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_qwerty2_left5.xml b/app/src/main/res/xml/rowkeys_qwerty2_left5.xml new file mode 100644 index 00000000..0ac062d4 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_qwerty2_left5.xml @@ -0,0 +1,48 @@ + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_qwerty2_right4.xml b/app/src/main/res/xml/rowkeys_qwerty2_right4.xml new file mode 100644 index 00000000..7ab21bdd --- /dev/null +++ b/app/src/main/res/xml/rowkeys_qwerty2_right4.xml @@ -0,0 +1,44 @@ + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_qwerty3.xml b/app/src/main/res/xml/rowkeys_qwerty3.xml new file mode 100644 index 00000000..bacf70be --- /dev/null +++ b/app/src/main/res/xml/rowkeys_qwerty3.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_qwerty3_left4.xml b/app/src/main/res/xml/rowkeys_qwerty3_left4.xml new file mode 100644 index 00000000..5b007b3a --- /dev/null +++ b/app/src/main/res/xml/rowkeys_qwerty3_left4.xml @@ -0,0 +1,44 @@ + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_qwerty3_right3.xml b/app/src/main/res/xml/rowkeys_qwerty3_right3.xml new file mode 100644 index 00000000..409a01e6 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_qwerty3_right3.xml @@ -0,0 +1,37 @@ + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_qwertz1.xml b/app/src/main/res/xml/rowkeys_qwertz1.xml new file mode 100644 index 00000000..18a48305 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_qwertz1.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_qwertz2.xml b/app/src/main/res/xml/rowkeys_qwertz2.xml new file mode 100644 index 00000000..cfde51a5 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_qwertz2.xml @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_qwertz2_left5.xml b/app/src/main/res/xml/rowkeys_qwertz2_left5.xml new file mode 100644 index 00000000..355d78fb --- /dev/null +++ b/app/src/main/res/xml/rowkeys_qwertz2_left5.xml @@ -0,0 +1,44 @@ + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_qwertz2_right4.xml b/app/src/main/res/xml/rowkeys_qwertz2_right4.xml new file mode 100644 index 00000000..7ab21bdd --- /dev/null +++ b/app/src/main/res/xml/rowkeys_qwertz2_right4.xml @@ -0,0 +1,44 @@ + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_qwertz3.xml b/app/src/main/res/xml/rowkeys_qwertz3.xml new file mode 100644 index 00000000..a1d7bb73 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_qwertz3.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_serbian_qwertz1.xml b/app/src/main/res/xml/rowkeys_serbian_qwertz1.xml new file mode 100644 index 00000000..84a497f8 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_serbian_qwertz1.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/rowkeys_serbian_qwertz2.xml b/app/src/main/res/xml/rowkeys_serbian_qwertz2.xml new file mode 100644 index 00000000..309e7e6d --- /dev/null +++ b/app/src/main/res/xml/rowkeys_serbian_qwertz2.xml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_serbian_qwertz3.xml b/app/src/main/res/xml/rowkeys_serbian_qwertz3.xml new file mode 100644 index 00000000..7caa2d9e --- /dev/null +++ b/app/src/main/res/xml/rowkeys_serbian_qwertz3.xml @@ -0,0 +1,27 @@ + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_sinhala1.xml b/app/src/main/res/xml/rowkeys_sinhala1.xml new file mode 100644 index 00000000..baf39d26 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_sinhala1.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_sinhala2.xml b/app/src/main/res/xml/rowkeys_sinhala2.xml new file mode 100644 index 00000000..a0d5cd45 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_sinhala2.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_sinhala3.xml b/app/src/main/res/xml/rowkeys_sinhala3.xml new file mode 100644 index 00000000..36330ebe --- /dev/null +++ b/app/src/main/res/xml/rowkeys_sinhala3.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_south_slavic1.xml b/app/src/main/res/xml/rowkeys_south_slavic1.xml new file mode 100644 index 00000000..e39a67d0 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_south_slavic1.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_south_slavic2.xml b/app/src/main/res/xml/rowkeys_south_slavic2.xml new file mode 100644 index 00000000..4be63790 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_south_slavic2.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_south_slavic3.xml b/app/src/main/res/xml/rowkeys_south_slavic3.xml new file mode 100644 index 00000000..597a3859 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_south_slavic3.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_spanish2.xml b/app/src/main/res/xml/rowkeys_spanish2.xml new file mode 100644 index 00000000..aa0dfff4 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_spanish2.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_swiss1.xml b/app/src/main/res/xml/rowkeys_swiss1.xml new file mode 100644 index 00000000..62bb86f1 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_swiss1.xml @@ -0,0 +1,29 @@ + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_swiss2.xml b/app/src/main/res/xml/rowkeys_swiss2.xml new file mode 100644 index 00000000..4ed3d62e --- /dev/null +++ b/app/src/main/res/xml/rowkeys_swiss2.xml @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_symbols1.xml b/app/src/main/res/xml/rowkeys_symbols1.xml new file mode 100644 index 00000000..90237f5c --- /dev/null +++ b/app/src/main/res/xml/rowkeys_symbols1.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_symbols2.xml b/app/src/main/res/xml/rowkeys_symbols2.xml new file mode 100644 index 00000000..55a1806c --- /dev/null +++ b/app/src/main/res/xml/rowkeys_symbols2.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_symbols3.xml b/app/src/main/res/xml/rowkeys_symbols3.xml new file mode 100644 index 00000000..29490e00 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_symbols3.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_symbols_shift1.xml b/app/src/main/res/xml/rowkeys_symbols_shift1.xml new file mode 100644 index 00000000..abc29f77 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_symbols_shift1.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_symbols_shift2.xml b/app/src/main/res/xml/rowkeys_symbols_shift2.xml new file mode 100644 index 00000000..e6565b48 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_symbols_shift2.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_symbols_shift3.xml b/app/src/main/res/xml/rowkeys_symbols_shift3.xml new file mode 100644 index 00000000..d3277547 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_symbols_shift3.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_tamil1.xml b/app/src/main/res/xml/rowkeys_tamil1.xml new file mode 100644 index 00000000..9b3423d5 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_tamil1.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_tamil2.xml b/app/src/main/res/xml/rowkeys_tamil2.xml new file mode 100644 index 00000000..d8143e47 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_tamil2.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_tamil3.xml b/app/src/main/res/xml/rowkeys_tamil3.xml new file mode 100644 index 00000000..d1f591be --- /dev/null +++ b/app/src/main/res/xml/rowkeys_tamil3.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_telugu1.xml b/app/src/main/res/xml/rowkeys_telugu1.xml new file mode 100644 index 00000000..7f46886c --- /dev/null +++ b/app/src/main/res/xml/rowkeys_telugu1.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_telugu2.xml b/app/src/main/res/xml/rowkeys_telugu2.xml new file mode 100644 index 00000000..d3d669a5 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_telugu2.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_telugu3.xml b/app/src/main/res/xml/rowkeys_telugu3.xml new file mode 100644 index 00000000..97b2b86a --- /dev/null +++ b/app/src/main/res/xml/rowkeys_telugu3.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_thai1.xml b/app/src/main/res/xml/rowkeys_thai1.xml new file mode 100644 index 00000000..e32c7b05 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_thai1.xml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_thai2.xml b/app/src/main/res/xml/rowkeys_thai2.xml new file mode 100644 index 00000000..c798a0d9 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_thai2.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_thai3.xml b/app/src/main/res/xml/rowkeys_thai3.xml new file mode 100644 index 00000000..96953e85 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_thai3.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_thai4.xml b/app/src/main/res/xml/rowkeys_thai4.xml new file mode 100644 index 00000000..9ab02070 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_thai4.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rowkeys_uzbek1.xml b/app/src/main/res/xml/rowkeys_uzbek1.xml new file mode 100644 index 00000000..80b84470 --- /dev/null +++ b/app/src/main/res/xml/rowkeys_uzbek1.xml @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/rowkeys_uzbek2.xml b/app/src/main/res/xml/rowkeys_uzbek2.xml new file mode 100644 index 00000000..32c47bcb --- /dev/null +++ b/app/src/main/res/xml/rowkeys_uzbek2.xml @@ -0,0 +1,29 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/rows_arabic.xml b/app/src/main/res/xml/rows_arabic.xml new file mode 100644 index 00000000..48a0d051 --- /dev/null +++ b/app/src/main/res/xml/rows_arabic.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_armenian_phonetic.xml b/app/src/main/res/xml/rows_armenian_phonetic.xml new file mode 100644 index 00000000..504f31b2 --- /dev/null +++ b/app/src/main/res/xml/rows_armenian_phonetic.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_azerty.xml b/app/src/main/res/xml/rows_azerty.xml new file mode 100644 index 00000000..33f77003 --- /dev/null +++ b/app/src/main/res/xml/rows_azerty.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_bengali.xml b/app/src/main/res/xml/rows_bengali.xml new file mode 100644 index 00000000..bee3f59b --- /dev/null +++ b/app/src/main/res/xml/rows_bengali.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_bengali_akkhor.xml b/app/src/main/res/xml/rows_bengali_akkhor.xml new file mode 100644 index 00000000..bc9ec963 --- /dev/null +++ b/app/src/main/res/xml/rows_bengali_akkhor.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_bepo.xml b/app/src/main/res/xml/rows_bepo.xml new file mode 100644 index 00000000..28ef41b7 --- /dev/null +++ b/app/src/main/res/xml/rows_bepo.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_bulgarian.xml b/app/src/main/res/xml/rows_bulgarian.xml new file mode 100644 index 00000000..5dc79ec6 --- /dev/null +++ b/app/src/main/res/xml/rows_bulgarian.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_bulgarian_bds.xml b/app/src/main/res/xml/rows_bulgarian_bds.xml new file mode 100644 index 00000000..ccc62f84 --- /dev/null +++ b/app/src/main/res/xml/rows_bulgarian_bds.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_colemak.xml b/app/src/main/res/xml/rows_colemak.xml new file mode 100644 index 00000000..d62cd941 --- /dev/null +++ b/app/src/main/res/xml/rows_colemak.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_dvorak.xml b/app/src/main/res/xml/rows_dvorak.xml new file mode 100644 index 00000000..eef1bcf1 --- /dev/null +++ b/app/src/main/res/xml/rows_dvorak.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_east_slavic.xml b/app/src/main/res/xml/rows_east_slavic.xml new file mode 100644 index 00000000..12d9d955 --- /dev/null +++ b/app/src/main/res/xml/rows_east_slavic.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_farsi.xml b/app/src/main/res/xml/rows_farsi.xml new file mode 100644 index 00000000..a58821ca --- /dev/null +++ b/app/src/main/res/xml/rows_farsi.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_georgian.xml b/app/src/main/res/xml/rows_georgian.xml new file mode 100644 index 00000000..527ee929 --- /dev/null +++ b/app/src/main/res/xml/rows_georgian.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_greek.xml b/app/src/main/res/xml/rows_greek.xml new file mode 100644 index 00000000..1f968369 --- /dev/null +++ b/app/src/main/res/xml/rows_greek.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_hebrew.xml b/app/src/main/res/xml/rows_hebrew.xml new file mode 100644 index 00000000..e7961962 --- /dev/null +++ b/app/src/main/res/xml/rows_hebrew.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_hindi.xml b/app/src/main/res/xml/rows_hindi.xml new file mode 100644 index 00000000..d67d1e50 --- /dev/null +++ b/app/src/main/res/xml/rows_hindi.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_hindi_compact.xml b/app/src/main/res/xml/rows_hindi_compact.xml new file mode 100644 index 00000000..bc07c301 --- /dev/null +++ b/app/src/main/res/xml/rows_hindi_compact.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_kannada.xml b/app/src/main/res/xml/rows_kannada.xml new file mode 100644 index 00000000..d3c3095b --- /dev/null +++ b/app/src/main/res/xml/rows_kannada.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/rows_khmer.xml b/app/src/main/res/xml/rows_khmer.xml new file mode 100644 index 00000000..ba7d5539 --- /dev/null +++ b/app/src/main/res/xml/rows_khmer.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_lao.xml b/app/src/main/res/xml/rows_lao.xml new file mode 100644 index 00000000..de2e7a1b --- /dev/null +++ b/app/src/main/res/xml/rows_lao.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_malayalam.xml b/app/src/main/res/xml/rows_malayalam.xml new file mode 100644 index 00000000..0af1a851 --- /dev/null +++ b/app/src/main/res/xml/rows_malayalam.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/rows_marathi.xml b/app/src/main/res/xml/rows_marathi.xml new file mode 100644 index 00000000..e1064903 --- /dev/null +++ b/app/src/main/res/xml/rows_marathi.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/rows_mongolian.xml b/app/src/main/res/xml/rows_mongolian.xml new file mode 100644 index 00000000..dcbe4165 --- /dev/null +++ b/app/src/main/res/xml/rows_mongolian.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_nepali_romanized.xml b/app/src/main/res/xml/rows_nepali_romanized.xml new file mode 100644 index 00000000..42419835 --- /dev/null +++ b/app/src/main/res/xml/rows_nepali_romanized.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_nepali_traditional.xml b/app/src/main/res/xml/rows_nepali_traditional.xml new file mode 100644 index 00000000..f4465a31 --- /dev/null +++ b/app/src/main/res/xml/rows_nepali_traditional.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_nordic.xml b/app/src/main/res/xml/rows_nordic.xml new file mode 100644 index 00000000..be15eaf6 --- /dev/null +++ b/app/src/main/res/xml/rows_nordic.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_number.xml b/app/src/main/res/xml/rows_number.xml new file mode 100644 index 00000000..5e5e4548 --- /dev/null +++ b/app/src/main/res/xml/rows_number.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_number_normal.xml b/app/src/main/res/xml/rows_number_normal.xml new file mode 100644 index 00000000..21ce5d63 --- /dev/null +++ b/app/src/main/res/xml/rows_number_normal.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_number_password.xml b/app/src/main/res/xml/rows_number_password.xml new file mode 100644 index 00000000..97d2a35a --- /dev/null +++ b/app/src/main/res/xml/rows_number_password.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_pcqwerty.xml b/app/src/main/res/xml/rows_pcqwerty.xml new file mode 100644 index 00000000..5efc6c9d --- /dev/null +++ b/app/src/main/res/xml/rows_pcqwerty.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_phone.xml b/app/src/main/res/xml/rows_phone.xml new file mode 100644 index 00000000..8aa86519 --- /dev/null +++ b/app/src/main/res/xml/rows_phone.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_phone_symbols.xml b/app/src/main/res/xml/rows_phone_symbols.xml new file mode 100644 index 00000000..859664ff --- /dev/null +++ b/app/src/main/res/xml/rows_phone_symbols.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_qwerty.xml b/app/src/main/res/xml/rows_qwerty.xml new file mode 100644 index 00000000..9016e478 --- /dev/null +++ b/app/src/main/res/xml/rows_qwerty.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_qwertz.xml b/app/src/main/res/xml/rows_qwertz.xml new file mode 100644 index 00000000..8a7b395a --- /dev/null +++ b/app/src/main/res/xml/rows_qwertz.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_serbian_qwertz.xml b/app/src/main/res/xml/rows_serbian_qwertz.xml new file mode 100644 index 00000000..23c3c85a --- /dev/null +++ b/app/src/main/res/xml/rows_serbian_qwertz.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/rows_sinhala.xml b/app/src/main/res/xml/rows_sinhala.xml new file mode 100644 index 00000000..201fd3ae --- /dev/null +++ b/app/src/main/res/xml/rows_sinhala.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_south_slavic.xml b/app/src/main/res/xml/rows_south_slavic.xml new file mode 100644 index 00000000..60185ce8 --- /dev/null +++ b/app/src/main/res/xml/rows_south_slavic.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_spanish.xml b/app/src/main/res/xml/rows_spanish.xml new file mode 100644 index 00000000..4c5ffc3b --- /dev/null +++ b/app/src/main/res/xml/rows_spanish.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_swiss.xml b/app/src/main/res/xml/rows_swiss.xml new file mode 100644 index 00000000..2e223514 --- /dev/null +++ b/app/src/main/res/xml/rows_swiss.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_symbols.xml b/app/src/main/res/xml/rows_symbols.xml new file mode 100644 index 00000000..eb8746ee --- /dev/null +++ b/app/src/main/res/xml/rows_symbols.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_symbols_shift.xml b/app/src/main/res/xml/rows_symbols_shift.xml new file mode 100644 index 00000000..318ec430 --- /dev/null +++ b/app/src/main/res/xml/rows_symbols_shift.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_tamil.xml b/app/src/main/res/xml/rows_tamil.xml new file mode 100644 index 00000000..d7242997 --- /dev/null +++ b/app/src/main/res/xml/rows_tamil.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/rows_telugu.xml b/app/src/main/res/xml/rows_telugu.xml new file mode 100644 index 00000000..11d8b884 --- /dev/null +++ b/app/src/main/res/xml/rows_telugu.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/rows_thai.xml b/app/src/main/res/xml/rows_thai.xml new file mode 100644 index 00000000..067822ca --- /dev/null +++ b/app/src/main/res/xml/rows_thai.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/rows_uzbek.xml b/app/src/main/res/xml/rows_uzbek.xml new file mode 100644 index 00000000..14fd469b --- /dev/null +++ b/app/src/main/res/xml/rows_uzbek.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/spell_checker_settings.xml b/app/src/main/res/xml/spell_checker_settings.xml new file mode 100644 index 00000000..3d950185 --- /dev/null +++ b/app/src/main/res/xml/spell_checker_settings.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/app/src/main/res/xml/spellchecker.xml b/app/src/main/res/xml/spellchecker.xml new file mode 100644 index 00000000..85c6cffb --- /dev/null +++ b/app/src/main/res/xml/spellchecker.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build.gradle b/build.gradle new file mode 100755 index 00000000..e0c5350a --- /dev/null +++ b/build.gradle @@ -0,0 +1,25 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + google() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.5.3' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..700bdd11 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +android.enableJetifier=true +android.useAndroidX=true +org.gradle.jvmargs=-Xmx1024m \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100755 index 00000000..13372aef Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100755 index 00000000..5665bf27 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Dec 27 16:19:07 CET 2019 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..9d82f789 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100755 index 00000000..aec99730 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/images/ic_launcher_keyboard.png b/images/ic_launcher_keyboard.png new file mode 100644 index 00000000..6c64b19a Binary files /dev/null and b/images/ic_launcher_keyboard.png differ diff --git a/images/openboard_Icon.xcf b/images/openboard_Icon.xcf new file mode 100644 index 00000000..e91c8117 Binary files /dev/null and b/images/openboard_Icon.xcf differ diff --git a/settings.gradle b/settings.gradle new file mode 100755 index 00000000..e7b4def4 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +include ':app'