From f97da47c024f01fddc466db4efb3200d0b37ae49 Mon Sep 17 00:00:00 2001 From: Yohei Yukawa Date: Tue, 22 May 2018 10:31:28 -0700 Subject: Opensource a test tool called EditTextVariations This CL opensouces a testing tool called EditTextVariations that has been used internally to test IME behaviors on various EditorInfo#inputType and EditorInfo#imeOptions. Bug: 80039502 Test: Manually verified as follows. 1. tapas EditTextVariations 2. make -j 3. adb install -r $ANDROID_TARGET_OUT_TESTCASES/EditTextVariations/EditTextVariations.apk Change-Id: Ia87e655573c2fd1fc09d56e4af90bfb1dfd65f9b --- .../edittextvariations/EchoingTextWatcher.java | 86 ++++ .../edittextvariations/EditTextVariations.java | 473 +++++++++++++++++++++ .../tools/edittextvariations/FinalClassField.java | 64 +++ .../tools/edittextvariations/InstanceMethod.java | 83 ++++ .../MultiLineShortMessageEditText.java | 46 ++ .../tools/edittextvariations/ThemeItem.java | 68 +++ 6 files changed, 820 insertions(+) create mode 100644 tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/EchoingTextWatcher.java create mode 100644 tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/EditTextVariations.java create mode 100644 tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/FinalClassField.java create mode 100644 tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/InstanceMethod.java create mode 100644 tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/MultiLineShortMessageEditText.java create mode 100644 tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/ThemeItem.java (limited to 'tools/EditTextVariations/src') diff --git a/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/EchoingTextWatcher.java b/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/EchoingTextWatcher.java new file mode 100644 index 000000000..1c652c2a2 --- /dev/null +++ b/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/EchoingTextWatcher.java @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.tools.edittextvariations; + +import android.annotation.SuppressLint; +import android.os.Handler; +import android.os.Message; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.widget.EditText; + +import java.util.Locale; + +final class EchoingTextWatcher implements TextWatcher { + private static final int SET_TEXT_DELAY = 500; + + final EditText mEditText; + CharSequence mExpected; + + @SuppressLint("HandlerLeak") + private Handler mHandler = new Handler() { + @Override + public void handleMessage(final Message msg) { + final String toBeappended = (String) msg.obj; + final CharSequence current = mEditText.getText(); + final CharSequence newText = TextUtils.concat(current, toBeappended); + mExpected = newText; + mEditText.setText(newText); + mEditText.setSelection(newText.length()); + } + }; + + @SuppressWarnings("unused") + public static void attachTo(final EditText editText) { + final EchoingTextWatcher watcher = new EchoingTextWatcher(editText); + } + + public EchoingTextWatcher(final EditText editText) { + mEditText = editText; + editText.addTextChangedListener(this); + } + + @Override + public void afterTextChanged(final Editable ss) { + } + + @Override + public void beforeTextChanged(final CharSequence s, final int start, final int count, + final int after) { + } + + @Override + public void onTextChanged(final CharSequence s, final int start, final int before, + final int count) { + if (count == 0 || before > 0 || TextUtils.equals(s, mExpected)) { + return; + } + final int len = s.length(); + if (len > 0) { + final String last = s.subSequence(len - 1, len).toString(); + final char lastChar = last.charAt(0); + if (Character.isUpperCase(lastChar)) { + final String lowerCase = last.toLowerCase(Locale.getDefault()); + mHandler.sendMessageDelayed(mHandler.obtainMessage(0, lowerCase), SET_TEXT_DELAY); + } else if (Character.isLowerCase(lastChar)) { + final String upperCase = last.toUpperCase(Locale.getDefault()); + mHandler.sendMessageDelayed(mHandler.obtainMessage(0, upperCase), SET_TEXT_DELAY); + } + } + } +} diff --git a/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/EditTextVariations.java b/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/EditTextVariations.java new file mode 100644 index 000000000..44e0a4d55 --- /dev/null +++ b/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/EditTextVariations.java @@ -0,0 +1,473 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.tools.edittextvariations; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.InputType; +import android.text.TextUtils; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.view.inputmethod.EditorInfo; +import android.webkit.JavascriptInterface; +import android.webkit.WebView; +import android.widget.ArrayAdapter; +import android.widget.AutoCompleteTextView; +import android.widget.EditText; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +public final class EditTextVariations extends Activity implements TextView.OnEditorActionListener, + DialogInterface.OnClickListener { + private static final String TAG = EditTextVariations.class.getSimpleName(); + private static final boolean DEBUG_INPUT_TEXT = false; + + private static final int MENU_CHANGE_THEME = 0; + private static final int MENU_VERSION = 1; + private static final int MENU_NAVIGATE_ON = 2; + private static final int MENU_NAVIGATE_OFF = 3; + private static final int MENU_SOFTINPUT_VISIBLE = 4; + private static final int MENU_SOFTINPUT_HIDDEN = 5; + private static final String PREF_THEME = "theme"; + private static final String PREF_NAVIGATE = "navigate"; + private static final String PREF_SOFTINPUT = "softinput"; + + private SharedPreferences prefs; + private View[] fields; + + private static final FinalClassField ApplicationInfo_FLAG_SUPPORTS_RTL = + FinalClassField.newInstance(ApplicationInfo.class, "FLAG_SUPPORTS_RTL", 1 << 22); + + // This flag should be defined IceCreamSandwich and later. + // Note that Froyo and Gingerbread have hidden IME_FLAG_NO_FULLSCREEN as + // value 0x80000000. + private static final FinalClassField EditorInfo_IME_FLAG_FORCE_ASCII = + FinalClassField.newInstance(EditorInfo.class, "IME_FLAG_FORCE_ASCII", + Build.VERSION.SDK_INT >= /* ICE_CREAM_SANDWICH */14 ? 0x80000000 : 0); + + private ArrayAdapter mAutoCompleteAdapter; + + /** Called when the activity is first created. */ + @SuppressLint("SetJavaScriptEnabled") + @Override + public void onCreate(final Bundle savedInstanceState) { + getApplicationInfo().flags |= ApplicationInfo_FLAG_SUPPORTS_RTL.value; + prefs = PreferenceManager.getDefaultSharedPreferences(this); + loadTheme(); + loadSoftInputMode(); + + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + + final String[] countries = getResources().getStringArray(R.array.countries_array); + mAutoCompleteAdapter = new ArrayAdapter<>( + this, android.R.layout.simple_dropdown_item_1line, countries); + + final boolean navigateMode = getNavigateMode(); + final ViewGroup vg = (ViewGroup) findViewById(R.id.edit_text_list); + final int n = vg.getChildCount(); + fields = new View[n]; + for (int i = 0; i < n; i++) { + final View v = vg.getChildAt(i); + if (v instanceof EditText) { + final int id = v.getId(); + final EditText e = (EditText) v; + int inputType = e.getInputType(); + int imeOptions = e.getImeOptions(); + if (id == R.id.text_auto_correct_previous) { + imeOptions &= ~EditorInfo.IME_MASK_ACTION; + imeOptions |= EditorInfo.IME_ACTION_PREVIOUS; + } + if (id == R.id.text_force_ascii_flag) { + imeOptions |= EditorInfo_IME_FLAG_FORCE_ASCII.value; + } + if (id == R.id.text_null) { + inputType = InputType.TYPE_NULL; + } + if (id == R.id.text_restarting) { + EchoingTextWatcher.attachTo(e); + } + if (navigateMode && i > 0) { + imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS; + } + if (navigateMode && i < n - 1) { + imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT; + } + + e.setInputType(inputType); + e.setImeOptions(imeOptions); + setupHintText(e); + if (navigateMode) { + e.setOnEditorActionListener(this); + } + } + if (v instanceof AutoCompleteTextView) { + final AutoCompleteTextView e = (AutoCompleteTextView) v; + e.setAdapter(mAutoCompleteAdapter); + e.setThreshold(1); + } + if (v instanceof WebView) { + final WebView wv = (WebView) v; + wv.getSettings().setJavaScriptEnabled(true); + wv.addJavascriptInterface(new Object() { + @JavascriptInterface + public String name() { + return getThemeName(); + } + }, "theme"); + wv.loadUrl("file:///android_asset/webview.html"); + } + fields[i] = v; + } + } + + @Override + public boolean onCreateOptionsMenu(final Menu menu) { + super.onCreateOptionsMenu(menu); + + menu.add(Menu.NONE, MENU_NAVIGATE_ON, 0, getString(R.string.menu_navigate_on)); + menu.add(Menu.NONE, MENU_NAVIGATE_OFF, 1, getString(R.string.menu_navigate_off)); + menu.add(Menu.NONE, MENU_SOFTINPUT_VISIBLE, 2, getString(R.string.menu_softinput_visible)); + menu.add(Menu.NONE, MENU_SOFTINPUT_HIDDEN, 3, getString(R.string.menu_softinput_hidden)); + menu.add(Menu.NONE, MENU_CHANGE_THEME, 4, R.string.menu_change_theme); + try { + final PackageInfo pinfo = getPackageManager().getPackageInfo(getPackageName(), 0); + menu.add(Menu.NONE, MENU_VERSION, 5, + getString(R.string.menu_version, pinfo.versionName)) + .setEnabled(false); + } catch (NameNotFoundException e) { + return false; + } + + return true; + } + + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + final int itemId = item.getItemId(); + if (itemId == MENU_CHANGE_THEME) { + final List items = new ArrayList<>(); + for (final ThemeItem theme : ThemeItem.THEME_LIST) { + items.add(theme.name); + } + final AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.menu_change_theme); + builder.setCancelable(true); + builder.setNegativeButton(android.R.string.cancel, null); + builder.setItems(items.toArray(new CharSequence[items.size()]), this); + builder.show(); + } else if (itemId == MENU_NAVIGATE_ON || itemId == MENU_NAVIGATE_OFF) { + saveNavigateMode(itemId == MENU_NAVIGATE_ON); + restartActivity(); + } else if (itemId == MENU_SOFTINPUT_VISIBLE || itemId == MENU_SOFTINPUT_HIDDEN) { + saveSoftInputMode(itemId == MENU_SOFTINPUT_VISIBLE); + restartActivity(); + } + return true; + } + + @Override + public void onClick(final DialogInterface dialog, final int which) { + saveTheme(ThemeItem.THEME_LIST.get(which)); + restartActivity(); + } + + private void restartActivity() { + final Intent intent = getIntent(); + finish(); + startActivity(intent); + } + + private static void setupHintText(final EditText e) { + final int imeOptions = e.getImeOptions(); + String hint = (e instanceof MultiLineShortMessageEditText) ? "*" : ""; + hint += inputTypeToString(e.getInputType()); + String text; + if (e.getImeActionLabel() != null) { + text = "actionLabel<" + e.getImeActionLabel() + ":" + e.getImeActionId() + ">"; + } else { + text = actionName(imeOptions & EditorInfo.IME_MASK_ACTION); + } + text = appendFlagText(text, + (imeOptions & EditorInfo.IME_FLAG_NO_EXTRACT_UI) != 0, "flagNoExtractUi"); + text = appendFlagText(text, + (imeOptions & EditorInfo.IME_FLAG_NO_FULLSCREEN) != 0, "flagNoFullscreen"); + text = appendFlagText(text, + (imeOptions & EditorInfo_IME_FLAG_FORCE_ASCII.value) != 0, "flagForceAscii"); + text = appendFlagText(text, + (imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0, ">"); + text = appendFlagText(text, + (imeOptions & EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS) != 0, "<"); + if (text.length() > 0) + hint += " " + text; + final String privateOptions = e.getPrivateImeOptions(); + if (!TextUtils.isEmpty(privateOptions)) { + hint += " ("; + String sep = ""; + for (final String opt : privateOptions.trim().split(",")) { + final String[] elem = opt.trim().split("\\."); + hint += sep + elem[elem.length - 1]; + sep = ","; + } + hint += ")"; + } + if (DEBUG_INPUT_TEXT) { + Log.d(TAG, String.format("class=0x%08x variation=0x%08x flags=0x%08x hint=%s", + e.getInputType() & InputType.TYPE_MASK_CLASS, + e.getInputType() & InputType.TYPE_MASK_VARIATION, + e.getInputType() & InputType.TYPE_MASK_FLAGS, hint)); + } + if (e.getId() == R.id.text_restarting) { + hint += " restarting"; + } + e.setHint(hint); + } + + private void saveBooleanPreference(final String key, final boolean value) { + final SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean(key, value); + editor.apply(); + } + + private void saveStringPreference(final String key, final String value) { + final SharedPreferences.Editor editor = prefs.edit(); + editor.putString(key, value); + editor.apply(); + } + + private void saveNavigateMode(final boolean enabled) { + saveBooleanPreference(PREF_NAVIGATE, enabled); + } + + private boolean getNavigateMode() { + return prefs.getBoolean(PREF_NAVIGATE, false); + } + + private void saveSoftInputMode(final boolean visible) { + saveBooleanPreference(PREF_SOFTINPUT, visible); + } + + private void loadSoftInputMode() { + final boolean visible = prefs.getBoolean(PREF_SOFTINPUT, false); + final Window w = getWindow(); + w.setSoftInputMode(visible + ? WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE + : WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN); + } + + private void saveTheme(final ThemeItem theme) { + saveStringPreference(PREF_THEME, theme.name); + } + + String getThemeName() { + return prefs.getString(PREF_THEME, ThemeItem.getDefaultThemeName()); + } + + private void loadTheme() { + final String themeName = getThemeName(); + for (final ThemeItem theme : ThemeItem.THEME_LIST) { + if (themeName.equals(theme.name)) { + setTheme(theme.id); + return; + } + } + } + + @Override + public boolean onEditorAction(final TextView v, final int action, final KeyEvent event) { + for (int i = 0; i < fields.length; i++) { + if (v == fields[i]) { + final int direction; + if (action == EditorInfo.IME_ACTION_PREVIOUS) { + direction = -1; + } else { + direction = +1; + } + + final int target = i + direction; + if (target < 0 || target >= fields.length) + return false; + + final View targetView = fields[target]; + targetView.requestFocus(); + return true; + } + } + return false; + } + + private static String actionName(final int action) { + switch (action & EditorInfo.IME_MASK_ACTION) { + 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(" + action + ")"; + } + } + + private static String inputTypeToString(final int inputType) { + if (inputType == InputType.TYPE_NULL) { + return "TYPE_NULL"; + } + final int clazz = inputType & InputType.TYPE_MASK_CLASS; + final int variation = inputType & InputType.TYPE_MASK_VARIATION; + final int flags = inputType & InputType.TYPE_MASK_FLAGS; + String base = "unknown(class=" + clazz + " variation=" + variation + " flag=0x" + + Integer.toHexString(flags); + + switch (clazz) { + case InputType.TYPE_CLASS_TEXT: + switch (variation) { + case InputType.TYPE_TEXT_VARIATION_NORMAL: + base = "text"; + break; + case InputType.TYPE_TEXT_VARIATION_URI: + base = "textUri"; + break; + case InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS: + base = "textEmailAddress"; + break; + case InputType.TYPE_TEXT_VARIATION_EMAIL_SUBJECT: + base = "textEmailSubject"; + break; + case InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE: + base = "textShortMessage"; + break; + case InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE: + base = "textLongMessage"; + break; + case InputType.TYPE_TEXT_VARIATION_PERSON_NAME: + base = "textPersonName"; + break; + case InputType.TYPE_TEXT_VARIATION_POSTAL_ADDRESS: + base = "textPostalAddress"; + break; + case InputType.TYPE_TEXT_VARIATION_PASSWORD: + base = "textPassword"; + break; + case InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD: + base = "textVisiblePassword"; + break; + case InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT: + base = "textWebEditText"; + break; + case InputType.TYPE_TEXT_VARIATION_FILTER: + base = "textFilter"; + break; + case InputType.TYPE_TEXT_VARIATION_PHONETIC: + base = "textPhonetic"; + break; + case InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS: + base = "textWebEmailAddress"; + break; + case InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD: + base = "textWebPassword"; + break; + } + base = appendFlagText(base, (flags & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0, + "textCapCharacters"); + base = appendFlagText(base, (flags & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0, + "textCapWords"); + base = appendFlagText(base, (flags & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0, + "textCapSentences"); + base = appendFlagText(base, (flags & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) != 0, + "textAutoCorrect"); + base = appendFlagText(base, (flags & InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) != 0, + "textAutoComplete"); + base = appendFlagText(base, (flags & InputType.TYPE_TEXT_FLAG_MULTI_LINE) != 0, + "textMultiLine"); + base = appendFlagText(base, (flags & InputType.TYPE_TEXT_FLAG_IME_MULTI_LINE) != 0, + "textImeMultiLine"); + base = appendFlagText(base, (flags & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) != 0, + "textNoSuggestions"); + break; + + case InputType.TYPE_CLASS_NUMBER: + if (variation == InputType.TYPE_NUMBER_VARIATION_NORMAL) { + base = "number"; + } else if (variation == InputType.TYPE_NUMBER_VARIATION_PASSWORD) { + base = "numberPassword"; + } + base = appendFlagText(base, (flags & InputType.TYPE_NUMBER_FLAG_SIGNED) != 0, + "numberSigned"); + base = appendFlagText(base, (flags & InputType.TYPE_NUMBER_FLAG_DECIMAL) != 0, + "numberDecimal"); + break; + + case InputType.TYPE_CLASS_PHONE: + base = "phone"; + break; + + case InputType.TYPE_CLASS_DATETIME: + switch (variation) { + case InputType.TYPE_DATETIME_VARIATION_NORMAL: + base = "datetime"; + break; + case InputType.TYPE_DATETIME_VARIATION_DATE: + base = "date"; + break; + case InputType.TYPE_DATETIME_VARIATION_TIME: + base = "time"; + break; + } + break; + } + + return base; + } + + private static String appendFlagText(final String text, final boolean flag, final String name) { + if (flag) { + if (text.length() == 0 || name.startsWith(text)) + return name; + return text + "|" + name; + } + return text; + } +} diff --git a/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/FinalClassField.java b/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/FinalClassField.java new file mode 100644 index 000000000..14c556a67 --- /dev/null +++ b/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/FinalClassField.java @@ -0,0 +1,64 @@ +/* + * 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 com.android.inputmethod.tools.edittextvariations; + +import java.lang.reflect.Field; + +public final class FinalClassField { + public final boolean defined; + public final String name; + public final String className; + public final T value; + + @SuppressWarnings("unchecked") + private FinalClassField(final Field field, final String className, final String fieldName, + final T compatValue) { + this.defined = field != null; + this.name = fieldName; + this.className = className; + T v = null; + try { + final Object obj = field.get(null); + v = (T) obj; + } catch (final Exception e) { + v = compatValue; + } + this.value = v; + } + + public static FinalClassField newInstance(final Class definedClass, final String name, + final T compatValue) { + if (definedClass == null) + throw new NullPointerException("defined class"); + String className = definedClass.getCanonicalName(); + try { + return new FinalClassField<>( + definedClass.getField(name), className, name, compatValue); + } catch (Exception e) { + return new FinalClassField<>(null, className, name, compatValue); + } + } + + public static FinalClassField newInstance(final String className, final String fieldName, + final T compatValue) { + try { + return newInstance(Class.forName(className), fieldName, compatValue); + } catch (ClassNotFoundException e) { + return new FinalClassField<>(null, className, fieldName, compatValue); + } + } +} diff --git a/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/InstanceMethod.java b/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/InstanceMethod.java new file mode 100644 index 000000000..05dc0d6dc --- /dev/null +++ b/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/InstanceMethod.java @@ -0,0 +1,83 @@ +/* + * 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 com.android.inputmethod.tools.edittextvariations; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public final class InstanceMethod { + public final boolean defined; + public final String name; + public final String className; + + private final Class clazz; + private final Method method; + + private InstanceMethod(final Class receiverClass, final Method instanceMethod, + final String receiverName, final String methodName) { + this.defined = instanceMethod != null; + this.clazz = receiverClass; + this.method = instanceMethod; + this.name = methodName; + this.className = receiverName; + } + + public Object invoke(final Object receiverObject, final Object... args) { + if (!defined) + throw new RuntimeException("method " + name + " not defined"); + if (receiverObject == null) + throw new NullPointerException("receiver object"); + if (clazz.isInstance(receiverObject)) { + try { + if (args.length == 0) { + return method.invoke(receiverObject); + } + return method.invoke(clazz, args); + } catch (IllegalArgumentException e) { + throw new RuntimeException("IllegalArgumentException"); + } catch (IllegalAccessException e) { + throw new RuntimeException("IllegalAccessException"); + } catch (InvocationTargetException e) { + throw new RuntimeException("InvocationTargetException"); + } + } + throw new RuntimeException("receiver type not matched: method=" + name + + " actual receiver=" + receiverObject.getClass().getCanonicalName()); + } + + public static InstanceMethod newInstance(final Class receiverClass, final String methodName, + final Class... parameterTypes) { + if (receiverClass == null) + throw new NullPointerException("receiver class"); + final String className = receiverClass.getCanonicalName(); + try { + return new InstanceMethod(receiverClass, + receiverClass.getMethod(methodName, parameterTypes), className, methodName); + } catch (Exception e) { + return new InstanceMethod(receiverClass, null, className, methodName); + } + } + + public static InstanceMethod newInstance(final String className, final String methodName, + final Class... parameterTypes) { + try { + return newInstance(Class.forName(className), methodName, parameterTypes); + } catch (ClassNotFoundException e) { + return new InstanceMethod(null, null, className, methodName); + } + } +} diff --git a/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/MultiLineShortMessageEditText.java b/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/MultiLineShortMessageEditText.java new file mode 100644 index 000000000..7ab700776 --- /dev/null +++ b/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/MultiLineShortMessageEditText.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.inputmethod.tools.edittextvariations; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; +import android.widget.EditText; + +public final class MultiLineShortMessageEditText extends EditText { + + public MultiLineShortMessageEditText(final Context context) { + super(context); + } + + public MultiLineShortMessageEditText(final Context context, final AttributeSet attrs) { + super(context, attrs); + } + + public MultiLineShortMessageEditText(final Context context, final AttributeSet attrs, + final int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public InputConnection onCreateInputConnection(final EditorInfo outAttrs) { + final InputConnection ic = super.onCreateInputConnection(outAttrs); + outAttrs.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION; + return ic; + } +} diff --git a/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/ThemeItem.java b/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/ThemeItem.java new file mode 100644 index 000000000..f3c6d4f54 --- /dev/null +++ b/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/ThemeItem.java @@ -0,0 +1,68 @@ +/* + * 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 com.android.inputmethod.tools.edittextvariations; + +import android.os.Build; + +import com.android.inputmethod.tools.edittextvariations.R; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class ThemeItem { + public final int id; + public final String name; + + private ThemeItem(final String name, final int resId) { + this.id = resId; + this.name = name; + } + + private static final String THEME_DEFAULT = "Default"; + private static final String THEME_HOLO = "Theme_Holo"; + private static final String THEME_HOLO_LIGHT = "Theme_Holo_Light"; + private static final String THEME_DEVICE_DEFAULT = "Theme_DeviceDefault"; + private static final String THEME_DEVICE_DEFAULT_LIGHT = "Theme_DeviceDefault_Light"; + private static final String THEME_MATERIAL = "Theme_Material"; + private static final String THEME_MATERIAL_LIGHT = "Theme_Material_Light"; + + public static String getDefaultThemeName() { + return THEME_DEFAULT; + } + + public static final List THEME_LIST = createThemeList( + THEME_HOLO, THEME_HOLO_LIGHT, THEME_DEVICE_DEFAULT, THEME_DEVICE_DEFAULT_LIGHT, + THEME_MATERIAL, THEME_MATERIAL_LIGHT); + + private static List createThemeList(final String... candidateList) { + final ArrayList list = new ArrayList<>(); + + // Default theme is always available as it's defined in our resource. + list.add(new ThemeItem(THEME_DEFAULT, R.style.defaultActivityTheme)); + + for (final String name : candidateList) { + final FinalClassField constant = + FinalClassField.newInstance(android.R.style.class, name, 0); + if (constant.defined) { + list.add(new ThemeItem(name, constant.value)); + } + } + + return Collections.unmodifiableList(list); + } +} -- cgit v1.2.3-83-g751a From da2486fd63d7d46e982d0e2fb2eba3dac07aff32 Mon Sep 17 00:00:00 2001 From: Yohei Yukawa Date: Wed, 16 Jan 2019 10:28:07 -0800 Subject: Support testing Direct Reply with EditTextVariations With this CL, EditTextVariations is able to send Direct Repply notification so that we can easily test IME behaviors there. You don't need to set up a chat application then let it receive a message from someone else any more just to test IME behaviors on Direct Reply. Fix: 122957841 Test: manually done as follows. 1. tapas EditTextVariations 2. make -j 3. adb install -r \ $ANDROID_TARGET_OUT_TESTCASES/EditTextVariations/EditTextVariations.apk 4. Open EditTextVariations 5. Tap the menu icon. 6. Select "Direct Reply" 7. Make sure that there is a Direct Reply notification. Change-Id: Iafffcc7d138b0f502116a5e557f0c3f17e9d0b73 --- tools/EditTextVariations/AndroidManifest.xml | 3 + tools/EditTextVariations/res/values/strings.xml | 2 + .../edittextvariations/EditTextVariations.java | 8 +- .../NotificationBroadcastReceiver.java | 31 ++++++ .../edittextvariations/NotificationUtils.java | 119 +++++++++++++++++++++ 5 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/NotificationBroadcastReceiver.java create mode 100644 tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/NotificationUtils.java (limited to 'tools/EditTextVariations/src') diff --git a/tools/EditTextVariations/AndroidManifest.xml b/tools/EditTextVariations/AndroidManifest.xml index 7694f4db9..96c244b32 100644 --- a/tools/EditTextVariations/AndroidManifest.xml +++ b/tools/EditTextVariations/AndroidManifest.xml @@ -40,5 +40,8 @@ + diff --git a/tools/EditTextVariations/res/values/strings.xml b/tools/EditTextVariations/res/values/strings.xml index 02387f2ff..cb896e8b6 100644 --- a/tools/EditTextVariations/res/values/strings.xml +++ b/tools/EditTextVariations/res/values/strings.xml @@ -33,6 +33,8 @@ Keyboard Visible Keyboard Hidden + + Direct Reply Custom diff --git a/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/EditTextVariations.java b/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/EditTextVariations.java index 44e0a4d55..6eb85a528 100644 --- a/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/EditTextVariations.java +++ b/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/EditTextVariations.java @@ -60,6 +60,7 @@ public final class EditTextVariations extends Activity implements TextView.OnEdi private static final int MENU_NAVIGATE_OFF = 3; private static final int MENU_SOFTINPUT_VISIBLE = 4; private static final int MENU_SOFTINPUT_HIDDEN = 5; + private static final int MENU_DIRECT_REPLY = 6; private static final String PREF_THEME = "theme"; private static final String PREF_NAVIGATE = "navigate"; private static final String PREF_SOFTINPUT = "softinput"; @@ -162,9 +163,12 @@ public final class EditTextVariations extends Activity implements TextView.OnEdi menu.add(Menu.NONE, MENU_SOFTINPUT_VISIBLE, 2, getString(R.string.menu_softinput_visible)); menu.add(Menu.NONE, MENU_SOFTINPUT_HIDDEN, 3, getString(R.string.menu_softinput_hidden)); menu.add(Menu.NONE, MENU_CHANGE_THEME, 4, R.string.menu_change_theme); + if (NotificationUtils.DIRECT_REPLY_SUPPORTED) { + menu.add(Menu.NONE, MENU_DIRECT_REPLY, 5, R.string.menu_direct_reply); + } try { final PackageInfo pinfo = getPackageManager().getPackageInfo(getPackageName(), 0); - menu.add(Menu.NONE, MENU_VERSION, 5, + menu.add(Menu.NONE, MENU_VERSION, 6, getString(R.string.menu_version, pinfo.versionName)) .setEnabled(false); } catch (NameNotFoundException e) { @@ -194,6 +198,8 @@ public final class EditTextVariations extends Activity implements TextView.OnEdi } else if (itemId == MENU_SOFTINPUT_VISIBLE || itemId == MENU_SOFTINPUT_HIDDEN) { saveSoftInputMode(itemId == MENU_SOFTINPUT_VISIBLE); restartActivity(); + } else if (itemId == MENU_DIRECT_REPLY) { + NotificationUtils.sendDirectReplyNotification(this); } return true; } diff --git a/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/NotificationBroadcastReceiver.java b/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/NotificationBroadcastReceiver.java new file mode 100644 index 000000000..97db49b15 --- /dev/null +++ b/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/NotificationBroadcastReceiver.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2019 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.tools.edittextvariations; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +/** + * A non-exported {@link BroadcastReceiver} to receive {@link Intent} from notifications. + */ +public final class NotificationBroadcastReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + NotificationUtils.onReceiveDirectReply(context, intent); + } +} diff --git a/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/NotificationUtils.java b/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/NotificationUtils.java new file mode 100644 index 000000000..64480bbf0 --- /dev/null +++ b/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/NotificationUtils.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2019 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.tools.edittextvariations; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.RemoteInput; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.os.Process; +import android.os.UserHandle; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +final class NotificationUtils { + private static final String REPLY_ACTION = "REPLY_ACTION"; + private static final String KEY_REPLY = "KEY_REPLY"; + private static final String KEY_NOTIFICATION_ID = "KEY_NOTIFICATION_ID"; + private static final String CHANNEL_NAME = "Channel Name"; + private static final String CHANNEL_DESCRIPTION = "Channel Description"; + private static final String CHANNEL_ID = "Channel ID"; + private static final AtomicBoolean sNotificationChannelInitialized = new AtomicBoolean(); + private static final AtomicInteger sNextNotificationId = new AtomicInteger(1); + + static final boolean DIRECT_REPLY_SUPPORTED = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N; + + static void ensureNotificationChannel(Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + // NotificationChannel is not implemented. No need to set up notification channel. + return; + } + if (!sNotificationChannelInitialized.compareAndSet(false, true)) { + // Already initialized. + return; + } + + // Create the NotificationChannel + final NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, + NotificationManager.IMPORTANCE_DEFAULT); + channel.setDescription(CHANNEL_DESCRIPTION); + // Register the channel with the system; you can't change the importance + // or other notification behaviors after this + context.getSystemService(NotificationManager.class).createNotificationChannel(channel); + } + + static void sendDirectReplyNotification(Context context) { + if (!DIRECT_REPLY_SUPPORTED) { + // DirectReply is not supported. + return; + } + + ensureNotificationChannel(context); + + RemoteInput remoteInput = new RemoteInput.Builder(KEY_REPLY) + .setLabel("Reply Label") + .build(); + + final int notificationId = sNextNotificationId.getAndIncrement(); + final PendingIntent pendingIntent = getReplyPendingIntent(context, notificationId); + final Notification.Action action = + new Notification.Action.Builder(null, "Direct Reply Test", pendingIntent) + .addRemoteInput(remoteInput) + .build(); + final Notification notification = new Notification.Builder(context, CHANNEL_ID) + .setContentText("Content Title") + .setSmallIcon(R.drawable.ic_launcher) + .setContentText("Message from " + UserHandle.getUserHandleForUid(Process.myUid())) + .setShowWhen(true) + .addAction(action) + .build(); + context.getSystemService(NotificationManager.class).notify(notificationId, notification); + } + + static void onReceiveDirectReply(Context context, Intent intent) { + final Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); + if (remoteInput == null) { + return; + } + final CharSequence reply = remoteInput.getCharSequence(KEY_REPLY); + final int notificationId = intent.getIntExtra(KEY_NOTIFICATION_ID, 0); + final Notification.Builder notificationBuilder = + new Notification.Builder(context, CHANNEL_ID); + notificationBuilder.setContentText("Content Title") + .setSmallIcon(R.drawable.ic_launcher) + .setContentText(String.format("Sent \"%s\" to %s", reply, + UserHandle.getUserHandleForUid(Process.myUid()))); + context.getSystemService(NotificationManager.class) + .notify(notificationId, notificationBuilder.build()); + } + + private static PendingIntent getReplyPendingIntent(Context context, int notificationId) { + final Intent intent = new Intent(context, NotificationBroadcastReceiver.class); + intent.setAction(REPLY_ACTION); + intent.putExtra(KEY_NOTIFICATION_ID, notificationId); + // Pass notificationId as the result code to get a new PendingIntent rather than an existing + // one. + return PendingIntent.getBroadcast(context.getApplicationContext(), notificationId, intent, + PendingIntent.FLAG_ONE_SHOT); + } +} -- cgit v1.2.3-83-g751a From bdf7d6f56d2017efbcf082e95773fcddce7736f0 Mon Sep 17 00:00:00 2001 From: Yohei Yukawa Date: Tue, 5 Feb 2019 22:46:42 -0800 Subject: Support testing Direct Reply with EditTextVariations on API 24/25 This is a follow up CL to my previous CL [1], which enabled us to test Direct-Reply with EditTextVariations. What this CL does are: * Specyfing min_sdk_version to avoid INSTALL_FAILED_OLDER_SDK error when installing on older devices. * Avoiding NoSuchMethodError on pre-O devices where Notification.Builder does not have a constructor that takes notification channel. * Fixing a race condition where notification can be sent before notification channel is created. [1]: Iafffcc7d138b0f502116a5e557f0c3f17e9d0b73 da2486fd63d7d46e982d0e2fb2eba3dac07aff32 Bug: 122957841 Test: Made sure that we can install EditTextVariations on N devices and "Direct-Reply" on EditTextVariations works there. Change-Id: Ib4fbd447608b111e763fde4287226cf7e206e65e --- tools/EditTextVariations/Android.bp | 1 + .../edittextvariations/NotificationUtils.java | 39 ++++++++++++---------- 2 files changed, 22 insertions(+), 18 deletions(-) (limited to 'tools/EditTextVariations/src') diff --git a/tools/EditTextVariations/Android.bp b/tools/EditTextVariations/Android.bp index 36296a58b..94b4951a7 100644 --- a/tools/EditTextVariations/Android.bp +++ b/tools/EditTextVariations/Android.bp @@ -18,4 +18,5 @@ android_test { srcs: ["src/**/*.java"], sdk_version: "current", + min_sdk_version: "11", } diff --git a/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/NotificationUtils.java b/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/NotificationUtils.java index 64480bbf0..a9b7132eb 100644 --- a/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/NotificationUtils.java +++ b/tools/EditTextVariations/src/com/android/inputmethod/tools/edittextvariations/NotificationUtils.java @@ -38,28 +38,33 @@ final class NotificationUtils { private static final String CHANNEL_NAME = "Channel Name"; private static final String CHANNEL_DESCRIPTION = "Channel Description"; private static final String CHANNEL_ID = "Channel ID"; - private static final AtomicBoolean sNotificationChannelInitialized = new AtomicBoolean(); private static final AtomicInteger sNextNotificationId = new AtomicInteger(1); + private static final Object sLock = new Object(); + private static boolean sNotificationChannelInitialized = false; + + static final boolean NOTIFICATION_CHANNEL_REQUIRED = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; static final boolean DIRECT_REPLY_SUPPORTED = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N; - static void ensureNotificationChannel(Context context) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + private static Notification.Builder createNotificationBuilder(Context context) { + if (!NOTIFICATION_CHANNEL_REQUIRED) { // NotificationChannel is not implemented. No need to set up notification channel. - return; - } - if (!sNotificationChannelInitialized.compareAndSet(false, true)) { - // Already initialized. - return; + return new Notification.Builder(context); } - // Create the NotificationChannel - final NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, - NotificationManager.IMPORTANCE_DEFAULT); - channel.setDescription(CHANNEL_DESCRIPTION); - // Register the channel with the system; you can't change the importance - // or other notification behaviors after this - context.getSystemService(NotificationManager.class).createNotificationChannel(channel); + // Make sure that a notification channel is created *before* we send a notification. + synchronized (sLock) { + if (!sNotificationChannelInitialized) { + final NotificationChannel channel = new NotificationChannel(CHANNEL_ID, + CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT); + channel.setDescription(CHANNEL_DESCRIPTION); + context.getSystemService(NotificationManager.class) + .createNotificationChannel(channel); + sNotificationChannelInitialized = true; + } + } + return new Notification.Builder(context, CHANNEL_ID); } static void sendDirectReplyNotification(Context context) { @@ -68,8 +73,6 @@ final class NotificationUtils { return; } - ensureNotificationChannel(context); - RemoteInput remoteInput = new RemoteInput.Builder(KEY_REPLY) .setLabel("Reply Label") .build(); @@ -80,7 +83,7 @@ final class NotificationUtils { new Notification.Action.Builder(null, "Direct Reply Test", pendingIntent) .addRemoteInput(remoteInput) .build(); - final Notification notification = new Notification.Builder(context, CHANNEL_ID) + final Notification notification = createNotificationBuilder(context) .setContentText("Content Title") .setSmallIcon(R.drawable.ic_launcher) .setContentText("Message from " + UserHandle.getUserHandleForUid(Process.myUid())) -- cgit v1.2.3-83-g751a