diff options
Diffstat (limited to 'java/src')
42 files changed, 1978 insertions, 896 deletions
diff --git a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java index bf1cea9c3..ee52de1d1 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibilityUtils.java @@ -80,16 +80,24 @@ public final class AccessibilityUtils { } /** + * 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 a spoken feedback service is turned on. + * and system accessibility is turned on. * * @return {@code true} if touch exploration is enabled. */ public boolean isTouchExplorationEnabled() { - return ENABLE_ACCESSIBILITY - && mAccessibilityManager.isEnabled() - && mAccessibilityManager.isTouchExplorationEnabled(); + return isAccessibilityEnabled() && mAccessibilityManager.isTouchExplorationEnabled(); } /** @@ -113,6 +121,7 @@ public final class AccessibilityUtils { * * @return {@code true} if the device should obscure password characters. */ + @SuppressWarnings("deprecation") public boolean shouldObscureInput(final EditorInfo editorInfo) { if (editorInfo == null) return false; diff --git a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java index d05fd9eb5..e6b44120f 100644 --- a/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java +++ b/java/src/com/android/inputmethod/accessibility/AccessibleKeyboardViewProxy.java @@ -22,8 +22,11 @@ import android.support.v4.view.AccessibilityDelegateCompat; import android.support.v4.view.ViewCompat; import android.support.v4.view.accessibility.AccessibilityEventCompat; import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.util.SparseIntArray; import android.view.MotionEvent; import android.view.View; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; @@ -35,6 +38,21 @@ import com.android.inputmethod.latin.R; public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateCompat { private static final AccessibleKeyboardViewProxy sInstance = new AccessibleKeyboardViewProxy(); + /** 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); + } + private InputMethodService mInputMethod; private MainKeyboardView mView; private AccessibilityEntityProvider mAccessibilityNodeProvider; @@ -85,11 +103,75 @@ public final class AccessibleKeyboardViewProxy extends AccessibilityDelegateComp mAccessibilityNodeProvider.setView(view); } + /** + * Called when the keyboard layout changes. + * <p> + * <b>Note:</b> This method will be called even if accessibility is not + * enabled. + */ public void setKeyboard() { - if (mAccessibilityNodeProvider == null) { + if (mAccessibilityNodeProvider != null) { + mAccessibilityNodeProvider.setKeyboard(); + } + + // Since this method is called even when accessibility is off, make sure + // to check the state before announcing anything. + if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) { + announceKeyboardMode(); + } + } + + /** + * Called when the keyboard is hidden and accessibility is enabled. + */ + public void onHideWindow() { + announceKeyboardHidden(); + } + + /** + * Announces which type of keyboard is being displayed. If the keyboard type + * is unknown, no announcement is made. + */ + private void announceKeyboardMode() { + final Keyboard keyboard = mView.getKeyboard(); + final int resId = KEYBOARD_MODE_RES_IDS.get(keyboard.mId.mMode); + if (resId == 0) { return; } - mAccessibilityNodeProvider.setKeyboard(); + + final Context context = mView.getContext(); + final String keyboardMode = context.getString(resId); + final String text = context.getString(R.string.announce_keyboard_mode, keyboardMode); + + sendWindowStateChanged(text); + } + + /** + * Announces that the keyboard has been hidden. + */ + private void announceKeyboardHidden() { + final Context context = mView.getContext(); + final String text = context.getString(R.string.announce_keyboard_hidden); + + sendWindowStateChanged(text); + } + + /** + * Sends a window state change event with the specified text. + * + * @param text + */ + private void sendWindowStateChanged(final String text) { + final AccessibilityEvent stateChange = AccessibilityEvent.obtain( + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + mView.onInitializeAccessibilityEvent(stateChange); + stateChange.getText().add(text); + stateChange.setContentDescription(null); + + final ViewParent parent = mView.getParent(); + if (parent != null) { + parent.requestSendAccessibilityEvent(mView, stateChange); + } } /** diff --git a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java index ea86d98cb..05d8269b7 100644 --- a/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java +++ b/java/src/com/android/inputmethod/accessibility/KeyCodeDescriptionMapper.java @@ -110,7 +110,9 @@ public final class KeyCodeDescriptionMapper { return getDescriptionForShiftKey(context, keyboard); } - if (code == Constants.CODE_ACTION_ENTER) { + 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); } diff --git a/java/src/com/android/inputmethod/compat/CompatUtils.java b/java/src/com/android/inputmethod/compat/CompatUtils.java index 5a2b6bd2b..660029baf 100644 --- a/java/src/com/android/inputmethod/compat/CompatUtils.java +++ b/java/src/com/android/inputmethod/compat/CompatUtils.java @@ -81,7 +81,7 @@ public final class CompatUtils { try { return constructor.newInstance(args); } catch (Exception e) { - Log.e(TAG, "Exception in newInstance: " + e.getClass().getSimpleName()); + Log.e(TAG, "Exception in newInstance", e); } return null; } @@ -92,7 +92,7 @@ public final class CompatUtils { try { return method.invoke(receiver, args); } catch (Exception e) { - Log.e(TAG, "Exception in invoke: " + e.getClass().getSimpleName()); + Log.e(TAG, "Exception in invoke", e); } return defaultValue; } @@ -103,7 +103,7 @@ public final class CompatUtils { try { return field.get(receiver); } catch (Exception e) { - Log.e(TAG, "Exception in getFieldValue: " + e.getClass().getSimpleName()); + Log.e(TAG, "Exception in getFieldValue", e); } return defaultValue; } @@ -113,7 +113,7 @@ public final class CompatUtils { try { field.set(receiver, value); } catch (Exception e) { - Log.e(TAG, "Exception in setFieldValue: " + e.getClass().getSimpleName()); + Log.e(TAG, "Exception in setFieldValue", e); } } } diff --git a/java/src/com/android/inputmethod/compat/IntentCompatUtils.java b/java/src/com/android/inputmethod/compat/IntentCompatUtils.java new file mode 100644 index 000000000..df2e22fe8 --- /dev/null +++ b/java/src/com/android/inputmethod/compat/IntentCompatUtils.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 com.android.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). + public static final String ACTION_USER_INITIALIZE = + (String)CompatUtils.getFieldValue(null, null, + CompatUtils.getField(Intent.class, "ACTION_USER_INITIALIZE")); + + private IntentCompatUtils() { + // This utility class is not publicly instantiable. + } + + public static boolean has_ACTION_USER_INITIALIZE(final Intent intent) { + return ACTION_USER_INITIALIZE != null && intent != null + && ACTION_USER_INITIALIZE.equals(intent.getAction()); + } +} diff --git a/java/src/com/android/inputmethod/compat/TextViewCompatUtils.java b/java/src/com/android/inputmethod/compat/TextViewCompatUtils.java new file mode 100644 index 000000000..d4f1ea830 --- /dev/null +++ b/java/src/com/android/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 com.android.inputmethod.compat; + +import android.graphics.drawable.Drawable; +import android.widget.TextView; + +import java.lang.reflect.Method; + +public final class TextViewCompatUtils { + // Note that TextView.setCompoundDrawablesRelative(Drawable,Drawable,Drawable,Drawable) has + // been introduced in API level 17 (Build.VERSION_CODE.JELLY_BEAN_MR1). + private static final Method METHOD_setCompoundDrawablesRelative = CompatUtils.getMethod( + TextView.class, "setCompoundDrawablesRelative", + Drawable.class, Drawable.class, Drawable.class, Drawable.class); + + private TextViewCompatUtils() { + // This utility class is not publicly instantiable. + } + + public static void setCompoundDrawablesRelative(final TextView textView, final Drawable start, + final Drawable top, final Drawable end, final Drawable bottom) { + if (METHOD_setCompoundDrawablesRelative == null) { + textView.setCompoundDrawables(start, top, end, bottom); + return; + } + CompatUtils.invoke(textView, null, METHOD_setCompoundDrawablesRelative, + start, top, end, bottom); + } +} diff --git a/java/src/com/android/inputmethod/compat/ViewCompatUtils.java b/java/src/com/android/inputmethod/compat/ViewCompatUtils.java new file mode 100644 index 000000000..a8fab8855 --- /dev/null +++ b/java/src/com/android/inputmethod/compat/ViewCompatUtils.java @@ -0,0 +1,68 @@ +/* + * 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 com.android.inputmethod.compat; + +import android.view.View; + +import java.lang.reflect.Method; + +public final class ViewCompatUtils { + // Note that View.LAYOUT_DIRECTION_LTR and View.LAYOUT_DIRECTION_RTL have been introduced in + // API level 17 (Build.VERSION_CODE.JELLY_BEAN_MR1). + public static final int LAYOUT_DIRECTION_LTR = (Integer)CompatUtils.getFieldValue(null, 0x0, + CompatUtils.getField(View.class, "LAYOUT_DIRECTION_LTR")); + public static final int LAYOUT_DIRECTION_RTL = (Integer)CompatUtils.getFieldValue(null, 0x1, + CompatUtils.getField(View.class, "LAYOUT_DIRECTION_RTL")); + + // Note that View.getPaddingEnd(), View.setPaddingRelative(int,int,int,int), and + // View.getLayoutDirection() 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", + Integer.TYPE, Integer.TYPE, Integer.TYPE, Integer.TYPE); + private static final Method METHOD_getLayoutDirection = CompatUtils.getMethod( + View.class, "getLayoutDirection"); + + 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); + } + + public static int getLayoutDirection(final View view) { + if (METHOD_getLayoutDirection == null) { + return LAYOUT_DIRECTION_LTR; + } + return (Integer)CompatUtils.invoke(view, 0, METHOD_getLayoutDirection); + } +} diff --git a/java/src/com/android/inputmethod/event/HardwareKeyboardEventDecoder.java b/java/src/com/android/inputmethod/event/HardwareKeyboardEventDecoder.java index a2463c20c..720d07433 100644 --- a/java/src/com/android/inputmethod/event/HardwareKeyboardEventDecoder.java +++ b/java/src/com/android/inputmethod/event/HardwareKeyboardEventDecoder.java @@ -58,11 +58,11 @@ public class HardwareKeyboardEventDecoder implements HardwareEventDecoder { } if (KeyEvent.KEYCODE_ENTER == keyCode) { // The Enter key. If the Shift key is not being pressed, this should send a - // CODE_ACTION_ENTER to trigger the action if any, or a carriage return - // otherwise. If the Shift key is depressed, this should send a - // CODE_SHIFT_ENTER and let Latin IME decide what to do with it. + // 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. return Event.createCommittableEvent(keyEvent.isShiftPressed() - ? Constants.CODE_SHIFT_ENTER : Constants.CODE_ACTION_ENTER, + ? Constants.CODE_SHIFT_ENTER : Constants.CODE_ENTER, null /* next */); } // If not Enter, then we have a committable character. This should be committed diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java index 1e5af5154..d160038ad 100644 --- a/java/src/com/android/inputmethod/keyboard/Key.java +++ b/java/src/com/android/inputmethod/keyboard/Key.java @@ -519,11 +519,11 @@ public class Key implements Comparable<Key> { // TODO: Handle "bold" here too? if ((mLabelFlags & LABEL_FLAGS_FONT_NORMAL) != 0) { return Typeface.DEFAULT; - } else if ((mLabelFlags & LABEL_FLAGS_FONT_MONO_SPACE) != 0) { + } + if ((mLabelFlags & LABEL_FLAGS_FONT_MONO_SPACE) != 0) { return Typeface.MONOSPACE; - } else { - return params.mTypeface; } + return params.mTypeface; } public final int selectTextSize(final KeyDrawParams params) { @@ -550,28 +550,51 @@ public class Key implements Comparable<Key> { public final int selectHintTextSize(final KeyDrawParams params) { if (hasHintLabel()) { return params.mHintLabelSize; - } else if (hasShiftedLetterHint()) { + } + if (hasShiftedLetterHint()) { return params.mShiftedLetterHintSize; - } else { - return params.mHintLetterSize; } + return params.mHintLetterSize; } public final int selectHintTextColor(final KeyDrawParams params) { if (hasHintLabel()) { return params.mHintLabelColor; - } else if (hasShiftedLetterHint()) { + } + if (hasShiftedLetterHint()) { return isShiftedLetterActivated() ? params.mShiftedLetterHintActivatedColor : params.mShiftedLetterHintInactivatedColor; - } else { - return params.mHintLetterColor; } + 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; + } + + public Typeface selectPreviewTypeface(final KeyDrawParams params) { + if (previewHasLetterSize()) { + return selectTypeface(params); + } + return Typeface.DEFAULT_BOLD; + } + public final boolean isAlignLeft() { return (mLabelFlags & LABEL_FLAGS_ALIGN_LEFT) != 0; } diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java index 4dab50fd8..350dc69b2 100644 --- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java @@ -635,15 +635,9 @@ public class KeyboardView extends View { invalidate(x, y, x + key.mWidth, y + key.mHeight); } - public void closing() { - mInvalidateAllKeys = true; - mKeyboard = null; - } - @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - closing(); freeOffscreenBuffer(); } } diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java index 4d10f0e69..d37b69b00 100644 --- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java @@ -811,18 +811,14 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack background.setState(KEY_PREVIEW_BACKGROUND_DEFAULT_STATE); background.setAlpha(PREVIEW_ALPHA); } - final String label = key.isShiftedLetterActivated() ? key.mHintLabel : key.mLabel; + final String label = key.getPreviewLabel(); // What we show as preview should match what we show on a key top in onDraw(). if (label != null) { // TODO Should take care of temporaryShiftLabel here. previewText.setCompoundDrawables(null, null, null, null); - if (StringUtils.codePointCount(label) > 1) { - previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, drawParams.mLetterSize); - previewText.setTypeface(Typeface.DEFAULT_BOLD); - } else { - previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, drawParams.mPreviewTextSize); - previewText.setTypeface(key.selectTypeface(drawParams)); - } + previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, + key.selectPreviewTextSize(drawParams)); + previewText.setTypeface(key.selectPreviewTypeface(drawParams)); previewText.setText(label); } else { previewText.setCompoundDrawables(null, null, null, @@ -1236,13 +1232,11 @@ public final class MainKeyboardView extends KeyboardView implements PointerTrack mDrawingHandler.cancelAllMessages(); } - @Override public void closing() { dismissAllKeyPreviews(); cancelAllMessages(); onDismissMoreKeysPanel(); mMoreKeysKeyboardCache.clear(); - super.closing(); } /** diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java index 9e75f8b8a..0d42ab2fe 100644 --- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java +++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java @@ -174,7 +174,6 @@ public class MoreKeysKeyboardView extends KeyboardView implements MoreKeysPanel @Override public boolean dismissMoreKeysPanel() { - super.closing(); if (mController == null) return false; return mController.onDismissMoreKeysPanel(); } diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java index 0ec6b0176..3e25c3b86 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardCodesSet.java @@ -48,7 +48,6 @@ public final class KeyboardCodesSet { "key_delete", "key_settings", "key_shortcut", - "key_action_enter", "key_action_next", "key_action_previous", "key_shift_enter", @@ -85,7 +84,6 @@ public final class KeyboardCodesSet { Constants.CODE_DELETE, Constants.CODE_SETTINGS, Constants.CODE_SHORTCUT, - Constants.CODE_ACTION_ENTER, Constants.CODE_ACTION_NEXT, Constants.CODE_ACTION_PREVIOUS, Constants.CODE_SHIFT_ENTER, @@ -118,7 +116,6 @@ public final class KeyboardCodesSet { DEFAULT[12], DEFAULT[13], DEFAULT[14], - DEFAULT[15], CODE_RIGHT_PARENTHESIS, CODE_LEFT_PARENTHESIS, CODE_GREATER_THAN_SIGN, @@ -142,7 +139,7 @@ public final class KeyboardCodesSet { }; static { - if (DEFAULT.length != RTL.length) { + if (DEFAULT.length != RTL.length || DEFAULT.length != ID_TO_NAME.length) { throw new RuntimeException("Internal inconsistency"); } for (int i = 0; i < ID_TO_NAME.length; i++) { diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java index 493093e95..d0b382e35 100644 --- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java +++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java @@ -145,94 +145,110 @@ public final class KeyboardTextsSet { /* 40 */ "more_keys_for_cyrillic_ie", /* 41 */ "more_keys_for_cyrillic_i", /* 42 */ "label_to_alpha_key", - /* 43 */ "more_keys_for_single_quote", - /* 44 */ "more_keys_for_double_quote", - /* 45 */ "more_keys_for_tablet_double_quote", - /* 46 */ "more_keys_for_currency_dollar", - /* 47 */ "keylabel_for_currency_generic", - /* 48 */ "more_keys_for_currency_generic", - /* 49 */ "more_keys_for_punctuation", - /* 50 */ "more_keys_for_star", - /* 51 */ "more_keys_for_bullet", - /* 52 */ "more_keys_for_plus", - /* 53 */ "more_keys_for_left_parenthesis", - /* 54 */ "more_keys_for_right_parenthesis", - /* 55 */ "more_keys_for_less_than", - /* 56 */ "more_keys_for_greater_than", - /* 57 */ "more_keys_for_arabic_diacritics", - /* 58 */ "keyhintlabel_for_arabic_diacritics", - /* 59 */ "keylabel_for_symbols_1", - /* 60 */ "keylabel_for_symbols_2", - /* 61 */ "keylabel_for_symbols_3", - /* 62 */ "keylabel_for_symbols_4", - /* 63 */ "keylabel_for_symbols_5", - /* 64 */ "keylabel_for_symbols_6", - /* 65 */ "keylabel_for_symbols_7", - /* 66 */ "keylabel_for_symbols_8", - /* 67 */ "keylabel_for_symbols_9", - /* 68 */ "keylabel_for_symbols_0", - /* 69 */ "label_to_symbol_key", - /* 70 */ "label_to_symbol_with_microphone_key", - /* 71 */ "additional_more_keys_for_symbols_1", - /* 72 */ "additional_more_keys_for_symbols_2", - /* 73 */ "additional_more_keys_for_symbols_3", - /* 74 */ "additional_more_keys_for_symbols_4", - /* 75 */ "additional_more_keys_for_symbols_5", - /* 76 */ "additional_more_keys_for_symbols_6", - /* 77 */ "additional_more_keys_for_symbols_7", - /* 78 */ "additional_more_keys_for_symbols_8", - /* 79 */ "additional_more_keys_for_symbols_9", - /* 80 */ "additional_more_keys_for_symbols_0", - /* 81 */ "more_keys_for_symbols_1", - /* 82 */ "more_keys_for_symbols_2", - /* 83 */ "more_keys_for_symbols_3", - /* 84 */ "more_keys_for_symbols_4", - /* 85 */ "more_keys_for_symbols_5", - /* 86 */ "more_keys_for_symbols_6", - /* 87 */ "more_keys_for_symbols_7", - /* 88 */ "more_keys_for_symbols_8", - /* 89 */ "more_keys_for_symbols_9", - /* 90 */ "more_keys_for_symbols_0", - /* 91 */ "keylabel_for_comma", - /* 92 */ "more_keys_for_comma", - /* 93 */ "keylabel_for_symbols_question", - /* 94 */ "keylabel_for_symbols_semicolon", - /* 95 */ "keylabel_for_symbols_percent", - /* 96 */ "more_keys_for_symbols_exclamation", - /* 97 */ "more_keys_for_symbols_question", - /* 98 */ "more_keys_for_symbols_semicolon", - /* 99 */ "more_keys_for_symbols_percent", - /* 100 */ "keylabel_for_tablet_comma", - /* 101 */ "keyhintlabel_for_tablet_comma", - /* 102 */ "more_keys_for_tablet_comma", - /* 103 */ "keyhintlabel_for_tablet_period", - /* 104 */ "more_keys_for_tablet_period", - /* 105 */ "keylabel_for_apostrophe", - /* 106 */ "keyhintlabel_for_apostrophe", - /* 107 */ "more_keys_for_apostrophe", - /* 108 */ "more_keys_for_q", - /* 109 */ "more_keys_for_x", - /* 110 */ "keylabel_for_q", - /* 111 */ "keylabel_for_w", - /* 112 */ "keylabel_for_y", - /* 113 */ "keylabel_for_x", - /* 114 */ "keylabel_for_spanish_row2_10", - /* 115 */ "more_keys_for_am_pm", - /* 116 */ "settings_as_more_key", - /* 117 */ "shortcut_as_more_key", - /* 118 */ "action_next_as_more_key", - /* 119 */ "action_previous_as_more_key", - /* 120 */ "label_to_more_symbol_key", - /* 121 */ "label_to_more_symbol_for_tablet_key", - /* 122 */ "label_tab_key", - /* 123 */ "label_to_phone_numeric_key", - /* 124 */ "label_to_phone_symbols_key", - /* 125 */ "label_time_am", - /* 126 */ "label_time_pm", - /* 127 */ "label_to_symbol_key_pcqwerty", - /* 128 */ "keylabel_for_popular_domain", - /* 129 */ "more_keys_for_popular_domain", - /* 130 */ "more_keys_for_smiley", + /* 43 */ "single_quotes", + /* 44 */ "double_quotes", + /* 45 */ "single_angle_quotes", + /* 46 */ "double_angle_quotes", + /* 47 */ "more_keys_for_currency_dollar", + /* 48 */ "keylabel_for_currency_generic", + /* 49 */ "more_keys_for_currency_generic", + /* 50 */ "more_keys_for_punctuation", + /* 51 */ "more_keys_for_star", + /* 52 */ "more_keys_for_bullet", + /* 53 */ "more_keys_for_plus", + /* 54 */ "more_keys_for_left_parenthesis", + /* 55 */ "more_keys_for_right_parenthesis", + /* 56 */ "more_keys_for_less_than", + /* 57 */ "more_keys_for_greater_than", + /* 58 */ "more_keys_for_arabic_diacritics", + /* 59 */ "keyhintlabel_for_arabic_diacritics", + /* 60 */ "keylabel_for_symbols_1", + /* 61 */ "keylabel_for_symbols_2", + /* 62 */ "keylabel_for_symbols_3", + /* 63 */ "keylabel_for_symbols_4", + /* 64 */ "keylabel_for_symbols_5", + /* 65 */ "keylabel_for_symbols_6", + /* 66 */ "keylabel_for_symbols_7", + /* 67 */ "keylabel_for_symbols_8", + /* 68 */ "keylabel_for_symbols_9", + /* 69 */ "keylabel_for_symbols_0", + /* 70 */ "label_to_symbol_key", + /* 71 */ "label_to_symbol_with_microphone_key", + /* 72 */ "additional_more_keys_for_symbols_1", + /* 73 */ "additional_more_keys_for_symbols_2", + /* 74 */ "additional_more_keys_for_symbols_3", + /* 75 */ "additional_more_keys_for_symbols_4", + /* 76 */ "additional_more_keys_for_symbols_5", + /* 77 */ "additional_more_keys_for_symbols_6", + /* 78 */ "additional_more_keys_for_symbols_7", + /* 79 */ "additional_more_keys_for_symbols_8", + /* 80 */ "additional_more_keys_for_symbols_9", + /* 81 */ "additional_more_keys_for_symbols_0", + /* 82 */ "more_keys_for_symbols_1", + /* 83 */ "more_keys_for_symbols_2", + /* 84 */ "more_keys_for_symbols_3", + /* 85 */ "more_keys_for_symbols_4", + /* 86 */ "more_keys_for_symbols_5", + /* 87 */ "more_keys_for_symbols_6", + /* 88 */ "more_keys_for_symbols_7", + /* 89 */ "more_keys_for_symbols_8", + /* 90 */ "more_keys_for_symbols_9", + /* 91 */ "more_keys_for_symbols_0", + /* 92 */ "keylabel_for_comma", + /* 93 */ "more_keys_for_comma", + /* 94 */ "keylabel_for_symbols_question", + /* 95 */ "keylabel_for_symbols_semicolon", + /* 96 */ "keylabel_for_symbols_percent", + /* 97 */ "more_keys_for_symbols_exclamation", + /* 98 */ "more_keys_for_symbols_question", + /* 99 */ "more_keys_for_symbols_semicolon", + /* 100 */ "more_keys_for_symbols_percent", + /* 101 */ "keylabel_for_tablet_comma", + /* 102 */ "keyhintlabel_for_tablet_comma", + /* 103 */ "more_keys_for_tablet_comma", + /* 104 */ "keyhintlabel_for_tablet_period", + /* 105 */ "more_keys_for_tablet_period", + /* 106 */ "keylabel_for_apostrophe", + /* 107 */ "keyhintlabel_for_apostrophe", + /* 108 */ "more_keys_for_apostrophe", + /* 109 */ "more_keys_for_q", + /* 110 */ "more_keys_for_x", + /* 111 */ "keylabel_for_q", + /* 112 */ "keylabel_for_w", + /* 113 */ "keylabel_for_y", + /* 114 */ "keylabel_for_x", + /* 115 */ "keylabel_for_spanish_row2_10", + /* 116 */ "more_keys_for_am_pm", + /* 117 */ "settings_as_more_key", + /* 118 */ "shortcut_as_more_key", + /* 119 */ "action_next_as_more_key", + /* 120 */ "action_previous_as_more_key", + /* 121 */ "label_to_more_symbol_key", + /* 122 */ "label_to_more_symbol_for_tablet_key", + /* 123 */ "label_tab_key", + /* 124 */ "label_to_phone_numeric_key", + /* 125 */ "label_to_phone_symbols_key", + /* 126 */ "label_time_am", + /* 127 */ "label_time_pm", + /* 128 */ "label_to_symbol_key_pcqwerty", + /* 129 */ "keylabel_for_popular_domain", + /* 130 */ "more_keys_for_popular_domain", + /* 131 */ "more_keys_for_smiley", + /* 132 */ "single_laqm_raqm", + /* 133 */ "single_laqm_raqm_rtl", + /* 134 */ "single_raqm_laqm", + /* 135 */ "double_laqm_raqm", + /* 136 */ "double_laqm_raqm_rtl", + /* 137 */ "double_raqm_laqm", + /* 138 */ "single_lqm_rqm", + /* 139 */ "single_9qm_lqm", + /* 140 */ "single_9qm_rqm", + /* 141 */ "double_lqm_rqm", + /* 142 */ "double_9qm_lqm", + /* 143 */ "double_9qm_rqm", + /* 144 */ "more_keys_for_single_quote", + /* 145 */ "more_keys_for_double_quote", + /* 146 */ "more_keys_for_tablet_double_quote", }; private static final String EMPTY = ""; @@ -247,155 +263,182 @@ public final class KeyboardTextsSet { /* ~41 */ // Label for "switch to alphabetic" key. /* 42 */ "ABC", - /* 43 */ "!fixedColumnOrder!4,\u2018,\u2019,\u201A,\u201B", - // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK. - // <string name="more_keys_for_double_quote">!fixedColumnOrder!6,“,”,„,‟,«,»</string> - /* 44 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB,\u00BB", - // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK. - // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,“,”,„,‟,«,»,‘,’,‚,‛</string> - /* 45 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B", + /* 43 */ "!text/single_lqm_rqm", + /* 44 */ "!text/double_lqm_rqm", + /* 45 */ "!text/single_laqm_raqm", + /* 46 */ "!text/double_laqm_raqm", // U+00A2: "¢" CENT SIGN // U+00A3: "£" POUND SIGN // U+20AC: "€" EURO SIGN // U+00A5: "¥" YEN SIGN // U+20B1: "₱" PESO SIGN - /* 46 */ "\u00A2,\u00A3,\u20AC,\u00A5,\u20B1", - /* 47 */ "$", - /* 48 */ "$,\u00A2,\u20AC,\u00A3,\u00A5,\u20B1", - /* 49 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\\,,?,@,&,\\%,+,;,/,(,)", + /* 47 */ "\u00A2,\u00A3,\u20AC,\u00A5,\u20B1", + /* 48 */ "$", + /* 49 */ "$,\u00A2,\u20AC,\u00A3,\u00A5,\u20B1", + /* 50 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\\,,?,@,&,\\%,+,;,/,(,)", // U+2020: "†" DAGGER // U+2021: "‡" DOUBLE DAGGER // U+2605: "★" BLACK STAR - /* 50 */ "\u2020,\u2021,\u2605", + /* 51 */ "\u2020,\u2021,\u2605", // U+266A: "♪" EIGHTH NOTE // U+2665: "♥" BLACK HEART SUIT // U+2660: "♠" BLACK SPADE SUIT // U+2666: "♦" BLACK DIAMOND SUIT // U+2663: "♣" BLACK CLUB SUIT - /* 51 */ "\u266A,\u2665,\u2660,\u2666,\u2663", + /* 52 */ "\u266A,\u2665,\u2660,\u2666,\u2663", // U+00B1: "±" PLUS-MINUS SIGN - /* 52 */ "\u00B1", + /* 53 */ "\u00B1", // The all letters need to be mirrored are found at // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt - /* 53 */ "!fixedColumnOrder!3,<,{,[", - /* 54 */ "!fixedColumnOrder!3,>,},]", + /* 54 */ "!fixedColumnOrder!3,<,{,[", + /* 55 */ "!fixedColumnOrder!3,>,},]", // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK // U+2264: "≤" LESS-THAN OR EQUAL TO // U+2265: "≥" GREATER-THAN EQUAL TO // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - // The following characters don't need BIDI mirroring. - // U+2018: "‘" LEFT SINGLE QUOTATION MARK - // U+2019: "’" RIGHT SINGLE QUOTATION MARK - // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK - // U+201B: "‛" SINGLE HIGH-REVERSED-9 QUOTATION MARK - // U+201C: "“" LEFT DOUBLE QUOTATION MARK - // U+201D: "”" RIGHT DOUBLE QUOTATION MARK - // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK - // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK - /* 55 */ "!fixedColumnOrder!3,\u2039,\u2264,\u00AB", - /* 56 */ "!fixedColumnOrder!3,\u203A,\u2265,\u00BB", - /* 57 */ EMPTY, + /* 56 */ "!fixedColumnOrder!3,\u2039,\u2264,\u00AB", + /* 57 */ "!fixedColumnOrder!3,\u203A,\u2265,\u00BB", /* 58 */ EMPTY, - /* 59 */ "1", - /* 60 */ "2", - /* 61 */ "3", - /* 62 */ "4", - /* 63 */ "5", - /* 64 */ "6", - /* 65 */ "7", - /* 66 */ "8", - /* 67 */ "9", - /* 68 */ "0", + /* 59 */ EMPTY, + /* 60 */ "1", + /* 61 */ "2", + /* 62 */ "3", + /* 63 */ "4", + /* 64 */ "5", + /* 65 */ "6", + /* 66 */ "7", + /* 67 */ "8", + /* 68 */ "9", + /* 69 */ "0", // Label for "switch to symbols" key. - /* 69 */ "?123", + /* 70 */ "?123", // Label for "switch to symbols with microphone" key. This string shouldn't include the "mic" // part because it'll be appended by the code. - /* 70 */ "123", - /* 71~ */ + /* 71 */ "123", + /* 72~ */ EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, - /* ~80 */ + /* ~81 */ // U+00B9: "¹" SUPERSCRIPT ONE // U+00BD: "½" VULGAR FRACTION ONE HALF // U+2153: "⅓" VULGAR FRACTION ONE THIRD // U+00BC: "¼" VULGAR FRACTION ONE QUARTER // U+215B: "⅛" VULGAR FRACTION ONE EIGHTH - /* 81 */ "\u00B9,\u00BD,\u2153,\u00BC,\u215B", + /* 82 */ "\u00B9,\u00BD,\u2153,\u00BC,\u215B", // U+00B2: "²" SUPERSCRIPT TWO // U+2154: "⅔" VULGAR FRACTION TWO THIRDS - /* 82 */ "\u00B2,\u2154", + /* 83 */ "\u00B2,\u2154", // U+00B3: "³" SUPERSCRIPT THREE // U+00BE: "¾" VULGAR FRACTION THREE QUARTERS // U+215C: "⅜" VULGAR FRACTION THREE EIGHTHS - /* 83 */ "\u00B3,\u00BE,\u215C", + /* 84 */ "\u00B3,\u00BE,\u215C", // U+2074: "⁴" SUPERSCRIPT FOUR - /* 84 */ "\u2074", + /* 85 */ "\u2074", // U+215D: "⅝" VULGAR FRACTION FIVE EIGHTHS - /* 85 */ "\u215D", - /* 86 */ EMPTY, + /* 86 */ "\u215D", + /* 87 */ EMPTY, // U+215E: "⅞" VULGAR FRACTION SEVEN EIGHTHS - /* 87 */ "\u215E", - /* 88 */ EMPTY, + /* 88 */ "\u215E", /* 89 */ EMPTY, + /* 90 */ EMPTY, // U+207F: "ⁿ" SUPERSCRIPT LATIN SMALL LETTER N // U+2205: "∅" EMPTY SET - /* 90 */ "\u207F,\u2205", - /* 91 */ ",", - /* 92 */ EMPTY, - /* 93 */ "?", - /* 94 */ ";", - /* 95 */ "%", + /* 91 */ "\u207F,\u2205", + /* 92 */ ",", + /* 93 */ EMPTY, + /* 94 */ "?", + /* 95 */ ";", + /* 96 */ "%", // U+00A1: "¡" INVERTED EXCLAMATION MARK - /* 96 */ "\u00A1", + /* 97 */ "\u00A1", // U+00BF: "¿" INVERTED QUESTION MARK - /* 97 */ "\u00BF", - /* 98 */ EMPTY, + /* 98 */ "\u00BF", + /* 99 */ EMPTY, // U+2030: "‰" PER MILLE SIGN - /* 99 */ "\u2030", - /* 100 */ ",", - /* 101 */ "!", + /* 100 */ "\u2030", + /* 101 */ ",", /* 102 */ "!", - /* 103 */ "?", + /* 103 */ "!", /* 104 */ "?", - /* 105 */ "\'", - /* 106 */ "\"", + /* 105 */ "?", + /* 106 */ "\'", /* 107 */ "\"", - /* 108 */ EMPTY, + /* 108 */ "\"", /* 109 */ EMPTY, - /* 110 */ "q", - /* 111 */ "w", - /* 112 */ "y", - /* 113 */ "x", + /* 110 */ EMPTY, + /* 111 */ "q", + /* 112 */ "w", + /* 113 */ "y", + /* 114 */ "x", // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE - /* 114 */ "\u00F1", - /* 115 */ "!fixedColumnOrder!2,!hasLabels!,!text/label_time_am,!text/label_time_pm", - /* 116 */ "!icon/settings_key|!code/key_settings", - /* 117 */ "!icon/shortcut_key|!code/key_shortcut", - /* 118 */ "!hasLabels!,!text/label_next_key|!code/key_action_next", - /* 119 */ "!hasLabels!,!text/label_previous_key|!code/key_action_previous", + /* 115 */ "\u00F1", + /* 116 */ "!fixedColumnOrder!2,!hasLabels!,!text/label_time_am,!text/label_time_pm", + /* 117 */ "!icon/settings_key|!code/key_settings", + /* 118 */ "!icon/shortcut_key|!code/key_shortcut", + /* 119 */ "!hasLabels!,!text/label_next_key|!code/key_action_next", + /* 120 */ "!hasLabels!,!text/label_previous_key|!code/key_action_previous", // Label for "switch to more symbol" modifier key. Must be short to fit on key! - /* 120 */ "= \\ <", + /* 121 */ "= \\ <", // Label for "switch to more symbol" modifier key on tablets. Must be short to fit on key! - /* 121 */ "~ \\ {", + /* 122 */ "~ \\ {", // Label for "Tab" key. Must be short to fit on key! - /* 122 */ "Tab", + /* 123 */ "Tab", // Label for "switch to phone numeric" key. Must be short to fit on key! - /* 123 */ "123", + /* 124 */ "123", // Label for "switch to phone symbols" key. Must be short to fit on key! // U+FF0A: "*" FULLWIDTH ASTERISK // U+FF03: "#" FULLWIDTH NUMBER SIGN - /* 124 */ "\uFF0A\uFF03", + /* 125 */ "\uFF0A\uFF03", // Key label for "ante meridiem" - /* 125 */ "AM", + /* 126 */ "AM", // Key label for "post meridiem" - /* 126 */ "PM", + /* 127 */ "PM", // Label for "switch to symbols" key on PC QWERTY layout - /* 127 */ "Sym", - /* 128 */ ".com", + /* 128 */ "Sym", + /* 129 */ ".com", // popular web domains for the locale - most popular, displayed on the keyboard - /* 129 */ "!hasLabels!,.net,.org,.gov,.edu", - /* 130 */ "!fixedColumnOrder!5,!hasLabels!,=-O|=-O ,:-P|:-P ,;-)|;-) ,:-(|:-( ,:-)|:-) ,:-!|:-! ,:-$|:-$ ,B-)|B-) ,:O|:O ,:-*|:-* ,:-D|:-D ,:\'(|:\'( ,:-\\\\|:-\\\\ ,O:-)|O:-) ,:-[|:-[ ", + /* 130 */ "!hasLabels!,.net,.org,.gov,.edu", + /* 131 */ "!fixedColumnOrder!5,!hasLabels!,=-O|=-O ,:-P|:-P ,;-)|;-) ,:-(|:-( ,:-)|:-) ,:-!|:-! ,:-$|:-$ ,B-)|B-) ,:O|:O ,:-*|:-* ,:-D|:-D ,:\'(|:\'( ,:-\\\\|:-\\\\ ,O:-)|O:-) ,:-[|:-[ ", + // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK + // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + // The following characters don't need BIDI mirroring. + // U+2018: "‘" LEFT SINGLE QUOTATION MARK + // U+2019: "’" RIGHT SINGLE QUOTATION MARK + // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK + // U+201C: "“" LEFT DOUBLE QUOTATION MARK + // U+201D: "”" RIGHT DOUBLE QUOTATION MARK + // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK + // Abbreviations are: + // laqm: LEFT-POINTING ANGLE QUOTATION MARK + // raqm: RIGHT-POINTING ANGLE QUOTATION MARK + // rtl: Right-To-Left script order + // lqm: LEFT QUOTATION MARK + // rqm: RIGHT QUOTATION MARK + // 9qm: LOW-9 QUOTATION MARK + // The following each quotation mark pair consist of + // <opening quotation mark>, <closing quotation mark> + // and is named after (single|double)_<opening quotation mark>_<closing quotation mark>. + /* 132 */ "\u2039,\u203A", + /* 133 */ "\u2039|\u203A,\u203A|\u2039", + /* 134 */ "\u203A,\u2039", + /* 135 */ "\u00AB,\u00BB", + /* 136 */ "\u00AB|\u00BB,\u00BB|\u00AB", + /* 137 */ "\u00BB,\u00AB", + // The following each quotation mark triplet consists of + // <another quotation mark>, <opening quotation mark>, <closing quotation mark> + // and is named after (single|double)_<opening quotation mark>_<closing quotation mark>. + /* 138 */ "\u201A,\u2018,\u2019", + /* 139 */ "\u2019,\u201A,\u2018", + /* 140 */ "\u2018,\u201A,\u2019", + /* 141 */ "\u201E,\u201C,\u201D", + /* 142 */ "\u201D,\u201E,\u201C", + /* 143 */ "\u201C,\u201E,\u201D", + /* 144 */ "!fixedColumnOrder!5,!text/single_quotes,!text/single_angle_quotes", + /* 145 */ "!fixedColumnOrder!5,!text/double_quotes,!text/double_angle_quotes", + /* 146 */ "!fixedColumnOrder!6,!text/double_quotes,!text/single_quotes,!text/double_angle_quotes,!text/single_angle_quotes", }; /* Language af: Afrikaans */ @@ -465,54 +508,36 @@ public final class KeyboardTextsSet { // U+062C: "پ" ARABIC LETTER PEH /* 42 */ "\u0623\u200C\u0628\u200C\u062C", /* 43 */ null, - // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK - // <string name="more_keys_for_double_quote">“,”,„,‟,«|»,»|«</string> - /* 44 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB", - // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK - // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,“,”,„,‟,«|»,»|«;,‘,’,‚,‛</string> - /* 45 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B", - // U+00A2: "¢" CENT SIGN - // U+00A3: "£" POUND SIGN - // U+20AC: "€" EURO SIGN - // U+00A5: "¥" YEN SIGN - // U+20B1: "₱" PESO SIGN - // U+FDFC: "﷼" RIAL SIGN - /* 46 */ "\u00A2,\u00A3,\u20AC,\u00A5,\u20B1,\uFDFC", - /* 47 */ null, - /* 48 */ null, + /* 44 */ null, + /* 45 */ "!text/single_laqm_raqm_rtl", + /* 46 */ "!text/double_laqm_raqm_rtl", + /* 47~ */ + null, null, null, + /* ~49 */ // U+061F: "؟" ARABIC QUESTION MARK // U+060C: "،" ARABIC COMMA // U+061B: "؛" ARABIC SEMICOLON - /* 49 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)", + /* 50 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)", // U+2605: "★" BLACK STAR // U+066D: "٭" ARABIC FIVE POINTED STAR - /* 50 */ "\u2605,\u066D", + /* 51 */ "\u2605,\u066D", // U+266A: "♪" EIGHTH NOTE - /* 51 */ "\u266A", - /* 52 */ null, + /* 52 */ "\u266A", + /* 53 */ null, // The all letters need to be mirrored are found at // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt // U+FD3E: "﴾" ORNATE LEFT PARENTHESIS // U+FD3F: "﴿" ORNATE RIGHT PARENTHESIS - /* 53 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]", - /* 54 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[", + /* 54 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]", + /* 55 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[", // U+2264: "≤" LESS-THAN OR EQUAL TO // U+2265: "≥" GREATER-THAN EQUAL TO // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - // The following characters don't need BIDI mirroring. - // U+2018: "‘" LEFT SINGLE QUOTATION MARK - // U+2019: "’" RIGHT SINGLE QUOTATION MARK - // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK - // U+201B: "‛" SINGLE HIGH-REVERSED-9 QUOTATION MARK - // U+201C: "“" LEFT DOUBLE QUOTATION MARK - // U+201D: "”" RIGHT DOUBLE QUOTATION MARK - // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK - // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK - /* 55 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB", - /* 56 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB", + /* 56 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB", + /* 57 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB", // U+0655: "ٕ" ARABIC HAMZA BELOW // U+0654: "ٔ" ARABIC HAMZA ABOVE // U+0652: "ْ" ARABIC SUKUN @@ -529,70 +554,70 @@ public final class KeyboardTextsSet { // U+0640: "ـ" ARABIC TATWEEL // In order to make Tatweel easily distinguishable from other punctuations, we use consecutive Tatweels only for its displayed label. // Note: The space character is needed as a preceding letter to draw Arabic diacritics characters correctly. - /* 57 */ "!fixedColumnOrder!7, \u0655|\u0655, \u0654|\u0654, \u0652|\u0652, \u064D|\u064D, \u064C|\u064C, \u064B|\u064B, \u0651|\u0651, \u0656|\u0656, \u0670|\u0670, \u0653|\u0653, \u0650|\u0650, \u064F|\u064F, \u064E|\u064E,\u0640\u0640\u0640|\u0640", - /* 58 */ "\u0651", + /* 58 */ "!fixedColumnOrder!7, \u0655|\u0655, \u0654|\u0654, \u0652|\u0652, \u064D|\u064D, \u064C|\u064C, \u064B|\u064B, \u0651|\u0651, \u0656|\u0656, \u0670|\u0670, \u0653|\u0653, \u0650|\u0650, \u064F|\u064F, \u064E|\u064E,\u0640\u0640\u0640|\u0640", + /* 59 */ "\u0651", // U+0661: "١" ARABIC-INDIC DIGIT ONE - /* 59 */ "\u0661", + /* 60 */ "\u0661", // U+0662: "٢" ARABIC-INDIC DIGIT TWO - /* 60 */ "\u0662", + /* 61 */ "\u0662", // U+0663: "٣" ARABIC-INDIC DIGIT THREE - /* 61 */ "\u0663", + /* 62 */ "\u0663", // U+0664: "٤" ARABIC-INDIC DIGIT FOUR - /* 62 */ "\u0664", + /* 63 */ "\u0664", // U+0665: "٥" ARABIC-INDIC DIGIT FIVE - /* 63 */ "\u0665", + /* 64 */ "\u0665", // U+0666: "٦" ARABIC-INDIC DIGIT SIX - /* 64 */ "\u0666", + /* 65 */ "\u0666", // U+0667: "٧" ARABIC-INDIC DIGIT SEVEN - /* 65 */ "\u0667", + /* 66 */ "\u0667", // U+0668: "٨" ARABIC-INDIC DIGIT EIGHT - /* 66 */ "\u0668", + /* 67 */ "\u0668", // U+0669: "٩" ARABIC-INDIC DIGIT NINE - /* 67 */ "\u0669", + /* 68 */ "\u0669", // U+0660: "٠" ARABIC-INDIC DIGIT ZERO - /* 68 */ "\u0660", + /* 69 */ "\u0660", // Label for "switch to symbols" key. // U+061F: "؟" ARABIC QUESTION MARK - /* 69 */ "\u0663\u0662\u0661\u061F", + /* 70 */ "\u0663\u0662\u0661\u061F", // Label for "switch to symbols with microphone" key. This string shouldn't include the "mic" // part because it'll be appended by the code. - /* 70 */ "\u0663\u0662\u0661", - /* 71 */ "1", - /* 72 */ "2", - /* 73 */ "3", - /* 74 */ "4", - /* 75 */ "5", - /* 76 */ "6", - /* 77 */ "7", - /* 78 */ "8", - /* 79 */ "9", + /* 71 */ "\u0663\u0662\u0661", + /* 72 */ "1", + /* 73 */ "2", + /* 74 */ "3", + /* 75 */ "4", + /* 76 */ "5", + /* 77 */ "6", + /* 78 */ "7", + /* 79 */ "8", + /* 80 */ "9", // U+066B: "٫" ARABIC DECIMAL SEPARATOR // U+066C: "٬" ARABIC THOUSANDS SEPARATOR - /* 80 */ "0,\u066B,\u066C", - /* 81~ */ + /* 81 */ "0,\u066B,\u066C", + /* 82~ */ null, null, null, null, null, null, null, null, null, null, - /* ~90 */ + /* ~91 */ // U+060C: "،" ARABIC COMMA - /* 91 */ "\u060C", - /* 92 */ "\\,", - /* 93 */ "\u061F", - /* 94 */ "\u061B", + /* 92 */ "\u060C", + /* 93 */ "\\,", + /* 94 */ "\u061F", + /* 95 */ "\u061B", // U+066A: "٪" ARABIC PERCENT SIGN - /* 95 */ "\u066A", - /* 96 */ null, - /* 97 */ "?", - /* 98 */ ";", + /* 96 */ "\u066A", + /* 97 */ null, + /* 98 */ "?", + /* 99 */ ";", // U+2030: "‰" PER MILLE SIGN - /* 99 */ "\\%,\u2030", - /* 100~ */ + /* 100 */ "\\%,\u2030", + /* 101~ */ null, null, null, null, null, - /* ~104 */ + /* ~105 */ // U+060C: "،" ARABIC COMMA // U+061B: "؛" ARABIC SEMICOLON // U+061F: "؟" ARABIC QUESTION MARK - /* 105 */ "\u060C", - /* 106 */ "\u061F", - /* 107 */ "\u061F,\u061B,!,:,-,/,\',\"", + /* 106 */ "\u060C", + /* 107 */ "\u061F", + /* 108 */ "\u061F,\u061B,!,:,-,/,\',\"", }; /* Language be: Belarusian */ @@ -627,6 +652,8 @@ public final class KeyboardTextsSet { // U+0411: "Б" CYRILLIC CAPITAL LETTER BE // U+0412: "В" CYRILLIC CAPITAL LETTER VE /* 42 */ "\u0410\u0411\u0412", + /* 43 */ "!text/single_9qm_lqm", + /* 44 */ "!text/double_9qm_lqm", }; /* Language bg: Bulgarian */ @@ -641,6 +668,9 @@ public final class KeyboardTextsSet { // U+0411: "Б" CYRILLIC CAPITAL LETTER BE // U+0412: "В" CYRILLIC CAPITAL LETTER VE /* 42 */ "\u0410\u0411\u0412", + /* 43 */ null, + // single_quotes of Bulgarian is default single_quotes_right_left. + /* 44 */ "!text/double_9qm_lqm", }; /* Language ca: Catalan */ @@ -771,6 +801,14 @@ public final class KeyboardTextsSet { // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE /* 12 */ "\u017E,\u017A,\u017C", + /* 13~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~42 */ + /* 43 */ "!text/single_9qm_lqm", + /* 44 */ "!text/double_9qm_lqm", + /* 45 */ "!text/single_raqm_laqm", + /* 46 */ "!text/double_raqm_laqm", }; /* Language da: Danish */ @@ -832,6 +870,14 @@ public final class KeyboardTextsSet { /* 23 */ "\u00E4", // U+00F6: "ö" LATIN SMALL LETTER O WITH DIAERESIS /* 24 */ "\u00F6", + /* 25~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, + /* ~42 */ + /* 43 */ "!text/single_9qm_lqm", + /* 44 */ "!text/double_9qm_lqm", + /* 45 */ "!text/single_raqm_laqm", + /* 46 */ "!text/double_raqm_laqm", }; /* Language de: German */ @@ -874,6 +920,15 @@ public final class KeyboardTextsSet { // U+00F1: "ñ" LATIN SMALL LETTER N WITH TILDE // U+0144: "ń" LATIN SMALL LETTER N WITH ACUTE /* 6 */ "\u00F1,\u0144", + /* 7~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, + /* ~42 */ + /* 43 */ "!text/single_9qm_lqm", + /* 44 */ "!text/double_9qm_lqm", + /* 45 */ "!text/single_raqm_laqm", + /* 46 */ "!text/double_raqm_laqm", }; /* Language el: Greek */ @@ -1058,20 +1113,20 @@ public final class KeyboardTextsSet { null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~107 */ - /* 108 */ "q", - /* 109 */ "x", + null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~108 */ + /* 109 */ "q", + /* 110 */ "x", // U+015D: "ŝ" LATIN SMALL LETTER S WITH CIRCUMFLEX - /* 110 */ "\u015D", + /* 111 */ "\u015D", // U+011D: "ĝ" LATIN SMALL LETTER G WITH CIRCUMFLEX - /* 111 */ "\u011D", + /* 112 */ "\u011D", // U+016D: "ŭ" LATIN SMALL LETTER U WITH BREVE - /* 112 */ "\u016D", + /* 113 */ "\u016D", // U+0109: "ĉ" LATIN SMALL LETTER C WITH CIRCUMFLEX - /* 113 */ "\u0109", + /* 114 */ "\u0109", // U+0135: "ĵ" LATIN SMALL LETTER J WITH CIRCUMFLEX - /* 114 */ "\u0135", + /* 115 */ "\u0135", }; /* Language es: Spanish */ @@ -1129,25 +1184,25 @@ public final class KeyboardTextsSet { /* 8~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, null, null, null, null, - /* ~48 */ + null, null, null, null, null, null, null, null, null, null, null, null, + /* ~49 */ // U+00A1: "¡" INVERTED EXCLAMATION MARK // U+00BF: "¿" INVERTED QUESTION MARK - /* 49 */ "!fixedColumnOrder!9,\u00A1,\",\',#,-,:,!,\\,,?,\u00BF,@,&,\\%,+,;,/,(,)", - /* 50~ */ + /* 50 */ "!fixedColumnOrder!9,\u00A1,\",\',#,-,:,!,\\,,?,\u00BF,@,&,\\%,+,;,/,(,)", + /* 51~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - /* ~101 */ + /* ~102 */ // U+00A1: "¡" INVERTED EXCLAMATION MARK - /* 102 */ "!,\u00A1", - /* 103 */ null, + /* 103 */ "!,\u00A1", + /* 104 */ null, // U+00BF: "¿" INVERTED QUESTION MARK - /* 104 */ "?,\u00BF", - /* 105 */ "\"", - /* 106 */ "\'", + /* 105 */ "?,\u00BF", + /* 106 */ "\"", /* 107 */ "\'", + /* 108 */ "\'", }; /* Language et: Estonian */ @@ -1248,6 +1303,12 @@ public final class KeyboardTextsSet { /* 22 */ "\u00E4", // U+00F5: "õ" LATIN SMALL LETTER O WITH TILDE /* 23 */ "\u00F5", + /* 24~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, + /* ~42 */ + /* 43 */ "!text/single_9qm_lqm", + /* 44 */ "!text/double_9qm_lqm", }; /* Language fa: Persian */ @@ -1264,55 +1325,36 @@ public final class KeyboardTextsSet { // U+067E: "پ" ARABIC LETTER PEH /* 42 */ "\u0627\u200C\u0628\u200C\u067E", /* 43 */ null, - // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK - // <string name="more_keys_for_double_quote">“,”,„,‟,«|»,»|«</string> - /* 44 */ "!fixedColumnOrder!4,\u201C,\u201D,\",\'", - // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK - // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,“,”,„,‟,«|»,»|«;,‘,’,‚,‛</string> - /* 45 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B", - /* 46 */ null, - // U+FDFC: "﷼" RIAL SIGN - // U+060B: "؋" AFGHANI SIGN - // U+00A2: "¢" CENT SIGN - // U+00A3: "£" POUND SIGN - // U+20AC: "€" EURO SIGN - // U+00A5: "¥" YEN SIGN - // U+20B1: "₱" PESO SIGN - /* 47 */ "\uFDFC", - /* 48 */ "$,\u00A2,\u20AC,\u00A3,\u00A5,\u20B1,\u060B", + /* 44 */ null, + /* 45 */ "!text/single_laqm_raqm_rtl", + /* 46 */ "!text/double_laqm_raqm_rtl", + /* 47~ */ + null, null, null, + /* ~49 */ // U+061F: "؟" ARABIC QUESTION MARK // U+060C: "،" ARABIC COMMA // U+061B: "؛" ARABIC SEMICOLON - /* 49 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)", + /* 50 */ "!fixedColumnOrder!8,\",\',#,-,:,!,\u060C,\u061F,@,&,\\%,+,\u061B,/,(,)", // U+2605: "★" BLACK STAR // U+066D: "٭" ARABIC FIVE POINTED STAR - /* 50 */ "\u2605,\u066D", + /* 51 */ "\u2605,\u066D", // U+266A: "♪" EIGHTH NOTE - /* 51 */ "\u266A", - /* 52 */ null, + /* 52 */ "\u266A", + /* 53 */ null, // The all letters need to be mirrored are found at // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt // U+FD3E: "﴾" ORNATE LEFT PARENTHESIS // U+FD3F: "﴿" ORNATE RIGHT PARENTHESIS - /* 53 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]", - /* 54 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[", + /* 54 */ "!fixedColumnOrder!4,\uFD3E|\uFD3F,<|>,{|},[|]", + /* 55 */ "!fixedColumnOrder!4,\uFD3F|\uFD3E,>|<,}|{,]|[", // U+2264: "≤" LESS-THAN OR EQUAL TO // U+2265: "≥" GREATER-THAN EQUAL TO // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - // The following characters don't need BIDI mirroring. - // U+2018: "‘" LEFT SINGLE QUOTATION MARK - // U+2019: "’" RIGHT SINGLE QUOTATION MARK - // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK - // U+201B: "‛" SINGLE HIGH-REVERSED-9 QUOTATION MARK - // U+201C: "“" LEFT DOUBLE QUOTATION MARK - // U+201D: "”" RIGHT DOUBLE QUOTATION MARK - // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK - // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK - /* 55 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,<|>", - /* 56 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,>|<", + /* 56 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,<|>", + /* 57 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,>|<", // U+0655: "ٕ" ARABIC HAMZA BELOW // U+0652: "ْ" ARABIC SUKUN // U+0651: "ّ" ARABIC SHADDA @@ -1329,74 +1371,74 @@ public final class KeyboardTextsSet { // U+0640: "ـ" ARABIC TATWEEL // In order to make Tatweel easily distinguishable from other punctuations, we use consecutive Tatweels only for its displayed label. // Note: The space character is needed as a preceding letter to draw Arabic diacritics characters correctly. - /* 57 */ "!fixedColumnOrder!7, \u0655|\u0655, \u0652|\u0652, \u0651|\u0651, \u064C|\u064C, \u064D|\u064D, \u064B|\u064B, \u0654|\u0654, \u0656|\u0656, \u0670|\u0670, \u0653|\u0653, \u064F|\u064F, \u0650|\u0650, \u064E|\u064E,\u0640\u0640\u0640|\u0640", - /* 58 */ "\u064B", + /* 58 */ "!fixedColumnOrder!7, \u0655|\u0655, \u0652|\u0652, \u0651|\u0651, \u064C|\u064C, \u064D|\u064D, \u064B|\u064B, \u0654|\u0654, \u0656|\u0656, \u0670|\u0670, \u0653|\u0653, \u064F|\u064F, \u0650|\u0650, \u064E|\u064E,\u0640\u0640\u0640|\u0640", + /* 59 */ "\u064B", // U+06F1: "۱" EXTENDED ARABIC-INDIC DIGIT ONE - /* 59 */ "\u06F1", + /* 60 */ "\u06F1", // U+06F2: "۲" EXTENDED ARABIC-INDIC DIGIT TWO - /* 60 */ "\u06F2", + /* 61 */ "\u06F2", // U+06F3: "۳" EXTENDED ARABIC-INDIC DIGIT THREE - /* 61 */ "\u06F3", + /* 62 */ "\u06F3", // U+06F4: "۴" EXTENDED ARABIC-INDIC DIGIT FOUR - /* 62 */ "\u06F4", + /* 63 */ "\u06F4", // U+06F5: "۵" EXTENDED ARABIC-INDIC DIGIT FIVE - /* 63 */ "\u06F5", + /* 64 */ "\u06F5", // U+06F6: "۶" EXTENDED ARABIC-INDIC DIGIT SIX - /* 64 */ "\u06F6", + /* 65 */ "\u06F6", // U+06F7: "۷" EXTENDED ARABIC-INDIC DIGIT SEVEN - /* 65 */ "\u06F7", + /* 66 */ "\u06F7", // U+06F8: "۸" EXTENDED ARABIC-INDIC DIGIT EIGHT - /* 66 */ "\u06F8", + /* 67 */ "\u06F8", // U+06F9: "۹" EXTENDED ARABIC-INDIC DIGIT NINE - /* 67 */ "\u06F9", + /* 68 */ "\u06F9", // U+06F0: "۰" EXTENDED ARABIC-INDIC DIGIT ZERO - /* 68 */ "\u06F0", + /* 69 */ "\u06F0", // Label for "switch to symbols" key. // U+061F: "؟" ARABIC QUESTION MARK - /* 69 */ "\u06F3\u06F2\u06F1\u061F", + /* 70 */ "\u06F3\u06F2\u06F1\u061F", // Label for "switch to symbols with microphone" key. This string shouldn't include the "mic" // part because it'll be appended by the code. - /* 70 */ "\u06F3\u06F2\u06F1", - /* 71 */ "1", - /* 72 */ "2", - /* 73 */ "3", - /* 74 */ "4", - /* 75 */ "5", - /* 76 */ "6", - /* 77 */ "7", - /* 78 */ "8", - /* 79 */ "9", + /* 71 */ "\u06F3\u06F2\u06F1", + /* 72 */ "1", + /* 73 */ "2", + /* 74 */ "3", + /* 75 */ "4", + /* 76 */ "5", + /* 77 */ "6", + /* 78 */ "7", + /* 79 */ "8", + /* 80 */ "9", // U+066B: "٫" ARABIC DECIMAL SEPARATOR // U+066C: "٬" ARABIC THOUSANDS SEPARATOR - /* 80 */ "0,\u066B,\u066C", - /* 81~ */ + /* 81 */ "0,\u066B,\u066C", + /* 82~ */ null, null, null, null, null, null, null, null, null, null, - /* ~90 */ + /* ~91 */ // U+060C: "،" ARABIC COMMA - /* 91 */ "\u060C", - /* 92 */ "\\,", - /* 93 */ "\u061F", - /* 94 */ "\u061B", + /* 92 */ "\u060C", + /* 93 */ "\\,", + /* 94 */ "\u061F", + /* 95 */ "\u061B", // U+066A: "٪" ARABIC PERCENT SIGN - /* 95 */ "\u066A", - /* 96 */ null, - /* 97 */ "?", - /* 98 */ ";", + /* 96 */ "\u066A", + /* 97 */ null, + /* 98 */ "?", + /* 99 */ ";", // U+2030: "‰" PER MILLE SIGN - /* 99 */ "\\%,\u2030", + /* 100 */ "\\%,\u2030", // U+060C: "،" ARABIC COMMA // U+061B: "؛" ARABIC SEMICOLON // U+061F: "؟" ARABIC QUESTION MARK // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK - /* 100 */ "\u060C", - /* 101 */ "!", - /* 102 */ "!,\\,", - /* 103 */ "\u061F", - /* 104 */ "\u061F,?", - /* 105 */ "\u060C", - /* 106 */ "\u061F", - /* 107 */ "!fixedColumnOrder!4,:,!,\u061F,\u061B,-,/,\u00AB|\u00BB,\u00BB|\u00AB", + /* 101 */ "\u060C", + /* 102 */ "!", + /* 103 */ "!,\\,", + /* 104 */ "\u061F", + /* 105 */ "\u061F,?", + /* 106 */ "\u060C", + /* 107 */ "\u061F", + /* 108 */ "!fixedColumnOrder!4,:,!,\u061F,\u061B,-,/,\u00AB|\u00BB,\u00BB|\u00AB", }; /* Language fi: Finnish */ @@ -1512,48 +1554,48 @@ public final class KeyboardTextsSet { // U+0917: "ग" DEVANAGARI LETTER GA /* 42 */ "\u0915\u0916\u0917", /* 43~ */ - null, null, null, null, - /* ~46 */ + null, null, null, null, null, + /* ~47 */ // U+20B9: "₹" INDIAN RUPEE SIGN - /* 47 */ "\u20B9", - /* 48~ */ + /* 48 */ "\u20B9", + /* 49~ */ null, null, null, null, null, null, null, null, null, null, null, - /* ~58 */ + /* ~59 */ // U+0967: "१" DEVANAGARI DIGIT ONE - /* 59 */ "\u0967", + /* 60 */ "\u0967", // U+0968: "२" DEVANAGARI DIGIT TWO - /* 60 */ "\u0968", + /* 61 */ "\u0968", // U+0969: "३" DEVANAGARI DIGIT THREE - /* 61 */ "\u0969", + /* 62 */ "\u0969", // U+096A: "४" DEVANAGARI DIGIT FOUR - /* 62 */ "\u096A", + /* 63 */ "\u096A", // U+096B: "५" DEVANAGARI DIGIT FIVE - /* 63 */ "\u096B", + /* 64 */ "\u096B", // U+096C: "६" DEVANAGARI DIGIT SIX - /* 64 */ "\u096C", + /* 65 */ "\u096C", // U+096D: "७" DEVANAGARI DIGIT SEVEN - /* 65 */ "\u096D", + /* 66 */ "\u096D", // U+096E: "८" DEVANAGARI DIGIT EIGHT - /* 66 */ "\u096E", + /* 67 */ "\u096E", // U+096F: "९" DEVANAGARI DIGIT NINE - /* 67 */ "\u096F", + /* 68 */ "\u096F", // U+0966: "०" DEVANAGARI DIGIT ZERO - /* 68 */ "\u0966", + /* 69 */ "\u0966", // Label for "switch to symbols" key. - /* 69 */ "?\u0967\u0968\u0969", + /* 70 */ "?\u0967\u0968\u0969", // Label for "switch to symbols with microphone" key. This string shouldn't include the "mic" // part because it'll be appended by the code. - /* 70 */ "\u0967\u0968\u0969", - /* 71 */ "1", - /* 72 */ "2", - /* 73 */ "3", - /* 74 */ "4", - /* 75 */ "5", - /* 76 */ "6", - /* 77 */ "7", - /* 78 */ "8", - /* 79 */ "9", - /* 80 */ "0", + /* 71 */ "\u0967\u0968\u0969", + /* 72 */ "1", + /* 73 */ "2", + /* 74 */ "3", + /* 75 */ "4", + /* 76 */ "5", + /* 77 */ "6", + /* 78 */ "7", + /* 79 */ "8", + /* 80 */ "9", + /* 81 */ "0", }; /* Language hr: Croatian */ @@ -1581,6 +1623,14 @@ public final class KeyboardTextsSet { // U+017A: "ź" LATIN SMALL LETTER Z WITH ACUTE // U+017C: "ż" LATIN SMALL LETTER Z WITH DOT ABOVE /* 12 */ "\u017E,\u017A,\u017C", + /* 13~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~42 */ + /* 43 */ "!text/single_9qm_rqm", + /* 44 */ "!text/double_9qm_rqm", + /* 45 */ "!text/single_raqm_laqm", + /* 46 */ "!text/double_raqm_laqm", }; /* Language hu: Hungarian */ @@ -1626,6 +1676,15 @@ public final class KeyboardTextsSet { // U+00F9: "ù" LATIN SMALL LETTER U WITH GRAVE // U+016B: "ū" LATIN SMALL LETTER U WITH MACRON /* 4 */ "\u00FA,\u00FC,\u0171,\u00FB,\u00F9,\u016B", + /* 5~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, + /* ~42 */ + /* 43 */ "!text/single_9qm_rqm", + /* 44 */ "!text/double_9qm_rqm", + /* 45 */ "!text/single_raqm_laqm", + /* 46 */ "!text/double_raqm_laqm", }; /* Language is: Icelandic */ @@ -1689,6 +1748,12 @@ public final class KeyboardTextsSet { /* 21 */ "\u00E6", // U+00FE: "þ" LATIN SMALL LETTER THORN /* 22 */ "\u00FE", + /* 23~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, + /* ~42 */ + /* 43 */ "!text/single_9qm_lqm", + /* 44 */ "!text/double_9qm_lqm", }; /* Language it: Italian */ @@ -1748,45 +1813,38 @@ public final class KeyboardTextsSet { // U+05D1: "ב" HEBREW LETTER BET // U+05D2: "ג" HEBREW LETTER GIMEL /* 42 */ "\u05D0\u05D1\u05D2", - /* 43 */ null, - // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK - // <string name="more_keys_for_double_quote">“,”,„,‟,«|»,»|«</string> - /* 44 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB", - // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK - // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,“,”,„,‟,«|»,»|«;,‘,’,‚,‛</string> - /* 45 */ "!fixedColumnOrder!4,\u201C,\u201D,\u00AB|\u00BB,\u00BB|\u00AB,\u2018,\u2019,\u201A,\u201B", - /* 46 */ null, - // U+20AA: "₪" NEW SHEQEL SIGN - /* 47 */ "\u20AA", - /* 48 */ null, - /* 49 */ null, + // The following characters don't need BIDI mirroring. + // U+2018: "‘" LEFT SINGLE QUOTATION MARK + // U+2019: "’" RIGHT SINGLE QUOTATION MARK + // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK + // U+201C: "“" LEFT DOUBLE QUOTATION MARK + // U+201D: "”" RIGHT DOUBLE QUOTATION MARK + // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK + /* 43 */ "\u2018,\u2019,\u201A", + /* 44 */ "\u201C,\u201D,\u201E", + /* 45 */ "!text/single_laqm_raqm_rtl", + /* 46 */ "!text/double_laqm_raqm_rtl", + /* 47~ */ + null, null, null, null, + /* ~50 */ // U+2605: "★" BLACK STAR - /* 50 */ "\u2605", - /* 51 */ null, + /* 51 */ "\u2605", + /* 52 */ null, // U+00B1: "±" PLUS-MINUS SIGN // U+FB29: "﬩" HEBREW LETTER ALTERNATIVE PLUS SIGN - /* 52 */ "\u00B1,\uFB29", + /* 53 */ "\u00B1,\uFB29", // The all letters need to be mirrored are found at // http://www.unicode.org/Public/6.1.0/ucd/BidiMirroring.txt - /* 53 */ "!fixedColumnOrder!3,<|>,{|},[|]", - /* 54 */ "!fixedColumnOrder!3,>|<,}|{,]|[", + /* 54 */ "!fixedColumnOrder!3,<|>,{|},[|]", + /* 55 */ "!fixedColumnOrder!3,>|<,}|{,]|[", // U+2264: "≤" LESS-THAN OR EQUAL TO // U+2265: "≥" GREATER-THAN EQUAL TO // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK // U+2039: "‹" SINGLE LEFT-POINTING ANGLE QUOTATION MARK // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK - // The following characters don't need BIDI mirroring. - // U+2018: "‘" LEFT SINGLE QUOTATION MARK - // U+2019: "’" RIGHT SINGLE QUOTATION MARK - // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK - // U+201B: "‛" SINGLE HIGH-REVERSED-9 QUOTATION MARK - // U+201C: "“" LEFT DOUBLE QUOTATION MARK - // U+201D: "”" RIGHT DOUBLE QUOTATION MARK - // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK - // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK - /* 55 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB", - /* 56 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB", + /* 56 */ "!fixedColumnOrder!3,\u2039|\u203A,\u2264|\u2265,\u00AB|\u00BB", + /* 57 */ "!fixedColumnOrder!3,\u203A|\u2039,\u2265|\u2264,\u00BB|\u00AB", }; /* Language ka: Georgian */ @@ -1801,6 +1859,8 @@ public final class KeyboardTextsSet { // U+10D1: "ბ" GEORGIAN LETTER BAN // U+10D2: "გ" GEORGIAN LETTER GAN /* 42 */ "\u10D0\u10D1\u10D2", + /* 43 */ "!text/single_9qm_lqm", + /* 44 */ "!text/double_9qm_lqm", }; /* Language ky: Kirghiz */ @@ -1930,6 +1990,12 @@ public final class KeyboardTextsSet { // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE /* 15 */ "\u0123,\u011F", + /* 16~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, + /* ~42 */ + /* 43 */ "!text/single_9qm_lqm", + /* 44 */ "!text/double_9qm_lqm", }; /* Language lv: Latvian */ @@ -2019,6 +2085,12 @@ public final class KeyboardTextsSet { // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE /* 15 */ "\u0123,\u011F", + /* 16~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, + /* ~42 */ + /* 43 */ "!text/single_9qm_lqm", + /* 44 */ "!text/double_9qm_lqm", }; /* Language mk: Macedonian */ @@ -2045,21 +2117,8 @@ public final class KeyboardTextsSet { // U+0411: "Б" CYRILLIC CAPITAL LETTER BE // U+0412: "В" CYRILLIC CAPITAL LETTER VE /* 42 */ "\u0410\u0411\u0412", - /* 43 */ null, - // U+2018: "‘" LEFT SINGLE QUOTATION MARK - // U+2019: "’" RIGHT SINGLE QUOTATION MARK - // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK - // U+201B: "‛" SINGLE HIGH-REVERSED-9 QUOTATION MARK - // U+201C: "“" LEFT DOUBLE QUOTATION MARK - // U+201D: "”" RIGHT DOUBLE QUOTATION MARK - // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK - // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK - // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK. - // <string name="more_keys_for_double_quote">!fixedColumnOrder!6,„,“,”,‟,«,»</string> - /* 44 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB", - // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK. - // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,“,”,„,‟,«,»,‘,’,‚,‛</string> - /* 45 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B", + /* 43 */ "!text/single_9qm_lqm", + /* 44 */ "!text/double_9qm_lqm", }; /* Language mn: Mongolian */ @@ -2075,10 +2134,10 @@ public final class KeyboardTextsSet { // U+0412: "В" CYRILLIC CAPITAL LETTER VE /* 42 */ "\u0410\u0411\u0412", /* 43~ */ - null, null, null, null, - /* ~46 */ + null, null, null, null, null, + /* ~47 */ // U+20AE: "₮" TUGRIK SIGN - /* 47 */ "\u20AE", + /* 48 */ "\u20AE", }; /* Language nb: Norwegian Bokmål */ @@ -2126,6 +2185,12 @@ public final class KeyboardTextsSet { /* 23 */ "\u00F6", // U+00E4: "ä" LATIN SMALL LETTER A WITH DIAERESIS /* 24 */ "\u00E4", + /* 25~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, + /* ~42 */ + /* 43 */ "!text/single_9qm_rqm", + /* 44 */ "!text/double_9qm_rqm", }; /* Language nl: Dutch */ @@ -2177,6 +2242,13 @@ public final class KeyboardTextsSet { /* 7 */ null, // U+0133: "ij" LATIN SMALL LIGATURE IJ /* 8 */ "\u0133", + /* 9~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, + /* ~42 */ + /* 43 */ "!text/single_9qm_rqm", + /* 44 */ "!text/double_9qm_rqm", }; /* Language pl: Polish */ @@ -2231,6 +2303,12 @@ public final class KeyboardTextsSet { /* 13 */ null, // U+0142: "ł" LATIN SMALL LETTER L WITH STROKE /* 14 */ "\u0142", + /* 15~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~42 */ + /* 43 */ "!text/single_9qm_rqm", + /* 44 */ "!text/double_9qm_rqm", }; /* Language pt: Portuguese */ @@ -2330,6 +2408,13 @@ public final class KeyboardTextsSet { /* ~10 */ // U+021B: "ț" LATIN SMALL LETTER T WITH COMMA BELOW /* 11 */ "\u021B", + /* 12~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, + /* ~42 */ + /* 43 */ "!text/single_9qm_rqm", + /* 44 */ "!text/double_9qm_rqm", }; /* Language ru: Russian */ @@ -2364,6 +2449,8 @@ public final class KeyboardTextsSet { // U+0411: "Б" CYRILLIC CAPITAL LETTER BE // U+0412: "В" CYRILLIC CAPITAL LETTER VE /* 42 */ "\u0410\u0411\u0412", + /* 43 */ "!text/single_9qm_lqm", + /* 44 */ "!text/double_9qm_lqm", }; /* Language sk: Slovak */ @@ -2454,6 +2541,14 @@ public final class KeyboardTextsSet { // U+0123: "ģ" LATIN SMALL LETTER G WITH CEDILLA // U+011F: "ğ" LATIN SMALL LETTER G WITH BREVE /* 15 */ "\u0123,\u011F", + /* 16~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, + /* ~42 */ + /* 43 */ "!text/single_9qm_lqm", + /* 44 */ "!text/double_9qm_lqm", + /* 45 */ "!text/single_raqm_laqm", + /* 46 */ "!text/double_raqm_laqm", }; /* Language sl: Slovenian */ @@ -2474,6 +2569,14 @@ public final class KeyboardTextsSet { /* 11 */ null, // U+017E: "ž" LATIN SMALL LETTER Z WITH CARON /* 12 */ "\u017E", + /* 13~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + /* ~42 */ + /* 43 */ "!text/single_9qm_lqm", + /* 44 */ "!text/double_9qm_lqm", + /* 45 */ "!text/single_raqm_laqm", + /* 46 */ "!text/double_raqm_laqm", }; /* Language sr: Serbian */ @@ -2519,21 +2622,10 @@ public final class KeyboardTextsSet { // U+0411: "Б" CYRILLIC CAPITAL LETTER BE // U+0412: "В" CYRILLIC CAPITAL LETTER VE /* 42 */ "\u0410\u0411\u0412", - /* 43 */ null, - // U+2018: "‘" LEFT SINGLE QUOTATION MARK - // U+2019: "’" RIGHT SINGLE QUOTATION MARK - // U+201A: "‚" SINGLE LOW-9 QUOTATION MARK - // U+201B: "‛" SINGLE HIGH-REVERSED-9 QUOTATION MARK - // U+201C: "“" LEFT DOUBLE QUOTATION MARK - // U+201D: "”" RIGHT DOUBLE QUOTATION MARK - // U+201E: "„" DOUBLE LOW-9 QUOTATION MARK - // U+201F: "‟" DOUBLE HIGH-REVERSED-9 QUOTATION MARK - // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK. - // <string name="more_keys_for_double_quote">!fixedColumnOrder!6,„,“,”,‟,«,»</string> - /* 44 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB", - // TODO: Neither DroidSans nor Roboto have the glyph for U+201F DOUBLE HIGH-REVERSED-9 QUOTATION MARK. - // <string name="more_keys_for_tablet_double_quote">!fixedColumnOrder!6,“,”,„,‟,«,»,‘,’,‚,‛</string> - /* 45 */ "!fixedColumnOrder!5,\u201E,\u201C,\u201D,\u00AB,\u00BB,\u2018,\u2019,\u201A,\u201B", + /* 43 */ "!text/single_9qm_lqm", + /* 44 */ "!text/double_9qm_lqm", + /* 45 */ "!text/single_raqm_laqm", + /* 46 */ "!text/double_raqm_laqm", }; /* Language sv: Swedish */ @@ -2576,6 +2668,12 @@ public final class KeyboardTextsSet { /* 23 */ "\u00F8", // U+00E6: "æ" LATIN SMALL LETTER AE /* 24 */ "\u00E6", + /* 25~ */ + null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, + null, null, null, null, null, + /* ~44 */ + /* 45 */ "!text/single_raqm_laqm", + /* 46 */ "!text/double_raqm_laqm", }; /* Language sw: Swahili */ @@ -2642,10 +2740,10 @@ public final class KeyboardTextsSet { // U+0E04: "ค" THAI CHARACTER KHO KHWAI /* 42 */ "\u0E01\u0E02\u0E04", /* 43~ */ - null, null, null, null, - /* ~46 */ + null, null, null, null, null, + /* ~47 */ // U+0E3F: "฿" THAI CURRENCY SYMBOL BAHT - /* 47 */ "\u0E3F", + /* 48 */ "\u0E3F", }; /* Language tl: Tagalog */ @@ -2780,11 +2878,13 @@ public final class KeyboardTextsSet { // U+0411: "Б" CYRILLIC CAPITAL LETTER BE // U+0412: "В" CYRILLIC CAPITAL LETTER VE /* 42 */ "\u0410\u0411\u0412", - /* 43~ */ - null, null, null, null, - /* ~46 */ + /* 43 */ "!text/single_9qm_lqm", + /* 44 */ "!text/double_9qm_lqm", + /* 45~ */ + null, null, null, + /* ~47 */ // U+20B4: "₴" HRYVNIA SIGN - /* 47 */ "\u20B4", + /* 48 */ "\u20B4", }; /* Language vi: Vietnamese */ @@ -2869,10 +2969,10 @@ public final class KeyboardTextsSet { /* 10~ */ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, - null, null, null, null, null, null, null, - /* ~46 */ + null, null, null, null, null, null, null, null, + /* ~47 */ // U+20AB: "₫" DONG SIGN - /* 47 */ "\u20AB", + /* 48 */ "\u20AB", }; /* Language zu: Zulu */ diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java index ad3163347..ab2a12fd0 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java @@ -16,7 +16,6 @@ package com.android.inputmethod.latin; -import android.content.Context; import android.text.TextUtils; import android.util.SparseArray; diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java index d9d664fb5..0d0ce5756 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java @@ -74,6 +74,8 @@ public final class BinaryDictionaryFileDumper { // The path fragment to append after the client ID for dictionary info requests. private static final String QUERY_PATH_DICT_INFO = "dict"; + // The path fragment to append after the client ID for dictionary datafile requests. + private static final String QUERY_PATH_DATAFILE = "datafile"; // The path fragment to append after the client ID for updating the metadata URI. private static final String QUERY_PATH_METADATA = "metadata"; private static final String INSERT_METADATA_CLIENT_ID_COLUMN = "clientid"; @@ -156,7 +158,7 @@ public final class BinaryDictionaryFileDumper { c.close(); return Collections.<WordListInfo>emptyList(); } - final List<WordListInfo> list = CollectionUtils.newArrayList(); + final ArrayList<WordListInfo> list = CollectionUtils.newArrayList(); do { final String wordListId = c.getString(0); final String wordListLocale = c.getString(1); @@ -186,13 +188,18 @@ public final class BinaryDictionaryFileDumper { /** * Helper method to encapsulate exception handling. */ - private static AssetFileDescriptor openAssetFileDescriptor(final ContentResolver resolver, - final Uri uri) { + private static AssetFileDescriptor openAssetFileDescriptor( + final ContentProviderClient providerClient, final Uri uri) { try { - return resolver.openAssetFileDescriptor(uri, "r"); + return providerClient.openAssetFile(uri, "r"); } catch (FileNotFoundException e) { - // I don't want to log the word list URI here for security concerns - Log.e(TAG, "Could not find a word list from the dictionary provider."); + // I don't want to log the word list URI here for security concerns. The exception + // contains the name of the file, so let's not pass it to Log.e here. + Log.e(TAG, "Could not find a word list from the dictionary provider." + /* intentionally don't pass the exception (see comment above) */); + return null; + } catch (RemoteException e) { + Log.e(TAG, "Can't communicate with the dictionary pack", e); return null; } } @@ -202,9 +209,8 @@ public final class BinaryDictionaryFileDumper { * to the cache file name designated by its id and locale, overwriting it if already present * and creating it (and its containing directory) if necessary. */ - private static AssetFileAddress cacheWordList(final String id, final String locale, - final ContentResolver resolver, final Context context) { - + private static AssetFileAddress cacheWordList(final String wordlistId, final String locale, + final ContentProviderClient providerClient, final Context context) { final int COMPRESSED_CRYPTED_COMPRESSED = 0; final int CRYPTED_COMPRESSED = 1; final int COMPRESSED_CRYPTED = 2; @@ -214,11 +220,20 @@ public final class BinaryDictionaryFileDumper { final int MODE_MIN = COMPRESSED_CRYPTED_COMPRESSED; final int MODE_MAX = NONE; - final Uri.Builder wordListUriBuilder = getProviderUriBuilder(id); - final String finalFileName = DictionaryInfoUtils.getCacheFileName(id, locale, context); + final String clientId = context.getString(R.string.dictionary_pack_client_id); + final Uri.Builder wordListUriBuilder; + try { + wordListUriBuilder = getContentUriBuilderForType(clientId, + providerClient, QUERY_PATH_DATAFILE, wordlistId /* extraPath */); + } catch (RemoteException e) { + Log.e(TAG, "Can't communicate with the dictionary pack", e); + return null; + } + final String finalFileName = + DictionaryInfoUtils.getCacheFileName(wordlistId, locale, context); String tempFileName; try { - tempFileName = BinaryDictionaryGetter.getTempFileName(id, context); + tempFileName = BinaryDictionaryGetter.getTempFileName(wordlistId, context); } catch (IOException e) { Log.e(TAG, "Can't open the temporary file", e); return null; @@ -236,7 +251,7 @@ public final class BinaryDictionaryFileDumper { final Uri wordListUri = wordListUriBuilder.build(); try { // Open input. - afd = openAssetFileDescriptor(resolver, wordListUri); + afd = openAssetFileDescriptor(providerClient, wordListUri); // If we can't open it at all, don't even try a number of times. if (null == afd) return null; originalSourceStream = afd.createInputStream(); @@ -284,10 +299,10 @@ public final class BinaryDictionaryFileDumper { } wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT, QUERY_PARAMETER_SUCCESS); - if (0 >= resolver.delete(wordListUriBuilder.build(), null, null)) { + if (0 >= providerClient.delete(wordListUriBuilder.build(), null, null)) { Log.e(TAG, "Could not have the dictionary pack delete a word list"); } - BinaryDictionaryGetter.removeFilesWithIdExcept(context, id, finalFile); + BinaryDictionaryGetter.removeFilesWithIdExcept(context, wordlistId, finalFile); // Success! Close files (through the finally{} clause) and return. return AssetFileAddress.makeFromFileName(finalFileName); } catch (Exception e) { @@ -327,8 +342,12 @@ public final class BinaryDictionaryFileDumper { // as invalid. wordListUriBuilder.appendQueryParameter(QUERY_PARAMETER_DELETE_RESULT, QUERY_PARAMETER_FAILURE); - if (0 >= resolver.delete(wordListUriBuilder.build(), null, null)) { - Log.e(TAG, "In addition, we were unable to delete it."); + try { + if (0 >= providerClient.delete(wordListUriBuilder.build(), null, null)) { + Log.e(TAG, "In addition, we were unable to delete it."); + } + } catch (RemoteException e) { + Log.e(TAG, "In addition, communication with the dictionary provider was cut", e); } return null; } @@ -345,17 +364,27 @@ public final class BinaryDictionaryFileDumper { */ public static List<AssetFileAddress> cacheWordListsFromContentProvider(final Locale locale, final Context context, final boolean hasDefaultWordList) { - final ContentResolver resolver = context.getContentResolver(); - final List<WordListInfo> idList = getWordListWordListInfos(locale, context, - hasDefaultWordList); - final List<AssetFileAddress> fileAddressList = CollectionUtils.newArrayList(); - for (WordListInfo id : idList) { - final AssetFileAddress afd = cacheWordList(id.mId, id.mLocale, resolver, context); - if (null != afd) { - fileAddressList.add(afd); + final ContentProviderClient providerClient = context.getContentResolver(). + acquireContentProviderClient(getProviderUriBuilder("").build()); + if (null == providerClient) { + Log.e(TAG, "Can't establish communication with the dictionary provider"); + return CollectionUtils.newArrayList(); + } + try { + final List<WordListInfo> idList = getWordListWordListInfos(locale, context, + hasDefaultWordList); + final ArrayList<AssetFileAddress> fileAddressList = CollectionUtils.newArrayList(); + for (WordListInfo id : idList) { + final AssetFileAddress afd = + cacheWordList(id.mId, id.mLocale, providerClient, context); + if (null != afd) { + fileAddressList.add(afd); + } } + return fileAddressList; + } finally { + providerClient.release(); } - return fileAddressList; } /** diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java index a96738b3e..e913f2852 100644 --- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java +++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java @@ -68,9 +68,13 @@ final class BinaryDictionaryGetter { /** * Generates a unique temporary file name in the app cache directory. */ - public static String getTempFileName(String id, Context context) throws IOException { - return File.createTempFile(DictionaryInfoUtils.replaceFileNameDangerousCharacters(id), - null).getAbsolutePath(); + public static String getTempFileName(final String id, final Context context) + throws IOException { + final String safeId = DictionaryInfoUtils.replaceFileNameDangerousCharacters(id); + // If the first argument is less than three chars, createTempFile throws a + // RuntimeException. We don't really care about what name we get, so just + // put a three-chars prefix makes us safe. + return File.createTempFile("xxx" + safeId, null).getAbsolutePath(); } /** diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java index 422448edf..50e50233e 100644 --- a/java/src/com/android/inputmethod/latin/Constants.java +++ b/java/src/com/android/inputmethod/latin/Constants.java @@ -179,14 +179,13 @@ public final class Constants { public static final int CODE_DELETE = -4; public static final int CODE_SETTINGS = -5; public static final int CODE_SHORTCUT = -6; - public static final int CODE_ACTION_ENTER = -7; - public static final int CODE_ACTION_NEXT = -8; - public static final int CODE_ACTION_PREVIOUS = -9; - public static final int CODE_LANGUAGE_SWITCH = -10; - public static final int CODE_RESEARCH = -11; - public static final int CODE_SHIFT_ENTER = -12; + public static final int CODE_ACTION_NEXT = -7; + public static final int CODE_ACTION_PREVIOUS = -8; + public static final int CODE_LANGUAGE_SWITCH = -9; + public static final int CODE_RESEARCH = -10; + public static final int CODE_SHIFT_ENTER = -11; // Code value representing the code is not specified. - public static final int CODE_UNSPECIFIED = -13; + public static final int CODE_UNSPECIFIED = -12; public static boolean isLetterCode(final int code) { return code >= CODE_SPACE; @@ -200,7 +199,6 @@ public final class Constants { case CODE_DELETE: return "delete"; case CODE_SETTINGS: return "settings"; case CODE_SHORTCUT: return "shortcut"; - case CODE_ACTION_ENTER: return "actionEnter"; case CODE_ACTION_NEXT: return "actionNext"; case CODE_ACTION_PREVIOUS: return "actionPrevious"; case CODE_LANGUAGE_SWITCH: return "languageSwitch"; diff --git a/java/src/com/android/inputmethod/latin/DebugSettings.java b/java/src/com/android/inputmethod/latin/DebugSettings.java index 7df266ef2..c2aade64d 100644 --- a/java/src/com/android/inputmethod/latin/DebugSettings.java +++ b/java/src/com/android/inputmethod/latin/DebugSettings.java @@ -57,7 +57,7 @@ public final class DebugSettings extends PreferenceFragment if (usabilityStudyPref instanceof CheckBoxPreference) { final CheckBoxPreference checkbox = (CheckBoxPreference)usabilityStudyPref; checkbox.setChecked(prefs.getBoolean(PREF_USABILITY_STUDY_MODE, - ResearchLogger.DEFAULT_USABILITY_STUDY_MODE)); + LatinImeLogger.getUsabilityStudyMode(prefs))); checkbox.setSummary(R.string.settings_warning_researcher_mode); } final Preference statisticsLoggingPref = findPreference(PREF_STATISTICS_LOGGING); diff --git a/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java index d2a946bf5..dcfa483f8 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java +++ b/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java @@ -41,8 +41,6 @@ public class DictionaryInfoUtils { private static final String RESOURCE_PACKAGE_NAME = DictionaryInfoUtils.class.getPackage().getName(); private static final String DEFAULT_MAIN_DICT = "main"; - private static final String ID_CATEGORY_SEPARATOR = - BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR; private static final String MAIN_DICT_PREFIX = "main_"; // 6 digits - unicode is limited to 21 bits private static final int MAX_HEX_DIGITS_FOR_CODEPOINT = 6; @@ -51,24 +49,28 @@ public class DictionaryInfoUtils { private static final String LOCALE_COLUMN = "locale"; private static final String WORDLISTID_COLUMN = "id"; private static final String LOCAL_FILENAME_COLUMN = "filename"; + private static final String DESCRIPTION_COLUMN = "description"; private static final String DATE_COLUMN = "date"; private static final String FILESIZE_COLUMN = "filesize"; private static final String VERSION_COLUMN = "version"; + public final String mId; public final Locale mLocale; + public final String mDescription; public final AssetFileAddress mFileAddress; public final int mVersion; - public final String mId; - public DictionaryInfo(final Locale locale, final AssetFileAddress fileAddress, - final int version) { + public DictionaryInfo(final String id, final Locale locale, final String description, + final AssetFileAddress fileAddress, final int version) { + mId = id; mLocale = locale; + mDescription = description; mFileAddress = fileAddress; mVersion = version; - mId = DEFAULT_MAIN_DICT + ID_CATEGORY_SEPARATOR + mLocale; } public ContentValues toContentValues() { final ContentValues values = new ContentValues(); values.put(WORDLISTID_COLUMN, mId); values.put(LOCALE_COLUMN, mLocale.toString()); + values.put(DESCRIPTION_COLUMN, mDescription); values.put(LOCAL_FILENAME_COLUMN, mFileAddress.mFilename); values.put(DATE_COLUMN, new File(mFileAddress.mFilename).lastModified() / DateUtils.SECOND_IN_MILLIS); @@ -283,9 +285,11 @@ public class DictionaryInfoUtils { final AssetFileAddress fileAddress) { final FileHeader header = BinaryDictIOUtils.getDictionaryFileHeaderOrNull( new File(fileAddress.mFilename), fileAddress.mOffset, fileAddress.mLength); + final String id = header.getId(); final Locale locale = LocaleUtils.constructLocaleFromString(header.getLocaleString()); + final String description = header.getDescription(); final String version = header.getVersion(); - return new DictionaryInfo(locale, fileAddress, Integer.parseInt(version)); + return new DictionaryInfo(id, locale, description, fileAddress, Integer.parseInt(version)); } private static void addOrUpdateDictInfo(final ArrayList<DictionaryInfo> dictList, diff --git a/java/src/com/android/inputmethod/latin/InputTypeUtils.java b/java/src/com/android/inputmethod/latin/InputTypeUtils.java index 2ad619b82..ecb20144b 100644 --- a/java/src/com/android/inputmethod/latin/InputTypeUtils.java +++ b/java/src/com/android/inputmethod/latin/InputTypeUtils.java @@ -106,18 +106,13 @@ public final class InputTypeUtils implements InputType { } public static int getImeOptionsActionIdFromEditorInfo(final EditorInfo editorInfo) { - final int actionId = editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION; if ((editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) { return EditorInfo.IME_ACTION_NONE; } else if (editorInfo.actionLabel != null) { return IME_ACTION_CUSTOM_LABEL; } else { - return actionId; + // Note: this is different from editorInfo.actionId, hence "ImeOptionsActionId" + return editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION; } } - - public static int getConcreteActionIdFromEditorInfo(final EditorInfo editorInfo) { - final int actionId = getImeOptionsActionIdFromEditorInfo(editorInfo); - return actionId == InputTypeUtils.IME_ACTION_CUSTOM_LABEL ? editorInfo.actionId : actionId; - } } diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java index 73ace2bfa..252fb02c8 100644 --- a/java/src/com/android/inputmethod/latin/LatinIME.java +++ b/java/src/com/android/inputmethod/latin/LatinIME.java @@ -132,6 +132,8 @@ public final class LatinIME extends InputMethodService implements KeyboardAction private View mKeyPreviewBackingView; private View mSuggestionsContainer; private SuggestionStripView mSuggestionStripView; + // Never null + private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY; @UsedForTesting Suggest mSuggest; private CompletionInfo[] mApplicationSpecifiedCompletions; private ApplicationInfo mTargetApplicationInfo; @@ -165,7 +167,6 @@ public final class LatinIME extends InputMethodService implements KeyboardAction private boolean mExpectingUpdateSelection; private int mDeleteCount; private long mLastKeyTime; - private int mActionId; private TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet(); // Member variables for remembering the current device orientation. @@ -427,7 +428,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction initSuggest(); if (ProductionFlag.IS_EXPERIMENTAL) { - ResearchLogger.getInstance().init(this, mKeyboardSwitcher); + ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mSuggest); } mDisplayOrientation = getResources().getConfiguration().orientation; @@ -562,6 +563,9 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } mSettings.onDestroy(); unregisterReceiver(mReceiver); + if (ProductionFlag.IS_EXPERIMENTAL) { + ResearchLogger.getInstance().onDestroy(); + } // TODO: The experimental version is not supported by the Dictionary Pack Service yet. if (!ProductionFlag.IS_EXPERIMENTAL) { unregisterReceiver(mDictionaryPackInstallReceiver); @@ -729,6 +733,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction // otherwise it will clear the suggestion strip. setPunctuationSuggestions(); } + mSuggestedWords = SuggestedWords.EMPTY; mConnection.resetCachesUponCursorMove(editorInfo.initialSelStart); @@ -756,7 +761,6 @@ public final class LatinIME extends InputMethodService implements KeyboardAction mLastSelectionStart = editorInfo.initialSelStart; mLastSelectionEnd = editorInfo.initialSelEnd; - mActionId = InputTypeUtils.getConcreteActionIdFromEditorInfo(editorInfo); mHandler.cancelUpdateSuggestionStrip(); mHandler.cancelDoubleSpacePeriodTimer(); @@ -954,6 +958,10 @@ public final class LatinIME extends InputMethodService implements KeyboardAction LatinImeLogger.commit(); mKeyboardSwitcher.onHideWindow(); + if (AccessibilityUtils.getInstance().isAccessibilityEnabled()) { + AccessibleKeyboardViewProxy.getInstance().onHideWindow(); + } + if (TRACE) Debug.stopMethodTracing(); if (mOptionsDialog != null && mOptionsDialog.isShowing()) { mOptionsDialog.dismiss(); @@ -994,7 +1002,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction false /* isPrediction */); // When in fullscreen mode, show completions generated by the application final boolean isAutoCorrection = false; - setSuggestionStrip(suggestedWords, isAutoCorrection); + setSuggestedWords(suggestedWords, isAutoCorrection); setAutoCorrectionIndicator(isAutoCorrection); setSuggestionStripShown(true); if (ProductionFlag.IS_EXPERIMENTAL) { @@ -1119,7 +1127,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction if (mSettings.getCurrent().mBigramPredictionEnabled) { clearSuggestionStrip(); } else { - setSuggestionStrip(mSettings.getCurrent().mSuggestPuncList, false); + setSuggestedWords(mSettings.getCurrent().mSuggestPuncList, false); } mConnection.resetCachesUponCursorMove(newCursorPosition); } @@ -1393,13 +1401,28 @@ public final class LatinIME extends InputMethodService implements KeyboardAction ResearchLogger.getInstance().onResearchKeySelected(this); } break; - case Constants.CODE_ACTION_ENTER: - if (EditorInfo.IME_ACTION_NONE != mActionId - && EditorInfo.IME_ACTION_UNSPECIFIED != mActionId) { - performEditorAction(mActionId); - break; + case Constants.CODE_ENTER: + final EditorInfo editorInfo = getCurrentInputEditorInfo(); + final int imeOptionsActionId = + InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo); + if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) { + // Either we have an actionLabel and we should performEditorAction with actionId + // regardless of its value. + performEditorAction(editorInfo.actionId); + } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) { + // We didn't have an actionLabel, but we had another action to execute. + // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast, + // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it + // means there should be an action and the app didn't bother to set a specific + // code for it - presumably it only handles one. It does not have to be treated + // in any specific way: anything that is not IME_ACTION_NONE should be sent to + // performEditorAction. + performEditorAction(imeOptionsActionId); + } else { + // No action label, and the action from imeOptions is NONE: this is a regular + // enter key that should input a carriage return. + didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState); } - didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState); break; case Constants.CODE_SHIFT_ENTER: didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState); @@ -1967,8 +1990,8 @@ public final class LatinIME extends InputMethodService implements KeyboardAction // Outside LatinIME, only used by the test suite. @UsedForTesting boolean isShowingPunctuationList() { - if (mSuggestionStripView == null) return false; - return mSettings.getCurrent().mSuggestPuncList == mSuggestionStripView.getSuggestions(); + if (mSuggestedWords == null) return false; + return mSettings.getCurrent().mSuggestPuncList == mSuggestedWords; } private boolean isSuggestionsStripVisible() { @@ -1984,11 +2007,12 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } private void clearSuggestionStrip() { - setSuggestionStrip(SuggestedWords.EMPTY, false); + setSuggestedWords(SuggestedWords.EMPTY, false); setAutoCorrectionIndicator(false); } - private void setSuggestionStrip(final SuggestedWords words, final boolean isAutoCorrection) { + private void setSuggestedWords(final SuggestedWords words, final boolean isAutoCorrection) { + mSuggestedWords = words; if (mSuggestionStripView != null) { mSuggestionStripView.setSuggestions(words); mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection); @@ -2071,15 +2095,16 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } private SuggestedWords getOlderSuggestions(final String typedWord) { - SuggestedWords previousSuggestions = mSuggestionStripView.getSuggestions(); - if (previousSuggestions == mSettings.getCurrent().mSuggestPuncList) { - previousSuggestions = SuggestedWords.EMPTY; + SuggestedWords previousSuggestedWords = mSuggestedWords; + if (previousSuggestedWords == mSettings.getCurrent().mSuggestPuncList) { + previousSuggestedWords = SuggestedWords.EMPTY; } if (typedWord == null) { - return previousSuggestions; + return previousSuggestedWords; } final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions = - SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, previousSuggestions); + SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, + previousSuggestedWords); return new SuggestedWords(typedWordAndPreviousSuggestions, false /* typedWordValid */, false /* hasAutoCorrectionCandidate */, @@ -2101,7 +2126,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction } mWordComposer.setAutoCorrection(autoCorrection); final boolean isAutoCorrection = suggestedWords.willAutoCorrect(); - setSuggestionStrip(suggestedWords, isAutoCorrection); + setSuggestedWords(suggestedWords, isAutoCorrection); setAutoCorrectionIndicator(isAutoCorrection); setSuggestionStripShown(isSuggestionsStripVisible()); } @@ -2124,7 +2149,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction Stats.onAutoCorrection(typedWord, autoCorrection, separatorString, mWordComposer); } if (ProductionFlag.IS_EXPERIMENTAL) { - final SuggestedWords suggestedWords = mSuggestionStripView.getSuggestions(); + final SuggestedWords suggestedWords = mSuggestedWords; ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection, separatorString, mWordComposer.isBatchMode(), suggestedWords); } @@ -2149,7 +2174,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction // interface @Override public void pickSuggestionManually(final int index, final String suggestion) { - final SuggestedWords suggestedWords = mSuggestionStripView.getSuggestions(); + final SuggestedWords suggestedWords = mSuggestedWords; // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput if (suggestion.length() == 1 && isShowingPunctuationList()) { // Word separators are suggested before the user inputs something. @@ -2181,6 +2206,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction if (mSettings.getCurrent().isApplicationSpecifiedCompletionsOn() && mApplicationSpecifiedCompletions != null && index >= 0 && index < mApplicationSpecifiedCompletions.length) { + mSuggestedWords = SuggestedWords.EMPTY; if (mSuggestionStripView != null) { mSuggestionStripView.clear(); } @@ -2236,7 +2262,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction */ private void commitChosenWord(final String chosenWord, final int commitType, final String separatorString) { - final SuggestedWords suggestedWords = mSuggestionStripView.getSuggestions(); + final SuggestedWords suggestedWords = mSuggestedWords; mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan( this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1); // Add the word to the user history dictionary @@ -2253,7 +2279,7 @@ public final class LatinIME extends InputMethodService implements KeyboardAction if (mSettings.getCurrent().mBigramPredictionEnabled) { clearSuggestionStrip(); } else { - setSuggestionStrip(mSettings.getCurrent().mSuggestPuncList, false); + setSuggestedWords(mSettings.getCurrent().mSuggestPuncList, false); } setAutoCorrectionIndicator(false); setSuggestionStripShown(isSuggestionsStripVisible()); @@ -2550,6 +2576,12 @@ public final class LatinIME extends InputMethodService implements KeyboardAction dialog.show(); } + // TODO: can this be removed somehow without breaking the tests? + @UsedForTesting + /* package for test */ String getFirstSuggestedWord() { + return mSuggestedWords.size() > 0 ? mSuggestedWords.getWord(0) : null; + } + public void debugDumpStateAndCrashWithException(final String context) { final StringBuilder s = new StringBuilder(); s.append("Target application : ").append(mTargetApplicationInfo.name) diff --git a/java/src/com/android/inputmethod/latin/LatinImeLogger.java b/java/src/com/android/inputmethod/latin/LatinImeLogger.java index e4e8b94b2..3f2b0a3f4 100644 --- a/java/src/com/android/inputmethod/latin/LatinImeLogger.java +++ b/java/src/com/android/inputmethod/latin/LatinImeLogger.java @@ -37,6 +37,10 @@ public final class LatinImeLogger implements SharedPreferences.OnSharedPreferenc public static void commit() { } + public static boolean getUsabilityStudyMode(final SharedPreferences prefs) { + return false; + } + public static void onDestroy() { } diff --git a/java/src/com/android/inputmethod/latin/LocaleUtils.java b/java/src/com/android/inputmethod/latin/LocaleUtils.java index fcf727041..5fde8158a 100644 --- a/java/src/com/android/inputmethod/latin/LocaleUtils.java +++ b/java/src/com/android/inputmethod/latin/LocaleUtils.java @@ -180,14 +180,15 @@ public final class LocaleUtils { synchronized (sLockForRunInLocale) { final Configuration conf = res.getConfiguration(); final Locale oldLocale = conf.locale; + final boolean needsChange = (newLocale != null && !newLocale.equals(oldLocale)); try { - if (newLocale != null && !newLocale.equals(oldLocale)) { + if (needsChange) { conf.locale = newLocale; res.updateConfiguration(conf, null); } return job(res); } finally { - if (newLocale != null && !newLocale.equals(oldLocale)) { + if (needsChange) { conf.locale = oldLocale; res.updateConfiguration(conf, null); } diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java index 02b44c7f6..435074bdb 100644 --- a/java/src/com/android/inputmethod/latin/Settings.java +++ b/java/src/com/android/inputmethod/latin/Settings.java @@ -18,6 +18,7 @@ package com.android.inputmethod.latin; import android.content.Context; import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; import android.content.res.Resources; import android.preference.PreferenceManager; @@ -64,6 +65,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static final String PREF_GESTURE_PREVIEW_TRAIL = "pref_gesture_preview_trail"; public static final String PREF_GESTURE_FLOATING_PREVIEW_TEXT = "pref_gesture_floating_preview_text"; + public static final String PREF_SHOW_SETUP_WIZARD_ICON = "pref_show_setup_wizard_icon"; public static final String PREF_INPUT_LANGUAGE = "input_language"; public static final String PREF_SELECTED_LANGUAGES = "selected_languages"; @@ -260,4 +262,16 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static boolean readUseFullscreenMode(final Resources res) { return res.getBoolean(R.bool.config_use_fullscreen_mode); } + + public static boolean readShowSetupWizardIcon(final SharedPreferences prefs, + final Context context) { + if (!prefs.contains(Settings.PREF_SHOW_SETUP_WIZARD_ICON)) { + final ApplicationInfo appInfo = context.getApplicationInfo(); + final boolean isApplicationInSystemImage = + (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + // Default value + return !isApplicationInSystemImage; + } + return prefs.getBoolean(Settings.PREF_SHOW_SETUP_WIZARD_ICON, false); + } } diff --git a/java/src/com/android/inputmethod/latin/SettingsFragment.java b/java/src/com/android/inputmethod/latin/SettingsFragment.java index edd064c0b..4c90e485a 100644 --- a/java/src/com/android/inputmethod/latin/SettingsFragment.java +++ b/java/src/com/android/inputmethod/latin/SettingsFragment.java @@ -31,6 +31,7 @@ import android.preference.PreferenceScreen; import android.view.inputmethod.InputMethodSubtype; import com.android.inputmethod.latin.define.ProductionFlag; +import com.android.inputmethod.latin.setup.LauncherIconVisibilityManager; import com.android.inputmethodcommon.InputMethodSettingsFragment; public final class SettingsFragment extends InputMethodSettingsFragment @@ -155,6 +156,10 @@ public final class SettingsFragment extends InputMethodSettingsFragment removePreference(Settings.PREF_GESTURE_SETTINGS, getPreferenceScreen()); } + final CheckBoxPreference showSetupWizardIcon = + (CheckBoxPreference)findPreference(Settings.PREF_SHOW_SETUP_WIZARD_ICON); + showSetupWizardIcon.setChecked(Settings.readShowSetupWizardIcon(prefs, context)); + setupKeyLongpressTimeoutSettings(prefs, res); setupKeypressVibrationDurationSettings(prefs, res); setupKeypressSoundVolumeSettings(prefs, res); @@ -196,6 +201,8 @@ public final class SettingsFragment extends InputMethodSettingsFragment final boolean gestureInputEnabled = Settings.readGestureInputEnabled(prefs, res); setPreferenceEnabled(Settings.PREF_GESTURE_PREVIEW_TRAIL, gestureInputEnabled); setPreferenceEnabled(Settings.PREF_GESTURE_FLOATING_PREVIEW_TEXT, gestureInputEnabled); + } else if (key.equals(Settings.PREF_SHOW_SETUP_WIZARD_ICON)) { + LauncherIconVisibilityManager.updateSetupWizardIconVisibility(getActivity()); } ensureConsistencyOfAutoCorrectionSettings(); updateVoiceModeSummary(); diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java index 81bc9f5d7..528028328 100644 --- a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java +++ b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java @@ -263,9 +263,10 @@ public final class UserHistoryDictionary extends ExpandableDictionary { UserHistoryDictIOUtils.readDictionaryBinary( new UserHistoryDictIOUtils.ByteArrayWrapper(buffer), listener); } catch (FileNotFoundException e) { - Log.e(TAG, "when loading: file not found" + e); + // This is an expected condition: we don't have a user history dictionary for this + // language yet. It will be created sometime later. } catch (IOException e) { - Log.e(TAG, "IOException when open bytebuffer: " + e); + Log.e(TAG, "IOException on opening a bytebuffer", e); } finally { if (inStream != null) { try { @@ -328,7 +329,7 @@ public final class UserHistoryDictionary extends ExpandableDictionary { Thread.sleep(15000); Log.w(TAG, "End stress in closing"); } catch (InterruptedException e) { - Log.e(TAG, "In stress test: " + e); + Log.e(TAG, "In stress test", e); } } @@ -343,7 +344,7 @@ public final class UserHistoryDictionary extends ExpandableDictionary { out.flush(); out.close(); } catch (IOException e) { - Log.e(TAG, "IO Exception while writing file: " + e); + Log.e(TAG, "IO Exception while writing file", e); } finally { if (out != null) { try { diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java index acfcd5354..7a604dc6a 100644 --- a/java/src/com/android/inputmethod/latin/Utils.java +++ b/java/src/com/android/inputmethod/latin/Utils.java @@ -28,6 +28,7 @@ import android.os.Process; import android.text.TextUtils; import android.util.Log; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; import java.io.BufferedReader; @@ -77,6 +78,7 @@ public final class Utils { private RingCharBuffer() { // Intentional empty constructor for singleton. } + @UsedForTesting public static RingCharBuffer getInstance() { return sRingCharBuffer; } @@ -93,6 +95,7 @@ public final class Utils { return ret < 0 ? ret + BUFSIZE : ret; } // TODO: accept code points + @UsedForTesting public void push(char c, int x, int y) { if (!mEnabled) return; mCharBuf[mEnd] = c; diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java index 31f616dd9..f7cb4346a 100644 --- a/java/src/com/android/inputmethod/latin/WordComposer.java +++ b/java/src/com/android/inputmethod/latin/WordComposer.java @@ -16,6 +16,7 @@ package com.android.inputmethod.latin; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.keyboard.Key; import com.android.inputmethod.keyboard.Keyboard; @@ -177,7 +178,8 @@ public final class WordComposer { /** * Internal method to retrieve reasonable proximity info for a character. */ - private void addKeyInfo(final int codePoint, final Keyboard keyboard) { + @UsedForTesting + public void addKeyInfo(final int codePoint, final Keyboard keyboard) { final int x, y; final Key key; if (keyboard != null && (key = keyboard.getKey(codePoint)) != null) { diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java index 83acca874..e1e5e5500 100644 --- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java +++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java @@ -258,6 +258,8 @@ public final class FormatSpec { public final FormatOptions mFormatOptions; private static final String DICTIONARY_VERSION_ATTRIBUTE = "version"; private static final String DICTIONARY_LOCALE_ATTRIBUTE = "locale"; + private static final String DICTIONARY_ID_ATTRIBUTE = "dictionary"; + private static final String DICTIONARY_DESCRIPTION_ATTRIBUTE = "description"; public FileHeader(final int headerSize, final DictionaryOptions dictionaryOptions, final FormatOptions formatOptions) { mHeaderSize = headerSize; @@ -274,6 +276,18 @@ public final class FormatSpec { public String getVersion() { return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_VERSION_ATTRIBUTE); } + + // Helper method to get the dictionary ID as a String + public String getId() { + return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_ID_ATTRIBUTE); + } + + // Helper method to get the description + public String getDescription() { + // TODO: Right now each dictionary file comes with a description in its own language. + // It will display as is no matter the device's locale. It should be internationalized. + return mDictionaryOptions.mAttributes.get(FileHeader.DICTIONARY_DESCRIPTION_ATTRIBUTE); + } } private FormatSpec() { diff --git a/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java b/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java new file mode 100644 index 000000000..1b893a65d --- /dev/null +++ b/java/src/com/android/inputmethod/latin/setup/LauncherIconVisibilityManager.java @@ -0,0 +1,122 @@ +/* + * 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 com.android.inputmethod.latin.setup; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.Process; +import android.preference.PreferenceManager; +import android.util.Log; + +import com.android.inputmethod.compat.IntentCompatUtils; +import com.android.inputmethod.latin.RichInputMethodManager; +import com.android.inputmethod.latin.Settings; + +/** + * This class detects the {@link Intent#ACTION_MY_PACKAGE_REPLACED} broadcast intent when this IME + * package has been replaced by a newer version of the same package. This class also detects + * {@link Intent#ACTION_BOOT_COMPLETED} and {@link Intent#ACTION_USER_INITIALIZE} broadcast intent. + * + * If this IME has already been installed in the system image and a new version of this IME has + * been installed, {@link Intent#ACTION_MY_PACKAGE_REPLACED} is received by this receiver and it + * will hide the setup wizard's icon. + * + * If this IME has already been installed in the data partition and a new version of this IME has + * been installed, {@link Intent#ACTION_MY_PACKAGE_REPLACED} is received by this receiver but it + * will not hide the setup wizard's icon, and the icon will appear on the launcher. + * + * If this IME hasn't been installed yet and has been newly installed, no + * {@link Intent#ACTION_MY_PACKAGE_REPLACED} will be sent and the setup wizard's icon will appear + * on the launcher. + * + * When the device has been booted, {@link Intent#ACTION_BOOT_COMPLETED} is received by this + * receiver and it checks whether the setup wizard's icon should be appeared or not on the launcher + * depending on which partition this IME is installed. + * + * When a multiuser account has been created, {@link Intent#ACTION_USER_INITIALIZE} is received + * by this receiver and it checks the whether the setup wizard's icon should be appeared or not on + * the launcher depending on which partition this IME is installed. + */ +public final class LauncherIconVisibilityManager extends BroadcastReceiver { + private static final String TAG = LauncherIconVisibilityManager.class.getSimpleName(); + + @Override + public void onReceive(final Context context, final Intent intent) { + if (shouldHandleThisIntent(intent, context)) { + updateSetupWizardIconVisibility(context); + } + + // The process that hosts this broadcast receiver is invoked and remains alive even after + // 1) the package has been re-installed, 2) the device has been booted, + // 3) a multiuser has been created. + // There is no good reason to keep the process alive if this IME isn't a current IME. + RichInputMethodManager.init(context); + if (!SetupActivity.isThisImeCurrent(context)) { + final int myPid = Process.myPid(); + Log.i(TAG, "Killing my process: pid=" + myPid); + Process.killProcess(myPid); + } + } + + private static boolean shouldHandleThisIntent(final Intent intent, final Context context) { + final String action = intent.getAction(); + if (Intent.ACTION_MY_PACKAGE_REPLACED.equals(action)) { + Log.i(TAG, "Package has been replaced: " + context.getPackageName()); + return true; + } else if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { + Log.i(TAG, "Boot has been completed"); + return true; + } else if (IntentCompatUtils.has_ACTION_USER_INITIALIZE(intent)) { + Log.i(TAG, "User initialize"); + return true; + } + return false; + } + + public static void updateSetupWizardIconVisibility(final Context context) { + final ComponentName setupWizardActivity = new ComponentName(context, SetupActivity.class); + final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + final boolean stateHasSet; + if (Settings.readShowSetupWizardIcon(prefs, context)) { + stateHasSet = setActivityState(context, setupWizardActivity, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED); + Log.i(TAG, (stateHasSet ? "Enable activity: " : "Activity has already been enabled: ") + + setupWizardActivity); + } else { + stateHasSet = setActivityState(context, setupWizardActivity, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED); + Log.i(TAG, (stateHasSet ? "Disable activity: " : "Activity has already been disabled: ") + + setupWizardActivity); + } + } + + private static boolean setActivityState(final Context context, + final ComponentName activityComponent, final int activityState) { + final PackageManager pm = context.getPackageManager(); + final int activityComponentState = pm.getComponentEnabledSetting(activityComponent); + if (activityComponentState == activityState) { + return false; + } + pm.setComponentEnabledSetting( + activityComponent, activityState, PackageManager.DONT_KILL_APP); + return true; + } +} diff --git a/java/src/com/android/inputmethod/latin/setup/SetupActivity.java b/java/src/com/android/inputmethod/latin/setup/SetupActivity.java index fab894584..e009fbc39 100644 --- a/java/src/com/android/inputmethod/latin/setup/SetupActivity.java +++ b/java/src/com/android/inputmethod/latin/setup/SetupActivity.java @@ -17,23 +17,341 @@ package com.android.inputmethod.latin.setup; import android.app.Activity; +import android.content.Context; import android.content.Intent; +import android.content.res.Resources; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.os.Message; +import android.provider.Settings; +import android.view.View; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.TextView; +import com.android.inputmethod.compat.TextViewCompatUtils; +import com.android.inputmethod.compat.ViewCompatUtils; +import com.android.inputmethod.latin.CollectionUtils; +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.RichInputMethodManager; import com.android.inputmethod.latin.SettingsActivity; +import com.android.inputmethod.latin.StaticInnerHandlerWrapper; + +import java.util.HashMap; public final class SetupActivity extends Activity { + private SetupStepIndicatorView mStepIndicatorView; + private final SetupStepGroup mSetupSteps = new SetupStepGroup(); + private static final String STATE_STEP = "step"; + private int mStepNumber; + private static final int STEP_1 = 1; + private static final int STEP_2 = 2; + private static final int STEP_3 = 3; + + private final SettingsPoolingHandler mHandler = new SettingsPoolingHandler(this); + + static final class SettingsPoolingHandler extends StaticInnerHandlerWrapper<SetupActivity> { + private static final int MSG_POLLING_IME_SETTINGS = 0; + private static final long IME_SETTINGS_POLLING_INTERVAL = 200; + + public SettingsPoolingHandler(final SetupActivity outerInstance) { + super(outerInstance); + } + + @Override + public void handleMessage(final Message msg) { + final SetupActivity setupActivity = getOuterInstance(); + switch (msg.what) { + case MSG_POLLING_IME_SETTINGS: + if (SetupActivity.isThisImeEnabled(setupActivity)) { + setupActivity.invokeSetupWizardOfThisIme(); + return; + } + startPollingImeSettings(); + break; + } + } + + public void startPollingImeSettings() { + sendMessageDelayed(obtainMessage(MSG_POLLING_IME_SETTINGS), + IME_SETTINGS_POLLING_INTERVAL); + } + + public void cancelPollingImeSettings() { + removeMessages(MSG_POLLING_IME_SETTINGS); + } + } + @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { + setTheme(android.R.style.Theme_DeviceDefault_Light_NoActionBar); super.onCreate(savedInstanceState); - // TODO: Implement setup wizard. + setContentView(R.layout.setup_wizard); + + RichInputMethodManager.init(this); + + if (savedInstanceState == null) { + mStepNumber = determineSetupStepNumber(); + } else { + mStepNumber = savedInstanceState.getInt(STATE_STEP); + } + + if (mStepNumber == STEP_3) { + // This IME already has been enabled and set as current IME. + // TODO: Implement tutorial. + invokeSettingsOfThisIme(); + finish(); + return; + } + + // TODO: Use sans-serif-thin font family depending on the system locale white list and + // the SDK version. + final TextView titleView = (TextView)findViewById(R.id.setup_title); + titleView.setText(getString(R.string.setup_title, getString(R.string.english_ime_name))); + + mStepIndicatorView = (SetupStepIndicatorView)findViewById(R.id.setup_step_indicator); + + final SetupStep step1 = new SetupStep(findViewById(R.id.setup_step1), + R.string.setup_step1_title, R.string.setup_step1_instruction, + R.drawable.ic_settings_language, R.string.language_settings); + step1.setAction(new Runnable() { + @Override + public void run() { + invokeLanguageAndInputSettings(); + mHandler.startPollingImeSettings(); + } + }); + mSetupSteps.addStep(STEP_1, step1); + + final SetupStep step2 = new SetupStep(findViewById(R.id.setup_step2), + R.string.setup_step2_title, R.string.setup_step2_instruction, + 0 /* actionIcon */, R.string.select_input_method); + step2.setAction(new Runnable() { + @Override + public void run() { + // Invoke input method picker. + RichInputMethodManager.getInstance().getInputMethodManager() + .showInputMethodPicker(); + } + }); + mSetupSteps.addStep(STEP_2, step2); + + final SetupStep step3 = new SetupStep(findViewById(R.id.setup_step3), + R.string.setup_step3_title, 0 /* instruction */, + R.drawable.sym_keyboard_language_switch, R.string.setup_step3_instruction); + step3.setAction(new Runnable() { + @Override + public void run() { + invokeSubtypeEnablerOfThisIme(); + } + }); + mSetupSteps.addStep(STEP_3, step3); + } + + private void invokeSetupWizardOfThisIme() { + final Intent intent = new Intent(); + intent.setClass(this, SetupActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + | Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + } + + private void invokeSettingsOfThisIme() { final Intent intent = new Intent(); intent.setClass(this, SettingsActivity.class); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED + intent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED | Intent.FLAG_ACTIVITY_CLEAR_TOP); startActivity(intent); - finish(); + } + + private void invokeLanguageAndInputSettings() { + final Intent intent = new Intent(); + intent.setAction(Settings.ACTION_INPUT_METHOD_SETTINGS); + intent.addCategory(Intent.CATEGORY_DEFAULT); + startActivity(intent); + } + + private void invokeSubtypeEnablerOfThisIme() { + final InputMethodInfo imi = + RichInputMethodManager.getInstance().getInputMethodInfoOfThisIme(); + final Intent intent = new Intent(); + intent.setAction(Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.putExtra(Settings.EXTRA_INPUT_METHOD_ID, imi.getId()); + startActivity(intent); + } + + /** + * Check if the IME specified by the context is enabled. + * Note that {@link RichInputMethodManager} must have been initialized before calling this + * method. + * + * @param context package context of the IME to be checked. + * @return true if this IME is enabled. + */ + public static boolean isThisImeEnabled(final Context context) { + final String packageName = context.getPackageName(); + final InputMethodManager imm = RichInputMethodManager.getInstance().getInputMethodManager(); + for (final InputMethodInfo imi : imm.getEnabledInputMethodList()) { + if (packageName.equals(imi.getPackageName())) { + return true; + } + } + return false; + } + + /** + * Check if the IME specified by the context is the current IME. + * Note that {@link RichInputMethodManager} must have been initialized before calling this + * method. + * + * @param context package context of the IME to be checked. + * @return true if this IME is the current IME. + */ + public static boolean isThisImeCurrent(final Context context) { + final InputMethodInfo myImi = + RichInputMethodManager.getInstance().getInputMethodInfoOfThisIme(); + final String currentImeId = Settings.Secure.getString( + context.getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD); + return myImi.getId().equals(currentImeId); + } + + private int determineSetupStepNumber() { + mHandler.cancelPollingImeSettings(); + if (!isThisImeEnabled(this)) { + return STEP_1; + } + if (!isThisImeCurrent(this)) { + return STEP_2; + } + return STEP_3; + } + + @Override + protected void onSaveInstanceState(final Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt(STATE_STEP, mStepNumber); + } + + @Override + protected void onRestoreInstanceState(final Bundle savedInstanceState) { + super.onRestoreInstanceState(savedInstanceState); + mStepNumber = savedInstanceState.getInt(STATE_STEP); + } + + @Override + protected void onStart() { + super.onStart(); + mStepNumber = determineSetupStepNumber(); + } + + @Override + protected void onRestart() { + super.onRestart(); + mStepNumber = determineSetupStepNumber(); + } + + @Override + protected void onResume() { + super.onResume(); + updateSetupStepView(); + } + + @Override + public void onWindowFocusChanged(final boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (!hasFocus) { + return; + } + mStepNumber = determineSetupStepNumber(); + updateSetupStepView(); + } + + private void updateSetupStepView() { + final int layoutDirection = ViewCompatUtils.getLayoutDirection(mStepIndicatorView); + mStepIndicatorView.setIndicatorPosition( + getIndicatorPosition(mStepNumber, mSetupSteps.getTotalStep(), layoutDirection)); + mSetupSteps.enableStep(mStepNumber); + } + + private static float getIndicatorPosition(final int step, final int totalStep, + final int layoutDirection) { + final float pos = ((step - STEP_1) * 2 + 1) / (float)(totalStep * 2); + return (layoutDirection == ViewCompatUtils.LAYOUT_DIRECTION_RTL) ? 1.0f - pos : pos; + } + + static final class SetupStep implements View.OnClickListener { + private final View mRootView; + private final TextView mActionLabel; + private Runnable mAction; + + public SetupStep(final View rootView, final int title, final int instruction, + final int actionIcon, final int actionLabel) { + mRootView = rootView; + final Resources res = rootView.getResources(); + final String applicationName = res.getString(R.string.english_ime_name); + + final TextView titleView = (TextView)rootView.findViewById(R.id.setup_step_title); + titleView.setText(res.getString(title, applicationName)); + + final TextView instructionView = (TextView)rootView.findViewById( + R.id.setup_step_instruction); + if (instruction == 0) { + instructionView.setVisibility(View.GONE); + } else { + instructionView.setText(res.getString(instruction, applicationName)); + } + + mActionLabel = (TextView)rootView.findViewById(R.id.setup_step_action_label); + mActionLabel.setText(res.getString(actionLabel)); + if (actionIcon == 0) { + final int paddingEnd = ViewCompatUtils.getPaddingEnd(mActionLabel); + ViewCompatUtils.setPaddingRelative(mActionLabel, paddingEnd, 0, paddingEnd, 0); + } else { + final int overrideColor = res.getColor(R.color.setup_text_action); + final Drawable icon = res.getDrawable(actionIcon); + icon.setColorFilter(overrideColor, PorterDuff.Mode.MULTIPLY); + icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); + TextViewCompatUtils.setCompoundDrawablesRelative( + mActionLabel, icon, null, null, null); + } + } + + public void setEnabled(final boolean enabled) { + mRootView.setVisibility(enabled ? View.VISIBLE : View.GONE); + } + + public void setAction(final Runnable action) { + mActionLabel.setOnClickListener(this); + mAction = action; + } + + @Override + public void onClick(final View v) { + if (mAction != null) { + mAction.run(); + } + } + } + + static final class SetupStepGroup { + private final HashMap<Integer, SetupStep> mGroup = CollectionUtils.newHashMap(); + + public void addStep(final int stepNo, final SetupStep step) { + mGroup.put(stepNo, step); + } + + public void enableStep(final int enableStepNo) { + for (final Integer stepNo : mGroup.keySet()) { + final SetupStep step = mGroup.get(stepNo); + step.setEnabled(stepNo == enableStepNo); + } + } + + public int getTotalStep() { + return mGroup.size(); + } } } diff --git a/java/src/com/android/inputmethod/latin/setup/SetupStepIndicatorView.java b/java/src/com/android/inputmethod/latin/setup/SetupStepIndicatorView.java new file mode 100644 index 000000000..077a21793 --- /dev/null +++ b/java/src/com/android/inputmethod/latin/setup/SetupStepIndicatorView.java @@ -0,0 +1,56 @@ +/* + * 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 com.android.inputmethod.latin.setup; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.util.AttributeSet; +import android.view.View; + +import com.android.inputmethod.latin.R; + +public final class SetupStepIndicatorView extends View { + private final Path mIndicatorPath = new Path(); + private final Paint mIndicatorPaint = new Paint(); + private float mXRatio; + + public SetupStepIndicatorView(final Context context, final AttributeSet attrs) { + super(context, attrs); + mIndicatorPaint.setColor(getResources().getColor(R.color.setup_step_background)); + mIndicatorPaint.setStyle(Paint.Style.FILL); + } + + public void setIndicatorPosition(final float xRatio) { + mXRatio = xRatio; + invalidate(); + } + + @Override + protected void onDraw(final Canvas canvas) { + super.onDraw(canvas); + final int xPos = (int)(getWidth() * mXRatio); + final int height = getHeight(); + mIndicatorPath.rewind(); + mIndicatorPath.moveTo(xPos, 0); + mIndicatorPath.lineTo(xPos + height, height); + mIndicatorPath.lineTo(xPos - height, height); + mIndicatorPath.close(); + canvas.drawPath(mIndicatorPath, mIndicatorPaint); + } +} diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java index bc51d5d62..5a29eee4e 100644 --- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java +++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java @@ -644,10 +644,6 @@ public final class SuggestionStripView extends RelativeLayout implements OnClick return false; } - public SuggestedWords getSuggestions() { - return mSuggestedWords; - } - public void clear() { mSuggestionsStrip.removeAllViews(); removeAllViews(); diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java index 1a9a720f3..839e2b7ba 100644 --- a/java/src/com/android/inputmethod/research/LogUnit.java +++ b/java/src/com/android/inputmethod/research/LogUnit.java @@ -16,7 +16,6 @@ package com.android.inputmethod.research; -import android.content.SharedPreferences; import android.os.SystemClock; import android.text.TextUtils; import android.util.JsonWriter; @@ -45,7 +44,7 @@ import java.util.List; * will not violate the user's privacy. Checks for this may include whether other LogUnits have * been published recently, or whether the LogUnit contains numbers, etc. */ -/* package */ class LogUnit { +public class LogUnit { private static final String TAG = LogUnit.class.getSimpleName(); private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG; @@ -151,10 +150,10 @@ import java.util.List; continue; } // Only retrieve the jsonWriter if we need to. If we don't get this far, then - // researchLog.getValidJsonWriterLocked() will not ever be called, and the file - // will not have been opened for writing. + // researchLog.getInitializedJsonWriterLocked() will not ever be called, and the + // file will not have been opened for writing. if (jsonWriter == null) { - jsonWriter = researchLog.getValidJsonWriterLocked(); + jsonWriter = researchLog.getInitializedJsonWriterLocked(); outputLogUnitStart(jsonWriter, canIncludePrivateData); } logStatement.outputToLocked(jsonWriter, mTimeList.get(i), mValuesList.get(i)); diff --git a/java/src/com/android/inputmethod/research/LoggingUtils.java b/java/src/com/android/inputmethod/research/LoggingUtils.java new file mode 100644 index 000000000..1261d6780 --- /dev/null +++ b/java/src/com/android/inputmethod/research/LoggingUtils.java @@ -0,0 +1,38 @@ +/* + * 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 com.android.inputmethod.research; + +import android.view.MotionEvent; + +/* package */ class LoggingUtils { + private LoggingUtils() { + // This utility class is not publicly instantiable. + } + + /* package */ static String getMotionEventActionTypeString(final int actionType) { + switch (actionType) { + case MotionEvent.ACTION_CANCEL: return "CANCEL"; + case MotionEvent.ACTION_UP: return "UP"; + case MotionEvent.ACTION_DOWN: return "DOWN"; + case MotionEvent.ACTION_POINTER_UP: return "POINTER_UP"; + case MotionEvent.ACTION_POINTER_DOWN: return "POINTER_DOWN"; + case MotionEvent.ACTION_MOVE: return "MOVE"; + case MotionEvent.ACTION_OUTSIDE: return "OUTSIDE"; + default: return "ACTION_" + actionType; + } + } +} diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java index 3a87bf1df..9aa60f859 100644 --- a/java/src/com/android/inputmethod/research/MainLogBuffer.java +++ b/java/src/com/android/inputmethod/research/MainLogBuffer.java @@ -18,6 +18,7 @@ package com.android.inputmethod.research; import android.util.Log; +import com.android.inputmethod.annotations.UsedForTesting; import com.android.inputmethod.latin.Dictionary; import com.android.inputmethod.latin.Suggest; import com.android.inputmethod.latin.define.ProductionFlag; @@ -64,16 +65,11 @@ public abstract class MainLogBuffer extends FixedLogBuffer { // The size of the n-grams logged. E.g. N_GRAM_SIZE = 2 means to sample bigrams. public static final int N_GRAM_SIZE = 2; - // Whether all words should be recorded, leaving unsampled word between bigrams. Useful for - // testing. - /* package for test */ static final boolean IS_LOGGING_EVERYTHING = false - && ProductionFlag.IS_EXPERIMENTAL_DEBUG; - - // The number of words between n-grams to omit from the log. - private static final int DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES = - IS_LOGGING_EVERYTHING ? 0 : (DEBUG ? 2 : 18); - - private Suggest mSuggest; + // TODO: Remove dependence on Suggest, and pass in Dictionary as a parameter to an appropriate + // method. + private final Suggest mSuggest; + @UsedForTesting + private Dictionary mDictionaryForTesting; private boolean mIsStopping = false; /* package for test */ int mNumWordsBetweenNGrams; @@ -82,15 +78,25 @@ public abstract class MainLogBuffer extends FixedLogBuffer { // after a sample is taken. /* package for test */ int mNumWordsUntilSafeToSample; - public MainLogBuffer() { - super(N_GRAM_SIZE + DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES); - mNumWordsBetweenNGrams = DEFAULT_NUMBER_OF_WORDS_BETWEEN_SAMPLES; - final Random random = new Random(); - mNumWordsUntilSafeToSample = DEBUG ? 0 : random.nextInt(mNumWordsBetweenNGrams + 1); + public MainLogBuffer(final int wordsBetweenSamples, final int numInitialWordsToIgnore, + final Suggest suggest) { + super(N_GRAM_SIZE + wordsBetweenSamples); + mNumWordsBetweenNGrams = wordsBetweenSamples; + mNumWordsUntilSafeToSample = DEBUG ? 0 : numInitialWordsToIgnore; + mSuggest = suggest; } - public void setSuggest(final Suggest suggest) { - mSuggest = suggest; + @UsedForTesting + /* package for test */ void setDictionaryForTesting(final Dictionary dictionary) { + mDictionaryForTesting = dictionary; + } + + private Dictionary getDictionary() { + if (mDictionaryForTesting != null) { + return mDictionaryForTesting; + } + if (mSuggest == null || !mSuggest.hasMainDictionary()) return null; + return mSuggest.getMainDictionary(); } public void resetWordCounter() { @@ -114,7 +120,7 @@ public abstract class MainLogBuffer extends FixedLogBuffer { */ private boolean isSafeNGram(final ArrayList<LogUnit> logUnits, final int minNGramSize) { // Bypass privacy checks when debugging. - if (IS_LOGGING_EVERYTHING) { + if (ResearchLogger.IS_LOGGING_EVERYTHING) { if (mIsStopping) { return true; } @@ -137,16 +143,13 @@ public abstract class MainLogBuffer extends FixedLogBuffer { if (mNumWordsUntilSafeToSample > 0) { return false; } - if (mSuggest == null || !mSuggest.hasMainDictionary()) { - // Main dictionary is unavailable. Since we cannot check it, we cannot tell if a - // word is out-of-vocabulary or not. Therefore, we must judge the entire buffer - // contents to potentially pose a privacy risk. - return false; - } // Reload the dictionary in case it has changed (e.g., because the user has changed // languages). - final Dictionary dictionary = mSuggest.getMainDictionary(); + final Dictionary dictionary = getDictionary(); if (dictionary == null) { + // Main dictionary is unavailable. Since we cannot check it, we cannot tell if a + // word is out-of-vocabulary or not. Therefore, we must judge the entire buffer + // contents to potentially pose a privacy risk. return false; } @@ -220,10 +223,10 @@ public abstract class MainLogBuffer extends FixedLogBuffer { final boolean canIncludePrivateData); @Override - protected void shiftOutWords(int numWords) { - int oldNumActualWords = getNumActualWords(); + protected void shiftOutWords(final int numWords) { + final int oldNumActualWords = getNumActualWords(); super.shiftOutWords(numWords); - int numWordsShifted = oldNumActualWords - getNumActualWords(); + final int numWordsShifted = oldNumActualWords - getNumActualWords(); mNumWordsUntilSafeToSample -= numWordsShifted; if (DEBUG) { Log.d(TAG, "wordsUntilSafeToSample now at " + mNumWordsUntilSafeToSample); diff --git a/java/src/com/android/inputmethod/research/ResearchLog.java b/java/src/com/android/inputmethod/research/ResearchLog.java index 5114977d8..9016e23b3 100644 --- a/java/src/com/android/inputmethod/research/ResearchLog.java +++ b/java/src/com/android/inputmethod/research/ResearchLog.java @@ -38,12 +38,19 @@ import java.util.concurrent.TimeUnit; /** * Logs the use of the LatinIME keyboard. * - * This class logs operations on the IME keyboard, including what the user has typed. - * Data is stored locally in a file in app-specific storage. + * This class logs operations on the IME keyboard, including what the user has typed. Data is + * written to a {@link JsonWriter}, which will write to a local file. + * + * The JsonWriter is created on-demand by calling {@link #getInitializedJsonWriterLocked}. + * + * This class uses an executor to perform file-writing operations on a separate thread. It also + * tries to avoid creating unnecessary files if there is nothing to write. It also handles + * flushing, making sure it happens, but not too frequently. * * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}. */ public class ResearchLog { + // TODO: Automatically initialize the JsonWriter rather than requiring the caller to manage it. private static final String TAG = ResearchLog.class.getSimpleName(); private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG; private static final long FLUSH_DELAY_IN_MS = 1000 * 5; @@ -87,6 +94,12 @@ public class ResearchLog { mContext = context; } + /** + * Waits for any publication requests to finish and closes the {@link JsonWriter} used for + * output. + * + * See class comment for details about {@code JsonWriter} construction. + */ public synchronized void close(final Runnable onClosed) { mExecutor.submit(new Callable<Object>() { @Override @@ -94,20 +107,15 @@ public class ResearchLog { try { if (mHasWrittenData) { mJsonWriter.endArray(); - mJsonWriter.flush(); - mJsonWriter.close(); - if (DEBUG) { - Log.d(TAG, "wrote log to " + mFile); - } mHasWrittenData = false; - } else { - if (DEBUG) { - Log.d(TAG, "close() called, but no data, not outputting"); - } + } + mJsonWriter.flush(); + mJsonWriter.close(); + if (DEBUG) { + Log.d(TAG, "wrote log to " + mFile); } } catch (Exception e) { - Log.d(TAG, "error when closing ResearchLog:"); - e.printStackTrace(); + Log.d(TAG, "error when closing ResearchLog:", e); } finally { if (mFile != null && mFile.exists()) { mFile.setWritable(false, false); @@ -125,6 +133,12 @@ public class ResearchLog { private boolean mIsAbortSuccessful; + /** + * Waits for publication requests to finish, closes the {@link JsonWriter}, but then deletes the + * backing file used for output. + * + * See class comment for details about {@code JsonWriter} construction. + */ public synchronized void abort() { mExecutor.submit(new Callable<Object>() { @Override @@ -184,6 +198,12 @@ public class ResearchLog { mFlushFuture = mExecutor.schedule(mFlushCallable, FLUSH_DELAY_IN_MS, TimeUnit.MILLISECONDS); } + /** + * Queues up {@code logUnit} to be published in the background. + * + * @param logUnit the {@link LogUnit} to be published + * @param canIncludePrivateData whether private data in the LogUnit should be included + */ public synchronized void publish(final LogUnit logUnit, final boolean canIncludePrivateData) { try { mExecutor.submit(new Callable<Object>() { @@ -206,29 +226,39 @@ public class ResearchLog { * Return a JsonWriter for this ResearchLog. It is initialized the first time this method is * called. The cached value is returned in future calls. */ - public JsonWriter getValidJsonWriterLocked() { + public JsonWriter getInitializedJsonWriterLocked() { + if (mJsonWriter != NULL_JSON_WRITER || mFile == null) return mJsonWriter; try { - if (mJsonWriter == NULL_JSON_WRITER && mFile != null) { - final FileOutputStream fos = - mContext.openFileOutput(mFile.getName(), Context.MODE_PRIVATE); - mJsonWriter = new JsonWriter(new BufferedWriter(new OutputStreamWriter(fos))); - mJsonWriter.beginArray(); + final JsonWriter jsonWriter = createJsonWriter(mContext, mFile); + if (jsonWriter != null) { + jsonWriter.beginArray(); + mJsonWriter = jsonWriter; mHasWrittenData = true; } - } catch (IOException e) { - e.printStackTrace(); - Log.w(TAG, "Error in JsonWriter; disabling logging"); + } catch (final IOException e) { + Log.w(TAG, "Error in JsonWriter; disabling logging", e); try { mJsonWriter.close(); - } catch (IllegalStateException e1) { + } catch (final IllegalStateException e1) { // Assume that this is just the json not being terminated properly. // Ignore - } catch (IOException e1) { - e1.printStackTrace(); + } catch (final IOException e1) { + Log.w(TAG, "Error in closing JsonWriter; disabling logging", e1); } finally { mJsonWriter = NULL_JSON_WRITER; } } return mJsonWriter; } + + /** + * Create the JsonWriter to write the ResearchLog to. + * + * This method may be overriden in testing to redirect the output. + */ + /* package for test */ JsonWriter createJsonWriter(final Context context, final File file) + throws IOException { + return new JsonWriter(new BufferedWriter(new OutputStreamWriter( + context.openFileOutput(file.getName(), Context.MODE_PRIVATE)))); + } } diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java index 45212913e..e705ddda1 100644 --- a/java/src/com/android/inputmethod/research/ResearchLogger.java +++ b/java/src/com/android/inputmethod/research/ResearchLogger.java @@ -88,6 +88,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Locale; +import java.util.Random; import java.util.UUID; /** @@ -121,31 +122,36 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang // field holds a channel name, the developer does not have to re-enter it when using the // feedback mechanism to generate multiple tests. private static final boolean FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD = false; - public static final boolean DEFAULT_USABILITY_STUDY_MODE = false; /* package */ static boolean sIsLogging = false; private static final int OUTPUT_FORMAT_VERSION = 5; private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode"; - private static final String PREF_RESEARCH_HAS_SEEN_SPLASH = "pref_research_has_seen_splash"; /* package */ static final String LOG_FILENAME_PREFIX = "researchLog"; private static final String LOG_FILENAME_SUFFIX = ".txt"; /* package */ static final String USER_RECORDING_FILENAME_PREFIX = "recording"; private static final String USER_RECORDING_FILENAME_SUFFIX = ".txt"; private static final SimpleDateFormat TIMESTAMP_DATEFORMAT = new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US); + // Whether all words should be recorded, leaving unsampled word between bigrams. Useful for + // testing. + /* package for test */ static final boolean IS_LOGGING_EVERYTHING = false + && ProductionFlag.IS_EXPERIMENTAL_DEBUG; + // The number of words between n-grams to omit from the log. + private static final int NUMBER_OF_WORDS_BETWEEN_SAMPLES = + IS_LOGGING_EVERYTHING ? 0 : (DEBUG ? 2 : 18); + // Whether to show an indicator on the screen that logging is on. Currently a very small red // dot in the lower right hand corner. Most users should not notice it. private static final boolean IS_SHOWING_INDICATOR = true; // Change the default indicator to something very visible. Currently two red vertical bars on // either side of they keyboard. private static final boolean IS_SHOWING_INDICATOR_CLEARLY = false || - (MainLogBuffer.IS_LOGGING_EVERYTHING && ProductionFlag.IS_EXPERIMENTAL_DEBUG); + (IS_LOGGING_EVERYTHING && ProductionFlag.IS_EXPERIMENTAL_DEBUG); // FEEDBACK_WORD_BUFFER_SIZE should add 1 because it must also hold the feedback LogUnit itself. public static final int FEEDBACK_WORD_BUFFER_SIZE = (Integer.MAX_VALUE - 1) + 1; // constants related to specific log points private static final String WHITESPACE_SEPARATORS = " \t\n\r"; private static final int MAX_INPUTVIEW_LENGTH_TO_CAPTURE = 8192; // must be >=1 - private static final String PREF_RESEARCH_LOGGER_UUID_STRING = "pref_research_logger_uuid"; private static final String PREF_RESEARCH_SAVED_CHANNEL = "pref_research_saved_channel"; private static final ResearchLogger sInstance = new ResearchLogger(); @@ -153,7 +159,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private static String sAllowedAccountDomain = null; // to write to a different filename, e.g., for testing, set mFile before calling start() /* package */ File mFilesDir; - /* package */ String mUUIDString; /* package */ ResearchLog mMainResearchLog; // mFeedbackLog records all events for the session, private or not (excepting // passwords). It is written to permanent storage only if the user explicitly commands @@ -199,7 +204,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private Intent mUploadIntent; private Intent mUploadNowIntent; - private LogUnit mCurrentLogUnit = new LogUnit(); + /* package for test */ LogUnit mCurrentLogUnit = new LogUnit(); // Gestured or tapped words may be committed after the gesture of the next word has started. // To ensure that the gesture data of the next word is not associated with the previous word, @@ -228,50 +233,44 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang return sInstance; } - public void init(final LatinIME latinIME, final KeyboardSwitcher keyboardSwitcher) { + public void init(final LatinIME latinIME, final KeyboardSwitcher keyboardSwitcher, + final Suggest suggest) { assert latinIME != null; - if (latinIME == null) { - Log.w(TAG, "IMS is null; logging is off"); - } else { - mFilesDir = latinIME.getFilesDir(); - if (mFilesDir == null || !mFilesDir.exists()) { - Log.w(TAG, "IME storage directory does not exist."); - } - } - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(latinIME); - if (prefs != null) { - mUUIDString = getUUID(prefs); - if (!prefs.contains(PREF_USABILITY_STUDY_MODE)) { - Editor e = prefs.edit(); - e.putBoolean(PREF_USABILITY_STUDY_MODE, DEFAULT_USABILITY_STUDY_MODE); - e.apply(); - } - sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false); - prefs.registerOnSharedPreferenceChangeListener(this); - - final long lastCleanupTime = prefs.getLong(PREF_LAST_CLEANUP_TIME, 0L); - final long now = System.currentTimeMillis(); - if (lastCleanupTime + DURATION_BETWEEN_DIR_CLEANUP_IN_MS < now) { - final long timeHorizon = now - MAX_LOGFILE_AGE_IN_MS; - cleanupLoggingDir(mFilesDir, timeHorizon); - Editor e = prefs.edit(); - e.putLong(PREF_LAST_CLEANUP_TIME, now); - e.apply(); - } + mLatinIME = latinIME; + mFilesDir = latinIME.getFilesDir(); + if (mFilesDir == null || !mFilesDir.exists()) { + Log.w(TAG, "IME storage directory does not exist. Cannot start logging."); + return; } + mPrefs = PreferenceManager.getDefaultSharedPreferences(latinIME); + mPrefs.registerOnSharedPreferenceChangeListener(this); + + // Initialize fields from preferences + sIsLogging = ResearchSettings.readResearchLoggerEnabledFlag(mPrefs); + + // Initialize fields from resources final Resources res = latinIME.getResources(); sAccountType = res.getString(R.string.research_account_type); sAllowedAccountDomain = res.getString(R.string.research_allowed_account_domain); - mLatinIME = latinIME; - mPrefs = prefs; + + // Cleanup logging directory + // TODO: Move this and other file-related components to separate file. + final long lastCleanupTime = mPrefs.getLong(PREF_LAST_CLEANUP_TIME, 0L); + final long now = System.currentTimeMillis(); + if (now - lastCleanupTime > DURATION_BETWEEN_DIR_CLEANUP_IN_MS) { + final long timeHorizon = now - MAX_LOGFILE_AGE_IN_MS; + cleanupLoggingDir(mFilesDir, timeHorizon); + mPrefs.edit().putLong(PREF_LAST_CLEANUP_TIME, now).apply(); + } + + // Initialize external services mUploadIntent = new Intent(mLatinIME, UploaderService.class); mUploadNowIntent = new Intent(mLatinIME, UploaderService.class); mUploadNowIntent.putExtra(UploaderService.EXTRA_UPLOAD_UNCONDITIONALLY, true); - mReplayer.setKeyboardSwitcher(keyboardSwitcher); - if (ProductionFlag.IS_EXPERIMENTAL) { scheduleUploadingService(mLatinIME); } + mReplayer.setKeyboardSwitcher(keyboardSwitcher); } /** @@ -313,14 +312,16 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang mMainKeyboardView = null; } - private boolean hasSeenSplash() { - return mPrefs.getBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, false); + public void onDestroy() { + if (mPrefs != null) { + mPrefs.unregisterOnSharedPreferenceChangeListener(this); + } } private Dialog mSplashDialog = null; private void maybeShowSplashScreen() { - if (hasSeenSplash()) { + if (ResearchSettings.readHasSeenSplash(mPrefs)) { return; } if (mSplashDialog != null && mSplashDialog.isShowing()) { @@ -373,32 +374,23 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } public void onUserLoggingConsent() { - setLoggingAllowed(true); if (mPrefs == null) { - return; + mPrefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME); + if (mPrefs == null) return; } - final Editor e = mPrefs.edit(); - e.putBoolean(PREF_RESEARCH_HAS_SEEN_SPLASH, true); - e.apply(); + sIsLogging = true; + ResearchSettings.writeResearchLoggerEnabledFlag(mPrefs, true); + ResearchSettings.writeHasSeenSplash(mPrefs, true); restart(); } - private void setLoggingAllowed(boolean enableLogging) { - if (mPrefs == null) { - return; - } - Editor e = mPrefs.edit(); - e.putBoolean(PREF_USABILITY_STUDY_MODE, enableLogging); - e.apply(); - sIsLogging = enableLogging; - } - private static int sLogFileCounter = 0; private File createLogFile(final File filesDir) { final StringBuilder sb = new StringBuilder(); sb.append(LOG_FILENAME_PREFIX).append('-'); - sb.append(mUUIDString).append('-'); + final String uuid = ResearchSettings.readResearchLoggerUuid(mPrefs); + sb.append(uuid).append('-'); sb.append(TIMESTAMP_DATEFORMAT.format(new Date())).append('-'); // Sometimes logFiles are created within milliseconds of each other. Append a counter to // separate these. @@ -416,7 +408,8 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang private File createUserRecordingFile(final File filesDir) { final StringBuilder sb = new StringBuilder(); sb.append(USER_RECORDING_FILENAME_PREFIX).append('-'); - sb.append(mUUIDString).append('-'); + final String uuid = ResearchSettings.readResearchLoggerUuid(mPrefs); + sb.append(uuid).append('-'); sb.append(TIMESTAMP_DATEFORMAT.format(new Date())); sb.append(USER_RECORDING_FILENAME_SUFFIX); return new File(filesDir, sb.toString()); @@ -458,17 +451,15 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang // Log.w(TAG, "not in usability mode; not logging"); return; } - if (mFilesDir == null || !mFilesDir.exists()) { - Log.w(TAG, "IME storage directory does not exist. Cannot start logging."); - return; - } if (mMainLogBuffer == null) { mMainResearchLog = new ResearchLog(createLogFile(mFilesDir), mLatinIME); - mMainLogBuffer = new MainLogBuffer() { + final int numWordsToIgnore = new Random().nextInt(NUMBER_OF_WORDS_BETWEEN_SAMPLES + 1); + mMainLogBuffer = new MainLogBuffer(NUMBER_OF_WORDS_BETWEEN_SAMPLES, numWordsToIgnore, + mSuggest) { @Override protected void publish(final ArrayList<LogUnit> logUnits, boolean canIncludePrivateData) { - canIncludePrivateData |= MainLogBuffer.IS_LOGGING_EVERYTHING; + canIncludePrivateData |= IS_LOGGING_EVERYTHING; final int length = logUnits.size(); for (int i = 0; i < length; i++) { final LogUnit logUnit = logUnits.get(i); @@ -487,7 +478,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } }; - mMainLogBuffer.setSuggest(mSuggest); } if (mFeedbackLogBuffer == null) { resetFeedbackLogging(); @@ -564,7 +554,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } @Override - public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { + public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) { if (key == null || prefs == null) { return; } @@ -586,7 +576,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang presentFeedbackDialog(latinIME); } - public void presentFeedbackDialog(LatinIME latinIME) { + public void presentFeedbackDialog(final LatinIME latinIME) { if (isMakingUserRecording()) { saveRecording(); } @@ -818,9 +808,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang if (mPrefs == null) { return; } - final Editor e = mPrefs.edit(); - e.putString(PREF_RESEARCH_SAVED_CHANNEL, channelName); - e.apply(); + mPrefs.edit().putString(PREF_RESEARCH_SAVED_CHANNEL, channelName).apply(); } } @@ -835,10 +823,13 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang mInFeedbackDialog = false; } - public void initSuggest(Suggest suggest) { + public void initSuggest(final Suggest suggest) { mSuggest = suggest; + // MainLogBuffer has out-of-date Suggest object. Need to close it down and create a new + // one. if (mMainLogBuffer != null) { - mMainLogBuffer.setSuggest(mSuggest); + stop(); + start(); } } @@ -1127,18 +1118,6 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang } } - private static String getUUID(final SharedPreferences prefs) { - String uuidString = prefs.getString(PREF_RESEARCH_LOGGER_UUID_STRING, null); - if (null == uuidString) { - UUID uuid = UUID.randomUUID(); - uuidString = uuid.toString(); - Editor editor = prefs.edit(); - editor.putString(PREF_RESEARCH_LOGGER_UUID_STRING, uuidString); - editor.apply(); - } - return uuidString; - } - private String scrubWord(String word) { final Dictionary dictionary = getDictionary(); if (dictionary == null) { @@ -1185,12 +1164,12 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang 0); final Integer versionCode = packageInfo.versionCode; final String versionName = packageInfo.versionName; + final String uuid = ResearchSettings.readResearchLoggerUuid(researchLogger.mPrefs); researchLogger.enqueueEvent(LOGSTATEMENT_LATIN_IME_ON_START_INPUT_VIEW_INTERNAL, - researchLogger.mUUIDString, editorInfo.packageName, - Integer.toHexString(editorInfo.inputType), + uuid, editorInfo.packageName, Integer.toHexString(editorInfo.inputType), Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId, Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName, - OUTPUT_FORMAT_VERSION, MainLogBuffer.IS_LOGGING_EVERYTHING, + OUTPUT_FORMAT_VERSION, IS_LOGGING_EVERYTHING, ProductionFlag.IS_EXPERIMENTAL_DEBUG); } catch (NameNotFoundException e) { e.printStackTrace(); @@ -1226,17 +1205,7 @@ public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChang public static void mainKeyboardView_processMotionEvent(final MotionEvent me, final int action, final long eventTime, final int index, final int id, final int x, final int y) { if (me != null) { - final String actionString; - switch (action) { - case MotionEvent.ACTION_CANCEL: actionString = "CANCEL"; break; - case MotionEvent.ACTION_UP: actionString = "UP"; break; - case MotionEvent.ACTION_DOWN: actionString = "DOWN"; break; - case MotionEvent.ACTION_POINTER_UP: actionString = "POINTER_UP"; break; - case MotionEvent.ACTION_POINTER_DOWN: actionString = "POINTER_DOWN"; break; - case MotionEvent.ACTION_MOVE: actionString = "MOVE"; break; - case MotionEvent.ACTION_OUTSIDE: actionString = "OUTSIDE"; break; - default: actionString = "ACTION_" + action; break; - } + final String actionString = LoggingUtils.getMotionEventActionTypeString(action); final ResearchLogger researchLogger = getInstance(); researchLogger.enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT, actionString, false /* IS_LOGGING_RELATED */, MotionEvent.obtain(me)); diff --git a/java/src/com/android/inputmethod/research/ResearchSettings.java b/java/src/com/android/inputmethod/research/ResearchSettings.java new file mode 100644 index 000000000..11e9ac77a --- /dev/null +++ b/java/src/com/android/inputmethod/research/ResearchSettings.java @@ -0,0 +1,61 @@ +/* + * 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 com.android.inputmethod.research; + +import android.content.SharedPreferences; + +import java.util.UUID; + +public final class ResearchSettings { + public static final String PREF_RESEARCH_LOGGER_UUID = "pref_research_logger_uuid"; + public static final String PREF_RESEARCH_LOGGER_ENABLED_FLAG = + "pref_research_logger_enabled_flag"; + public static final String PREF_RESEARCH_LOGGER_HAS_SEEN_SPLASH = + "pref_research_logger_has_seen_splash"; + + private ResearchSettings() { + // Intentional empty constructor for singleton. + } + + public static String readResearchLoggerUuid(final SharedPreferences prefs) { + if (prefs.contains(PREF_RESEARCH_LOGGER_UUID)) { + return prefs.getString(PREF_RESEARCH_LOGGER_UUID, null); + } + // Generate a random string as uuid if not yet set + final String newUuid = UUID.randomUUID().toString(); + prefs.edit().putString(PREF_RESEARCH_LOGGER_UUID, newUuid).apply(); + return newUuid; + } + + public static boolean readResearchLoggerEnabledFlag(final SharedPreferences prefs) { + return prefs.getBoolean(PREF_RESEARCH_LOGGER_ENABLED_FLAG, false); + } + + public static void writeResearchLoggerEnabledFlag(final SharedPreferences prefs, + final boolean isEnabled) { + prefs.edit().putBoolean(PREF_RESEARCH_LOGGER_ENABLED_FLAG, isEnabled).apply(); + } + + public static boolean readHasSeenSplash(final SharedPreferences prefs) { + return prefs.getBoolean(PREF_RESEARCH_LOGGER_HAS_SEEN_SPLASH, false); + } + + public static void writeHasSeenSplash(final SharedPreferences prefs, + final boolean hasSeenSplash) { + prefs.edit().putBoolean(PREF_RESEARCH_LOGGER_HAS_SEEN_SPLASH, hasSeenSplash).apply(); + } +} diff --git a/java/src/com/android/inputmethod/research/Uploader.java b/java/src/com/android/inputmethod/research/Uploader.java new file mode 100644 index 000000000..df495a88d --- /dev/null +++ b/java/src/com/android/inputmethod/research/Uploader.java @@ -0,0 +1,180 @@ +/* + * 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 com.android.inputmethod.research; + +import android.Manifest; +import android.app.AlarmManager; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.BatteryManager; +import android.text.TextUtils; +import android.util.Log; + +import com.android.inputmethod.latin.R; +import com.android.inputmethod.latin.define.ProductionFlag; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +/** + * Manages the uploading of ResearchLog files. + */ +public final class Uploader { + private static final String TAG = Uploader.class.getSimpleName(); + private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG; + // Set IS_INHIBITING_AUTO_UPLOAD to true for local testing + private static final boolean IS_INHIBITING_AUTO_UPLOAD = false + && ProductionFlag.IS_EXPERIMENTAL_DEBUG; // Force false for non-debug builds + private static final int BUF_SIZE = 1024 * 8; + + private final Context mContext; + private final File mFilesDir; + private final URL mUrl; + + public Uploader(final Context context) { + mContext = context; + mFilesDir = context.getFilesDir(); + + final String urlString = context.getString(R.string.research_logger_upload_url); + if (TextUtils.isEmpty(urlString)) { + mUrl = null; + return; + } + URL url = null; + try { + url = new URL(urlString); + } catch (final MalformedURLException e) { + Log.e(TAG, "Bad URL for uploading", e); + } + mUrl = url; + } + + public boolean isPossibleToUpload() { + return hasUploadingPermission() && mUrl != null && !IS_INHIBITING_AUTO_UPLOAD; + } + + private boolean hasUploadingPermission() { + final PackageManager packageManager = mContext.getPackageManager(); + return packageManager.checkPermission(Manifest.permission.INTERNET, + mContext.getPackageName()) == PackageManager.PERMISSION_GRANTED; + } + + public boolean isConvenientToUpload() { + return isExternallyPowered() && hasWifiConnection(); + } + + private boolean isExternallyPowered() { + final Intent intent = mContext.registerReceiver(null, new IntentFilter( + Intent.ACTION_BATTERY_CHANGED)); + final int pluggedState = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); + return pluggedState == BatteryManager.BATTERY_PLUGGED_AC + || pluggedState == BatteryManager.BATTERY_PLUGGED_USB; + } + + private boolean hasWifiConnection() { + final ConnectivityManager manager = + (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); + final NetworkInfo wifiInfo = manager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + return wifiInfo.isConnected(); + } + + public void doUpload() { + if (mFilesDir == null) { + return; + } + final File[] files = mFilesDir.listFiles(new FileFilter() { + @Override + public boolean accept(final File pathname) { + return pathname.getName().startsWith(ResearchLogger.LOG_FILENAME_PREFIX) + && !pathname.canWrite(); + } + }); + for (final File file : files) { + uploadFile(file); + } + } + + private void uploadFile(final File file) { + if (DEBUG) { + Log.d(TAG, "attempting upload of " + file.getAbsolutePath()); + } + final int contentLength = (int) file.length(); + HttpURLConnection connection = null; + InputStream fileInputStream = null; + try { + fileInputStream = new FileInputStream(file); + connection = (HttpURLConnection) mUrl.openConnection(); + connection.setRequestMethod("PUT"); + connection.setDoOutput(true); + connection.setFixedLengthStreamingMode(contentLength); + final OutputStream outputStream = connection.getOutputStream(); + uploadContents(fileInputStream, outputStream); + if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { + Log.d(TAG, "upload failed: " + connection.getResponseCode()); + final InputStream netInputStream = connection.getInputStream(); + final BufferedReader reader = new BufferedReader(new InputStreamReader( + netInputStream)); + String line; + while ((line = reader.readLine()) != null) { + Log.d(TAG, "| " + reader.readLine()); + } + reader.close(); + return; + } + file.delete(); + if (DEBUG) { + Log.d(TAG, "upload successful"); + } + } catch (final IOException e) { + Log.e(TAG, "Exception uploading file", e); + } finally { + if (fileInputStream != null) { + try { + fileInputStream.close(); + } catch (final IOException e) { + Log.e(TAG, "Exception closing uploaded file", e); + } + } + if (connection != null) { + connection.disconnect(); + } + } + } + + private static void uploadContents(final InputStream is, final OutputStream os) + throws IOException { + // TODO: Switch to NIO. + final byte[] buf = new byte[BUF_SIZE]; + int numBytesRead; + while ((numBytesRead = is.read(buf)) != -1) { + os.write(buf, 0, numBytesRead); + } + } +} diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java index 89c67fbb2..26b651056 100644 --- a/java/src/com/android/inputmethod/research/UploaderService.java +++ b/java/src/com/android/inputmethod/research/UploaderService.java @@ -16,189 +16,44 @@ package com.android.inputmethod.research; -import android.Manifest; import android.app.AlarmManager; import android.app.IntentService; -import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageManager; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.BatteryManager; import android.os.Bundle; -import android.util.Log; -import com.android.inputmethod.latin.R; import com.android.inputmethod.latin.define.ProductionFlag; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileFilter; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; - +/** + * Service to invoke the uploader. + * + * Can be regularly invoked, invoked on boot, etc. + */ public final class UploaderService extends IntentService { private static final String TAG = UploaderService.class.getSimpleName(); private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG; - // Set IS_INHIBITING_AUTO_UPLOAD to true for local testing - private static final boolean IS_INHIBITING_AUTO_UPLOAD = false - && ProductionFlag.IS_EXPERIMENTAL_DEBUG; // Force false in production public static final long RUN_INTERVAL = AlarmManager.INTERVAL_HOUR; public static final String EXTRA_UPLOAD_UNCONDITIONALLY = UploaderService.class.getName() + ".extra.UPLOAD_UNCONDITIONALLY"; - private static final int BUF_SIZE = 1024 * 8; protected static final int TIMEOUT_IN_MS = 1000 * 4; - private boolean mCanUpload; - private File mFilesDir; - private URL mUrl; - public UploaderService() { super("Research Uploader Service"); } @Override - public void onCreate() { - super.onCreate(); - - mCanUpload = false; - mFilesDir = null; - mUrl = null; - - final PackageManager packageManager = getPackageManager(); - final boolean hasPermission = packageManager.checkPermission(Manifest.permission.INTERNET, - getPackageName()) == PackageManager.PERMISSION_GRANTED; - if (!hasPermission) { - return; - } - - try { - final String urlString = getString(R.string.research_logger_upload_url); - if (urlString == null || urlString.equals("")) { - return; - } - mFilesDir = getFilesDir(); - mUrl = new URL(urlString); - mCanUpload = true; - } catch (MalformedURLException e) { - e.printStackTrace(); - } - } - - @Override - protected void onHandleIntent(Intent intent) { - if (!mCanUpload) { - return; - } - boolean isUploadingUnconditionally = false; - Bundle bundle = intent.getExtras(); - if (bundle != null && bundle.containsKey(EXTRA_UPLOAD_UNCONDITIONALLY)) { - isUploadingUnconditionally = bundle.getBoolean(EXTRA_UPLOAD_UNCONDITIONALLY); - } - doUpload(isUploadingUnconditionally); - } - - private boolean isExternallyPowered() { - final Intent intent = registerReceiver(null, new IntentFilter( - Intent.ACTION_BATTERY_CHANGED)); - final int pluggedState = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1); - return pluggedState == BatteryManager.BATTERY_PLUGGED_AC - || pluggedState == BatteryManager.BATTERY_PLUGGED_USB; - } - - private boolean hasWifiConnection() { - final ConnectivityManager manager = - (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); - final NetworkInfo wifiInfo = manager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); - return wifiInfo.isConnected(); - } - - private void doUpload(final boolean isUploadingUnconditionally) { - if (!isUploadingUnconditionally && (!isExternallyPowered() || !hasWifiConnection() - || IS_INHIBITING_AUTO_UPLOAD)) { - return; - } - if (mFilesDir == null) { - return; - } - final File[] files = mFilesDir.listFiles(new FileFilter() { - @Override - public boolean accept(File pathname) { - return pathname.getName().startsWith(ResearchLogger.LOG_FILENAME_PREFIX) - && !pathname.canWrite(); - } - }); - boolean success = true; - if (files.length == 0) { - success = false; - } - for (final File file : files) { - if (!uploadFile(file)) { - success = false; - } + protected void onHandleIntent(final Intent intent) { + final Uploader uploader = new Uploader(this); + if (!uploader.isPossibleToUpload()) return; + if (isUploadingUnconditionally(intent.getExtras()) || uploader.isConvenientToUpload()) { + uploader.doUpload(); } } - private boolean uploadFile(File file) { - if (DEBUG) { - Log.d(TAG, "attempting upload of " + file.getAbsolutePath()); - } - boolean success = false; - final int contentLength = (int) file.length(); - HttpURLConnection connection = null; - InputStream fileInputStream = null; - try { - fileInputStream = new FileInputStream(file); - connection = (HttpURLConnection) mUrl.openConnection(); - connection.setRequestMethod("PUT"); - connection.setDoOutput(true); - connection.setFixedLengthStreamingMode(contentLength); - final OutputStream os = connection.getOutputStream(); - final byte[] buf = new byte[BUF_SIZE]; - int numBytesRead; - while ((numBytesRead = fileInputStream.read(buf)) != -1) { - os.write(buf, 0, numBytesRead); - if (DEBUG) { - Log.d(TAG, new String(buf)); - } - } - if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { - Log.d(TAG, "upload failed: " + connection.getResponseCode()); - InputStream netInputStream = connection.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(netInputStream)); - String line; - while ((line = reader.readLine()) != null) { - Log.d(TAG, "| " + reader.readLine()); - } - reader.close(); - return success; - } - file.delete(); - success = true; - if (DEBUG) { - Log.d(TAG, "upload successful"); - } - } catch (Exception e) { - e.printStackTrace(); - } finally { - if (fileInputStream != null) { - try { - fileInputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - if (connection != null) { - connection.disconnect(); - } + private boolean isUploadingUnconditionally(final Bundle bundle) { + if (bundle == null) return false; + if (bundle.containsKey(EXTRA_UPLOAD_UNCONDITIONALLY)) { + return bundle.getBoolean(EXTRA_UPLOAD_UNCONDITIONALLY); } - return success; + return false; } } |