diff options
author | TreeHugger Robot <treehugger-gerrit@google.com> | 2018-07-10 05:14:25 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2018-07-10 05:14:25 +0000 |
commit | a15511e9c4519735cffeb0caa12eaac12ed2bc54 (patch) | |
tree | 83a0685e66ff1b459d89956e04869b17e795e72c | |
parent | 262855c12ac95ec83cc26fa1d0098f83aa6d6603 (diff) | |
parent | 464da581aa65b0b29a398fed13d1f708d09e3961 (diff) |
Merge "Preparation task for Settings Fragment Migration"
17 files changed, 2372 insertions, 5 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/CustomDialogPreferenceCompat.java b/packages/SettingsLib/src/com/android/settingslib/CustomDialogPreferenceCompat.java new file mode 100644 index 000000000000..6ac9d4e21f40 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/CustomDialogPreferenceCompat.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2018 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.settingslib; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.util.AttributeSet; +import android.view.View; + +import androidx.appcompat.app.AlertDialog; +import androidx.preference.DialogPreference; +import androidx.preference.PreferenceDialogFragmentCompat; + +public class CustomDialogPreferenceCompat extends DialogPreference { + + private CustomPreferenceDialogFragment mFragment; + private DialogInterface.OnShowListener mOnShowListener; + + public CustomDialogPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public CustomDialogPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public CustomDialogPreferenceCompat(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CustomDialogPreferenceCompat(Context context) { + super(context); + } + + public boolean isDialogOpen() { + return getDialog() != null && getDialog().isShowing(); + } + + public Dialog getDialog() { + return mFragment != null ? mFragment.getDialog() : null; + } + + public void setOnShowListener(DialogInterface.OnShowListener listner) { + mOnShowListener = listner; + } + + protected void onPrepareDialogBuilder(AlertDialog.Builder builder, + DialogInterface.OnClickListener listener) { + } + + protected void onDialogClosed(boolean positiveResult) { + } + + protected void onClick(DialogInterface dialog, int which) { + } + + protected void onBindDialogView(View view) { + } + + private void setFragment(CustomPreferenceDialogFragment fragment) { + mFragment = fragment; + } + + private DialogInterface.OnShowListener getOnShowListener() { + return mOnShowListener; + } + + public static class CustomPreferenceDialogFragment extends PreferenceDialogFragmentCompat { + + public static CustomPreferenceDialogFragment newInstance(String key) { + final CustomPreferenceDialogFragment fragment = new CustomPreferenceDialogFragment(); + final Bundle b = new Bundle(1); + b.putString(ARG_KEY, key); + fragment.setArguments(b); + return fragment; + } + + private CustomDialogPreferenceCompat getCustomizablePreference() { + return (CustomDialogPreferenceCompat) getPreference(); + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + super.onPrepareDialogBuilder(builder); + getCustomizablePreference().setFragment(this); + getCustomizablePreference().onPrepareDialogBuilder(builder, this); + } + + @Override + public void onDialogClosed(boolean positiveResult) { + getCustomizablePreference().onDialogClosed(positiveResult); + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + getCustomizablePreference().onBindDialogView(view); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.setOnShowListener(getCustomizablePreference().getOnShowListener()); + return dialog; + } + + @Override + public void onClick(DialogInterface dialog, int which) { + super.onClick(dialog, which); + getCustomizablePreference().onClick(dialog, which); + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/CustomEditTextPreferenceCompat.java b/packages/SettingsLib/src/com/android/settingslib/CustomEditTextPreferenceCompat.java new file mode 100644 index 000000000000..6ddc89af03ad --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/CustomEditTextPreferenceCompat.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2018 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.settingslib; + +import static android.text.InputType.TYPE_CLASS_TEXT; +import static android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.util.AttributeSet; +import android.view.View; +import android.widget.EditText; + +import androidx.annotation.CallSuper; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.EditTextPreference; +import androidx.preference.EditTextPreferenceDialogFragmentCompat; + +public class CustomEditTextPreferenceCompat extends EditTextPreference { + + private CustomPreferenceDialogFragment mFragment; + + public CustomEditTextPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public CustomEditTextPreferenceCompat(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public CustomEditTextPreferenceCompat(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CustomEditTextPreferenceCompat(Context context) { + super(context); + } + + public EditText getEditText() { + if (mFragment != null) { + final Dialog dialog = mFragment.getDialog(); + if (dialog != null) { + return (EditText) dialog.findViewById(android.R.id.edit); + } + } + return null; + } + + public boolean isDialogOpen() { + return getDialog() != null && getDialog().isShowing(); + } + + public Dialog getDialog() { + return mFragment != null ? mFragment.getDialog() : null; + } + + protected void onPrepareDialogBuilder(AlertDialog.Builder builder, + DialogInterface.OnClickListener listener) { + } + + protected void onDialogClosed(boolean positiveResult) { + } + + protected void onClick(DialogInterface dialog, int which) { + } + + @CallSuper + protected void onBindDialogView(View view) { + final EditText editText = view.findViewById(android.R.id.edit); + if (editText != null) { + editText.setInputType(TYPE_CLASS_TEXT | TYPE_TEXT_FLAG_CAP_SENTENCES); + editText.requestFocus(); + } + } + + private void setFragment(CustomPreferenceDialogFragment fragment) { + mFragment = fragment; + } + + public static class CustomPreferenceDialogFragment extends + EditTextPreferenceDialogFragmentCompat { + + public static CustomPreferenceDialogFragment newInstance(String key) { + final CustomPreferenceDialogFragment fragment = new CustomPreferenceDialogFragment(); + final Bundle b = new Bundle(1); + b.putString(ARG_KEY, key); + fragment.setArguments(b); + return fragment; + } + + private CustomEditTextPreferenceCompat getCustomizablePreference() { + return (CustomEditTextPreferenceCompat) getPreference(); + } + + @Override + protected void onBindDialogView(View view) { + super.onBindDialogView(view); + getCustomizablePreference().onBindDialogView(view); + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + super.onPrepareDialogBuilder(builder); + getCustomizablePreference().setFragment(this); + getCustomizablePreference().onPrepareDialogBuilder(builder, this); + } + + @Override + public void onDialogClosed(boolean positiveResult) { + super.onDialogClosed(positiveResult); + getCustomizablePreference().onDialogClosed(positiveResult); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + super.onClick(dialog, which); + getCustomizablePreference().onClick(dialog, which); + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeEnablerManagerCompat.java b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeEnablerManagerCompat.java new file mode 100644 index 000000000000..ad1368c7731d --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeEnablerManagerCompat.java @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2018 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.settingslib.inputmethod; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.text.TextUtils; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodSubtype; + +import com.android.settingslib.R; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; +import androidx.preference.TwoStatePreference; + +public class InputMethodAndSubtypeEnablerManagerCompat implements + Preference.OnPreferenceChangeListener { + + private final PreferenceFragmentCompat mFragment; + + private boolean mHaveHardKeyboard; + private final HashMap<String, List<Preference>> mInputMethodAndSubtypePrefsMap = + new HashMap<>(); + private final HashMap<String, TwoStatePreference> mAutoSelectionPrefsMap = new HashMap<>(); + private InputMethodManager mImm; + // TODO: Change mInputMethodInfoList to Map + private List<InputMethodInfo> mInputMethodInfoList; + private final Collator mCollator = Collator.getInstance(); + + public InputMethodAndSubtypeEnablerManagerCompat(PreferenceFragmentCompat fragment) { + mFragment = fragment; + mImm = fragment.getContext().getSystemService(InputMethodManager.class); + + mInputMethodInfoList = mImm.getInputMethodList(); + } + + public void init(PreferenceFragmentCompat fragment, String targetImi, PreferenceScreen root) { + final Configuration config = fragment.getResources().getConfiguration(); + mHaveHardKeyboard = (config.keyboard == Configuration.KEYBOARD_QWERTY); + + for (final InputMethodInfo imi : mInputMethodInfoList) { + // Add subtype preferences of this IME when it is specified or no IME is specified. + if (imi.getId().equals(targetImi) || TextUtils.isEmpty(targetImi)) { + addInputMethodSubtypePreferences(fragment, imi, root); + } + } + } + + public void refresh(Context context, PreferenceFragmentCompat fragment) { + // Refresh internal states in mInputMethodSettingValues to keep the latest + // "InputMethodInfo"s and "InputMethodSubtype"s + InputMethodSettingValuesWrapper + .getInstance(context).refreshAllInputMethodAndSubtypes(); + InputMethodAndSubtypeUtilCompat.loadInputMethodSubtypeList(fragment, + context.getContentResolver(), mInputMethodInfoList, mInputMethodAndSubtypePrefsMap); + updateAutoSelectionPreferences(); + } + + public void save(Context context, PreferenceFragmentCompat fragment) { + InputMethodAndSubtypeUtilCompat.saveInputMethodSubtypeList(fragment, + context.getContentResolver(), mInputMethodInfoList, mHaveHardKeyboard); + } + + @Override + public boolean onPreferenceChange(final Preference pref, final Object newValue) { + if (!(newValue instanceof Boolean)) { + return true; // Invoke default behavior. + } + final boolean isChecking = (Boolean) newValue; + for (final String imiId : mAutoSelectionPrefsMap.keySet()) { + // An auto select subtype preference is changing. + if (mAutoSelectionPrefsMap.get(imiId) == pref) { + final TwoStatePreference autoSelectionPref = (TwoStatePreference) pref; + autoSelectionPref.setChecked(isChecking); + // Enable or disable subtypes depending on the auto selection preference. + setAutoSelectionSubtypesEnabled(imiId, autoSelectionPref.isChecked()); + return false; + } + } + // A subtype preference is changing. + if (pref instanceof InputMethodSubtypePreference) { + final InputMethodSubtypePreference subtypePref = (InputMethodSubtypePreference) pref; + subtypePref.setChecked(isChecking); + if (!subtypePref.isChecked()) { + // It takes care of the case where no subtypes are explicitly enabled then the auto + // selection preference is going to be checked. + updateAutoSelectionPreferences(); + } + return false; + } + return true; // Invoke default behavior. + } + + private void addInputMethodSubtypePreferences(PreferenceFragmentCompat fragment, + InputMethodInfo imi, final PreferenceScreen root) { + Context prefContext = fragment.getPreferenceManager().getContext(); + + final int subtypeCount = imi.getSubtypeCount(); + if (subtypeCount <= 1) { + return; + } + final String imiId = imi.getId(); + final PreferenceCategory keyboardSettingsCategory = + new PreferenceCategory(prefContext); + root.addPreference(keyboardSettingsCategory); + final PackageManager pm = prefContext.getPackageManager(); + final CharSequence label = imi.loadLabel(pm); + + keyboardSettingsCategory.setTitle(label); + keyboardSettingsCategory.setKey(imiId); + // TODO: Use toggle Preference if images are ready. + final TwoStatePreference autoSelectionPref = + new SwitchWithNoTextPreference(prefContext); + mAutoSelectionPrefsMap.put(imiId, autoSelectionPref); + keyboardSettingsCategory.addPreference(autoSelectionPref); + autoSelectionPref.setOnPreferenceChangeListener(this); + + final PreferenceCategory activeInputMethodsCategory = + new PreferenceCategory(prefContext); + activeInputMethodsCategory.setTitle(R.string.active_input_method_subtypes); + root.addPreference(activeInputMethodsCategory); + + CharSequence autoSubtypeLabel = null; + final ArrayList<Preference> subtypePreferences = new ArrayList<>(); + for (int index = 0; index < subtypeCount; ++index) { + final InputMethodSubtype subtype = imi.getSubtypeAt(index); + if (subtype.overridesImplicitlyEnabledSubtype()) { + if (autoSubtypeLabel == null) { + autoSubtypeLabel = InputMethodAndSubtypeUtil.getSubtypeLocaleNameAsSentence( + subtype, prefContext, imi); + } + } else { + final Preference subtypePref = new InputMethodSubtypePreference( + prefContext, subtype, imi); + subtypePreferences.add(subtypePref); + } + } + subtypePreferences.sort((lhs, rhs) -> { + if (lhs instanceof InputMethodSubtypePreference) { + return ((InputMethodSubtypePreference) lhs).compareTo(rhs, mCollator); + } + return lhs.compareTo(rhs); + }); + for (final Preference pref : subtypePreferences) { + activeInputMethodsCategory.addPreference(pref); + pref.setOnPreferenceChangeListener(this); + InputMethodAndSubtypeUtil.removeUnnecessaryNonPersistentPreference(pref); + } + mInputMethodAndSubtypePrefsMap.put(imiId, subtypePreferences); + if (TextUtils.isEmpty(autoSubtypeLabel)) { + autoSelectionPref.setTitle( + R.string.use_system_language_to_select_input_method_subtypes); + } else { + autoSelectionPref.setTitle(autoSubtypeLabel); + } + } + + private boolean isNoSubtypesExplicitlySelected(final String imiId) { + final List<Preference> subtypePrefs = mInputMethodAndSubtypePrefsMap.get(imiId); + for (final Preference pref : subtypePrefs) { + if (pref instanceof TwoStatePreference && ((TwoStatePreference) pref).isChecked()) { + return false; + } + } + return true; + } + + private void setAutoSelectionSubtypesEnabled(final String imiId, + final boolean autoSelectionEnabled) { + final TwoStatePreference autoSelectionPref = mAutoSelectionPrefsMap.get(imiId); + if (autoSelectionPref == null) { + return; + } + autoSelectionPref.setChecked(autoSelectionEnabled); + final List<Preference> subtypePrefs = mInputMethodAndSubtypePrefsMap.get(imiId); + for (final Preference pref : subtypePrefs) { + if (pref instanceof TwoStatePreference) { + // When autoSelectionEnabled is true, all subtype prefs need to be disabled with + // implicitly checked subtypes. In case of false, all subtype prefs need to be + // enabled. + pref.setEnabled(!autoSelectionEnabled); + if (autoSelectionEnabled) { + ((TwoStatePreference) pref).setChecked(false); + } + } + } + if (autoSelectionEnabled) { + InputMethodAndSubtypeUtilCompat.saveInputMethodSubtypeList( + mFragment, mFragment.getContext().getContentResolver(), + mInputMethodInfoList, mHaveHardKeyboard); + updateImplicitlyEnabledSubtypes(imiId); + } + } + + private void updateImplicitlyEnabledSubtypes(final String targetImiId) { + // When targetImiId is null, apply to all subtypes of all IMEs + for (final InputMethodInfo imi : mInputMethodInfoList) { + final String imiId = imi.getId(); + final TwoStatePreference autoSelectionPref = mAutoSelectionPrefsMap.get(imiId); + // No need to update implicitly enabled subtypes when the user has unchecked the + // "subtype auto selection". + if (autoSelectionPref == null || !autoSelectionPref.isChecked()) { + continue; + } + if (imiId.equals(targetImiId) || targetImiId == null) { + updateImplicitlyEnabledSubtypesOf(imi); + } + } + } + + private void updateImplicitlyEnabledSubtypesOf(final InputMethodInfo imi) { + final String imiId = imi.getId(); + final List<Preference> subtypePrefs = mInputMethodAndSubtypePrefsMap.get(imiId); + final List<InputMethodSubtype> implicitlyEnabledSubtypes = + mImm.getEnabledInputMethodSubtypeList(imi, true); + if (subtypePrefs == null || implicitlyEnabledSubtypes == null) { + return; + } + for (final Preference pref : subtypePrefs) { + if (!(pref instanceof TwoStatePreference)) { + continue; + } + final TwoStatePreference subtypePref = (TwoStatePreference) pref; + subtypePref.setChecked(false); + for (final InputMethodSubtype subtype : implicitlyEnabledSubtypes) { + final String implicitlyEnabledSubtypePrefKey = imiId + subtype.hashCode(); + if (subtypePref.getKey().equals(implicitlyEnabledSubtypePrefKey)) { + subtypePref.setChecked(true); + break; + } + } + } + } + + private void updateAutoSelectionPreferences() { + for (final String imiId : mInputMethodAndSubtypePrefsMap.keySet()) { + setAutoSelectionSubtypesEnabled(imiId, isNoSubtypesExplicitlySelected(imiId)); + } + updateImplicitlyEnabledSubtypes(null /* targetImiId */ /* check */); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtilCompat.java b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtilCompat.java new file mode 100644 index 000000000000..9ad2adbcc432 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtilCompat.java @@ -0,0 +1,436 @@ +/* + * Copyright (C) 2018 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.settingslib.inputmethod; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.icu.text.ListFormatter; +import android.provider.Settings; +import android.provider.Settings.SettingNotFoundException; +import android.text.TextUtils; +import android.util.Log; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodSubtype; + +import com.android.internal.app.LocaleHelper; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; +import androidx.preference.TwoStatePreference; + +// TODO: Consolidate this with {@link InputMethodSettingValuesWrapper}. +public class InputMethodAndSubtypeUtilCompat { + + private static final boolean DEBUG = false; + private static final String TAG = "InputMethdAndSubtypeUtlCompat"; + + private static final String SUBTYPE_MODE_KEYBOARD = "keyboard"; + private static final char INPUT_METHOD_SEPARATER = ':'; + private static final char INPUT_METHOD_SUBTYPE_SEPARATER = ';'; + private static final int NOT_A_SUBTYPE_ID = -1; + + private static final TextUtils.SimpleStringSplitter sStringInputMethodSplitter + = new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATER); + + private static final TextUtils.SimpleStringSplitter sStringInputMethodSubtypeSplitter + = new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATER); + + // InputMethods and subtypes are saved in the settings as follows: + // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1 + public static String buildInputMethodsAndSubtypesString( + final HashMap<String, HashSet<String>> imeToSubtypesMap) { + final StringBuilder builder = new StringBuilder(); + for (final String imi : imeToSubtypesMap.keySet()) { + if (builder.length() > 0) { + builder.append(INPUT_METHOD_SEPARATER); + } + final HashSet<String> subtypeIdSet = imeToSubtypesMap.get(imi); + builder.append(imi); + for (final String subtypeId : subtypeIdSet) { + builder.append(INPUT_METHOD_SUBTYPE_SEPARATER).append(subtypeId); + } + } + return builder.toString(); + } + + private static String buildInputMethodsString(final HashSet<String> imiList) { + final StringBuilder builder = new StringBuilder(); + for (final String imi : imiList) { + if (builder.length() > 0) { + builder.append(INPUT_METHOD_SEPARATER); + } + builder.append(imi); + } + return builder.toString(); + } + + private static int getInputMethodSubtypeSelected(ContentResolver resolver) { + try { + return Settings.Secure.getInt(resolver, + Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE); + } catch (SettingNotFoundException e) { + return NOT_A_SUBTYPE_ID; + } + } + + private static boolean isInputMethodSubtypeSelected(ContentResolver resolver) { + return getInputMethodSubtypeSelected(resolver) != NOT_A_SUBTYPE_ID; + } + + private static void putSelectedInputMethodSubtype(ContentResolver resolver, int hashCode) { + Settings.Secure.putInt(resolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, hashCode); + } + + // Needs to modify InputMethodManageService if you want to change the format of saved string. + static HashMap<String, HashSet<String>> getEnabledInputMethodsAndSubtypeList( + ContentResolver resolver) { + final String enabledInputMethodsStr = Settings.Secure.getString( + resolver, Settings.Secure.ENABLED_INPUT_METHODS); + if (DEBUG) { + Log.d(TAG, "--- Load enabled input methods: " + enabledInputMethodsStr); + } + return parseInputMethodsAndSubtypesString(enabledInputMethodsStr); + } + + public static HashMap<String, HashSet<String>> parseInputMethodsAndSubtypesString( + final String inputMethodsAndSubtypesString) { + final HashMap<String, HashSet<String>> subtypesMap = new HashMap<>(); + if (TextUtils.isEmpty(inputMethodsAndSubtypesString)) { + return subtypesMap; + } + sStringInputMethodSplitter.setString(inputMethodsAndSubtypesString); + while (sStringInputMethodSplitter.hasNext()) { + final String nextImsStr = sStringInputMethodSplitter.next(); + sStringInputMethodSubtypeSplitter.setString(nextImsStr); + if (sStringInputMethodSubtypeSplitter.hasNext()) { + final HashSet<String> subtypeIdSet = new HashSet<>(); + // The first element is {@link InputMethodInfoId}. + final String imiId = sStringInputMethodSubtypeSplitter.next(); + while (sStringInputMethodSubtypeSplitter.hasNext()) { + subtypeIdSet.add(sStringInputMethodSubtypeSplitter.next()); + } + subtypesMap.put(imiId, subtypeIdSet); + } + } + return subtypesMap; + } + + private static HashSet<String> getDisabledSystemIMEs(ContentResolver resolver) { + HashSet<String> set = new HashSet<>(); + String disabledIMEsStr = Settings.Secure.getString( + resolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS); + if (TextUtils.isEmpty(disabledIMEsStr)) { + return set; + } + sStringInputMethodSplitter.setString(disabledIMEsStr); + while(sStringInputMethodSplitter.hasNext()) { + set.add(sStringInputMethodSplitter.next()); + } + return set; + } + + public static void saveInputMethodSubtypeList(PreferenceFragmentCompat context, + ContentResolver resolver, List<InputMethodInfo> inputMethodInfos, + boolean hasHardKeyboard) { + String currentInputMethodId = Settings.Secure.getString(resolver, + Settings.Secure.DEFAULT_INPUT_METHOD); + final int selectedInputMethodSubtype = getInputMethodSubtypeSelected(resolver); + final HashMap<String, HashSet<String>> enabledIMEsAndSubtypesMap = + getEnabledInputMethodsAndSubtypeList(resolver); + final HashSet<String> disabledSystemIMEs = getDisabledSystemIMEs(resolver); + + boolean needsToResetSelectedSubtype = false; + for (final InputMethodInfo imi : inputMethodInfos) { + final String imiId = imi.getId(); + final Preference pref = context.findPreference(imiId); + if (pref == null) { + continue; + } + // In the choose input method screen or in the subtype enabler screen, + // <code>pref</code> is an instance of TwoStatePreference. + final boolean isImeChecked = (pref instanceof TwoStatePreference) ? + ((TwoStatePreference) pref).isChecked() + : enabledIMEsAndSubtypesMap.containsKey(imiId); + final boolean isCurrentInputMethod = imiId.equals(currentInputMethodId); + final boolean systemIme = imi.isSystem(); + if ((!hasHardKeyboard && InputMethodSettingValuesWrapper.getInstance( + context.getActivity()).isAlwaysCheckedIme(imi)) + || isImeChecked) { + if (!enabledIMEsAndSubtypesMap.containsKey(imiId)) { + // imiId has just been enabled + enabledIMEsAndSubtypesMap.put(imiId, new HashSet<>()); + } + final HashSet<String> subtypesSet = enabledIMEsAndSubtypesMap.get(imiId); + + boolean subtypePrefFound = false; + final int subtypeCount = imi.getSubtypeCount(); + for (int i = 0; i < subtypeCount; ++i) { + final InputMethodSubtype subtype = imi.getSubtypeAt(i); + final String subtypeHashCodeStr = String.valueOf(subtype.hashCode()); + final TwoStatePreference subtypePref = (TwoStatePreference) context + .findPreference(imiId + subtypeHashCodeStr); + // In the Configure input method screen which does not have subtype preferences. + if (subtypePref == null) { + continue; + } + if (!subtypePrefFound) { + // Once subtype preference is found, subtypeSet needs to be cleared. + // Because of system change, hashCode value could have been changed. + subtypesSet.clear(); + // If selected subtype preference is disabled, needs to reset. + needsToResetSelectedSubtype = true; + subtypePrefFound = true; + } + // Checking <code>subtypePref.isEnabled()</code> is insufficient to determine + // whether the user manually enabled this subtype or not. Implicitly-enabled + // subtypes are also checked just as an indicator to users. We also need to + // check <code>subtypePref.isEnabled()</code> so that only manually enabled + // subtypes can be saved here. + if (subtypePref.isEnabled() && subtypePref.isChecked()) { + subtypesSet.add(subtypeHashCodeStr); + if (isCurrentInputMethod) { + if (selectedInputMethodSubtype == subtype.hashCode()) { + // Selected subtype is still enabled, there is no need to reset + // selected subtype. + needsToResetSelectedSubtype = false; + } + } + } else { + subtypesSet.remove(subtypeHashCodeStr); + } + } + } else { + enabledIMEsAndSubtypesMap.remove(imiId); + if (isCurrentInputMethod) { + // We are processing the current input method, but found that it's not enabled. + // This means that the current input method has been uninstalled. + // If currentInputMethod is already uninstalled, InputMethodManagerService will + // find the applicable IME from the history and the system locale. + if (DEBUG) { + Log.d(TAG, "Current IME was uninstalled or disabled."); + } + currentInputMethodId = null; + } + } + // If it's a disabled system ime, add it to the disabled list so that it + // doesn't get enabled automatically on any changes to the package list + if (systemIme && hasHardKeyboard) { + if (disabledSystemIMEs.contains(imiId)) { + if (isImeChecked) { + disabledSystemIMEs.remove(imiId); + } + } else { + if (!isImeChecked) { + disabledSystemIMEs.add(imiId); + } + } + } + } + + final String enabledIMEsAndSubtypesString = buildInputMethodsAndSubtypesString( + enabledIMEsAndSubtypesMap); + final String disabledSystemIMEsString = buildInputMethodsString(disabledSystemIMEs); + if (DEBUG) { + Log.d(TAG, "--- Save enabled inputmethod settings. :" + enabledIMEsAndSubtypesString); + Log.d(TAG, "--- Save disabled system inputmethod settings. :" + + disabledSystemIMEsString); + Log.d(TAG, "--- Save default inputmethod settings. :" + currentInputMethodId); + Log.d(TAG, "--- Needs to reset the selected subtype :" + needsToResetSelectedSubtype); + Log.d(TAG, "--- Subtype is selected :" + isInputMethodSubtypeSelected(resolver)); + } + + // Redefines SelectedSubtype when all subtypes are unchecked or there is no subtype + // selected. And if the selected subtype of the current input method was disabled, + // We should reset the selected input method's subtype. + if (needsToResetSelectedSubtype || !isInputMethodSubtypeSelected(resolver)) { + if (DEBUG) { + Log.d(TAG, "--- Reset inputmethod subtype because it's not defined."); + } + putSelectedInputMethodSubtype(resolver, NOT_A_SUBTYPE_ID); + } + + Settings.Secure.putString(resolver, + Settings.Secure.ENABLED_INPUT_METHODS, enabledIMEsAndSubtypesString); + if (disabledSystemIMEsString.length() > 0) { + Settings.Secure.putString(resolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS, + disabledSystemIMEsString); + } + // If the current input method is unset, InputMethodManagerService will find the applicable + // IME from the history and the system locale. + Settings.Secure.putString(resolver, Settings.Secure.DEFAULT_INPUT_METHOD, + currentInputMethodId != null ? currentInputMethodId : ""); + } + + public static void loadInputMethodSubtypeList(final PreferenceFragmentCompat context, + final ContentResolver resolver, final List<InputMethodInfo> inputMethodInfos, + final Map<String, List<Preference>> inputMethodPrefsMap) { + final HashMap<String, HashSet<String>> enabledSubtypes = + getEnabledInputMethodsAndSubtypeList(resolver); + + for (final InputMethodInfo imi : inputMethodInfos) { + final String imiId = imi.getId(); + final Preference pref = context.findPreference(imiId); + if (pref instanceof TwoStatePreference) { + final TwoStatePreference subtypePref = (TwoStatePreference) pref; + final boolean isEnabled = enabledSubtypes.containsKey(imiId); + subtypePref.setChecked(isEnabled); + if (inputMethodPrefsMap != null) { + for (final Preference childPref: inputMethodPrefsMap.get(imiId)) { + childPref.setEnabled(isEnabled); + } + } + setSubtypesPreferenceEnabled(context, inputMethodInfos, imiId, isEnabled); + } + } + updateSubtypesPreferenceChecked(context, inputMethodInfos, enabledSubtypes); + } + + private static void setSubtypesPreferenceEnabled(final PreferenceFragmentCompat context, + final List<InputMethodInfo> inputMethodProperties, final String id, + final boolean enabled) { + final PreferenceScreen preferenceScreen = context.getPreferenceScreen(); + for (final InputMethodInfo imi : inputMethodProperties) { + if (id.equals(imi.getId())) { + final int subtypeCount = imi.getSubtypeCount(); + for (int i = 0; i < subtypeCount; ++i) { + final InputMethodSubtype subtype = imi.getSubtypeAt(i); + final TwoStatePreference pref = (TwoStatePreference) preferenceScreen + .findPreference(id + subtype.hashCode()); + if (pref != null) { + pref.setEnabled(enabled); + } + } + } + } + } + + private static void updateSubtypesPreferenceChecked(final PreferenceFragmentCompat context, + final List<InputMethodInfo> inputMethodProperties, + final HashMap<String, HashSet<String>> enabledSubtypes) { + final PreferenceScreen preferenceScreen = context.getPreferenceScreen(); + for (final InputMethodInfo imi : inputMethodProperties) { + final String id = imi.getId(); + if (!enabledSubtypes.containsKey(id)) { + // There is no need to enable/disable subtypes of disabled IMEs. + continue; + } + final HashSet<String> enabledSubtypesSet = enabledSubtypes.get(id); + final int subtypeCount = imi.getSubtypeCount(); + for (int i = 0; i < subtypeCount; ++i) { + final InputMethodSubtype subtype = imi.getSubtypeAt(i); + final String hashCode = String.valueOf(subtype.hashCode()); + if (DEBUG) { + Log.d(TAG, "--- Set checked state: " + "id" + ", " + hashCode + ", " + + enabledSubtypesSet.contains(hashCode)); + } + final TwoStatePreference pref = (TwoStatePreference) preferenceScreen + .findPreference(id + hashCode); + if (pref != null) { + pref.setChecked(enabledSubtypesSet.contains(hashCode)); + } + } + } + } + + public static void removeUnnecessaryNonPersistentPreference(final Preference pref) { + final String key = pref.getKey(); + if (pref.isPersistent() || key == null) { + return; + } + final SharedPreferences prefs = pref.getSharedPreferences(); + if (prefs != null && prefs.contains(key)) { + prefs.edit().remove(key).apply(); + } + } + + @NonNull + public static String getSubtypeLocaleNameAsSentence(@Nullable InputMethodSubtype subtype, + @NonNull final Context context, @NonNull final InputMethodInfo inputMethodInfo) { + if (subtype == null) { + return ""; + } + final Locale locale = getDisplayLocale(context); + final CharSequence subtypeName = subtype.getDisplayName(context, + inputMethodInfo.getPackageName(), inputMethodInfo.getServiceInfo() + .applicationInfo); + return LocaleHelper.toSentenceCase(subtypeName.toString(), locale); + } + + @NonNull + public static String getSubtypeLocaleNameListAsSentence( + @NonNull final List<InputMethodSubtype> subtypes, @NonNull final Context context, + @NonNull final InputMethodInfo inputMethodInfo) { + if (subtypes.isEmpty()) { + return ""; + } + final Locale locale = getDisplayLocale(context); + final int subtypeCount = subtypes.size(); + final CharSequence[] subtypeNames = new CharSequence[subtypeCount]; + for (int i = 0; i < subtypeCount; i++) { + subtypeNames[i] = subtypes.get(i).getDisplayName(context, + inputMethodInfo.getPackageName(), inputMethodInfo.getServiceInfo() + .applicationInfo); + } + return LocaleHelper.toSentenceCase( + ListFormatter.getInstance(locale).format((Object[]) subtypeNames), locale); + } + + @NonNull + private static Locale getDisplayLocale(@Nullable final Context context) { + if (context == null) { + return Locale.getDefault(); + } + if (context.getResources() == null) { + return Locale.getDefault(); + } + final Configuration configuration = context.getResources().getConfiguration(); + if (configuration == null) { + return Locale.getDefault(); + } + final Locale configurationLocale = configuration.getLocales().get(0); + if (configurationLocale == null) { + return Locale.getDefault(); + } + return configurationLocale; + } + + public static boolean isValidSystemNonAuxAsciiCapableIme(InputMethodInfo imi) { + if (imi.isAuxiliaryIme() || !imi.isSystem()) { + return false; + } + final int subtypeCount = imi.getSubtypeCount(); + for (int i = 0; i < subtypeCount; ++i) { + final InputMethodSubtype subtype = imi.getSubtypeAt(i); + if (SUBTYPE_MODE_KEYBOARD.equalsIgnoreCase(subtype.getMode()) + && subtype.isAsciiCapable()) { + return true; + } + } + return false; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlLoaderCompat.java b/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlLoaderCompat.java new file mode 100644 index 000000000000..d7c14ad66baa --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlLoaderCompat.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2018 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.settingslib.license; + +import android.content.Context; +import android.util.Log; + +import com.android.settingslib.utils.AsyncLoaderCompat; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import androidx.annotation.VisibleForTesting; + +/** + * LicenseHtmlLoader is a loader which loads a license html file from default license xml files. + */ +public class LicenseHtmlLoaderCompat extends AsyncLoaderCompat<File> { + private static final String TAG = "LicenseHtmlLoaderCompat"; + + private static final String[] DEFAULT_LICENSE_XML_PATHS = { + "/system/etc/NOTICE.xml.gz", + "/vendor/etc/NOTICE.xml.gz", + "/odm/etc/NOTICE.xml.gz", + "/oem/etc/NOTICE.xml.gz"}; + private static final String NOTICE_HTML_FILE_NAME = "NOTICE.html"; + + private Context mContext; + + public LicenseHtmlLoaderCompat(Context context) { + super(context); + mContext = context; + } + + @Override + public File loadInBackground() { + return generateHtmlFromDefaultXmlFiles(); + } + + @Override + protected void onDiscardResult(File f) { + } + + private File generateHtmlFromDefaultXmlFiles() { + final List<File> xmlFiles = getVaildXmlFiles(); + if (xmlFiles.isEmpty()) { + Log.e(TAG, "No notice file exists."); + return null; + } + + File cachedHtmlFile = getCachedHtmlFile(); + if (!isCachedHtmlFileOutdated(xmlFiles, cachedHtmlFile) + || generateHtmlFile(xmlFiles, cachedHtmlFile)) { + return cachedHtmlFile; + } + + return null; + } + + @VisibleForTesting + List<File> getVaildXmlFiles() { + final List<File> xmlFiles = new ArrayList(); + for (final String xmlPath : DEFAULT_LICENSE_XML_PATHS) { + File file = new File(xmlPath); + if (file.exists() && file.length() != 0) { + xmlFiles.add(file); + } + } + return xmlFiles; + } + + @VisibleForTesting + File getCachedHtmlFile() { + return new File(mContext.getCacheDir(), NOTICE_HTML_FILE_NAME); + } + + @VisibleForTesting + boolean isCachedHtmlFileOutdated(List<File> xmlFiles, File cachedHtmlFile) { + boolean outdated = true; + if (cachedHtmlFile.exists() && cachedHtmlFile.length() != 0) { + outdated = false; + for (File file : xmlFiles) { + if (cachedHtmlFile.lastModified() < file.lastModified()) { + outdated = true; + break; + } + } + } + return outdated; + } + + @VisibleForTesting + boolean generateHtmlFile(List<File> xmlFiles, File htmlFile) { + return LicenseHtmlGeneratorFromXml.generateHtml(xmlFiles, htmlFile); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/net/ChartDataLoaderCompat.java b/packages/SettingsLib/src/com/android/settingslib/net/ChartDataLoaderCompat.java new file mode 100644 index 000000000000..3adbd4d01ca0 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/net/ChartDataLoaderCompat.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2018 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.settingslib.net; + +import static android.net.NetworkStats.SET_DEFAULT; +import static android.net.NetworkStats.SET_FOREGROUND; +import static android.net.NetworkStats.TAG_NONE; +import static android.net.NetworkStatsHistory.FIELD_RX_BYTES; +import static android.net.NetworkStatsHistory.FIELD_TX_BYTES; +import static android.text.format.DateUtils.HOUR_IN_MILLIS; + +import android.content.Context; +import android.net.INetworkStatsSession; +import android.net.NetworkStatsHistory; +import android.net.NetworkTemplate; +import android.os.Bundle; +import android.os.RemoteException; + +import com.android.settingslib.AppItem; + +import androidx.loader.content.AsyncTaskLoader; + +/** + * Loader for historical chart data for both network and UID details. + */ +public class ChartDataLoaderCompat extends AsyncTaskLoader<ChartData> { + private static final String KEY_TEMPLATE = "template"; + private static final String KEY_APP = "app"; + private static final String KEY_FIELDS = "fields"; + + private final INetworkStatsSession mSession; + private final Bundle mArgs; + + public static Bundle buildArgs(NetworkTemplate template, AppItem app) { + return buildArgs(template, app, FIELD_RX_BYTES | FIELD_TX_BYTES); + } + + public static Bundle buildArgs(NetworkTemplate template, AppItem app, int fields) { + final Bundle args = new Bundle(); + args.putParcelable(KEY_TEMPLATE, template); + args.putParcelable(KEY_APP, app); + args.putInt(KEY_FIELDS, fields); + return args; + } + + public ChartDataLoaderCompat(Context context, INetworkStatsSession session, Bundle args) { + super(context); + mSession = session; + mArgs = args; + } + + @Override + protected void onStartLoading() { + super.onStartLoading(); + forceLoad(); + } + + @Override + public ChartData loadInBackground() { + final NetworkTemplate template = mArgs.getParcelable(KEY_TEMPLATE); + final AppItem app = mArgs.getParcelable(KEY_APP); + final int fields = mArgs.getInt(KEY_FIELDS); + + try { + return loadInBackground(template, app, fields); + } catch (RemoteException e) { + // since we can't do much without history, and we don't want to + // leave with half-baked UI, we bail hard. + throw new RuntimeException("problem reading network stats", e); + } + } + + private ChartData loadInBackground(NetworkTemplate template, AppItem app, int fields) + throws RemoteException { + final ChartData data = new ChartData(); + data.network = mSession.getHistoryForNetwork(template, fields); + + if (app != null) { + // load stats for current uid and template + final int size = app.uids.size(); + for (int i = 0; i < size; i++) { + final int uid = app.uids.keyAt(i); + data.detailDefault = collectHistoryForUid( + template, uid, SET_DEFAULT, data.detailDefault); + data.detailForeground = collectHistoryForUid( + template, uid, SET_FOREGROUND, data.detailForeground); + } + + if (size > 0) { + data.detail = new NetworkStatsHistory(data.detailForeground.getBucketDuration()); + data.detail.recordEntireHistory(data.detailDefault); + data.detail.recordEntireHistory(data.detailForeground); + } else { + data.detailDefault = new NetworkStatsHistory(HOUR_IN_MILLIS); + data.detailForeground = new NetworkStatsHistory(HOUR_IN_MILLIS); + data.detail = new NetworkStatsHistory(HOUR_IN_MILLIS); + } + } + + return data; + } + + @Override + protected void onStopLoading() { + super.onStopLoading(); + cancelLoad(); + } + + @Override + protected void onReset() { + super.onReset(); + cancelLoad(); + } + + /** + * Collect {@link NetworkStatsHistory} for the requested UID, combining with + * an existing {@link NetworkStatsHistory} if provided. + */ + private NetworkStatsHistory collectHistoryForUid( + NetworkTemplate template, int uid, int set, NetworkStatsHistory existing) + throws RemoteException { + final NetworkStatsHistory history = mSession.getHistoryForUid( + template, uid, set, TAG_NONE, FIELD_RX_BYTES | FIELD_TX_BYTES); + + if (existing != null) { + existing.recordEntireHistory(history); + return existing; + } else { + return history; + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/net/SummaryForAllUidLoaderCompat.java b/packages/SettingsLib/src/com/android/settingslib/net/SummaryForAllUidLoaderCompat.java new file mode 100644 index 000000000000..c311337d6cf5 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/net/SummaryForAllUidLoaderCompat.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2018 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.settingslib.net; + +import android.content.Context; +import android.net.INetworkStatsSession; +import android.net.NetworkStats; +import android.net.NetworkTemplate; +import android.os.Bundle; +import android.os.RemoteException; + +import androidx.loader.content.AsyncTaskLoader; + +public class SummaryForAllUidLoaderCompat extends AsyncTaskLoader<NetworkStats> { + private static final String KEY_TEMPLATE = "template"; + private static final String KEY_START = "start"; + private static final String KEY_END = "end"; + + private final INetworkStatsSession mSession; + private final Bundle mArgs; + + public static Bundle buildArgs(NetworkTemplate template, long start, long end) { + final Bundle args = new Bundle(); + args.putParcelable(KEY_TEMPLATE, template); + args.putLong(KEY_START, start); + args.putLong(KEY_END, end); + return args; + } + + public SummaryForAllUidLoaderCompat(Context context, INetworkStatsSession session, + Bundle args) { + super(context); + mSession = session; + mArgs = args; + } + + @Override + protected void onStartLoading() { + super.onStartLoading(); + forceLoad(); + } + + @Override + public NetworkStats loadInBackground() { + final NetworkTemplate template = mArgs.getParcelable(KEY_TEMPLATE); + final long start = mArgs.getLong(KEY_START); + final long end = mArgs.getLong(KEY_END); + + try { + return mSession.getSummaryForAllUid(template, start, end, false); + } catch (RemoteException e) { + return null; + } + } + + @Override + protected void onStopLoading() { + super.onStopLoading(); + cancelLoad(); + } + + @Override + protected void onReset() { + super.onReset(); + cancelLoad(); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionControllerMixinCompat.java b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionControllerMixinCompat.java new file mode 100644 index 000000000000..179121739896 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionControllerMixinCompat.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2018 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.settingslib.suggestions; + + +import android.content.ComponentName; +import android.content.Context; +import android.os.Bundle; +import android.service.settings.suggestions.Suggestion; +import android.util.Log; + +import com.android.settingslib.core.lifecycle.Lifecycle; + +import java.util.List; + +import androidx.annotation.Nullable; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; + +/** + * Manages IPC communication to SettingsIntelligence for suggestion related services. + */ +public class SuggestionControllerMixinCompat implements + SuggestionController.ServiceConnectionListener, androidx.lifecycle.LifecycleObserver, + LoaderManager.LoaderCallbacks<List<Suggestion>> { + + public interface SuggestionControllerHost { + /** + * Called when suggestion data fetching is ready. + */ + void onSuggestionReady(List<Suggestion> data); + + /** + * Returns {@link LoaderManager} associated with the host. If host is not attached to + * activity then return null. + */ + @Nullable + LoaderManager getLoaderManager(); + } + + private static final String TAG = "SuggestionCtrlMixin"; + private static final boolean DEBUG = false; + + private final Context mContext; + private final SuggestionController mSuggestionController; + private final SuggestionControllerHost mHost; + + private boolean mSuggestionLoaded; + + public SuggestionControllerMixinCompat(Context context, SuggestionControllerHost host, + Lifecycle lifecycle, ComponentName componentName) { + mContext = context.getApplicationContext(); + mHost = host; + mSuggestionController = new SuggestionController(mContext, componentName, + this /* serviceConnectionListener */); + if (lifecycle != null) { + lifecycle.addObserver(this); + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_START) + public void onStart() { + if (DEBUG) { + Log.d(TAG, "SuggestionController started"); + } + mSuggestionController.start(); + } + + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + public void onStop() { + if (DEBUG) { + Log.d(TAG, "SuggestionController stopped."); + } + mSuggestionController.stop(); + } + + @Override + public void onServiceConnected() { + final LoaderManager loaderManager = mHost.getLoaderManager(); + if (loaderManager != null) { + loaderManager.restartLoader(SuggestionLoader.LOADER_ID_SUGGESTIONS, + null /* args */, this /* callback */); + } + } + + @Override + public void onServiceDisconnected() { + if (DEBUG) { + Log.d(TAG, "SuggestionService disconnected"); + } + final LoaderManager loaderManager = mHost.getLoaderManager(); + if (loaderManager != null) { + loaderManager.destroyLoader(SuggestionLoader.LOADER_ID_SUGGESTIONS); + } + } + + @Override + public Loader<List<Suggestion>> onCreateLoader(int id, Bundle args) { + if (id == SuggestionLoader.LOADER_ID_SUGGESTIONS) { + mSuggestionLoaded = false; + return new SuggestionLoaderCompat(mContext, mSuggestionController); + } + throw new IllegalArgumentException("This loader id is not supported " + id); + } + + @Override + public void onLoadFinished(Loader<List<Suggestion>> loader, List<Suggestion> data) { + mSuggestionLoaded = true; + mHost.onSuggestionReady(data); + } + + @Override + public void onLoaderReset(Loader<List<Suggestion>> loader) { + mSuggestionLoaded = false; + } + + public boolean isSuggestionLoaded() { + return mSuggestionLoaded; + } + + public void dismissSuggestion(Suggestion suggestion) { + mSuggestionController.dismissSuggestions(suggestion); + } + + public void launchSuggestion(Suggestion suggestion) { + mSuggestionController.launchSuggestion(suggestion); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionLoaderCompat.java b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionLoaderCompat.java new file mode 100644 index 000000000000..066de19172de --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionLoaderCompat.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2017 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.settingslib.suggestions; + +import android.content.Context; +import android.service.settings.suggestions.Suggestion; +import android.util.Log; + +import com.android.settingslib.utils.AsyncLoaderCompat; + +import java.util.List; + +public class SuggestionLoaderCompat extends AsyncLoaderCompat<List<Suggestion>> { + + public static final int LOADER_ID_SUGGESTIONS = 42; + private static final String TAG = "SuggestionLoader"; + + private final SuggestionController mSuggestionController; + + public SuggestionLoaderCompat(Context context, SuggestionController controller) { + super(context); + mSuggestionController = controller; + } + + @Override + protected void onDiscardResult(List<Suggestion> result) { + + } + + @Override + public List<Suggestion> loadInBackground() { + final List<Suggestion> data = mSuggestionController.getSuggestions(); + if (data == null) { + Log.d(TAG, "data is null"); + } else { + Log.d(TAG, "data size " + data.size()); + } + return data; + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/AsyncLoaderCompat.java b/packages/SettingsLib/src/com/android/settingslib/utils/AsyncLoaderCompat.java new file mode 100644 index 000000000000..916d7e312631 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/utils/AsyncLoaderCompat.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2018 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.settingslib.utils; + +import android.content.Context; + +import androidx.loader.content.AsyncTaskLoader; + +/** + * This class fills in some boilerplate for AsyncTaskLoader to actually load things. + * + * Subclasses need to implement {@link AsyncLoaderCompat#loadInBackground()} to perform the actual + * background task, and {@link AsyncLoaderCompat#onDiscardResult(T)} to clean up previously loaded + * results. + * + * This loader is based on the MailAsyncTaskLoader from the AOSP EmailUnified repo. + * + * @param <T> the data type to be loaded. + */ +public abstract class AsyncLoaderCompat<T> extends AsyncTaskLoader<T> { + private T mResult; + + public AsyncLoaderCompat(final Context context) { + super(context); + } + + @Override + protected void onStartLoading() { + if (mResult != null) { + deliverResult(mResult); + } + + if (takeContentChanged() || mResult == null) { + forceLoad(); + } + } + + @Override + protected void onStopLoading() { + cancelLoad(); + } + + @Override + public void deliverResult(final T data) { + if (isReset()) { + if (data != null) { + onDiscardResult(data); + } + return; + } + + final T oldResult = mResult; + mResult = data; + + if (isStarted()) { + super.deliverResult(data); + } + + if (oldResult != null && oldResult != mResult) { + onDiscardResult(oldResult); + } + } + + @Override + protected void onReset() { + super.onReset(); + + onStopLoading(); + + if (mResult != null) { + onDiscardResult(mResult); + } + mResult = null; + } + + @Override + public void onCanceled(final T data) { + super.onCanceled(data); + + if (data != null) { + onDiscardResult(data); + } + } + + /** + * Called when discarding the load results so subclasses can take care of clean-up or + * recycling tasks. This is not called if the same result (by way of pointer equality) is + * returned again by a subsequent call to loadInBackground, or if result is null. + * + * Note that this may be called concurrently with loadInBackground(), and in some circumstances + * may be called more than once for a given object. + * + * @param result The value returned from {@link AsyncLoaderCompat#loadInBackground()} which + * is to be discarded. + */ + protected abstract void onDiscardResult(T result); +} diff --git a/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreferenceMixinCompat.java b/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreferenceMixinCompat.java new file mode 100644 index 000000000000..260ac838cb32 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreferenceMixinCompat.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2018 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.settingslib.widget; + +import android.content.Context; + +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.SetPreferenceScreen; + +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; + +public class FooterPreferenceMixinCompat implements LifecycleObserver, SetPreferenceScreen { + + private final PreferenceFragmentCompat mFragment; + private FooterPreference mFooterPreference; + + public FooterPreferenceMixinCompat(PreferenceFragmentCompat fragment, Lifecycle lifecycle) { + mFragment = fragment; + lifecycle.addObserver(this); + } + + @Override + public void setPreferenceScreen(PreferenceScreen preferenceScreen) { + if (mFooterPreference != null) { + preferenceScreen.addPreference(mFooterPreference); + } + } + + /** + * Creates a new {@link FooterPreference}. + */ + public FooterPreference createFooterPreference() { + final PreferenceScreen screen = mFragment.getPreferenceScreen(); + if (mFooterPreference != null && screen != null) { + screen.removePreference(mFooterPreference); + } + mFooterPreference = new FooterPreference(getPrefContext()); + + if (screen != null) { + screen.addPreference(mFooterPreference); + } + return mFooterPreference; + } + + /** + * Returns an UI context with theme properly set for new Preference objects. + */ + private Context getPrefContext() { + return mFragment.getPreferenceManager().getContext(); + } + + public boolean hasFooter() { + return mFooterPreference != null; + } +} + diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/CustomEditTextPreferenceComaptTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/CustomEditTextPreferenceComaptTest.java new file mode 100644 index 000000000000..9ba996752f49 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/CustomEditTextPreferenceComaptTest.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2018 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.settingslib; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.view.View; +import android.widget.EditText; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.util.ReflectionHelpers; + +@RunWith(SettingsLibRobolectricTestRunner.class) +public class CustomEditTextPreferenceComaptTest { + + @Mock + private View mView; + + private TestPreference mPreference; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mPreference = new TestPreference(RuntimeEnvironment.application); + } + + @Test + public void bindDialogView_shouldRequestFocus() { + final String testText = ""; + final EditText editText = spy(new EditText(RuntimeEnvironment.application)); + editText.setText(testText); + when(mView.findViewById(android.R.id.edit)).thenReturn(editText); + + mPreference.onBindDialogView(mView); + + verify(editText).requestFocus(); + } + + @Test + public void getEditText_noDialog_shouldNotCrash() { + ReflectionHelpers.setField(mPreference, "mFragment", + mock(CustomEditTextPreferenceCompat.CustomPreferenceDialogFragment.class)); + + mPreference.getEditText(); + + // no crash + } + + private static class TestPreference extends CustomEditTextPreferenceCompat { + public TestPreference(Context context) { + super(context); + } + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtilCompatTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtilCompatTest.java new file mode 100644 index 000000000000..ddadac105c18 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtilCompatTest.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2018 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.settingslib.inputmethod; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.pm.ApplicationInfo; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodSubtype; +import android.view.inputmethod.InputMethodSubtype.InputMethodSubtypeBuilder; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; + +@RunWith(RobolectricTestRunner.class) +public class InputMethodAndSubtypeUtilCompatTest { + + private static final HashSet<String> EMPTY_STRING_SET = new HashSet<>(); + + private static HashSet<String> asHashSet(String... strings) { + HashSet<String> hashSet = new HashSet<>(); + for (String s : strings) { + hashSet.add(s); + } + return hashSet; + } + + @Test + public void parseInputMethodsAndSubtypesString_EmptyString() { + assertThat(InputMethodAndSubtypeUtilCompat. + parseInputMethodsAndSubtypesString("")).isEmpty(); + assertThat(InputMethodAndSubtypeUtilCompat. + parseInputMethodsAndSubtypesString(null)).isEmpty(); + } + + @Test + public void parseInputMethodsAndSubtypesString_SingleImeNoSubtype() { + HashMap<String, HashSet<String>> r = + InputMethodAndSubtypeUtilCompat.parseInputMethodsAndSubtypesString("ime0"); + assertThat(r).containsExactly("ime0", EMPTY_STRING_SET); + } + + @Test + public void parseInputMethodsAndSubtypesString_MultipleImesNoSubtype() { + HashMap<String, HashSet<String>> r = + InputMethodAndSubtypeUtilCompat.parseInputMethodsAndSubtypesString("ime0:ime1"); + assertThat(r).containsExactly("ime0", EMPTY_STRING_SET, "ime1", EMPTY_STRING_SET); + } + + @Test + public void parseInputMethodsAndSubtypesString_SingleImeSingleSubtype() { + HashMap<String, HashSet<String>> r = + InputMethodAndSubtypeUtilCompat.parseInputMethodsAndSubtypesString("ime0;subtype0"); + assertThat(r).containsExactly("ime0", asHashSet("subtype0")); + } + + @Test + public void parseInputMethodsAndSubtypesString_SingleImeDuplicateSameSubtypes() { + HashMap<String, HashSet<String>> r = + InputMethodAndSubtypeUtilCompat.parseInputMethodsAndSubtypesString( + "ime0;subtype0;subtype0"); + assertThat(r).containsExactly("ime0", asHashSet("subtype0")); + } + + @Test + public void parseInputMethodsAndSubtypesString_SingleImeMultipleSubtypes() { + HashMap<String, HashSet<String>> r = + InputMethodAndSubtypeUtilCompat.parseInputMethodsAndSubtypesString( + "ime0;subtype0;subtype1"); + assertThat(r).containsExactly("ime0", asHashSet("subtype0", "subtype1")); + } + + @Test + public void parseInputMethodsAndSubtypesString_MultiplePairsOfImeSubtype() { + assertThat(InputMethodAndSubtypeUtilCompat.parseInputMethodsAndSubtypesString( + "ime0;subtype0:ime1;subtype1")) + .containsExactly("ime0", asHashSet("subtype0"), "ime1", asHashSet("subtype1")); + assertThat(InputMethodAndSubtypeUtilCompat.parseInputMethodsAndSubtypesString( + "ime0;subtype0;subtype1:ime1;subtype2")) + .containsExactly("ime0", asHashSet("subtype0", "subtype1"), + "ime1", asHashSet("subtype2")); + assertThat(InputMethodAndSubtypeUtilCompat.parseInputMethodsAndSubtypesString( + "ime0;subtype0;subtype1:ime1;subtype1;subtype2")) + .containsExactly("ime0", asHashSet("subtype0", "subtype1"), + "ime1", asHashSet("subtype1", "subtype2")); + + } + + @Test + public void parseInputMethodsAndSubtypesString_MixedImeSubtypePairsAndImeNoSubtype() { + HashMap<String, HashSet<String>> r = + InputMethodAndSubtypeUtilCompat.parseInputMethodsAndSubtypesString( + "ime0;subtype0;subtype1:ime1;subtype1;subtype2:ime2"); + assertThat(r).containsExactly("ime0", asHashSet("subtype0", "subtype1"), + "ime1", asHashSet("subtype1", "subtype2"), + "ime2", EMPTY_STRING_SET); + } + + @Test + public void buildInputMethodsAndSubtypesString_EmptyInput() { + HashMap<String, HashSet<String>> map = new HashMap<>(); + assertThat(map).isEmpty(); + } + + @Test + public void buildInputMethodsAndSubtypesString_SingleIme() { + HashMap<String, HashSet<String>> map = new HashMap<>(); + map.put("ime0", new HashSet<>()); + String result = InputMethodAndSubtypeUtilCompat.buildInputMethodsAndSubtypesString(map); + assertThat(result).isEqualTo("ime0"); + } + + @Test + public void buildInputMethodsAndSubtypesString_SingleImeSingleSubtype() { + HashMap<String, HashSet<String>> map = new HashMap<>(); + map.put("ime0", asHashSet("subtype0")); + String result = InputMethodAndSubtypeUtilCompat.buildInputMethodsAndSubtypesString(map); + assertThat(result).isEqualTo("ime0;subtype0"); + } + + @Test + public void buildInputMethodsAndSubtypesString_SingleImeMultipleSubtypes() { + HashMap<String, HashSet<String>> map = new HashMap<>(); + map.put("ime0", asHashSet("subtype0", "subtype1")); + String result = InputMethodAndSubtypeUtilCompat.buildInputMethodsAndSubtypesString(map); + + // We do not expect what order will be used to concatenate items in + // InputMethodAndSubtypeUtil.buildInputMethodsAndSubtypesString() hence accept all possible + // permutations here. + assertThat(result).matches("ime0;subtype0;subtype1|ime0;subtype1;subtype0"); + } + + @Test + public void buildInputMethodsAndSubtypesString_MultipleImesNoSubtypes() { + HashMap<String, HashSet<String>> map = new HashMap<>(); + map.put("ime0", EMPTY_STRING_SET); + map.put("ime1", EMPTY_STRING_SET); + String result = InputMethodAndSubtypeUtilCompat.buildInputMethodsAndSubtypesString(map); + + // We do not expect what order will be used to concatenate items in + // InputMethodAndSubtypeUtil.buildInputMethodsAndSubtypesString() hence accept all possible + // permutations here. + assertThat(result).matches("ime0:ime1|ime1:ime0"); + } + + @Test + public void buildInputMethodsAndSubtypesString_MultipleImesWithAndWithoutSubtypes() { + HashMap<String, HashSet<String>> map = new HashMap<>(); + map.put("ime0", asHashSet("subtype0", "subtype1")); + map.put("ime1", EMPTY_STRING_SET); + String result = InputMethodAndSubtypeUtilCompat.buildInputMethodsAndSubtypesString(map); + + // We do not expect what order will be used to concatenate items in + // InputMethodAndSubtypeUtil.buildInputMethodsAndSubtypesString() hence accept all possible + // permutations here. + assertThat(result).matches("ime0;subtype0;subtype1:ime1|ime0;subtype1;subtype0:ime1" + + "|ime1:ime0;subtype0;subtype1|ime1:ime0;subtype1;subtype0"); + } + + @Test + public void buildInputMethodsAndSubtypesString_MultipleImesWithSubtypes() { + HashMap<String, HashSet<String>> map = new HashMap<>(); + map.put("ime0", asHashSet("subtype0", "subtype1")); + map.put("ime1", asHashSet("subtype2", "subtype3")); + String result = InputMethodAndSubtypeUtilCompat.buildInputMethodsAndSubtypesString(map); + + // We do not expect what order will be used to concatenate items in + // InputMethodAndSubtypeUtil.buildInputMethodsAndSubtypesString() hence accept all possible + // permutations here. + assertThat(result).matches("ime0;subtype0;subtype1:ime1;subtype2;subtype3" + + "|ime0;subtype1;subtype0:ime1;subtype2;subtype3" + + "|ime0;subtype0;subtype1:ime1;subtype3;subtype2" + + "|ime0;subtype1;subtype0:ime1;subtype3;subtype2" + + "|ime1;subtype2;subtype3:ime0;subtype0;subtype1" + + "|ime2;subtype3;subtype2:ime0;subtype0;subtype1" + + "|ime3;subtype2;subtype3:ime0;subtype1;subtype0" + + "|ime4;subtype3;subtype2:ime0;subtype1;subtype0"); + } + + @Test + public void isValidSystemNonAuxAsciiCapableIme() { + // System IME w/ no subtype + assertThat(InputMethodAndSubtypeUtilCompat.isValidSystemNonAuxAsciiCapableIme( + createDummyIme(true, false))) + .isFalse(); + + // System IME w/ non-Aux and non-ASCII-capable "keyboard" subtype + assertThat(InputMethodAndSubtypeUtilCompat.isValidSystemNonAuxAsciiCapableIme( + createDummyIme(true, false, createDummySubtype("keyboard", false, false)))) + .isFalse(); + + // System IME w/ non-Aux and ASCII-capable "keyboard" subtype + assertThat(InputMethodAndSubtypeUtilCompat.isValidSystemNonAuxAsciiCapableIme( + createDummyIme(true, false, createDummySubtype("keyboard", false, true)))) + .isTrue(); + + // System IME w/ Aux and ASCII-capable "keyboard" subtype + assertThat(InputMethodAndSubtypeUtilCompat.isValidSystemNonAuxAsciiCapableIme( + createDummyIme(true, true, createDummySubtype("keyboard", true, true)))) + .isFalse(); + + // System IME w/ non-Aux and ASCII-capable "voice" subtype + assertThat(InputMethodAndSubtypeUtilCompat.isValidSystemNonAuxAsciiCapableIme( + createDummyIme(true, false, createDummySubtype("voice", false, true)))) + .isFalse(); + + // System IME w/ non-Aux and non-ASCII-capable subtype + Non-Aux and ASCII-capable subtype + assertThat(InputMethodAndSubtypeUtilCompat.isValidSystemNonAuxAsciiCapableIme( + createDummyIme(true, false, + createDummySubtype("keyboard", false, true), + createDummySubtype("keyboard", false, false)))) + .isTrue(); + + // Non-system IME w/ non-Aux and ASCII-capable "keyboard" subtype + assertThat(InputMethodAndSubtypeUtilCompat.isValidSystemNonAuxAsciiCapableIme( + createDummyIme(false, false, createDummySubtype("keyboard", false, true)))) + .isFalse(); + } + + private static InputMethodInfo createDummyIme(boolean isSystem, boolean isAuxIme, + InputMethodSubtype... subtypes) { + final ResolveInfo ri = new ResolveInfo(); + final ServiceInfo si = new ServiceInfo(); + final ApplicationInfo ai = new ApplicationInfo(); + ai.packageName = "com.example.android.dummyime"; + ai.enabled = true; + ai.flags |= (isSystem ? ApplicationInfo.FLAG_SYSTEM : 0); + si.applicationInfo = ai; + si.enabled = true; + si.packageName = "com.example.android.dummyime"; + si.name = "Dummy IME"; + si.exported = true; + si.nonLocalizedLabel = "Dummy IME"; + ri.serviceInfo = si; + return new InputMethodInfo(ri, isAuxIme, "", Arrays.asList(subtypes), 1, false); + } + + private static InputMethodSubtype createDummySubtype( + String mode, boolean isAuxiliary, boolean isAsciiCapable) { + return new InputMethodSubtypeBuilder() + .setSubtypeNameResId(0) + .setSubtypeIconResId(0) + .setSubtypeLocale("en_US") + .setLanguageTag("en-US") + .setSubtypeMode(mode) + .setIsAuxiliary(isAuxiliary) + .setIsAsciiCapable(isAsciiCapable) + .build(); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlLoaderCompatTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlLoaderCompatTest.java new file mode 100644 index 000000000000..f981f365ec2b --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/license/LicenseHtmlLoaderCompatTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2017 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.settingslib.license; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.content.Context; + +import com.android.settingslib.SettingsLibRobolectricTestRunner; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.File; +import java.util.ArrayList; + +@RunWith(SettingsLibRobolectricTestRunner.class) +public class LicenseHtmlLoaderCompatTest { + @Mock + private Context mContext; + + LicenseHtmlLoaderCompat newLicenseHtmlLoader(ArrayList<File> xmlFiles, + File cachedHtmlFile, boolean isCachedHtmlFileOutdated, + boolean generateHtmlFileSucceeded) { + LicenseHtmlLoaderCompat loader = spy(new LicenseHtmlLoaderCompat(mContext)); + doReturn(xmlFiles).when(loader).getVaildXmlFiles(); + doReturn(cachedHtmlFile).when(loader).getCachedHtmlFile(); + doReturn(isCachedHtmlFileOutdated).when(loader).isCachedHtmlFileOutdated(any(), any()); + doReturn(generateHtmlFileSucceeded).when(loader).generateHtmlFile(any(), any()); + return loader; + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testLoadInBackground() { + ArrayList<File> xmlFiles = new ArrayList(); + xmlFiles.add(new File("test.xml")); + File cachedHtmlFile = new File("test.html"); + + LicenseHtmlLoaderCompat loader = newLicenseHtmlLoader(xmlFiles, cachedHtmlFile, true, true); + + assertThat(loader.loadInBackground()).isEqualTo(cachedHtmlFile); + verify(loader).generateHtmlFile(any(), any()); + } + + @Test + public void testLoadInBackgroundWithNoVaildXmlFiles() { + ArrayList<File> xmlFiles = new ArrayList(); + File cachedHtmlFile = new File("test.html"); + + LicenseHtmlLoaderCompat loader = newLicenseHtmlLoader(xmlFiles, cachedHtmlFile, true, true); + + assertThat(loader.loadInBackground()).isNull(); + verify(loader, never()).generateHtmlFile(any(), any()); + } + + @Test + public void testLoadInBackgroundWithNonOutdatedCachedHtmlFile() { + ArrayList<File> xmlFiles = new ArrayList(); + xmlFiles.add(new File("test.xml")); + File cachedHtmlFile = new File("test.html"); + + LicenseHtmlLoaderCompat loader = newLicenseHtmlLoader(xmlFiles, cachedHtmlFile, false, + true); + + assertThat(loader.loadInBackground()).isEqualTo(cachedHtmlFile); + verify(loader, never()).generateHtmlFile(any(), any()); + } + + @Test + public void testLoadInBackgroundWithGenerateHtmlFileFailed() { + ArrayList<File> xmlFiles = new ArrayList(); + xmlFiles.add(new File("test.xml")); + File cachedHtmlFile = new File("test.html"); + + LicenseHtmlLoaderCompat loader = newLicenseHtmlLoader(xmlFiles, cachedHtmlFile, true, + false); + + assertThat(loader.loadInBackground()).isNull(); + verify(loader).generateHtmlFile(any(), any()); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/suggestions/SuggestionControllerMixinCompatTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/suggestions/SuggestionControllerMixinCompatTest.java new file mode 100644 index 000000000000..1ee3afa9c1ce --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/suggestions/SuggestionControllerMixinCompatTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2018 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.settingslib.suggestions; + +import static androidx.lifecycle.Lifecycle.Event.ON_START; +import static androidx.lifecycle.Lifecycle.Event.ON_STOP; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.Context; + +import com.android.settingslib.SettingsLibRobolectricTestRunner; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import androidx.lifecycle.LifecycleOwner; +import androidx.loader.app.LoaderManager; + +@RunWith(SettingsLibRobolectricTestRunner.class) +@Config(shadows = ShadowSuggestionController.class) +public class SuggestionControllerMixinCompatTest { + + @Mock + private SuggestionControllerMixinCompat.SuggestionControllerHost mHost; + + private Context mContext; + private LifecycleOwner mLifecycleOwner; + private Lifecycle mLifecycle; + private SuggestionControllerMixinCompat mMixin; + private ComponentName mComponentName; + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mContext = RuntimeEnvironment.application; + mLifecycleOwner = () -> mLifecycle; + mLifecycle = new Lifecycle(mLifecycleOwner); + mComponentName = new ComponentName( + "com.android.settings.intelligence", + "com.android.settings.intelligence.suggestions.SuggestionService"); + } + + @After + public void tearDown() { + ShadowSuggestionController.reset(); + } + + @Test + public void goThroughLifecycle_onStartStop_shouldStartStopController() { + mMixin = new SuggestionControllerMixinCompat(mContext, mHost, mLifecycle, mComponentName); + + mLifecycle.handleLifecycleEvent(ON_START); + assertThat(ShadowSuggestionController.sStartCalled).isTrue(); + + mLifecycle.handleLifecycleEvent(ON_STOP); + assertThat(ShadowSuggestionController.sStopCalled).isTrue(); + } + + @Test + public void onServiceConnected_shouldGetSuggestion() { + final LoaderManager loaderManager = mock(LoaderManager.class); + when(mHost.getLoaderManager()).thenReturn(loaderManager); + + mMixin = new SuggestionControllerMixinCompat(mContext, mHost, mLifecycle, mComponentName); + mMixin.onServiceConnected(); + + verify(loaderManager).restartLoader(SuggestionLoader.LOADER_ID_SUGGESTIONS, + null /* args */, mMixin /* callback */); + } + + @Test + public void onServiceConnected_hostNotAttached_shouldDoNothing() { + when(mHost.getLoaderManager()).thenReturn(null); + + mMixin = new SuggestionControllerMixinCompat(mContext, mHost, mLifecycle, mComponentName); + mMixin.onServiceConnected(); + + verify(mHost).getLoaderManager(); + } + + @Test + public void onServiceDisconnected_hostNotAttached_shouldDoNothing() { + when(mHost.getLoaderManager()).thenReturn(null); + + mMixin = new SuggestionControllerMixinCompat(mContext, mHost, mLifecycle, mComponentName); + mMixin.onServiceDisconnected(); + + verify(mHost).getLoaderManager(); + } + + @Test + public void doneLoadingg_shouldSetSuggestionLoaded() { + mMixin = new SuggestionControllerMixinCompat(mContext, mHost, mLifecycle, mComponentName); + + mMixin.onLoadFinished(mock(SuggestionLoaderCompat.class), null); + + assertThat(mMixin.isSuggestionLoaded()).isTrue(); + + mMixin.onLoaderReset(mock(SuggestionLoaderCompat.class)); + + assertThat(mMixin.isSuggestionLoaded()).isFalse(); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/FooterPreferenceMixinCompatTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/FooterPreferenceMixinCompatTest.java new file mode 100644 index 000000000000..1abbaba441ee --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/FooterPreferenceMixinCompatTest.java @@ -0,0 +1,100 @@ +/* + * 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 com.android.settingslib.widget; + +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.android.settingslib.SettingsLibRobolectricTestRunner; +import com.android.settingslib.core.lifecycle.Lifecycle; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.shadows.ShadowApplication; + +import androidx.lifecycle.LifecycleOwner; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; + +@RunWith(SettingsLibRobolectricTestRunner.class) +public class FooterPreferenceMixinCompatTest { + + @Mock + private PreferenceFragmentCompat mFragment; + @Mock + private PreferenceScreen mScreen; + + private LifecycleOwner mLifecycleOwner; + private Lifecycle mLifecycle; + private FooterPreferenceMixinCompat mMixin; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mLifecycleOwner = () -> mLifecycle; + mLifecycle = new Lifecycle(mLifecycleOwner); + when(mFragment.getPreferenceManager()).thenReturn(mock(PreferenceManager.class)); + when(mFragment.getPreferenceManager().getContext()) + .thenReturn(ShadowApplication.getInstance().getApplicationContext()); + mMixin = new FooterPreferenceMixinCompat(mFragment, mLifecycle); + } + + @Test + public void createFooter_screenNotAvailable_noCrash() { + assertThat(mMixin.createFooterPreference()).isNotNull(); + } + + @Test + public void createFooter_screenAvailable_canAttachToScreen() { + when(mFragment.getPreferenceScreen()).thenReturn(mScreen); + + final FooterPreference preference = mMixin.createFooterPreference(); + + assertThat(preference).isNotNull(); + verify(mScreen).addPreference(preference); + } + + @Test + public void createFooter_screenAvailableDelayed_canAttachToScreen() { + final FooterPreference preference = mMixin.createFooterPreference(); + + mLifecycle.setPreferenceScreen(mScreen); + + assertThat(preference).isNotNull(); + verify(mScreen).addPreference(preference); + } + + @Test + public void createFooterTwice_screenAvailable_replaceOldFooter() { + when(mFragment.getPreferenceScreen()).thenReturn(mScreen); + + mMixin.createFooterPreference(); + mMixin.createFooterPreference(); + + verify(mScreen).removePreference(any(FooterPreference.class)); + verify(mScreen, times(2)).addPreference(any(FooterPreference.class)); + } + +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/FooterPreferenceMixinTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/FooterPreferenceMixinTest.java index 78b7616716f5..8604d186f2dd 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/FooterPreferenceMixinTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/FooterPreferenceMixinTest.java @@ -23,11 +23,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import androidx.lifecycle.LifecycleOwner; -import androidx.preference.PreferenceFragment; -import androidx.preference.PreferenceManager; -import androidx.preference.PreferenceScreen; - import com.android.settingslib.SettingsLibRobolectricTestRunner; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -38,6 +33,11 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.shadows.ShadowApplication; +import androidx.lifecycle.LifecycleOwner; +import androidx.preference.PreferenceFragment; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; + @RunWith(SettingsLibRobolectricTestRunner.class) public class FooterPreferenceMixinTest { |