diff options
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 { |