diff options
author | 2024-12-16 21:45:41 -0500 | |
---|---|---|
committer | 2025-01-11 14:17:35 -0500 | |
commit | e9a0e66716dab4dd3184d009d8920de1961efdfa (patch) | |
tree | 02dcc096643d74645bf28459c2834c3d4a2ad7f2 /java/src/org/kelar/inputmethod/compat | |
parent | fb3b9360d70596d7e921de8bf7d3ca99564a077e (diff) | |
download | latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.tar.gz latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.tar.xz latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.zip |
Rename to Kelar Keyboard (org.kelar.inputmethod.latin)
Diffstat (limited to 'java/src/org/kelar/inputmethod/compat')
28 files changed, 2059 insertions, 0 deletions
diff --git a/java/src/org/kelar/inputmethod/compat/ActivityManagerCompatUtils.java b/java/src/org/kelar/inputmethod/compat/ActivityManagerCompatUtils.java new file mode 100644 index 000000000..b39d274d7 --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/ActivityManagerCompatUtils.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.app.ActivityManager; +import android.content.Context; + +import java.lang.reflect.Method; + +public class ActivityManagerCompatUtils { + private static final Object LOCK = new Object(); + private static volatile Boolean sBoolean = null; + private static final Method METHOD_isLowRamDevice = CompatUtils.getMethod( + ActivityManager.class, "isLowRamDevice"); + + private ActivityManagerCompatUtils() { + // Do not instantiate this class. + } + + public static boolean isLowRamDevice(Context context) { + if (sBoolean == null) { + synchronized(LOCK) { + if (sBoolean == null) { + final ActivityManager am = + (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + sBoolean = (Boolean)CompatUtils.invoke(am, false, METHOD_isLowRamDevice); + } + } + } + return sBoolean; + } +} diff --git a/java/src/org/kelar/inputmethod/compat/AppWorkaroundsHelper.java b/java/src/org/kelar/inputmethod/compat/AppWorkaroundsHelper.java new file mode 100644 index 000000000..42fbd62c4 --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/AppWorkaroundsHelper.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.content.pm.PackageInfo; + +@SuppressWarnings("unused") +public class AppWorkaroundsHelper { + private AppWorkaroundsHelper() { + // This helper class is not publicly instantiable. + } + + public static boolean evaluateIsBrokenByRecorrection(final PackageInfo info) { + return false; + } +} diff --git a/java/src/org/kelar/inputmethod/compat/AppWorkaroundsUtils.java b/java/src/org/kelar/inputmethod/compat/AppWorkaroundsUtils.java new file mode 100644 index 000000000..5e0187813 --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/AppWorkaroundsUtils.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.content.pm.PackageInfo; +import android.os.Build.VERSION_CODES; + +/** + * A class to encapsulate work-arounds specific to particular apps. + */ +public class AppWorkaroundsUtils { + private final PackageInfo mPackageInfo; // May be null + private final boolean mIsBrokenByRecorrection; + + public AppWorkaroundsUtils(final PackageInfo packageInfo) { + mPackageInfo = packageInfo; + mIsBrokenByRecorrection = AppWorkaroundsHelper.evaluateIsBrokenByRecorrection( + packageInfo); + } + + public boolean isBrokenByRecorrection() { + return mIsBrokenByRecorrection; + } + + public boolean isBeforeJellyBean() { + if (null == mPackageInfo || null == mPackageInfo.applicationInfo) { + return false; + } + return mPackageInfo.applicationInfo.targetSdkVersion < VERSION_CODES.JELLY_BEAN; + } + + @Override + public String toString() { + if (null == mPackageInfo || null == mPackageInfo.applicationInfo) { + return ""; + } + final StringBuilder s = new StringBuilder(); + s.append("Target application : ") + .append(mPackageInfo.applicationInfo.name) + .append("\nPackage : ") + .append(mPackageInfo.applicationInfo.packageName) + .append("\nTarget app sdk version : ") + .append(mPackageInfo.applicationInfo.targetSdkVersion); + return s.toString(); + } +} diff --git a/java/src/org/kelar/inputmethod/compat/BuildCompatUtils.java b/java/src/org/kelar/inputmethod/compat/BuildCompatUtils.java new file mode 100644 index 000000000..c1080eb4c --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/BuildCompatUtils.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.os.Build; + +public final class BuildCompatUtils { + private BuildCompatUtils() { + // This utility class is not publicly instantiable. + } + + private static final boolean IS_RELEASE_BUILD = Build.VERSION.CODENAME.equals("REL"); + + /** + * The "effective" API version. + * {@link android.os.Build.VERSION#SDK_INT} if the platform is a release build. + * {@link android.os.Build.VERSION#SDK_INT} plus 1 if the platform is a development build. + */ + public static final int EFFECTIVE_SDK_INT = IS_RELEASE_BUILD + ? Build.VERSION.SDK_INT + : Build.VERSION.SDK_INT + 1; +} diff --git a/java/src/org/kelar/inputmethod/compat/CharacterCompat.java b/java/src/org/kelar/inputmethod/compat/CharacterCompat.java new file mode 100644 index 000000000..601a5dcf6 --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/CharacterCompat.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import java.lang.reflect.Method; + +public final class CharacterCompat { + // Note that Character.isAlphabetic(int), has been introduced in API level 19 + // (Build.VERSION_CODE.KITKAT). + private static final Method METHOD_isAlphabetic = CompatUtils.getMethod( + Character.class, "isAlphabetic", int.class); + + private CharacterCompat() { + // This utility class is not publicly instantiable. + } + + public static boolean isAlphabetic(final int code) { + if (METHOD_isAlphabetic != null) { + return (Boolean)CompatUtils.invoke(null, false, METHOD_isAlphabetic, code); + } + switch (Character.getType(code)) { + case Character.UPPERCASE_LETTER: + case Character.LOWERCASE_LETTER: + case Character.TITLECASE_LETTER: + case Character.MODIFIER_LETTER: + case Character.OTHER_LETTER: + case Character.LETTER_NUMBER: + return true; + default: + return false; + } + } +} diff --git a/java/src/org/kelar/inputmethod/compat/CompatUtils.java b/java/src/org/kelar/inputmethod/compat/CompatUtils.java new file mode 100644 index 000000000..475685927 --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/CompatUtils.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.text.TextUtils; +import android.util.Log; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public final class CompatUtils { + private static final String TAG = CompatUtils.class.getSimpleName(); + + private CompatUtils() { + // This utility class is not publicly instantiable. + } + + public static Class<?> getClass(final String className) { + try { + return Class.forName(className); + } catch (final ClassNotFoundException e) { + return null; + } + } + + public static Method getMethod(final Class<?> targetClass, final String name, + final Class<?>... parameterTypes) { + if (targetClass == null || TextUtils.isEmpty(name)) { + return null; + } + try { + return targetClass.getMethod(name, parameterTypes); + } catch (final SecurityException | NoSuchMethodException e) { + // ignore + } + return null; + } + + public static Field getField(final Class<?> targetClass, final String name) { + if (targetClass == null || TextUtils.isEmpty(name)) { + return null; + } + try { + return targetClass.getField(name); + } catch (final SecurityException | NoSuchFieldException e) { + // ignore + } + return null; + } + + public static Constructor<?> getConstructor(final Class<?> targetClass, + final Class<?> ... types) { + if (targetClass == null || types == null) { + return null; + } + try { + return targetClass.getConstructor(types); + } catch (final SecurityException | NoSuchMethodException e) { + // ignore + } + return null; + } + + public static Object newInstance(final Constructor<?> constructor, final Object ... args) { + if (constructor == null) { + return null; + } + try { + return constructor.newInstance(args); + } catch (final InstantiationException | IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + Log.e(TAG, "Exception in newInstance", e); + } + return null; + } + + public static Object invoke(final Object receiver, final Object defaultValue, + final Method method, final Object... args) { + if (method == null) { + return defaultValue; + } + try { + return method.invoke(receiver, args); + } catch (final IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + Log.e(TAG, "Exception in invoke", e); + } + return defaultValue; + } + + public static Object getFieldValue(final Object receiver, final Object defaultValue, + final Field field) { + if (field == null) { + return defaultValue; + } + try { + return field.get(receiver); + } catch (final IllegalAccessException | IllegalArgumentException e) { + Log.e(TAG, "Exception in getFieldValue", e); + } + return defaultValue; + } + + public static void setFieldValue(final Object receiver, final Field field, final Object value) { + if (field == null) { + return; + } + try { + field.set(receiver, value); + } catch (final IllegalAccessException | IllegalArgumentException e) { + Log.e(TAG, "Exception in setFieldValue", e); + } + } + + public static ClassWrapper getClassWrapper(final String className) { + return new ClassWrapper(getClass(className)); + } + + public static final class ClassWrapper { + private final Class<?> mClass; + public ClassWrapper(final Class<?> targetClass) { + mClass = targetClass; + } + + public boolean exists() { + return mClass != null; + } + + public <T> ToObjectMethodWrapper<T> getMethod(final String name, + final T defaultValue, final Class<?>... parameterTypes) { + return new ToObjectMethodWrapper<>(CompatUtils.getMethod(mClass, name, parameterTypes), + defaultValue); + } + + public ToIntMethodWrapper getPrimitiveMethod(final String name, final int defaultValue, + final Class<?>... parameterTypes) { + return new ToIntMethodWrapper(CompatUtils.getMethod(mClass, name, parameterTypes), + defaultValue); + } + + public ToFloatMethodWrapper getPrimitiveMethod(final String name, final float defaultValue, + final Class<?>... parameterTypes) { + return new ToFloatMethodWrapper(CompatUtils.getMethod(mClass, name, parameterTypes), + defaultValue); + } + + public ToBooleanMethodWrapper getPrimitiveMethod(final String name, + final boolean defaultValue, final Class<?>... parameterTypes) { + return new ToBooleanMethodWrapper(CompatUtils.getMethod(mClass, name, parameterTypes), + defaultValue); + } + } + + public static final class ToObjectMethodWrapper<T> { + private final Method mMethod; + private final T mDefaultValue; + public ToObjectMethodWrapper(final Method method, final T defaultValue) { + mMethod = method; + mDefaultValue = defaultValue; + } + @SuppressWarnings("unchecked") + public T invoke(final Object receiver, final Object... args) { + return (T) CompatUtils.invoke(receiver, mDefaultValue, mMethod, args); + } + } + + public static final class ToIntMethodWrapper { + private final Method mMethod; + private final int mDefaultValue; + public ToIntMethodWrapper(final Method method, final int defaultValue) { + mMethod = method; + mDefaultValue = defaultValue; + } + public int invoke(final Object receiver, final Object... args) { + return (int) CompatUtils.invoke(receiver, mDefaultValue, mMethod, args); + } + } + + public static final class ToFloatMethodWrapper { + private final Method mMethod; + private final float mDefaultValue; + public ToFloatMethodWrapper(final Method method, final float defaultValue) { + mMethod = method; + mDefaultValue = defaultValue; + } + public float invoke(final Object receiver, final Object... args) { + return (float) CompatUtils.invoke(receiver, mDefaultValue, mMethod, args); + } + } + + public static final class ToBooleanMethodWrapper { + private final Method mMethod; + private final boolean mDefaultValue; + public ToBooleanMethodWrapper(final Method method, final boolean defaultValue) { + mMethod = method; + mDefaultValue = defaultValue; + } + public boolean invoke(final Object receiver, final Object... args) { + return (boolean) CompatUtils.invoke(receiver, mDefaultValue, mMethod, args); + } + } +} diff --git a/java/src/org/kelar/inputmethod/compat/ConnectivityManagerCompatUtils.java b/java/src/org/kelar/inputmethod/compat/ConnectivityManagerCompatUtils.java new file mode 100644 index 000000000..469c8d590 --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/ConnectivityManagerCompatUtils.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.net.ConnectivityManager; + +import java.lang.reflect.Method; + +public final class ConnectivityManagerCompatUtils { + // ConnectivityManager#isActiveNetworkMetered() has been introduced + // in API level 16 (Build.VERSION_CODES.JELLY_BEAN). + private static final Method METHOD_isActiveNetworkMetered = CompatUtils.getMethod( + ConnectivityManager.class, "isActiveNetworkMetered"); + + public static boolean isActiveNetworkMetered(final ConnectivityManager manager) { + return (Boolean)CompatUtils.invoke(manager, + // If the API telling whether the network is metered or not is not available, + // then the closest thing is "if it's a mobile connection". + manager.getActiveNetworkInfo().getType() == ConnectivityManager.TYPE_MOBILE, + METHOD_isActiveNetworkMetered); + } +} diff --git a/java/src/org/kelar/inputmethod/compat/CursorAnchorInfoCompatWrapper.java b/java/src/org/kelar/inputmethod/compat/CursorAnchorInfoCompatWrapper.java new file mode 100644 index 000000000..203f99109 --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/CursorAnchorInfoCompatWrapper.java @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.annotation.TargetApi; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.os.Build; +import android.view.inputmethod.CursorAnchorInfo; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * A wrapper for {@link CursorAnchorInfo}, which has been introduced in API Level 21. You can use + * this wrapper to avoid direct dependency on newly introduced types. + */ +public class CursorAnchorInfoCompatWrapper { + + /** + * The insertion marker or character bounds have at least one visible region. + */ + public static final int FLAG_HAS_VISIBLE_REGION = 0x01; + + /** + * The insertion marker or character bounds have at least one invisible (clipped) region. + */ + public static final int FLAG_HAS_INVISIBLE_REGION = 0x02; + + /** + * The insertion marker or character bounds is placed at right-to-left (RTL) character. + */ + public static final int FLAG_IS_RTL = 0x04; + + CursorAnchorInfoCompatWrapper() { + // This class is not publicly instantiable. + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Nullable + public static CursorAnchorInfoCompatWrapper wrap(@Nullable final CursorAnchorInfo instance) { + if (BuildCompatUtils.EFFECTIVE_SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return null; + } + if (instance == null) { + return null; + } + return new RealWrapper(instance); + } + + public int getSelectionStart() { + throw new UnsupportedOperationException("not supported."); + } + + public int getSelectionEnd() { + throw new UnsupportedOperationException("not supported."); + } + + public CharSequence getComposingText() { + throw new UnsupportedOperationException("not supported."); + } + + public int getComposingTextStart() { + throw new UnsupportedOperationException("not supported."); + } + + public Matrix getMatrix() { + throw new UnsupportedOperationException("not supported."); + } + + @SuppressWarnings("unused") + public RectF getCharacterBounds(final int index) { + throw new UnsupportedOperationException("not supported."); + } + + @SuppressWarnings("unused") + public int getCharacterBoundsFlags(final int index) { + throw new UnsupportedOperationException("not supported."); + } + + public float getInsertionMarkerBaseline() { + throw new UnsupportedOperationException("not supported."); + } + + public float getInsertionMarkerBottom() { + throw new UnsupportedOperationException("not supported."); + } + + public float getInsertionMarkerHorizontal() { + throw new UnsupportedOperationException("not supported."); + } + + public float getInsertionMarkerTop() { + throw new UnsupportedOperationException("not supported."); + } + + public int getInsertionMarkerFlags() { + throw new UnsupportedOperationException("not supported."); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static final class RealWrapper extends CursorAnchorInfoCompatWrapper { + + @Nonnull + private final CursorAnchorInfo mInstance; + + public RealWrapper(@Nonnull final CursorAnchorInfo info) { + mInstance = info; + } + + @Override + public int getSelectionStart() { + return mInstance.getSelectionStart(); + } + + @Override + public int getSelectionEnd() { + return mInstance.getSelectionEnd(); + } + + @Override + public CharSequence getComposingText() { + return mInstance.getComposingText(); + } + + @Override + public int getComposingTextStart() { + return mInstance.getComposingTextStart(); + } + + @Override + public Matrix getMatrix() { + return mInstance.getMatrix(); + } + + @Override + public RectF getCharacterBounds(final int index) { + return mInstance.getCharacterBounds(index); + } + + @Override + public int getCharacterBoundsFlags(final int index) { + return mInstance.getCharacterBoundsFlags(index); + } + + @Override + public float getInsertionMarkerBaseline() { + return mInstance.getInsertionMarkerBaseline(); + } + + @Override + public float getInsertionMarkerBottom() { + return mInstance.getInsertionMarkerBottom(); + } + + @Override + public float getInsertionMarkerHorizontal() { + return mInstance.getInsertionMarkerHorizontal(); + } + + @Override + public float getInsertionMarkerTop() { + return mInstance.getInsertionMarkerTop(); + } + + @Override + public int getInsertionMarkerFlags() { + return mInstance.getInsertionMarkerFlags(); + } + } +} diff --git a/java/src/org/kelar/inputmethod/compat/EditorInfoCompatUtils.java b/java/src/org/kelar/inputmethod/compat/EditorInfoCompatUtils.java new file mode 100644 index 000000000..307f6891a --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/EditorInfoCompatUtils.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.view.inputmethod.EditorInfo; + +import java.lang.reflect.Field; +import java.util.Locale; + +public final class EditorInfoCompatUtils { + // Note that EditorInfo.IME_FLAG_FORCE_ASCII has been introduced + // in API level 16 (Build.VERSION_CODES.JELLY_BEAN). + private static final Field FIELD_IME_FLAG_FORCE_ASCII = CompatUtils.getField( + EditorInfo.class, "IME_FLAG_FORCE_ASCII"); + private static final Integer OBJ_IME_FLAG_FORCE_ASCII = (Integer) CompatUtils.getFieldValue( + null /* receiver */, null /* defaultValue */, FIELD_IME_FLAG_FORCE_ASCII); + private static final Field FIELD_HINT_LOCALES = CompatUtils.getField( + EditorInfo.class, "hintLocales"); + + private EditorInfoCompatUtils() { + // This utility class is not publicly instantiable. + } + + public static boolean hasFlagForceAscii(final int imeOptions) { + if (OBJ_IME_FLAG_FORCE_ASCII == null) return false; + return (imeOptions & OBJ_IME_FLAG_FORCE_ASCII) != 0; + } + + public static String imeActionName(final int imeOptions) { + final int actionId = imeOptions & EditorInfo.IME_MASK_ACTION; + switch (actionId) { + case EditorInfo.IME_ACTION_UNSPECIFIED: + return "actionUnspecified"; + case EditorInfo.IME_ACTION_NONE: + return "actionNone"; + case EditorInfo.IME_ACTION_GO: + return "actionGo"; + case EditorInfo.IME_ACTION_SEARCH: + return "actionSearch"; + case EditorInfo.IME_ACTION_SEND: + return "actionSend"; + case EditorInfo.IME_ACTION_NEXT: + return "actionNext"; + case EditorInfo.IME_ACTION_DONE: + return "actionDone"; + case EditorInfo.IME_ACTION_PREVIOUS: + return "actionPrevious"; + default: + return "actionUnknown(" + actionId + ")"; + } + } + + public static String imeOptionsName(final int imeOptions) { + final String action = imeActionName(imeOptions); + final StringBuilder flags = new StringBuilder(); + if ((imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) != 0) { + flags.append("flagNoEnterAction|"); + } + if ((imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) { + flags.append("flagNavigateNext|"); + } + if ((imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0) { + flags.append("flagNavigatePrevious|"); + } + if (hasFlagForceAscii(imeOptions)) { + flags.append("flagForceAscii|"); + } + return (action != null) ? flags + action : flags.toString(); + } + + public static Locale getPrimaryHintLocale(final EditorInfo editorInfo) { + if (editorInfo == null) { + return null; + } + final Object localeList = CompatUtils.getFieldValue(editorInfo, null, FIELD_HINT_LOCALES); + if (localeList == null) { + return null; + } + if (LocaleListCompatUtils.isEmpty(localeList)) { + return null; + } + return LocaleListCompatUtils.get(localeList, 0); + } +} diff --git a/java/src/org/kelar/inputmethod/compat/InputConnectionCompatUtils.java b/java/src/org/kelar/inputmethod/compat/InputConnectionCompatUtils.java new file mode 100644 index 000000000..e9eecc511 --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/InputConnectionCompatUtils.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputMethodManager; + +public final class InputConnectionCompatUtils { + private static final CompatUtils.ClassWrapper sInputConnectionType; + private static final CompatUtils.ToBooleanMethodWrapper sRequestCursorUpdatesMethod; + static { + sInputConnectionType = new CompatUtils.ClassWrapper(InputConnection.class); + sRequestCursorUpdatesMethod = sInputConnectionType.getPrimitiveMethod( + "requestCursorUpdates", false, int.class); + } + + public static boolean isRequestCursorUpdatesAvailable() { + return sRequestCursorUpdatesMethod != null; + } + + /** + * Local copies of some constants in InputConnection until the SDK becomes publicly available. + */ + private static int CURSOR_UPDATE_IMMEDIATE = 1 << 0; + private static int CURSOR_UPDATE_MONITOR = 1 << 1; + + private static boolean requestCursorUpdatesImpl(final InputConnection inputConnection, + final int cursorUpdateMode) { + if (!isRequestCursorUpdatesAvailable()) { + return false; + } + return sRequestCursorUpdatesMethod.invoke(inputConnection, cursorUpdateMode); + } + + /** + * Requests the editor to call back {@link InputMethodManager#updateCursorAnchorInfo}. + * @param inputConnection the input connection to which the request is to be sent. + * @param enableMonitor {@code true} to request the editor to call back the method whenever the + * cursor/anchor position is changed. + * @param requestImmediateCallback {@code true} to request the editor to call back the method + * as soon as possible to notify the current cursor/anchor position to the input method. + * @return {@code false} if the request is not handled. Otherwise returns {@code true}. + */ + public static boolean requestCursorUpdates(final InputConnection inputConnection, + final boolean enableMonitor, final boolean requestImmediateCallback) { + final int cursorUpdateMode = (enableMonitor ? CURSOR_UPDATE_MONITOR : 0) + | (requestImmediateCallback ? CURSOR_UPDATE_IMMEDIATE : 0); + return requestCursorUpdatesImpl(inputConnection, cursorUpdateMode); + } +} diff --git a/java/src/org/kelar/inputmethod/compat/InputMethodManagerCompatWrapper.java b/java/src/org/kelar/inputmethod/compat/InputMethodManagerCompatWrapper.java new file mode 100644 index 000000000..ce081a3f8 --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/InputMethodManagerCompatWrapper.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.content.Context; +import android.os.IBinder; +import android.view.inputmethod.InputMethodManager; + +import java.lang.reflect.Method; + +public final class InputMethodManagerCompatWrapper { + // Note that InputMethodManager.switchToNextInputMethod() has been introduced + // in API level 16 (Build.VERSION_CODES.JELLY_BEAN). + private static final Method METHOD_switchToNextInputMethod = CompatUtils.getMethod( + InputMethodManager.class, "switchToNextInputMethod", IBinder.class, boolean.class); + + // Note that InputMethodManager.shouldOfferSwitchingToNextInputMethod() has been introduced + // in API level 19 (Build.VERSION_CODES.KITKAT). + private static final Method METHOD_shouldOfferSwitchingToNextInputMethod = + CompatUtils.getMethod(InputMethodManager.class, + "shouldOfferSwitchingToNextInputMethod", IBinder.class); + + public final InputMethodManager mImm; + + public InputMethodManagerCompatWrapper(final Context context) { + mImm = (InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE); + } + + public boolean switchToNextInputMethod(final IBinder token, final boolean onlyCurrentIme) { + return (Boolean)CompatUtils.invoke(mImm, false /* defaultValue */, + METHOD_switchToNextInputMethod, token, onlyCurrentIme); + } + + public boolean shouldOfferSwitchingToNextInputMethod(final IBinder token) { + return (Boolean)CompatUtils.invoke(mImm, false /* defaultValue */, + METHOD_shouldOfferSwitchingToNextInputMethod, token); + } +} diff --git a/java/src/org/kelar/inputmethod/compat/InputMethodServiceCompatUtils.java b/java/src/org/kelar/inputmethod/compat/InputMethodServiceCompatUtils.java new file mode 100644 index 000000000..8549f9e1b --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/InputMethodServiceCompatUtils.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.inputmethodservice.InputMethodService; + +import java.lang.reflect.Method; + +public final class InputMethodServiceCompatUtils { + // Note that {@link InputMethodService#enableHardwareAcceleration} has been introduced + // in API level 17 (Build.VERSION_CODES.JELLY_BEAN_MR1). + private static final Method METHOD_enableHardwareAcceleration = + CompatUtils.getMethod(InputMethodService.class, "enableHardwareAcceleration"); + + private InputMethodServiceCompatUtils() { + // This utility class is not publicly instantiable. + } + + public static boolean enableHardwareAcceleration(final InputMethodService ims) { + return (Boolean)CompatUtils.invoke(ims, false /* defaultValue */, + METHOD_enableHardwareAcceleration); + } +} diff --git a/java/src/org/kelar/inputmethod/compat/InputMethodSubtypeCompatUtils.java b/java/src/org/kelar/inputmethod/compat/InputMethodSubtypeCompatUtils.java new file mode 100644 index 000000000..31d257ffe --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/InputMethodSubtypeCompatUtils.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.os.Build; +import android.text.TextUtils; +import android.view.inputmethod.InputMethodSubtype; + +import org.kelar.inputmethod.annotations.UsedForTesting; +import org.kelar.inputmethod.latin.RichInputMethodSubtype; +import org.kelar.inputmethod.latin.common.Constants; +import org.kelar.inputmethod.latin.common.LocaleUtils; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Locale; + +import javax.annotation.Nonnull; + +public final class InputMethodSubtypeCompatUtils { + private static final String TAG = InputMethodSubtypeCompatUtils.class.getSimpleName(); + // Note that InputMethodSubtype(int nameId, int iconId, String locale, String mode, + // String extraValue, boolean isAuxiliary, boolean overridesImplicitlyEnabledSubtype, int id) + // has been introduced in API level 17 (Build.VERSION_CODE.JELLY_BEAN_MR1). + private static final Constructor<?> CONSTRUCTOR_INPUT_METHOD_SUBTYPE = + CompatUtils.getConstructor(InputMethodSubtype.class, + int.class, int.class, String.class, String.class, String.class, boolean.class, + boolean.class, int.class); + static { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (CONSTRUCTOR_INPUT_METHOD_SUBTYPE == null) { + android.util.Log.w(TAG, "Warning!!! Constructor is not defined."); + } + } + } + + // Note that {@link InputMethodSubtype#isAsciiCapable()} has been introduced in API level 19 + // (Build.VERSION_CODE.KITKAT). + private static final Method METHOD_isAsciiCapable = CompatUtils.getMethod( + InputMethodSubtype.class, "isAsciiCapable"); + + private InputMethodSubtypeCompatUtils() { + // This utility class is not publicly instantiable. + } + + @SuppressWarnings("deprecation") + @Nonnull + public static InputMethodSubtype newInputMethodSubtype(int nameId, int iconId, String locale, + String mode, String extraValue, boolean isAuxiliary, + boolean overridesImplicitlyEnabledSubtype, int id) { + if (CONSTRUCTOR_INPUT_METHOD_SUBTYPE == null + || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) { + return new InputMethodSubtype(nameId, iconId, locale, mode, extraValue, isAuxiliary, + overridesImplicitlyEnabledSubtype); + } + return (InputMethodSubtype) CompatUtils.newInstance(CONSTRUCTOR_INPUT_METHOD_SUBTYPE, + nameId, iconId, locale, mode, extraValue, isAuxiliary, + overridesImplicitlyEnabledSubtype, id); + } + + public static boolean isAsciiCapable(final RichInputMethodSubtype subtype) { + return isAsciiCapable(subtype.getRawSubtype()); + } + + public static boolean isAsciiCapable(final InputMethodSubtype subtype) { + return isAsciiCapableWithAPI(subtype) + || subtype.containsExtraValueKey(Constants.Subtype.ExtraValue.ASCII_CAPABLE); + } + + // Note that InputMethodSubtype.getLanguageTag() is expected to be available in Android N+. + private static final Method GET_LANGUAGE_TAG = + CompatUtils.getMethod(InputMethodSubtype.class, "getLanguageTag"); + + public static Locale getLocaleObject(final InputMethodSubtype subtype) { + // Locale.forLanguageTag() is available only in Android L and later. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + final String languageTag = (String) CompatUtils.invoke(subtype, null, GET_LANGUAGE_TAG); + if (!TextUtils.isEmpty(languageTag)) { + return Locale.forLanguageTag(languageTag); + } + } + return LocaleUtils.constructLocaleFromString(subtype.getLocale()); + } + + @UsedForTesting + public static boolean isAsciiCapableWithAPI(final InputMethodSubtype subtype) { + return (Boolean)CompatUtils.invoke(subtype, false, METHOD_isAsciiCapable); + } +} diff --git a/java/src/org/kelar/inputmethod/compat/IntentCompatUtils.java b/java/src/org/kelar/inputmethod/compat/IntentCompatUtils.java new file mode 100644 index 000000000..efe3e9ccc --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/IntentCompatUtils.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.content.Intent; + +public final class IntentCompatUtils { + // Note that Intent.ACTION_USER_INITIALIZE have been introduced in API level 17 + // (Build.VERSION_CODE.JELLY_BEAN_MR1). + private static final String ACTION_USER_INITIALIZE = + (String)CompatUtils.getFieldValue(null /* receiver */, null /* defaultValue */, + CompatUtils.getField(Intent.class, "ACTION_USER_INITIALIZE")); + + private IntentCompatUtils() { + // This utility class is not publicly instantiable. + } + + public static boolean is_ACTION_USER_INITIALIZE(final String action) { + return ACTION_USER_INITIALIZE != null && ACTION_USER_INITIALIZE.equals(action); + } +} diff --git a/java/src/org/kelar/inputmethod/compat/LocaleListCompatUtils.java b/java/src/org/kelar/inputmethod/compat/LocaleListCompatUtils.java new file mode 100644 index 000000000..44d485f13 --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/LocaleListCompatUtils.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import java.lang.reflect.Method; +import java.util.Locale; + +public final class LocaleListCompatUtils { + private static final Class CLASS_LocaleList = CompatUtils.getClass("android.os.LocaleList"); + private static final Method METHOD_get = + CompatUtils.getMethod(CLASS_LocaleList, "get", int.class); + private static final Method METHOD_isEmpty = + CompatUtils.getMethod(CLASS_LocaleList, "isEmpty"); + + private LocaleListCompatUtils() { + // This utility class is not publicly instantiable. + } + + public static boolean isEmpty(final Object localeList) { + return (Boolean) CompatUtils.invoke(localeList, Boolean.FALSE, METHOD_isEmpty); + } + + public static Locale get(final Object localeList, final int index) { + return (Locale) CompatUtils.invoke(localeList, null, METHOD_get, index); + } +} diff --git a/java/src/org/kelar/inputmethod/compat/LocaleSpanCompatUtils.java b/java/src/org/kelar/inputmethod/compat/LocaleSpanCompatUtils.java new file mode 100644 index 000000000..431507f89 --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/LocaleSpanCompatUtils.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.text.Spannable; +import android.text.Spanned; +import android.text.style.LocaleSpan; +import android.util.Log; + +import org.kelar.inputmethod.annotations.UsedForTesting; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Locale; + +@UsedForTesting +public final class LocaleSpanCompatUtils { + private static final String TAG = LocaleSpanCompatUtils.class.getSimpleName(); + + // Note that LocaleSpan(Locale locale) has been introduced in API level 17 + // (Build.VERSION_CODE.JELLY_BEAN_MR1). + private static Class<?> getLocaleSpanClass() { + try { + return Class.forName("android.text.style.LocaleSpan"); + } catch (ClassNotFoundException e) { + return null; + } + } + private static final Class<?> LOCALE_SPAN_TYPE; + private static final Constructor<?> LOCALE_SPAN_CONSTRUCTOR; + private static final Method LOCALE_SPAN_GET_LOCALE; + static { + LOCALE_SPAN_TYPE = getLocaleSpanClass(); + LOCALE_SPAN_CONSTRUCTOR = CompatUtils.getConstructor(LOCALE_SPAN_TYPE, Locale.class); + LOCALE_SPAN_GET_LOCALE = CompatUtils.getMethod(LOCALE_SPAN_TYPE, "getLocale"); + } + + @UsedForTesting + public static boolean isLocaleSpanAvailable() { + return (LOCALE_SPAN_CONSTRUCTOR != null && LOCALE_SPAN_GET_LOCALE != null); + } + + @UsedForTesting + public static Object newLocaleSpan(final Locale locale) { + return CompatUtils.newInstance(LOCALE_SPAN_CONSTRUCTOR, locale); + } + + @UsedForTesting + public static Locale getLocaleFromLocaleSpan(final Object localeSpan) { + return (Locale) CompatUtils.invoke(localeSpan, null, LOCALE_SPAN_GET_LOCALE); + } + + /** + * Ensures that the specified range is covered with only one {@link LocaleSpan} with the given + * locale. If the region is already covered by one or more {@link LocaleSpan}, their ranges are + * updated so that each character has only one locale. + * @param spannable the spannable object to be updated. + * @param start the start index from which {@link LocaleSpan} is attached (inclusive). + * @param end the end index to which {@link LocaleSpan} is attached (exclusive). + * @param locale the locale to be attached to the specified range. + */ + @UsedForTesting + public static void updateLocaleSpan(final Spannable spannable, final int start, + final int end, final Locale locale) { + if (end < start) { + Log.e(TAG, "Invalid range: start=" + start + " end=" + end); + return; + } + if (!isLocaleSpanAvailable()) { + return; + } + // A brief summary of our strategy; + // 1. Enumerate all LocaleSpans between [start - 1, end + 1]. + // 2. For each LocaleSpan S: + // - Update the range of S so as not to cover [start, end] if S doesn't have the + // expected locale. + // - Mark S as "to be merged" if S has the expected locale. + // 3. Merge all the LocaleSpans that are marked as "to be merged" into one LocaleSpan. + // If no appropriate span is found, create a new one with newLocaleSpan method. + final int searchStart = Math.max(start - 1, 0); + final int searchEnd = Math.min(end + 1, spannable.length()); + // LocaleSpans found in the target range. See the step 1 in the above comment. + final Object[] existingLocaleSpans = spannable.getSpans(searchStart, searchEnd, + LOCALE_SPAN_TYPE); + // LocaleSpans that are marked as "to be merged". See the step 2 in the above comment. + final ArrayList<Object> existingLocaleSpansToBeMerged = new ArrayList<>(); + boolean isStartExclusive = true; + boolean isEndExclusive = true; + int newStart = start; + int newEnd = end; + for (final Object existingLocaleSpan : existingLocaleSpans) { + final Locale attachedLocale = getLocaleFromLocaleSpan(existingLocaleSpan); + if (!locale.equals(attachedLocale)) { + // This LocaleSpan does not have the expected locale. Update its range if it has + // an intersection with the range [start, end] (the first case of the step 2 in the + // above comment). + removeLocaleSpanFromRange(existingLocaleSpan, spannable, start, end); + continue; + } + final int spanStart = spannable.getSpanStart(existingLocaleSpan); + final int spanEnd = spannable.getSpanEnd(existingLocaleSpan); + if (spanEnd < spanStart) { + Log.e(TAG, "Invalid span: spanStart=" + spanStart + " spanEnd=" + spanEnd); + continue; + } + if (spanEnd < start || end < spanStart) { + // No intersection found. + continue; + } + + // Here existingLocaleSpan has the expected locale and an intersection with the + // range [start, end] (the second case of the the step 2 in the above comment). + final int spanFlag = spannable.getSpanFlags(existingLocaleSpan); + if (spanStart < newStart) { + newStart = spanStart; + isStartExclusive = ((spanFlag & Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) == + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + if (newEnd < spanEnd) { + newEnd = spanEnd; + isEndExclusive = ((spanFlag & Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) == + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + existingLocaleSpansToBeMerged.add(existingLocaleSpan); + } + + int originalLocaleSpanFlag = 0; + Object localeSpan = null; + if (existingLocaleSpansToBeMerged.isEmpty()) { + // If there is no LocaleSpan that is marked as to be merged, create a new one. + localeSpan = newLocaleSpan(locale); + } else { + // Reuse the first LocaleSpan to avoid unnecessary object instantiation. + localeSpan = existingLocaleSpansToBeMerged.get(0); + originalLocaleSpanFlag = spannable.getSpanFlags(localeSpan); + // No need to keep other instances. + for (int i = 1; i < existingLocaleSpansToBeMerged.size(); ++i) { + spannable.removeSpan(existingLocaleSpansToBeMerged.get(i)); + } + } + final int localeSpanFlag = getSpanFlag(originalLocaleSpanFlag, isStartExclusive, + isEndExclusive); + spannable.setSpan(localeSpan, newStart, newEnd, localeSpanFlag); + } + + private static void removeLocaleSpanFromRange(final Object localeSpan, + final Spannable spannable, final int removeStart, final int removeEnd) { + if (!isLocaleSpanAvailable()) { + return; + } + final int spanStart = spannable.getSpanStart(localeSpan); + final int spanEnd = spannable.getSpanEnd(localeSpan); + if (spanStart > spanEnd) { + Log.e(TAG, "Invalid span: spanStart=" + spanStart + " spanEnd=" + spanEnd); + return; + } + if (spanEnd < removeStart) { + // spanStart < spanEnd < removeStart < removeEnd + return; + } + if (removeEnd < spanStart) { + // spanStart < removeEnd < spanStart < spanEnd + return; + } + final int spanFlags = spannable.getSpanFlags(localeSpan); + if (spanStart < removeStart) { + if (removeEnd < spanEnd) { + // spanStart < removeStart < removeEnd < spanEnd + final Locale locale = getLocaleFromLocaleSpan(localeSpan); + spannable.setSpan(localeSpan, spanStart, removeStart, spanFlags); + final Object attionalLocaleSpan = newLocaleSpan(locale); + spannable.setSpan(attionalLocaleSpan, removeEnd, spanEnd, spanFlags); + return; + } + // spanStart < removeStart < spanEnd <= removeEnd + spannable.setSpan(localeSpan, spanStart, removeStart, spanFlags); + return; + } + if (removeEnd < spanEnd) { + // removeStart <= spanStart < removeEnd < spanEnd + spannable.setSpan(localeSpan, removeEnd, spanEnd, spanFlags); + return; + } + // removeStart <= spanStart < spanEnd < removeEnd + spannable.removeSpan(localeSpan); + } + + private static int getSpanFlag(final int originalFlag, + final boolean isStartExclusive, final boolean isEndExclusive) { + return (originalFlag & ~Spanned.SPAN_POINT_MARK_MASK) | + getSpanPointMarkFlag(isStartExclusive, isEndExclusive); + } + + private static int getSpanPointMarkFlag(final boolean isStartExclusive, + final boolean isEndExclusive) { + if (isStartExclusive) { + return isEndExclusive ? Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + : Spanned.SPAN_EXCLUSIVE_INCLUSIVE; + } + return isEndExclusive ? Spanned.SPAN_INCLUSIVE_EXCLUSIVE + : Spanned.SPAN_INCLUSIVE_INCLUSIVE; + } +} diff --git a/java/src/org/kelar/inputmethod/compat/LooperCompatUtils.java b/java/src/org/kelar/inputmethod/compat/LooperCompatUtils.java new file mode 100644 index 000000000..a48f4c1ab --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/LooperCompatUtils.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.os.Looper; + +import java.lang.reflect.Method; + +/** + * Helper to call Looper#quitSafely, which was introduced in API + * level 18 (Build.VERSION_CODES.JELLY_BEAN_MR2). + * + * In unit tests, we create lots of instances of LatinIME, which means we need to clean up + * some Loopers lest we leak file descriptors. In normal use on a device though, this is never + * necessary (although it does not hurt). + */ +public final class LooperCompatUtils { + private static final Method METHOD_quitSafely = CompatUtils.getMethod( + Looper.class, "quitSafely"); + + public static void quitSafely(final Looper looper) { + if (null != METHOD_quitSafely) { + CompatUtils.invoke(looper, null /* default return value */, METHOD_quitSafely); + } else { + looper.quit(); + } + } +} diff --git a/java/src/org/kelar/inputmethod/compat/NotificationCompatUtils.java b/java/src/org/kelar/inputmethod/compat/NotificationCompatUtils.java new file mode 100644 index 000000000..7797edacf --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/NotificationCompatUtils.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.app.Notification; +import android.os.Build; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +public class NotificationCompatUtils { + // Note that TextInfo.getCharSequence() is supposed to be available in API level 21 and later. + private static final Method METHOD_setColor = + CompatUtils.getMethod(Notification.Builder.class, "setColor", int.class); + private static final Method METHOD_setVisibility = + CompatUtils.getMethod(Notification.Builder.class, "setVisibility", int.class); + private static final Method METHOD_setCategory = + CompatUtils.getMethod(Notification.Builder.class, "setCategory", String.class); + private static final Method METHOD_setPriority = + CompatUtils.getMethod(Notification.Builder.class, "setPriority", int.class); + private static final Method METHOD_build = + CompatUtils.getMethod(Notification.Builder.class, "build"); + private static final Field FIELD_VISIBILITY_SECRET = + CompatUtils.getField(Notification.class, "VISIBILITY_SECRET"); + private static final int VISIBILITY_SECRET = null == FIELD_VISIBILITY_SECRET ? 0 + : (Integer) CompatUtils.getFieldValue(null /* receiver */, null /* defaultValue */, + FIELD_VISIBILITY_SECRET); + private static final Field FIELD_CATEGORY_RECOMMENDATION = + CompatUtils.getField(Notification.class, "CATEGORY_RECOMMENDATION"); + private static final String CATEGORY_RECOMMENDATION = null == FIELD_CATEGORY_RECOMMENDATION ? "" + : (String) CompatUtils.getFieldValue(null /* receiver */, null /* defaultValue */, + FIELD_CATEGORY_RECOMMENDATION); + private static final Field FIELD_PRIORITY_LOW = + CompatUtils.getField(Notification.class, "PRIORITY_LOW"); + private static final int PRIORITY_LOW = null == FIELD_PRIORITY_LOW ? 0 + : (Integer) CompatUtils.getFieldValue(null /* receiver */, null /* defaultValue */, + FIELD_PRIORITY_LOW); + + private NotificationCompatUtils() { + // This class is non-instantiable. + } + + // Sets the accent color + public static void setColor(final Notification.Builder builder, final int color) { + CompatUtils.invoke(builder, null, METHOD_setColor, color); + } + + public static void setVisibilityToSecret(final Notification.Builder builder) { + CompatUtils.invoke(builder, null, METHOD_setVisibility, VISIBILITY_SECRET); + } + + public static void setCategoryToRecommendation(final Notification.Builder builder) { + CompatUtils.invoke(builder, null, METHOD_setCategory, CATEGORY_RECOMMENDATION); + } + + public static void setPriorityToLow(final Notification.Builder builder) { + CompatUtils.invoke(builder, null, METHOD_setPriority, PRIORITY_LOW); + } + + @SuppressWarnings("deprecation") + public static Notification build(final Notification.Builder builder) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + // #build was added in API level 16, JELLY_BEAN + return (Notification) CompatUtils.invoke(builder, null, METHOD_build); + } + // #getNotification was deprecated in API level 16, JELLY_BEAN + return builder.getNotification(); + } +} diff --git a/java/src/org/kelar/inputmethod/compat/SettingsSecureCompatUtils.java b/java/src/org/kelar/inputmethod/compat/SettingsSecureCompatUtils.java new file mode 100644 index 000000000..2b47ee274 --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/SettingsSecureCompatUtils.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import java.lang.reflect.Field; + +public final class SettingsSecureCompatUtils { + // Note that Settings.Secure.ACCESSIBILITY_SPEAK_PASSWORD has been introduced + // in API level 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1). + private static final Field FIELD_ACCESSIBILITY_SPEAK_PASSWORD = CompatUtils.getField( + android.provider.Settings.Secure.class, "ACCESSIBILITY_SPEAK_PASSWORD"); + + private SettingsSecureCompatUtils() { + // This class is non-instantiable. + } + + /** + * Whether to speak passwords while in accessibility mode. + */ + public static final String ACCESSIBILITY_SPEAK_PASSWORD = (String) CompatUtils.getFieldValue( + null /* receiver */, null /* defaultValue */, FIELD_ACCESSIBILITY_SPEAK_PASSWORD); +} diff --git a/java/src/org/kelar/inputmethod/compat/SuggestionSpanUtils.java b/java/src/org/kelar/inputmethod/compat/SuggestionSpanUtils.java new file mode 100644 index 000000000..080d4754b --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/SuggestionSpanUtils.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.content.Context; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.style.SuggestionSpan; + +import org.kelar.inputmethod.annotations.UsedForTesting; +import org.kelar.inputmethod.latin.SuggestedWords; +import org.kelar.inputmethod.latin.SuggestedWords.SuggestedWordInfo; +import org.kelar.inputmethod.latin.common.LocaleUtils; +import org.kelar.inputmethod.latin.define.DebugFlags; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Locale; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public final class SuggestionSpanUtils { + // Note that SuggestionSpan.FLAG_AUTO_CORRECTION has been introduced + // in API level 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1). + private static final Field FIELD_FLAG_AUTO_CORRECTION = CompatUtils.getField( + SuggestionSpan.class, "FLAG_AUTO_CORRECTION"); + private static final Integer OBJ_FLAG_AUTO_CORRECTION = (Integer) CompatUtils.getFieldValue( + null /* receiver */, null /* defaultValue */, FIELD_FLAG_AUTO_CORRECTION); + + static { + if (DebugFlags.DEBUG_ENABLED) { + if (OBJ_FLAG_AUTO_CORRECTION == null) { + throw new RuntimeException("Field is accidentially null."); + } + } + } + + private SuggestionSpanUtils() { + // This utility class is not publicly instantiable. + } + + @UsedForTesting + public static CharSequence getTextWithAutoCorrectionIndicatorUnderline( + final Context context, final String text, @Nonnull final Locale locale) { + if (TextUtils.isEmpty(text) || OBJ_FLAG_AUTO_CORRECTION == null) { + return text; + } + final Spannable spannable = new SpannableString(text); + final SuggestionSpan suggestionSpan = new SuggestionSpan(context, locale, + new String[] {} /* suggestions */, OBJ_FLAG_AUTO_CORRECTION, null); + spannable.setSpan(suggestionSpan, 0, text.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING); + return spannable; + } + + @UsedForTesting + public static CharSequence getTextWithSuggestionSpan(final Context context, + final String pickedWord, final SuggestedWords suggestedWords, final Locale locale) { + if (TextUtils.isEmpty(pickedWord) || suggestedWords.isEmpty() + || suggestedWords.isPrediction() || suggestedWords.isPunctuationSuggestions()) { + return pickedWord; + } + + final ArrayList<String> suggestionsList = new ArrayList<>(); + for (int i = 0; i < suggestedWords.size(); ++i) { + if (suggestionsList.size() >= SuggestionSpan.SUGGESTIONS_MAX_SIZE) { + break; + } + final SuggestedWordInfo info = suggestedWords.getInfo(i); + if (info.isKindOf(SuggestedWordInfo.KIND_PREDICTION)) { + continue; + } + final String word = suggestedWords.getWord(i); + if (!TextUtils.equals(pickedWord, word)) { + suggestionsList.add(word.toString()); + } + } + final SuggestionSpan suggestionSpan = new SuggestionSpan(context, locale, + suggestionsList.toArray(new String[suggestionsList.size()]), 0 /* flags */, null); + final Spannable spannable = new SpannableString(pickedWord); + spannable.setSpan(suggestionSpan, 0, pickedWord.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + return spannable; + } + + /** + * Returns first {@link Locale} found in the given array of {@link SuggestionSpan}. + * @param suggestionSpans the array of {@link SuggestionSpan} to be examined. + * @return the first {@link Locale} found in {@code suggestionSpans}. {@code null} when not + * found. + */ + @UsedForTesting + @Nullable + public static Locale findFirstLocaleFromSuggestionSpans( + final SuggestionSpan[] suggestionSpans) { + for (final SuggestionSpan suggestionSpan : suggestionSpans) { + final String localeString = suggestionSpan.getLocale(); + if (TextUtils.isEmpty(localeString)) { + continue; + } + return LocaleUtils.constructLocaleFromString(localeString); + } + return null; + } +} diff --git a/java/src/org/kelar/inputmethod/compat/SuggestionsInfoCompatUtils.java b/java/src/org/kelar/inputmethod/compat/SuggestionsInfoCompatUtils.java new file mode 100644 index 000000000..fd7210726 --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/SuggestionsInfoCompatUtils.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.view.textservice.SuggestionsInfo; + +import java.lang.reflect.Field; + +public final class SuggestionsInfoCompatUtils { + // Note that SuggestionsInfo.RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS has been introduced + // in API level 15 (Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1). + private static final Field FIELD_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS = + CompatUtils.getField(SuggestionsInfo.class, "RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS"); + private static final Integer OBJ_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS = + (Integer) CompatUtils.getFieldValue(null /* receiver */, null /* defaultValue */, + FIELD_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS); + private static final int RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS = + OBJ_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS != null + ? OBJ_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS : 0; + + private SuggestionsInfoCompatUtils() { + // This utility class is not publicly instantiable. + } + + /** + * Returns the flag value of the attributes of the suggestions that can be obtained by + * {@link SuggestionsInfo#getSuggestionsAttributes()}: this tells that the text service thinks + * the result suggestions include highly recommended ones. + */ + public static int getValueOf_RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS() { + return RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS; + } +} diff --git a/java/src/org/kelar/inputmethod/compat/TextInfoCompatUtils.java b/java/src/org/kelar/inputmethod/compat/TextInfoCompatUtils.java new file mode 100644 index 000000000..d5b97b75e --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/TextInfoCompatUtils.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.view.textservice.TextInfo; + +import org.kelar.inputmethod.annotations.UsedForTesting; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +@UsedForTesting +public final class TextInfoCompatUtils { + // Note that TextInfo.getCharSequence() is supposed to be available in API level 21 and later. + private static final Method TEXT_INFO_GET_CHAR_SEQUENCE = + CompatUtils.getMethod(TextInfo.class, "getCharSequence"); + private static final Constructor<?> TEXT_INFO_CONSTRUCTOR_FOR_CHAR_SEQUENCE = + CompatUtils.getConstructor(TextInfo.class, CharSequence.class, int.class, int.class, + int.class, int.class); + + @UsedForTesting + public static boolean isCharSequenceSupported() { + return TEXT_INFO_GET_CHAR_SEQUENCE != null && + TEXT_INFO_CONSTRUCTOR_FOR_CHAR_SEQUENCE != null; + } + + @UsedForTesting + public static TextInfo newInstance(CharSequence charSequence, int start, int end, int cookie, + int sequenceNumber) { + if (TEXT_INFO_CONSTRUCTOR_FOR_CHAR_SEQUENCE != null) { + return (TextInfo) CompatUtils.newInstance(TEXT_INFO_CONSTRUCTOR_FOR_CHAR_SEQUENCE, + charSequence, start, end, cookie, sequenceNumber); + } + return new TextInfo(charSequence.subSequence(start, end).toString(), cookie, + sequenceNumber); + } + + /** + * Returns the result of {@link TextInfo#getCharSequence()} when available. Otherwise returns + * the result of {@link TextInfo#getText()} as fall back. + * @param textInfo the instance for which {@link TextInfo#getCharSequence()} or + * {@link TextInfo#getText()} is called. + * @return the result of {@link TextInfo#getCharSequence()} when available. Otherwise returns + * the result of {@link TextInfo#getText()} as fall back. If {@code textInfo} is {@code null}, + * returns {@code null}. + */ + @UsedForTesting + public static CharSequence getCharSequenceOrString(final TextInfo textInfo) { + final CharSequence defaultValue = (textInfo == null ? null : textInfo.getText()); + return (CharSequence) CompatUtils.invoke(textInfo, defaultValue, + TEXT_INFO_GET_CHAR_SEQUENCE); + } +} diff --git a/java/src/org/kelar/inputmethod/compat/TextViewCompatUtils.java b/java/src/org/kelar/inputmethod/compat/TextViewCompatUtils.java new file mode 100644 index 000000000..dfad0e3a8 --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/TextViewCompatUtils.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.graphics.drawable.Drawable; +import android.widget.TextView; + +import java.lang.reflect.Method; + +public final class TextViewCompatUtils { + // Note that TextView.setCompoundDrawablesRelativeWithIntrinsicBounds(Drawable,Drawable, + // Drawable,Drawable) has been introduced in API level 17 (Build.VERSION_CODE.JELLY_BEAN_MR1). + private static final Method METHOD_setCompoundDrawablesRelativeWithIntrinsicBounds = + CompatUtils.getMethod(TextView.class, "setCompoundDrawablesRelativeWithIntrinsicBounds", + Drawable.class, Drawable.class, Drawable.class, Drawable.class); + + private TextViewCompatUtils() { + // This utility class is not publicly instantiable. + } + + public static void setCompoundDrawablesRelativeWithIntrinsicBounds(final TextView textView, + final Drawable start, final Drawable top, final Drawable end, final Drawable bottom) { + if (METHOD_setCompoundDrawablesRelativeWithIntrinsicBounds == null) { + textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom); + return; + } + CompatUtils.invoke(textView, null, METHOD_setCompoundDrawablesRelativeWithIntrinsicBounds, + start, top, end, bottom); + } +} diff --git a/java/src/org/kelar/inputmethod/compat/UserDictionaryCompatUtils.java b/java/src/org/kelar/inputmethod/compat/UserDictionaryCompatUtils.java new file mode 100644 index 000000000..0a4635c86 --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/UserDictionaryCompatUtils.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.provider.UserDictionary; + +import java.util.Locale; + +public final class UserDictionaryCompatUtils { + @SuppressWarnings("deprecation") + public static void addWord(final Context context, final String word, + final int freq, final String shortcut, final Locale locale) { + if (BuildCompatUtils.EFFECTIVE_SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + addWordWithShortcut(context, word, freq, shortcut, locale); + return; + } + // Fall back to the pre-JellyBean method. + final Locale currentLocale = context.getResources().getConfiguration().locale; + final int localeType = currentLocale.equals(locale) + ? UserDictionary.Words.LOCALE_TYPE_CURRENT : UserDictionary.Words.LOCALE_TYPE_ALL; + UserDictionary.Words.addWord(context, word, freq, localeType); + } + + // {@link UserDictionary.Words#addWord(Context,String,int,String,Locale)} was introduced + // in API level 16 (Build.VERSION_CODES.JELLY_BEAN). + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private static void addWordWithShortcut(final Context context, final String word, + final int freq, final String shortcut, final Locale locale) { + UserDictionary.Words.addWord(context, word, freq, shortcut, locale); + } +} + diff --git a/java/src/org/kelar/inputmethod/compat/UserManagerCompatUtils.java b/java/src/org/kelar/inputmethod/compat/UserManagerCompatUtils.java new file mode 100644 index 000000000..f72e28726 --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/UserManagerCompatUtils.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.content.Context; +import android.os.Build; +import android.os.UserManager; +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.reflect.Method; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +/** + * A temporary solution until {@code UserManagerCompat.isUserUnlocked()} in the support-v4 library + * becomes publicly available. + */ +public final class UserManagerCompatUtils { + private static final Method METHOD_isUserUnlocked; + + static { + // We do not try to search the method in Android M and prior. + if (BuildCompatUtils.EFFECTIVE_SDK_INT <= Build.VERSION_CODES.M) { + METHOD_isUserUnlocked = null; + } else { + METHOD_isUserUnlocked = CompatUtils.getMethod(UserManager.class, "isUserUnlocked"); + } + } + + private UserManagerCompatUtils() { + // This utility class is not publicly instantiable. + } + + public static final int LOCK_STATE_UNKNOWN = 0; + public static final int LOCK_STATE_UNLOCKED = 1; + public static final int LOCK_STATE_LOCKED = 2; + + @Retention(SOURCE) + @IntDef({LOCK_STATE_UNKNOWN, LOCK_STATE_UNLOCKED, LOCK_STATE_LOCKED}) + public @interface LockState {} + + /** + * Check if the calling user is running in an "unlocked" state. A user is unlocked only after + * they've entered their credentials (such as a lock pattern or PIN), and credential-encrypted + * private app data storage is available. + * @param context context from which {@link UserManager} should be obtained. + * @return One of {@link LockState}. + */ + @LockState + public static int getUserLockState(final Context context) { + if (METHOD_isUserUnlocked == null) { + return LOCK_STATE_UNKNOWN; + } + final UserManager userManager = context.getSystemService(UserManager.class); + if (userManager == null) { + return LOCK_STATE_UNKNOWN; + } + final Boolean result = + (Boolean) CompatUtils.invoke(userManager, null, METHOD_isUserUnlocked); + if (result == null) { + return LOCK_STATE_UNKNOWN; + } + return result ? LOCK_STATE_UNLOCKED : LOCK_STATE_LOCKED; + } +} diff --git a/java/src/org/kelar/inputmethod/compat/ViewCompatUtils.java b/java/src/org/kelar/inputmethod/compat/ViewCompatUtils.java new file mode 100644 index 000000000..9b91897e5 --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/ViewCompatUtils.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.view.View; + +import java.lang.reflect.Method; + +// TODO: Use {@link androidx.core.view.ViewCompat} instead of this utility class. +// Currently {@link #getPaddingEnd(View)} and {@link #setPaddingRelative(View,int,int,int,int)} +// are missing from android-support-v4 static library in KitKat SDK. +public final class ViewCompatUtils { + // Note that View.getPaddingEnd(), View.setPaddingRelative(int,int,int,int) have been + // introduced in API level 17 (Build.VERSION_CODE.JELLY_BEAN_MR1). + private static final Method METHOD_getPaddingEnd = CompatUtils.getMethod( + View.class, "getPaddingEnd"); + private static final Method METHOD_setPaddingRelative = CompatUtils.getMethod( + View.class, "setPaddingRelative", + int.class, int.class, int.class, int.class); + // Note that View.setTextAlignment(int) has been introduced in API level 17. + private static final Method METHOD_setTextAlignment = CompatUtils.getMethod( + View.class, "setTextAlignment", int.class); + + private ViewCompatUtils() { + // This utility class is not publicly instantiable. + } + + public static int getPaddingEnd(final View view) { + if (METHOD_getPaddingEnd == null) { + return view.getPaddingRight(); + } + return (Integer)CompatUtils.invoke(view, 0, METHOD_getPaddingEnd); + } + + public static void setPaddingRelative(final View view, final int start, final int top, + final int end, final int bottom) { + if (METHOD_setPaddingRelative == null) { + view.setPadding(start, top, end, bottom); + return; + } + CompatUtils.invoke(view, null, METHOD_setPaddingRelative, start, top, end, bottom); + } + + // These TEXT_ALIGNMENT_* constants have been introduced in API 17. + public static final int TEXT_ALIGNMENT_INHERIT = 0; + public static final int TEXT_ALIGNMENT_GRAVITY = 1; + public static final int TEXT_ALIGNMENT_TEXT_START = 2; + public static final int TEXT_ALIGNMENT_TEXT_END = 3; + public static final int TEXT_ALIGNMENT_CENTER = 4; + public static final int TEXT_ALIGNMENT_VIEW_START = 5; + public static final int TEXT_ALIGNMENT_VIEW_END = 6; + + public static void setTextAlignment(final View view, final int textAlignment) { + CompatUtils.invoke(view, null, METHOD_setTextAlignment, textAlignment); + } +} diff --git a/java/src/org/kelar/inputmethod/compat/ViewOutlineProviderCompatUtils.java b/java/src/org/kelar/inputmethod/compat/ViewOutlineProviderCompatUtils.java new file mode 100644 index 000000000..64f4230ba --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/ViewOutlineProviderCompatUtils.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.inputmethodservice.InputMethodService; +import android.os.Build; +import android.view.View; + +public class ViewOutlineProviderCompatUtils { + private ViewOutlineProviderCompatUtils() { + // This utility class is not publicly instantiable. + } + + public interface InsetsUpdater { + public void setInsets(final InputMethodService.Insets insets); + } + + private static final InsetsUpdater EMPTY_INSETS_UPDATER = new InsetsUpdater() { + @Override + public void setInsets(final InputMethodService.Insets insets) {} + }; + + public static InsetsUpdater setInsetsOutlineProvider(final View view) { + if (BuildCompatUtils.EFFECTIVE_SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + return EMPTY_INSETS_UPDATER; + } + return ViewOutlineProviderCompatUtilsLXX.setInsetsOutlineProvider(view); + } +} diff --git a/java/src/org/kelar/inputmethod/compat/ViewOutlineProviderCompatUtilsLXX.java b/java/src/org/kelar/inputmethod/compat/ViewOutlineProviderCompatUtilsLXX.java new file mode 100644 index 000000000..8049d7d04 --- /dev/null +++ b/java/src/org/kelar/inputmethod/compat/ViewOutlineProviderCompatUtilsLXX.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.kelar.inputmethod.compat; + +import android.annotation.TargetApi; +import android.graphics.Outline; +import android.inputmethodservice.InputMethodService; +import android.os.Build; +import android.view.View; +import android.view.ViewOutlineProvider; + +import org.kelar.inputmethod.compat.ViewOutlineProviderCompatUtils.InsetsUpdater; + +@TargetApi(Build.VERSION_CODES.LOLLIPOP) +class ViewOutlineProviderCompatUtilsLXX { + private ViewOutlineProviderCompatUtilsLXX() { + // This utility class is not publicly instantiable. + } + + static InsetsUpdater setInsetsOutlineProvider(final View view) { + final InsetsOutlineProvider provider = new InsetsOutlineProvider(view); + view.setOutlineProvider(provider); + return provider; + } + + private static class InsetsOutlineProvider extends ViewOutlineProvider + implements InsetsUpdater { + private final View mView; + private static final int NO_DATA = -1; + private int mLastVisibleTopInsets = NO_DATA; + + public InsetsOutlineProvider(final View view) { + mView = view; + view.setOutlineProvider(this); + } + + @Override + public void setInsets(final InputMethodService.Insets insets) { + final int visibleTopInsets = insets.visibleTopInsets; + if (mLastVisibleTopInsets != visibleTopInsets) { + mLastVisibleTopInsets = visibleTopInsets; + mView.invalidateOutline(); + } + } + + @Override + public void getOutline(final View view, final Outline outline) { + if (mLastVisibleTopInsets == NO_DATA) { + // Call default implementation. + ViewOutlineProvider.BACKGROUND.getOutline(view, outline); + return; + } + // TODO: Revisit this when floating/resize keyboard is supported. + outline.setRect( + view.getLeft(), mLastVisibleTopInsets, view.getRight(), view.getBottom()); + } + } +} |