diff options
Diffstat (limited to 'packages/SettingsLib/src')
145 files changed, 6389 insertions, 4168 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/CustomDialogPreference.java b/packages/SettingsLib/src/com/android/settingslib/CustomDialogPreference.java index cf1c2c348367..192a40c5e79a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/CustomDialogPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/CustomDialogPreference.java @@ -20,11 +20,18 @@ import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; -import androidx.preference.PreferenceDialogFragment; -import androidx.preference.DialogPreference; import android.util.AttributeSet; import android.view.View; +import androidx.preference.DialogPreference; +import androidx.preference.PreferenceDialogFragment; + +/** + * Framework version is deprecated, use the compat version instead. + * + * @deprecated + */ +@Deprecated public class CustomDialogPreference extends DialogPreference { private CustomPreferenceDialogFragment mFragment; 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/CustomEditTextPreference.java b/packages/SettingsLib/src/com/android/settingslib/CustomEditTextPreference.java index 04c39540cf27..3caa0bb9cf4b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/CustomEditTextPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/CustomEditTextPreference.java @@ -23,13 +23,20 @@ import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; -import androidx.annotation.CallSuper; -import androidx.preference.EditTextPreferenceDialogFragment; -import androidx.preference.EditTextPreference; import android.util.AttributeSet; import android.view.View; import android.widget.EditText; +import androidx.annotation.CallSuper; +import androidx.preference.EditTextPreference; +import androidx.preference.EditTextPreferenceDialogFragment; + +/** + * Framework version is deprecated, use the compat version instead. + * + * @deprecated + */ +@Deprecated public class CustomEditTextPreference extends EditTextPreference { private CustomPreferenceDialogFragment mFragment; 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/DeviceInfoUtils.java b/packages/SettingsLib/src/com/android/settingslib/DeviceInfoUtils.java index c98bc39fe574..bc5a2c05e379 100644 --- a/packages/SettingsLib/src/com/android/settingslib/DeviceInfoUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/DeviceInfoUtils.java @@ -16,6 +16,8 @@ package com.android.settingslib; +import static android.content.Context.TELEPHONY_SERVICE; + import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; @@ -30,6 +32,7 @@ import android.telephony.TelephonyManager; import android.text.TextUtils; import android.text.format.DateFormat; import android.util.Log; + import androidx.annotation.VisibleForTesting; import java.io.BufferedReader; @@ -43,8 +46,6 @@ import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; -import static android.content.Context.TELEPHONY_SERVICE; - public class DeviceInfoUtils { private static final String TAG = "DeviceInfoUtils"; diff --git a/packages/SettingsLib/src/com/android/settingslib/HelpUtils.java b/packages/SettingsLib/src/com/android/settingslib/HelpUtils.java deleted file mode 100644 index 8055caaad536..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/HelpUtils.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib; - -import android.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.res.Resources; -import android.content.res.Resources.Theme; -import android.content.res.TypedArray; -import android.net.Uri; -import android.provider.Settings.Global; -import android.text.TextUtils; -import android.util.Log; -import android.util.TypedValue; -import android.view.Menu; -import android.view.MenuItem; -import android.view.MenuItem.OnMenuItemClickListener; -import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.nano.MetricsProto.MetricsEvent; - -import java.net.URISyntaxException; -import java.util.Locale; - -/** - * Functions to easily prepare contextual help menu option items with an intent that opens up the - * browser to a particular URL, while taking into account the preferred language and app version. - */ -public class HelpUtils { - private final static String TAG = HelpUtils.class.getSimpleName(); - - private static final int MENU_HELP = Menu.FIRST + 100; - - /** - * Help URL query parameter key for the preferred language. - */ - private final static String PARAM_LANGUAGE_CODE = "hl"; - - /** - * Help URL query parameter key for the app version. - */ - private final static String PARAM_VERSION = "version"; - - // Constants for help intents. - private static final String EXTRA_CONTEXT = "EXTRA_CONTEXT"; - private static final String EXTRA_THEME = "EXTRA_THEME"; - private static final String EXTRA_PRIMARY_COLOR = "EXTRA_PRIMARY_COLOR"; - private static final String EXTRA_BACKUP_URI = "EXTRA_BACKUP_URI"; - - /** - * Cached version code to prevent repeated calls to the package manager. - */ - private static String sCachedVersionCode = null; - - /** Static helper that is not instantiable*/ - private HelpUtils() { } - - public static boolean prepareHelpMenuItem(Activity activity, Menu menu, String helpUri, - String backupContext) { - MenuItem helpItem = menu.add(0, MENU_HELP, 0, R.string.help_feedback_label); - helpItem.setIcon(R.drawable.ic_help_actionbar); - return prepareHelpMenuItem(activity, helpItem, helpUri, backupContext); - } - - public static boolean prepareHelpMenuItem(Activity activity, Menu menu, int helpUriResource, - String backupContext) { - MenuItem helpItem = menu.add(0, MENU_HELP, 0, R.string.help_feedback_label); - helpItem.setIcon(R.drawable.ic_help_actionbar); - return prepareHelpMenuItem(activity, helpItem, activity.getString(helpUriResource), - backupContext); - } - - /** - * Prepares the help menu item by doing the following. - * - If the helpUrlString is empty or null, the help menu item is made invisible. - * - Otherwise, this makes the help menu item visible and sets the intent for the help menu - * item to view the URL. - * - * @return returns whether the help menu item has been made visible. - */ - public static boolean prepareHelpMenuItem(final Activity activity, MenuItem helpMenuItem, - String helpUriString, String backupContext) { - if (Global.getInt(activity.getContentResolver(), Global.DEVICE_PROVISIONED, 0) == 0) { - return false; - } - if (TextUtils.isEmpty(helpUriString)) { - // The help url string is empty or null, so set the help menu item to be invisible. - helpMenuItem.setVisible(false); - - // return that the help menu item is not visible (i.e. false) - return false; - } else { - final Intent intent = getHelpIntent(activity, helpUriString, backupContext); - - // Set the intent to the help menu item, show the help menu item in the overflow - // menu, and make it visible. - if (intent != null) { - helpMenuItem.setOnMenuItemClickListener(new OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - MetricsLogger.action(activity, - MetricsEvent.ACTION_SETTING_HELP_AND_FEEDBACK, - intent.getStringExtra(EXTRA_CONTEXT)); - try { - activity.startActivityForResult(intent, 0); - } catch (ActivityNotFoundException exc) { - Log.e(TAG, "No activity found for intent: " + intent); - } - return true; - } - }); - helpMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - helpMenuItem.setVisible(true); - } else { - helpMenuItem.setVisible(false); - return false; - } - - // return that the help menu item is visible (i.e., true) - return true; - } - } - - public static Intent getHelpIntent(Context context, String helpUriString, - String backupContext) { - if (Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0) == 0) { - return null; - } - // Try to handle as Intent Uri, otherwise just treat as Uri. - try { - Intent intent = Intent.parseUri(helpUriString, - Intent.URI_ANDROID_APP_SCHEME | Intent.URI_INTENT_SCHEME); - addIntentParameters(context, intent, backupContext, true /* sendPackageName */); - ComponentName component = intent.resolveActivity(context.getPackageManager()); - if (component != null) { - return intent; - } else if (intent.hasExtra(EXTRA_BACKUP_URI)) { - // This extra contains a backup URI for when the intent isn't available. - return getHelpIntent(context, intent.getStringExtra(EXTRA_BACKUP_URI), - backupContext); - } else { - return null; - } - } catch (URISyntaxException e) { - } - // The help url string exists, so first add in some extra query parameters. - final Uri fullUri = uriWithAddedParameters(context, Uri.parse(helpUriString)); - - // Then, create an intent that will be fired when the user - // selects this help menu item. - Intent intent = new Intent(Intent.ACTION_VIEW, fullUri); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK - | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - return intent; - } - - public static void addIntentParameters(Context context, Intent intent, String backupContext, - boolean sendPackageName) { - if (!intent.hasExtra(EXTRA_CONTEXT)) { - // Insert some context if none exists. - intent.putExtra(EXTRA_CONTEXT, backupContext); - } - - Resources resources = context.getResources(); - boolean includePackageName = - resources.getBoolean(com.android.internal.R.bool.config_sendPackageName); - - if (sendPackageName && includePackageName) { - String[] packageNameKey = - {resources.getString(com.android.internal.R.string.config_helpPackageNameKey)}; - String[] packageNameValue = - {resources.getString( - com.android.internal.R.string.config_helpPackageNameValue)}; - String helpIntentExtraKey = - resources.getString(com.android.internal.R.string.config_helpIntentExtraKey); - String helpIntentNameKey = - resources.getString(com.android.internal.R.string.config_helpIntentNameKey); - String feedbackIntentExtraKey = - resources.getString( - com.android.internal.R.string.config_feedbackIntentExtraKey); - String feedbackIntentNameKey = - resources.getString(com.android.internal.R.string.config_feedbackIntentNameKey); - intent.putExtra(helpIntentExtraKey, packageNameKey); - intent.putExtra(helpIntentNameKey, packageNameValue); - intent.putExtra(feedbackIntentExtraKey, packageNameKey); - intent.putExtra(feedbackIntentNameKey, packageNameValue); - } - intent.putExtra(EXTRA_THEME, 0 /* Light theme */); - TypedArray array = context.obtainStyledAttributes(new int[]{android.R.attr.colorPrimary}); - intent.putExtra(EXTRA_PRIMARY_COLOR, array.getColor(0, 0)); - array.recycle(); - } - - /** - * Adds two query parameters into the Uri, namely the language code and the version code - * of the app's package as gotten via the context. - * @return the uri with added query parameters - */ - private static Uri uriWithAddedParameters(Context context, Uri baseUri) { - Uri.Builder builder = baseUri.buildUpon(); - - // Add in the preferred language - builder.appendQueryParameter(PARAM_LANGUAGE_CODE, Locale.getDefault().toString()); - - // Add in the package version code - if (sCachedVersionCode == null) { - // There is no cached version code, so try to get it from the package manager. - try { - // cache the version code - PackageInfo info = context.getPackageManager().getPackageInfo( - context.getPackageName(), 0); - sCachedVersionCode = Long.toString(info.getLongVersionCode()); - - // append the version code to the uri - builder.appendQueryParameter(PARAM_VERSION, sCachedVersionCode); - } catch (NameNotFoundException e) { - // Cannot find the package name, so don't add in the version parameter - // This shouldn't happen. - Log.wtf(TAG, "Invalid package name for context", e); - } - } else { - builder.appendQueryParameter(PARAM_VERSION, sCachedVersionCode); - } - - // Build the full uri and return it - return builder.build(); - } -} diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockImageSpan.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockImageSpan.java index 360a34c959f2..787f27efbc30 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockImageSpan.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockImageSpan.java @@ -37,7 +37,7 @@ public class RestrictedLockImageSpan extends ImageSpan { mContext = context; mExtraPadding = mContext.getResources().getDimensionPixelSize( R.dimen.restricted_icon_padding); - mRestrictedPadlock = RestrictedLockUtils.getRestrictedPadlock(mContext); + mRestrictedPadlock = RestrictedLockUtilsInternal.getRestrictedPadlock(mContext); } @Override diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtils.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java index 7f518c1d71d3..1457fcfadc89 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedLockUtilsInternal.java @@ -26,16 +26,14 @@ import android.app.AppGlobals; import android.app.admin.DevicePolicyManager; import android.content.ComponentName; import android.content.Context; -import android.content.Intent; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.UserInfo; +import android.content.res.TypedArray; import android.graphics.drawable.Drawable; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; -import android.provider.Settings; -import androidx.annotation.VisibleForTesting; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.style.ForegroundColorSpan; @@ -43,23 +41,30 @@ import android.text.style.ImageSpan; import android.view.MenuItem; import android.widget.TextView; +import androidx.annotation.VisibleForTesting; + import com.android.internal.widget.LockPatternUtils; import java.util.List; -import java.util.Objects; /** * Utility class to host methods usable in adding a restricted padlock icon and showing admin * support message dialog. */ -public class RestrictedLockUtils { +public class RestrictedLockUtilsInternal extends RestrictedLockUtils { /** * @return drawables for displaying with settings that are locked by a device admin. */ public static Drawable getRestrictedPadlock(Context context) { - Drawable restrictedPadlock = context.getDrawable(R.drawable.ic_info); + Drawable restrictedPadlock = context.getDrawable(android.R.drawable.ic_info); final int iconSize = context.getResources().getDimensionPixelSize( - R.dimen.restricted_icon_size); + android.R.dimen.config_restrictedIconSize); + + TypedArray ta = context.obtainStyledAttributes(new int[]{android.R.attr.colorAccent}); + int colorAccent = ta.getColor(0, 0); + ta.recycle(); + restrictedPadlock.setTint(colorAccent); + restrictedPadlock.setBounds(0, 0, iconSize, iconSize); return restrictedPadlock; } @@ -159,6 +164,17 @@ public class RestrictedLockUtils { } /** + * @return the UserHandle for a userId. Return null for USER_NULL + */ + private static UserHandle getUserHandleOf(@UserIdInt int userId) { + if (userId == UserHandle.USER_NULL) { + return null; + } else { + return UserHandle.of(userId); + } + } + + /** * Filter a set of device admins based on a predicate {@code check}. This is equivalent to * {@code admins.stream().filter(check).map(x → new EnforcedAdmin(admin, userId)} except it's * returning a zero/one/many-type thing. @@ -178,11 +194,13 @@ public class RestrictedLockUtils { if (admins == null) { return null; } + + final UserHandle user = getUserHandleOf(userId); EnforcedAdmin enforcedAdmin = null; for (ComponentName admin : admins) { if (check.isEnforcing(dpm, admin, userId)) { if (enforcedAdmin == null) { - enforcedAdmin = new EnforcedAdmin(admin, userId); + enforcedAdmin = new EnforcedAdmin(admin, user); } else { return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN; } @@ -206,7 +224,7 @@ public class RestrictedLockUtils { IPackageManager ipm = AppGlobals.getPackageManager(); try { if (ipm.getBlockUninstallForUser(packageName, userId)) { - return getProfileOrDeviceOwner(context, userId); + return getProfileOrDeviceOwner(context, getUserHandleOf(userId)); } } catch (RemoteException e) { // Nothing to do @@ -225,7 +243,7 @@ public class RestrictedLockUtils { IPackageManager ipm = AppGlobals.getPackageManager(); try { if (ipm.isPackageSuspendedForUser(packageName, userId)) { - return getProfileOrDeviceOwner(context, userId); + return getProfileOrDeviceOwner(context, getUserHandleOf(userId)); } } catch (RemoteException | IllegalArgumentException e) { // Nothing to do @@ -240,14 +258,15 @@ public class RestrictedLockUtils { if (dpm == null) { return null; } - EnforcedAdmin admin = getProfileOrDeviceOwner(context, userId); + EnforcedAdmin admin = getProfileOrDeviceOwner(context, getUserHandleOf(userId)); boolean permitted = true; if (admin != null) { permitted = dpm.isInputMethodPermittedByAdmin(admin.component, packageName, userId); } int managedProfileId = getManagedProfileId(context, userId); - EnforcedAdmin profileAdmin = getProfileOrDeviceOwner(context, managedProfileId); + EnforcedAdmin profileAdmin = getProfileOrDeviceOwner(context, + getUserHandleOf(managedProfileId)); boolean permittedByProfileAdmin = true; if (profileAdmin != null) { permittedByProfileAdmin = dpm.isInputMethodPermittedByAdmin(profileAdmin.component, @@ -293,14 +312,15 @@ public class RestrictedLockUtils { if (dpm == null) { return null; } - EnforcedAdmin admin = getProfileOrDeviceOwner(context, userId); + EnforcedAdmin admin = getProfileOrDeviceOwner(context, getUserHandleOf(userId)); boolean permitted = true; if (admin != null) { permitted = dpm.isAccessibilityServicePermittedByAdmin(admin.component, packageName, userId); } int managedProfileId = getManagedProfileId(context, userId); - EnforcedAdmin profileAdmin = getProfileOrDeviceOwner(context, managedProfileId); + EnforcedAdmin profileAdmin = getProfileOrDeviceOwner(context, + getUserHandleOf(managedProfileId)); boolean permittedByProfileAdmin = true; if (profileAdmin != null) { permittedByProfileAdmin = dpm.isAccessibilityServicePermittedByAdmin( @@ -360,7 +380,7 @@ public class RestrictedLockUtils { if (!isAccountTypeDisabled) { return null; } - return getProfileOrDeviceOwner(context, userId); + return getProfileOrDeviceOwner(context, getUserHandleOf(userId)); } /** @@ -372,7 +392,8 @@ public class RestrictedLockUtils { */ public static EnforcedAdmin checkIfMeteredDataRestricted(Context context, String packageName, int userId) { - final EnforcedAdmin enforcedAdmin = getProfileOrDeviceOwner(context, userId); + final EnforcedAdmin enforcedAdmin = getProfileOrDeviceOwner(context, + getUserHandleOf(userId)); if (enforcedAdmin == null) { return null; } @@ -397,7 +418,7 @@ public class RestrictedLockUtils { return null; } ComponentName adminComponent = dpm.getDeviceOwnerComponentOnCallingUser(); - return new EnforcedAdmin(adminComponent, UserHandle.myUserId()); + return new EnforcedAdmin(adminComponent, getUserHandleOf(UserHandle.myUserId())); } /** @@ -429,10 +450,11 @@ public class RestrictedLockUtils { return null; } EnforcedAdmin enforcedAdmin = null; + final UserHandle user = getUserHandleOf(userId); for (ComponentName admin : admins) { if (check.isEnforcing(dpm, admin, userId)) { if (enforcedAdmin == null) { - enforcedAdmin = new EnforcedAdmin(admin, userId); + enforcedAdmin = new EnforcedAdmin(admin, user); } else { return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN; } @@ -483,13 +505,14 @@ public class RestrictedLockUtils { if (admins == null) { continue; } + final UserHandle user = getUserHandleOf(userInfo.id); final boolean isSeparateProfileChallengeEnabled = sProxy.isSeparateProfileChallengeEnabled(lockPatternUtils, userInfo.id); for (ComponentName admin : admins) { if (!isSeparateProfileChallengeEnabled) { if (check.isEnforcing(dpm, admin, userInfo.id)) { if (enforcedAdmin == null) { - enforcedAdmin = new EnforcedAdmin(admin, userInfo.id); + enforcedAdmin = new EnforcedAdmin(admin, user); } else { return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN; } @@ -506,7 +529,7 @@ public class RestrictedLockUtils { DevicePolicyManager parentDpm = sProxy.getParentProfileInstance(dpm, userInfo); if (check.isEnforcing(parentDpm, admin, userInfo.id)) { if (enforcedAdmin == null) { - enforcedAdmin = new EnforcedAdmin(admin, userInfo.id); + enforcedAdmin = new EnforcedAdmin(admin, user); } else { return EnforcedAdmin.MULTIPLE_ENFORCED_ADMIN; } @@ -517,33 +540,6 @@ public class RestrictedLockUtils { return enforcedAdmin; } - public static EnforcedAdmin getProfileOrDeviceOwner(Context context, int userId) { - return getProfileOrDeviceOwner(context, null, userId); - } - - public static EnforcedAdmin getProfileOrDeviceOwner( - Context context, String enforcedRestriction, int userId) { - if (userId == UserHandle.USER_NULL) { - return null; - } - final DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService( - Context.DEVICE_POLICY_SERVICE); - if (dpm == null) { - return null; - } - ComponentName adminComponent = dpm.getProfileOwnerAsUser(userId); - if (adminComponent != null) { - return new EnforcedAdmin(adminComponent, enforcedRestriction, userId); - } - if (dpm.getDeviceOwnerUserId() == userId) { - adminComponent = dpm.getDeviceOwnerComponentOnAnyUser(); - if (adminComponent != null) { - return new EnforcedAdmin(adminComponent, enforcedRestriction, userId); - } - } - return null; - } - public static EnforcedAdmin getDeviceOwner(Context context) { return getDeviceOwner(context, null); } @@ -557,7 +553,7 @@ public class RestrictedLockUtils { ComponentName adminComponent = dpm.getDeviceOwnerComponentOnAnyUser(); if (adminComponent != null) { return new EnforcedAdmin( - adminComponent, enforcedRestriction, dpm.getDeviceOwnerUserId()); + adminComponent, enforcedRestriction, dpm.getDeviceOwnerUser()); } return null; } @@ -578,7 +574,7 @@ public class RestrictedLockUtils { } ComponentName adminComponent = dpm.getProfileOwnerAsUser(userId); if (adminComponent != null) { - return new EnforcedAdmin(adminComponent, enforcedRestriction, userId); + return new EnforcedAdmin(adminComponent, enforcedRestriction, getUserHandleOf(userId)); } return null; } @@ -631,45 +627,6 @@ public class RestrictedLockUtils { } } - /** - * Send the intent to trigger the {@link android.settings.ShowAdminSupportDetailsDialog}. - */ - public static void sendShowAdminSupportDetailsIntent(Context context, EnforcedAdmin admin) { - final Intent intent = getShowAdminSupportDetailsIntent(context, admin); - int targetUserId = UserHandle.myUserId(); - if (admin != null && admin.userId != UserHandle.USER_NULL - && isCurrentUserOrProfile(context, admin.userId)) { - targetUserId = admin.userId; - } - intent.putExtra(DevicePolicyManager.EXTRA_RESTRICTION, admin.enforcedRestriction); - context.startActivityAsUser(intent, new UserHandle(targetUserId)); - } - - public static Intent getShowAdminSupportDetailsIntent(Context context, EnforcedAdmin admin) { - final Intent intent = new Intent(Settings.ACTION_SHOW_ADMIN_SUPPORT_DETAILS); - if (admin != null) { - if (admin.component != null) { - intent.putExtra(DevicePolicyManager.EXTRA_DEVICE_ADMIN, admin.component); - } - int adminUserId = UserHandle.myUserId(); - if (admin.userId != UserHandle.USER_NULL) { - adminUserId = admin.userId; - } - intent.putExtra(Intent.EXTRA_USER_ID, adminUserId); - } - return intent; - } - - public static boolean isCurrentUserOrProfile(Context context, int userId) { - UserManager um = UserManager.get(context); - for (UserInfo userInfo : um.getProfiles(UserHandle.myUserId())) { - if (userInfo.id == userId) { - return true; - } - } - return false; - } - public static boolean isAdminInCurrentUserOrProfile(Context context, ComponentName admin) { DevicePolicyManager dpm = (DevicePolicyManager) context.getSystemService( Context.DEVICE_POLICY_SERVICE); @@ -715,75 +672,6 @@ public class RestrictedLockUtils { textView.setText(sb); } - public static class EnforcedAdmin { - @Nullable - public ComponentName component = null; - /** - * The restriction enforced by admin. It could be any user restriction or policy like - * {@link DevicePolicyManager#POLICY_DISABLE_CAMERA}. - */ - @Nullable - public String enforcedRestriction = null; - public int userId = UserHandle.USER_NULL; - - // We use this to represent the case where a policy is enforced by multiple admins. - public final static EnforcedAdmin MULTIPLE_ENFORCED_ADMIN = new EnforcedAdmin(); - - public static EnforcedAdmin createDefaultEnforcedAdminWithRestriction( - String enforcedRestriction) { - EnforcedAdmin enforcedAdmin = new EnforcedAdmin(); - enforcedAdmin.enforcedRestriction = enforcedRestriction; - return enforcedAdmin; - } - - public EnforcedAdmin(ComponentName component, int userId) { - this.component = component; - this.userId = userId; - } - - public EnforcedAdmin(ComponentName component, String enforcedRestriction, int userId) { - this.component = component; - this.enforcedRestriction = enforcedRestriction; - this.userId = userId; - } - - public EnforcedAdmin(EnforcedAdmin other) { - if (other == null) { - throw new IllegalArgumentException(); - } - this.component = other.component; - this.enforcedRestriction = other.enforcedRestriction; - this.userId = other.userId; - } - - public EnforcedAdmin() { - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - EnforcedAdmin that = (EnforcedAdmin) o; - return userId == that.userId && - Objects.equals(component, that.component) && - Objects.equals(enforcedRestriction, that.enforcedRestriction); - } - - @Override - public int hashCode() { - return Objects.hash(component, enforcedRestriction, userId); - } - - @Override - public String toString() { - return "EnforcedAdmin{" + - "component=" + component + - ", enforcedRestriction='" + enforcedRestriction + - ", userId=" + userId + - '}'; - } - } - /** * Static {@link LockPatternUtils} and {@link DevicePolicyManager} wrapper for testing purposes. * {@link LockPatternUtils} is an internal API not supported by robolectric. diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreference.java index 79e011c6e7ed..ad7e995412aa 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreference.java @@ -16,15 +16,16 @@ package com.android.settingslib; +import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; + import android.content.Context; import android.os.UserHandle; -import androidx.core.content.res.TypedArrayUtils; -import androidx.preference.PreferenceManager; -import androidx.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.view.View; -import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import androidx.core.content.res.TypedArrayUtils; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceViewHolder; /** * Preference class that supports being disabled by a user restriction diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java index a930bb8373ba..1ba1f72cb2e1 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedPreferenceHelper.java @@ -16,19 +16,18 @@ package com.android.settingslib; +import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; + import android.content.Context; import android.content.res.TypedArray; -import android.graphics.drawable.Drawable; import android.os.UserHandle; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.text.TextUtils; import android.util.AttributeSet; import android.util.TypedValue; -import android.view.View; import android.widget.TextView; -import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; /** * Helper class for managing settings preferences that can be disabled @@ -63,7 +62,7 @@ public class RestrictedPreferenceHelper { } mAttrUserRestriction = data == null ? null : data.toString(); // If the system has set the user restriction, then we shouldn't add the padlock. - if (RestrictedLockUtils.hasBaseUserRestriction(mContext, mAttrUserRestriction, + if (RestrictedLockUtilsInternal.hasBaseUserRestriction(mContext, mAttrUserRestriction, UserHandle.myUserId())) { mAttrUserRestriction = null; return; @@ -134,7 +133,7 @@ public class RestrictedPreferenceHelper { * @param userId user to check the restriction for. */ public void checkRestrictionAndSetDisabled(String userRestriction, int userId) { - EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced(mContext, + EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext, userRestriction, userId); setDisabledByAdmin(admin); } diff --git a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java index fe6d9fe77b73..0ed507c46372 100644 --- a/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/RestrictedSwitchPreference.java @@ -21,15 +21,16 @@ import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; import android.content.Context; import android.content.res.TypedArray; import android.os.UserHandle; -import androidx.preference.SwitchPreference; -import androidx.core.content.res.TypedArrayUtils; -import androidx.preference.PreferenceManager; -import androidx.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; import android.widget.TextView; +import androidx.core.content.res.TypedArrayUtils; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceViewHolder; +import androidx.preference.SwitchPreference; + /** * Version of SwitchPreference that can be disabled by a device admin * using a user restriction. diff --git a/packages/SettingsLib/src/com/android/settingslib/TetherUtil.java b/packages/SettingsLib/src/com/android/settingslib/TetherUtil.java index 535b2929563a..c3993e9063b2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/TetherUtil.java +++ b/packages/SettingsLib/src/com/android/settingslib/TetherUtil.java @@ -15,11 +15,16 @@ */ package com.android.settingslib; +import static android.os.UserManager.DISALLOW_CONFIG_TETHERING; + import android.content.Context; +import android.net.ConnectivityManager; import android.os.SystemProperties; -import androidx.annotation.VisibleForTesting; +import android.os.UserHandle; import android.telephony.CarrierConfigManager; +import androidx.annotation.VisibleForTesting; + public class TetherUtil { @VisibleForTesting @@ -49,4 +54,14 @@ public class TetherUtil { } return (provisionApp.length == 2); } + + public static boolean isTetherAvailable(Context context) { + final ConnectivityManager cm = context.getSystemService(ConnectivityManager.class); + final boolean tetherConfigDisallowed = RestrictedLockUtilsInternal + .checkIfRestrictionEnforced(context, DISALLOW_CONFIG_TETHERING, + UserHandle.myUserId()) != null; + final boolean hasBaseUserRestriction = RestrictedLockUtilsInternal.hasBaseUserRestriction( + context, DISALLOW_CONFIG_TETHERING, UserHandle.myUserId()); + return (cm.isTetheringSupported() || tetherConfigDisallowed) && !hasBaseUserRestriction; + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/TwoTargetPreference.java b/packages/SettingsLib/src/com/android/settingslib/TwoTargetPreference.java index 3a26f4649b64..02895a479352 100644 --- a/packages/SettingsLib/src/com/android/settingslib/TwoTargetPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/TwoTargetPreference.java @@ -18,13 +18,14 @@ package com.android.settingslib; import android.annotation.IntDef; import android.content.Context; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.util.AttributeSet; import android.view.View; import android.widget.ImageView; import android.widget.LinearLayout; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java index 505cfeac220c..6abe76a1e753 100644 --- a/packages/SettingsLib/src/com/android/settingslib/Utils.java +++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java @@ -23,11 +23,12 @@ import android.os.UserHandle; import android.os.UserManager; import android.print.PrintManager; import android.provider.Settings; +import android.telephony.ServiceState; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.UserIcons; import com.android.settingslib.drawable.UserIconDrawable; -import com.android.settingslib.wrapper.LocationManagerWrapper; + import java.text.NumberFormat; public class Utils { @@ -69,8 +70,7 @@ public class Utils { intent, UserHandle.of(userId), android.Manifest.permission.WRITE_SECURE_SETTINGS); LocationManager locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); - LocationManagerWrapper wrapper = new LocationManagerWrapper(locationManager); - wrapper.setLocationEnabledForUser(enabled, UserHandle.of(userId)); + locationManager.setLocationEnabledForUser(enabled, UserHandle.of(userId)); } public static boolean updateLocationMode(Context context, int oldMode, int newMode, int userId, @@ -203,21 +203,28 @@ public class Utils { return statusString; } - @ColorInt - public static int getColorAccent(Context context) { + public static ColorStateList getColorAccent(Context context) { return getColorAttr(context, android.R.attr.colorAccent); } - @ColorInt - public static int getColorError(Context context) { + public static ColorStateList getColorError(Context context) { return getColorAttr(context, android.R.attr.colorError); } @ColorInt - public static int getDefaultColor(Context context, int resId) { + public static int getColorAccentDefaultColor(Context context) { + return getColorAttrDefaultColor(context, android.R.attr.colorAccent); + } + + @ColorInt + public static int getColorErrorDefaultColor(Context context) { + return getColorAttrDefaultColor(context, android.R.attr.colorError); + } + + @ColorInt + public static int getColorStateListDefaultColor(Context context, int resId) { final ColorStateList list = context.getResources().getColorStateList(resId, context.getTheme()); - return list.getDefaultColor(); } @@ -242,13 +249,24 @@ public class Utils { } @ColorInt - public static int getColorAttr(Context context, int attr) { + public static int getColorAttrDefaultColor(Context context, int attr) { TypedArray ta = context.obtainStyledAttributes(new int[]{attr}); @ColorInt int colorAccent = ta.getColor(0, 0); ta.recycle(); return colorAccent; } + public static ColorStateList getColorAttr(Context context, int attr) { + TypedArray ta = context.obtainStyledAttributes(new int[]{attr}); + ColorStateList stateList = null; + try { + stateList = ta.getColorStateList(0); + } finally { + ta.recycle(); + } + return stateList; + } + public static int getThemeAttr(Context context, int attr) { TypedArray ta = context.obtainStyledAttributes(new int[]{attr}); int theme = ta.getResourceId(0, 0); @@ -374,4 +392,52 @@ public class Utils { || audioMode == AudioManager.MODE_IN_CALL || audioMode == AudioManager.MODE_IN_COMMUNICATION; } + + /** + * Return the service state is in-service or not. + * To make behavior consistent with SystemUI and Settings/AboutPhone/SIM status UI + * + * @param serviceState Service state. {@link ServiceState} + */ + public static boolean isInService(ServiceState serviceState) { + if (serviceState == null) { + return false; + } + int state = getCombinedServiceState(serviceState); + if (state == ServiceState.STATE_POWER_OFF + || state == ServiceState.STATE_OUT_OF_SERVICE + || state == ServiceState.STATE_EMERGENCY_ONLY) { + return false; + } else { + return true; + } + } + + /** + * Return the combined service state. + * To make behavior consistent with SystemUI and Settings/AboutPhone/SIM status UI + * + * @param serviceState Service state. {@link ServiceState} + */ + public static int getCombinedServiceState(ServiceState serviceState) { + if (serviceState == null) { + return ServiceState.STATE_OUT_OF_SERVICE; + } + + // Consider the device to be in service if either voice or data + // service is available. Some SIM cards are marketed as data-only + // and do not support voice service, and on these SIM cards, we + // want to show signal bars for data service as well as the "no + // service" or "emergency calls only" text that indicates that voice + // is not available. + int state = serviceState.getState(); + int dataState = serviceState.getDataRegState(); + if (state == ServiceState.STATE_OUT_OF_SERVICE + || state == ServiceState.STATE_EMERGENCY_ONLY) { + if (dataState == ServiceState.STATE_IN_SERVICE) { + return ServiceState.STATE_IN_SERVICE; + } + } + return state; + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/accessibility/AccessibilityUtils.java b/packages/SettingsLib/src/com/android/settingslib/accessibility/AccessibilityUtils.java index 8473c06c1ac7..a18600abf788 100644 --- a/packages/SettingsLib/src/com/android/settingslib/accessibility/AccessibilityUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/accessibility/AccessibilityUtils.java @@ -40,9 +40,6 @@ import java.util.Set; public class AccessibilityUtils { public static final char ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR = ':'; - final static TextUtils.SimpleStringSplitter sStringColonSplitter = - new TextUtils.SimpleStringSplitter(ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR); - /** * @return the set of enabled accessibility services. If there are no services, * it returns the unmodifiable {@link Collections#emptySet()}. @@ -72,16 +69,16 @@ public class AccessibilityUtils { final String enabledServicesSetting = Settings.Secure.getStringForUser( context.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, userId); - if (enabledServicesSetting == null) { + if (TextUtils.isEmpty(enabledServicesSetting)) { return Collections.emptySet(); } final Set<ComponentName> enabledServices = new HashSet<>(); - final TextUtils.SimpleStringSplitter colonSplitter = sStringColonSplitter; + final TextUtils.StringSplitter colonSplitter = + new TextUtils.SimpleStringSplitter(ENABLED_ACCESSIBILITY_SERVICES_SEPARATOR); colonSplitter.setString(enabledServicesSetting); - while (colonSplitter.hasNext()) { - final String componentNameString = colonSplitter.next(); + for (String componentNameString : colonSplitter) { final ComponentName enabledService = ComponentName.unflattenFromString( componentNameString); if (enabledService != null) { @@ -168,8 +165,7 @@ public class AccessibilityUtils { * an OEM-configurable default if the setting has never been set. * * @param context A valid context - * @param userId The user whose settings should be checked - * + * @param userId The user whose settings should be checked * @return The component name, flattened to a string, of the target service. */ public static String getShortcutTargetServiceComponentNameString( @@ -187,9 +183,9 @@ public class AccessibilityUtils { * Check if the accessibility shortcut is enabled for a user * * @param context A valid context - * @param userId The user of interest + * @param userId The user of interest * @return {@code true} if the shortcut is enabled for the user. {@code false} otherwise. - * Note that the shortcut may be enabled, but no action associated with it. + * Note that the shortcut may be enabled, but no action associated with it. */ public static boolean isShortcutEnabled(Context context, int userId) { return Settings.Secure.getIntForUser(context.getContentResolver(), diff --git a/packages/SettingsLib/src/com/android/settingslib/animation/AppearAnimationUtils.java b/packages/SettingsLib/src/com/android/settingslib/animation/AppearAnimationUtils.java index df76125a99f2..efac6bc3572d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/animation/AppearAnimationUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/animation/AppearAnimationUtils.java @@ -19,15 +19,12 @@ package com.android.settingslib.animation; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; -import android.animation.ValueAnimator; import android.content.Context; import android.view.RenderNodeAnimator; import android.view.View; -import android.view.ViewPropertyAnimator; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; -import com.android.internal.widget.LockPatternView; import com.android.settingslib.R; /** diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java index fd80edf839bc..a936df2bf2eb 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/ApplicationsState.java @@ -22,9 +22,6 @@ import android.app.AppGlobals; import android.app.Application; import android.app.usage.StorageStats; import android.app.usage.StorageStatsManager; -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.LifecycleObserver; -import androidx.lifecycle.OnLifecycleEvent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -49,12 +46,16 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; -import androidx.annotation.VisibleForTesting; import android.text.format.Formatter; import android.util.IconDrawableFactory; import android.util.Log; import android.util.SparseArray; +import androidx.annotation.VisibleForTesting; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.OnLifecycleEvent; + import com.android.internal.R; import com.android.internal.util.ArrayUtils; @@ -253,7 +254,8 @@ public class ApplicationsState { user.isAdmin() ? mAdminRetrieveFlags : mRetrieveFlags, user.id); mApplications.addAll(list.getList()); - } catch (RemoteException e) { + } catch (Exception e) { + Log.e(TAG, "Error during doResumeIfNeededLocked", e); } } @@ -674,6 +676,7 @@ public class ApplicationsState { if (!mResumed) { mResumed = true; mSessionsChanged = true; + doPauseLocked(); doResumeIfNeededLocked(); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/DefaultAppInfo.java b/packages/SettingsLib/src/com/android/settingslib/applications/DefaultAppInfo.java index 246ca474da32..3c451127508d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/DefaultAppInfo.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/DefaultAppInfo.java @@ -25,11 +25,9 @@ import android.content.pm.PackageItemInfo; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.os.RemoteException; -import android.os.UserHandle; import android.util.IconDrawableFactory; import com.android.settingslib.widget.CandidateInfo; -import com.android.settingslib.wrapper.PackageManagerWrapper; /** * Data model representing an app in DefaultAppPicker UI. @@ -40,18 +38,18 @@ public class DefaultAppInfo extends CandidateInfo { public final ComponentName componentName; public final PackageItemInfo packageItemInfo; public final String summary; - protected final PackageManagerWrapper mPm; + protected final PackageManager mPm; private final Context mContext; - public DefaultAppInfo(Context context, PackageManagerWrapper pm, int uid, ComponentName cn) { + public DefaultAppInfo(Context context, PackageManager pm, int uid, ComponentName cn) { this(context, pm, uid, cn, null /* summary */, true /* enabled */); } - public DefaultAppInfo(Context context, PackageManagerWrapper pm, PackageItemInfo info) { - this(context, pm, info, null /* summary */, true /* enabled */); + public DefaultAppInfo(Context context, PackageManager pm, int uid, PackageItemInfo info) { + this(context, pm, uid, info, null /* summary */, true /* enabled */); } - public DefaultAppInfo(Context context, PackageManagerWrapper pm, int uid, ComponentName cn, + public DefaultAppInfo(Context context, PackageManager pm, int uid, ComponentName cn, String summary, boolean enabled) { super(enabled); mContext = context; @@ -62,12 +60,12 @@ public class DefaultAppInfo extends CandidateInfo { this.summary = summary; } - public DefaultAppInfo(Context context, PackageManagerWrapper pm, PackageItemInfo info, + public DefaultAppInfo(Context context, PackageManager pm, int uid, PackageItemInfo info, String summary, boolean enabled) { super(enabled); mContext = context; mPm = pm; - userId = UserHandle.myUserId(); + userId = uid; packageItemInfo = info; componentName = null; this.summary = summary; @@ -79,17 +77,17 @@ public class DefaultAppInfo extends CandidateInfo { try { final ComponentInfo componentInfo = getComponentInfo(); if (componentInfo != null) { - return componentInfo.loadLabel(mPm.getPackageManager()); + return componentInfo.loadLabel(mPm); } else { final ApplicationInfo appInfo = mPm.getApplicationInfoAsUser( componentName.getPackageName(), 0, userId); - return appInfo.loadLabel(mPm.getPackageManager()); + return appInfo.loadLabel(mPm); } } catch (PackageManager.NameNotFoundException e) { return null; } } else if (packageItemInfo != null) { - return packageItemInfo.loadLabel(mPm.getPackageManager()); + return packageItemInfo.loadLabel(mPm); } else { return null; } diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/ServiceListing.java b/packages/SettingsLib/src/com/android/settingslib/applications/ServiceListing.java index 3c3c70ac364e..454d1dce0b2f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/ServiceListing.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/ServiceListing.java @@ -32,8 +32,6 @@ import android.os.Handler; import android.provider.Settings; import android.util.Slog; -import com.android.settingslib.wrapper.PackageManagerWrapper; - import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -127,8 +125,7 @@ public class ServiceListing { mServices.clear(); final int user = ActivityManager.getCurrentUser(); - final PackageManagerWrapper pmWrapper = - new PackageManagerWrapper(mContext.getPackageManager()); + final PackageManager pmWrapper = mContext.getPackageManager(); List<ResolveInfo> installedServices = pmWrapper.queryIntentServicesAsUser( new Intent(mIntentAction), PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/StorageStatsSource.java b/packages/SettingsLib/src/com/android/settingslib/applications/StorageStatsSource.java index b15f35ddc240..b457406dda03 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/StorageStatsSource.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/StorageStatsSource.java @@ -21,6 +21,7 @@ import android.app.usage.StorageStatsManager; import android.content.Context; import android.content.pm.PackageManager; import android.os.UserHandle; + import androidx.annotation.VisibleForTesting; import java.io.IOException; diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java index 3c5816096f8c..58feef55bd29 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java @@ -42,7 +42,6 @@ public class A2dpProfile implements LocalBluetoothProfile { private BluetoothA2dp mService; private boolean mIsProfileReady; - private final LocalBluetoothAdapter mLocalAdapter; private final CachedBluetoothDeviceManager mDeviceManager; static final ParcelUuid[] SINK_UUIDS = { @@ -71,7 +70,7 @@ public class A2dpProfile implements LocalBluetoothProfile { // we may add a new device here, but generally this should not happen if (device == null) { Log.w(TAG, "A2dpProfile found new device: " + nextDevice); - device = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, nextDevice); + device = mDeviceManager.addDevice(nextDevice); } device.onProfileStateChanged(A2dpProfile.this, BluetoothProfile.STATE_CONNECTED); device.refresh(); @@ -94,14 +93,12 @@ public class A2dpProfile implements LocalBluetoothProfile { return BluetoothProfile.A2DP; } - A2dpProfile(Context context, LocalBluetoothAdapter adapter, - CachedBluetoothDeviceManager deviceManager, + A2dpProfile(Context context, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager) { mContext = context; - mLocalAdapter = adapter; mDeviceManager = deviceManager; mProfileManager = profileManager; - mLocalAdapter.getProfileProxy(context, new A2dpServiceListener(), + BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new A2dpServiceListener(), BluetoothProfile.A2DP); } @@ -296,7 +293,7 @@ public class A2dpProfile implements LocalBluetoothProfile { return R.string.bluetooth_a2dp_profile_summary_connected; default: - return Utils.getConnectionStateSummary(state); + return BluetoothUtils.getConnectionStateSummary(state); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpSinkProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpSinkProfile.java index 656f23ffb70c..988062de0a37 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpSinkProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpSinkProfile.java @@ -37,7 +37,6 @@ final class A2dpSinkProfile implements LocalBluetoothProfile { private BluetoothA2dpSink mService; private boolean mIsProfileReady; - private final LocalBluetoothAdapter mLocalAdapter; private final CachedBluetoothDeviceManager mDeviceManager; static final ParcelUuid[] SRC_UUIDS = { @@ -56,7 +55,7 @@ final class A2dpSinkProfile implements LocalBluetoothProfile { implements BluetoothProfile.ServiceListener { public void onServiceConnected(int profile, BluetoothProfile proxy) { - Log.d(TAG, "Bluetooth service connected, profile:" + profile); + Log.d(TAG, "Bluetooth service connected"); mService = (BluetoothA2dpSink) proxy; // We just bound to the service, so refresh the UI for any connected A2DP devices. List<BluetoothDevice> deviceList = mService.getConnectedDevices(); @@ -66,7 +65,7 @@ final class A2dpSinkProfile implements LocalBluetoothProfile { // we may add a new device here, but generally this should not happen if (device == null) { Log.w(TAG, "A2dpSinkProfile found new device: " + nextDevice); - device = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, nextDevice); + device = mDeviceManager.addDevice(nextDevice); } device.onProfileStateChanged(A2dpSinkProfile.this, BluetoothProfile.STATE_CONNECTED); device.refresh(); @@ -75,7 +74,7 @@ final class A2dpSinkProfile implements LocalBluetoothProfile { } public void onServiceDisconnected(int profile) { - Log.d(TAG, "Bluetooth service disconnected, profile:" + profile); + Log.d(TAG, "Bluetooth service disconnected"); mIsProfileReady=false; } } @@ -89,13 +88,11 @@ final class A2dpSinkProfile implements LocalBluetoothProfile { return BluetoothProfile.A2DP_SINK; } - A2dpSinkProfile(Context context, LocalBluetoothAdapter adapter, - CachedBluetoothDeviceManager deviceManager, + A2dpSinkProfile(Context context, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager) { - mLocalAdapter = adapter; mDeviceManager = deviceManager; mProfileManager = profileManager; - mLocalAdapter.getProfileProxy(context, new A2dpSinkServiceListener(), + BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new A2dpSinkServiceListener(), BluetoothProfile.A2DP_SINK); } @@ -205,7 +202,7 @@ final class A2dpSinkProfile implements LocalBluetoothProfile { return R.string.bluetooth_a2dp_profile_summary_connected; default: - return Utils.getConnectionStateSummary(state); + return BluetoothUtils.getConnectionStateSummary(state); } } @@ -218,7 +215,7 @@ final class A2dpSinkProfile implements LocalBluetoothProfile { if (mService != null) { try { BluetoothAdapter.getDefaultAdapter().closeProfileProxy(BluetoothProfile.A2DP_SINK, - mService); + mService); mService = null; }catch (Throwable t) { Log.w(TAG, "Error cleaning up A2DP proxy", t); diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java index c7f8c205ec6d..3152e65d5a36 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothCallback.java @@ -22,15 +22,122 @@ package com.android.settingslib.bluetooth; * UI to receive events from {@link BluetoothEventManager}. */ public interface BluetoothCallback { - void onBluetoothStateChanged(int bluetoothState); - void onScanningStateChanged(boolean started); - void onDeviceAdded(CachedBluetoothDevice cachedDevice); - void onDeviceDeleted(CachedBluetoothDevice cachedDevice); - void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState); - void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state); - void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile); - void onAudioModeChanged(); + /** + * It will be called when the state of the local Bluetooth adapter has been changed. + * It is listening {@link android.bluetooth.BluetoothAdapter#ACTION_STATE_CHANGED}. + * For example, Bluetooth has been turned on or off. + * + * @param bluetoothState the current Bluetooth state, the possible values are: + * {@link android.bluetooth.BluetoothAdapter#STATE_OFF}, + * {@link android.bluetooth.BluetoothAdapter#STATE_TURNING_ON}, + * {@link android.bluetooth.BluetoothAdapter#STATE_ON}, + * {@link android.bluetooth.BluetoothAdapter#STATE_TURNING_OFF}. + */ + default void onBluetoothStateChanged(int bluetoothState) {} + + /** + * It will be called when the local Bluetooth adapter has started + * or finished the remote device discovery process. + * It is listening {@link android.bluetooth.BluetoothAdapter#ACTION_DISCOVERY_STARTED} and + * {@link android.bluetooth.BluetoothAdapter#ACTION_DISCOVERY_FINISHED}. + * + * @param started indicate the current process is started or finished. + */ + default void onScanningStateChanged(boolean started) {} + + /** + * It will be called in following situations: + * 1. In scanning mode, when a new device has been found. + * 2. When a profile service is connected and existing connected devices has been found. + * This API only invoked once for each device and all devices will be cached in + * {@link CachedBluetoothDeviceManager}. + * + * @param cachedDevice the Bluetooth device. + */ + default void onDeviceAdded(CachedBluetoothDevice cachedDevice) {} + + /** + * It will be called when requiring to remove a remote device from CachedBluetoothDevice list + * + * @param cachedDevice the Bluetooth device. + */ + default void onDeviceDeleted(CachedBluetoothDevice cachedDevice) {} + + /** + * It will be called when bond state of a remote device is changed. + * It is listening {@link android.bluetooth.BluetoothDevice#ACTION_BOND_STATE_CHANGED} + * + * @param cachedDevice the Bluetooth device. + * @param bondState the Bluetooth device bond state, the possible values are: + * {@link android.bluetooth.BluetoothDevice#BOND_NONE}, + * {@link android.bluetooth.BluetoothDevice#BOND_BONDING}, + * {@link android.bluetooth.BluetoothDevice#BOND_BONDED}. + */ + default void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {} + + /** + * It will be called in following situations: + * 1. When the adapter is not connected to any profiles of any remote devices + * and it attempts a connection to a profile. + * 2. When the adapter disconnects from the last profile of the last device. + * It is listening {@link android.bluetooth.BluetoothAdapter#ACTION_CONNECTION_STATE_CHANGED} + * + * @param cachedDevice the Bluetooth device. + * @param state the Bluetooth device connection state, the possible values are: + * {@link android.bluetooth.BluetoothAdapter#STATE_DISCONNECTED}, + * {@link android.bluetooth.BluetoothAdapter#STATE_CONNECTING}, + * {@link android.bluetooth.BluetoothAdapter#STATE_CONNECTED}, + * {@link android.bluetooth.BluetoothAdapter#STATE_DISCONNECTING}. + */ + default void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) {} + + /** + * It will be called when device been set as active for {@code bluetoothProfile} + * It is listening in following intent: + * {@link android.bluetooth.BluetoothA2dp#ACTION_ACTIVE_DEVICE_CHANGED} + * {@link android.bluetooth.BluetoothHeadset#ACTION_ACTIVE_DEVICE_CHANGED} + * {@link android.bluetooth.BluetoothHearingAid#ACTION_ACTIVE_DEVICE_CHANGED} + * + * @param activeDevice the active Bluetooth device. + * @param bluetoothProfile the profile of active Bluetooth device. + */ + default void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) {} + + /** + * It will be called in following situations: + * 1. When the call state on the device is changed. + * 2. When the audio connection state of the A2DP profile is changed. + * It is listening in following intent: + * {@link android.bluetooth.BluetoothHeadset#ACTION_AUDIO_STATE_CHANGED} + * {@link android.telephony.TelephonyManager#ACTION_PHONE_STATE_CHANGED} + */ + default void onAudioModeChanged() {} + + /** + * It will be called when one of the bluetooth device profile connection state is changed. + * + * @param cachedDevice the active Bluetooth device. + * @param state the BluetoothProfile connection state, the possible values are: + * {@link android.bluetooth.BluetoothProfile#STATE_CONNECTED}, + * {@link android.bluetooth.BluetoothProfile#STATE_CONNECTING}, + * {@link android.bluetooth.BluetoothProfile#STATE_DISCONNECTED}, + * {@link android.bluetooth.BluetoothProfile#STATE_DISCONNECTING}. + * @param bluetoothProfile the BluetoothProfile id. + */ default void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state, int bluetoothProfile) { } + + /** + * Called when ACL connection state is changed. It listens to + * {@link android.bluetooth.BluetoothDevice#ACTION_ACL_CONNECTED} and {@link + * android.bluetooth.BluetoothDevice#ACTION_ACL_DISCONNECTED} + * + * @param cachedDevice Bluetooth device that changed + * @param state the Bluetooth device connection state, the possible values are: + * {@link android.bluetooth.BluetoothAdapter#STATE_DISCONNECTED}, + * {@link android.bluetooth.BluetoothAdapter#STATE_CONNECTED} + */ + default void onAclConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java index 9d8336f14f85..2b7babd06b47 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothEventManager.java @@ -18,7 +18,6 @@ package com.android.settingslib.bluetooth; import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothHearingAid; @@ -27,9 +26,13 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.UserHandle; import android.telephony.TelephonyManager; import android.util.Log; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + import com.android.settingslib.R; import java.util.ArrayList; @@ -49,43 +52,39 @@ public class BluetoothEventManager { private final LocalBluetoothAdapter mLocalAdapter; private final CachedBluetoothDeviceManager mDeviceManager; - private LocalBluetoothProfileManager mProfileManager; private final IntentFilter mAdapterIntentFilter, mProfileIntentFilter; private final Map<String, Handler> mHandlerMap; - private Context mContext; - - private final Collection<BluetoothCallback> mCallbacks = - new ArrayList<BluetoothCallback>(); - - private android.os.Handler mReceiverHandler; + private final BroadcastReceiver mBroadcastReceiver = new BluetoothBroadcastReceiver(); + private final BroadcastReceiver mProfileBroadcastReceiver = new BluetoothBroadcastReceiver(); + private final Collection<BluetoothCallback> mCallbacks = new ArrayList<>(); + private final android.os.Handler mReceiverHandler; + private final UserHandle mUserHandle; + private final Context mContext; interface Handler { void onReceive(Context context, Intent intent, BluetoothDevice device); } - private void addHandler(String action, Handler handler) { - mHandlerMap.put(action, handler); - mAdapterIntentFilter.addAction(action); - } - - void addProfileHandler(String action, Handler handler) { - mHandlerMap.put(action, handler); - mProfileIntentFilter.addAction(action); - } - - // Set profile manager after construction due to circular dependency - void setProfileManager(LocalBluetoothProfileManager manager) { - mProfileManager = manager; - } - + /** + * Creates BluetoothEventManager with the ability to pass in {@link UserHandle} that tells it to + * listen for bluetooth events for that particular userHandle. + * + * <p> If passing in userHandle that's different from the user running the process, + * {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission is required. If + * userHandle passed in is {@code null}, we register event receiver for the + * {@code context.getUser()} handle. + */ BluetoothEventManager(LocalBluetoothAdapter adapter, - CachedBluetoothDeviceManager deviceManager, Context context) { + CachedBluetoothDeviceManager deviceManager, Context context, + android.os.Handler handler, @Nullable UserHandle userHandle) { mLocalAdapter = adapter; mDeviceManager = deviceManager; mAdapterIntentFilter = new IntentFilter(); mProfileIntentFilter = new IntentFilter(); - mHandlerMap = new HashMap<String, Handler>(); + mHandlerMap = new HashMap<>(); mContext = context; + mUserHandle = userHandle; + mReceiverHandler = handler; // Bluetooth on/off broadcasts addHandler(BluetoothAdapter.ACTION_STATE_CHANGED, new AdapterStateChangedHandler()); @@ -108,16 +107,11 @@ public class BluetoothEventManager { addHandler(BluetoothDevice.ACTION_UUID, new UuidChangedHandler()); addHandler(BluetoothDevice.ACTION_BATTERY_LEVEL_CHANGED, new BatteryLevelChangedHandler()); - // Dock event broadcasts - addHandler(Intent.ACTION_DOCK_EVENT, new DockEventHandler()); - // Active device broadcasts - addHandler(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED, - new ActiveDeviceChangedHandler()); - addHandler(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED, - new ActiveDeviceChangedHandler()); + addHandler(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED, new ActiveDeviceChangedHandler()); + addHandler(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED, new ActiveDeviceChangedHandler()); addHandler(BluetoothHearingAid.ACTION_ACTIVE_DEVICE_CHANGED, - new ActiveDeviceChangedHandler()); + new ActiveDeviceChangedHandler()); // Headset state changed broadcasts addHandler(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED, @@ -125,20 +119,11 @@ public class BluetoothEventManager { addHandler(TelephonyManager.ACTION_PHONE_STATE_CHANGED, new AudioModeChangedHandler()); - mContext.registerReceiver(mBroadcastReceiver, mAdapterIntentFilter, null, mReceiverHandler); - mContext.registerReceiver(mProfileBroadcastReceiver, mProfileIntentFilter, null, mReceiverHandler); - } + // ACL connection changed broadcasts + addHandler(BluetoothDevice.ACTION_ACL_CONNECTED, new AclStateChangedHandler()); + addHandler(BluetoothDevice.ACTION_ACL_DISCONNECTED, new AclStateChangedHandler()); - void registerProfileIntentReceiver() { - mContext.registerReceiver(mProfileBroadcastReceiver, mProfileIntentFilter, null, mReceiverHandler); - } - - public void setReceiverHandler(android.os.Handler handler) { - mContext.unregisterReceiver(mBroadcastReceiver); - mContext.unregisterReceiver(mProfileBroadcastReceiver); - mReceiverHandler = handler; - mContext.registerReceiver(mBroadcastReceiver, mAdapterIntentFilter, null, mReceiverHandler); - registerProfileIntentReceiver(); + registerAdapterIntentReceiver(); } /** Register to start receiving callbacks for Bluetooth events. */ @@ -155,21 +140,122 @@ public class BluetoothEventManager { } } - private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - BluetoothDevice device = intent - .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + @VisibleForTesting + void registerProfileIntentReceiver() { + registerIntentReceiver(mProfileBroadcastReceiver, mProfileIntentFilter); + } - Handler handler = mHandlerMap.get(action); - if (handler != null) { - handler.onReceive(context, intent, device); + @VisibleForTesting + void registerAdapterIntentReceiver() { + registerIntentReceiver(mBroadcastReceiver, mAdapterIntentFilter); + } + + /** + * Registers the provided receiver to receive the broadcasts that correspond to the + * passed intent filter, in the context of the provided handler. + */ + private void registerIntentReceiver(BroadcastReceiver receiver, IntentFilter filter) { + if (mUserHandle == null) { + // If userHandle has not been provided, simply call registerReceiver. + mContext.registerReceiver(receiver, filter, null, mReceiverHandler); + } else { + // userHandle was explicitly specified, so need to call multi-user aware API. + mContext.registerReceiverAsUser(receiver, mUserHandle, filter, null, mReceiverHandler); + } + } + + @VisibleForTesting + void addProfileHandler(String action, Handler handler) { + mHandlerMap.put(action, handler); + mProfileIntentFilter.addAction(action); + } + + boolean readPairedDevices() { + Set<BluetoothDevice> bondedDevices = mLocalAdapter.getBondedDevices(); + if (bondedDevices == null) { + return false; + } + + boolean deviceAdded = false; + for (BluetoothDevice device : bondedDevices) { + CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device); + if (cachedDevice == null) { + mDeviceManager.addDevice(device); + deviceAdded = true; + } + } + + return deviceAdded; + } + + void dispatchDeviceAdded(CachedBluetoothDevice cachedDevice) { + synchronized (mCallbacks) { + for (BluetoothCallback callback : mCallbacks) { + callback.onDeviceAdded(cachedDevice); + } + } + } + + void dispatchDeviceRemoved(CachedBluetoothDevice cachedDevice) { + synchronized (mCallbacks) { + for (BluetoothCallback callback : mCallbacks) { + callback.onDeviceDeleted(cachedDevice); + } + } + } + + void dispatchProfileConnectionStateChanged(CachedBluetoothDevice device, int state, + int bluetoothProfile) { + synchronized (mCallbacks) { + for (BluetoothCallback callback : mCallbacks) { + callback.onProfileConnectionStateChanged(device, state, bluetoothProfile); + } + } + } + + private void dispatchConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { + synchronized (mCallbacks) { + for (BluetoothCallback callback : mCallbacks) { + callback.onConnectionStateChanged(cachedDevice, state); + } + } + } + + private void dispatchAudioModeChanged() { + mDeviceManager.dispatchAudioModeChanged(); + synchronized (mCallbacks) { + for (BluetoothCallback callback : mCallbacks) { + callback.onAudioModeChanged(); + } + } + } + + private void dispatchActiveDeviceChanged(CachedBluetoothDevice activeDevice, + int bluetoothProfile) { + mDeviceManager.onActiveDeviceChanged(activeDevice, bluetoothProfile); + synchronized (mCallbacks) { + for (BluetoothCallback callback : mCallbacks) { + callback.onActiveDeviceChanged(activeDevice, bluetoothProfile); } } - }; + } + + private void dispatchAclStateChanged(CachedBluetoothDevice activeDevice, + int state) { + synchronized (mCallbacks) { + for (BluetoothCallback callback : mCallbacks) { + callback.onAclConnectionStateChanged(activeDevice, state); + } + } + } + + @VisibleForTesting + void addHandler(String action, Handler handler) { + mHandlerMap.put(action, handler); + mAdapterIntentFilter.addAction(action); + } - private final BroadcastReceiver mProfileBroadcastReceiver = new BroadcastReceiver() { + private class BluetoothBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); @@ -181,19 +267,13 @@ public class BluetoothEventManager { handler.onReceive(context, intent, device); } } - }; + } private class AdapterStateChangedHandler implements Handler { public void onReceive(Context context, Intent intent, BluetoothDevice device) { int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, - BluetoothAdapter.ERROR); - // Reregister Profile Broadcast Receiver as part of TURN OFF - if (state == BluetoothAdapter.STATE_OFF) - { - context.unregisterReceiver(mProfileBroadcastReceiver); - registerProfileIntentReceiver(); - } + BluetoothAdapter.ERROR); // update local profiles and get paired devices mLocalAdapter.setBluetoothStateInt(state); // send callback to update UI and possibly start scanning @@ -228,19 +308,21 @@ public class BluetoothEventManager { public void onReceive(Context context, Intent intent, BluetoothDevice device) { short rssi = intent.getShortExtra(BluetoothDevice.EXTRA_RSSI, Short.MIN_VALUE); - BluetoothClass btClass = intent.getParcelableExtra(BluetoothDevice.EXTRA_CLASS); String name = intent.getStringExtra(BluetoothDevice.EXTRA_NAME); // TODO Pick up UUID. They should be available for 2.1 devices. // Skip for now, there's a bluez problem and we are not getting uuids even for 2.1. CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device); if (cachedDevice == null) { - cachedDevice = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, device); + cachedDevice = mDeviceManager.addDevice(device); Log.d(TAG, "DeviceFoundHandler created new CachedBluetoothDevice: " + cachedDevice); + } else if (cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED + &&!cachedDevice.getDevice().isConnected()) { + // Dispatch device add callback to show bonded but + // not connected devices in discovery mode + dispatchDeviceAdded(cachedDevice); } cachedDevice.setRssi(rssi); - cachedDevice.setBtClass(btClass); - cachedDevice.setNewName(name); cachedDevice.setJustDiscovered(true); } } @@ -255,30 +337,6 @@ public class BluetoothEventManager { } } - private void dispatchConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { - synchronized (mCallbacks) { - for (BluetoothCallback callback : mCallbacks) { - callback.onConnectionStateChanged(cachedDevice, state); - } - } - } - - void dispatchDeviceAdded(CachedBluetoothDevice cachedDevice) { - synchronized (mCallbacks) { - for (BluetoothCallback callback : mCallbacks) { - callback.onDeviceAdded(cachedDevice); - } - } - } - - void dispatchDeviceRemoved(CachedBluetoothDevice cachedDevice) { - synchronized (mCallbacks) { - for (BluetoothCallback callback : mCallbacks) { - callback.onDeviceDeleted(cachedDevice); - } - } - } - private class NameChangedHandler implements Handler { public void onReceive(Context context, Intent intent, BluetoothDevice device) { @@ -294,22 +352,12 @@ public class BluetoothEventManager { return; } int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, - BluetoothDevice.ERROR); + BluetoothDevice.ERROR); CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device); if (cachedDevice == null) { - Log.w(TAG, "CachedBluetoothDevice for device " + device + - " not found, calling readPairedDevices()."); - if (readPairedDevices()) { - cachedDevice = mDeviceManager.findDevice(device); - } - - if (cachedDevice == null) { - Log.w(TAG, "Got bonding state changed for " + device + - ", but we have no record of that device."); - - cachedDevice = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, device); - dispatchDeviceAdded(cachedDevice); - } + Log.w(TAG, "Got bonding state changed for " + device + + ", but we have no record of that device."); + cachedDevice = mDeviceManager.addDevice(device); } synchronized (mCallbacks) { @@ -341,26 +389,26 @@ public class BluetoothEventManager { int errorMsg; switch(reason) { - case BluetoothDevice.UNBOND_REASON_AUTH_FAILED: - errorMsg = R.string.bluetooth_pairing_pin_error_message; - break; - case BluetoothDevice.UNBOND_REASON_AUTH_REJECTED: - errorMsg = R.string.bluetooth_pairing_rejected_error_message; - break; - case BluetoothDevice.UNBOND_REASON_REMOTE_DEVICE_DOWN: - errorMsg = R.string.bluetooth_pairing_device_down_error_message; - break; - case BluetoothDevice.UNBOND_REASON_DISCOVERY_IN_PROGRESS: - case BluetoothDevice.UNBOND_REASON_AUTH_TIMEOUT: - case BluetoothDevice.UNBOND_REASON_REPEATED_ATTEMPTS: - case BluetoothDevice.UNBOND_REASON_REMOTE_AUTH_CANCELED: - errorMsg = R.string.bluetooth_pairing_error_message; - break; - default: - Log.w(TAG, "showUnbondMessage: Not displaying any message for reason: " + reason); - return; + case BluetoothDevice.UNBOND_REASON_AUTH_FAILED: + errorMsg = R.string.bluetooth_pairing_pin_error_message; + break; + case BluetoothDevice.UNBOND_REASON_AUTH_REJECTED: + errorMsg = R.string.bluetooth_pairing_rejected_error_message; + break; + case BluetoothDevice.UNBOND_REASON_REMOTE_DEVICE_DOWN: + errorMsg = R.string.bluetooth_pairing_device_down_error_message; + break; + case BluetoothDevice.UNBOND_REASON_DISCOVERY_IN_PROGRESS: + case BluetoothDevice.UNBOND_REASON_AUTH_TIMEOUT: + case BluetoothDevice.UNBOND_REASON_REPEATED_ATTEMPTS: + case BluetoothDevice.UNBOND_REASON_REMOTE_AUTH_CANCELED: + errorMsg = R.string.bluetooth_pairing_error_message; + break; + default: + Log.w(TAG, "showUnbondMessage: Not displaying any message for reason: " + reason); + return; } - Utils.showError(context, name, errorMsg); + BluetoothUtils.showError(context, name, errorMsg); } } @@ -378,22 +426,6 @@ public class BluetoothEventManager { } } - private class DockEventHandler implements Handler { - public void onReceive(Context context, Intent intent, BluetoothDevice device) { - // Remove if unpair device upon undocking - int anythingButUnDocked = Intent.EXTRA_DOCK_STATE_UNDOCKED + 1; - int state = intent.getIntExtra(Intent.EXTRA_DOCK_STATE, anythingButUnDocked); - if (state == Intent.EXTRA_DOCK_STATE_UNDOCKED) { - if (device != null && device.getBondState() == BluetoothDevice.BOND_NONE) { - CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device); - if (cachedDevice != null) { - cachedDevice.setJustDiscovered(false); - } - } - } - } - } - private class BatteryLevelChangedHandler implements Handler { public void onReceive(Context context, Intent intent, BluetoothDevice device) { @@ -404,25 +436,6 @@ public class BluetoothEventManager { } } - boolean readPairedDevices() { - Set<BluetoothDevice> bondedDevices = mLocalAdapter.getBondedDevices(); - if (bondedDevices == null) { - return false; - } - - boolean deviceAdded = false; - for (BluetoothDevice device : bondedDevices) { - CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device); - if (cachedDevice == null) { - cachedDevice = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, device); - dispatchDeviceAdded(cachedDevice); - deviceAdded = true; - } - } - - return deviceAdded; - } - private class ActiveDeviceChangedHandler implements Handler { @Override public void onReceive(Context context, Intent intent, BluetoothDevice device) { @@ -447,13 +460,29 @@ public class BluetoothEventManager { } } - private void dispatchActiveDeviceChanged(CachedBluetoothDevice activeDevice, - int bluetoothProfile) { - mDeviceManager.onActiveDeviceChanged(activeDevice, bluetoothProfile); - synchronized (mCallbacks) { - for (BluetoothCallback callback : mCallbacks) { - callback.onActiveDeviceChanged(activeDevice, bluetoothProfile); + private class AclStateChangedHandler implements Handler { + @Override + public void onReceive(Context context, Intent intent, BluetoothDevice device) { + final String action = intent.getAction(); + if (action == null) { + Log.w(TAG, "AclStateChangedHandler: action is null"); + return; + } + final CachedBluetoothDevice activeDevice = mDeviceManager.findDevice(device); + final int state; + switch (action) { + case BluetoothDevice.ACTION_ACL_CONNECTED: + state = BluetoothAdapter.STATE_CONNECTED; + break; + case BluetoothDevice.ACTION_ACL_DISCONNECTED: + state = BluetoothAdapter.STATE_DISCONNECTED; + break; + default: + Log.w(TAG, "ActiveDeviceChangedHandler: unknown action " + action); + return; + } + dispatchAclStateChanged(activeDevice, state); } } @@ -469,23 +498,4 @@ public class BluetoothEventManager { dispatchAudioModeChanged(); } } - - private void dispatchAudioModeChanged() { - mDeviceManager.dispatchAudioModeChanged(); - synchronized (mCallbacks) { - for (BluetoothCallback callback : mCallbacks) { - callback.onAudioModeChanged(); - } - } - } - - void dispatchProfileConnectionStateChanged(CachedBluetoothDevice device, int state, - int bluetoothProfile) { - synchronized (mCallbacks) { - for (BluetoothCallback callback : mCallbacks) { - callback.onProfileConnectionStateChanged(device, state, bluetoothProfile); - } - } - mDeviceManager.onProfileConnectionStateChanged(device, state, bluetoothProfile); - } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/Utils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java index 26f3ab69ba37..ee80aa159ab1 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/Utils.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java @@ -5,15 +5,16 @@ import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.content.Context; import android.graphics.drawable.Drawable; -import androidx.annotation.DrawableRes; import android.util.Pair; +import androidx.annotation.DrawableRes; + import com.android.settingslib.R; import com.android.settingslib.graph.BluetoothDeviceLayerDrawable; import java.util.List; -public class Utils { +public class BluetoothUtils { public static final boolean V = false; // verbose logging public static final boolean D = true; // regular logging @@ -21,16 +22,16 @@ public class Utils { public static int getConnectionStateSummary(int connectionState) { switch (connectionState) { - case BluetoothProfile.STATE_CONNECTED: - return R.string.bluetooth_connected; - case BluetoothProfile.STATE_CONNECTING: - return R.string.bluetooth_connecting; - case BluetoothProfile.STATE_DISCONNECTED: - return R.string.bluetooth_disconnected; - case BluetoothProfile.STATE_DISCONNECTING: - return R.string.bluetooth_disconnecting; - default: - return 0; + case BluetoothProfile.STATE_CONNECTED: + return R.string.bluetooth_connected; + case BluetoothProfile.STATE_CONNECTING: + return R.string.bluetooth_connecting; + case BluetoothProfile.STATE_DISCONNECTED: + return R.string.bluetooth_disconnected; + case BluetoothProfile.STATE_DISCONNECTING: + return R.string.bluetooth_disconnecting; + default: + return 0; } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index 2aabb8a533e6..d6c64913f048 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -16,6 +16,7 @@ package com.android.settingslib.bluetooth; +import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHearingAid; @@ -23,12 +24,11 @@ import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.content.Context; import android.content.SharedPreferences; -import android.media.AudioManager; import android.os.ParcelUuid; import android.os.SystemClock; import android.text.TextUtils; import android.util.Log; -import android.bluetooth.BluetoothAdapter; + import androidx.annotation.VisibleForTesting; import com.android.settingslib.R; @@ -36,7 +36,6 @@ import com.android.settingslib.R; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.List; /** @@ -47,69 +46,42 @@ import java.util.List; */ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> { private static final String TAG = "CachedBluetoothDevice"; - private static final boolean DEBUG = Utils.V; + + // See mConnectAttempted + private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000; + private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000; private final Context mContext; - private final LocalBluetoothAdapter mLocalAdapter; + private final BluetoothAdapter mLocalAdapter; private final LocalBluetoothProfileManager mProfileManager; - private final AudioManager mAudioManager; - private final BluetoothDevice mDevice; - //TODO: consider remove, BluetoothDevice.getName() is already cached - private String mName; + BluetoothDevice mDevice; private long mHiSyncId; // Need this since there is no method for getting RSSI - private short mRssi; - //TODO: consider remove, BluetoothDevice.getBluetoothClass() is already cached - private BluetoothClass mBtClass; - private HashMap<LocalBluetoothProfile, Integer> mProfileConnectionState; - + short mRssi; + // mProfiles and mRemovedProfiles does not do swap() between main and sub device. It is + // because current sub device is only for HearingAid and its profile is the same. private final List<LocalBluetoothProfile> mProfiles = - new ArrayList<LocalBluetoothProfile>(); + Collections.synchronizedList(new ArrayList<>()); // List of profiles that were previously in mProfiles, but have been removed private final List<LocalBluetoothProfile> mRemovedProfiles = - new ArrayList<LocalBluetoothProfile>(); + Collections.synchronizedList(new ArrayList<>()); // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP private boolean mLocalNapRoleConnected; - private boolean mJustDiscovered; + boolean mJustDiscovered; private int mMessageRejectionCount; private final Collection<Callback> mCallbacks = new ArrayList<Callback>(); - // Following constants indicate the user's choices of Phone book/message access settings - // User hasn't made any choice or settings app has wiped out the memory - public final static int ACCESS_UNKNOWN = 0; - // User has accepted the connection and let Settings app remember the decision - public final static int ACCESS_ALLOWED = 1; - // User has rejected the connection and let Settings app remember the decision - public final static int ACCESS_REJECTED = 2; - // How many times user should reject the connection to make the choice persist. private final static int MESSAGE_REJECTION_COUNT_LIMIT_TO_PERSIST = 2; private final static String MESSAGE_REJECTION_COUNT_PREFS_NAME = "bluetooth_message_reject"; /** - * When we connect to multiple profiles, we only want to display a single - * error even if they all fail. This tracks that state. - */ - private boolean mIsConnectingErrorPossible; - - public long getHiSyncId() { - return mHiSyncId; - } - - public void setHiSyncId(long id) { - if (Utils.D) { - Log.d(TAG, "setHiSyncId: mDevice " + mDevice + ", id " + id); - } - mHiSyncId = id; - } - - /** * Last time a bt profile auto-connect was attempted. * If an ACTION_UUID intent comes in within * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect @@ -117,14 +89,23 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> */ private long mConnectAttempted; - // See mConnectAttempted - private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000; - private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000; - // Active device state private boolean mIsActiveDeviceA2dp = false; private boolean mIsActiveDeviceHeadset = false; private boolean mIsActiveDeviceHearingAid = false; + // Group second device for Hearing Aid + private CachedBluetoothDevice mSubDevice; + + CachedBluetoothDevice(Context context, LocalBluetoothProfileManager profileManager, + BluetoothDevice device) { + mContext = context; + mLocalAdapter = BluetoothAdapter.getDefaultAdapter(); + mProfileManager = profileManager; + mDevice = device; + fillData(); + mHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID; + } + /** * Describes the current device and profile for logging. * @@ -142,16 +123,17 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } void onProfileStateChanged(LocalBluetoothProfile profile, int newProfileState) { - if (Utils.D) { - Log.d(TAG, "onProfileStateChanged: profile " + profile + ", device=" + mDevice - + ", newProfileState " + newProfileState); + if (BluetoothUtils.D) { + Log.d(TAG, "onProfileStateChanged: profile " + profile + + " newProfileState " + newProfileState); } - if (mLocalAdapter.getBluetoothState() == BluetoothAdapter.STATE_TURNING_OFF) + if (mLocalAdapter.getState() == BluetoothAdapter.STATE_TURNING_OFF) { - if (Utils.D) Log.d(TAG, " BT Turninig Off...Profile conn state change ignored..."); + if (BluetoothUtils.D) { + Log.d(TAG, " BT Turninig Off...Profile conn state change ignored..."); + } return; } - mProfileConnectionState.put(profile, newProfileState); if (newProfileState == BluetoothProfile.STATE_CONNECTED) { if (profile instanceof MapProfile) { profile.setPreferred(mDevice, true); @@ -179,20 +161,6 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> fetchActiveDevices(); } - CachedBluetoothDevice(Context context, - LocalBluetoothAdapter adapter, - LocalBluetoothProfileManager profileManager, - BluetoothDevice device) { - mContext = context; - mLocalAdapter = adapter; - mProfileManager = profileManager; - mAudioManager = context.getSystemService(AudioManager.class); - mDevice = device; - mProfileConnectionState = new HashMap<LocalBluetoothProfile, Integer>(); - fillData(); - mHiSyncId = BluetoothHearingAid.HI_SYNC_ID_INVALID; - } - public void disconnect() { for (LocalBluetoothProfile profile : mProfiles) { disconnect(profile); @@ -209,7 +177,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> public void disconnect(LocalBluetoothProfile profile) { if (profile.disconnect(mDevice)) { - if (Utils.D) { + if (BluetoothUtils.D) { Log.d(TAG, "Command sent successfully:DISCONNECT " + describe(profile)); } } @@ -224,8 +192,15 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> connectWithoutResettingTimer(connectAllProfiles); } - public boolean isHearingAidDevice() { - return mHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID; + public long getHiSyncId() { + return mHiSyncId; + } + + public void setHiSyncId(long id) { + if (BluetoothUtils.D) { + Log.d(TAG, "setHiSyncId: mDevice " + mDevice + ", id " + id); + } + mHiSyncId = id; } void onBondingDockConnect() { @@ -248,9 +223,6 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> return; } - // Reset the only-show-one-error-dialog tracking variable - mIsConnectingErrorPossible = true; - int preferredProfiles = 0; for (LocalBluetoothProfile profile : mProfiles) { if (connectAllProfiles ? profile.accessProfileEnabled() : profile.isAutoConnectable()) { @@ -260,7 +232,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } } } - if (DEBUG) Log.d(TAG, "Preferred profiles = " + preferredProfiles); + if (BluetoothUtils.D) Log.d(TAG, "Preferred profiles = " + preferredProfiles); if (preferredProfiles == 0) { connectAutoConnectableProfiles(); @@ -271,8 +243,6 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> if (!ensurePaired()) { return; } - // Reset the only-show-one-error-dialog tracking variable - mIsConnectingErrorPossible = true; for (LocalBluetoothProfile profile : mProfiles) { if (profile.isAutoConnectable()) { @@ -289,8 +259,6 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> */ public void connectProfile(LocalBluetoothProfile profile) { mConnectAttempted = SystemClock.elapsedRealtime(); - // Reset the only-show-one-error-dialog tracking variable - mIsConnectingErrorPossible = true; connectInt(profile); // Refresh the UI based on profile.connect() call refresh(); @@ -301,12 +269,12 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> return; } if (profile.connect(mDevice)) { - if (Utils.D) { + if (BluetoothUtils.D) { Log.d(TAG, "Command sent successfully:CONNECT " + describe(profile)); } return; } - Log.i(TAG, "Failed to connect " + profile.toString() + " to " + mName); + Log.i(TAG, "Failed to connect " + profile.toString() + " to " + getName()); } private boolean ensurePaired() { @@ -331,14 +299,6 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> return true; } - /** - * Return true if user initiated pairing on this device. The message text is - * slightly different for local vs. remote initiated pairing dialogs. - */ - boolean isUserInitiatedPairing() { - return mDevice.isBondingInitiatedLocally(); - } - public void unpair() { int state = getBondState(); @@ -351,10 +311,10 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> if (dev != null) { final boolean successful = dev.removeBond(); if (successful) { - if (Utils.D) { + if (BluetoothUtils.D) { Log.d(TAG, "Command sent successfully:REMOVE_BOND " + describe(null)); } - } else if (Utils.V) { + } else if (BluetoothUtils.V) { Log.v(TAG, "Framework rejected command immediately:REMOVE_BOND " + describe(null)); } @@ -363,28 +323,13 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } public int getProfileConnectionState(LocalBluetoothProfile profile) { - if (mProfileConnectionState.get(profile) == null) { - // If cache is empty make the binder call to get the state - int state = profile.getConnectionStatus(mDevice); - mProfileConnectionState.put(profile, state); - } - return mProfileConnectionState.get(profile); - } - - public void clearProfileConnectionState () - { - if (Utils.D) { - Log.d(TAG," Clearing all connection state for dev:" + mDevice.getName()); - } - for (LocalBluetoothProfile profile :getProfiles()) { - mProfileConnectionState.put(profile, BluetoothProfile.STATE_DISCONNECTED); - } + return profile != null + ? profile.getConnectionStatus(mDevice) + : BluetoothProfile.STATE_DISCONNECTED; } // TODO: do any of these need to run async on a background thread? private void fillData() { - fetchName(); - fetchBtClass(); updateProfiles(); fetchActiveDevices(); migratePhonebookPermissionChoice(); @@ -407,21 +352,15 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> return mDevice.getAddress(); } - public String getName() { - return mName; - } - /** - * Populate name from BluetoothDevice.ACTION_FOUND intent + * Get name from remote device + * @return {@link BluetoothDevice#getAliasName()} if + * {@link BluetoothDevice#getAliasName()} is not null otherwise return + * {@link BluetoothDevice#getAddress()} */ - void setNewName(String name) { - if (mName == null) { - mName = name; - if (mName == null || TextUtils.isEmpty(mName)) { - mName = mDevice.getAddress(); - } - dispatchAttributesChanged(); - } + public String getName() { + final String aliasName = mDevice.getAliasName(); + return TextUtils.isEmpty(aliasName) ? getAddress() : aliasName; } /** @@ -429,9 +368,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> * @param name new alias name to be set, should never be null */ public void setName(String name) { - // Prevent mName to be set to null if setName(null) is called - if (name != null && !TextUtils.equals(name, mName)) { - mName = name; + // Prevent getName() to be set to null if setName(null) is called + if (name != null && !TextUtils.equals(name, getName())) { mDevice.setAlias(name); dispatchAttributesChanged(); } @@ -468,17 +406,10 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } void refreshName() { - fetchName(); - dispatchAttributesChanged(); - } - - private void fetchName() { - mName = mDevice.getAliasName(); - - if (TextUtils.isEmpty(mName)) { - mName = mDevice.getAddress(); - if (DEBUG) Log.d(TAG, "Device has no name (yet), use address: " + mName); + if (BluetoothUtils.D) { + Log.d(TAG, "Device name: " + getName()); } + dispatchAttributesChanged(); } /** @@ -611,13 +542,6 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> return getBondState() == BluetoothDevice.BOND_BONDING; } - /** - * Fetches a new value for the cached BT class. - */ - private void fetchBtClass() { - mBtClass = mDevice.getBluetoothClass(); - } - private boolean updateProfiles() { ParcelUuid[] uuids = mDevice.getUuids(); if (uuids == null) return false; @@ -633,7 +557,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles, mLocalNapRoleConnected, mDevice); - if (DEBUG) { + if (BluetoothUtils.D) { Log.e(TAG, "updating profiles for " + mDevice.getAliasName()); BluetoothClass bluetoothClass = mDevice.getBluetoothClass(); @@ -662,15 +586,6 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } /** - * Refreshes the UI for the BT class, including fetching the latest value - * for the class. - */ - void refreshBtClass() { - fetchBtClass(); - dispatchAttributesChanged(); - } - - /** * Refreshes the UI when framework alerts us of a UUID change. */ void onUuidChanged() { @@ -682,7 +597,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> timeout = MAX_HOGP_DELAY_FOR_AUTO_CONNECT; } - if (DEBUG) { + if (BluetoothUtils.D) { Log.d(TAG, "onUuidChanged: Time since last connect" + (SystemClock.elapsedRealtime() - mConnectAttempted)); } @@ -702,9 +617,9 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> void onBondingStateChanged(int bondState) { if (bondState == BluetoothDevice.BOND_NONE) { mProfiles.clear(); - setPhonebookPermissionChoice(ACCESS_UNKNOWN); - setMessagePermissionChoice(ACCESS_UNKNOWN); - setSimPermissionChoice(ACCESS_UNKNOWN); + mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_UNKNOWN); + mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_UNKNOWN); + mDevice.setSimAccessPermission(BluetoothDevice.ACCESS_UNKNOWN); mMessageRejectionCount = 0; saveMessageRejectionCount(); } @@ -720,15 +635,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } } - void setBtClass(BluetoothClass btClass) { - if (btClass != null && mBtClass != btClass) { - mBtClass = btClass; - dispatchAttributesChanged(); - } - } - public BluetoothClass getBtClass() { - return mBtClass; + return mDevice.getBluetoothClass(); } public List<LocalBluetoothProfile> getProfiles() { @@ -762,7 +670,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } } - private void dispatchAttributesChanged() { + void dispatchAttributesChanged() { synchronized (mCallbacks) { for (Callback callback : mCallbacks) { callback.onDeviceAttributesChanged(); @@ -810,33 +718,13 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> if (comparison != 0) return comparison; // Fallback on name - return mName.compareTo(another.mName); + return getName().compareTo(another.getName()); } public interface Callback { void onDeviceAttributesChanged(); } - public int getPhonebookPermissionChoice() { - int permission = mDevice.getPhonebookAccessPermission(); - if (permission == BluetoothDevice.ACCESS_ALLOWED) { - return ACCESS_ALLOWED; - } else if (permission == BluetoothDevice.ACCESS_REJECTED) { - return ACCESS_REJECTED; - } - return ACCESS_UNKNOWN; - } - - public void setPhonebookPermissionChoice(int permissionChoice) { - int permission = BluetoothDevice.ACCESS_UNKNOWN; - if (permissionChoice == ACCESS_ALLOWED) { - permission = BluetoothDevice.ACCESS_ALLOWED; - } else if (permissionChoice == ACCESS_REJECTED) { - permission = BluetoothDevice.ACCESS_REJECTED; - } - mDevice.setPhonebookAccessPermission(permission); - } - // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth // app's shared preferences). private void migratePhonebookPermissionChoice() { @@ -847,10 +735,11 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { - int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN); - if (oldPermission == ACCESS_ALLOWED) { + int oldPermission = + preferences.getInt(mDevice.getAddress(), BluetoothDevice.ACCESS_UNKNOWN); + if (oldPermission == BluetoothDevice.ACCESS_ALLOWED) { mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); - } else if (oldPermission == ACCESS_REJECTED) { + } else if (oldPermission == BluetoothDevice.ACCESS_REJECTED) { mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); } } @@ -860,46 +749,6 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> editor.commit(); } - public int getMessagePermissionChoice() { - int permission = mDevice.getMessageAccessPermission(); - if (permission == BluetoothDevice.ACCESS_ALLOWED) { - return ACCESS_ALLOWED; - } else if (permission == BluetoothDevice.ACCESS_REJECTED) { - return ACCESS_REJECTED; - } - return ACCESS_UNKNOWN; - } - - public void setMessagePermissionChoice(int permissionChoice) { - int permission = BluetoothDevice.ACCESS_UNKNOWN; - if (permissionChoice == ACCESS_ALLOWED) { - permission = BluetoothDevice.ACCESS_ALLOWED; - } else if (permissionChoice == ACCESS_REJECTED) { - permission = BluetoothDevice.ACCESS_REJECTED; - } - mDevice.setMessageAccessPermission(permission); - } - - public int getSimPermissionChoice() { - int permission = mDevice.getSimAccessPermission(); - if (permission == BluetoothDevice.ACCESS_ALLOWED) { - return ACCESS_ALLOWED; - } else if (permission == BluetoothDevice.ACCESS_REJECTED) { - return ACCESS_REJECTED; - } - return ACCESS_UNKNOWN; - } - - void setSimPermissionChoice(int permissionChoice) { - int permission = BluetoothDevice.ACCESS_UNKNOWN; - if (permissionChoice == ACCESS_ALLOWED) { - permission = BluetoothDevice.ACCESS_ALLOWED; - } else if (permissionChoice == ACCESS_REJECTED) { - permission = BluetoothDevice.ACCESS_REJECTED; - } - mDevice.setSimAccessPermission(permission); - } - // Migrates data from old data store (in Settings app's shared preferences) to new (in Bluetooth // app's shared preferences). private void migrateMessagePermissionChoice() { @@ -910,10 +759,11 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } if (mDevice.getMessageAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { - int oldPermission = preferences.getInt(mDevice.getAddress(), ACCESS_UNKNOWN); - if (oldPermission == ACCESS_ALLOWED) { + int oldPermission = + preferences.getInt(mDevice.getAddress(), BluetoothDevice.ACCESS_UNKNOWN); + if (oldPermission == BluetoothDevice.ACCESS_ALLOWED) { mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_ALLOWED); - } else if (oldPermission == ACCESS_REJECTED) { + } else if (oldPermission == BluetoothDevice.ACCESS_REJECTED) { mDevice.setMessageAccessPermission(BluetoothDevice.ACCESS_REJECTED); } } @@ -958,14 +808,14 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> if (BluetoothUuid.containsAnyUuid(uuids, PbapServerProfile.PBAB_CLIENT_UUIDS)) { // The pairing dialog now warns of phone-book access for paired devices. // No separate prompt is displayed after pairing. - if (getPhonebookPermissionChoice() == CachedBluetoothDevice.ACCESS_UNKNOWN) { + if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_UNKNOWN) { if (mDevice.getBluetoothClass().getDeviceClass() == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE || mDevice.getBluetoothClass().getDeviceClass() == BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET) { - setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_ALLOWED); + mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); } else { - setPhonebookPermissionChoice(CachedBluetoothDevice.ACCESS_REJECTED); + mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); } } } @@ -999,7 +849,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> switch (connectionStatus) { case BluetoothProfile.STATE_CONNECTING: case BluetoothProfile.STATE_DISCONNECTING: - return mContext.getString(Utils.getConnectionStateSummary(connectionStatus)); + return mContext.getString( + BluetoothUtils.getConnectionStateSummary(connectionStatus)); case BluetoothProfile.STATE_CONNECTED: profileConnected = true; @@ -1041,7 +892,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> if (batteryLevelPercentageString != null) { //device is in phone call if (com.android.settingslib.Utils.isAudioModeOngoingCall(mContext)) { - if (mIsActiveDeviceHeadset) { + if (mIsActiveDeviceHearingAid || mIsActiveDeviceHeadset) { stringRes = R.string.bluetooth_active_battery_level; } else { stringRes = R.string.bluetooth_battery_level; @@ -1057,7 +908,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } else { //no battery information if (com.android.settingslib.Utils.isAudioModeOngoingCall(mContext)) { - if (mIsActiveDeviceHeadset) { + if (mIsActiveDeviceHearingAid || mIsActiveDeviceHeadset) { stringRes = R.string.bluetooth_active_no_battery_level; } } else { @@ -1094,7 +945,8 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> switch (connectionStatus) { case BluetoothProfile.STATE_CONNECTING: case BluetoothProfile.STATE_DISCONNECTING: - return mContext.getString(Utils.getConnectionStateSummary(connectionStatus)); + return mContext.getString( + BluetoothUtils.getConnectionStateSummary(connectionStatus)); case BluetoothProfile.STATE_CONNECTED: profileConnected = true; @@ -1192,7 +1044,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> /** * @return {@code true} if {@code cachedBluetoothDevice} is a2dp device */ - public boolean isA2dpDevice() { + public boolean isConnectedA2dpDevice() { A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); return a2dpProfile != null && a2dpProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED; @@ -1201,7 +1053,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> /** * @return {@code true} if {@code cachedBluetoothDevice} is HFP device */ - public boolean isHfpDevice() { + public boolean isConnectedHfpDevice() { HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile(); return headsetProfile != null && headsetProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED; @@ -1215,4 +1067,28 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> return hearingAidProfile != null && hearingAidProfile.getConnectionStatus(mDevice) == BluetoothProfile.STATE_CONNECTED; } + + public CachedBluetoothDevice getSubDevice() { + return mSubDevice; + } + + public void setSubDevice(CachedBluetoothDevice subDevice) { + mSubDevice = subDevice; + } + + public void switchSubDeviceContent() { + // Backup from main device + BluetoothDevice tmpDevice = mDevice; + short tmpRssi = mRssi; + boolean tmpJustDiscovered = mJustDiscovered; + // Set main device from sub device + mDevice = mSubDevice.mDevice; + mRssi = mSubDevice.mRssi; + mJustDiscovered = mSubDevice.mJustDiscovered; + // Set sub device from backup + mSubDevice.mDevice = tmpDevice; + mSubDevice.mRssi = tmpRssi; + mSubDevice.mJustDiscovered = tmpJustDiscovered; + fetchActiveDevices(); + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java index 1bfcf9ade8a5..f7f65893efb5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java @@ -18,8 +18,6 @@ package com.android.settingslib.bluetooth; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothHearingAid; -import android.bluetooth.BluetoothProfile; import android.content.Context; import android.util.Log; @@ -27,11 +25,7 @@ import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Set; import java.util.Objects; /** @@ -39,26 +33,20 @@ import java.util.Objects; */ public class CachedBluetoothDeviceManager { private static final String TAG = "CachedBluetoothDeviceManager"; - private static final boolean DEBUG = Utils.D; + private static final boolean DEBUG = BluetoothUtils.D; private Context mContext; private final LocalBluetoothManager mBtManager; @VisibleForTesting - final List<CachedBluetoothDevice> mCachedDevices = - new ArrayList<CachedBluetoothDevice>(); - // Contains the list of hearing aid devices that should not be shown in the UI. + final List<CachedBluetoothDevice> mCachedDevices = new ArrayList<CachedBluetoothDevice>(); @VisibleForTesting - final List<CachedBluetoothDevice> mHearingAidDevicesNotAddedInCache - = new ArrayList<CachedBluetoothDevice>(); - // Maintains a list of devices which are added in mCachedDevices and have hiSyncIds. - @VisibleForTesting - final Map<Long, CachedBluetoothDevice> mCachedDevicesMapForHearingAids - = new HashMap<Long, CachedBluetoothDevice>(); + HearingAidDeviceManager mHearingAidDeviceManager; CachedBluetoothDeviceManager(Context context, LocalBluetoothManager localBtManager) { mContext = context; mBtManager = localBtManager; + mHearingAidDeviceManager = new HearingAidDeviceManager(localBtManager, mCachedDevices); } public synchronized Collection<CachedBluetoothDevice> getCachedDevicesCopy() { @@ -92,12 +80,13 @@ public class CachedBluetoothDeviceManager { if (cachedDevice.getDevice().equals(device)) { return cachedDevice; } - } - for (CachedBluetoothDevice notCachedDevice : mHearingAidDevicesNotAddedInCache) { - if (notCachedDevice.getDevice().equals(device)) { - return notCachedDevice; + // Check sub devices if it exists + CachedBluetoothDevice subDevice = cachedDevice.getSubDevice(); + if (subDevice != null && subDevice.getDevice().equals(device)) { + return subDevice; } } + return null; } @@ -107,29 +96,14 @@ public class CachedBluetoothDeviceManager { * @param device the address of the new Bluetooth device * @return the newly created CachedBluetoothDevice object */ - public CachedBluetoothDevice addDevice(LocalBluetoothAdapter adapter, - LocalBluetoothProfileManager profileManager, - BluetoothDevice device) { - CachedBluetoothDevice newDevice = new CachedBluetoothDevice(mContext, adapter, - profileManager, device); - if (profileManager.getHearingAidProfile() != null - && profileManager.getHearingAidProfile().getHiSyncId(newDevice.getDevice()) - != BluetoothHearingAid.HI_SYNC_ID_INVALID) { - newDevice.setHiSyncId(profileManager.getHearingAidProfile() - .getHiSyncId(newDevice.getDevice())); - } - // Just add one of the hearing aids from a pair in the list that is shown in the UI. - if (isPairAddedInCache(newDevice.getHiSyncId())) { - synchronized (this) { - mHearingAidDevicesNotAddedInCache.add(newDevice); - } - } else { - synchronized (this) { + public CachedBluetoothDevice addDevice(BluetoothDevice device) { + LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); + CachedBluetoothDevice newDevice = new CachedBluetoothDevice(mContext, profileManager, + device); + mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(newDevice); + synchronized (this) { + if (!mHearingAidDeviceManager.setSubDeviceIfNeeded(newDevice)) { mCachedDevices.add(newDevice); - if (newDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID - && !mCachedDevicesMapForHearingAids.containsKey(newDevice.getHiSyncId())) { - mCachedDevicesMapForHearingAids.put(newDevice.getHiSyncId(), newDevice); - } mBtManager.getEventManager().dispatchDeviceAdded(newDevice); } } @@ -138,49 +112,18 @@ public class CachedBluetoothDeviceManager { } /** - * Returns true if the one of the two hearing aid devices is already cached for UI. - * - * @param long hiSyncId - * @return {@code True} if one of the two hearing aid devices is is already cached for UI. - */ - private synchronized boolean isPairAddedInCache(long hiSyncId) { - if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID) { - return false; - } - if(mCachedDevicesMapForHearingAids.containsKey(hiSyncId)) { - return true; - } - return false; - } - - /** * Returns device summary of the pair of the hearing aid passed as the parameter. * * @param CachedBluetoothDevice device - * @return Device summary, or if the pair does not exist or if its not a hearing aid, + * @return Device summary, or if the pair does not exist or if it is not a hearing aid, * then {@code null}. */ - public synchronized String getHearingAidPairDeviceSummary(CachedBluetoothDevice device) { - String pairDeviceSummary = null; - CachedBluetoothDevice otherHearingAidDevice = - getHearingAidOtherDevice(device, device.getHiSyncId()); - if (otherHearingAidDevice != null) { - pairDeviceSummary = otherHearingAidDevice.getConnectionSummary(); + public synchronized String getSubDeviceSummary(CachedBluetoothDevice device) { + CachedBluetoothDevice subDevice = device.getSubDevice(); + if (subDevice != null && subDevice.isConnected()) { + return subDevice.getConnectionSummary(); } - log("getHearingAidPairDeviceSummary: pairDeviceSummary=" + pairDeviceSummary - + ", otherHearingAidDevice=" + otherHearingAidDevice); - - return pairDeviceSummary; - } - - /** - * Adds the 2nd hearing aid in a pair in a list that maintains the hearing aids that are - * not dispalyed in the UI. - * - * @param CachedBluetoothDevice device - */ - public synchronized void addDeviceNotaddedInMap(CachedBluetoothDevice device) { - mHearingAidDevicesNotAddedInCache.add(device); + return null; } /** @@ -188,28 +131,8 @@ public class CachedBluetoothDeviceManager { * Hearing Aid Service is connected and the HiSyncId's are now available. * @param LocalBluetoothProfileManager profileManager */ - public synchronized void updateHearingAidsDevices(LocalBluetoothProfileManager profileManager) { - HearingAidProfile profileProxy = profileManager.getHearingAidProfile(); - if (profileProxy == null) { - log("updateHearingAidsDevices: getHearingAidProfile() is null"); - return; - } - final Set<Long> syncIdChangedSet = new HashSet<Long>(); - for (CachedBluetoothDevice cachedDevice : mCachedDevices) { - if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID) { - continue; - } - - long newHiSyncId = profileProxy.getHiSyncId(cachedDevice.getDevice()); - - if (newHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID) { - cachedDevice.setHiSyncId(newHiSyncId); - syncIdChangedSet.add(newHiSyncId); - } - } - for (Long syncId : syncIdChangedSet) { - onHiSyncIdChanged(syncId); - } + public synchronized void updateHearingAidsDevices() { + mHearingAidDeviceManager.updateHearingAidsDevices(); } /** @@ -233,15 +156,21 @@ public class CachedBluetoothDeviceManager { } public synchronized void clearNonBondedDevices() { - - mCachedDevicesMapForHearingAids.entrySet().removeIf(entries - -> entries.getValue().getBondState() == BluetoothDevice.BOND_NONE); - + clearNonBondedSubDevices(); mCachedDevices.removeIf(cachedDevice -> cachedDevice.getBondState() == BluetoothDevice.BOND_NONE); + } - mHearingAidDevicesNotAddedInCache.removeIf(hearingAidDevice - -> hearingAidDevice.getBondState() == BluetoothDevice.BOND_NONE); + private void clearNonBondedSubDevices() { + for (int i = mCachedDevices.size() - 1; i >= 0; i--) { + CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); + CachedBluetoothDevice subDevice = cachedDevice.getSubDevice(); + if (subDevice != null + && subDevice.getDevice().getBondState() == BluetoothDevice.BOND_NONE) { + // Sub device exists and it is not bonded + cachedDevice.setSubDevice(null); + } + } } public synchronized void onScanningStateChanged(boolean started) { @@ -251,17 +180,17 @@ public class CachedBluetoothDeviceManager { for (int i = mCachedDevices.size() - 1; i >= 0; i--) { CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); cachedDevice.setJustDiscovered(false); - } - for (int i = mHearingAidDevicesNotAddedInCache.size() - 1; i >= 0; i--) { - CachedBluetoothDevice notCachedDevice = mHearingAidDevicesNotAddedInCache.get(i); - notCachedDevice.setJustDiscovered(false); + final CachedBluetoothDevice subDevice = cachedDevice.getSubDevice(); + if (subDevice != null) { + subDevice.setJustDiscovered(false); + } } } public synchronized void onBtClassChanged(BluetoothDevice device) { CachedBluetoothDevice cachedDevice = findDevice(device); if (cachedDevice != null) { - cachedDevice.refreshBtClass(); + cachedDevice.dispatchAttributesChanged(); } } @@ -278,172 +207,45 @@ public class CachedBluetoothDeviceManager { if (bluetoothState == BluetoothAdapter.STATE_TURNING_OFF) { for (int i = mCachedDevices.size() - 1; i >= 0; i--) { CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); + CachedBluetoothDevice subDevice = cachedDevice.getSubDevice(); + if (subDevice != null) { + if (subDevice.getBondState() != BluetoothDevice.BOND_BONDED) { + cachedDevice.setSubDevice(null); + } + } if (cachedDevice.getBondState() != BluetoothDevice.BOND_BONDED) { cachedDevice.setJustDiscovered(false); mCachedDevices.remove(i); - if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID - && mCachedDevicesMapForHearingAids.containsKey(cachedDevice.getHiSyncId())) - { - mCachedDevicesMapForHearingAids.remove(cachedDevice.getHiSyncId()); - } - } else { - // For bonded devices, we need to clear the connection status so that - // when BT is enabled next time, device connection status shall be retrieved - // by making a binder call. - cachedDevice.clearProfileConnectionState(); - } - } - for (int i = mHearingAidDevicesNotAddedInCache.size() - 1; i >= 0; i--) { - CachedBluetoothDevice notCachedDevice = mHearingAidDevicesNotAddedInCache.get(i); - if (notCachedDevice.getBondState() != BluetoothDevice.BOND_BONDED) { - notCachedDevice.setJustDiscovered(false); - mHearingAidDevicesNotAddedInCache.remove(i); - } else { - // For bonded devices, we need to clear the connection status so that - // when BT is enabled next time, device connection status shall be retrieved - // by making a binder call. - notCachedDevice.clearProfileConnectionState(); } } } } public synchronized void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, - int bluetoothProfile) { + int bluetoothProfile) { for (CachedBluetoothDevice cachedDevice : mCachedDevices) { boolean isActive = Objects.equals(cachedDevice, activeDevice); cachedDevice.onActiveDeviceChanged(isActive, bluetoothProfile); } } - public synchronized void onHiSyncIdChanged(long hiSyncId) { - int firstMatchedIndex = -1; - - for (int i = mCachedDevices.size() - 1; i >= 0; i--) { - CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); - if (cachedDevice.getHiSyncId() == hiSyncId) { - if (firstMatchedIndex != -1) { - /* Found the second one */ - int indexToRemoveFromUi; - CachedBluetoothDevice deviceToRemoveFromUi; - - // Since the hiSyncIds have been updated for a connected pair of hearing aids, - // we remove the entry of one the hearing aids from the UI. Unless the - // hiSyncId get updated, the system does not know it is a hearing aid, so we add - // both the hearing aids as separate entries in the UI first, then remove one - // of them after the hiSyncId is populated. We will choose the device that - // is not connected to be removed. - if (cachedDevice.isConnected()) { - indexToRemoveFromUi = firstMatchedIndex; - deviceToRemoveFromUi = mCachedDevices.get(firstMatchedIndex); - mCachedDevicesMapForHearingAids.put(hiSyncId, cachedDevice); - } else { - indexToRemoveFromUi = i; - deviceToRemoveFromUi = cachedDevice; - mCachedDevicesMapForHearingAids.put(hiSyncId, - mCachedDevices.get(firstMatchedIndex)); - } - - mCachedDevices.remove(indexToRemoveFromUi); - mHearingAidDevicesNotAddedInCache.add(deviceToRemoveFromUi); - log("onHiSyncIdChanged: removed from UI device=" + deviceToRemoveFromUi - + ", with hiSyncId=" + hiSyncId); - mBtManager.getEventManager().dispatchDeviceRemoved(deviceToRemoveFromUi); - break; - } else { - mCachedDevicesMapForHearingAids.put(hiSyncId, cachedDevice); - firstMatchedIndex = i; - } - } - } - } - - public CachedBluetoothDevice getHearingAidOtherDevice(CachedBluetoothDevice thisDevice, - long hiSyncId) { - if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID) { - return null; - } - - // Searched the lists for the other side device with the matching hiSyncId. - for (CachedBluetoothDevice notCachedDevice : mHearingAidDevicesNotAddedInCache) { - if ((hiSyncId == notCachedDevice.getHiSyncId()) && - (!Objects.equals(notCachedDevice, thisDevice))) { - return notCachedDevice; - } - } - - CachedBluetoothDevice cachedDevice = mCachedDevicesMapForHearingAids.get(hiSyncId); - if (!Objects.equals(cachedDevice, thisDevice)) { - return cachedDevice; - } - return null; - } - - private void hearingAidSwitchDisplayDevice(CachedBluetoothDevice toDisplayDevice, - CachedBluetoothDevice toHideDevice, long hiSyncId) - { - log("hearingAidSwitchDisplayDevice: toDisplayDevice=" + toDisplayDevice - + ", toHideDevice=" + toHideDevice); - - // Remove the "toHideDevice" device from the UI. - mHearingAidDevicesNotAddedInCache.add(toHideDevice); - mCachedDevices.remove(toHideDevice); - mBtManager.getEventManager().dispatchDeviceRemoved(toHideDevice); - - // Add the "toDisplayDevice" device to the UI. - mHearingAidDevicesNotAddedInCache.remove(toDisplayDevice); - mCachedDevices.add(toDisplayDevice); - mCachedDevicesMapForHearingAids.put(hiSyncId, toDisplayDevice); - mBtManager.getEventManager().dispatchDeviceAdded(toDisplayDevice); - } - - public synchronized void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, - int state, int bluetoothProfile) { - if (bluetoothProfile == BluetoothProfile.HEARING_AID - && cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID - && cachedDevice.getBondState() == BluetoothDevice.BOND_BONDED) { - - long hiSyncId = cachedDevice.getHiSyncId(); - - CachedBluetoothDevice otherDevice = getHearingAidOtherDevice(cachedDevice, hiSyncId); - if (otherDevice == null) { - // no other side device. Nothing to do. - return; - } - - if (state == BluetoothProfile.STATE_CONNECTED && - mHearingAidDevicesNotAddedInCache.contains(cachedDevice)) { - hearingAidSwitchDisplayDevice(cachedDevice, otherDevice, hiSyncId); - } else if (state == BluetoothProfile.STATE_DISCONNECTED - && otherDevice.isConnected()) { - CachedBluetoothDevice mapDevice = mCachedDevicesMapForHearingAids.get(hiSyncId); - if ((mapDevice != null) && (Objects.equals(cachedDevice, mapDevice))) { - hearingAidSwitchDisplayDevice(otherDevice, cachedDevice, hiSyncId); - } - } - } + public synchronized boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice + cachedDevice, int state) { + return mHearingAidDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice, + state); } public synchronized void onDeviceUnpaired(CachedBluetoothDevice device) { - final long hiSyncId = device.getHiSyncId(); - - if (hiSyncId == BluetoothHearingAid.HI_SYNC_ID_INVALID) return; - - for (int i = mHearingAidDevicesNotAddedInCache.size() - 1; i >= 0; i--) { - CachedBluetoothDevice cachedDevice = mHearingAidDevicesNotAddedInCache.get(i); - if (cachedDevice.getHiSyncId() == hiSyncId) { - // TODO: Look for more cleanups on unpairing the device. - mHearingAidDevicesNotAddedInCache.remove(i); - if (device == cachedDevice) continue; - log("onDeviceUnpaired: Unpair device=" + cachedDevice); - cachedDevice.unpair(); - } - } - - CachedBluetoothDevice mappedDevice = mCachedDevicesMapForHearingAids.get(hiSyncId); - if ((mappedDevice != null) && (!Objects.equals(device, mappedDevice))) { - log("onDeviceUnpaired: Unpair mapped device=" + mappedDevice); - mappedDevice.unpair(); + CachedBluetoothDevice mainDevice = mHearingAidDeviceManager.findMainDevice(device); + CachedBluetoothDevice subDevice = device.getSubDevice(); + if (subDevice != null) { + // Main device is unpaired, to unpair sub device + subDevice.unpair(); + device.setSubDevice(null); + } else if (mainDevice != null) { + // Sub device unpaired, to unpair main device + mainDevice.unpair(); + mainDevice.setSubDevice(null); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HeadsetProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HeadsetProfile.java index 3bb8450207ab..62507f58426f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HeadsetProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HeadsetProfile.java @@ -40,7 +40,6 @@ public class HeadsetProfile implements LocalBluetoothProfile { private BluetoothHeadset mService; private boolean mIsProfileReady; - private final LocalBluetoothAdapter mLocalAdapter; private final CachedBluetoothDeviceManager mDeviceManager; private final LocalBluetoothProfileManager mProfileManager; @@ -69,7 +68,7 @@ public class HeadsetProfile implements LocalBluetoothProfile { // we may add a new device here, but generally this should not happen if (device == null) { Log.w(TAG, "HeadsetProfile found new device: " + nextDevice); - device = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, nextDevice); + device = mDeviceManager.addDevice(nextDevice); } device.onProfileStateChanged(HeadsetProfile.this, BluetoothProfile.STATE_CONNECTED); @@ -96,13 +95,11 @@ public class HeadsetProfile implements LocalBluetoothProfile { return BluetoothProfile.HEADSET; } - HeadsetProfile(Context context, LocalBluetoothAdapter adapter, - CachedBluetoothDeviceManager deviceManager, + HeadsetProfile(Context context, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager) { - mLocalAdapter = adapter; mDeviceManager = deviceManager; mProfileManager = profileManager; - mLocalAdapter.getProfileProxy(context, new HeadsetServiceListener(), + BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new HeadsetServiceListener(), BluetoothProfile.HEADSET); } @@ -226,7 +223,7 @@ public class HeadsetProfile implements LocalBluetoothProfile { return R.string.bluetooth_headset_profile_summary_connected; default: - return Utils.getConnectionStateSummary(state); + return BluetoothUtils.getConnectionStateSummary(state); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java new file mode 100644 index 000000000000..20ece69d7281 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java @@ -0,0 +1,223 @@ +/* + * 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.bluetooth; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHearingAid; +import android.bluetooth.BluetoothProfile; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * HearingAidDeviceManager manages the set of remote HearingAid Bluetooth devices. + */ +public class HearingAidDeviceManager { + private static final String TAG = "HearingAidDeviceManager"; + private static final boolean DEBUG = BluetoothUtils.D; + + private final LocalBluetoothManager mBtManager; + private final List<CachedBluetoothDevice> mCachedDevices; + HearingAidDeviceManager(LocalBluetoothManager localBtManager, + List<CachedBluetoothDevice> CachedDevices) { + mBtManager = localBtManager; + mCachedDevices = CachedDevices; + } + + void initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice) { + long hiSyncId = getHiSyncId(newDevice.getDevice()); + if (isValidHiSyncId(hiSyncId)) { + // Once hiSyncId is valid, assign hiSyncId + newDevice.setHiSyncId(hiSyncId); + } + } + + private long getHiSyncId(BluetoothDevice device) { + LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); + HearingAidProfile profileProxy = profileManager.getHearingAidProfile(); + if (profileProxy != null) { + return profileProxy.getHiSyncId(device); + } + return BluetoothHearingAid.HI_SYNC_ID_INVALID; + } + + boolean setSubDeviceIfNeeded(CachedBluetoothDevice newDevice) { + final long hiSyncId = newDevice.getHiSyncId(); + if (isValidHiSyncId(hiSyncId)) { + final CachedBluetoothDevice hearingAidDevice = getCachedDevice(hiSyncId); + // Just add one of the hearing aids from a pair in the list that is shown in the UI. + // Once there is another device with the same hiSyncId, to add new device as sub + // device. + if (hearingAidDevice != null) { + hearingAidDevice.setSubDevice(newDevice); + return true; + } + } + return false; + } + + private boolean isValidHiSyncId(long hiSyncId) { + return hiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID; + } + + private CachedBluetoothDevice getCachedDevice(long hiSyncId) { + for (int i = mCachedDevices.size() - 1; i >= 0; i--) { + CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); + if (cachedDevice.getHiSyncId() == hiSyncId) { + return cachedDevice; + } + } + return null; + } + + // To collect all HearingAid devices and call #onHiSyncIdChanged to group device by HiSyncId + void updateHearingAidsDevices() { + final Set<Long> newSyncIdSet = new HashSet<Long>(); + for (CachedBluetoothDevice cachedDevice : mCachedDevices) { + // Do nothing if HiSyncId has been assigned + if (!isValidHiSyncId(cachedDevice.getHiSyncId())) { + final long newHiSyncId = getHiSyncId(cachedDevice.getDevice()); + // Do nothing if there is no HiSyncId on Bluetooth device + if (isValidHiSyncId(newHiSyncId)) { + cachedDevice.setHiSyncId(newHiSyncId); + newSyncIdSet.add(newHiSyncId); + } + } + } + for (Long syncId : newSyncIdSet) { + onHiSyncIdChanged(syncId); + } + } + + // Group devices by hiSyncId + @VisibleForTesting + void onHiSyncIdChanged(long hiSyncId) { + int firstMatchedIndex = -1; + + for (int i = mCachedDevices.size() - 1; i >= 0; i--) { + CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); + if (cachedDevice.getHiSyncId() != hiSyncId) { + continue; + } + if (firstMatchedIndex == -1) { + // Found the first one + firstMatchedIndex = i; + continue; + } + // Found the second one + int indexToRemoveFromUi; + CachedBluetoothDevice subDevice; + CachedBluetoothDevice mainDevice; + // Since the hiSyncIds have been updated for a connected pair of hearing aids, + // we remove the entry of one the hearing aids from the UI. Unless the + // hiSyncId get updated, the system does not know it is a hearing aid, so we add + // both the hearing aids as separate entries in the UI first, then remove one + // of them after the hiSyncId is populated. We will choose the device that + // is not connected to be removed. + if (cachedDevice.isConnected()) { + mainDevice = cachedDevice; + indexToRemoveFromUi = firstMatchedIndex; + subDevice = mCachedDevices.get(firstMatchedIndex); + } else { + mainDevice = mCachedDevices.get(firstMatchedIndex); + indexToRemoveFromUi = i; + subDevice = cachedDevice; + } + + mainDevice.setSubDevice(subDevice); + mCachedDevices.remove(indexToRemoveFromUi); + log("onHiSyncIdChanged: removed from UI device =" + subDevice + + ", with hiSyncId=" + hiSyncId); + mBtManager.getEventManager().dispatchDeviceRemoved(subDevice); + break; + } + } + + // @return {@code true}, the event is processed inside the method. It is for updating + // hearing aid device on main-sub relationship when receiving connected or disconnected. + // @return {@code false}, it is not hearing aid device or to process it same as other profiles + boolean onProfileConnectionStateChangedIfProcessed(CachedBluetoothDevice cachedDevice, + int state) { + switch (state) { + case BluetoothProfile.STATE_CONNECTED: + onHiSyncIdChanged(cachedDevice.getHiSyncId()); + CachedBluetoothDevice mainDevice = findMainDevice(cachedDevice); + if (mainDevice != null){ + if (mainDevice.isConnected()) { + // When main device exists and in connected state, receiving sub device + // connection. To refresh main device UI + mainDevice.refresh(); + return true; + } else { + // When both Hearing Aid devices are disconnected, receiving sub device + // connection. To switch content and dispatch to notify UI change + mBtManager.getEventManager().dispatchDeviceRemoved(mainDevice); + mainDevice.switchSubDeviceContent(); + mainDevice.refresh(); + // It is necessary to do remove and add for updating the mapping on + // preference and device + mBtManager.getEventManager().dispatchDeviceAdded(mainDevice); + return true; + } + } + break; + case BluetoothProfile.STATE_DISCONNECTED: + mainDevice = findMainDevice(cachedDevice); + if (mainDevice != null) { + // When main device exists, receiving sub device disconnection + // To update main device UI + mainDevice.refresh(); + return true; + } + CachedBluetoothDevice subDevice = cachedDevice.getSubDevice(); + if (subDevice != null && subDevice.isConnected()) { + // Main device is disconnected and sub device is connected + // To copy data from sub device to main device + mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice); + cachedDevice.switchSubDeviceContent(); + cachedDevice.refresh(); + // It is necessary to do remove and add for updating the mapping on + // preference and device + mBtManager.getEventManager().dispatchDeviceAdded(cachedDevice); + return true; + } + break; + } + return false; + } + + CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) { + for (CachedBluetoothDevice cachedDevice : mCachedDevices) { + if (isValidHiSyncId(cachedDevice.getHiSyncId())) { + CachedBluetoothDevice subDevice = cachedDevice.getSubDevice(); + if (subDevice != null && subDevice.equals(device)) { + return cachedDevice; + } + } + } + return null; + } + + private void log(String msg) { + if (DEBUG) { + Log.d(TAG, msg); + } + } +}
\ No newline at end of file diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java index d4620de4bc6f..adb5ab34b5c1 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java @@ -16,23 +16,17 @@ package com.android.settingslib.bluetooth; -import android.bluetooth.BluetoothHearingAid; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; -import android.bluetooth.BluetoothCodecConfig; -import android.bluetooth.BluetoothCodecStatus; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHearingAid; import android.bluetooth.BluetoothProfile; -import android.bluetooth.BluetoothUuid; import android.content.Context; -import android.os.ParcelUuid; import android.util.Log; -import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.R; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; public class HearingAidProfile implements LocalBluetoothProfile { @@ -44,7 +38,6 @@ public class HearingAidProfile implements LocalBluetoothProfile { private BluetoothHearingAid mService; private boolean mIsProfileReady; - private final LocalBluetoothAdapter mLocalAdapter; private final CachedBluetoothDeviceManager mDeviceManager; static final String NAME = "HearingAid"; @@ -70,7 +63,7 @@ public class HearingAidProfile implements LocalBluetoothProfile { if (V) { Log.d(TAG, "HearingAidProfile found new device: " + nextDevice); } - device = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, nextDevice); + device = mDeviceManager.addDevice(nextDevice); } device.onProfileStateChanged(HearingAidProfile.this, BluetoothProfile.STATE_CONNECTED); @@ -78,7 +71,7 @@ public class HearingAidProfile implements LocalBluetoothProfile { } // Check current list of CachedDevices to see if any are Hearing Aid devices. - mDeviceManager.updateHearingAidsDevices(mProfileManager); + mDeviceManager.updateHearingAidsDevices(); mIsProfileReady=true; } @@ -98,15 +91,13 @@ public class HearingAidProfile implements LocalBluetoothProfile { return BluetoothProfile.HEARING_AID; } - HearingAidProfile(Context context, LocalBluetoothAdapter adapter, - CachedBluetoothDeviceManager deviceManager, + HearingAidProfile(Context context, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager) { mContext = context; - mLocalAdapter = adapter; mDeviceManager = deviceManager; mProfileManager = profileManager; - mLocalAdapter.getProfileProxy(context, new HearingAidServiceListener(), - BluetoothProfile.HEARING_AID); + BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, + new HearingAidServiceListener(), BluetoothProfile.HEARING_AID); } public boolean accessProfileEnabled() { @@ -234,7 +225,7 @@ public class HearingAidProfile implements LocalBluetoothProfile { return R.string.bluetooth_hearing_aid_profile_summary_connected; default: - return Utils.getConnectionStateSummary(state); + return BluetoothUtils.getConnectionStateSummary(state); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HfpClientProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HfpClientProfile.java index e975dea61a69..4879144a5994 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HfpClientProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HfpClientProfile.java @@ -16,10 +16,10 @@ package com.android.settingslib.bluetooth; -import android.bluetooth.BluetoothHeadsetClient; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHeadsetClient; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.content.Context; @@ -40,7 +40,6 @@ final class HfpClientProfile implements LocalBluetoothProfile { private BluetoothHeadsetClient mService; private boolean mIsProfileReady; - private final LocalBluetoothAdapter mLocalAdapter; private final CachedBluetoothDeviceManager mDeviceManager; static final ParcelUuid[] SRC_UUIDS = { @@ -60,7 +59,7 @@ final class HfpClientProfile implements LocalBluetoothProfile { @Override public void onServiceConnected(int profile, BluetoothProfile proxy) { - Log.d(TAG, "Bluetooth service connected, profile:" + profile); + Log.d(TAG, "Bluetooth service connected"); mService = (BluetoothHeadsetClient) proxy; // We just bound to the service, so refresh the UI for any connected HFP devices. List<BluetoothDevice> deviceList = mService.getConnectedDevices(); @@ -70,7 +69,7 @@ final class HfpClientProfile implements LocalBluetoothProfile { // we may add a new device here, but generally this should not happen if (device == null) { Log.w(TAG, "HfpClient profile found new device: " + nextDevice); - device = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, nextDevice); + device = mDeviceManager.addDevice(nextDevice); } device.onProfileStateChanged( HfpClientProfile.this, BluetoothProfile.STATE_CONNECTED); @@ -81,7 +80,7 @@ final class HfpClientProfile implements LocalBluetoothProfile { @Override public void onServiceDisconnected(int profile) { - Log.d(TAG, "Bluetooth service disconnected, profile:" + profile); + Log.d(TAG, "Bluetooth service disconnected"); mIsProfileReady=false; } } @@ -96,14 +95,12 @@ final class HfpClientProfile implements LocalBluetoothProfile { return BluetoothProfile.HEADSET_CLIENT; } - HfpClientProfile(Context context, LocalBluetoothAdapter adapter, - CachedBluetoothDeviceManager deviceManager, + HfpClientProfile(Context context, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager) { - mLocalAdapter = adapter; mDeviceManager = deviceManager; mProfileManager = profileManager; - mLocalAdapter.getProfileProxy(context, new HfpClientServiceListener(), - BluetoothProfile.HEADSET_CLIENT); + BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, + new HfpClientServiceListener(), BluetoothProfile.HEADSET_CLIENT); } @Override @@ -210,7 +207,7 @@ final class HfpClientProfile implements LocalBluetoothProfile { return R.string.bluetooth_headset_profile_summary_connected; default: - return Utils.getConnectionStateSummary(state); + return BluetoothUtils.getConnectionStateSummary(state); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HidDeviceProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HidDeviceProfile.java index 0a94375f3aa4..61e5b6b3e125 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HidDeviceProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HidDeviceProfile.java @@ -26,7 +26,6 @@ import android.util.Log; import com.android.settingslib.R; -import java.util.Collection; import java.util.List; /** @@ -39,7 +38,6 @@ public class HidDeviceProfile implements LocalBluetoothProfile { // HID Device Profile is always preferred. private static final int PREFERRED_VALUE = -1; - private final LocalBluetoothAdapter mLocalAdapter; private final CachedBluetoothDeviceManager mDeviceManager; private final LocalBluetoothProfileManager mProfileManager; static final String NAME = "HID DEVICE"; @@ -47,14 +45,12 @@ public class HidDeviceProfile implements LocalBluetoothProfile { private BluetoothHidDevice mService; private boolean mIsProfileReady; - HidDeviceProfile(Context context, LocalBluetoothAdapter adapter, - CachedBluetoothDeviceManager deviceManager, + HidDeviceProfile(Context context,CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager) { - mLocalAdapter = adapter; mDeviceManager = deviceManager; mProfileManager = profileManager; - adapter.getProfileProxy(context, new HidDeviceServiceListener(), - BluetoothProfile.HID_DEVICE); + BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, + new HidDeviceServiceListener(), BluetoothProfile.HID_DEVICE); } // These callbacks run on the main thread. @@ -71,7 +67,7 @@ public class HidDeviceProfile implements LocalBluetoothProfile { // we may add a new device here, but generally this should not happen if (device == null) { Log.w(TAG, "HidProfile found new device: " + nextDevice); - device = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, nextDevice); + device = mDeviceManager.addDevice(nextDevice); } Log.d(TAG, "Connection status changed: " + device); device.onProfileStateChanged(HidDeviceProfile.this, @@ -171,7 +167,7 @@ public class HidDeviceProfile implements LocalBluetoothProfile { case BluetoothProfile.STATE_CONNECTED: return R.string.bluetooth_hid_profile_summary_connected; default: - return Utils.getConnectionStateSummary(state); + return BluetoothUtils.getConnectionStateSummary(state); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HidProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HidProfile.java index 1e064818cc72..75d16db13efe 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HidProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HidProfile.java @@ -38,7 +38,6 @@ public class HidProfile implements LocalBluetoothProfile { private BluetoothHidHost mService; private boolean mIsProfileReady; - private final LocalBluetoothAdapter mLocalAdapter; private final CachedBluetoothDeviceManager mDeviceManager; private final LocalBluetoothProfileManager mProfileManager; @@ -62,7 +61,7 @@ public class HidProfile implements LocalBluetoothProfile { // we may add a new device here, but generally this should not happen if (device == null) { Log.w(TAG, "HidProfile found new device: " + nextDevice); - device = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, nextDevice); + device = mDeviceManager.addDevice(nextDevice); } device.onProfileStateChanged(HidProfile.this, BluetoothProfile.STATE_CONNECTED); device.refresh(); @@ -85,13 +84,12 @@ public class HidProfile implements LocalBluetoothProfile { return BluetoothProfile.HID_HOST; } - HidProfile(Context context, LocalBluetoothAdapter adapter, + HidProfile(Context context, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager) { - mLocalAdapter = adapter; mDeviceManager = deviceManager; mProfileManager = profileManager; - adapter.getProfileProxy(context, new HidHostServiceListener(), + BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new HidHostServiceListener(), BluetoothProfile.HID_HOST); } @@ -164,7 +162,7 @@ public class HidProfile implements LocalBluetoothProfile { return R.string.bluetooth_hid_profile_summary_connected; default: - return Utils.getConnectionStateSummary(state); + return BluetoothUtils.getConnectionStateSummary(state); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java index cbeeed8ea538..8f40ab47fe1b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java @@ -35,7 +35,10 @@ import java.util.Set; * <p>Connection and bonding state changes affecting specific devices * are handled by {@link CachedBluetoothDeviceManager}, * {@link BluetoothEventManager}, and {@link LocalBluetoothProfileManager}. + * + * @deprecated use {@link BluetoothAdapter} instead. */ +@Deprecated public class LocalBluetoothAdapter { private static final String TAG = "LocalBluetoothAdapter"; @@ -232,7 +235,7 @@ public class LocalBluetoothAdapter { ? BluetoothAdapter.STATE_TURNING_ON : BluetoothAdapter.STATE_TURNING_OFF); } else { - if (Utils.V) { + if (BluetoothUtils.V) { Log.v(TAG, "setBluetoothEnabled call, manager didn't return " + "success for enabled: " + enabled); } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothManager.java index 1c50953bac12..53c6075ccff4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothManager.java @@ -17,10 +17,15 @@ package com.android.settingslib.bluetooth; import android.content.Context; +import android.os.Handler; +import android.os.UserHandle; import android.util.Log; import java.lang.ref.WeakReference; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresPermission; + /** * LocalBluetoothManager provides a simplified interface on top of a subset of * the Bluetooth API. Note that {@link #getInstance} will return null @@ -48,6 +53,7 @@ public class LocalBluetoothManager { /** The broadcast receiver event manager. */ private final BluetoothEventManager mEventManager; + @Nullable public static synchronized LocalBluetoothManager getInstance(Context context, BluetoothManagerCallback onInitCallback) { if (sInstance == null) { @@ -56,25 +62,61 @@ public class LocalBluetoothManager { return null; } // This will be around as long as this process is - Context appContext = context.getApplicationContext(); - sInstance = new LocalBluetoothManager(adapter, appContext); + sInstance = new LocalBluetoothManager(adapter, context, /* handler= */ null, + /* userHandle= */ null); if (onInitCallback != null) { - onInitCallback.onBluetoothManagerInitialized(appContext, sInstance); + onInitCallback.onBluetoothManagerInitialized(context.getApplicationContext(), + sInstance); } } return sInstance; } - private LocalBluetoothManager(LocalBluetoothAdapter adapter, Context context) { - mContext = context; - mLocalAdapter = adapter; + /** + * Returns a new instance of {@link LocalBluetoothManager} or null if Bluetooth is not + * supported for this hardware. This instance should be globally cached by the caller. + */ + @Nullable + public static LocalBluetoothManager create(Context context, Handler handler) { + LocalBluetoothAdapter adapter = LocalBluetoothAdapter.getInstance(); + if (adapter == null) { + return null; + } + return new LocalBluetoothManager(adapter, context, handler, /* userHandle= */ null); + } - mCachedDeviceManager = new CachedBluetoothDeviceManager(context, this); - mEventManager = new BluetoothEventManager(mLocalAdapter, - mCachedDeviceManager, context); - mProfileManager = new LocalBluetoothProfileManager(context, + /** + * Returns a new instance of {@link LocalBluetoothManager} or null if Bluetooth is not + * supported for this hardware. This instance should be globally cached by the caller. + * + * <p> Allows to specify a {@link UserHandle} for which to receive bluetooth events. + * + * <p> Requires {@link android.Manifest.permission#INTERACT_ACROSS_USERS_FULL} permission. + */ + @Nullable + @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) + public static LocalBluetoothManager create(Context context, Handler handler, + UserHandle userHandle) { + LocalBluetoothAdapter adapter = LocalBluetoothAdapter.getInstance(); + if (adapter == null) { + return null; + } + return new LocalBluetoothManager(adapter, context, handler, + userHandle); + } + + private LocalBluetoothManager(LocalBluetoothAdapter adapter, Context context, Handler handler, + UserHandle userHandle) { + mContext = context.getApplicationContext(); + mLocalAdapter = adapter; + mCachedDeviceManager = new CachedBluetoothDeviceManager(mContext, this); + mEventManager = new BluetoothEventManager(mLocalAdapter, mCachedDeviceManager, mContext, + handler, userHandle); + mProfileManager = new LocalBluetoothProfileManager(mContext, mLocalAdapter, mCachedDeviceManager, mEventManager); + + mProfileManager.updateLocalProfiles(); mEventManager.readPairedDevices(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java index d1ccc8fcc443..29c6d719641a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java @@ -18,6 +18,7 @@ package com.android.settingslib.bluetooth; import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothA2dpSink; +import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothHeadsetClient; @@ -35,9 +36,12 @@ import android.bluetooth.BluetoothUuid; import android.content.Context; import android.content.Intent; import android.os.ParcelUuid; -import androidx.annotation.VisibleForTesting; import android.util.Log; -import com.android.internal.R; + +import androidx.annotation.VisibleForTesting; + +import com.android.internal.util.CollectionUtils; + import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -51,9 +55,7 @@ import java.util.Map; */ public class LocalBluetoothProfileManager { private static final String TAG = "LocalBluetoothProfileManager"; - private static final boolean DEBUG = Utils.D; - /** Singleton instance. */ - private static LocalBluetoothProfileManager sInstance; + private static final boolean DEBUG = BluetoothUtils.D; /** * An interface for notifying BluetoothHeadset IPC clients when they have @@ -80,7 +82,6 @@ public class LocalBluetoothProfileManager { } private final Context mContext; - private final LocalBluetoothAdapter mLocalAdapter; private final CachedBluetoothDeviceManager mDeviceManager; private final BluetoothEventManager mEventManager; @@ -90,14 +91,12 @@ public class LocalBluetoothProfileManager { private HfpClientProfile mHfpClientProfile; private MapProfile mMapProfile; private MapClientProfile mMapClientProfile; - private final HidProfile mHidProfile; + private HidProfile mHidProfile; private HidDeviceProfile mHidDeviceProfile; private OppProfile mOppProfile; - private final PanProfile mPanProfile; + private PanProfile mPanProfile; private PbapClientProfile mPbapClientProfile; - private final PbapServerProfile mPbapProfile; - private final boolean mUsePbapPce; - private final boolean mUseMapClient; + private PbapServerProfile mPbapProfile; private HearingAidProfile mHearingAidProfile; private SapProfile mSapProfile; @@ -113,201 +112,113 @@ public class LocalBluetoothProfileManager { BluetoothEventManager eventManager) { mContext = context; - mLocalAdapter = adapter; mDeviceManager = deviceManager; mEventManager = eventManager; - mUsePbapPce = mContext.getResources().getBoolean(R.bool.enable_pbap_pce_profile); - // MAP Client is typically used in the same situations as PBAP Client - mUseMapClient = mContext.getResources().getBoolean(R.bool.enable_pbap_pce_profile); // pass this reference to adapter and event manager (circular dependency) - mLocalAdapter.setProfileManager(this); - mEventManager.setProfileManager(this); - - ParcelUuid[] uuids = adapter.getUuids(); - - List<Integer> supportedList = mLocalAdapter.getSupportedProfiles(); - if (supportedList.contains(BluetoothProfile.HEARING_AID)) { - mHearingAidProfile = new HearingAidProfile(mContext, mLocalAdapter, mDeviceManager, - this); - addProfile(mHearingAidProfile, HearingAidProfile.NAME, - BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED); - } - - // uuids may be null if Bluetooth is turned off - if (uuids != null) { - updateLocalProfiles(uuids); - } - - // Always add HID host, HID device, and PAN profiles - mHidProfile = new HidProfile(context, mLocalAdapter, mDeviceManager, this); - addProfile(mHidProfile, HidProfile.NAME, - BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED); - - mPanProfile = new PanProfile(context, mLocalAdapter); - addPanProfile(mPanProfile, PanProfile.NAME, - BluetoothPan.ACTION_CONNECTION_STATE_CHANGED); - - mHidDeviceProfile = new HidDeviceProfile(context, mLocalAdapter, mDeviceManager, this); - addProfile(mHidDeviceProfile, HidDeviceProfile.NAME, - BluetoothHidDevice.ACTION_CONNECTION_STATE_CHANGED); - - if(DEBUG) Log.d(TAG, "Adding local MAP profile"); - if (mUseMapClient) { - mMapClientProfile = new MapClientProfile(mContext, mLocalAdapter, mDeviceManager, this); - addProfile(mMapClientProfile, MapClientProfile.NAME, - BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED); - } else { - mMapProfile = new MapProfile(mContext, mLocalAdapter, mDeviceManager, this); - addProfile(mMapProfile, MapProfile.NAME, - BluetoothMap.ACTION_CONNECTION_STATE_CHANGED); - } - - //Create PBAP server profile - if(DEBUG) Log.d(TAG, "Adding local PBAP profile"); - - mPbapProfile = new PbapServerProfile(context); - addProfile(mPbapProfile, PbapServerProfile.NAME, - BluetoothPbap.ACTION_CONNECTION_STATE_CHANGED); + adapter.setProfileManager(this); if (DEBUG) Log.d(TAG, "LocalBluetoothProfileManager construction complete"); } /** - * Initialize or update the local profile objects. If a UUID was previously - * present but has been removed, we print a warning but don't remove the - * profile object as it might be referenced elsewhere, or the UUID might - * come back and we don't want multiple copies of the profile objects. - * @param uuids + * create profile instance according to bluetooth supported profile list */ - void updateLocalProfiles(ParcelUuid[] uuids) { - // A2DP SRC - if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.AudioSource)) { - if (mA2dpProfile == null) { - if(DEBUG) Log.d(TAG, "Adding local A2DP SRC profile"); - mA2dpProfile = new A2dpProfile(mContext, mLocalAdapter, mDeviceManager, this); - addProfile(mA2dpProfile, A2dpProfile.NAME, - BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); - } - } else if (mA2dpProfile != null) { - Log.w(TAG, "Warning: A2DP profile was previously added but the UUID is now missing."); - } - - // A2DP SINK - if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.AudioSink)) { - if (mA2dpSinkProfile == null) { - if(DEBUG) Log.d(TAG, "Adding local A2DP Sink profile"); - mA2dpSinkProfile = new A2dpSinkProfile(mContext, mLocalAdapter, - mDeviceManager, this); - addProfile(mA2dpSinkProfile, A2dpSinkProfile.NAME, - BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED); - } - } else if (mA2dpSinkProfile != null) { - Log.w(TAG, "Warning: A2DP Sink profile was previously added but the " - + "UUID is now missing."); - } - - // Headset / Handsfree - if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Handsfree_AG) || - BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.HSP_AG)) { - if (mHeadsetProfile == null) { - if (DEBUG) Log.d(TAG, "Adding local HEADSET profile"); - mHeadsetProfile = new HeadsetProfile(mContext, mLocalAdapter, - mDeviceManager, this); - addHeadsetProfile(mHeadsetProfile, HeadsetProfile.NAME, - BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED, - BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED, - BluetoothHeadset.STATE_AUDIO_DISCONNECTED); - } - } else if (mHeadsetProfile != null) { - Log.w(TAG, "Warning: HEADSET profile was previously added but the " - + "UUID is now missing."); - } - - // Headset HF - if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Handsfree)) { - if (mHfpClientProfile == null) { - if(DEBUG) Log.d(TAG, "Adding local HfpClient profile"); - mHfpClientProfile = - new HfpClientProfile(mContext, mLocalAdapter, mDeviceManager, this); - addHeadsetProfile(mHfpClientProfile, HfpClientProfile.NAME, - BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED, - BluetoothHeadsetClient.ACTION_AUDIO_STATE_CHANGED, - BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED); - } - } else if (mHfpClientProfile != null) { - Log.w(TAG, - "Warning: Hfp Client profile was previously added but the UUID is now missing."); - } else { - Log.d(TAG, "Handsfree Uuid not found."); - } - - // Message Access Profile Client - if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.MNS)) { - if (mMapClientProfile == null) { - if(DEBUG) Log.d(TAG, "Adding local Map Client profile"); - mMapClientProfile = - new MapClientProfile(mContext, mLocalAdapter, mDeviceManager, this); - addProfile(mMapClientProfile, MapClientProfile.NAME, - BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED); - } - } else if (mMapClientProfile != null) { - Log.w(TAG, - "Warning: MAP Client profile was previously added but the " - + "UUID is now missing."); - } else { - Log.d(TAG, "MAP Client Uuid not found."); + void updateLocalProfiles() { + List<Integer> supportedList = BluetoothAdapter.getDefaultAdapter().getSupportedProfiles(); + if (CollectionUtils.isEmpty(supportedList)) { + if (DEBUG) Log.d(TAG, "supportedList is null"); + return; } - - // OPP - if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.ObexObjectPush)) { - if (mOppProfile == null) { - if(DEBUG) Log.d(TAG, "Adding local OPP profile"); - mOppProfile = new OppProfile(); - // Note: no event handler for OPP, only name map. - mProfileNameMap.put(OppProfile.NAME, mOppProfile); - } - } else if (mOppProfile != null) { - Log.w(TAG, "Warning: OPP profile was previously added but the UUID is now missing."); - } - - // PBAP Client - if (mUsePbapPce) { - if (mPbapClientProfile == null) { - if(DEBUG) Log.d(TAG, "Adding local PBAP Client profile"); - mPbapClientProfile = new PbapClientProfile(mContext, mLocalAdapter, mDeviceManager, - this); - addProfile(mPbapClientProfile, PbapClientProfile.NAME, - BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED); - } - } else if (mPbapClientProfile != null) { - Log.w(TAG, - "Warning: PBAP Client profile was previously added but the UUID is now missing."); - } - - // Hearing Aid Client - if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.HearingAid)) { - if (mHearingAidProfile == null) { - if(DEBUG) Log.d(TAG, "Adding local Hearing Aid profile"); - mHearingAidProfile = new HearingAidProfile(mContext, mLocalAdapter, - mDeviceManager, this); - addProfile(mHearingAidProfile, HearingAidProfile.NAME, - BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED); + if (mA2dpProfile == null && supportedList.contains(BluetoothProfile.A2DP)) { + if (DEBUG) Log.d(TAG, "Adding local A2DP profile"); + mA2dpProfile = new A2dpProfile(mContext, mDeviceManager, this); + addProfile(mA2dpProfile, A2dpProfile.NAME, + BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); + } + if (mA2dpSinkProfile == null && supportedList.contains(BluetoothProfile.A2DP_SINK)) { + if (DEBUG) Log.d(TAG, "Adding local A2DP SINK profile"); + mA2dpSinkProfile = new A2dpSinkProfile(mContext, mDeviceManager, this); + addProfile(mA2dpSinkProfile, A2dpSinkProfile.NAME, + BluetoothA2dpSink.ACTION_CONNECTION_STATE_CHANGED); + } + if (mHeadsetProfile == null && supportedList.contains(BluetoothProfile.HEADSET)) { + if (DEBUG) Log.d(TAG, "Adding local HEADSET profile"); + mHeadsetProfile = new HeadsetProfile(mContext, mDeviceManager, this); + addHeadsetProfile(mHeadsetProfile, HeadsetProfile.NAME, + BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED, + BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED, + BluetoothHeadset.STATE_AUDIO_DISCONNECTED); + } + if (mHfpClientProfile == null && supportedList.contains(BluetoothProfile.HEADSET_CLIENT)) { + if (DEBUG) Log.d(TAG, "Adding local HfpClient profile"); + mHfpClientProfile = new HfpClientProfile(mContext, mDeviceManager, this); + addHeadsetProfile(mHfpClientProfile, HfpClientProfile.NAME, + BluetoothHeadsetClient.ACTION_CONNECTION_STATE_CHANGED, + BluetoothHeadsetClient.ACTION_AUDIO_STATE_CHANGED, + BluetoothHeadsetClient.STATE_AUDIO_DISCONNECTED); + } + if (mMapClientProfile == null && supportedList.contains(BluetoothProfile.MAP_CLIENT)) { + if (DEBUG) Log.d(TAG, "Adding local MAP CLIENT profile"); + mMapClientProfile = new MapClientProfile(mContext, mDeviceManager,this); + addProfile(mMapClientProfile, MapClientProfile.NAME, + BluetoothMapClient.ACTION_CONNECTION_STATE_CHANGED); + } + if (mMapProfile == null && supportedList.contains(BluetoothProfile.MAP)) { + if (DEBUG) Log.d(TAG, "Adding local MAP profile"); + mMapProfile = new MapProfile(mContext, mDeviceManager, this); + addProfile(mMapProfile, MapProfile.NAME, BluetoothMap.ACTION_CONNECTION_STATE_CHANGED); + } + if (mOppProfile == null && supportedList.contains(BluetoothProfile.OPP)) { + if (DEBUG) Log.d(TAG, "Adding local OPP profile"); + mOppProfile = new OppProfile(); + // Note: no event handler for OPP, only name map. + mProfileNameMap.put(OppProfile.NAME, mOppProfile); + } + if (mHearingAidProfile == null && supportedList.contains(BluetoothProfile.HEARING_AID)) { + if (DEBUG) Log.d(TAG, "Adding local Hearing Aid profile"); + mHearingAidProfile = new HearingAidProfile(mContext, mDeviceManager, + this); + addProfile(mHearingAidProfile, HearingAidProfile.NAME, + BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED); + } + if (mHidProfile == null && supportedList.contains(BluetoothProfile.HID_HOST)) { + if (DEBUG) Log.d(TAG, "Adding local HID_HOST profile"); + mHidProfile = new HidProfile(mContext, mDeviceManager, this); + addProfile(mHidProfile, HidProfile.NAME, + BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED); + } + if (mHidDeviceProfile == null && supportedList.contains(BluetoothProfile.HID_DEVICE)) { + if (DEBUG) Log.d(TAG, "Adding local HID_DEVICE profile"); + mHidDeviceProfile = new HidDeviceProfile(mContext, mDeviceManager, this); + addProfile(mHidDeviceProfile, HidDeviceProfile.NAME, + BluetoothHidDevice.ACTION_CONNECTION_STATE_CHANGED); + } + if (mPanProfile == null && supportedList.contains(BluetoothProfile.PAN)) { + if (DEBUG) Log.d(TAG, "Adding local PAN profile"); + mPanProfile = new PanProfile(mContext); + addPanProfile(mPanProfile, PanProfile.NAME, + BluetoothPan.ACTION_CONNECTION_STATE_CHANGED); + } + if (mPbapProfile == null && supportedList.contains(BluetoothProfile.PBAP)) { + if (DEBUG) Log.d(TAG, "Adding local PBAP profile"); + mPbapProfile = new PbapServerProfile(mContext); + addProfile(mPbapProfile, PbapServerProfile.NAME, + BluetoothPbap.ACTION_CONNECTION_STATE_CHANGED); + } + if (mPbapClientProfile == null && supportedList.contains(BluetoothProfile.PBAP_CLIENT)) { + if (DEBUG) Log.d(TAG, "Adding local PBAP Client profile"); + mPbapClientProfile = new PbapClientProfile(mContext, mDeviceManager,this); + addProfile(mPbapClientProfile, PbapClientProfile.NAME, + BluetoothPbapClient.ACTION_CONNECTION_STATE_CHANGED); + } + if (mSapProfile == null && supportedList.contains(BluetoothProfile.SAP)) { + if (DEBUG) { + Log.d(TAG, "Adding local SAP profile"); } - } else if (mHearingAidProfile != null) { - Log.w(TAG, "Warning: Hearing Aid profile was previously added but the " - + "UUID is now missing."); - } - - // SAP - if (mSapProfile == null && BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.SAP)) { - Log.d(TAG, "Adding local SAP profile"); - mSapProfile = new SapProfile(mContext, mLocalAdapter, mDeviceManager, this); - addProfile(mSapProfile, SapProfile.NAME, - BluetoothSap.ACTION_CONNECTION_STATE_CHANGED); + mSapProfile = new SapProfile(mContext, mDeviceManager, this); + addProfile(mSapProfile, SapProfile.NAME, BluetoothSap.ACTION_CONNECTION_STATE_CHANGED); } mEventManager.registerProfileIntentReceiver(); - - // There is no local SDP record for HID and Settings app doesn't control PBAP Server. } private void addHeadsetProfile(LocalBluetoothProfile profile, String profileName, @@ -341,10 +252,7 @@ public class LocalBluetoothProfileManager { // Called from LocalBluetoothAdapter when state changes to ON void setBluetoothStateOn() { - ParcelUuid[] uuids = mLocalAdapter.getUuids(); - if (uuids != null) { - updateLocalProfiles(uuids); - } + updateLocalProfiles(); mEventManager.readPairedDevices(); } @@ -362,8 +270,7 @@ public class LocalBluetoothProfileManager { CachedBluetoothDevice cachedDevice = mDeviceManager.findDevice(device); if (cachedDevice == null) { Log.w(TAG, "StateChangedHandler found new device: " + device); - cachedDevice = mDeviceManager.addDevice(mLocalAdapter, - LocalBluetoothProfileManager.this, device); + cachedDevice = mDeviceManager.addDevice(device); } onReceiveInternal(intent, cachedDevice); } @@ -376,38 +283,26 @@ public class LocalBluetoothProfileManager { Log.i(TAG, "Failed to connect " + mProfile + " device"); } - boolean isHearingAidProfile = (getHearingAidProfile() != null) && - (mProfile instanceof HearingAidProfile); - - if (isHearingAidProfile && (newState == BluetoothProfile.STATE_CONNECTED)) { + if (getHearingAidProfile() != null && + mProfile instanceof HearingAidProfile && + (newState == BluetoothProfile.STATE_CONNECTED)) { // Check if the HiSyncID has being initialized if (cachedDevice.getHiSyncId() == BluetoothHearingAid.HI_SYNC_ID_INVALID) { - long newHiSyncId = getHearingAidProfile().getHiSyncId(cachedDevice.getDevice()); - if (newHiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID) { cachedDevice.setHiSyncId(newHiSyncId); - mDeviceManager.onHiSyncIdChanged(newHiSyncId); } } } - cachedDevice.onProfileStateChanged(mProfile, newState); - cachedDevice.refresh(); - - if (isHearingAidProfile) { - CachedBluetoothDevice otherDevice = - mDeviceManager.getHearingAidOtherDevice(cachedDevice, cachedDevice.getHiSyncId()); - if (otherDevice != null) { - if (DEBUG) { - Log.d(TAG, "Refreshing other hearing aid=" + otherDevice - + ", newState=" + newState); - } - otherDevice.refresh(); - } + // Dispatch profile changed after device update + if (!(cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID + && mDeviceManager.onProfileConnectionStateChangedIfProcessed(cachedDevice, + newState))) { + cachedDevice.refresh(); + mEventManager.dispatchProfileConnectionStateChanged(cachedDevice, newState, + mProfile.getProfileId()); } - mEventManager.dispatchProfileConnectionStateChanged(cachedDevice, newState, - mProfile.getProfileId()); } } @@ -650,7 +545,9 @@ public class LocalBluetoothProfileManager { removedProfiles.remove(mMapClientProfile); } - if (mUsePbapPce) { + if ((mPbapClientProfile != null) && + BluetoothUuid.isUuidPresent(localUuids, BluetoothUuid.PBAP_PCE) && + BluetoothUuid.containsAnyUuid(uuids, PbapClientProfile.SRC_UUIDS)) { profiles.add(mPbapClientProfile); removedProfiles.remove(mPbapClientProfile); } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/MapClientProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/MapClientProfile.java index 57712e3cbb28..1e22f440b5f8 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/MapClientProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/MapClientProfile.java @@ -40,7 +40,6 @@ public final class MapClientProfile implements LocalBluetoothProfile { private BluetoothMapClient mService; private boolean mIsProfileReady; - private final LocalBluetoothAdapter mLocalAdapter; private final CachedBluetoothDeviceManager mDeviceManager; private final LocalBluetoothProfileManager mProfileManager; @@ -70,7 +69,7 @@ public final class MapClientProfile implements LocalBluetoothProfile { // we may add a new device here, but generally this should not happen if (device == null) { Log.w(TAG, "MapProfile found new device: " + nextDevice); - device = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, nextDevice); + device = mDeviceManager.addDevice(nextDevice); } device.onProfileStateChanged(MapClientProfile.this, BluetoothProfile.STATE_CONNECTED); @@ -98,14 +97,12 @@ public final class MapClientProfile implements LocalBluetoothProfile { return BluetoothProfile.MAP_CLIENT; } - MapClientProfile(Context context, LocalBluetoothAdapter adapter, - CachedBluetoothDeviceManager deviceManager, + MapClientProfile(Context context, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager) { - mLocalAdapter = adapter; mDeviceManager = deviceManager; mProfileManager = profileManager; - mLocalAdapter.getProfileProxy(context, new MapClientServiceListener(), - BluetoothProfile.MAP_CLIENT); + BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, + new MapClientServiceListener(), BluetoothProfile.MAP_CLIENT); } public boolean accessProfileEnabled() { @@ -200,7 +197,7 @@ public final class MapClientProfile implements LocalBluetoothProfile { return R.string.bluetooth_map_profile_summary_connected; default: - return Utils.getConnectionStateSummary(state); + return BluetoothUtils.getConnectionStateSummary(state); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/MapProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/MapProfile.java index e59a036731d9..758202412c93 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/MapProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/MapProfile.java @@ -40,7 +40,6 @@ public class MapProfile implements LocalBluetoothProfile { private BluetoothMap mService; private boolean mIsProfileReady; - private final LocalBluetoothAdapter mLocalAdapter; private final CachedBluetoothDeviceManager mDeviceManager; private final LocalBluetoothProfileManager mProfileManager; @@ -69,7 +68,7 @@ public class MapProfile implements LocalBluetoothProfile { // we may add a new device here, but generally this should not happen if (device == null) { Log.w(TAG, "MapProfile found new device: " + nextDevice); - device = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, nextDevice); + device = mDeviceManager.addDevice(nextDevice); } device.onProfileStateChanged(MapProfile.this, BluetoothProfile.STATE_CONNECTED); @@ -97,13 +96,11 @@ public class MapProfile implements LocalBluetoothProfile { return BluetoothProfile.MAP; } - MapProfile(Context context, LocalBluetoothAdapter adapter, - CachedBluetoothDeviceManager deviceManager, + MapProfile(Context context, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager) { - mLocalAdapter = adapter; mDeviceManager = deviceManager; mProfileManager = profileManager; - mLocalAdapter.getProfileProxy(context, new MapServiceListener(), + BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new MapServiceListener(), BluetoothProfile.MAP); } @@ -196,7 +193,7 @@ public class MapProfile implements LocalBluetoothProfile { return R.string.bluetooth_map_profile_summary_connected; default: - return Utils.getConnectionStateSummary(state); + return BluetoothUtils.getConnectionStateSummary(state); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/OppProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/OppProfile.java index 129732ccba3e..e1e5dbe29a1a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/OppProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/OppProfile.java @@ -16,12 +16,12 @@ package com.android.settingslib.bluetooth; -import com.android.settingslib.R; - import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; +import com.android.settingslib.R; + /** * OppProfile handles Bluetooth OPP. */ diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/PanProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/PanProfile.java index 0d566c77eac2..7b811624a6f3 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/PanProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/PanProfile.java @@ -38,7 +38,6 @@ public class PanProfile implements LocalBluetoothProfile { private BluetoothPan mService; private boolean mIsProfileReady; - private final LocalBluetoothAdapter mLocalAdapter; // Tethering direction for each device private final HashMap<BluetoothDevice, Integer> mDeviceRoleMap = @@ -74,9 +73,8 @@ public class PanProfile implements LocalBluetoothProfile { return BluetoothProfile.PAN; } - PanProfile(Context context, LocalBluetoothAdapter adapter) { - mLocalAdapter = adapter; - mLocalAdapter.getProfileProxy(context, new PanServiceListener(), + PanProfile(Context context) { + BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new PanServiceListener(), BluetoothProfile.PAN); } @@ -153,7 +151,7 @@ public class PanProfile implements LocalBluetoothProfile { } default: - return Utils.getConnectionStateSummary(state); + return BluetoothUtils.getConnectionStateSummary(state); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/PbapClientProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/PbapClientProfile.java index 07523bd8faab..1f15601f2756 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/PbapClientProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/PbapClientProfile.java @@ -16,10 +16,10 @@ package com.android.settingslib.bluetooth; -import android.bluetooth.BluetoothPbapClient; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothPbapClient; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.content.Context; @@ -38,7 +38,6 @@ public final class PbapClientProfile implements LocalBluetoothProfile { private BluetoothPbapClient mService; private boolean mIsProfileReady; - private final LocalBluetoothAdapter mLocalAdapter; private final CachedBluetoothDeviceManager mDeviceManager; static final ParcelUuid[] SRC_UUIDS = { @@ -66,7 +65,7 @@ public final class PbapClientProfile implements LocalBluetoothProfile { // we may add a new device here, but generally this should not happen if (device == null) { Log.w(TAG, "PbapClientProfile found new device: " + nextDevice); - device = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, nextDevice); + device = mDeviceManager.addDevice(nextDevice); } device.onProfileStateChanged(PbapClientProfile.this, BluetoothProfile.STATE_CONNECTED); device.refresh(); @@ -100,14 +99,12 @@ public final class PbapClientProfile implements LocalBluetoothProfile { return BluetoothProfile.PBAP_CLIENT; } - PbapClientProfile(Context context, LocalBluetoothAdapter adapter, - CachedBluetoothDeviceManager deviceManager, + PbapClientProfile(Context context, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager) { - mLocalAdapter = adapter; mDeviceManager = deviceManager; mProfileManager = profileManager; - mLocalAdapter.getProfileProxy(context, new PbapClientServiceListener(), - BluetoothProfile.PBAP_CLIENT); + BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, + new PbapClientServiceListener(), BluetoothProfile.PBAP_CLIENT); } public boolean accessProfileEnabled() { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/SapProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/SapProfile.java index c8a56a199b15..b4acc4810faf 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/SapProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/SapProfile.java @@ -19,8 +19,8 @@ package com.android.settingslib.bluetooth; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothSap; import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothSap; import android.bluetooth.BluetoothUuid; import android.content.Context; import android.os.ParcelUuid; @@ -40,7 +40,6 @@ final class SapProfile implements LocalBluetoothProfile { private BluetoothSap mService; private boolean mIsProfileReady; - private final LocalBluetoothAdapter mLocalAdapter; private final CachedBluetoothDeviceManager mDeviceManager; private final LocalBluetoothProfileManager mProfileManager; @@ -68,7 +67,7 @@ final class SapProfile implements LocalBluetoothProfile { // we may add a new device here, but generally this should not happen if (device == null) { Log.w(TAG, "SapProfile found new device: " + nextDevice); - device = mDeviceManager.addDevice(mLocalAdapter, mProfileManager, nextDevice); + device = mDeviceManager.addDevice(nextDevice); } device.onProfileStateChanged(SapProfile.this, BluetoothProfile.STATE_CONNECTED); @@ -95,13 +94,11 @@ final class SapProfile implements LocalBluetoothProfile { return BluetoothProfile.SAP; } - SapProfile(Context context, LocalBluetoothAdapter adapter, - CachedBluetoothDeviceManager deviceManager, + SapProfile(Context context, CachedBluetoothDeviceManager deviceManager, LocalBluetoothProfileManager profileManager) { - mLocalAdapter = adapter; mDeviceManager = deviceManager; mProfileManager = profileManager; - mLocalAdapter.getProfileProxy(context, new SapServiceListener(), + BluetoothAdapter.getDefaultAdapter().getProfileProxy(context, new SapServiceListener(), BluetoothProfile.SAP); } @@ -196,7 +193,7 @@ final class SapProfile implements LocalBluetoothProfile { return R.string.bluetooth_sap_profile_summary_connected; default: - return Utils.getConnectionStateSummary(state); + return BluetoothUtils.getConnectionStateSummary(state); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java index f4d647d61005..9572fb3c629d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java @@ -1,6 +1,7 @@ package com.android.settingslib.core; import android.content.Context; + import androidx.preference.Preference; import androidx.preference.PreferenceGroup; import androidx.preference.PreferenceScreen; diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/EventLogWriter.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/EventLogWriter.java index 72273046ef29..1aeb075abf30 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/EventLogWriter.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/EventLogWriter.java @@ -18,7 +18,7 @@ package com.android.settingslib.core.instrumentation; import android.content.Context; import android.metrics.LogMaker; -import android.util.Log; +import android.text.TextUtils; import android.util.Pair; import com.android.internal.logging.MetricsLogger; @@ -29,8 +29,7 @@ import com.android.internal.logging.nano.MetricsProto; */ public class EventLogWriter implements LogWriter { - private final MetricsLogger mMetricsLogger = new MetricsLogger(); - + @Override public void visible(Context context, int source, int category) { final LogMaker logMaker = new LogMaker(category) .setType(MetricsProto.MetricsEvent.TYPE_OPEN) @@ -38,53 +37,27 @@ public class EventLogWriter implements LogWriter { MetricsLogger.action(logMaker); } + @Override public void hidden(Context context, int category) { MetricsLogger.hidden(context, category); } - public void action(int category, int value, Pair<Integer, Object>... taggedData) { - if (taggedData == null || taggedData.length == 0) { - mMetricsLogger.action(category, value); - } else { - final LogMaker logMaker = new LogMaker(category) - .setType(MetricsProto.MetricsEvent.TYPE_ACTION) - .setSubtype(value); - for (Pair<Integer, Object> pair : taggedData) { - logMaker.addTaggedData(pair.first, pair.second); - } - mMetricsLogger.write(logMaker); - } - } - - public void action(int category, boolean value, Pair<Integer, Object>... taggedData) { - action(category, value ? 1 : 0, taggedData); - } - + @Override public void action(Context context, int category, Pair<Integer, Object>... taggedData) { action(context, category, "", taggedData); } - public void actionWithSource(Context context, int source, int category) { - final LogMaker logMaker = new LogMaker(category) - .setType(MetricsProto.MetricsEvent.TYPE_ACTION); - if (source != MetricsProto.MetricsEvent.VIEW_UNKNOWN) { - logMaker.addTaggedData(MetricsProto.MetricsEvent.FIELD_CONTEXT, source); - } - MetricsLogger.action(logMaker); - } - - /** @deprecated use {@link #action(int, int, Pair[])} */ - @Deprecated + @Override public void action(Context context, int category, int value) { MetricsLogger.action(context, category, value); } - /** @deprecated use {@link #action(int, boolean, Pair[])} */ - @Deprecated + @Override public void action(Context context, int category, boolean value) { MetricsLogger.action(context, category, value); } + @Override public void action(Context context, int category, String pkg, Pair<Integer, Object>... taggedData) { if (taggedData == null || taggedData.length == 0) { @@ -100,11 +73,20 @@ public class EventLogWriter implements LogWriter { } } - public void count(Context context, String name, int value) { - MetricsLogger.count(context, name, value); - } - - public void histogram(Context context, String name, int bucket) { - MetricsLogger.histogram(context, name, bucket); + @Override + public void action(int attribution, int action, int pageId, String key, int value) { + final LogMaker logMaker = new LogMaker(action) + .setType(MetricsProto.MetricsEvent.TYPE_ACTION); + if (attribution != MetricsProto.MetricsEvent.VIEW_UNKNOWN) { + logMaker.addTaggedData(MetricsProto.MetricsEvent.FIELD_CONTEXT, pageId); + } + if (!TextUtils.isEmpty(key)) { + logMaker.addTaggedData(MetricsProto.MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME, + key); + logMaker.addTaggedData( + MetricsProto.MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE, + value); + } + MetricsLogger.action(logMaker); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/LogWriter.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/LogWriter.java index 4b9f5727208d..b60364ea7271 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/LogWriter.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/LogWriter.java @@ -34,16 +34,6 @@ public interface LogWriter { void hidden(Context context, int category); /** - * Logs a user action. - */ - void action(int category, int value, Pair<Integer, Object>... taggedData); - - /** - * Logs a user action. - */ - void action(int category, boolean value, Pair<Integer, Object>... taggedData); - - /** * Logs an user action. */ void action(Context context, int category, Pair<Integer, Object>... taggedData); @@ -51,20 +41,11 @@ public interface LogWriter { /** * Logs an user action. */ - void actionWithSource(Context context, int source, int category); - - /** - * Logs an user action. - * @deprecated use {@link #action(int, int, Pair[])} - */ - @Deprecated void action(Context context, int category, int value); /** * Logs an user action. - * @deprecated use {@link #action(int, boolean, Pair[])} */ - @Deprecated void action(Context context, int category, boolean value); /** @@ -73,12 +54,7 @@ public interface LogWriter { void action(Context context, int category, String pkg, Pair<Integer, Object>... taggedData); /** - * Logs a count. - */ - void count(Context context, String name, int value); - - /** - * Logs a histogram event. + * Generically log action. */ - void histogram(Context context, String name, int bucket); + void action(int attribution, int action, int pageId, String key, int value); } diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java index 1e5b378e931c..188204e82e70 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java @@ -15,6 +15,8 @@ */ package com.android.settingslib.core.instrumentation; +import android.app.Activity; +import android.app.settings.SettingsEnums; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -30,7 +32,12 @@ import java.util.List; * FeatureProvider for metrics. */ public class MetricsFeatureProvider { - private List<LogWriter> mLoggerWriters; + /** + * The metrics category constant for logging source when a setting fragment is opened. + */ + public static final String EXTRA_SOURCE_METRICS_CATEGORY = ":settings:source_metrics"; + + protected List<LogWriter> mLoggerWriters; public MetricsFeatureProvider() { mLoggerWriters = new ArrayList<>(); @@ -41,6 +48,25 @@ public class MetricsFeatureProvider { mLoggerWriters.add(new EventLogWriter()); } + /** + * Returns the attribution id for specified activity. If no attribution is set, returns {@link + * SettingsEnums#PAGE_UNKNOWN}. + * + * <p/> Attribution is a {@link SettingsEnums} page id that indicates where the specified + * activity is launched from. + */ + public int getAttribution(Activity activity) { + if (activity == null) { + return SettingsEnums.PAGE_UNKNOWN; + } + final Intent intent = activity.getIntent(); + if (intent == null) { + return SettingsEnums.PAGE_UNKNOWN; + } + return intent.getIntExtra(EXTRA_SOURCE_METRICS_CATEGORY, + SettingsEnums.PAGE_UNKNOWN); + } + public void visible(Context context, int source, int category) { for (LogWriter writer : mLoggerWriters) { writer.visible(context, source, category); @@ -53,75 +79,43 @@ public class MetricsFeatureProvider { } } - public void actionWithSource(Context context, int source, int category) { + public void action(Context context, int category, Pair<Integer, Object>... taggedData) { for (LogWriter writer : mLoggerWriters) { - writer.actionWithSource(context, source, category); + writer.action(context, category, taggedData); } } /** - * Logs a user action. Includes the elapsed time since the containing - * fragment has been visible. + * Logs a generic Settings event. */ - public void action(VisibilityLoggerMixin visibilityLogger, int category, int value) { + public void action(Context context, int category, String pkg, + Pair<Integer, Object>... taggedData) { for (LogWriter writer : mLoggerWriters) { - writer.action(category, value, - sinceVisibleTaggedData(visibilityLogger.elapsedTimeSinceVisible())); + writer.action(context, category, pkg, taggedData); } } /** - * Logs a user action. Includes the elapsed time since the containing - * fragment has been visible. + * Logs a generic Settings event. */ - public void action(VisibilityLoggerMixin visibilityLogger, int category, boolean value) { + public void action(int attribution, int action, int pageId, String key, int value) { for (LogWriter writer : mLoggerWriters) { - writer.action(category, value, - sinceVisibleTaggedData(visibilityLogger.elapsedTimeSinceVisible())); + writer.action(attribution, action, pageId, key, value); } } - public void action(Context context, int category, Pair<Integer, Object>... taggedData) { - for (LogWriter writer : mLoggerWriters) { - writer.action(context, category, taggedData); - } - } - - /** @deprecated use {@link #action(VisibilityLoggerMixin, int, int)} */ - @Deprecated public void action(Context context, int category, int value) { for (LogWriter writer : mLoggerWriters) { writer.action(context, category, value); } } - /** @deprecated use {@link #action(VisibilityLoggerMixin, int, boolean)} */ - @Deprecated public void action(Context context, int category, boolean value) { for (LogWriter writer : mLoggerWriters) { writer.action(context, category, value); } } - public void action(Context context, int category, String pkg, - Pair<Integer, Object>... taggedData) { - for (LogWriter writer : mLoggerWriters) { - writer.action(context, category, pkg, taggedData); - } - } - - public void count(Context context, String name, int value) { - for (LogWriter writer : mLoggerWriters) { - writer.count(context, name, value); - } - } - - public void histogram(Context context, String name, int bucket) { - for (LogWriter writer : mLoggerWriters) { - writer.histogram(context, name, bucket); - } - } - public int getMetricsCategory(Object object) { if (object == null || !(object instanceof Instrumentable)) { return MetricsEvent.VIEW_UNKNOWN; @@ -153,7 +147,4 @@ public class MetricsFeatureProvider { Pair.create(MetricsEvent.FIELD_CONTEXT, sourceMetricsCategory)); } - private Pair<Integer, Object> sinceVisibleTaggedData(long timestamp) { - return Pair.create(MetricsEvent.NOTIFICATION_SINCE_VISIBLE_MILLIS, timestamp); - } } diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java index a79f125d4c99..71f3789405c8 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java @@ -20,11 +20,12 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.AsyncTask; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Log; import android.util.Pair; +import androidx.annotation.VisibleForTesting; + import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import java.util.Map; @@ -115,8 +116,6 @@ public class SharedPreferencesLogger implements SharedPreferences { mPreferenceKeySet.add(prefKey); return; } - // TODO: Remove count logging to save some resource. - mMetricsFeature.count(mContext, buildCountName(prefKey, value), 1); final Pair<Integer, Object> valueData; if (value instanceof Long) { diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/VisibilityLoggerMixin.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/VisibilityLoggerMixin.java index cb1ca59d2f43..aed02a268aea 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/VisibilityLoggerMixin.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/VisibilityLoggerMixin.java @@ -16,17 +16,18 @@ package com.android.settingslib.core.instrumentation; +import static com.android.settingslib.core.instrumentation.Instrumentable.METRICS_CATEGORY_UNKNOWN; + import android.app.Activity; +import android.content.Intent; +import android.os.SystemClock; + import androidx.lifecycle.Lifecycle.Event; import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.OnLifecycleEvent; -import android.content.Intent; -import android.os.SystemClock; import com.android.internal.logging.nano.MetricsProto; -import static com.android.settingslib.core.instrumentation.Instrumentable.METRICS_CATEGORY_UNKNOWN; - /** * Logs visibility change of a fragment. */ @@ -40,11 +41,6 @@ public class VisibilityLoggerMixin implements LifecycleObserver { private int mSourceMetricsCategory = MetricsProto.MetricsEvent.VIEW_UNKNOWN; private long mVisibleTimestamp; - /** - * The metrics category constant for logging source when a setting fragment is opened. - */ - public static final String EXTRA_SOURCE_METRICS_CATEGORY = ":settings:source_metrics"; - private VisibilityLoggerMixin() { mMetricsCategory = METRICS_CATEGORY_UNKNOWN; } @@ -81,7 +77,8 @@ public class VisibilityLoggerMixin implements LifecycleObserver { if (intent == null) { return; } - mSourceMetricsCategory = intent.getIntExtra(EXTRA_SOURCE_METRICS_CATEGORY, + mSourceMetricsCategory = intent.getIntExtra( + MetricsFeatureProvider.EXTRA_SOURCE_METRICS_CATEGORY, MetricsProto.MetricsEvent.VIEW_UNKNOWN); } diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/Lifecycle.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/Lifecycle.java index 7ec757a56217..56de280a0049 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/Lifecycle.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/Lifecycle.java @@ -18,19 +18,20 @@ package com.android.settingslib.core.lifecycle; import static androidx.lifecycle.Lifecycle.Event.ON_ANY; import android.annotation.UiThread; -import androidx.lifecycle.LifecycleOwner; -import androidx.lifecycle.LifecycleRegistry; -import androidx.lifecycle.OnLifecycleEvent; import android.content.Context; import android.os.Bundle; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.preference.PreferenceScreen; import android.util.Log; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.preference.PreferenceScreen; + import com.android.settingslib.core.lifecycle.events.OnAttach; import com.android.settingslib.core.lifecycle.events.OnCreate; import com.android.settingslib.core.lifecycle.events.OnCreateOptionsMenu; diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableActivity.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableActivity.java index f9aa062b64da..2bd0b27063ea 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableActivity.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableActivity.java @@ -24,20 +24,22 @@ import static androidx.lifecycle.Lifecycle.Event.ON_STOP; import android.annotation.Nullable; import android.app.Activity; -import androidx.lifecycle.LifecycleOwner; import android.os.Bundle; import android.os.PersistableBundle; import android.view.Menu; import android.view.MenuItem; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.LifecycleOwner; + /** * {@link Activity} that has hooks to observe activity lifecycle events. */ -public class ObservableActivity extends Activity implements LifecycleOwner { +public class ObservableActivity extends FragmentActivity implements LifecycleOwner { private final Lifecycle mLifecycle = new Lifecycle(this); - public Lifecycle getLifecycle() { + public Lifecycle getSettingsLifecycle() { return mLifecycle; } diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableDialogFragment.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableDialogFragment.java index 972e062bb396..869f54f7d13a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableDialogFragment.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableDialogFragment.java @@ -22,14 +22,15 @@ import static androidx.lifecycle.Lifecycle.Event.ON_RESUME; import static androidx.lifecycle.Lifecycle.Event.ON_START; import static androidx.lifecycle.Lifecycle.Event.ON_STOP; -import android.app.DialogFragment; -import androidx.lifecycle.LifecycleOwner; import android.content.Context; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.LifecycleOwner; + /** * {@link DialogFragment} that has hooks to observe fragment lifecycle events. */ @@ -37,6 +38,10 @@ public class ObservableDialogFragment extends DialogFragment implements Lifecycl protected final Lifecycle mLifecycle = new Lifecycle(this); + public Lifecycle getSettingsLifecycle() { + return mLifecycle; + } + @Override public void onAttach(Context context) { super.onAttach(context); @@ -100,9 +105,4 @@ public class ObservableDialogFragment extends DialogFragment implements Lifecycl } return lifecycleHandled; } - - @Override - public Lifecycle getLifecycle() { - return mLifecycle; - } } diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableFragment.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableFragment.java index 55597cc1247a..6ba930dcac96 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableFragment.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservableFragment.java @@ -24,19 +24,20 @@ import static androidx.lifecycle.Lifecycle.Event.ON_START; import static androidx.lifecycle.Lifecycle.Event.ON_STOP; import android.annotation.CallSuper; -import android.app.Fragment; -import androidx.lifecycle.LifecycleOwner; import android.content.Context; import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.LifecycleOwner; + public class ObservableFragment extends Fragment implements LifecycleOwner { private final Lifecycle mLifecycle = new Lifecycle(this); - public Lifecycle getLifecycle() { + public Lifecycle getSettingsLifecycle() { return mLifecycle; } diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservablePreferenceFragment.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservablePreferenceFragment.java index 904681c4aa3c..bd1e5a588968 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservablePreferenceFragment.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/ObservablePreferenceFragment.java @@ -24,24 +24,25 @@ import static androidx.lifecycle.Lifecycle.Event.ON_START; import static androidx.lifecycle.Lifecycle.Event.ON_STOP; import android.annotation.CallSuper; -import androidx.lifecycle.LifecycleOwner; import android.content.Context; import android.os.Bundle; -import androidx.preference.PreferenceFragment; -import androidx.preference.PreferenceScreen; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import androidx.lifecycle.LifecycleOwner; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; + /** - * {@link PreferenceFragment} that has hooks to observe fragment lifecycle events. + * {@link PreferenceFragmentCompat} that has hooks to observe fragment lifecycle events. */ -public abstract class ObservablePreferenceFragment extends PreferenceFragment +public abstract class ObservablePreferenceFragment extends PreferenceFragmentCompat implements LifecycleOwner { private final Lifecycle mLifecycle = new Lifecycle(this); - public Lifecycle getLifecycle() { + public Lifecycle getSettingsLifecycle() { return mLifecycle; } diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnCreate.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnCreate.java index ada1537250b6..ae6741194662 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnCreate.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnCreate.java @@ -16,12 +16,10 @@ package com.android.settingslib.core.lifecycle.events; -import androidx.lifecycle.Lifecycle; -import androidx.lifecycle.OnLifecycleEvent; import android.os.Bundle; /** - * @deprecated use {@link OnLifecycleEvent(Lifecycle.Event) } + * @deprecated use {@link androidx.lifecycle.OnLifecycleEvent} */ @Deprecated public interface OnCreate { diff --git a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnPrepareOptionsMenu.java b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnPrepareOptionsMenu.java index b9f13713c59f..92ae9fdd50a9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnPrepareOptionsMenu.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/lifecycle/events/OnPrepareOptionsMenu.java @@ -17,7 +17,6 @@ package com.android.settingslib.core.lifecycle.events; import android.view.Menu; -import android.view.MenuInflater; public interface OnPrepareOptionsMenu { void onPrepareOptionsMenu(Menu menu); diff --git a/packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java b/packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java index 955f64a357dc..8fac3fd0570c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java +++ b/packages/SettingsLib/src/com/android/settingslib/datetime/ZoneGetter.java @@ -20,9 +20,6 @@ import android.content.Context; import android.content.res.XmlResourceParser; import android.icu.text.TimeZoneFormat; import android.icu.text.TimeZoneNames; -import androidx.annotation.VisibleForTesting; -import androidx.core.text.BidiFormatter; -import androidx.core.text.TextDirectionHeuristicsCompat; import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.TextUtils; @@ -31,6 +28,10 @@ import android.text.style.TtsSpan; import android.util.Log; import android.view.View; +import androidx.annotation.VisibleForTesting; +import androidx.core.text.BidiFormatter; +import androidx.core.text.TextDirectionHeuristicsCompat; + import com.android.settingslib.R; import libcore.util.TimeZoneFinder; @@ -381,7 +382,7 @@ public class ZoneGetter { // Create a lookup of local zone IDs. final List<String> zoneIds = lookupTimeZoneIdsByCountry(locale.getCountry()); - localZoneIds = new HashSet<>(zoneIds); + localZoneIds = zoneIds != null ? new HashSet<>(zoneIds) : new HashSet<>(); } @VisibleForTesting diff --git a/packages/SettingsLib/src/com/android/settingslib/development/AbstractEnableAdbPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/development/AbstractEnableAdbPreferenceController.java index a2ae9031d7b3..caabf9af35f6 100644 --- a/packages/SettingsLib/src/com/android/settingslib/development/AbstractEnableAdbPreferenceController.java +++ b/packages/SettingsLib/src/com/android/settingslib/development/AbstractEnableAdbPreferenceController.java @@ -22,13 +22,14 @@ import android.content.Context; import android.content.Intent; import android.os.UserManager; import android.provider.Settings; +import android.text.TextUtils; + import androidx.annotation.VisibleForTesting; -import androidx.preference.SwitchPreference; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; +import androidx.preference.SwitchPreference; import androidx.preference.TwoStatePreference; -import android.text.TextUtils; import com.android.settingslib.core.ConfirmationDialogController; @@ -59,8 +60,7 @@ public abstract class AbstractEnableAdbPreferenceController extends @Override public boolean isAvailable() { - final UserManager um = mContext.getSystemService(UserManager.class); - return um != null && (um.isAdminUser() || um.isDemoUser()); + return mContext.getSystemService(UserManager.class).isAdminUser(); } @Override diff --git a/packages/SettingsLib/src/com/android/settingslib/development/AbstractLogdSizePreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/development/AbstractLogdSizePreferenceController.java index e2f6b7b87560..5a823976cd0e 100644 --- a/packages/SettingsLib/src/com/android/settingslib/development/AbstractLogdSizePreferenceController.java +++ b/packages/SettingsLib/src/com/android/settingslib/development/AbstractLogdSizePreferenceController.java @@ -19,6 +19,7 @@ package com.android.settingslib.development; import android.content.Context; import android.content.Intent; import android.os.SystemProperties; + import androidx.annotation.VisibleForTesting; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.preference.ListPreference; diff --git a/packages/SettingsLib/src/com/android/settingslib/development/AbstractLogpersistPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/development/AbstractLogpersistPreferenceController.java index f277c16a1e2c..87226746668e 100644 --- a/packages/SettingsLib/src/com/android/settingslib/development/AbstractLogpersistPreferenceController.java +++ b/packages/SettingsLib/src/com/android/settingslib/development/AbstractLogpersistPreferenceController.java @@ -22,12 +22,13 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.os.SystemProperties; +import android.text.TextUtils; + import androidx.annotation.VisibleForTesting; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; import com.android.settingslib.R; import com.android.settingslib.core.ConfirmationDialogController; diff --git a/packages/SettingsLib/src/com/android/settingslib/development/DeveloperOptionsPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/development/DeveloperOptionsPreferenceController.java index 15d5522bbd7b..f757aa4e4dab 100644 --- a/packages/SettingsLib/src/com/android/settingslib/development/DeveloperOptionsPreferenceController.java +++ b/packages/SettingsLib/src/com/android/settingslib/development/DeveloperOptionsPreferenceController.java @@ -17,6 +17,7 @@ package com.android.settingslib.development; import android.content.Context; + import androidx.preference.Preference; import androidx.preference.PreferenceScreen; diff --git a/packages/SettingsLib/src/com/android/settingslib/development/DevelopmentSettingsEnabler.java b/packages/SettingsLib/src/com/android/settingslib/development/DevelopmentSettingsEnabler.java index 8d186032bfef..b191f888aa1a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/development/DevelopmentSettingsEnabler.java +++ b/packages/SettingsLib/src/com/android/settingslib/development/DevelopmentSettingsEnabler.java @@ -21,6 +21,7 @@ import android.content.Intent; import android.os.Build; import android.os.UserManager; import android.provider.Settings; + import androidx.localbroadcastmanager.content.LocalBroadcastManager; public class DevelopmentSettingsEnabler { @@ -45,7 +46,7 @@ public class DevelopmentSettingsEnabler { Build.TYPE.equals("eng") ? 1 : 0) != 0; final boolean hasRestriction = um.hasUserRestriction( UserManager.DISALLOW_DEBUGGING_FEATURES); - final boolean isAdminOrDemo = um.isAdminUser() || um.isDemoUser(); - return isAdminOrDemo && !hasRestriction && settingEnabled; + final boolean isAdmin = um.isAdminUser(); + return isAdmin && !hasRestriction && settingEnabled; } } diff --git a/packages/SettingsLib/src/com/android/settingslib/development/SystemPropPoker.java b/packages/SettingsLib/src/com/android/settingslib/development/SystemPropPoker.java index dba22d0c6af3..3f16be184c41 100644 --- a/packages/SettingsLib/src/com/android/settingslib/development/SystemPropPoker.java +++ b/packages/SettingsLib/src/com/android/settingslib/development/SystemPropPoker.java @@ -21,9 +21,10 @@ import android.os.IBinder; import android.os.Parcel; import android.os.RemoteException; import android.os.ServiceManager; +import android.util.Log; + import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -import android.util.Log; public class SystemPropPoker { private static final String TAG = "SystemPropPoker"; diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractBluetoothAddressPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractBluetoothAddressPreferenceController.java index 821b4d5da049..745591235dbb 100644 --- a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractBluetoothAddressPreferenceController.java +++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractBluetoothAddressPreferenceController.java @@ -19,10 +19,11 @@ package com.android.settingslib.deviceinfo; import android.annotation.SuppressLint; import android.bluetooth.BluetoothAdapter; import android.content.Context; +import android.text.TextUtils; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; import com.android.settingslib.R; import com.android.settingslib.core.lifecycle.Lifecycle; diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractImsStatusPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractImsStatusPreferenceController.java index 10260de289d3..a5f403690dab 100644 --- a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractImsStatusPreferenceController.java +++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractImsStatusPreferenceController.java @@ -21,13 +21,14 @@ import android.content.Context; import android.net.ConnectivityManager; import android.net.wifi.WifiManager; import android.os.PersistableBundle; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; import android.telephony.CarrierConfigManager; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceScreen; + import com.android.settingslib.R; import com.android.settingslib.core.lifecycle.Lifecycle; diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractIpAddressPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractIpAddressPreferenceController.java index 45cd86667f1e..24da72ea611a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractIpAddressPreferenceController.java +++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractIpAddressPreferenceController.java @@ -20,6 +20,7 @@ import android.content.Context; import android.net.ConnectivityManager; import android.net.LinkProperties; import android.net.wifi.WifiManager; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractSerialNumberPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractSerialNumberPreferenceController.java index 60b29fb8682f..d28792ef30de 100644 --- a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractSerialNumberPreferenceController.java +++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractSerialNumberPreferenceController.java @@ -18,10 +18,11 @@ package com.android.settingslib.deviceinfo; import android.content.Context; import android.os.Build; +import android.text.TextUtils; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; import com.android.settingslib.core.AbstractPreferenceController; diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractUptimePreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractUptimePreferenceController.java index 332a2a463b74..5f7226969699 100644 --- a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractUptimePreferenceController.java +++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractUptimePreferenceController.java @@ -20,10 +20,11 @@ import android.content.Context; import android.os.Handler; import android.os.Message; import android.os.SystemClock; +import android.text.format.DateUtils; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.format.DateUtils; import com.android.settingslib.core.AbstractPreferenceController; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -88,7 +89,7 @@ public abstract class AbstractUptimePreferenceController extends AbstractPrefere } private void updateTimes() { - mUptime.setSummary(DateUtils.formatDuration(SystemClock.elapsedRealtime())); + mUptime.setSummary(DateUtils.formatElapsedTime(SystemClock.elapsedRealtime() / 1000)); } private static class MyHandler extends Handler { diff --git a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractWifiMacAddressPreferenceController.java b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractWifiMacAddressPreferenceController.java index 89d259507d8a..9699294df587 100644 --- a/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractWifiMacAddressPreferenceController.java +++ b/packages/SettingsLib/src/com/android/settingslib/deviceinfo/AbstractWifiMacAddressPreferenceController.java @@ -22,10 +22,11 @@ import android.net.ConnectivityManager; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; import android.provider.Settings; +import android.text.TextUtils; + import androidx.annotation.VisibleForTesting; import androidx.preference.Preference; import androidx.preference.PreferenceScreen; -import android.text.TextUtils; import com.android.settingslib.R; import com.android.settingslib.core.lifecycle.Lifecycle; @@ -38,6 +39,10 @@ public abstract class AbstractWifiMacAddressPreferenceController @VisibleForTesting static final String KEY_WIFI_MAC_ADDRESS = "wifi_mac_address"; + @VisibleForTesting + static final int OFF = 0; + @VisibleForTesting + static final int ON = 1; private static final String[] CONNECTIVITY_INTENTS = { ConnectivityManager.CONNECTIVITY_ACTION, @@ -80,13 +85,14 @@ public abstract class AbstractWifiMacAddressPreferenceController protected void updateConnectivity() { WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); final int macRandomizationMode = Settings.Global.getInt(mContext.getContentResolver(), - Settings.Global.WIFI_CONNECTED_MAC_RANDOMIZATION_ENABLED, 0); + Settings.Global.WIFI_CONNECTED_MAC_RANDOMIZATION_ENABLED, OFF); final String macAddress = wifiInfo == null ? null : wifiInfo.getMacAddress(); - if (TextUtils.isEmpty(macAddress)) { - mWifiMacAddress.setSummary(R.string.status_unavailable); - } else if (macRandomizationMode == 1 && WifiInfo.DEFAULT_MAC_ADDRESS.equals(macAddress)) { + if (macRandomizationMode == ON && WifiInfo.DEFAULT_MAC_ADDRESS.equals(macAddress)) { mWifiMacAddress.setSummary(R.string.wifi_status_mac_randomized); + } else if (TextUtils.isEmpty(macAddress) + || WifiInfo.DEFAULT_MAC_ADDRESS.equals(macAddress)) { + mWifiMacAddress.setSummary(R.string.status_unavailable); } else { mWifiMacAddress.setSummary(macAddress); } diff --git a/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java b/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java index af8fd4c46a64..e0ca1ab0c07c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/display/DisplayDensityUtils.java @@ -16,11 +16,8 @@ package com.android.settingslib.display; -import com.android.settingslib.R; - import android.content.Context; import android.content.res.Resources; -import android.hardware.display.DisplayManager; import android.os.AsyncTask; import android.os.RemoteException; import android.os.UserHandle; @@ -31,6 +28,8 @@ import android.view.Display; import android.view.IWindowManager; import android.view.WindowManagerGlobal; +import com.android.settingslib.R; + import java.util.Arrays; /** diff --git a/packages/SettingsLib/src/com/android/settingslib/drawable/UserIconDrawable.java b/packages/SettingsLib/src/com/android/settingslib/drawable/UserIconDrawable.java index 54d1aba09ae3..274696bfec0e 100644 --- a/packages/SettingsLib/src/com/android/settingslib/drawable/UserIconDrawable.java +++ b/packages/SettingsLib/src/com/android/settingslib/drawable/UserIconDrawable.java @@ -16,6 +16,7 @@ package com.android.settingslib.drawable; +import android.annotation.ColorInt; import android.annotation.DrawableRes; import android.annotation.NonNull; import android.app.admin.DevicePolicyManager; @@ -171,11 +172,13 @@ public class UserIconDrawable extends Drawable implements Drawable.Callback { public UserIconDrawable setBadgeIfManagedUser(Context context, int userId) { Drawable badge = null; - boolean isManaged = context.getSystemService(DevicePolicyManager.class) - .getProfileOwnerAsUser(userId) != null; - if (isManaged) { - badge = getDrawableForDisplayDensity( - context, com.android.internal.R.drawable.ic_corp_badge_case); + if (userId != UserHandle.USER_NULL) { + boolean isManaged = context.getSystemService(DevicePolicyManager.class) + .getProfileOwnerAsUser(userId) != null; + if (isManaged) { + badge = getDrawableForDisplayDensity( + context, com.android.internal.R.drawable.ic_corp_badge_case); + } } return setBadge(badge); } @@ -251,11 +254,8 @@ public class UserIconDrawable extends Drawable implements Drawable.Callback { mPaint.setColorFilter(null); } else { int color = mTintColor.getColorForState(getState(), mTintColor.getDefaultColor()); - if (mPaint.getColorFilter() == null) { + if (shouldUpdateColorFilter(color, mTintMode)) { mPaint.setColorFilter(new PorterDuffColorFilter(color, mTintMode)); - } else { - ((PorterDuffColorFilter) mPaint.getColorFilter()).setMode(mTintMode); - ((PorterDuffColorFilter) mPaint.getColorFilter()).setColor(color); } } @@ -263,6 +263,18 @@ public class UserIconDrawable extends Drawable implements Drawable.Callback { } } + private boolean shouldUpdateColorFilter(@ColorInt int color, PorterDuff.Mode mode) { + ColorFilter colorFilter = mPaint.getColorFilter(); + if (colorFilter instanceof PorterDuffColorFilter) { + PorterDuffColorFilter porterDuffColorFilter = (PorterDuffColorFilter) colorFilter; + int currentColor = porterDuffColorFilter.getColor(); + PorterDuff.Mode currentMode = porterDuffColorFilter.getMode(); + return currentColor != color || currentMode != mode; + } else { + return true; + } + } + @Override public void setAlpha(int alpha) { mPaint.setAlpha(alpha); diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java index dd8bfda43ff9..92fd86894d56 100644 --- a/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java +++ b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java @@ -51,6 +51,8 @@ public final class CategoryKey { public static final String CATEGORY_GESTURES = "com.android.settings.category.ia.gestures"; public static final String CATEGORY_NIGHT_DISPLAY = "com.android.settings.category.ia.night_display"; + public static final String CATEGORY_PRIVACY = + "com.android.settings.category.ia.privacy"; public static final Map<String, String> KEY_COMPAT_MAP; diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryManager.java b/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryManager.java deleted file mode 100644 index d9605367dc56..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/drawer/CategoryManager.java +++ /dev/null @@ -1,238 +0,0 @@ -/** - * 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.drawer; - -import android.content.ComponentName; -import android.content.Context; -import androidx.annotation.VisibleForTesting; -import android.util.ArrayMap; -import android.util.ArraySet; -import android.util.Log; -import android.util.Pair; - -import com.android.settingslib.applications.InterestingConfigChanges; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -import static java.lang.String.CASE_INSENSITIVE_ORDER; - -public class CategoryManager { - - private static final String TAG = "CategoryManager"; - - private static CategoryManager sInstance; - private final InterestingConfigChanges mInterestingConfigChanges; - - // Tile cache (key: <packageName, activityName>, value: tile) - private final Map<Pair<String, String>, Tile> mTileByComponentCache; - - // Tile cache (key: category key, value: category) - private final Map<String, DashboardCategory> mCategoryByKeyMap; - - private List<DashboardCategory> mCategories; - private String mExtraAction; - - public static CategoryManager get(Context context) { - return get(context, null); - } - - public static CategoryManager get(Context context, String action) { - if (sInstance == null) { - sInstance = new CategoryManager(context, action); - } - return sInstance; - } - - CategoryManager(Context context, String action) { - mTileByComponentCache = new ArrayMap<>(); - mCategoryByKeyMap = new ArrayMap<>(); - mInterestingConfigChanges = new InterestingConfigChanges(); - mInterestingConfigChanges.applyNewConfig(context.getResources()); - mExtraAction = action; - } - - public synchronized DashboardCategory getTilesByCategory(Context context, String categoryKey) { - return getTilesByCategory(context, categoryKey, TileUtils.SETTING_PKG); - } - - public synchronized DashboardCategory getTilesByCategory(Context context, String categoryKey, - String settingPkg) { - tryInitCategories(context, settingPkg); - - return mCategoryByKeyMap.get(categoryKey); - } - - public synchronized List<DashboardCategory> getCategories(Context context) { - return getCategories(context, TileUtils.SETTING_PKG); - } - - public synchronized List<DashboardCategory> getCategories(Context context, String settingPkg) { - tryInitCategories(context, settingPkg); - return mCategories; - } - - public synchronized void reloadAllCategories(Context context, String settingPkg) { - final boolean forceClearCache = mInterestingConfigChanges.applyNewConfig( - context.getResources()); - mCategories = null; - tryInitCategories(context, forceClearCache, settingPkg); - } - - public synchronized void updateCategoryFromBlacklist(Set<ComponentName> tileBlacklist) { - if (mCategories == null) { - Log.w(TAG, "Category is null, skipping blacklist update"); - } - for (int i = 0; i < mCategories.size(); i++) { - DashboardCategory category = mCategories.get(i); - for (int j = 0; j < category.getTilesCount(); j++) { - Tile tile = category.getTile(j); - if (tileBlacklist.contains(tile.intent.getComponent())) { - category.removeTile(j--); - } - } - } - } - - private synchronized void tryInitCategories(Context context, String settingPkg) { - // Keep cached tiles by default. The cache is only invalidated when InterestingConfigChange - // happens. - tryInitCategories(context, false /* forceClearCache */, settingPkg); - } - - private synchronized void tryInitCategories(Context context, boolean forceClearCache, - String settingPkg) { - if (mCategories == null) { - if (forceClearCache) { - mTileByComponentCache.clear(); - } - mCategoryByKeyMap.clear(); - mCategories = TileUtils.getCategories(context, mTileByComponentCache, - false /* categoryDefinedInManifest */, mExtraAction, settingPkg); - for (DashboardCategory category : mCategories) { - mCategoryByKeyMap.put(category.key, category); - } - backwardCompatCleanupForCategory(mTileByComponentCache, mCategoryByKeyMap); - sortCategories(context, mCategoryByKeyMap); - filterDuplicateTiles(mCategoryByKeyMap); - } - } - - @VisibleForTesting - synchronized void backwardCompatCleanupForCategory( - Map<Pair<String, String>, Tile> tileByComponentCache, - Map<String, DashboardCategory> categoryByKeyMap) { - // A package can use a) CategoryKey, b) old category keys, c) both. - // Check if a package uses old category key only. - // If yes, map them to new category key. - - // Build a package name -> tile map first. - final Map<String, List<Tile>> packageToTileMap = new HashMap<>(); - for (Entry<Pair<String, String>, Tile> tileEntry : tileByComponentCache.entrySet()) { - final String packageName = tileEntry.getKey().first; - List<Tile> tiles = packageToTileMap.get(packageName); - if (tiles == null) { - tiles = new ArrayList<>(); - packageToTileMap.put(packageName, tiles); - } - tiles.add(tileEntry.getValue()); - } - - for (Entry<String, List<Tile>> entry : packageToTileMap.entrySet()) { - final List<Tile> tiles = entry.getValue(); - // Loop map, find if all tiles from same package uses old key only. - boolean useNewKey = false; - boolean useOldKey = false; - for (Tile tile : tiles) { - if (CategoryKey.KEY_COMPAT_MAP.containsKey(tile.category)) { - useOldKey = true; - } else { - useNewKey = true; - break; - } - } - // Uses only old key, map them to new keys one by one. - if (useOldKey && !useNewKey) { - for (Tile tile : tiles) { - final String newCategoryKey = CategoryKey.KEY_COMPAT_MAP.get(tile.category); - tile.category = newCategoryKey; - // move tile to new category. - DashboardCategory newCategory = categoryByKeyMap.get(newCategoryKey); - if (newCategory == null) { - newCategory = new DashboardCategory(); - categoryByKeyMap.put(newCategoryKey, newCategory); - } - newCategory.addTile(tile); - } - } - } - } - - /** - * Sort the tiles injected from all apps such that if they have the same priority value, - * they wil lbe sorted by package name. - * <p/> - * A list of tiles are considered sorted when their priority value decreases in a linear - * scan. - */ - @VisibleForTesting - synchronized void sortCategories(Context context, - Map<String, DashboardCategory> categoryByKeyMap) { - for (Entry<String, DashboardCategory> categoryEntry : categoryByKeyMap.entrySet()) { - categoryEntry.getValue().sortTiles(context.getPackageName()); - } - } - - /** - * Filter out duplicate tiles from category. Duplicate tiles are the ones pointing to the - * same intent. - */ - @VisibleForTesting - synchronized void filterDuplicateTiles(Map<String, DashboardCategory> categoryByKeyMap) { - for (Entry<String, DashboardCategory> categoryEntry : categoryByKeyMap.entrySet()) { - final DashboardCategory category = categoryEntry.getValue(); - final int count = category.getTilesCount(); - final Set<ComponentName> components = new ArraySet<>(); - for (int i = count - 1; i >= 0; i--) { - final Tile tile = category.getTile(i); - if (tile.intent == null) { - continue; - } - final ComponentName tileComponent = tile.intent.getComponent(); - if (components.contains(tileComponent)) { - category.removeTile(i); - } else { - components.add(tileComponent); - } - } - } - } - - /** - * Sort priority value for tiles within a single {@code DashboardCategory}. - * - * @see #sortCategories(Context, Map) - */ - private synchronized void sortCategoriesForExternalTiles(Context context, - DashboardCategory dashboardCategory) { - dashboardCategory.sortTiles(context.getPackageName()); - - } -} diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardCategory.java b/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardCategory.java index 3a03644b6226..a3dda658bec7 100644 --- a/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardCategory.java +++ b/packages/SettingsLib/src/com/android/settingslib/drawer/DashboardCategory.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -18,55 +18,39 @@ package com.android.settingslib.drawer; import static java.lang.String.CASE_INSENSITIVE_ORDER; -import android.content.ComponentName; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; -import android.util.Log; import java.util.ArrayList; import java.util.Collections; -import java.util.Comparator; import java.util.List; public class DashboardCategory implements Parcelable { - private static final String TAG = "DashboardCategory"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - - /** - * Title of the category that is shown to the user. - */ - public CharSequence title; - /** * Key used for placing external tiles. */ - public String key; - - /** - * Used to control display order. - */ - public int priority; + public final String key; /** * List of the category's children */ private List<Tile> mTiles = new ArrayList<>(); - DashboardCategory(DashboardCategory in) { - if (in != null) { - title = in.title; - key = in.key; - priority = in.priority; - for (Tile tile : in.mTiles) { - mTiles.add(tile); - } - } + public DashboardCategory(String key) { + this.key = key; } - public DashboardCategory() { - // Empty + DashboardCategory(Parcel in) { + key = in.readString(); + + final int count = in.readInt(); + + for (int n = 0; n < count; n++) { + Tile tile = Tile.CREATOR.createFromParcel(in); + mTiles.add(tile); + } } /** @@ -87,14 +71,6 @@ public class DashboardCategory implements Parcelable { mTiles.add(tile); } - public synchronized void addTile(int n, Tile tile) { - mTiles.add(n, tile); - } - - public synchronized void removeTile(Tile tile) { - mTiles.remove(tile); - } - public synchronized void removeTile(int n) { mTiles.remove(n); } @@ -107,44 +83,29 @@ public class DashboardCategory implements Parcelable { return mTiles.get(n); } - public synchronized boolean containsComponent(ComponentName component) { - for (Tile tile : mTiles) { - if (TextUtils.equals(tile.intent.getComponent().getClassName(), - component.getClassName())) { - if (DEBUG) { - Log.d(TAG, "category " + key + "contains component" + component); - } - return true; - } - } - if (DEBUG) { - Log.d(TAG, "category " + key + " does not contain component" + component); - } - return false; - } - /** * Sort priority value for tiles in this category. */ public void sortTiles() { - Collections.sort(mTiles, TILE_COMPARATOR); + Collections.sort(mTiles, Tile.TILE_COMPARATOR); } /** * Sort priority value and package name for tiles in this category. */ public synchronized void sortTiles(String skipPackageName) { - // Sort mTiles based on [priority, package within priority] + // Sort mTiles based on [order, package within order] Collections.sort(mTiles, (tile1, tile2) -> { - final String package1 = tile1.intent.getComponent().getPackageName(); - final String package2 = tile2.intent.getComponent().getPackageName(); - final int packageCompare = CASE_INSENSITIVE_ORDER.compare(package1, package2); - // First sort by priority - final int priorityCompare = tile2.priority - tile1.priority; - if (priorityCompare != 0) { - return priorityCompare; + // First sort by order + final int orderCompare = tile2.getOrder() - tile1.getOrder(); + if (orderCompare != 0) { + return orderCompare; } + // Then sort by package name, skip package take precedence + final String package1 = tile1.getPackageName(); + final String package2 = tile2.getPackageName(); + final int packageCompare = CASE_INSENSITIVE_ORDER.compare(package1, package2); if (packageCompare != 0) { if (TextUtils.equals(package1, skipPackageName)) { return -1; @@ -164,9 +125,7 @@ public class DashboardCategory implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - TextUtils.writeToParcel(title, dest, flags); dest.writeString(key); - dest.writeInt(priority); final int count = mTiles.size(); dest.writeInt(count); @@ -177,23 +136,6 @@ public class DashboardCategory implements Parcelable { } } - public void readFromParcel(Parcel in) { - title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); - key = in.readString(); - priority = in.readInt(); - - final int count = in.readInt(); - - for (int n = 0; n < count; n++) { - Tile tile = Tile.CREATOR.createFromParcel(in); - mTiles.add(tile); - } - } - - DashboardCategory(Parcel in) { - readFromParcel(in); - } - public static final Creator<DashboardCategory> CREATOR = new Creator<DashboardCategory>() { public DashboardCategory createFromParcel(Parcel source) { return new DashboardCategory(source); @@ -204,12 +146,4 @@ public class DashboardCategory implements Parcelable { } }; - public static final Comparator<Tile> TILE_COMPARATOR = - new Comparator<Tile>() { - @Override - public int compare(Tile lhs, Tile rhs) { - return rhs.priority - lhs.priority; - } - }; - } diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/ProfileSelectDialog.java b/packages/SettingsLib/src/com/android/settingslib/drawer/ProfileSelectDialog.java deleted file mode 100644 index c79b1466d606..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/drawer/ProfileSelectDialog.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2015 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.drawer; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.app.DialogFragment; -import android.app.FragmentManager; -import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnClickListener; -import android.content.Intent; -import android.os.Bundle; -import android.os.UserHandle; -import android.os.UserManager; -import android.util.Log; - -import java.util.List; - -public class ProfileSelectDialog extends DialogFragment implements OnClickListener { - - private static final String TAG = "ProfileSelectDialog"; - private static final String ARG_SELECTED_TILE = "selectedTile"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - - private Tile mSelectedTile; - - public static void show(FragmentManager manager, Tile tile) { - ProfileSelectDialog dialog = new ProfileSelectDialog(); - Bundle args = new Bundle(); - args.putParcelable(ARG_SELECTED_TILE, tile); - dialog.setArguments(args); - dialog.show(manager, "select_profile"); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mSelectedTile = getArguments().getParcelable(ARG_SELECTED_TILE); - } - - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - Context context = getActivity(); - AlertDialog.Builder builder = new AlertDialog.Builder(context); - UserAdapter adapter = UserAdapter.createUserAdapter(UserManager.get(context), context, - mSelectedTile.userHandle); - builder.setTitle(com.android.settingslib.R.string.choose_profile) - .setAdapter(adapter, this); - - return builder.create(); - } - - @Override - public void onClick(DialogInterface dialog, int which) { - UserHandle user = mSelectedTile.userHandle.get(which); - // Show menu on top level items. - mSelectedTile.intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - getActivity().startActivityAsUser(mSelectedTile.intent, user); - } - - public static void updateUserHandlesIfNeeded(Context context, Tile tile) { - List<UserHandle> userHandles = tile.userHandle; - if (tile.userHandle == null || tile.userHandle.size() <= 1) { - return; - } - final UserManager userManager = UserManager.get(context); - for (int i = userHandles.size() - 1; i >= 0; i--) { - if (userManager.getUserInfo(userHandles.get(i).getIdentifier()) == null) { - if (DEBUG) { - Log.d(TAG, "Delete the user: " + userHandles.get(i).getIdentifier()); - } - userHandles.remove(i); - } - } - } -} diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java b/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java deleted file mode 100644 index 68ead09d9d57..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/drawer/SettingsDrawerActivity.java +++ /dev/null @@ -1,214 +0,0 @@ -/** - * Copyright (C) 2015 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.drawer; - -import android.annotation.LayoutRes; -import android.annotation.Nullable; -import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.PackageManager; -import android.content.res.TypedArray; -import android.os.AsyncTask; -import android.os.Bundle; -import android.util.ArraySet; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowManager.LayoutParams; -import android.widget.FrameLayout; -import android.widget.Toolbar; - -import com.android.settingslib.R; - -import java.util.ArrayList; -import java.util.List; - -public class SettingsDrawerActivity extends Activity { - - protected static final boolean DEBUG_TIMING = false; - private static final String TAG = "SettingsDrawerActivity"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); - - public static final String EXTRA_SHOW_MENU = "show_drawer_menu"; - - // Serves as a temporary list of tiles to ignore until we heard back from the PM that they - // are disabled. - private static ArraySet<ComponentName> sTileBlacklist = new ArraySet<>(); - - private final PackageReceiver mPackageReceiver = new PackageReceiver(); - private final List<CategoryListener> mCategoryListeners = new ArrayList<>(); - - private FrameLayout mContentHeaderContainer; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - long startTime = System.currentTimeMillis(); - - TypedArray theme = getTheme().obtainStyledAttributes(android.R.styleable.Theme); - if (!theme.getBoolean(android.R.styleable.Theme_windowNoTitle, false)) { - getWindow().addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); - requestWindowFeature(Window.FEATURE_NO_TITLE); - } - super.setContentView(R.layout.settings_with_drawer); - mContentHeaderContainer = findViewById(R.id.content_header_container); - - Toolbar toolbar = findViewById(R.id.action_bar); - if (theme.getBoolean(android.R.styleable.Theme_windowNoTitle, false)) { - toolbar.setVisibility(View.GONE); - return; - } - setActionBar(toolbar); - - if (DEBUG_TIMING) { - Log.d(TAG, "onCreate took " + (System.currentTimeMillis() - startTime) - + " ms"); - } - } - - @Override - public boolean onNavigateUp() { - if (!super.onNavigateUp()) { - finish(); - } - return true; - } - - @Override - protected void onResume() { - super.onResume(); - final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); - filter.addAction(Intent.ACTION_PACKAGE_REMOVED); - filter.addAction(Intent.ACTION_PACKAGE_CHANGED); - filter.addAction(Intent.ACTION_PACKAGE_REPLACED); - filter.addDataScheme("package"); - registerReceiver(mPackageReceiver, filter); - - new CategoriesUpdateTask().execute(); - } - - @Override - protected void onPause() { - unregisterReceiver(mPackageReceiver); - super.onPause(); - } - - public void addCategoryListener(CategoryListener listener) { - mCategoryListeners.add(listener); - } - - public void remCategoryListener(CategoryListener listener) { - mCategoryListeners.remove(listener); - } - - @Override - public void setContentView(@LayoutRes int layoutResID) { - final ViewGroup parent = findViewById(R.id.content_frame); - if (parent != null) { - parent.removeAllViews(); - } - LayoutInflater.from(this).inflate(layoutResID, parent); - } - - @Override - public void setContentView(View view) { - ((ViewGroup) findViewById(R.id.content_frame)).addView(view); - } - - @Override - public void setContentView(View view, ViewGroup.LayoutParams params) { - ((ViewGroup) findViewById(R.id.content_frame)).addView(view, params); - } - - private void onCategoriesChanged() { - final int N = mCategoryListeners.size(); - for (int i = 0; i < N; i++) { - mCategoryListeners.get(i).onCategoriesChanged(); - } - } - - /** - * @return whether or not the enabled state actually changed. - */ - public boolean setTileEnabled(ComponentName component, boolean enabled) { - PackageManager pm = getPackageManager(); - int state = pm.getComponentEnabledSetting(component); - boolean isEnabled = state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED; - if (isEnabled != enabled || state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) { - if (enabled) { - sTileBlacklist.remove(component); - } else { - sTileBlacklist.add(component); - } - pm.setComponentEnabledSetting(component, enabled - ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED - : PackageManager.COMPONENT_ENABLED_STATE_DISABLED, - PackageManager.DONT_KILL_APP); - return true; - } - return false; - } - - /** - * Updates dashboard categories. Only necessary to call this after setTileEnabled - */ - public void updateCategories() { - new CategoriesUpdateTask().execute(); - } - - public String getSettingPkg() { - return TileUtils.SETTING_PKG; - } - - public interface CategoryListener { - void onCategoriesChanged(); - } - - private class CategoriesUpdateTask extends AsyncTask<Void, Void, Void> { - - private final CategoryManager mCategoryManager; - - public CategoriesUpdateTask() { - mCategoryManager = CategoryManager.get(SettingsDrawerActivity.this); - } - - @Override - protected Void doInBackground(Void... params) { - mCategoryManager.reloadAllCategories(SettingsDrawerActivity.this, getSettingPkg()); - return null; - } - - @Override - protected void onPostExecute(Void result) { - mCategoryManager.updateCategoryFromBlacklist(sTileBlacklist); - onCategoriesChanged(); - } - } - - private class PackageReceiver extends BroadcastReceiver { - @Override - public void onReceive(Context context, Intent intent) { - new CategoriesUpdateTask().execute(); - } - } -} diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/Tile.java b/packages/SettingsLib/src/com/android/settingslib/drawer/Tile.java index f1d43bfe4096..a6b24104fa90 100644 --- a/packages/SettingsLib/src/com/android/settingslib/drawer/Tile.java +++ b/packages/SettingsLib/src/com/android/settingslib/drawer/Tile.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -16,157 +16,321 @@ package com.android.settingslib.drawer; +import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_ORDER; +import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_PROFILE; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE; +import static com.android.settingslib.drawer.TileUtils.PROFILE_ALL; +import static com.android.settingslib.drawer.TileUtils.PROFILE_PRIMARY; + +import android.content.Context; import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; import android.graphics.drawable.Icon; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandle; import android.text.TextUtils; -import android.widget.RemoteViews; +import android.util.Log; import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; /** * Description of a single dashboard tile that the user can select. */ public class Tile implements Parcelable { - /** - * Title of the tile that is shown to the user. - * @attr ref android.R.styleable#PreferenceHeader_title - */ - public CharSequence title; + private static final String TAG = "Tile"; /** - * Optional summary describing what this tile controls. - * @attr ref android.R.styleable#PreferenceHeader_summary + * Optional list of user handles which the intent should be launched on. */ - public CharSequence summary; + public ArrayList<UserHandle> userHandle = new ArrayList<>(); + + private final String mActivityPackage; + private final String mActivityName; + private final Intent mIntent; + + private ActivityInfo mActivityInfo; + private CharSequence mSummaryOverride; + private Bundle mMetaData; + private String mCategory; + + public Tile(ActivityInfo activityInfo, String category) { + mActivityInfo = activityInfo; + mActivityPackage = mActivityInfo.packageName; + mActivityName = mActivityInfo.name; + mMetaData = activityInfo.metaData; + mCategory = category; + mIntent = new Intent().setClassName(mActivityPackage, mActivityName); + } + + Tile(Parcel in) { + mActivityPackage = in.readString(); + mActivityName = in.readString(); + mIntent = new Intent().setClassName(mActivityPackage, mActivityName); + final int N = in.readInt(); + for (int i = 0; i < N; i++) { + userHandle.add(UserHandle.CREATOR.createFromParcel(in)); + } + mCategory = in.readString(); + mMetaData = in.readBundle(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mActivityPackage); + dest.writeString(mActivityName); + final int N = userHandle.size(); + dest.writeInt(N); + for (int i = 0; i < N; i++) { + userHandle.get(i).writeToParcel(dest, flags); + } + dest.writeString(mCategory); + dest.writeBundle(mMetaData); + } + + public int getId() { + return Objects.hash(mActivityPackage, mActivityName); + } + + public String getDescription() { + return mActivityPackage + "/" + mActivityName; + } + + public String getPackageName() { + return mActivityPackage; + } /** - * Optional icon to show for this tile. - * @attr ref android.R.styleable#PreferenceHeader_icon + * Intent to launch when the preference is selected. */ - public Icon icon; + public Intent getIntent() { + return mIntent; + } /** - * Whether the icon can be tinted. This should be set to true for monochrome (single-color) - * icons that can be tinted to match the design. + * Category in which the tile should be placed. */ - public boolean isIconTintable; + public String getCategory() { + return mCategory; + } + + public void setCategory(String newCategoryKey) { + mCategory = newCategoryKey; + } /** - * Intent to launch when the preference is selected. + * Priority of this tile, used for display ordering. */ - public Intent intent; + public int getOrder() { + if (hasOrder()) { + return mMetaData.getInt(META_DATA_KEY_ORDER); + } else { + return 0; + } + } + + public boolean hasOrder() { + return mMetaData.containsKey(META_DATA_KEY_ORDER) + && mMetaData.get(META_DATA_KEY_ORDER) instanceof Integer; + } /** - * Optional list of user handles which the intent should be launched on. + * Title of the tile that is shown to the user. */ - public ArrayList<UserHandle> userHandle = new ArrayList<>(); + public CharSequence getTitle(Context context) { + CharSequence title = null; + final PackageManager packageManager = context.getPackageManager(); + if (mMetaData.containsKey(META_DATA_PREFERENCE_TITLE)) { + if (mMetaData.get(META_DATA_PREFERENCE_TITLE) instanceof Integer) { + try { + final Resources res = + packageManager.getResourcesForApplication(mActivityPackage); + title = res.getString(mMetaData.getInt(META_DATA_PREFERENCE_TITLE)); + } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) { + Log.d(TAG, "Couldn't find info", e); + } + } else { + title = mMetaData.getString(META_DATA_PREFERENCE_TITLE); + } + } + // Set the preference title to the activity's label if no + // meta-data is found + if (title == null) { + title = getActivityInfo(context).loadLabel(packageManager); + } + return title; + } /** - * Optional additional data for use by subclasses of the activity + * Returns the raw metadata for summary, this is used for comparing 2 summary text without + * loading the real string. */ - public Bundle extras; + public String getSummaryReference() { + if (mSummaryOverride != null) { + return mSummaryOverride.toString(); + } + if (mMetaData != null && mMetaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) { + return mMetaData.get(META_DATA_PREFERENCE_SUMMARY).toString(); + } + return null; + } /** - * Category in which the tile should be placed. + * Overrides the summary. This can happen when injected tile wants to provide dynamic summary. */ - public String category; + public void overrideSummary(CharSequence summaryOverride) { + mSummaryOverride = summaryOverride; + } /** - * Priority of the intent filter that created this tile, used for display ordering. + * Optional summary describing what this tile controls. */ - public int priority; + public CharSequence getSummary(Context context) { + if (mSummaryOverride != null) { + return mSummaryOverride; + } + CharSequence summary = null; + final PackageManager packageManager = context.getPackageManager(); + if (mMetaData != null) { + if (mMetaData.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) { + return null; + } + if (mMetaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) { + if (mMetaData.get(META_DATA_PREFERENCE_SUMMARY) instanceof Integer) { + try { + final Resources res = + packageManager.getResourcesForApplication(mActivityPackage); + summary = res.getString(mMetaData.getInt(META_DATA_PREFERENCE_SUMMARY)); + } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) { + Log.d(TAG, "Couldn't find info", e); + } + } else { + summary = mMetaData.getString(META_DATA_PREFERENCE_SUMMARY); + } + } + } + return summary; + } + + public void setMetaData(Bundle metaData) { + mMetaData = metaData; + } /** * The metaData from the activity that defines this tile. */ - public Bundle metaData; + public Bundle getMetaData() { + return mMetaData; + } /** * Optional key to use for this tile. */ - public String key; - - /** - * Optional remote view which will be displayed instead of the regular title-summary item. - */ - public RemoteViews remoteViews; - - public Tile() { - // Empty + public String getKey(Context context) { + if (!hasKey()) { + return null; + } + if (mMetaData.get(META_DATA_PREFERENCE_KEYHINT) instanceof Integer) { + return context.getResources().getString(mMetaData.getInt(META_DATA_PREFERENCE_KEYHINT)); + } else { + return mMetaData.getString(META_DATA_PREFERENCE_KEYHINT); + } } - @Override - public int describeContents() { - return 0; + public boolean hasKey() { + return mMetaData != null && mMetaData.containsKey(META_DATA_PREFERENCE_KEYHINT); } - @Override - public void writeToParcel(Parcel dest, int flags) { - TextUtils.writeToParcel(title, dest, flags); - TextUtils.writeToParcel(summary, dest, flags); - if (icon != null) { - dest.writeByte((byte) 1); - icon.writeToParcel(dest, flags); - } else { - dest.writeByte((byte) 0); + /** + * Optional icon to show for this tile. + * + * @attr ref android.R.styleable#PreferenceHeader_icon + */ + public Icon getIcon(Context context) { + if (context == null || mMetaData == null) { + return null; } - if (intent != null) { - dest.writeByte((byte) 1); - intent.writeToParcel(dest, flags); - } else { - dest.writeByte((byte) 0); + + int iconResId = mMetaData.getInt(META_DATA_PREFERENCE_ICON); + // Set the icon + if (iconResId == 0) { + // Only fallback to activityinfo.icon if metadata does not contain ICON_URI. + // ICON_URI should be loaded in app UI when need the icon object. Handling IPC at this + // level is too complex because we don't have a strong threading contract for this class + if (!mMetaData.containsKey(META_DATA_PREFERENCE_ICON_URI)) { + iconResId = getActivityInfo(context).icon; + } } - final int N = userHandle.size(); - dest.writeInt(N); - for (int i = 0; i < N; i++) { - userHandle.get(i).writeToParcel(dest, flags); + if (iconResId != 0) { + return Icon.createWithResource(getActivityInfo(context).packageName, iconResId); + } else { + return null; } - dest.writeBundle(extras); - dest.writeString(category); - dest.writeInt(priority); - dest.writeBundle(metaData); - dest.writeString(key); - dest.writeParcelable(remoteViews, flags); - dest.writeBoolean(isIconTintable); } - public void readFromParcel(Parcel in) { - title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); - summary = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); - if (in.readByte() != 0) { - icon = Icon.CREATOR.createFromParcel(in); - } - if (in.readByte() != 0) { - intent = Intent.CREATOR.createFromParcel(in); - } - final int N = in.readInt(); - for (int i = 0; i < N; i++) { - userHandle.add(UserHandle.CREATOR.createFromParcel(in)); + /** + * Whether the icon can be tinted. This is true when icon needs to be monochrome (single-color) + */ + public boolean isIconTintable(Context context) { + if (mMetaData != null + && mMetaData.containsKey(TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE)) { + return mMetaData.getBoolean(TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE); } - extras = in.readBundle(); - category = in.readString(); - priority = in.readInt(); - metaData = in.readBundle(); - key = in.readString(); - remoteViews = in.readParcelable(RemoteViews.class.getClassLoader()); - isIconTintable = in.readBoolean(); + final String pkgName = context.getPackageName(); + // If this drawable is coming from outside Settings, tint it to match the color. + final ActivityInfo activityInfo = getActivityInfo(context); + return activityInfo != null + && !TextUtils.equals(pkgName, activityInfo.packageName); } - Tile(Parcel in) { - readFromParcel(in); + private ActivityInfo getActivityInfo(Context context) { + if (mActivityInfo == null) { + final PackageManager pm = context.getApplicationContext().getPackageManager(); + final Intent intent = new Intent().setClassName(mActivityPackage, mActivityName); + final List<ResolveInfo> infoList = + pm.queryIntentActivities(intent, PackageManager.GET_META_DATA); + if (infoList != null && !infoList.isEmpty()) { + mActivityInfo = infoList.get(0).activityInfo; + } + } + return mActivityInfo; } public static final Creator<Tile> CREATOR = new Creator<Tile>() { public Tile createFromParcel(Parcel source) { return new Tile(source); } + public Tile[] newArray(int size) { return new Tile[size]; } }; + + public boolean isPrimaryProfileOnly() { + String profile = mMetaData != null ? + mMetaData.getString(META_DATA_KEY_PROFILE) : PROFILE_ALL; + profile = (profile != null ? profile : PROFILE_ALL); + return TextUtils.equals(profile, PROFILE_PRIMARY); + } + + public static final Comparator<Tile> TILE_COMPARATOR = + (lhs, rhs) -> rhs.getOrder() - lhs.getOrder(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java b/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java index 0f0e4e57886c..67cfe6bfd18a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/drawer/TileUtils.java @@ -20,11 +20,8 @@ import android.content.Context; import android.content.IContentProvider; import android.content.Intent; import android.content.pm.ActivityInfo; -import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; -import android.content.res.Resources; -import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Bundle; import android.os.RemoteException; @@ -34,21 +31,21 @@ import android.provider.Settings.Global; import android.text.TextUtils; import android.util.Log; import android.util.Pair; -import android.widget.RemoteViews; + +import androidx.annotation.VisibleForTesting; import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; public class TileUtils { - private static final boolean DEBUG = false; private static final boolean DEBUG_TIMING = false; private static final String LOG_TAG = "TileUtils"; + @VisibleForTesting + static final String SETTING_PKG = "com.android.settings"; /** * Settings will search for system activities of this action and add them as a top level @@ -65,21 +62,17 @@ public class TileUtils { * * <p>A summary my be defined by meta-data named {@link #META_DATA_PREFERENCE_SUMMARY} */ - public static final String EXTRA_SETTINGS_ACTION = - "com.android.settings.action.EXTRA_SETTINGS"; + public static final String EXTRA_SETTINGS_ACTION = "com.android.settings.action.EXTRA_SETTINGS"; /** * @See {@link #EXTRA_SETTINGS_ACTION}. */ - private static final String IA_SETTINGS_ACTION = - "com.android.settings.action.IA_SETTINGS"; - + public static final String IA_SETTINGS_ACTION = "com.android.settings.action.IA_SETTINGS"; /** * Same as #EXTRA_SETTINGS_ACTION but used for the platform Settings activities. */ - private static final String SETTINGS_ACTION = - "com.android.settings.action.SETTINGS"; + private static final String SETTINGS_ACTION = "com.android.settings.action.SETTINGS"; private static final String OPERATOR_SETTINGS = "com.android.settings.OPERATOR_APPLICATION_SETTING"; @@ -96,11 +89,7 @@ public class TileUtils { /** * The key used to get the category from metadata of activities of action * {@link #EXTRA_SETTINGS_ACTION} - * The value must be one of: - * <li>com.android.settings.category.wireless</li> - * <li>com.android.settings.category.device</li> - * <li>com.android.settings.category.personal</li> - * <li>com.android.settings.category.system</li> + * The value must be from {@link CategoryKey}. */ private static final String EXTRA_CATEGORY_KEY = "com.android.settings.category"; @@ -108,7 +97,7 @@ public class TileUtils { * The key used to get the package name of the icon resource for the preference. */ private static final String EXTRA_PREFERENCE_ICON_PACKAGE = - "com.android.settings.icon_package"; + "com.android.settings.icon_package"; /** * Name of the meta-data item that should be set in the AndroidManifest.xml @@ -117,6 +106,12 @@ public class TileUtils { public static final String META_DATA_PREFERENCE_KEYHINT = "com.android.settings.keyhint"; /** + * Order of the item that should be displayed on screen. Bigger value items displays closer on + * top. + */ + public static final String META_DATA_KEY_ORDER = "com.android.settings.order"; + + /** * Name of the meta-data item that should be set in the AndroidManifest.xml * to specify the icon that should be displayed for the preference. */ @@ -128,6 +123,12 @@ public class TileUtils { */ public static final String META_DATA_PREFERENCE_ICON_BACKGROUND_HINT = "com.android.settings.bg.hint"; + /** + * Name of the meta-data item that should be set in the AndroidManifest.xml + * to specify the icon background color as raw ARGB. + */ + public static final String META_DATA_PREFERENCE_ICON_BACKGROUND_ARGB = + "com.android.settings.bg.argb"; /** * Name of the meta-data item that should be set in the AndroidManifest.xml @@ -172,49 +173,38 @@ public class TileUtils { "com.android.settings.summary_uri"; /** - * Name of the meta-data item that should be set in the AndroidManifest.xml to specify the - * custom view which should be displayed for the preference. The custom view will be inflated - * as a remote view. + * Value for {@link #META_DATA_KEY_PROFILE}. When the device has a managed profile, + * the app will always be run in the primary profile. * - * This also can be used with {@link #META_DATA_PREFERENCE_SUMMARY_URI}, by setting the id - * of the summary TextView to '@android:id/summary'. + * @see #META_DATA_KEY_PROFILE */ - public static final String META_DATA_PREFERENCE_CUSTOM_VIEW = - "com.android.settings.custom_view"; - - public static final String SETTING_PKG = "com.android.settings"; + public static final String PROFILE_PRIMARY = "primary_profile_only"; /** - * Build a list of DashboardCategory. Each category must be defined in manifest. - * eg: .Settings$DeviceSettings - * @deprecated + * Value for {@link #META_DATA_KEY_PROFILE}. When the device has a managed profile, the user + * will be presented with a dialog to choose the profile the app will be run in. + * + * @see #META_DATA_KEY_PROFILE */ - @Deprecated - public static List<DashboardCategory> getCategories(Context context, - Map<Pair<String, String>, Tile> cache) { - return getCategories(context, cache, true /*categoryDefinedInManifest*/); - } + public static final String PROFILE_ALL = "all_profiles"; /** - * Build a list of DashboardCategory. - * @param categoryDefinedInManifest If true, an dummy activity must exists in manifest to - * represent this category (eg: .Settings$DeviceSettings) + * Name of the meta-data item that should be set in the AndroidManifest.xml + * to specify the profile in which the app should be run when the device has a managed profile. + * The default value is {@link #PROFILE_ALL} which means the user will be presented with a + * dialog to choose the profile. If set to {@link #PROFILE_PRIMARY} the app will always be + * run in the primary profile. + * + * @see #PROFILE_PRIMARY + * @see #PROFILE_ALL */ - public static List<DashboardCategory> getCategories(Context context, - Map<Pair<String, String>, Tile> cache, boolean categoryDefinedInManifest) { - return getCategories(context, cache, categoryDefinedInManifest, null, SETTING_PKG); - } + public static final String META_DATA_KEY_PROFILE = "com.android.settings.profile"; /** * Build a list of DashboardCategory. - * @param categoryDefinedInManifest If true, an dummy activity must exists in manifest to - * represent this category (eg: .Settings$DeviceSettings) - * @param extraAction additional intent filter action to be usetileutild to build the dashboard - * categories */ public static List<DashboardCategory> getCategories(Context context, - Map<Pair<String, String>, Tile> cache, boolean categoryDefinedInManifest, - String extraAction, String settingPkg) { + Map<Pair<String, String>, Tile> cache) { final long startTime = System.currentTimeMillis(); boolean setup = Global.getInt(context.getContentResolver(), Global.DEVICE_PROVISIONED, 0) != 0; @@ -224,37 +214,30 @@ public class TileUtils { // TODO: Needs much optimization, too many PM queries going on here. if (user.getIdentifier() == ActivityManager.getCurrentUser()) { // Only add Settings for this user. - getTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true, - settingPkg); + getTilesForAction(context, user, SETTINGS_ACTION, cache, null, tiles, true); getTilesForAction(context, user, OPERATOR_SETTINGS, cache, - OPERATOR_DEFAULT_CATEGORY, tiles, false, true, settingPkg); + OPERATOR_DEFAULT_CATEGORY, tiles, false); getTilesForAction(context, user, MANUFACTURER_SETTINGS, cache, - MANUFACTURER_DEFAULT_CATEGORY, tiles, false, true, settingPkg); + MANUFACTURER_DEFAULT_CATEGORY, tiles, false); } if (setup) { - getTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false, - settingPkg); - if (!categoryDefinedInManifest) { - getTilesForAction(context, user, IA_SETTINGS_ACTION, cache, null, tiles, false, - settingPkg); - if (extraAction != null) { - getTilesForAction(context, user, extraAction, cache, null, tiles, false, - settingPkg); - } - } + getTilesForAction(context, user, EXTRA_SETTINGS_ACTION, cache, null, tiles, false); + getTilesForAction(context, user, IA_SETTINGS_ACTION, cache, null, tiles, false); } } HashMap<String, DashboardCategory> categoryMap = new HashMap<>(); for (Tile tile : tiles) { - DashboardCategory category = categoryMap.get(tile.category); + final String categoryKey = tile.getCategory(); + DashboardCategory category = categoryMap.get(categoryKey); if (category == null) { - category = createCategory(context, tile.category, categoryDefinedInManifest); + category = new DashboardCategory(categoryKey); + if (category == null) { - Log.w(LOG_TAG, "Couldn't find category " + tile.category); + Log.w(LOG_TAG, "Couldn't find category " + categoryKey); continue; } - categoryMap.put(category.key, category); + categoryMap.put(categoryKey, category); } category.addTile(tile); } @@ -262,83 +245,25 @@ public class TileUtils { for (DashboardCategory category : categories) { category.sortTiles(); } - Collections.sort(categories, CATEGORY_COMPARATOR); - if (DEBUG_TIMING) Log.d(LOG_TAG, "getCategories took " - + (System.currentTimeMillis() - startTime) + " ms"); - return categories; - } - /** - * Create a new DashboardCategory from key. - * - * @param context Context to query intent - * @param categoryKey The category key - * @param categoryDefinedInManifest If true, an dummy activity must exists in manifest to - * represent this category (eg: .Settings$DeviceSettings) - */ - private static DashboardCategory createCategory(Context context, String categoryKey, - boolean categoryDefinedInManifest) { - DashboardCategory category = new DashboardCategory(); - category.key = categoryKey; - if (!categoryDefinedInManifest) { - return category; - } - PackageManager pm = context.getPackageManager(); - List<ResolveInfo> results = pm.queryIntentActivities(new Intent(categoryKey), 0); - if (results.size() == 0) { - return null; + if (DEBUG_TIMING) { + Log.d(LOG_TAG, "getCategories took " + + (System.currentTimeMillis() - startTime) + " ms"); } - for (ResolveInfo resolved : results) { - if (!resolved.system) { - // Do not allow any app to add to settings, only system ones. - continue; - } - category.title = resolved.activityInfo.loadLabel(pm); - category.priority = SETTING_PKG.equals( - resolved.activityInfo.applicationInfo.packageName) ? resolved.priority : 0; - if (DEBUG) Log.d(LOG_TAG, "Adding category " + category.title); - } - - return category; - } - - private static void getTilesForAction(Context context, - UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache, - String defaultCategory, ArrayList<Tile> outTiles, boolean requireSettings, - String settingPkg) { - getTilesForAction(context, user, action, addedCache, defaultCategory, outTiles, - requireSettings, requireSettings, settingPkg); + return categories; } - private static void getTilesForAction(Context context, + @VisibleForTesting + static void getTilesForAction(Context context, UserHandle user, String action, Map<Pair<String, String>, Tile> addedCache, - String defaultCategory, ArrayList<Tile> outTiles, boolean requireSettings, - boolean usePriority, String settingPkg) { - Intent intent = new Intent(action); + String defaultCategory, List<Tile> outTiles, boolean requireSettings) { + final Intent intent = new Intent(action); if (requireSettings) { - intent.setPackage(settingPkg); + intent.setPackage(SETTING_PKG); } - getTilesForIntent(context, user, intent, addedCache, defaultCategory, outTiles, - usePriority, true, true); - } - - public static void getTilesForIntent( - Context context, UserHandle user, Intent intent, - Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles, - boolean usePriority, boolean checkCategory, boolean forceTintExternalIcon) { - getTilesForIntent(context, user, intent, addedCache, defaultCategory, outTiles, - usePriority, checkCategory, forceTintExternalIcon, false /* shouldUpdateTiles */); - } - - public static void getTilesForIntent( - Context context, UserHandle user, Intent intent, - Map<Pair<String, String>, Tile> addedCache, String defaultCategory, List<Tile> outTiles, - boolean usePriority, boolean checkCategory, boolean forceTintExternalIcon, - boolean shouldUpdateTiles) { - PackageManager pm = context.getPackageManager(); + final PackageManager pm = context.getPackageManager(); List<ResolveInfo> results = pm.queryIntentActivitiesAsUser(intent, PackageManager.GET_META_DATA, user.getIdentifier()); - Map<String, IContentProvider> providerMap = new HashMap<>(); for (ResolveInfo resolved : results) { if (!resolved.system) { // Do not allow any app to add to settings, only system ones. @@ -349,7 +274,7 @@ public class TileUtils { String categoryKey = defaultCategory; // Load category - if (checkCategory && ((metaData == null) || !metaData.containsKey(EXTRA_CATEGORY_KEY)) + if ((metaData == null || !metaData.containsKey(EXTRA_CATEGORY_KEY)) && categoryKey == null) { Log.w(LOG_TAG, "Found " + resolved.activityInfo.name + " for intent " + intent + " missing metadata " @@ -359,22 +284,13 @@ public class TileUtils { categoryKey = metaData.getString(EXTRA_CATEGORY_KEY); } - Pair<String, String> key = new Pair<String, String>(activityInfo.packageName, - activityInfo.name); + Pair<String, String> key = new Pair<>(activityInfo.packageName, activityInfo.name); Tile tile = addedCache.get(key); if (tile == null) { - tile = new Tile(); - tile.intent = new Intent().setClassName( - activityInfo.packageName, activityInfo.name); - tile.category = categoryKey; - tile.priority = usePriority ? resolved.priority : 0; - tile.metaData = activityInfo.metaData; - updateTileData(context, tile, activityInfo, activityInfo.applicationInfo, - pm, providerMap, forceTintExternalIcon); - if (DEBUG) Log.d(LOG_TAG, "Adding tile " + tile.title); + tile = new Tile(activityInfo, categoryKey); addedCache.put(key, tile); - } else if (shouldUpdateTiles) { - updateSummaryAndTitle(context, providerMap, tile); + } else { + tile.setMetaData(metaData); } if (!tile.userHandle.contains(user)) { @@ -386,131 +302,12 @@ public class TileUtils { } } - private static boolean updateTileData(Context context, Tile tile, - ActivityInfo activityInfo, ApplicationInfo applicationInfo, PackageManager pm, - Map<String, IContentProvider> providerMap, boolean forceTintExternalIcon) { - if (applicationInfo.isSystemApp()) { - boolean forceTintIcon = false; - int icon = 0; - Pair<String, Integer> iconFromUri = null; - CharSequence title = null; - String summary = null; - String keyHint = null; - boolean isIconTintable = false; - - // Get the activity's meta-data - try { - Resources res = pm.getResourcesForApplication(applicationInfo.packageName); - Bundle metaData = activityInfo.metaData; - - if (forceTintExternalIcon - && !context.getPackageName().equals(applicationInfo.packageName)) { - isIconTintable = true; - forceTintIcon = true; - } - - if (res != null && metaData != null) { - if (metaData.containsKey(META_DATA_PREFERENCE_ICON)) { - icon = metaData.getInt(META_DATA_PREFERENCE_ICON); - } - if (metaData.containsKey(META_DATA_PREFERENCE_ICON_TINTABLE)) { - if (forceTintIcon) { - Log.w(LOG_TAG, "Ignoring icon tintable for " + activityInfo); - } else { - isIconTintable = - metaData.getBoolean(META_DATA_PREFERENCE_ICON_TINTABLE); - } - } - if (metaData.containsKey(META_DATA_PREFERENCE_TITLE)) { - if (metaData.get(META_DATA_PREFERENCE_TITLE) instanceof Integer) { - title = res.getString(metaData.getInt(META_DATA_PREFERENCE_TITLE)); - } else { - title = metaData.getString(META_DATA_PREFERENCE_TITLE); - } - } - if (metaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) { - if (metaData.get(META_DATA_PREFERENCE_SUMMARY) instanceof Integer) { - summary = res.getString(metaData.getInt(META_DATA_PREFERENCE_SUMMARY)); - } else { - summary = metaData.getString(META_DATA_PREFERENCE_SUMMARY); - } - } - if (metaData.containsKey(META_DATA_PREFERENCE_KEYHINT)) { - if (metaData.get(META_DATA_PREFERENCE_KEYHINT) instanceof Integer) { - keyHint = res.getString(metaData.getInt(META_DATA_PREFERENCE_KEYHINT)); - } else { - keyHint = metaData.getString(META_DATA_PREFERENCE_KEYHINT); - } - } - if (metaData.containsKey(META_DATA_PREFERENCE_CUSTOM_VIEW)) { - int layoutId = metaData.getInt(META_DATA_PREFERENCE_CUSTOM_VIEW); - tile.remoteViews = new RemoteViews(applicationInfo.packageName, layoutId); - updateSummaryAndTitle(context, providerMap, tile); - } - } - } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) { - if (DEBUG) Log.d(LOG_TAG, "Couldn't find info", e); - } - - // Set the preference title to the activity's label if no - // meta-data is found - if (TextUtils.isEmpty(title)) { - title = activityInfo.loadLabel(pm).toString(); - } - - // Set the icon - if (icon == 0) { - // Only fallback to activityinfo.icon if metadata does not contain ICON_URI. - // ICON_URI should be loaded in app UI when need the icon object. - if (!tile.metaData.containsKey(META_DATA_PREFERENCE_ICON_URI)) { - icon = activityInfo.icon; - } - } - if (icon != 0) { - tile.icon = Icon.createWithResource(activityInfo.packageName, icon); - } - - // Set title and summary for the preference - tile.title = title; - tile.summary = summary; - // Replace the intent with this specific activity - tile.intent = new Intent().setClassName(activityInfo.packageName, - activityInfo.name); - // Suggest a key for this tile - tile.key = keyHint; - tile.isIconTintable = isIconTintable; - - return true; - } - - return false; - } - - private static void updateSummaryAndTitle( - Context context, Map<String, IContentProvider> providerMap, Tile tile) { - if (tile == null || tile.metaData == null - || !tile.metaData.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) { - return; - } - - String uriString = tile.metaData.getString(META_DATA_PREFERENCE_SUMMARY_URI); - Bundle bundle = getBundleFromUri(context, uriString, providerMap); - String overrideSummary = getString(bundle, META_DATA_PREFERENCE_SUMMARY); - String overrideTitle = getString(bundle, META_DATA_PREFERENCE_TITLE); - if (overrideSummary != null) { - tile.remoteViews.setTextViewText(android.R.id.summary, overrideSummary); - } - - if (overrideTitle != null) { - tile.remoteViews.setTextViewText(android.R.id.title, overrideTitle); - } - } - /** * Gets the icon package name and resource id from content provider. - * @param context context + * + * @param context context * @param packageName package name of the target activity - * @param uriString URI for the content provider + * @param uriString URI for the content provider * @param providerMap Maps URI authorities to providers * @return package name and resource id of the icon specified */ @@ -538,10 +335,11 @@ public class TileUtils { /** * Gets text associated with the input key from the content provider. - * @param context context - * @param uriString URI for the content provider + * + * @param context context + * @param uriString URI for the content provider * @param providerMap Maps URI authorities to providers - * @param key Key mapping to the text in bundle returned by the content provider + * @param key Key mapping to the text in bundle returned by the content provider * @return Text associated with the key, if returned by the content provider */ public static String getTextFromUri(Context context, String uriString, @@ -571,10 +369,6 @@ public class TileUtils { } } - private static String getString(Bundle bundle, String key) { - return bundle == null ? null : bundle.getString(key); - } - private static IContentProvider getProviderFromUri(Context context, Uri uri, Map<String, IContentProvider> providerMap) { if (uri == null) { @@ -601,12 +395,4 @@ public class TileUtils { } return pathSegments.get(0); } - - private static final Comparator<DashboardCategory> CATEGORY_COMPARATOR = - new Comparator<DashboardCategory>() { - @Override - public int compare(DashboardCategory lhs, DashboardCategory rhs) { - return rhs.priority - lhs.priority; - } - }; } diff --git a/packages/SettingsLib/src/com/android/settingslib/drawer/UserAdapter.java b/packages/SettingsLib/src/com/android/settingslib/drawer/UserAdapter.java deleted file mode 100644 index 8a09df2849c9..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/drawer/UserAdapter.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright (C) 2014 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.drawer; - -import android.app.ActivityManager; - -import android.content.Context; -import android.content.pm.UserInfo; -import android.database.DataSetObserver; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.os.UserHandle; -import android.os.UserManager; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.ListAdapter; -import android.widget.SpinnerAdapter; -import android.widget.TextView; -import com.android.internal.util.UserIcons; -import com.android.settingslib.drawable.UserIconDrawable; - -import com.android.settingslib.R; - -import java.util.ArrayList; -import java.util.List; - -/** - * Adapter for a spinner that shows a list of users. - */ -public class UserAdapter implements SpinnerAdapter, ListAdapter { - /** Holder for user details */ - public static class UserDetails { - private final UserHandle mUserHandle; - private final String mName; - private final Drawable mIcon; - - public UserDetails(UserHandle userHandle, UserManager um, Context context) { - mUserHandle = userHandle; - UserInfo userInfo = um.getUserInfo(mUserHandle.getIdentifier()); - Drawable icon; - if (userInfo.isManagedProfile()) { - mName = context.getString(R.string.managed_user_title); - icon = context.getDrawable( - com.android.internal.R.drawable.ic_corp_badge); - } else { - mName = userInfo.name; - final int userId = userInfo.id; - if (um.getUserIcon(userId) != null) { - icon = new BitmapDrawable(context.getResources(), um.getUserIcon(userId)); - } else { - icon = UserIcons.getDefaultUserIcon( - context.getResources(), userId, /* light= */ false); - } - } - this.mIcon = encircle(context, icon); - } - - private static Drawable encircle(Context context, Drawable icon) { - return new UserIconDrawable(UserIconDrawable.getSizeForList(context)) - .setIconDrawable(icon).bake(); - } - } - private ArrayList<UserDetails> data; - private final LayoutInflater mInflater; - - public UserAdapter(Context context, ArrayList<UserDetails> users) { - if (users == null) { - throw new IllegalArgumentException("A list of user details must be provided"); - } - this.data = users; - mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - } - - public UserHandle getUserHandle(int position) { - if (position < 0 || position >= data.size()) { - return null; - } - return data.get(position).mUserHandle; - } - - @Override - public View getDropDownView(int position, View convertView, ViewGroup parent) { - final View row = convertView != null ? convertView : createUser(parent); - - UserDetails user = data.get(position); - ((ImageView) row.findViewById(android.R.id.icon)).setImageDrawable(user.mIcon); - ((TextView) row.findViewById(android.R.id.title)).setText(getTitle(user)); - return row; - } - - private int getTitle(UserDetails user) { - int userHandle = user.mUserHandle.getIdentifier(); - if (userHandle == UserHandle.USER_CURRENT - || userHandle == ActivityManager.getCurrentUser()) { - return R.string.category_personal; - } else { - return R.string.category_work; - } - } - - private View createUser(ViewGroup parent) { - return mInflater.inflate(R.layout.user_preference, parent, false); - } - - @Override - public void registerDataSetObserver(DataSetObserver observer) { - // We don't support observers - } - - @Override - public void unregisterDataSetObserver(DataSetObserver observer) { - // We don't support observers - } - - @Override - public int getCount() { - return data.size(); - } - - @Override - public UserDetails getItem(int position) { - return data.get(position); - } - - @Override - public long getItemId(int position) { - return data.get(position).mUserHandle.getIdentifier(); - } - - @Override - public boolean hasStableIds() { - return false; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - return getDropDownView(position, convertView, parent); - } - - @Override - public int getItemViewType(int position) { - return 0; - } - - @Override - public int getViewTypeCount() { - return 1; - } - - @Override - public boolean isEmpty() { - return data.isEmpty(); - } - - @Override - public boolean areAllItemsEnabled() { - return true; - } - - @Override - public boolean isEnabled(int position) { - return true; - } - - /** - * Creates a {@link UserAdapter} if there is more than one profile on the device. - * - * <p> The adapter can be used to populate a spinner that switches between the Settings - * app on the different profiles. - * - * @return a {@link UserAdapter} or null if there is only one profile. - */ - public static UserAdapter createUserSpinnerAdapter(UserManager userManager, - Context context) { - List<UserHandle> userProfiles = userManager.getUserProfiles(); - if (userProfiles.size() < 2) { - return null; - } - - UserHandle myUserHandle = new UserHandle(UserHandle.myUserId()); - // The first option should be the current profile - userProfiles.remove(myUserHandle); - userProfiles.add(0, myUserHandle); - - return createUserAdapter(userManager, context, userProfiles); - } - - public static UserAdapter createUserAdapter(UserManager userManager, - Context context, List<UserHandle> userProfiles) { - ArrayList<UserDetails> userDetails = new ArrayList<UserDetails>(userProfiles.size()); - final int count = userProfiles.size(); - for (int i = 0; i < count; i++) { - userDetails.add(new UserDetails(userProfiles.get(i), userManager, context)); - } - return new UserAdapter(context, userDetails); - } -} diff --git a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java index 25e590b34e3b..9feacac60365 100644 --- a/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java +++ b/packages/SettingsLib/src/com/android/settingslib/fuelgauge/PowerWhitelistBackend.java @@ -23,12 +23,13 @@ import android.content.pm.PackageManager; import android.os.IDeviceIdleController; import android.os.RemoteException; import android.os.ServiceManager; -import androidx.annotation.VisibleForTesting; import android.telecom.DefaultDialerManager; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; +import androidx.annotation.VisibleForTesting; + import com.android.internal.telephony.SmsApplication; import com.android.internal.util.ArrayUtils; diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/BatteryMeterDrawableBase.java b/packages/SettingsLib/src/com/android/settingslib/graph/BatteryMeterDrawableBase.java index 2fc6db08c7f9..17c2b02ed576 100644 --- a/packages/SettingsLib/src/com/android/settingslib/graph/BatteryMeterDrawableBase.java +++ b/packages/SettingsLib/src/com/android/settingslib/graph/BatteryMeterDrawableBase.java @@ -107,7 +107,8 @@ public class BatteryMeterDrawableBase extends Drawable { for (int i = 0; i < N; i++) { mColors[2 * i] = levels.getInt(i, 0); if (colors.getType(i) == TypedValue.TYPE_ATTRIBUTE) { - mColors[2 * i + 1] = Utils.getColorAttr(context, colors.getThemeAttributeId(i, 0)); + mColors[2 * i + 1] = Utils.getColorAttrDefaultColor(context, + colors.getThemeAttributeId(i, 0)); } else { mColors[2 * i + 1] = colors.getColor(i, 0); } @@ -149,14 +150,16 @@ public class BatteryMeterDrawableBase extends Drawable { mWarningTextPaint.setColor(mColors[1]); } - mChargeColor = Utils.getDefaultColor(mContext, R.color.meter_consumed_color); + mChargeColor = Utils.getColorStateListDefaultColor(mContext, R.color.meter_consumed_color); mBoltPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mBoltPaint.setColor(Utils.getDefaultColor(mContext, R.color.batterymeter_bolt_color)); + mBoltPaint.setColor(Utils.getColorStateListDefaultColor(mContext, + R.color.batterymeter_bolt_color)); mBoltPoints = loadPoints(res, R.array.batterymeter_bolt_points); mPlusPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - mPlusPaint.setColor(Utils.getDefaultColor(mContext, R.color.batterymeter_plus_color)); + mPlusPaint.setColor(Utils.getColorStateListDefaultColor(mContext, + R.color.batterymeter_plus_color)); mPlusPoints = loadPoints(res, R.array.batterymeter_plus_points); mPowersavePaint = new Paint(Paint.ANTI_ALIAS_FLAG); @@ -207,6 +210,10 @@ public class BatteryMeterDrawableBase extends Drawable { postInvalidate(); } + public boolean getPowerSave() { + return mPowerSaveEnabled; + } + protected void setPowerSaveAsColorError(boolean asError) { mPowerSaveAsColorError = asError; } diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/BluetoothDeviceLayerDrawable.java b/packages/SettingsLib/src/com/android/settingslib/graph/BluetoothDeviceLayerDrawable.java index 17523013a94f..f01eb2a5606e 100644 --- a/packages/SettingsLib/src/com/android/settingslib/graph/BluetoothDeviceLayerDrawable.java +++ b/packages/SettingsLib/src/com/android/settingslib/graph/BluetoothDeviceLayerDrawable.java @@ -19,18 +19,13 @@ package com.android.settingslib.graph; import android.annotation.NonNull; import android.content.Context; import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Matrix; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; -import android.graphics.Rect; -import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; -import androidx.annotation.VisibleForTesting; import android.view.Gravity; -import android.view.View; + +import androidx.annotation.VisibleForTesting; import com.android.settingslib.R; import com.android.settingslib.Utils; @@ -117,7 +112,8 @@ public class BluetoothDeviceLayerDrawable extends LayerDrawable { R.fraction.bt_battery_button_height_fraction, 1, 1); mAspectRatio = resources.getFraction(R.fraction.bt_battery_ratio_fraction, 1, 1); - final int tintColor = Utils.getColorAttr(context, android.R.attr.colorControlNormal); + final int tintColor = Utils.getColorAttrDefaultColor(context, + android.R.attr.colorControlNormal); setColorFilter(new PorterDuffColorFilter(tintColor, PorterDuff.Mode.SRC_IN)); setBatteryLevel(batteryLevel); mFrameColor = frameColor; diff --git a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java index 846e30d50063..a6b57550c358 100644 --- a/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java +++ b/packages/SettingsLib/src/com/android/settingslib/graph/SignalDrawable.java @@ -132,13 +132,17 @@ public class SignalDrawable extends Drawable { public SignalDrawable(Context context) { mDarkModeBackgroundColor = - Utils.getDefaultColor(context, R.color.dark_mode_icon_color_dual_tone_background); + Utils.getColorStateListDefaultColor(context, + R.color.dark_mode_icon_color_dual_tone_background); mDarkModeFillColor = - Utils.getDefaultColor(context, R.color.dark_mode_icon_color_dual_tone_fill); + Utils.getColorStateListDefaultColor(context, + R.color.dark_mode_icon_color_dual_tone_fill); mLightModeBackgroundColor = - Utils.getDefaultColor(context, R.color.light_mode_icon_color_dual_tone_background); + Utils.getColorStateListDefaultColor(context, + R.color.light_mode_icon_color_dual_tone_background); mLightModeFillColor = - Utils.getDefaultColor(context, R.color.light_mode_icon_color_dual_tone_fill); + Utils.getColorStateListDefaultColor(context, + R.color.light_mode_icon_color_dual_tone_fill); mIntrinsicSize = context.getResources().getDimensionPixelSize(R.dimen.signal_icon_size); mHandler = new Handler(); diff --git a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeEnablerManager.java b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeEnablerManager.java index 628e70af8318..117b48ff852d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeEnablerManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeEnablerManager.java @@ -19,16 +19,17 @@ package com.android.settingslib.inputmethod; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Configuration; -import androidx.preference.PreferenceFragment; -import androidx.preference.Preference; -import androidx.preference.PreferenceCategory; -import androidx.preference.PreferenceScreen; -import androidx.preference.TwoStatePreference; import android.text.TextUtils; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; +import androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragment; +import androidx.preference.PreferenceScreen; +import androidx.preference.TwoStatePreference; + import com.android.settingslib.R; import java.text.Collator; 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..6f8c7ac22077 --- /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 androidx.preference.Preference; +import androidx.preference.PreferenceCategory; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; +import androidx.preference.TwoStatePreference; + +import com.android.settingslib.R; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +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/InputMethodAndSubtypeUtil.java b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtil.java index 0a5c28c7b127..057123bcd224 100644 --- a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtil.java +++ b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtil.java @@ -25,17 +25,17 @@ import android.content.res.Configuration; import android.icu.text.ListFormatter; import android.provider.Settings; import android.provider.Settings.SettingNotFoundException; -import androidx.preference.PreferenceFragment; -import androidx.preference.Preference; -import androidx.preference.PreferenceScreen; -import androidx.preference.TwoStatePreference; import android.text.TextUtils; import android.util.Log; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragment; +import androidx.preference.PreferenceScreen; +import androidx.preference.TwoStatePreference; + import com.android.internal.app.LocaleHelper; -import com.android.internal.inputmethod.InputMethodUtils; import java.util.HashMap; import java.util.HashSet; @@ -49,6 +49,7 @@ public class InputMethodAndSubtypeUtil { private static final boolean DEBUG = false; private static final String TAG = "InputMethdAndSubtypeUtl"; + 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; @@ -61,7 +62,7 @@ public class InputMethodAndSubtypeUtil { // InputMethods and subtypes are saved in the settings as follows: // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1 - private static String buildInputMethodsAndSubtypesString( + public static String buildInputMethodsAndSubtypesString( final HashMap<String, HashSet<String>> imeToSubtypesMap) { final StringBuilder builder = new StringBuilder(); for (final String imi : imeToSubtypesMap.keySet()) { @@ -106,7 +107,7 @@ public class InputMethodAndSubtypeUtil { } // Needs to modify InputMethodManageService if you want to change the format of saved string. - private static HashMap<String, HashSet<String>> getEnabledInputMethodsAndSubtypeList( + static HashMap<String, HashSet<String>> getEnabledInputMethodsAndSubtypeList( ContentResolver resolver) { final String enabledInputMethodsStr = Settings.Secure.getString( resolver, Settings.Secure.ENABLED_INPUT_METHODS); @@ -116,7 +117,7 @@ public class InputMethodAndSubtypeUtil { return parseInputMethodsAndSubtypesString(enabledInputMethodsStr); } - private static HashMap<String, HashSet<String>> parseInputMethodsAndSubtypesString( + public static HashMap<String, HashSet<String>> parseInputMethodsAndSubtypesString( final String inputMethodsAndSubtypesString) { final HashMap<String, HashSet<String>> subtypesMap = new HashMap<>(); if (TextUtils.isEmpty(inputMethodsAndSubtypesString)) { @@ -176,9 +177,9 @@ public class InputMethodAndSubtypeUtil { ((TwoStatePreference) pref).isChecked() : enabledIMEsAndSubtypesMap.containsKey(imiId); final boolean isCurrentInputMethod = imiId.equals(currentInputMethodId); - final boolean systemIme = InputMethodUtils.isSystemIme(imi); + final boolean systemIme = imi.isSystem(); if ((!hasHardKeyboard && InputMethodSettingValuesWrapper.getInstance( - context.getActivity()).isAlwaysCheckedIme(imi, context.getActivity())) + context.getActivity()).isAlwaysCheckedIme(imi)) || isImeChecked) { if (!enabledIMEsAndSubtypesMap.containsKey(imiId)) { // imiId has just been enabled @@ -417,4 +418,19 @@ public class InputMethodAndSubtypeUtil { } 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/inputmethod/InputMethodAndSubtypeUtilCompat.java b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodAndSubtypeUtilCompat.java new file mode 100644 index 000000000000..692dfbff50f6 --- /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 androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; +import androidx.preference.TwoStatePreference; + +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; + +// 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/inputmethod/InputMethodPreference.java b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodPreference.java index 2e34dc5cac83..5c126b1d839b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodPreference.java @@ -24,9 +24,6 @@ import android.content.Context; import android.content.Intent; import android.content.res.Configuration; import android.os.UserHandle; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceChangeListener; -import androidx.preference.Preference.OnPreferenceClickListener; import android.text.TextUtils; import android.util.Log; import android.view.inputmethod.InputMethodInfo; @@ -34,10 +31,13 @@ import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; import android.widget.Toast; +import androidx.preference.Preference; +import androidx.preference.Preference.OnPreferenceChangeListener; +import androidx.preference.Preference.OnPreferenceClickListener; + import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.inputmethod.InputMethodUtils; import com.android.settingslib.R; -import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedSwitchPreference; import java.text.Collator; @@ -124,8 +124,8 @@ public class InputMethodPreference extends RestrictedSwitchPreference implements setIntent(intent); } mInputMethodSettingValues = InputMethodSettingValuesWrapper.getInstance(context); - mHasPriorityInSorting = InputMethodUtils.isSystemIme(imi) - && mInputMethodSettingValues.isValidSystemNonAuxAsciiCapableIme(imi, context); + mHasPriorityInSorting = imi.isSystem() + && InputMethodAndSubtypeUtil.isValidSystemNonAuxAsciiCapableIme(imi); setOnPreferenceClickListener(this); setOnPreferenceChangeListener(this); } @@ -153,7 +153,7 @@ public class InputMethodPreference extends RestrictedSwitchPreference implements setCheckedInternal(false); return false; } - if (InputMethodUtils.isSystemIme(mImi)) { + if (mImi.isSystem()) { // Enable a system IME. No need to show a security warning dialog, // but we might need to prompt if it's not Direct Boot aware. // TV doesn't doesn't need to worry about this, but other platforms should show @@ -198,8 +198,7 @@ public class InputMethodPreference extends RestrictedSwitchPreference implements } public void updatePreferenceViews() { - final boolean isAlwaysChecked = mInputMethodSettingValues.isAlwaysCheckedIme( - mImi, getContext()); + final boolean isAlwaysChecked = mInputMethodSettingValues.isAlwaysCheckedIme(mImi); // When this preference has a switch and an input method should be always enabled, // this preference should be disabled to prevent accidentally disabling an input method. // This preference should also be disabled in case the admin does not allow this input @@ -209,7 +208,7 @@ public class InputMethodPreference extends RestrictedSwitchPreference implements setEnabled(false); } else if (!mIsAllowedByOrganization) { EnforcedAdmin admin = - RestrictedLockUtils.checkIfInputMethodDisallowed(getContext(), + RestrictedLockUtilsInternal.checkIfInputMethodDisallowed(getContext(), mImi.getPackageName(), UserHandle.myUserId()); setDisabledByAdmin(admin); } else { diff --git a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodSettingValuesWrapper.java b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodSettingValuesWrapper.java index fac50bda3c69..b6786d424e6f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodSettingValuesWrapper.java +++ b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodSettingValuesWrapper.java @@ -16,39 +16,33 @@ package com.android.settingslib.inputmethod; -import android.app.ActivityManager; +import android.annotation.UiThread; +import android.content.ContentResolver; import android.content.Context; -import android.os.RemoteException; import android.util.Log; -import android.util.Slog; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; -import android.view.inputmethod.InputMethodSubtype; - -import com.android.internal.inputmethod.InputMethodUtils; -import com.android.internal.inputmethod.InputMethodUtils.InputMethodSettings; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Locale; /** - * This class is a wrapper for InputMethodSettings. You need to refresh internal states - * manually on some events when "InputMethodInfo"s and "InputMethodSubtype"s can be - * changed. + * This class is a wrapper for {@link InputMethodManager} and + * {@link android.provider.Settings.Secure#ENABLED_INPUT_METHODS}. You need to refresh internal + * states manually on some events when "InputMethodInfo"s and "InputMethodSubtype"s can be changed. + * + * <p>TODO: Consolidate this with {@link InputMethodAndSubtypeUtil}.</p> */ -// TODO: Consolidate this with {@link InputMethodAndSubtypeUtil}. +@UiThread public class InputMethodSettingValuesWrapper { private static final String TAG = InputMethodSettingValuesWrapper.class.getSimpleName(); private static volatile InputMethodSettingValuesWrapper sInstance; private final ArrayList<InputMethodInfo> mMethodList = new ArrayList<>(); - private final HashMap<String, InputMethodInfo> mMethodMap = new HashMap<>(); - private final InputMethodSettings mSettings; + private final ContentResolver mContentResolver; private final InputMethodManager mImm; - private final HashSet<InputMethodInfo> mAsciiCapableEnabledImis = new HashSet<>(); public static InputMethodSettingValuesWrapper getInstance(Context context) { if (sInstance == null) { @@ -61,87 +55,42 @@ public class InputMethodSettingValuesWrapper { return sInstance; } - private static int getDefaultCurrentUserId() { - try { - return ActivityManager.getService().getCurrentUser().id; - } catch (RemoteException e) { - Slog.w(TAG, "Couldn't get current user ID; guessing it's 0", e); - } - return 0; - } - // Ensure singleton private InputMethodSettingValuesWrapper(Context context) { - mSettings = new InputMethodSettings(context.getResources(), context.getContentResolver(), - mMethodMap, mMethodList, getDefaultCurrentUserId(), false /* copyOnWrite */); - mImm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + mContentResolver = context.getContentResolver(); + mImm = context.getSystemService(InputMethodManager.class); refreshAllInputMethodAndSubtypes(); } public void refreshAllInputMethodAndSubtypes() { - synchronized (mMethodMap) { - mMethodList.clear(); - mMethodMap.clear(); - final List<InputMethodInfo> imms = mImm.getInputMethodList(); - mMethodList.addAll(imms); - for (InputMethodInfo imi : imms) { - mMethodMap.put(imi.getId(), imi); - } - updateAsciiCapableEnabledImis(); - } - } - - // TODO: Add a cts to ensure at least one AsciiCapableSubtypeEnabledImis exist - private void updateAsciiCapableEnabledImis() { - synchronized (mMethodMap) { - mAsciiCapableEnabledImis.clear(); - final List<InputMethodInfo> enabledImis = mSettings.getEnabledInputMethodListLocked(); - for (final InputMethodInfo imi : enabledImis) { - final int subtypeCount = imi.getSubtypeCount(); - for (int i = 0; i < subtypeCount; ++i) { - final InputMethodSubtype subtype = imi.getSubtypeAt(i); - if (InputMethodUtils.SUBTYPE_MODE_KEYBOARD.equalsIgnoreCase(subtype.getMode()) - && subtype.isAsciiCapable()) { - mAsciiCapableEnabledImis.add(imi); - break; - } - } - } - } + mMethodList.clear(); + mMethodList.addAll(mImm.getInputMethodList()); } public List<InputMethodInfo> getInputMethodList() { - synchronized (mMethodMap) { - return mMethodList; - } + return new ArrayList<>(mMethodList); } - public boolean isAlwaysCheckedIme(InputMethodInfo imi, Context context) { + public boolean isAlwaysCheckedIme(InputMethodInfo imi) { final boolean isEnabled = isEnabledImi(imi); - synchronized (mMethodMap) { - if (mSettings.getEnabledInputMethodListLocked().size() <= 1 && isEnabled) { - return true; - } + if (getEnabledInputMethodList().size() <= 1 && isEnabled) { + return true; } final int enabledValidSystemNonAuxAsciiCapableImeCount = - getEnabledValidSystemNonAuxAsciiCapableImeCount(context); + getEnabledValidSystemNonAuxAsciiCapableImeCount(); return enabledValidSystemNonAuxAsciiCapableImeCount <= 1 && !(enabledValidSystemNonAuxAsciiCapableImeCount == 1 && !isEnabled) - && InputMethodUtils.isSystemIme(imi) - && isValidSystemNonAuxAsciiCapableIme(imi, context); - + && imi.isSystem() + && InputMethodAndSubtypeUtil.isValidSystemNonAuxAsciiCapableIme(imi); } - private int getEnabledValidSystemNonAuxAsciiCapableImeCount(Context context) { + private int getEnabledValidSystemNonAuxAsciiCapableImeCount() { int count = 0; - final List<InputMethodInfo> enabledImis; - synchronized (mMethodMap) { - enabledImis = mSettings.getEnabledInputMethodListLocked(); - } + final List<InputMethodInfo> enabledImis = getEnabledInputMethodList(); for (final InputMethodInfo imi : enabledImis) { - if (isValidSystemNonAuxAsciiCapableIme(imi, context)) { + if (InputMethodAndSubtypeUtil.isValidSystemNonAuxAsciiCapableIme(imi)) { ++count; } } @@ -152,10 +101,7 @@ public class InputMethodSettingValuesWrapper { } public boolean isEnabledImi(InputMethodInfo imi) { - final List<InputMethodInfo> enabledImis; - synchronized (mMethodMap) { - enabledImis = mSettings.getEnabledInputMethodListLocked(); - } + final List<InputMethodInfo> enabledImis = getEnabledInputMethodList(); for (final InputMethodInfo tempImi : enabledImis) { if (tempImi.getId().equals(imi.getId())) { return true; @@ -164,22 +110,22 @@ public class InputMethodSettingValuesWrapper { return false; } - public boolean isValidSystemNonAuxAsciiCapableIme(InputMethodInfo imi, Context context) { - if (imi.isAuxiliaryIme()) { - return false; - } - final Locale systemLocale = context.getResources().getConfiguration().locale; - if (InputMethodUtils.isSystemImeThatHasSubtypeOf(imi, context, - true /* checkDefaultAttribute */, systemLocale, false /* checkCountry */, - InputMethodUtils.SUBTYPE_MODE_ANY)) { - return true; - } - if (mAsciiCapableEnabledImis.isEmpty()) { - Log.w(TAG, "ascii capable subtype enabled imi not found. Fall back to English" - + " Keyboard subtype."); - return InputMethodUtils.containsSubtypeOf(imi, Locale.ENGLISH, false /* checkCountry */, - InputMethodUtils.SUBTYPE_MODE_KEYBOARD); + /** + * Returns the list of the enabled {@link InputMethodInfo} determined by + * {@link android.provider.Settings.Secure#ENABLED_INPUT_METHODS} rather than just returning + * {@link InputMethodManager#getEnabledInputMethodList()}. + * + * @return the list of the enabled {@link InputMethodInfo} + */ + private ArrayList<InputMethodInfo> getEnabledInputMethodList() { + final HashMap<String, HashSet<String>> enabledInputMethodsAndSubtypes = + InputMethodAndSubtypeUtil.getEnabledInputMethodsAndSubtypeList(mContentResolver); + final ArrayList<InputMethodInfo> result = new ArrayList<>(); + for (InputMethodInfo imi : mMethodList) { + if (enabledInputMethodsAndSubtypes.keySet().contains(imi.getId())) { + result.add(imi); + } } - return mAsciiCapableEnabledImis.contains(imi); + return result; } } diff --git a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodSubtypePreference.java b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodSubtypePreference.java index a0b1df25baf6..69bfffb55616 100644 --- a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodSubtypePreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodSubtypePreference.java @@ -17,13 +17,13 @@ package com.android.settingslib.inputmethod; import android.content.Context; -import androidx.preference.Preference; import android.text.TextUtils; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; +import androidx.preference.Preference; + import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.inputmethod.InputMethodUtils; import java.text.Collator; import java.util.Locale; @@ -42,7 +42,7 @@ public class InputMethodSubtypePreference extends SwitchWithNoTextPreference { this(context, imi.getId() + subtype.hashCode(), InputMethodAndSubtypeUtil.getSubtypeLocaleNameAsSentence(subtype, context, imi), - subtype.getLocale(), + subtype.getLocaleObject(), context.getResources().getConfiguration().locale); } @@ -51,20 +51,19 @@ public class InputMethodSubtypePreference extends SwitchWithNoTextPreference { final Context context, final String prefKey, final CharSequence title, - final String subtypeLocaleString, + final Locale subtypeLocale, final Locale systemLocale) { super(context); setPersistent(false); setKey(prefKey); setTitle(title); - if (TextUtils.isEmpty(subtypeLocaleString)) { + if (subtypeLocale == null) { mIsSystemLocale = false; mIsSystemLanguage = false; } else { - mIsSystemLocale = subtypeLocaleString.equals(systemLocale.toString()); + mIsSystemLocale = subtypeLocale.equals(systemLocale); mIsSystemLanguage = mIsSystemLocale - || InputMethodUtils.getLanguageFromLocaleString(subtypeLocaleString) - .equals(systemLocale.getLanguage()); + || TextUtils.equals(subtypeLocale.getLanguage(), systemLocale.getLanguage()); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/inputmethod/SwitchWithNoTextPreference.java b/packages/SettingsLib/src/com/android/settingslib/inputmethod/SwitchWithNoTextPreference.java index 2b38697a65ce..2360298822ba 100644 --- a/packages/SettingsLib/src/com/android/settingslib/inputmethod/SwitchWithNoTextPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/inputmethod/SwitchWithNoTextPreference.java @@ -17,6 +17,7 @@ package com.android.settingslib.inputmethod; import android.content.Context; + import androidx.preference.SwitchPreference; public class SwitchWithNoTextPreference extends SwitchPreference { diff --git a/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXml.java b/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXml.java index b30950916136..9db4a35c1d78 100644 --- a/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXml.java +++ b/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlGeneratorFromXml.java @@ -16,11 +16,12 @@ package com.android.settingslib.license; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Log; import android.util.Xml; +import androidx.annotation.VisibleForTesting; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -45,7 +46,7 @@ import java.util.zip.GZIPInputStream; * TODO: Remove duplicate codes once backward support ends. */ class LicenseHtmlGeneratorFromXml { - private static final String TAG = "LicenseHtmlGeneratorFromXml"; + private static final String TAG = "LicenseGeneratorFromXml"; private static final String TAG_ROOT = "licenses"; private static final String TAG_FILE_NAME = "file-name"; diff --git a/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlLoader.java b/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlLoader.java index 03cbad72c9b0..8c03918ce266 100644 --- a/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlLoader.java +++ b/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlLoader.java @@ -17,15 +17,10 @@ package com.android.settingslib.license; import android.content.Context; -import androidx.annotation.VisibleForTesting; -import android.util.Log; -import com.android.settingslib.R; import com.android.settingslib.utils.AsyncLoader; import java.io.File; -import java.util.ArrayList; -import java.util.List; /** * LicenseHtmlLoader is a loader which loads a license html file from default license xml files. @@ -33,15 +28,6 @@ import java.util.List; public class LicenseHtmlLoader extends AsyncLoader<File> { private static final String TAG = "LicenseHtmlLoader"; - 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", - "/product/etc/NOTICE.xml.gz", - "/product_services/etc/NOTICE.xml.gz"}; - private static final String NOTICE_HTML_FILE_NAME = "NOTICE.html"; - private Context mContext; public LicenseHtmlLoader(Context context) { @@ -51,64 +37,10 @@ public class LicenseHtmlLoader extends AsyncLoader<File> { @Override public File loadInBackground() { - return generateHtmlFromDefaultXmlFiles(); + return new LicenseHtmlLoaderCompat(mContext).loadInBackground(); } @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, - mContext.getString(R.string.notice_header)); - } } 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..0b6996365372 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlLoaderCompat.java @@ -0,0 +1,109 @@ +/* + * 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.R; +import com.android.settingslib.utils.AsyncLoaderCompat; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * 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"; + + 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", + "/product/etc/NOTICE.xml.gz", + "/product_services/etc/NOTICE.xml.gz"}; + static final String NOTICE_HTML_FILE_NAME = "NOTICE.html"; + + private final 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(mContext); + if (!isCachedHtmlFileOutdated(xmlFiles, cachedHtmlFile) + || generateHtmlFile(mContext, xmlFiles, cachedHtmlFile)) { + return cachedHtmlFile; + } + + return null; + } + + private 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; + } + + private File getCachedHtmlFile(Context context) { + return new File(context.getCacheDir(), NOTICE_HTML_FILE_NAME); + } + + private 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; + } + + private boolean generateHtmlFile(Context context, List<File> xmlFiles, File htmlFile) { + return LicenseHtmlGeneratorFromXml.generateHtml(xmlFiles, htmlFile, + context.getString(R.string.notice_header)); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/location/InjectedSetting.java b/packages/SettingsLib/src/com/android/settingslib/location/InjectedSetting.java new file mode 100644 index 000000000000..1805f1a90d10 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/location/InjectedSetting.java @@ -0,0 +1,191 @@ +/* + * 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.location; + +import android.content.Intent; +import android.os.UserHandle; +import android.text.TextUtils; +import android.util.Log; + +import com.android.internal.annotations.Immutable; + +import java.util.Objects; + +/** + * Specifies a setting that is being injected into Settings > Location > Location services. + * + * @see android.location.SettingInjectorService + */ +@Immutable +public class InjectedSetting { + + /** + * Package for the subclass of {@link android.location.SettingInjectorService} and for the + * settings activity. + */ + public final String packageName; + + /** + * Class name for the subclass of {@link android.location.SettingInjectorService} that + * specifies dynamic values for the location setting. + */ + public final String className; + + /** + * The {@link android.support.v7.preference.Preference#getTitle()} value. + */ + public final String title; + + /** + * The {@link android.support.v7.preference.Preference#getIcon()} value. + */ + public final int iconId; + + /** + * The user/profile associated with this setting (e.g. managed profile) + */ + public final UserHandle mUserHandle; + + /** + * The activity to launch to allow the user to modify the settings value. Assumed to be in the + * {@link #packageName} package. + */ + public final String settingsActivity; + + /** + * The user restriction associated with this setting. + */ + public final String userRestriction; + + private InjectedSetting(Builder builder) { + this.packageName = builder.mPackageName; + this.className = builder.mClassName; + this.title = builder.mTitle; + this.iconId = builder.mIconId; + this.mUserHandle = builder.mUserHandle; + this.settingsActivity = builder.mSettingsActivity; + this.userRestriction = builder.mUserRestriction; + } + + @Override + public String toString() { + return "InjectedSetting{" + + "mPackageName='" + packageName + '\'' + + ", mClassName='" + className + '\'' + + ", label=" + title + + ", iconId=" + iconId + + ", userId=" + mUserHandle.getIdentifier() + + ", settingsActivity='" + settingsActivity + '\'' + + ", userRestriction='" + userRestriction + + '}'; + } + + /** + * Returns the intent to start the {@link #className} service. + */ + public Intent getServiceIntent() { + Intent intent = new Intent(); + intent.setClassName(packageName, className); + return intent; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof InjectedSetting)) return false; + + InjectedSetting that = (InjectedSetting) o; + + return Objects.equals(packageName, that.packageName) + && Objects.equals(className, that.className) + && Objects.equals(title, that.title) + && Objects.equals(iconId, that.iconId) + && Objects.equals(mUserHandle, that.mUserHandle) + && Objects.equals(settingsActivity, that.settingsActivity) + && Objects.equals(userRestriction, that.userRestriction); + } + + @Override + public int hashCode() { + int result = packageName.hashCode(); + result = 31 * result + className.hashCode(); + result = 31 * result + title.hashCode(); + result = 31 * result + iconId; + result = 31 * result + (mUserHandle == null ? 0 : mUserHandle.hashCode()); + result = 31 * result + settingsActivity.hashCode(); + result = 31 * result + (userRestriction == null ? 0 : userRestriction.hashCode()); + return result; + } + + public static class Builder { + private String mPackageName; + private String mClassName; + private String mTitle; + private int mIconId; + private UserHandle mUserHandle; + private String mSettingsActivity; + private String mUserRestriction; + + public Builder setPackageName(String packageName) { + mPackageName = packageName; + return this; + } + + public Builder setClassName(String className) { + mClassName = className; + return this; + } + + public Builder setTitle(String title) { + mTitle = title; + return this; + } + + public Builder setIconId(int iconId) { + mIconId = iconId; + return this; + } + + public Builder setUserHandle(UserHandle userHandle) { + mUserHandle = userHandle; + return this; + } + + public Builder setSettingsActivity(String settingsActivity) { + mSettingsActivity = settingsActivity; + return this; + } + + public Builder setUserRestriction(String userRestriction) { + mUserRestriction = userRestriction; + return this; + } + + public InjectedSetting build() { + if (mPackageName == null || mClassName == null || TextUtils.isEmpty(mTitle) + || TextUtils.isEmpty(mSettingsActivity)) { + if (Log.isLoggable(SettingsInjector.TAG, Log.WARN)) { + Log.w(SettingsInjector.TAG, "Illegal setting specification: package=" + + mPackageName + ", class=" + mClassName + + ", title=" + mTitle + ", settingsActivity=" + mSettingsActivity); + } + return null; + } + return new InjectedSetting(this); + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/location/RecentLocationApps.java b/packages/SettingsLib/src/com/android/settingslib/location/RecentLocationApps.java index 96bed93165fb..b8e1251dd79a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/location/RecentLocationApps.java +++ b/packages/SettingsLib/src/com/android/settingslib/location/RecentLocationApps.java @@ -25,10 +25,12 @@ import android.graphics.drawable.Drawable; import android.os.Process; import android.os.UserHandle; import android.os.UserManager; -import androidx.annotation.VisibleForTesting; import android.text.format.DateUtils; import android.util.IconDrawableFactory; import android.util.Log; + +import androidx.annotation.VisibleForTesting; + import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; diff --git a/packages/SettingsLib/src/com/android/settingslib/location/SettingsInjector.java b/packages/SettingsLib/src/com/android/settingslib/location/SettingsInjector.java new file mode 100644 index 000000000000..780fcbab9822 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/location/SettingsInjector.java @@ -0,0 +1,576 @@ +/* + * 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.location; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageItemInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.ServiceInfo; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.content.res.XmlResourceParser; +import android.graphics.drawable.Drawable; +import android.location.SettingInjectorService; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.SystemClock; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.AttributeSet; +import android.util.IconDrawableFactory; +import android.util.Log; +import android.util.Xml; + +import androidx.preference.Preference; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +/** + * Adds the preferences specified by the {@link InjectedSetting} objects to a preference group. + * + * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}. We do not use that + * class directly because it is not a good match for our use case: we do not need the caching, and + * so do not want the additional resource hit at app install/upgrade time; and we would have to + * suppress the tie-breaking between multiple services reporting settings with the same name. + * Code-sharing would require extracting {@link + * android.content.pm.RegisteredServicesCache#parseServiceAttributes(android.content.res.Resources, + * String, android.util.AttributeSet)} into an interface, which didn't seem worth it. + */ +public class SettingsInjector { + static final String TAG = "SettingsInjector"; + + /** + * If reading the status of a setting takes longer than this, we go ahead and start reading + * the next setting. + */ + private static final long INJECTED_STATUS_UPDATE_TIMEOUT_MILLIS = 1000; + + /** + * {@link Message#what} value for starting to load status values + * in case we aren't already in the process of loading them. + */ + private static final int WHAT_RELOAD = 1; + + /** + * {@link Message#what} value sent after receiving a status message. + */ + private static final int WHAT_RECEIVED_STATUS = 2; + + /** + * {@link Message#what} value sent after the timeout waiting for a status message. + */ + private static final int WHAT_TIMEOUT = 3; + + private final Context mContext; + + /** + * The settings that were injected + */ + protected final Set<Setting> mSettings; + + private final Handler mHandler; + + public SettingsInjector(Context context) { + mContext = context; + mSettings = new HashSet<Setting>(); + mHandler = new StatusLoadingHandler(); + } + + /** + * Returns a list for a profile with one {@link InjectedSetting} object for each + * {@link android.app.Service} that responds to + * {@link SettingInjectorService#ACTION_SERVICE_INTENT} and provides the expected setting + * metadata. + * + * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}. + * + * TODO: unit test + */ + protected List<InjectedSetting> getSettings(final UserHandle userHandle) { + PackageManager pm = mContext.getPackageManager(); + Intent intent = new Intent(SettingInjectorService.ACTION_SERVICE_INTENT); + + final int profileId = userHandle.getIdentifier(); + List<ResolveInfo> resolveInfos = + pm.queryIntentServicesAsUser(intent, PackageManager.GET_META_DATA, profileId); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Found services for profile id " + profileId + ": " + resolveInfos); + } + List<InjectedSetting> settings = new ArrayList<InjectedSetting>(resolveInfos.size()); + for (ResolveInfo resolveInfo : resolveInfos) { + try { + InjectedSetting setting = parseServiceInfo(resolveInfo, userHandle, pm); + if (setting == null) { + Log.w(TAG, "Unable to load service info " + resolveInfo); + } else { + settings.add(setting); + } + } catch (XmlPullParserException e) { + Log.w(TAG, "Unable to load service info " + resolveInfo, e); + } catch (IOException e) { + Log.w(TAG, "Unable to load service info " + resolveInfo, e); + } + } + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Loaded settings for profile id " + profileId + ": " + settings); + } + + return settings; + } + + /** + * Adds the InjectedSetting information to a Preference object + */ + private void populatePreference(Preference preference, InjectedSetting setting) { + final PackageManager pm = mContext.getPackageManager(); + Drawable appIcon = null; + try { + final PackageItemInfo itemInfo = new PackageItemInfo(); + itemInfo.icon = setting.iconId; + itemInfo.packageName = setting.packageName; + final ApplicationInfo appInfo = pm.getApplicationInfo(setting.packageName, + PackageManager.GET_META_DATA); + appIcon = IconDrawableFactory.newInstance(mContext) + .getBadgedIcon(itemInfo, appInfo, setting.mUserHandle.getIdentifier()); + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Can't get ApplicationInfo for " + setting.packageName, e); + } + preference.setTitle(setting.title); + preference.setSummary(null); + preference.setIcon(appIcon); + preference.setOnPreferenceClickListener(new ServiceSettingClickedListener(setting)); + } + + /** + * Gets a list of preferences that other apps have injected. + * + * @param profileId Identifier of the user/profile to obtain the injected settings for or + * UserHandle.USER_CURRENT for all profiles associated with current user. + */ + public List<Preference> getInjectedSettings(Context prefContext, final int profileId) { + final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + final List<UserHandle> profiles = um.getUserProfiles(); + ArrayList<Preference> prefs = new ArrayList<>(); + for (UserHandle userHandle : profiles) { + if (profileId == UserHandle.USER_CURRENT || profileId == userHandle.getIdentifier()) { + Iterable<InjectedSetting> settings = getSettings(userHandle); + for (InjectedSetting setting : settings) { + Preference preference = createPreference(prefContext, setting); + populatePreference(preference, setting); + prefs.add(preference); + mSettings.add(new Setting(setting, preference)); + } + } + } + + reloadStatusMessages(); + + return prefs; + } + + /** + * Creates an injected Preference + * + * @return the created Preference + */ + protected Preference createPreference(Context prefContext, InjectedSetting setting) { + return new Preference(prefContext); + } + + /** + * Returns the settings parsed from the attributes of the + * {@link SettingInjectorService#META_DATA_NAME} tag, or null. + * + * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}. + */ + private static InjectedSetting parseServiceInfo(ResolveInfo service, UserHandle userHandle, + PackageManager pm) throws XmlPullParserException, IOException { + + ServiceInfo si = service.serviceInfo; + ApplicationInfo ai = si.applicationInfo; + + if ((ai.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Ignoring attempt to inject setting from app not in system image: " + + service); + return null; + } + } + + XmlResourceParser parser = null; + try { + parser = si.loadXmlMetaData(pm, SettingInjectorService.META_DATA_NAME); + if (parser == null) { + throw new XmlPullParserException("No " + SettingInjectorService.META_DATA_NAME + + " meta-data for " + service + ": " + si); + } + + AttributeSet attrs = Xml.asAttributeSet(parser); + + int type; + while ((type = parser.next()) != XmlPullParser.END_DOCUMENT + && type != XmlPullParser.START_TAG) { + } + + String nodeName = parser.getName(); + if (!SettingInjectorService.ATTRIBUTES_NAME.equals(nodeName)) { + throw new XmlPullParserException("Meta-data does not start with " + + SettingInjectorService.ATTRIBUTES_NAME + " tag"); + } + + Resources res = pm.getResourcesForApplicationAsUser(si.packageName, + userHandle.getIdentifier()); + return parseAttributes(si.packageName, si.name, userHandle, res, attrs); + } catch (PackageManager.NameNotFoundException e) { + throw new XmlPullParserException( + "Unable to load resources for package " + si.packageName); + } finally { + if (parser != null) { + parser.close(); + } + } + } + + /** + * Returns an immutable representation of the static attributes for the setting, or null. + */ + private static InjectedSetting parseAttributes(String packageName, String className, + UserHandle userHandle, Resources res, AttributeSet attrs) { + + TypedArray sa = res.obtainAttributes(attrs, android.R.styleable.SettingInjectorService); + try { + // Note that to help guard against malicious string injection, we do not allow dynamic + // specification of the label (setting title) + final String title = sa.getString(android.R.styleable.SettingInjectorService_title); + final int iconId = + sa.getResourceId(android.R.styleable.SettingInjectorService_icon, 0); + final String settingsActivity = + sa.getString(android.R.styleable.SettingInjectorService_settingsActivity); + final String userRestriction = sa.getString( + android.R.styleable.SettingInjectorService_userRestriction); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "parsed title: " + title + ", iconId: " + iconId + + ", settingsActivity: " + settingsActivity); + } + return new InjectedSetting.Builder() + .setPackageName(packageName) + .setClassName(className) + .setTitle(title) + .setIconId(iconId) + .setUserHandle(userHandle) + .setSettingsActivity(settingsActivity) + .setUserRestriction(userRestriction) + .build(); + } finally { + sa.recycle(); + } + } + + /** + * Checks wheteher there is any preference that other apps have injected. + * + * @param profileId Identifier of the user/profile to obtain the injected settings for or + * UserHandle.USER_CURRENT for all profiles associated with current user. + */ + public boolean hasInjectedSettings(final int profileId) { + final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + final List<UserHandle> profiles = um.getUserProfiles(); + final int profileCount = profiles.size(); + for (int i = 0; i < profileCount; ++i) { + final UserHandle userHandle = profiles.get(i); + if (profileId == UserHandle.USER_CURRENT || profileId == userHandle.getIdentifier()) { + Iterable<InjectedSetting> settings = getSettings(userHandle); + for (InjectedSetting setting : settings) { + return true; + } + } + } + return false; + } + + /** + * Reloads the status messages for all the preference items. + */ + public void reloadStatusMessages() { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "reloadingStatusMessages: " + mSettings); + } + mHandler.sendMessage(mHandler.obtainMessage(WHAT_RELOAD)); + } + + protected class ServiceSettingClickedListener + implements Preference.OnPreferenceClickListener { + private InjectedSetting mInfo; + + public ServiceSettingClickedListener(InjectedSetting info) { + mInfo = info; + } + + @Override + public boolean onPreferenceClick(Preference preference) { + // Activity to start if they click on the preference. Must start in new task to ensure + // that "android.settings.LOCATION_SOURCE_SETTINGS" brings user back to + // Settings > Location. + Intent settingIntent = new Intent(); + settingIntent.setClassName(mInfo.packageName, mInfo.settingsActivity); + // Sometimes the user may navigate back to "Settings" and launch another different + // injected setting after one injected setting has been launched. + // + // FLAG_ACTIVITY_CLEAR_TOP allows multiple Activities to stack on each other. When + // "back" button is clicked, the user will navigate through all the injected settings + // launched before. Such behavior could be quite confusing sometimes. + // + // In order to avoid such confusion, we use FLAG_ACTIVITY_CLEAR_TASK, which always clear + // up all existing injected settings and make sure that "back" button always brings the + // user back to "Settings" directly. + settingIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + mContext.startActivityAsUser(settingIntent, mInfo.mUserHandle); + return true; + } + } + + /** + * Loads the setting status values one at a time. Each load starts a subclass of {@link + * SettingInjectorService}, so to reduce memory pressure we don't want to load too many at + * once. + */ + private final class StatusLoadingHandler extends Handler { + + /** + * Settings whose status values need to be loaded. A set is used to prevent redundant loads. + */ + private Set<Setting> mSettingsToLoad = new HashSet<Setting>(); + + /** + * Settings that are being loaded now and haven't timed out. In practice this should have + * zero or one elements. + */ + private Set<Setting> mSettingsBeingLoaded = new HashSet<Setting>(); + + /** + * Settings that are being loaded but have timed out. If only one setting has timed out, we + * will go ahead and start loading the next setting so that one slow load won't delay the + * load of the other settings. + */ + private Set<Setting> mTimedOutSettings = new HashSet<Setting>(); + + private boolean mReloadRequested; + + private StatusLoadingHandler() { + super(Looper.getMainLooper()); + } + @Override + public void handleMessage(Message msg) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "handleMessage start: " + msg + ", " + this); + } + + // Update state in response to message + switch (msg.what) { + case WHAT_RELOAD: + mReloadRequested = true; + break; + case WHAT_RECEIVED_STATUS: + final Setting receivedSetting = (Setting) msg.obj; + receivedSetting.maybeLogElapsedTime(); + mSettingsBeingLoaded.remove(receivedSetting); + mTimedOutSettings.remove(receivedSetting); + removeMessages(WHAT_TIMEOUT, receivedSetting); + break; + case WHAT_TIMEOUT: + final Setting timedOutSetting = (Setting) msg.obj; + mSettingsBeingLoaded.remove(timedOutSetting); + mTimedOutSettings.add(timedOutSetting); + if (Log.isLoggable(TAG, Log.WARN)) { + Log.w(TAG, "Timed out after " + timedOutSetting.getElapsedTime() + + " millis trying to get status for: " + timedOutSetting); + } + break; + default: + Log.wtf(TAG, "Unexpected what: " + msg); + } + + // Decide whether to load additional settings based on the new state. Start by seeing + // if we have headroom to load another setting. + if (mSettingsBeingLoaded.size() > 0 || mTimedOutSettings.size() > 1) { + // Don't load any more settings until one of the pending settings has completed. + // To reduce memory pressure, we want to be loading at most one setting (plus at + // most one timed-out setting) at a time. This means we'll be responsible for + // bringing in at most two services. + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "too many services already live for " + msg + ", " + this); + } + return; + } + + if (mReloadRequested && mSettingsToLoad.isEmpty() && mSettingsBeingLoaded.isEmpty() + && mTimedOutSettings.isEmpty()) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "reloading because idle and reload requesteed " + msg + ", " + this); + } + // Reload requested, so must reload all settings + mSettingsToLoad.addAll(mSettings); + mReloadRequested = false; + } + + // Remove the next setting to load from the queue, if any + Iterator<Setting> iter = mSettingsToLoad.iterator(); + if (!iter.hasNext()) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "nothing left to do for " + msg + ", " + this); + } + return; + } + Setting setting = iter.next(); + iter.remove(); + + // Request the status value + setting.startService(); + mSettingsBeingLoaded.add(setting); + + // Ensure that if receiving the status value takes too long, we start loading the + // next value anyway + Message timeoutMsg = obtainMessage(WHAT_TIMEOUT, setting); + sendMessageDelayed(timeoutMsg, INJECTED_STATUS_UPDATE_TIMEOUT_MILLIS); + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "handleMessage end " + msg + ", " + this + + ", started loading " + setting); + } + } + + @Override + public String toString() { + return "StatusLoadingHandler{" + + "mSettingsToLoad=" + mSettingsToLoad + + ", mSettingsBeingLoaded=" + mSettingsBeingLoaded + + ", mTimedOutSettings=" + mTimedOutSettings + + ", mReloadRequested=" + mReloadRequested + + '}'; + } + } + + /** + * Represents an injected setting and the corresponding preference. + */ + protected final class Setting { + + public final InjectedSetting setting; + public final Preference preference; + public long startMillis; + + public Setting(InjectedSetting setting, Preference preference) { + this.setting = setting; + this.preference = preference; + } + + @Override + public String toString() { + return "Setting{" + + "setting=" + setting + + ", preference=" + preference + + '}'; + } + + /** + * Returns true if they both have the same {@link #setting} value. Ignores mutable + * {@link #preference} and {@link #startMillis} so that it's safe to use in sets. + */ + @Override + public boolean equals(Object o) { + return this == o || o instanceof Setting && setting.equals(((Setting) o).setting); + } + + @Override + public int hashCode() { + return setting.hashCode(); + } + + /** + * Starts the service to fetch for the current status for the setting, and updates the + * preference when the service replies. + */ + public void startService() { + final ActivityManager am = (ActivityManager) + mContext.getSystemService(Context.ACTIVITY_SERVICE); + if (!am.isUserRunning(setting.mUserHandle.getIdentifier())) { + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "Cannot start service as user " + + setting.mUserHandle.getIdentifier() + " is not running"); + } + return; + } + Handler handler = new Handler() { + @Override + public void handleMessage(Message msg) { + Bundle bundle = msg.getData(); + boolean enabled = bundle.getBoolean(SettingInjectorService.ENABLED_KEY, true); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, setting + ": received " + msg + ", bundle: " + bundle); + } + preference.setSummary(null); + preference.setEnabled(enabled); + mHandler.sendMessage( + mHandler.obtainMessage(WHAT_RECEIVED_STATUS, Setting.this)); + } + }; + Messenger messenger = new Messenger(handler); + + Intent intent = setting.getServiceIntent(); + intent.putExtra(SettingInjectorService.MESSENGER_KEY, messenger); + + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, setting + ": sending update intent: " + intent + + ", handler: " + handler); + startMillis = SystemClock.elapsedRealtime(); + } else { + startMillis = 0; + } + + // Start the service, making sure that this is attributed to the user associated with + // the setting rather than the system user. + mContext.startServiceAsUser(intent, setting.mUserHandle); + } + + public long getElapsedTime() { + long end = SystemClock.elapsedRealtime(); + return end - startMillis; + } + + public void maybeLogElapsedTime() { + if (Log.isLoggable(TAG, Log.DEBUG) && startMillis != 0) { + long elapsed = getElapsedTime(); + Log.d(TAG, this + " update took " + elapsed + " millis"); + } + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java new file mode 100644 index 000000000000..e5353486c23b --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java @@ -0,0 +1,73 @@ +/* + * Copyright 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.media; + +import android.content.Context; +import android.util.Log; + +import com.android.settingslib.R; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; + +/** + * BluetoothMediaDevice extends MediaDevice to represents Bluetooth device. + */ +public class BluetoothMediaDevice extends MediaDevice { + + private static final String TAG = "BluetoothMediaDevice"; + + private CachedBluetoothDevice mCachedDevice; + + BluetoothMediaDevice(Context context, CachedBluetoothDevice device) { + super(context, MediaDeviceType.TYPE_BLUETOOTH_DEVICE); + mCachedDevice = device; + } + + @Override + public String getName() { + return mCachedDevice.getName(); + } + + @Override + public int getIcon() { + //TODO(b/117129183): This is not final icon for bluetooth device, just for demo. + return R.drawable.ic_bt_headphones_a2dp; + } + + @Override + public String getId() { + return MediaDeviceUtils.getId(mCachedDevice); + } + + @Override + public void connect() { + //TODO(b/117129183): add callback to notify LocalMediaManager connection state. + mIsConnected = mCachedDevice.setActive(); + Log.d(TAG, "connect() device : " + getName() + ", is selected : " + mIsConnected); + } + + @Override + public void disconnect() { + //TODO(b/117129183): disconnected last select device + mIsConnected = false; + } + + /** + * Get current CachedBluetoothDevice + */ + public CachedBluetoothDevice getCachedDevice() { + return mCachedDevice; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaManager.java new file mode 100644 index 000000000000..04188e936cf6 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaManager.java @@ -0,0 +1,261 @@ +/* + * Copyright 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.media; + +import android.app.Notification; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.util.Log; + +import com.android.settingslib.bluetooth.A2dpProfile; +import com.android.settingslib.bluetooth.BluetoothCallback; +import com.android.settingslib.bluetooth.CachedBluetoothDevice; +import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; +import com.android.settingslib.bluetooth.HearingAidProfile; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; + +import java.util.ArrayList; +import java.util.List; + +/** + * BluetoothMediaManager provide interface to get Bluetooth device list. + */ +public class BluetoothMediaManager extends MediaManager implements BluetoothCallback { + + private static final String TAG = "BluetoothMediaManager"; + + private final DeviceAttributeChangeCallback mCachedDeviceCallback = + new DeviceAttributeChangeCallback(); + + private LocalBluetoothManager mLocalBluetoothManager; + private LocalBluetoothProfileManager mProfileManager; + + private MediaDevice mLastAddedDevice; + private MediaDevice mLastRemovedDevice; + + BluetoothMediaManager(Context context, LocalBluetoothManager localBluetoothManager, + Notification notification) { + super(context, notification); + + mLocalBluetoothManager = localBluetoothManager; + mProfileManager = mLocalBluetoothManager.getProfileManager(); + } + + @Override + public void startScan() { + mMediaDevices.clear(); + mLocalBluetoothManager.getEventManager().registerCallback(this); + buildBluetoothDeviceList(); + dispatchDeviceListAdded(); + } + + private void buildBluetoothDeviceList() { + addConnectedA2dpDevices(); + addConnectedHearingAidDevices(); + } + + private void addConnectedA2dpDevices() { + final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); + if (a2dpProfile == null) { + Log.w(TAG, "addConnectedA2dpDevices() a2dp profile is null!"); + return; + } + final List<BluetoothDevice> devices = a2dpProfile.getConnectedDevices(); + final CachedBluetoothDeviceManager cachedBluetoothDeviceManager = + mLocalBluetoothManager.getCachedDeviceManager(); + + for (BluetoothDevice device : devices) { + final CachedBluetoothDevice cachedDevice = + cachedBluetoothDeviceManager.findDevice(device); + + if (cachedDevice == null) { + Log.w(TAG, "Can't found CachedBluetoothDevice : " + device.getName()); + continue; + } + + Log.d(TAG, "addConnectedA2dpDevices() device : " + cachedDevice.getName() + + ", is connected : " + cachedDevice.isConnected()); + + if (cachedDevice.isConnected()) { + addMediaDevice(cachedDevice); + } + } + } + + private void addConnectedHearingAidDevices() { + final HearingAidProfile hapProfile = mProfileManager.getHearingAidProfile(); + if (hapProfile == null) { + Log.w(TAG, "addConnectedA2dpDevices() hap profile is null!"); + return; + } + final List<Long> devicesHiSyncIds = new ArrayList<>(); + final List<BluetoothDevice> devices = hapProfile.getConnectedDevices(); + final CachedBluetoothDeviceManager cachedBluetoothDeviceManager = + mLocalBluetoothManager.getCachedDeviceManager(); + + for (BluetoothDevice device : devices) { + final CachedBluetoothDevice cachedDevice = + cachedBluetoothDeviceManager.findDevice(device); + + if (cachedDevice == null) { + Log.w(TAG, "Can't found CachedBluetoothDevice : " + device.getName()); + continue; + } + + Log.d(TAG, "addConnectedHearingAidDevices() device : " + cachedDevice.getName() + + ", is connected : " + cachedDevice.isConnected()); + final long hiSyncId = hapProfile.getHiSyncId(device); + + // device with same hiSyncId should not be shown in the UI. + // So do not add it into connectedDevices. + if (!devicesHiSyncIds.contains(hiSyncId) && cachedDevice.isConnected()) { + devicesHiSyncIds.add(hiSyncId); + addMediaDevice(cachedDevice); + } + } + } + + private void addMediaDevice(CachedBluetoothDevice cachedDevice) { + MediaDevice mediaDevice = findMediaDevice(MediaDeviceUtils.getId(cachedDevice)); + if (mediaDevice == null) { + mediaDevice = new BluetoothMediaDevice(mContext, cachedDevice); + cachedDevice.registerCallback(mCachedDeviceCallback); + mLastAddedDevice = mediaDevice; + mMediaDevices.add(mediaDevice); + } + } + + @Override + public void stopScan() { + mLocalBluetoothManager.getEventManager().unregisterCallback(this); + unregisterCachedDeviceCallback(); + } + + private void unregisterCachedDeviceCallback() { + for (MediaDevice device : mMediaDevices) { + if (device instanceof BluetoothMediaDevice) { + ((BluetoothMediaDevice) device).getCachedDevice() + .unregisterCallback(mCachedDeviceCallback); + } + } + } + + @Override + public void onBluetoothStateChanged(int bluetoothState) { + if (BluetoothAdapter.STATE_ON == bluetoothState) { + buildBluetoothDeviceList(); + dispatchDeviceListAdded(); + } else if (BluetoothAdapter.STATE_OFF == bluetoothState) { + final List<MediaDevice> removeDevicesList = new ArrayList<>(); + for (MediaDevice device : mMediaDevices) { + if (device instanceof BluetoothMediaDevice) { + ((BluetoothMediaDevice) device).getCachedDevice() + .unregisterCallback(mCachedDeviceCallback); + removeDevicesList.add(device); + } + } + mMediaDevices.removeAll(removeDevicesList); + dispatchDeviceListRemoved(removeDevicesList); + } + } + + @Override + public void onDeviceAdded(CachedBluetoothDevice cachedDevice) { + if (isCachedDeviceConnected(cachedDevice)) { + addMediaDevice(cachedDevice); + dispatchDeviceAdded(cachedDevice); + } + } + + private boolean isCachedDeviceConnected(CachedBluetoothDevice cachedDevice) { + final boolean isConnectedHearingAidDevice = cachedDevice.isConnectedHearingAidDevice(); + final boolean isConnectedA2dpDevice = cachedDevice.isConnectedA2dpDevice(); + Log.d(TAG, "isCachedDeviceConnected() cachedDevice : " + cachedDevice.getName() + + ", is hearing aid connected : " + isConnectedHearingAidDevice + + ", is a2dp connected : " + isConnectedA2dpDevice); + + return isConnectedHearingAidDevice || isConnectedA2dpDevice; + } + + private void dispatchDeviceAdded(CachedBluetoothDevice cachedDevice) { + if (mLastAddedDevice != null + && MediaDeviceUtils.getId(cachedDevice) == mLastAddedDevice.getId()) { + dispatchDeviceAdded(mLastAddedDevice); + } + } + + @Override + public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) { + if (!isCachedDeviceConnected(cachedDevice)) { + removeMediaDevice(cachedDevice); + dispatchDeviceRemoved(cachedDevice); + } + } + + private void removeMediaDevice(CachedBluetoothDevice cachedDevice) { + final MediaDevice mediaDevice = findMediaDevice(MediaDeviceUtils.getId(cachedDevice)); + if (mediaDevice != null) { + cachedDevice.unregisterCallback(mCachedDeviceCallback); + mLastRemovedDevice = mediaDevice; + mMediaDevices.remove(mediaDevice); + } + } + + void dispatchDeviceRemoved(CachedBluetoothDevice cachedDevice) { + if (mLastRemovedDevice != null + && MediaDeviceUtils.getId(cachedDevice) == mLastRemovedDevice.getId()) { + dispatchDeviceRemoved(mLastRemovedDevice); + } + } + + @Override + public void onProfileConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state, + int bluetoothProfile) { + Log.d(TAG, "onProfileConnectionStateChanged() device: " + cachedDevice.getName() + + ", state: " + state + ", bluetoothProfile: " + bluetoothProfile); + + if (isCachedDeviceConnected(cachedDevice)) { + addMediaDevice(cachedDevice); + dispatchDeviceAdded(cachedDevice); + } else { + removeMediaDevice(cachedDevice); + dispatchDeviceRemoved(cachedDevice); + } + } + + @Override + public void onAclConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { + Log.d(TAG, "onAclConnectionStateChanged() device: " + cachedDevice.getName() + + ", state: " + state); + + if (isCachedDeviceConnected(cachedDevice)) { + addMediaDevice(cachedDevice); + dispatchDeviceAdded(cachedDevice); + } else { + removeMediaDevice(cachedDevice); + dispatchDeviceRemoved(cachedDevice); + } + } + + class DeviceAttributeChangeCallback implements CachedBluetoothDevice.Callback { + @Override + public void onDeviceAttributesChanged() { + dispatchDeviceAttributesChanged(); + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java new file mode 100644 index 000000000000..498a0fc4d8b2 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaDevice.java @@ -0,0 +1,65 @@ +/* + * Copyright 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.media; + +import android.content.Context; + +import androidx.mediarouter.media.MediaRouter; + +import com.android.settingslib.R; + +/** + * InfoMediaDevice extends MediaDevice to represents wifi device. + */ +public class InfoMediaDevice extends MediaDevice { + + private static final String TAG = "InfoMediaDevice"; + + private MediaRouter.RouteInfo mRouteInfo; + + InfoMediaDevice(Context context, MediaRouter.RouteInfo info) { + super(context, MediaDeviceType.TYPE_CAST_DEVICE); + mRouteInfo = info; + } + + @Override + public String getName() { + return mRouteInfo.getName(); + } + + @Override + public int getIcon() { + //TODO(b/117129183): This is not final icon for cast device, just for demo. + return R.drawable.ic_settings_print; + } + + @Override + public String getId() { + return MediaDeviceUtils.getId(mRouteInfo); + } + + @Override + public void connect() { + //TODO(b/117129183): use MediaController2 to transfer media + mIsConnected = true; + } + + @Override + public void disconnect() { + //TODO(b/117129183): disconnected last select device + mIsConnected = false; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java new file mode 100644 index 000000000000..6907238082fc --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java @@ -0,0 +1,92 @@ +/* + * Copyright 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.media; + +import android.app.Notification; +import android.content.Context; +import android.util.Log; + +import androidx.mediarouter.media.MediaRouteSelector; +import androidx.mediarouter.media.MediaRouter; + +/** + * InfoMediaManager provide interface to get InfoMediaDevice list. + */ +public class InfoMediaManager extends MediaManager { + + private static final String TAG = "InfoMediaManager"; + + private final MediaRouterCallback mMediaRouterCallback = new MediaRouterCallback(); + + private MediaRouter mMediaRouter; + private String mPackageName; + + InfoMediaManager(Context context, String packageName, Notification notification) { + super(context, notification); + + mMediaRouter = MediaRouter.getInstance(context); + mPackageName = packageName; + } + + @Override + public void startScan() { + mMediaDevices.clear(); + startScanCastDevice(); + } + + private void startScanCastDevice() { + final MediaRouteSelector selector = new MediaRouteSelector.Builder() + .addControlCategory(getControlCategoryByPackageName(mPackageName)) + .build(); + + mMediaRouter.addCallback(selector, mMediaRouterCallback, + MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY); + } + + private String getControlCategoryByPackageName(String packageName) { + //TODO(b/117129183): Use package name to get ControlCategory. + //Since api not ready, return fixed ControlCategory for prototype. + return "com.google.android.gms.cast.CATEGORY_CAST/4F8B3483"; + } + + @Override + public void stopScan() { + mMediaRouter.removeCallback(mMediaRouterCallback); + } + + class MediaRouterCallback extends MediaRouter.Callback { + @Override + public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo route) { + MediaDevice mediaDevice = findMediaDevice(MediaDeviceUtils.getId(route)); + if (mediaDevice == null) { + mediaDevice = new InfoMediaDevice(mContext, route); + Log.d(TAG, "onRouteAdded() route : " + route.getName()); + mMediaDevices.add(mediaDevice); + dispatchDeviceAdded(mediaDevice); + } + } + + @Override + public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo route) { + final MediaDevice mediaDevice = findMediaDevice(MediaDeviceUtils.getId(route)); + if (mediaDevice != null) { + Log.d(TAG, "onRouteRemoved() route : " + route.getName()); + mMediaDevices.remove(mediaDevice); + dispatchDeviceRemoved(mediaDevice); + } + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java new file mode 100644 index 000000000000..e375ea016ec2 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/media/LocalMediaManager.java @@ -0,0 +1,234 @@ +/* + * Copyright 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.media; + +import android.app.Notification; +import android.content.Context; +import android.util.Log; + +import androidx.annotation.IntDef; + +import com.android.settingslib.bluetooth.BluetoothCallback; +import com.android.settingslib.bluetooth.LocalBluetoothManager; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * LocalMediaManager provide interface to get MediaDevice list and transfer media to MediaDevice. + */ +public class LocalMediaManager implements BluetoothCallback { + + private static final String TAG = "LocalMediaManager"; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({MediaDeviceState.STATE_CONNECTED, + MediaDeviceState.STATE_CONNECTING, + MediaDeviceState.STATE_DISCONNECTED}) + public @interface MediaDeviceState { + int STATE_CONNECTED = 1; + int STATE_CONNECTING = 2; + int STATE_DISCONNECTED = 3; + } + + private final Collection<DeviceCallback> mCallbacks = new ArrayList<>(); + private final MediaDeviceCallback mMediaDeviceCallback = new MediaDeviceCallback(); + + private Context mContext; + private List<MediaDevice> mMediaDevices = new ArrayList<>(); + private BluetoothMediaManager mBluetoothMediaManager; + private InfoMediaManager mInfoMediaManager; + + private LocalBluetoothManager mLocalBluetoothManager; + private MediaDevice mLastConnectedDevice; + private MediaDevice mPhoneDevice; + + /** + * Register to start receiving callbacks for MediaDevice events. + */ + public void registerCallback(DeviceCallback callback) { + synchronized (mCallbacks) { + mCallbacks.add(callback); + } + } + + /** + * Unregister to stop receiving callbacks for MediaDevice events + */ + public void unregisterCallback(DeviceCallback callback) { + synchronized (mCallbacks) { + mCallbacks.remove(callback); + } + } + + public LocalMediaManager(Context context, String packageName, Notification notification) { + mContext = context; + mLocalBluetoothManager = + LocalBluetoothManager.getInstance(context, /* onInitCallback= */ null); + if (mLocalBluetoothManager == null) { + Log.e(TAG, "Bluetooth is not supported on this device"); + return; + } + + mBluetoothMediaManager = + new BluetoothMediaManager(context, mLocalBluetoothManager, notification); + mInfoMediaManager = new InfoMediaManager(context, packageName, notification); + } + + /** + * Connect the MediaDevice to transfer media + * @param connectDevice the MediaDevice + */ + public void connectDevice(MediaDevice connectDevice) { + if (connectDevice == mLastConnectedDevice) { + return; + } + + if (mLastConnectedDevice != null) { + mLastConnectedDevice.disconnect(); + } + + connectDevice.connect(); + if (connectDevice.isConnected()) { + mLastConnectedDevice = connectDevice; + } + + final int state = connectDevice.isConnected() + ? MediaDeviceState.STATE_CONNECTED + : MediaDeviceState.STATE_DISCONNECTED; + dispatchSelectedDeviceStateChanged(connectDevice, state); + } + + void dispatchSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state) { + synchronized (mCallbacks) { + for (DeviceCallback callback : mCallbacks) { + callback.onSelectedDeviceStateChanged(device, state); + } + } + } + + /** + * Start scan connected MediaDevice + */ + public void startScan() { + mMediaDevices.clear(); + mBluetoothMediaManager.registerCallback(mMediaDeviceCallback); + mInfoMediaManager.registerCallback(mMediaDeviceCallback); + mBluetoothMediaManager.startScan(); + mInfoMediaManager.startScan(); + } + + private void addPhoneDeviceIfNecessary() { + // add phone device to list if there have any Bluetooth device and cast device. + if (mMediaDevices.size() > 0 && !mMediaDevices.contains(mPhoneDevice)) { + if (mPhoneDevice == null) { + mPhoneDevice = new PhoneMediaDevice(mContext, mLocalBluetoothManager); + } + mMediaDevices.add(mPhoneDevice); + } + } + + private void removePhoneMediaDeviceIfNecessary() { + // if PhoneMediaDevice is the last item in the list, remove it. + if (mMediaDevices.size() == 1 && mMediaDevices.contains(mPhoneDevice)) { + mMediaDevices.clear(); + } + } + + void dispatchDeviceListUpdate() { + synchronized (mCallbacks) { + for (DeviceCallback callback : mCallbacks) { + callback.onDeviceListUpdate(new ArrayList<>(mMediaDevices)); + } + } + } + + /** + * Stop scan MediaDevice + */ + public void stopScan() { + mBluetoothMediaManager.unregisterCallback(mMediaDeviceCallback); + mInfoMediaManager.unregisterCallback(mMediaDeviceCallback); + mBluetoothMediaManager.stopScan(); + mInfoMediaManager.stopScan(); + } + + class MediaDeviceCallback implements MediaManager.MediaDeviceCallback { + @Override + public void onDeviceAdded(MediaDevice device) { + if (!mMediaDevices.contains(device)) { + mMediaDevices.add(device); + addPhoneDeviceIfNecessary(); + dispatchDeviceListUpdate(); + } + } + + @Override + public void onDeviceListAdded(List<MediaDevice> devices) { + mMediaDevices.addAll(devices); + addPhoneDeviceIfNecessary(); + dispatchDeviceListUpdate(); + } + + @Override + public void onDeviceRemoved(MediaDevice device) { + if (mMediaDevices.contains(device)) { + mMediaDevices.remove(device); + removePhoneMediaDeviceIfNecessary(); + dispatchDeviceListUpdate(); + } + } + + @Override + public void onDeviceListRemoved(List<MediaDevice> devices) { + mMediaDevices.removeAll(devices); + removePhoneMediaDeviceIfNecessary(); + dispatchDeviceListUpdate(); + } + + @Override + public void onDeviceAttributesChanged() { + dispatchDeviceListUpdate(); + } + } + + + /** + * Callback for notifying device information updating + */ + public interface DeviceCallback { + /** + * Callback for notifying device list updated. + * + * @param devices MediaDevice list + */ + void onDeviceListUpdate(List<MediaDevice> devices); + + /** + * Callback for notifying the connected device is changed. + * + * @param device the changed connected MediaDevice + * @param state the current MediaDevice state, the possible values are: + * {@link MediaDeviceState#STATE_CONNECTED}, + * {@link MediaDeviceState#STATE_CONNECTING}, + * {@link MediaDeviceState#STATE_DISCONNECTED} + */ + void onSelectedDeviceStateChanged(MediaDevice device, @MediaDeviceState int state); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java new file mode 100644 index 000000000000..6c536f080207 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDevice.java @@ -0,0 +1,89 @@ +/* + * Copyright 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.media; + +import android.content.Context; + +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * MediaDevice represents a media device(such like Bluetooth device, cast device and phone device). + */ +public abstract class MediaDevice { + + private static final String TAG = "MediaDevice"; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({MediaDeviceType.TYPE_BLUETOOTH_DEVICE, + MediaDeviceType.TYPE_CAST_DEVICE, + MediaDeviceType.TYPE_PHONE_DEVICE}) + public @interface MediaDeviceType { + int TYPE_BLUETOOTH_DEVICE = 1; + int TYPE_CAST_DEVICE = 2; + int TYPE_PHONE_DEVICE = 3; + } + + protected boolean mIsConnected = false; + protected Context mContext; + protected int mType; + + MediaDevice(Context context, @MediaDeviceType int type) { + mType = type; + mContext = context; + } + + /** + * Check the MediaDevice is be connected to transfer. + * + * @return true if the MediaDevice is be connected to transfer, false otherwise. + */ + protected boolean isConnected() { + return mIsConnected; + } + + /** + * Get name from MediaDevice. + * + * @return name of MediaDevice. + */ + public abstract String getName(); + + /** + * Get resource id of MediaDevice. + * + * @return resource id of MediaDevice. + */ + public abstract int getIcon(); + + /** + * Get unique ID that represent MediaDevice + * @return unique id of MediaDevice + */ + public abstract String getId(); + + /** + * Transfer MediaDevice for media + */ + public abstract void connect(); + + /** + * Stop transfer MediaDevice + */ + public abstract void disconnect(); +} diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaDeviceUtils.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaDeviceUtils.java new file mode 100644 index 000000000000..060e9ad3368f --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaDeviceUtils.java @@ -0,0 +1,46 @@ +/* + * Copyright 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.media; + +import androidx.mediarouter.media.MediaRouter; + +import com.android.settingslib.bluetooth.CachedBluetoothDevice; + +/** + * MediaDeviceUtils provides utility function for MediaDevice + */ +public class MediaDeviceUtils { + + /** + * Use CachedBluetoothDevice address to represent unique id + * + * @param cachedDevice the CachedBluetoothDevice + * @return CachedBluetoothDevice address + */ + public static String getId(CachedBluetoothDevice cachedDevice) { + return cachedDevice.getAddress(); + } + + /** + * Use RouteInfo id to represent unique id + * + * @param route the RouteInfo + * @return RouteInfo id + */ + public static String getId(MediaRouter.RouteInfo route) { + return route.getId(); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java new file mode 100644 index 000000000000..72b6b09d637e --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/media/MediaManager.java @@ -0,0 +1,157 @@ +/* + * Copyright 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.media; + +import android.app.Notification; +import android.content.Context; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * MediaManager provide interface to get MediaDevice list. + */ +public abstract class MediaManager { + + private static final String TAG = "MediaManager"; + + protected final Collection<MediaDeviceCallback> mCallbacks = new ArrayList<>(); + protected final List<MediaDevice> mMediaDevices = new ArrayList<>(); + + protected Context mContext; + protected Notification mNotification; + + MediaManager(Context context, Notification notification) { + mContext = context; + mNotification = notification; + } + + protected void registerCallback(MediaDeviceCallback callback) { + synchronized (mCallbacks) { + if (!mCallbacks.contains(callback)) { + mCallbacks.add(callback); + } + } + } + + protected void unregisterCallback(MediaDeviceCallback callback) { + synchronized (mCallbacks) { + if (mCallbacks.contains(callback)) { + mCallbacks.remove(callback); + } + } + } + + /** + * Start scan connected MediaDevice + */ + public abstract void startScan(); + + /** + * Stop scan MediaDevice + */ + public abstract void stopScan(); + + protected MediaDevice findMediaDevice(String id) { + for (MediaDevice mediaDevice : mMediaDevices) { + if (mediaDevice.getId().equals(id)) { + return mediaDevice; + } + } + Log.e(TAG, "findMediaDevice() can't found device"); + return null; + } + + protected void dispatchDeviceAdded(MediaDevice mediaDevice) { + synchronized (mCallbacks) { + for (MediaDeviceCallback callback : mCallbacks) { + callback.onDeviceAdded(mediaDevice); + } + } + } + + protected void dispatchDeviceRemoved(MediaDevice mediaDevice) { + synchronized (mCallbacks) { + for (MediaDeviceCallback callback : mCallbacks) { + callback.onDeviceRemoved(mediaDevice); + } + } + } + + protected void dispatchDeviceListAdded() { + synchronized (mCallbacks) { + for (MediaDeviceCallback callback : mCallbacks) { + callback.onDeviceListAdded(mMediaDevices); + } + } + } + + protected void dispatchDeviceListRemoved(List<MediaDevice> devices) { + synchronized (mCallbacks) { + for (MediaDeviceCallback callback : mCallbacks) { + callback.onDeviceListRemoved(devices); + } + } + } + + protected void dispatchDeviceAttributesChanged() { + synchronized (mCallbacks) { + for (MediaDeviceCallback callback : mCallbacks) { + callback.onDeviceAttributesChanged(); + } + } + } + + /** + * Callback for notifying device is added, removed and attributes changed. + */ + public interface MediaDeviceCallback { + /** + * Callback for notifying MediaDevice is added. + * + * @param device the MediaDevice + */ + void onDeviceAdded(MediaDevice device); + + /** + * Callback for notifying MediaDevice list is added. + * + * @param devices the MediaDevice list + */ + void onDeviceListAdded(List<MediaDevice> devices); + + /** + * Callback for notifying MediaDevice is removed. + * + * @param device the MediaDevice + */ + void onDeviceRemoved(MediaDevice device); + + /** + * Callback for notifying MediaDevice list is removed. + * + * @param devices the MediaDevice list + */ + void onDeviceListRemoved(List<MediaDevice> devices); + + /** + * Callback for notifying MediaDevice attributes is changed. + */ + void onDeviceAttributesChanged(); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java new file mode 100644 index 000000000000..5e49d6b49e11 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/media/PhoneMediaDevice.java @@ -0,0 +1,80 @@ +/* + * Copyright 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.media; + +import android.content.Context; +import android.util.Log; + +import com.android.settingslib.R; +import com.android.settingslib.bluetooth.A2dpProfile; +import com.android.settingslib.bluetooth.HearingAidProfile; +import com.android.settingslib.bluetooth.LocalBluetoothManager; +import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; + +/** + * PhoneMediaDevice extends MediaDevice to represents Phone device. + */ +public class PhoneMediaDevice extends MediaDevice { + + private static final String TAG = "PhoneMediaDevice"; + + public static final String ID = "phone_media_device_id_1"; + + private LocalBluetoothProfileManager mProfileManager; + private LocalBluetoothManager mLocalBluetoothManager; + + PhoneMediaDevice(Context context, LocalBluetoothManager localBluetoothManager) { + super(context, MediaDeviceType.TYPE_PHONE_DEVICE); + + mLocalBluetoothManager = localBluetoothManager; + mProfileManager = mLocalBluetoothManager.getProfileManager(); + } + + @Override + public String getName() { + return mContext + .getString(com.android.settingslib.R.string.media_transfer_phone_device_name); + } + + @Override + public int getIcon() { + //TODO(b/117129183): This is not final icon for phone device, just for demo. + return R.drawable.ic_bt_cellphone; + } + + @Override + public String getId() { + return ID; + } + + @Override + public void connect() { + final HearingAidProfile hapProfile = mProfileManager.getHearingAidProfile(); + final A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile(); + + if (hapProfile != null && a2dpProfile != null) { + mIsConnected = + hapProfile.setActiveDevice(null) && a2dpProfile.setActiveDevice(null); + } + Log.d(TAG, "connect() device : " + getName() + ", is selected : " + mIsConnected); + } + + @Override + public void disconnect() { + //TODO(b/117129183): disconnected last select device + mIsConnected = false; + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/net/ChartDataLoader.java b/packages/SettingsLib/src/com/android/settingslib/net/ChartDataLoader.java index 223c05546b7f..60d22a0d5803 100644 --- a/packages/SettingsLib/src/com/android/settingslib/net/ChartDataLoader.java +++ b/packages/SettingsLib/src/com/android/settingslib/net/ChartDataLoader.java @@ -34,8 +34,11 @@ import android.os.RemoteException; import com.android.settingslib.AppItem; /** - * Loader for historical chart data for both network and UID details. + * Framework loader is deprecated, use the compat version instead. + * + * @deprecated */ +@Deprecated public class ChartDataLoader extends AsyncTaskLoader<ChartData> { private static final String KEY_TEMPLATE = "template"; private static final String KEY_APP = "app"; 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..e9c523881373 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/net/ChartDataLoaderCompat.java @@ -0,0 +1,152 @@ +/* + * 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 androidx.loader.content.AsyncTaskLoader; + +import com.android.settingslib.AppItem; + +/** + * Loader for historical chart data for both network and UID details. + * + * Deprecated in favor of {@link NetworkCycleChartDataLoader} and + * {@link NetworkCycleDataForUidLoader} + * + * @deprecated + */ +@Deprecated +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/DataUsageController.java b/packages/SettingsLib/src/com/android/settingslib/net/DataUsageController.java index f7aa29796ce8..183d4856ac6f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/net/DataUsageController.java +++ b/packages/SettingsLib/src/com/android/settingslib/net/DataUsageController.java @@ -24,6 +24,8 @@ import static android.telephony.TelephonyManager.SIM_STATE_READY; import static android.text.format.DateUtils.FORMAT_ABBREV_MONTH; import static android.text.format.DateUtils.FORMAT_SHOW_DATE; +import android.app.usage.NetworkStats.Bucket; +import android.app.usage.NetworkStatsManager; import android.content.Context; import android.net.ConnectivityManager; import android.net.INetworkStatsService; @@ -37,18 +39,23 @@ import android.os.ServiceManager; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.text.format.DateUtils; +import android.util.FeatureFlagUtils; import android.util.Log; -import android.util.Pair; +import android.util.Range; import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; import java.time.ZonedDateTime; import java.util.Date; +import java.util.Iterator; import java.util.Locale; public class DataUsageController { private static final String TAG = "DataUsageController"; + @VisibleForTesting + static final String DATA_USAGE_V2 = "settings_data_usage_v2"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); private static final int FIELDS = FIELD_RX_BYTES | FIELD_TX_BYTES; private static final StringBuilder PERIOD_BUILDER = new StringBuilder(50); @@ -60,6 +67,7 @@ public class DataUsageController { private final ConnectivityManager mConnectivityManager; private final INetworkStatsService mStatsService; private final NetworkPolicyManager mPolicyManager; + private final NetworkStatsManager mNetworkStatsManager; private INetworkStatsSession mSession; private Callback mCallback; @@ -72,6 +80,7 @@ public class DataUsageController { mStatsService = INetworkStatsService.Stub.asInterface( ServiceManager.getService(Context.NETWORK_STATS_SERVICE)); mPolicyManager = NetworkPolicyManager.from(mContext); + mNetworkStatsManager = context.getSystemService(NetworkStatsManager.class); } public void setNetworkController(NetworkNameProvider networkController) { @@ -86,7 +95,9 @@ public class DataUsageController { * mContext.getResources().getInteger(R.integer.default_data_warning_level_mb); } - private INetworkStatsSession getSession() { + @VisibleForTesting + @Deprecated + INetworkStatsSession getSession() { if (mSession == null) { try { mSession = mStatsService.openSession(); @@ -125,55 +136,95 @@ public class DataUsageController { } public DataUsageInfo getDataUsageInfo(NetworkTemplate template) { - final INetworkStatsSession session = getSession(); - if (session == null) { - return warn("no stats session"); - } final NetworkPolicy policy = findNetworkPolicy(template); - try { - final NetworkStatsHistory history = session.getHistoryForNetwork(template, FIELDS); + final long now = System.currentTimeMillis(); + final long start, end; + final Iterator<Range<ZonedDateTime>> it = (policy != null) ? policy.cycleIterator() : null; + if (it != null && it.hasNext()) { + final Range<ZonedDateTime> cycle = it.next(); + start = cycle.getLower().toInstant().toEpochMilli(); + end = cycle.getUpper().toInstant().toEpochMilli(); + } else { + // period = last 4 wks + end = now; + start = now - DateUtils.WEEK_IN_MILLIS * 4; + } + final long totalBytes; + final long callStart = System.currentTimeMillis(); + if (FeatureFlagUtils.isEnabled(mContext, DATA_USAGE_V2)) { + totalBytes = getUsageLevel(template, start, end); + } else { + totalBytes = getUsageLevel(template, start, end, now); + } + if (totalBytes < 0L) { + return warn("no entry data"); + } + final DataUsageInfo usage = new DataUsageInfo(); + usage.startDate = start; + usage.usageLevel = totalBytes; + usage.period = formatDateRange(start, end); + usage.cycleStart = start; + usage.cycleEnd = end; + + if (policy != null) { + usage.limitLevel = policy.limitBytes > 0 ? policy.limitBytes : 0; + usage.warningLevel = policy.warningBytes > 0 ? policy.warningBytes : 0; + } else { + usage.warningLevel = getDefaultWarningLevel(); + } + if (usage != null && mNetworkController != null) { + usage.carrier = mNetworkController.getMobileDataNetworkName(); + } + return usage; + } + + /** + * Get the total usage level recorded in the network history + * @param template the network template to retrieve the network history + * @return the total usage level recorded in the network history or -1L if there is error + * retrieving the data. + */ + public long getHistoricalUsageLevel(NetworkTemplate template) { + if (FeatureFlagUtils.isEnabled(mContext, DATA_USAGE_V2)) { + return getUsageLevel(template, 0L /* start */, System.currentTimeMillis() /* end */); + } else { final long now = System.currentTimeMillis(); - final long start, end; - if (policy != null) { - final Pair<ZonedDateTime, ZonedDateTime> cycle = NetworkPolicyManager - .cycleIterator(policy).next(); - start = cycle.first.toInstant().toEpochMilli(); - end = cycle.second.toInstant().toEpochMilli(); - } else { - // period = last 4 wks - end = now; - start = now - DateUtils.WEEK_IN_MILLIS * 4; - } - final long callStart = System.currentTimeMillis(); - final NetworkStatsHistory.Entry entry = history.getValues(start, end, now, null); - final long callEnd = System.currentTimeMillis(); - if (DEBUG) Log.d(TAG, String.format("history call from %s to %s now=%s took %sms: %s", - new Date(start), new Date(end), new Date(now), callEnd - callStart, - historyEntryToString(entry))); - if (entry == null) { - return warn("no entry data"); - } - final long totalBytes = entry.rxBytes + entry.txBytes; - final DataUsageInfo usage = new DataUsageInfo(); - usage.startDate = start; - usage.usageLevel = totalBytes; - usage.period = formatDateRange(start, end); - usage.cycleStart = start; - usage.cycleEnd = end; - - if (policy != null) { - usage.limitLevel = policy.limitBytes > 0 ? policy.limitBytes : 0; - usage.warningLevel = policy.warningBytes > 0 ? policy.warningBytes : 0; - } else { - usage.warningLevel = getDefaultWarningLevel(); + return getUsageLevel(template, 0L /* start */, now /* end */, now); + } + } + + @Deprecated + private long getUsageLevel(NetworkTemplate template, long start, long end, long now) { + final INetworkStatsSession session = getSession(); + if (session != null) { + try { + final NetworkStatsHistory history = + session.getHistoryForNetwork(template, FIELDS); + final NetworkStatsHistory.Entry entry = history.getValues( + start, end, System.currentTimeMillis() /* now */, null /* recycle */); + if (entry != null) { + return entry.rxBytes + entry.txBytes; + } + Log.w(TAG, "Failed to get data usage, no entry data"); + } catch (RemoteException e) { + Log.w(TAG, "Failed to get data usage, remote call failed"); } - if (usage != null && mNetworkController != null) { - usage.carrier = mNetworkController.getMobileDataNetworkName(); + } + return -1L; + } + + private long getUsageLevel(NetworkTemplate template, long start, long end) { + try { + final Bucket bucket = mNetworkStatsManager.querySummaryForDevice( + getNetworkType(template), getActiveSubscriberId(mContext), start, end); + if (bucket != null) { + return bucket.getRxBytes() + bucket.getTxBytes(); } - return usage; + Log.w(TAG, "Failed to get data usage, no entry data"); } catch (RemoteException e) { - return warn("remote call failed"); + Log.w(TAG, "Failed to get data usage, remote call failed"); } + return -1L; } private NetworkPolicy findNetworkPolicy(NetworkTemplate template) { @@ -190,6 +241,7 @@ public class DataUsageController { return null; } + @Deprecated private static String historyEntryToString(NetworkStatsHistory.Entry entry) { return entry == null ? null : new StringBuilder("Entry[") .append("bucketDuration=").append(entry.bucketDuration) @@ -203,6 +255,17 @@ public class DataUsageController { .append(']').toString(); } + private static String statsBucketToString(Bucket bucket) { + return bucket == null ? null : new StringBuilder("Entry[") + .append("bucketDuration=").append(bucket.getEndTimeStamp() - bucket.getStartTimeStamp()) + .append(",bucketStart=").append(bucket.getStartTimeStamp()) + .append(",rxBytes=").append(bucket.getRxBytes()) + .append(",rxPackets=").append(bucket.getRxPackets()) + .append(",txBytes=").append(bucket.getTxBytes()) + .append(",txPackets=").append(bucket.getTxPackets()) + .append(']').toString(); + } + public void setMobileDataEnabled(boolean enabled) { Log.d(TAG, "setMobileDataEnabled: enabled=" + enabled); mTelephonyManager.setDataEnabled(enabled); @@ -221,6 +284,25 @@ public class DataUsageController { return mTelephonyManager.getDataEnabled(); } + static int getNetworkType(NetworkTemplate networkTemplate) { + if (networkTemplate == null) { + return ConnectivityManager.TYPE_NONE; + } + final int matchRule = networkTemplate.getMatchRule(); + switch (matchRule) { + case NetworkTemplate.MATCH_MOBILE: + case NetworkTemplate.MATCH_MOBILE_WILDCARD: + return ConnectivityManager.TYPE_MOBILE; + case NetworkTemplate.MATCH_WIFI: + case NetworkTemplate.MATCH_WIFI_WILDCARD: + return ConnectivityManager.TYPE_WIFI; + case NetworkTemplate.MATCH_ETHERNET: + return ConnectivityManager.TYPE_ETHERNET; + default: + return ConnectivityManager.TYPE_MOBILE; + } + } + private static String getActiveSubscriberId(Context context) { final TelephonyManager tele = TelephonyManager.from(context); final String actualSubscriberId = tele.getSubscriberId( diff --git a/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleChartData.java b/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleChartData.java new file mode 100644 index 000000000000..9b3ff8b2e165 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleChartData.java @@ -0,0 +1,56 @@ +/* + * 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 java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Usage data in a billing cycle with bucketized data for plotting the usage chart. + */ +public class NetworkCycleChartData extends NetworkCycleData { + public static final long BUCKET_DURATION_MS = TimeUnit.DAYS.toMillis(1); + + private List<NetworkCycleData> mUsageBuckets; + + private NetworkCycleChartData() { + } + + public List<NetworkCycleData> getUsageBuckets() { + return mUsageBuckets; + } + + public static class Builder extends NetworkCycleData.Builder { + private NetworkCycleChartData mObject = new NetworkCycleChartData(); + + public Builder setUsageBuckets(List<NetworkCycleData> buckets) { + getObject().mUsageBuckets = buckets; + return this; + } + + @Override + protected NetworkCycleChartData getObject() { + return mObject; + } + + @Override + public NetworkCycleChartData build() { + return getObject(); + } + } + +} diff --git a/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleChartDataLoader.java b/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleChartDataLoader.java new file mode 100644 index 000000000000..ec5a0b5cc4cd --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleChartDataLoader.java @@ -0,0 +1,108 @@ +/* + * 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.app.usage.NetworkStats; +import android.content.Context; +import android.os.RemoteException; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * Loader for network data usage history. It returns a list of usage data per billing cycle with + * bucketized usages. + */ +public class NetworkCycleChartDataLoader + extends NetworkCycleDataLoader<List<NetworkCycleChartData>> { + + private static final String TAG = "NetworkCycleChartLoader"; + + private final List<NetworkCycleChartData> mData; + + private NetworkCycleChartDataLoader(Builder builder) { + super(builder); + mData = new ArrayList<NetworkCycleChartData>(); + } + + @Override + void recordUsage(long start, long end) { + try { + final NetworkStats.Bucket bucket = mNetworkStatsManager.querySummaryForDevice( + mNetworkType, mSubId, start, end); + final long total = bucket == null ? 0L : bucket.getRxBytes() + bucket.getTxBytes(); + if (total > 0L) { + final NetworkCycleChartData.Builder builder = new NetworkCycleChartData.Builder(); + builder.setUsageBuckets(getUsageBuckets(start, end)) + .setStartTime(start) + .setEndTime(end) + .setTotalUsage(total); + mData.add(builder.build()); + } + } catch (RemoteException e) { + Log.e(TAG, "Exception querying network detail.", e); + } + } + + @Override + List<NetworkCycleChartData> getCycleUsage() { + return mData; + } + + public static Builder<?> builder(Context context) { + return new Builder<NetworkCycleChartDataLoader>(context) { + @Override + public NetworkCycleChartDataLoader build() { + return new NetworkCycleChartDataLoader(this); + } + }; + } + + private List<NetworkCycleData> getUsageBuckets(long start, long end) { + final List<NetworkCycleData> data = new ArrayList<>(); + long bucketStart = start; + long bucketEnd = start + NetworkCycleChartData.BUCKET_DURATION_MS; + while (bucketEnd <= end) { + long usage = 0L; + try { + final NetworkStats.Bucket bucket = mNetworkStatsManager.querySummaryForDevice( + mNetworkType, mSubId, bucketStart, bucketEnd); + if (bucket != null) { + usage = bucket.getRxBytes() + bucket.getTxBytes(); + } + } catch (RemoteException e) { + Log.e(TAG, "Exception querying network detail.", e); + } + data.add(new NetworkCycleData.Builder() + .setStartTime(bucketStart).setEndTime(bucketEnd).setTotalUsage(usage).build()); + bucketStart = bucketEnd; + bucketEnd += NetworkCycleChartData.BUCKET_DURATION_MS; + } + return data; + } + + public static abstract class Builder<T extends NetworkCycleChartDataLoader> + extends NetworkCycleDataLoader.Builder<T> { + + public Builder(Context context) { + super(context); + } + + } + +} diff --git a/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleData.java b/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleData.java new file mode 100644 index 000000000000..26c65a2c4a48 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleData.java @@ -0,0 +1,70 @@ +/* + * 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; + +/** + * Base data structure representing usage data in a billing cycle. + */ +public class NetworkCycleData { + + private long mStartTime; + private long mEndTime; + private long mTotalUsage; + + protected NetworkCycleData() { + } + + public long getStartTime() { + return mStartTime; + } + + public long getEndTime() { + return mEndTime; + } + + public long getTotalUsage() { + return mTotalUsage; + } + + public static class Builder { + + private NetworkCycleData mObject = new NetworkCycleData(); + + public Builder setStartTime(long start) { + getObject().mStartTime = start; + return this; + } + + public Builder setEndTime(long end) { + getObject().mEndTime = end; + return this; + } + + public Builder setTotalUsage(long total) { + getObject().mTotalUsage = total; + return this; + } + + protected NetworkCycleData getObject() { + return mObject; + } + + public NetworkCycleData build() { + return getObject(); + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataForUid.java b/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataForUid.java new file mode 100644 index 000000000000..9d13717bbbcc --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataForUid.java @@ -0,0 +1,65 @@ +/* + * 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 java.util.concurrent.TimeUnit; + +/** + * Usage data in a billing cycle for a specific Uid. + */ +public class NetworkCycleDataForUid extends NetworkCycleData { + + private long mBackgroudUsage; + private long mForegroudUsage; + + private NetworkCycleDataForUid() { + } + + public long getBackgroudUsage() { + return mBackgroudUsage; + } + + public long getForegroudUsage() { + return mForegroudUsage; + } + + public static class Builder extends NetworkCycleData.Builder { + + private NetworkCycleDataForUid mObject = new NetworkCycleDataForUid(); + + public Builder setBackgroundUsage(long backgroundUsage) { + getObject().mBackgroudUsage = backgroundUsage; + return this; + } + + public Builder setForegroundUsage(long foregroundUsage) { + getObject().mForegroudUsage = foregroundUsage; + return this; + } + + @Override + public NetworkCycleDataForUid getObject() { + return mObject; + } + + @Override + public NetworkCycleDataForUid build() { + return getObject(); + } + } + +} diff --git a/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataForUidLoader.java b/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataForUidLoader.java new file mode 100644 index 000000000000..cc970b93f601 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataForUidLoader.java @@ -0,0 +1,112 @@ +/* + * 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.app.usage.NetworkStats.Bucket.STATE_FOREGROUND; +import static android.net.NetworkStats.TAG_NONE; + +import android.app.usage.NetworkStats; +import android.content.Context; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * Loader for network data usage history. It returns a list of usage data per billing cycle for a + * specific Uid. + */ +public class NetworkCycleDataForUidLoader extends + NetworkCycleDataLoader<List<NetworkCycleDataForUid>> { + private static final String TAG = "NetworkDataForUidLoader"; + + private final List<NetworkCycleDataForUid> mData; + private final int mUid; + private final boolean mRetrieveDetail; + + private NetworkCycleDataForUidLoader(Builder builder) { + super(builder); + mUid = builder.mUid; + mRetrieveDetail = builder.mRetrieveDetail; + mData = new ArrayList<NetworkCycleDataForUid>(); + } + + @Override + void recordUsage(long start, long end) { + try { + final NetworkStats stats = mNetworkStatsManager.queryDetailsForUid( + mNetworkType, mSubId, start, end, mUid); + final long total = getTotalUsage(stats); + if (total > 0L) { + final NetworkCycleDataForUid.Builder builder = new NetworkCycleDataForUid.Builder(); + builder.setStartTime(start) + .setEndTime(end) + .setTotalUsage(total); + if (mRetrieveDetail) { + final long foreground = getForegroundUsage(start, end); + builder.setBackgroundUsage(total - foreground) + .setForegroundUsage(foreground); + } + mData.add(builder.build()); + } + } catch (Exception e) { + Log.e(TAG, "Exception querying network detail.", e); + } + } + + @Override + List<NetworkCycleDataForUid> getCycleUsage() { + return mData; + } + + public static Builder<?> builder(Context context) { + return new Builder<NetworkCycleDataForUidLoader>(context) { + @Override + public NetworkCycleDataForUidLoader build() { + return new NetworkCycleDataForUidLoader(this); + } + }; + } + + private long getForegroundUsage(long start, long end) { + final NetworkStats stats = mNetworkStatsManager.queryDetailsForUidTagState( + mNetworkType, mSubId, start, end, mUid, TAG_NONE, STATE_FOREGROUND); + return getTotalUsage(stats); + } + + public static abstract class Builder<T extends NetworkCycleDataForUidLoader> + extends NetworkCycleDataLoader.Builder<T> { + + private int mUid; + private boolean mRetrieveDetail = true; + + public Builder(Context context) { + super(context); + } + + public Builder<T> setUid(int uid) { + mUid = uid; + return this; + } + + public Builder<T> setRetrieveDetail(boolean retrieveDetail) { + mRetrieveDetail = retrieveDetail; + return this; + } + } + +} diff --git a/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataLoader.java b/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataLoader.java new file mode 100644 index 000000000000..d9578014e846 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/net/NetworkCycleDataLoader.java @@ -0,0 +1,186 @@ +/* + * 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.NetworkStatsHistory.FIELD_RX_BYTES; +import static android.net.NetworkStatsHistory.FIELD_TX_BYTES; + +import android.app.usage.NetworkStats; +import android.app.usage.NetworkStatsManager; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.INetworkStatsService; +import android.net.INetworkStatsSession; +import android.net.NetworkPolicy; +import android.net.NetworkPolicyManager; +import android.net.NetworkStatsHistory; +import android.net.NetworkTemplate; +import android.net.TrafficStats; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.text.format.DateUtils; +import android.util.Pair; + +import com.android.settingslib.NetworkPolicyEditor; + +import java.time.ZonedDateTime; +import java.util.Iterator; + +import androidx.annotation.VisibleForTesting; +import androidx.loader.content.AsyncTaskLoader; + +/** + * Loader for network data usage history. It returns a list of usage data per billing cycle. + */ +public abstract class NetworkCycleDataLoader<D> extends AsyncTaskLoader<D> { + private static final String TAG = "NetworkCycleDataLoader"; + protected final NetworkStatsManager mNetworkStatsManager; + protected final String mSubId; + protected final int mNetworkType; + private final NetworkPolicy mPolicy; + private final NetworkTemplate mNetworkTemplate; + @VisibleForTesting + final INetworkStatsService mNetworkStatsService; + + protected NetworkCycleDataLoader(Builder<?> builder) { + super(builder.mContext); + mSubId = builder.mSubId; + mNetworkType = builder.mNetworkType; + mNetworkTemplate = builder.mNetworkTemplate; + mNetworkStatsManager = (NetworkStatsManager) + builder.mContext.getSystemService(Context.NETWORK_STATS_SERVICE); + mNetworkStatsService = INetworkStatsService.Stub.asInterface( + ServiceManager.getService(Context.NETWORK_STATS_SERVICE)); + final NetworkPolicyEditor policyEditor = + new NetworkPolicyEditor(NetworkPolicyManager.from(builder.mContext)); + policyEditor.read(); + mPolicy = policyEditor.getPolicy(mNetworkTemplate); + } + + @Override + protected void onStartLoading() { + super.onStartLoading(); + forceLoad(); + } + + public D loadInBackground() { + if (mPolicy == null) { + loadFourWeeksData(); + } else { + loadPolicyData(); + } + return getCycleUsage(); + } + + @VisibleForTesting + void loadPolicyData() { + final Iterator<Pair<ZonedDateTime, ZonedDateTime>> iterator = + NetworkPolicyManager.cycleIterator(mPolicy); + while (iterator.hasNext()) { + final Pair<ZonedDateTime, ZonedDateTime> cycle = iterator.next(); + final long cycleStart = cycle.first.toInstant().toEpochMilli(); + final long cycleEnd = cycle.second.toInstant().toEpochMilli(); + recordUsage(cycleStart, cycleEnd); + } + } + + @Override + protected void onStopLoading() { + super.onStopLoading(); + cancelLoad(); + } + + @Override + protected void onReset() { + super.onReset(); + cancelLoad(); + } + + @VisibleForTesting + void loadFourWeeksData() { + try { + final INetworkStatsSession networkSession = mNetworkStatsService.openSession(); + final NetworkStatsHistory networkHistory = networkSession.getHistoryForNetwork( + mNetworkTemplate, FIELD_RX_BYTES | FIELD_TX_BYTES); + final long historyStart = networkHistory.getStart(); + final long historyEnd = networkHistory.getEnd(); + + long cycleEnd = historyEnd; + while (cycleEnd > historyStart) { + final long cycleStart = Math.max( + historyStart, cycleEnd - (DateUtils.WEEK_IN_MILLIS * 4)); + recordUsage(cycleStart, cycleEnd); + cycleEnd = cycleStart; + } + + TrafficStats.closeQuietly(networkSession); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + + @VisibleForTesting + abstract void recordUsage(long start, long end); + + abstract D getCycleUsage(); + + public static Builder<?> builder(Context context) { + return new Builder<NetworkCycleDataLoader>(context) { + @Override + public NetworkCycleDataLoader build() { + return null; + } + }; + } + + protected long getTotalUsage(NetworkStats stats) { + long bytes = 0L; + if (stats != null) { + final NetworkStats.Bucket bucket = new NetworkStats.Bucket(); + while (stats.hasNextBucket() && stats.getNextBucket(bucket)) { + bytes += bucket.getRxBytes() + bucket.getTxBytes(); + } + stats.close(); + } + return bytes; + } + + public static abstract class Builder<T extends NetworkCycleDataLoader> { + private final Context mContext; + private String mSubId; + private int mNetworkType; + private NetworkTemplate mNetworkTemplate; + + public Builder (Context context) { + mContext = context; + } + + public Builder<T> setSubscriberId(String subId) { + mSubId = subId; + return this; + } + + public Builder<T> setNetworkTemplate(NetworkTemplate template) { + mNetworkTemplate = template; + mNetworkType = DataUsageController.getNetworkType(template); + return this; + } + + public abstract T build(); + } + +} diff --git a/packages/SettingsLib/src/com/android/settingslib/net/NetworkStatsSummaryLoader.java b/packages/SettingsLib/src/com/android/settingslib/net/NetworkStatsSummaryLoader.java new file mode 100644 index 000000000000..34e6097ea46e --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/net/NetworkStatsSummaryLoader.java @@ -0,0 +1,112 @@ +/* + * 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.app.usage.NetworkStatsManager; +import android.app.usage.NetworkStats; +import android.content.Context; +import android.os.RemoteException; +import android.util.Log; + +import androidx.loader.content.AsyncTaskLoader; + +/** + * Loader for retrieving the network stats summary for all UIDs. + */ +public class NetworkStatsSummaryLoader extends AsyncTaskLoader<NetworkStats> { + + private static final String TAG = "NetworkDetailLoader"; + private final NetworkStatsManager mNetworkStatsManager; + private final long mStart; + private final long mEnd; + private final String mSubId; + private final int mNetworkType; + + private NetworkStatsSummaryLoader(Builder builder) { + super(builder.mContext); + mStart = builder.mStart; + mEnd = builder.mEnd; + mSubId = builder.mSubId; + mNetworkType = builder.mNetworkType; + mNetworkStatsManager = (NetworkStatsManager) + builder.mContext.getSystemService(Context.NETWORK_STATS_SERVICE); + } + + @Override + protected void onStartLoading() { + super.onStartLoading(); + forceLoad(); + } + + @Override + public NetworkStats loadInBackground() { + try { + return mNetworkStatsManager.querySummary(mNetworkType, mSubId, mStart, mEnd); + } catch (RemoteException e) { + Log.e(TAG, "Exception querying network detail.", e); + return null; + } + } + + @Override + protected void onStopLoading() { + super.onStopLoading(); + cancelLoad(); + } + + @Override + protected void onReset() { + super.onReset(); + cancelLoad(); + } + + public static class Builder { + private final Context mContext; + private long mStart; + private long mEnd; + private String mSubId; + private int mNetworkType; + + public Builder(Context context) { + mContext = context; + } + + public Builder setStartTime(long start) { + mStart = start; + return this; + } + + public Builder setEndTime(long end) { + mEnd = end; + return this; + } + + public Builder setSubscriberId(String subId) { + mSubId = subId; + return this; + } + + public Builder setNetworkType(int networkType) { + mNetworkType = networkType; + return this; + } + + public NetworkStatsSummaryLoader build() { + return new NetworkStatsSummaryLoader(this); + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/net/SummaryForAllUidLoader.java b/packages/SettingsLib/src/com/android/settingslib/net/SummaryForAllUidLoader.java index 572bae1112da..649aeffd0d5f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/net/SummaryForAllUidLoader.java +++ b/packages/SettingsLib/src/com/android/settingslib/net/SummaryForAllUidLoader.java @@ -24,6 +24,12 @@ import android.net.NetworkTemplate; import android.os.Bundle; import android.os.RemoteException; +/** + * Framework loader is deprecated, use the compat version instead. + * + * @deprecated + */ +@Deprecated public class SummaryForAllUidLoader extends AsyncTaskLoader<NetworkStats> { private static final String KEY_TEMPLATE = "template"; private static final String KEY_START = "start"; 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..82bb0115c66f --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/net/SummaryForAllUidLoaderCompat.java @@ -0,0 +1,87 @@ +/* + * 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; + +/** + * Deprecated in favor of {@link NetworkStatsDetailLoader} + * + * @deprecated + */ +@Deprecated +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/net/UidDetailProvider.java b/packages/SettingsLib/src/com/android/settingslib/net/UidDetailProvider.java index 224b9679a9a2..c14f5588cddf 100644 --- a/packages/SettingsLib/src/com/android/settingslib/net/UidDetailProvider.java +++ b/packages/SettingsLib/src/com/android/settingslib/net/UidDetailProvider.java @@ -28,9 +28,9 @@ import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.net.ConnectivityManager; import android.net.TrafficStats; -import android.os.UserManager; -import android.os.UserHandle; import android.os.RemoteException; +import android.os.UserHandle; +import android.os.UserManager; import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/ZenDurationDialog.java b/packages/SettingsLib/src/com/android/settingslib/notification/ZenDurationDialog.java index 6da486ab3aa8..b9197fe1ebd5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/notification/ZenDurationDialog.java +++ b/packages/SettingsLib/src/com/android/settingslib/notification/ZenDurationDialog.java @@ -17,14 +17,12 @@ package com.android.settingslib.notification; import android.app.ActivityManager; -import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.provider.Settings; import android.service.notification.Condition; import android.service.notification.ZenModeConfig; -import androidx.annotation.VisibleForTesting; import android.view.LayoutInflater; import android.view.View; import android.widget.CompoundButton; @@ -35,6 +33,9 @@ import android.widget.RadioGroup; import android.widget.ScrollView; import android.widget.TextView; +import androidx.annotation.VisibleForTesting; +import androidx.appcompat.app.AlertDialog; + import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto; import com.android.internal.policy.PhoneWindow; @@ -66,12 +67,17 @@ public class ZenDurationDialog { } public Dialog createDialog() { - int zenDuration = Settings.Global.getInt( - mContext.getContentResolver(), Settings.Global.ZEN_DURATION, - Settings.Global.ZEN_DURATION_FOREVER); + final AlertDialog.Builder builder = new AlertDialog.Builder(mContext); + setupDialog(builder); + return builder.create(); + } + + public void setupDialog(AlertDialog.Builder builder) { + int zenDuration = Settings.Secure.getInt( + mContext.getContentResolver(), Settings.Secure.ZEN_DURATION, + Settings.Secure.ZEN_DURATION_FOREVER); - final AlertDialog.Builder builder = new AlertDialog.Builder(mContext) - .setTitle(R.string.zen_mode_duration_settings_title) + builder.setTitle(R.string.zen_mode_duration_settings_title) .setNegativeButton(R.string.cancel, null) .setPositiveButton(R.string.okay, new DialogInterface.OnClickListener() { @@ -84,19 +90,18 @@ public class ZenDurationDialog { View contentView = getContentView(); setupRadioButtons(zenDuration); builder.setView(contentView); - return builder.create(); } @VisibleForTesting protected void updateZenDuration(int currZenDuration) { final int checkedRadioButtonId = mZenRadioGroup.getCheckedRadioButtonId(); - int newZenDuration = Settings.Global.getInt( - mContext.getContentResolver(), Settings.Global.ZEN_DURATION, - Settings.Global.ZEN_DURATION_FOREVER); + int newZenDuration = Settings.Secure.getInt( + mContext.getContentResolver(), Settings.Secure.ZEN_DURATION, + Settings.Secure.ZEN_DURATION_FOREVER); switch (checkedRadioButtonId) { case FOREVER_CONDITION_INDEX: - newZenDuration = Settings.Global.ZEN_DURATION_FOREVER; + newZenDuration = Settings.Secure.ZEN_DURATION_FOREVER; MetricsLogger.action(mContext, MetricsProto.MetricsEvent. NOTIFICATION_ZEN_MODE_DURATION_FOREVER); @@ -110,7 +115,7 @@ public class ZenDurationDialog { newZenDuration); break; case ALWAYS_ASK_CONDITION_INDEX: - newZenDuration = Settings.Global.ZEN_DURATION_PROMPT; + newZenDuration = Settings.Secure.ZEN_DURATION_PROMPT; MetricsLogger.action(mContext, MetricsProto.MetricsEvent. NOTIFICATION_ZEN_MODE_DURATION_PROMPT); @@ -118,8 +123,8 @@ public class ZenDurationDialog { } if (currZenDuration != newZenDuration) { - Settings.Global.putInt(mContext.getContentResolver(), - Settings.Global.ZEN_DURATION, newZenDuration); + Settings.Secure.putInt(mContext.getContentResolver(), + Settings.Secure.ZEN_DURATION, newZenDuration); } } @@ -269,8 +274,7 @@ public class ZenDurationDialog { String radioContentText = ""; switch (rowIndex) { case FOREVER_CONDITION_INDEX: - radioContentText = mContext.getString( - com.android.internal.R.string.zen_mode_forever); + radioContentText = mContext.getString(R.string.zen_mode_forever); break; case COUNTDOWN_CONDITION_INDEX: Condition condition = ZenModeConfig.toTimeCondition(mContext, diff --git a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionCategory.java b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionCategory.java deleted file mode 100644 index 19e556ad55bb..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionCategory.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * 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; - -public class SuggestionCategory { - public String category; - public String pkg; - public boolean multiple; - public boolean exclusive; - public long exclusiveExpireDaysInMillis; -} diff --git a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionController.java b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionController.java index 6a8b01a23688..ec774872ce67 100644 --- a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionController.java +++ b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionController.java @@ -24,9 +24,10 @@ import android.os.IBinder; import android.os.RemoteException; import android.service.settings.suggestions.ISuggestionService; import android.service.settings.suggestions.Suggestion; +import android.util.Log; + import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; -import android.util.Log; import java.util.List; diff --git a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionControllerMixin.java b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionControllerMixin.java index ee8b6fe60478..b0e987ee2779 100644 --- a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionControllerMixin.java +++ b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionControllerMixin.java @@ -17,22 +17,26 @@ package com.android.settingslib.suggestions; import android.app.LoaderManager; -import androidx.lifecycle.OnLifecycleEvent; import android.content.ComponentName; import android.content.Context; import android.content.Loader; import android.os.Bundle; import android.service.settings.suggestions.Suggestion; -import androidx.annotation.Nullable; import android.util.Log; +import androidx.annotation.Nullable; +import androidx.lifecycle.OnLifecycleEvent; + import com.android.settingslib.core.lifecycle.Lifecycle; import java.util.List; /** - * Manages IPC communication to SettingsIntelligence for suggestion related services. + * Framework mixin is deprecated, use the compat version instead. + * + * @deprecated */ +@Deprecated public class SuggestionControllerMixin implements SuggestionController.ServiceConnectionListener, androidx.lifecycle.LifecycleObserver, LoaderManager.LoaderCallbacks<List<Suggestion>> { @@ -64,7 +68,7 @@ public class SuggestionControllerMixin implements SuggestionController.ServiceCo mContext = context.getApplicationContext(); mHost = host; mSuggestionController = new SuggestionController(mContext, componentName, - this /* serviceConnectionListener */); + this /* serviceConnectionListener */); if (lifecycle != null) { lifecycle.addObserver(this); } 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..eea49bcb2384 --- /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 androidx.annotation.Nullable; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; + +import com.android.settingslib.core.lifecycle.Lifecycle; + +import java.util.List; + +/** + * 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/SuggestionList.java b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionList.java deleted file mode 100644 index a89092040e71..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionList.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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.Intent; -import android.util.ArrayMap; -import android.util.ArraySet; - -import com.android.settingslib.drawer.Tile; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; - -public class SuggestionList { - // Category -> list of suggestion map - private final Map<SuggestionCategory, List<Tile>> mSuggestions; - - // A flatten list of all suggestions. - private List<Tile> mSuggestionList; - - public SuggestionList() { - mSuggestions = new ArrayMap<>(); - } - - public void addSuggestions(SuggestionCategory category, List<Tile> suggestions) { - mSuggestions.put(category, suggestions); - } - - public List<Tile> getSuggestions() { - if (mSuggestionList != null) { - return mSuggestionList; - } - mSuggestionList = new ArrayList<>(); - for (List<Tile> suggestions : mSuggestions.values()) { - mSuggestionList.addAll(suggestions); - } - dedupeSuggestions(mSuggestionList); - return mSuggestionList; - } - - public boolean isExclusiveSuggestionCategory() { - if (mSuggestions.size() != 1) { - // If there is no category, or more than 1 category, it's not exclusive by definition. - return false; - } - for (SuggestionCategory category : mSuggestions.keySet()) { - if (category.exclusive) { - return true; - } - } - return false; - } - - /** - * Filter suggestions list so they are all unique. - */ - private void dedupeSuggestions(List<Tile> suggestions) { - final Set<String> intents = new ArraySet<>(); - for (int i = suggestions.size() - 1; i >= 0; i--) { - final Tile suggestion = suggestions.get(i); - final String intentUri = suggestion.intent.toUri(Intent.URI_INTENT_SCHEME); - if (intents.contains(intentUri)) { - suggestions.remove(i); - } else { - intents.add(intentUri); - } - } - } -} diff --git a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionLoader.java b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionLoader.java index 9c1af1edc778..8011424bb0d2 100644 --- a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionLoader.java +++ b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionLoader.java @@ -24,6 +24,12 @@ import com.android.settingslib.utils.AsyncLoader; import java.util.List; +/** + * Framework loader is deprecated, use the compat version instead. + * + * @deprecated + */ +@Deprecated public class SuggestionLoader extends AsyncLoader<List<Suggestion>> { public static final int LOADER_ID_SUGGESTIONS = 42; 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/suggestions/SuggestionParser.java b/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionParser.java deleted file mode 100644 index 8705c9846a6f..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/suggestions/SuggestionParser.java +++ /dev/null @@ -1,498 +0,0 @@ -/* - * 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.Manifest; -import android.accounts.Account; -import android.accounts.AccountManager; -import android.annotation.RequiresPermission; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.content.pm.UserInfo; -import android.content.res.Resources; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.os.UserHandle; -import android.os.UserManager; -import android.provider.Settings; -import androidx.annotation.VisibleForTesting; -import android.text.TextUtils; -import android.text.format.DateUtils; -import android.util.ArrayMap; -import android.util.AttributeSet; -import android.util.Log; -import android.util.Pair; -import android.util.Xml; -import android.view.InflateException; - -import com.android.settingslib.drawer.Tile; -import com.android.settingslib.drawer.TileUtils; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -public class SuggestionParser { - - private static final String TAG = "SuggestionParser"; - - // If defined, only returns this suggestion if the feature is supported. - public static final String META_DATA_REQUIRE_FEATURE = "com.android.settings.require_feature"; - - // If defined, only display this optional step if an account of that type exists. - private static final String META_DATA_REQUIRE_ACCOUNT = "com.android.settings.require_account"; - - // If defined and not true, do not should optional step. - private static final String META_DATA_IS_SUPPORTED = "com.android.settings.is_supported"; - - // If defined, only display this optional step if the current user is of that type. - private static final String META_DATA_REQUIRE_USER_TYPE = - "com.android.settings.require_user_type"; - - // If defined, only display this optional step if a connection is available. - private static final String META_DATA_IS_CONNECTION_REQUIRED = - "com.android.settings.require_connection"; - - // The valid values that setup wizard recognizes for differentiating user types. - private static final String META_DATA_PRIMARY_USER_TYPE_VALUE = "primary"; - private static final String META_DATA_ADMIN_USER_TYPE_VALUE = "admin"; - private static final String META_DATA_GUEST_USER_TYPE_VALUE = "guest"; - private static final String META_DATA_RESTRICTED_USER_TYPE_VALUE = "restricted"; - - /** - * Allows suggestions to appear after a certain number of days, and to re-appear if dismissed. - * For instance: - * 0,10 - * Will appear immediately, but if the user removes it, it will come back after 10 days. - * - * Another example: - * 10,30 - * Will only show up after 10 days, and then again after 30. - */ - public static final String META_DATA_DISMISS_CONTROL = "com.android.settings.dismiss"; - - // Shared prefs keys for storing dismissed state. - // Index into current dismissed state. - public static final String SETUP_TIME = "_setup_time"; - private static final String IS_DISMISSED = "_is_dismissed"; - - // Default dismiss control for smart suggestions. - private static final String DEFAULT_SMART_DISMISS_CONTROL = "0"; - - private final Context mContext; - private final List<SuggestionCategory> mSuggestionList; - private final ArrayMap<Pair<String, String>, Tile> mAddCache = new ArrayMap<>(); - private final SharedPreferences mSharedPrefs; - private final String mDefaultDismissControl; - - public SuggestionParser(Context context, SharedPreferences sharedPrefs, int orderXml, - String defaultDismissControl) { - this( - context, - sharedPrefs, - (List<SuggestionCategory>) new SuggestionOrderInflater(context).parse(orderXml), - defaultDismissControl); - } - - public SuggestionParser(Context context, SharedPreferences sharedPrefs, int orderXml) { - this(context, sharedPrefs, orderXml, DEFAULT_SMART_DISMISS_CONTROL); - } - - @VisibleForTesting - public SuggestionParser( - Context context, - SharedPreferences sharedPrefs, - List<SuggestionCategory> suggestionList, - String defaultDismissControl) { - mContext = context; - mSuggestionList = suggestionList; - mSharedPrefs = sharedPrefs; - mDefaultDismissControl = defaultDismissControl; - } - - public SuggestionList getSuggestions(boolean isSmartSuggestionEnabled) { - final SuggestionList suggestionList = new SuggestionList(); - final int N = mSuggestionList.size(); - for (int i = 0; i < N; i++) { - final SuggestionCategory category = mSuggestionList.get(i); - if (category.exclusive && !isExclusiveCategoryExpired(category)) { - // If suggestions from an exclusive category are present, parsing is stopped - // and only suggestions from that category are displayed. Note that subsequent - // exclusive categories are also ignored. - final List<Tile> exclusiveSuggestions = new ArrayList<>(); - - // Read suggestion and force isSmartSuggestion to be false so the rule defined - // from each suggestion itself is used. - readSuggestions(category, exclusiveSuggestions, false /* isSmartSuggestion */); - if (!exclusiveSuggestions.isEmpty()) { - final SuggestionList exclusiveList = new SuggestionList(); - exclusiveList.addSuggestions(category, exclusiveSuggestions); - return exclusiveList; - } - } else { - // Either the category is not exclusive, or the exclusiveness expired so we should - // treat it as a normal category. - final List<Tile> suggestions = new ArrayList<>(); - readSuggestions(category, suggestions, isSmartSuggestionEnabled); - suggestionList.addSuggestions(category, suggestions); - } - } - return suggestionList; - } - - /** - * Dismisses a suggestion, returns true if the suggestion has no more dismisses left and should - * be disabled. - */ - public boolean dismissSuggestion(Tile suggestion) { - final String keyBase = suggestion.intent.getComponent().flattenToShortString(); - mSharedPrefs.edit() - .putBoolean(keyBase + IS_DISMISSED, true) - .commit(); - return true; - } - - @VisibleForTesting - public void filterSuggestions( - List<Tile> suggestions, int countBefore, boolean isSmartSuggestionEnabled) { - for (int i = countBefore; i < suggestions.size(); i++) { - if (!isAvailable(suggestions.get(i)) || - !isSupported(suggestions.get(i)) || - !satisifesRequiredUserType(suggestions.get(i)) || - !satisfiesRequiredAccount(suggestions.get(i)) || - !satisfiesConnectivity(suggestions.get(i)) || - isDismissed(suggestions.get(i), isSmartSuggestionEnabled)) { - suggestions.remove(i--); - } - } - } - - @VisibleForTesting - void readSuggestions( - SuggestionCategory category, List<Tile> suggestions, boolean isSmartSuggestionEnabled) { - int countBefore = suggestions.size(); - Intent intent = new Intent(Intent.ACTION_MAIN); - intent.addCategory(category.category); - if (category.pkg != null) { - intent.setPackage(category.pkg); - } - TileUtils.getTilesForIntent(mContext, new UserHandle(UserHandle.myUserId()), intent, - mAddCache, null, suggestions, true, false, false, true /* shouldUpdateTiles */); - filterSuggestions(suggestions, countBefore, isSmartSuggestionEnabled); - if (!category.multiple && suggestions.size() > (countBefore + 1)) { - // If there are too many, remove them all and only re-add the one with the highest - // priority. - Tile item = suggestions.remove(suggestions.size() - 1); - while (suggestions.size() > countBefore) { - Tile last = suggestions.remove(suggestions.size() - 1); - if (last.priority > item.priority) { - item = last; - } - } - // If category is marked as done, do not add any item. - if (!isCategoryDone(category.category)) { - suggestions.add(item); - } - } - } - - private boolean isAvailable(Tile suggestion) { - final String featuresRequired = suggestion.metaData.getString(META_DATA_REQUIRE_FEATURE); - if (featuresRequired != null) { - for (String feature : featuresRequired.split(",")) { - if (TextUtils.isEmpty(feature)) { - Log.w(TAG, "Found empty substring when parsing required features: " - + featuresRequired); - } else if (!mContext.getPackageManager().hasSystemFeature(feature)) { - Log.i(TAG, suggestion.title + " requires unavailable feature " + feature); - return false; - } - } - } - return true; - } - - @RequiresPermission(Manifest.permission.MANAGE_USERS) - private boolean satisifesRequiredUserType(Tile suggestion) { - final String requiredUser = suggestion.metaData.getString(META_DATA_REQUIRE_USER_TYPE); - if (requiredUser != null) { - final UserManager userManager = mContext.getSystemService(UserManager.class); - UserInfo userInfo = userManager.getUserInfo(UserHandle.myUserId()); - for (String userType : requiredUser.split("\\|")) { - final boolean primaryUserCondtionMet = userInfo.isPrimary() - && META_DATA_PRIMARY_USER_TYPE_VALUE.equals(userType); - final boolean adminUserConditionMet = userInfo.isAdmin() - && META_DATA_ADMIN_USER_TYPE_VALUE.equals(userType); - final boolean guestUserCondtionMet = userInfo.isGuest() - && META_DATA_GUEST_USER_TYPE_VALUE.equals(userType); - final boolean restrictedUserCondtionMet = userInfo.isRestricted() - && META_DATA_RESTRICTED_USER_TYPE_VALUE.equals(userType); - if (primaryUserCondtionMet || adminUserConditionMet || guestUserCondtionMet - || restrictedUserCondtionMet) { - return true; - } - } - Log.i(TAG, suggestion.title + " requires user type " + requiredUser); - return false; - } - return true; - } - - public boolean satisfiesRequiredAccount(Tile suggestion) { - final String requiredAccountType = suggestion.metaData.getString(META_DATA_REQUIRE_ACCOUNT); - if (requiredAccountType == null) { - return true; - } - AccountManager accountManager = mContext.getSystemService(AccountManager.class); - Account[] accounts = accountManager.getAccountsByType(requiredAccountType); - boolean satisfiesRequiredAccount = accounts.length > 0; - if (!satisfiesRequiredAccount) { - Log.i(TAG, suggestion.title + " requires unavailable account type " - + requiredAccountType); - } - return satisfiesRequiredAccount; - } - - public boolean isSupported(Tile suggestion) { - final int isSupportedResource = suggestion.metaData.getInt(META_DATA_IS_SUPPORTED); - try { - if (suggestion.intent == null) { - return false; - } - final Resources res = mContext.getPackageManager().getResourcesForActivity( - suggestion.intent.getComponent()); - boolean isSupported = - isSupportedResource != 0 ? res.getBoolean(isSupportedResource) : true; - if (!isSupported) { - Log.i(TAG, suggestion.title + " requires unsupported resource " - + isSupportedResource); - } - return isSupported; - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "Cannot find resources for " + suggestion.intent.getComponent()); - return false; - } catch (Resources.NotFoundException e) { - Log.w(TAG, "Cannot find resources for " + suggestion.intent.getComponent(), e); - return false; - } - } - - private boolean satisfiesConnectivity(Tile suggestion) { - final boolean isConnectionRequired = - suggestion.metaData.getBoolean(META_DATA_IS_CONNECTION_REQUIRED); - if (!isConnectionRequired) { - return true; - } - ConnectivityManager cm = - (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo netInfo = cm.getActiveNetworkInfo(); - boolean satisfiesConnectivity = netInfo != null && netInfo.isConnectedOrConnecting(); - if (!satisfiesConnectivity) { - Log.i(TAG, suggestion.title + " is missing required connection."); - } - return satisfiesConnectivity; - } - - public boolean isCategoryDone(String category) { - String name = Settings.Secure.COMPLETED_CATEGORY_PREFIX + category; - return Settings.Secure.getInt(mContext.getContentResolver(), name, 0) != 0; - } - - public void markCategoryDone(String category) { - String name = Settings.Secure.COMPLETED_CATEGORY_PREFIX + category; - Settings.Secure.putInt(mContext.getContentResolver(), name, 1); - } - - /** - * Whether or not the category's exclusiveness has expired. - */ - private boolean isExclusiveCategoryExpired(SuggestionCategory category) { - final String keySetupTime = category.category + SETUP_TIME; - final long currentTime = System.currentTimeMillis(); - if (!mSharedPrefs.contains(keySetupTime)) { - mSharedPrefs.edit() - .putLong(keySetupTime, currentTime) - .commit(); - } - if (category.exclusiveExpireDaysInMillis < 0) { - // negative means never expires - return false; - } - final long setupTime = mSharedPrefs.getLong(keySetupTime, 0); - final long elapsedTime = currentTime - setupTime; - Log.d(TAG, "Day " + elapsedTime / DateUtils.DAY_IN_MILLIS + " for " + category.category); - return elapsedTime > category.exclusiveExpireDaysInMillis; - } - - @VisibleForTesting - boolean isDismissed(Tile suggestion, boolean isSmartSuggestionEnabled) { - String dismissControl = getDismissControl(suggestion, isSmartSuggestionEnabled); - String keyBase = suggestion.intent.getComponent().flattenToShortString(); - if (!mSharedPrefs.contains(keyBase + SETUP_TIME)) { - mSharedPrefs.edit() - .putLong(keyBase + SETUP_TIME, System.currentTimeMillis()) - .commit(); - } - // Check if it's already manually dismissed - final boolean isDismissed = mSharedPrefs.getBoolean(keyBase + IS_DISMISSED, false); - if (isDismissed) { - return true; - } - if (dismissControl == null) { - return false; - } - // Parse when suggestion should first appear. return true to artificially hide suggestion - // before then. - int firstAppearDay = parseDismissString(dismissControl); - long firstAppearDayInMs = getEndTime(mSharedPrefs.getLong(keyBase + SETUP_TIME, 0), - firstAppearDay); - if (System.currentTimeMillis() >= firstAppearDayInMs) { - // Dismiss timeout has passed, undismiss it. - mSharedPrefs.edit() - .putBoolean(keyBase + IS_DISMISSED, false) - .commit(); - return false; - } - return true; - } - - private long getEndTime(long startTime, int daysDelay) { - long days = daysDelay * DateUtils.DAY_IN_MILLIS; - return startTime + days; - } - - /** - * Parse the first int from a string formatted as "0,1,2..." - * The value means suggestion should first appear on Day X. - */ - private int parseDismissString(String dismissControl) { - final String[] dismissStrs = dismissControl.split(","); - return Integer.parseInt(dismissStrs[0]); - } - - private String getDismissControl(Tile suggestion, boolean isSmartSuggestionEnabled) { - if (isSmartSuggestionEnabled) { - return mDefaultDismissControl; - } else { - return suggestion.metaData.getString(META_DATA_DISMISS_CONTROL); - } - } - - private static class SuggestionOrderInflater { - private static final String TAG_LIST = "optional-steps"; - private static final String TAG_ITEM = "step"; - - private static final String ATTR_CATEGORY = "category"; - private static final String ATTR_PACKAGE = "package"; - private static final String ATTR_MULTIPLE = "multiple"; - private static final String ATTR_EXCLUSIVE = "exclusive"; - private static final String ATTR_EXCLUSIVE_EXPIRE_DAYS = "exclusiveExpireDays"; - - private final Context mContext; - - public SuggestionOrderInflater(Context context) { - mContext = context; - } - - public Object parse(int resource) { - XmlPullParser parser = mContext.getResources().getXml(resource); - final AttributeSet attrs = Xml.asAttributeSet(parser); - try { - // Look for the root node. - int type; - do { - type = parser.next(); - } while (type != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT); - - if (type != XmlPullParser.START_TAG) { - throw new InflateException(parser.getPositionDescription() - + ": No start tag found!"); - } - - // Temp is the root that was found in the xml - Object xmlRoot = onCreateItem(parser.getName(), attrs); - - // Inflate all children under temp - rParse(parser, xmlRoot, attrs); - return xmlRoot; - } catch (XmlPullParserException | IOException e) { - Log.w(TAG, "Problem parser resource " + resource, e); - return null; - } - } - - /** - * Recursive method used to descend down the xml hierarchy and instantiate - * items, instantiate their children. - */ - private void rParse(XmlPullParser parser, Object parent, final AttributeSet attrs) - throws XmlPullParserException, IOException { - final int depth = parser.getDepth(); - - int type; - while (((type = parser.next()) != XmlPullParser.END_TAG || - parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { - if (type != XmlPullParser.START_TAG) { - continue; - } - - final String name = parser.getName(); - - Object item = onCreateItem(name, attrs); - onAddChildItem(parent, item); - rParse(parser, item, attrs); - } - } - - protected void onAddChildItem(Object parent, Object child) { - if (parent instanceof List<?> && child instanceof SuggestionCategory) { - ((List<SuggestionCategory>) parent).add((SuggestionCategory) child); - } else { - throw new IllegalArgumentException("Parent was not a list"); - } - } - - protected Object onCreateItem(String name, AttributeSet attrs) { - if (name.equals(TAG_LIST)) { - return new ArrayList<SuggestionCategory>(); - } else if (name.equals(TAG_ITEM)) { - SuggestionCategory category = new SuggestionCategory(); - category.category = attrs.getAttributeValue(null, ATTR_CATEGORY); - category.pkg = attrs.getAttributeValue(null, ATTR_PACKAGE); - String multiple = attrs.getAttributeValue(null, ATTR_MULTIPLE); - category.multiple = !TextUtils.isEmpty(multiple) && Boolean.parseBoolean(multiple); - String exclusive = attrs.getAttributeValue(null, ATTR_EXCLUSIVE); - category.exclusive = - !TextUtils.isEmpty(exclusive) && Boolean.parseBoolean(exclusive); - String expireDaysAttr = attrs.getAttributeValue(null, - ATTR_EXCLUSIVE_EXPIRE_DAYS); - long expireDays = !TextUtils.isEmpty(expireDaysAttr) - ? Integer.parseInt(expireDaysAttr) - : -1; - category.exclusiveExpireDaysInMillis = DateUtils.DAY_IN_MILLIS * expireDays; - return category; - } else { - throw new IllegalArgumentException("Unknown item " + name); - } - } - } -} - diff --git a/packages/SettingsLib/src/com/android/settingslib/users/AppRestrictionsHelper.java b/packages/SettingsLib/src/com/android/settingslib/users/AppRestrictionsHelper.java index 7cdbe719df78..9451b3620418 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/AppRestrictionsHelper.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/AppRestrictionsHelper.java @@ -31,12 +31,13 @@ import android.graphics.drawable.Drawable; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; -import androidx.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Log; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; +import androidx.annotation.VisibleForTesting; + import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/AsyncLoader.java b/packages/SettingsLib/src/com/android/settingslib/utils/AsyncLoader.java index 06770ac09558..64b9ffe7a735 100644 --- a/packages/SettingsLib/src/com/android/settingslib/utils/AsyncLoader.java +++ b/packages/SettingsLib/src/com/android/settingslib/utils/AsyncLoader.java @@ -29,7 +29,9 @@ import android.content.Context; * This loader is based on the MailAsyncTaskLoader from the AOSP EmailUnified repo. * * @param <T> the data type to be loaded. + * @deprecated Framework loader is deprecated, use the compat version instead. */ +@Deprecated public abstract class AsyncLoader<T> extends AsyncTaskLoader<T> { private T mResult; 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/utils/IconCache.java b/packages/SettingsLib/src/com/android/settingslib/utils/IconCache.java index f0548ff7a923..c8c8ac22089a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/utils/IconCache.java +++ b/packages/SettingsLib/src/com/android/settingslib/utils/IconCache.java @@ -19,6 +19,7 @@ package com.android.settingslib.utils; import android.content.Context; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; + import androidx.annotation.VisibleForTesting; import androidx.collection.ArrayMap; diff --git a/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java b/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java index e16da84ce713..43c97df24a58 100644 --- a/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java +++ b/packages/SettingsLib/src/com/android/settingslib/utils/PowerUtil.java @@ -22,9 +22,10 @@ import android.icu.text.MeasureFormat; import android.icu.text.MeasureFormat.FormatWidth; import android.icu.util.Measure; import android.icu.util.MeasureUnit; -import androidx.annotation.Nullable; import android.text.TextUtils; +import androidx.annotation.Nullable; + import com.android.settingslib.R; import java.time.Instant; @@ -80,6 +81,30 @@ public class PowerUtil { return null; } + /** + * Method to produce a shortened string describing the remaining battery. Suitable for Quick + * Settings and other areas where space is constrained. + * + * @param context context to fetch descriptions from + * @param drainTimeMs The estimated time remaining before the phone dies in milliseconds. + * + * @return a properly formatted and localized short string describing how much time remains + * before the battery runs out. + */ + @Nullable + public static String getBatteryRemainingShortStringFormatted( + Context context, long drainTimeMs) { + if (drainTimeMs <= 0) { + return null; + } + + if (drainTimeMs <= ONE_DAY_MILLIS) { + return getRegularTimeRemainingShortString(context, drainTimeMs); + } else { + return getMoreThanOneDayShortString(context, drainTimeMs); + } + } + private static String getShutdownImminentString(Context context, String percentageString) { return TextUtils.isEmpty(percentageString) ? context.getString(R.string.power_remaining_duration_only_shutdown_imminent) @@ -119,6 +144,14 @@ public class PowerUtil { } } + private static String getMoreThanOneDayShortString(Context context, long drainTimeMs) { + final long roundedTimeMs = roundTimeToNearestThreshold(drainTimeMs, ONE_HOUR_MILLIS); + CharSequence timeString = StringUtil.formatElapsedTime(context, roundedTimeMs, + false /* withSeconds */); + + return context.getString(R.string.power_remaining_duration_only_short, timeString); + } + private static String getMoreThanTwoDaysString(Context context, String percentageString) { final Locale currentLocale = context.getResources().getConfiguration().getLocales().get(0); final MeasureFormat frmt = MeasureFormat.getInstance(currentLocale, FormatWidth.SHORT); @@ -161,6 +194,22 @@ public class PowerUtil { } } + private static String getRegularTimeRemainingShortString(Context context, long drainTimeMs) { + // Get the time of day we think device will die rounded to the nearest 15 min. + final long roundedTimeOfDayMs = + roundTimeToNearestThreshold( + System.currentTimeMillis() + drainTimeMs, + FIFTEEN_MINUTES_MILLIS); + + // convert the time to a properly formatted string. + String skeleton = android.text.format.DateFormat.getTimeFormatString(context); + DateFormat fmt = DateFormat.getInstanceForSkeleton(skeleton); + Date date = Date.from(Instant.ofEpochMilli(roundedTimeOfDayMs)); + CharSequence timeString = fmt.format(date); + + return context.getString(R.string.power_discharge_by_only_short, timeString); + } + public static long convertUsToMs(long timeUs) { return timeUs / 1000; } diff --git a/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreference.java b/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreference.java index 87a56c7548d2..f02044dc52e0 100644 --- a/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreference.java @@ -17,13 +17,14 @@ package com.android.settingslib.widget; import android.content.Context; -import androidx.core.content.res.TypedArrayUtils; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.text.method.LinkMovementMethod; import android.util.AttributeSet; import android.widget.TextView; +import androidx.core.content.res.TypedArrayUtils; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + import com.android.settingslib.R; /** diff --git a/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreferenceMixin.java b/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreferenceMixin.java index 5883754fa2ed..fcf236337703 100644 --- a/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreferenceMixin.java +++ b/packages/SettingsLib/src/com/android/settingslib/widget/FooterPreferenceMixin.java @@ -17,6 +17,7 @@ package com.android.settingslib.widget; import android.content.Context; + import androidx.preference.PreferenceFragment; import androidx.preference.PreferenceScreen; @@ -24,6 +25,12 @@ import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; import com.android.settingslib.core.lifecycle.events.SetPreferenceScreen; +/** + * Framework mixin is deprecated, use the compat version instead. + * + * @deprecated + */ +@Deprecated public class FooterPreferenceMixin implements LifecycleObserver, SetPreferenceScreen { private final PreferenceFragment mFragment; 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..d45e56d486ae --- /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 androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; + +import com.android.settingslib.core.lifecycle.Lifecycle; +import com.android.settingslib.core.lifecycle.LifecycleObserver; +import com.android.settingslib.core.lifecycle.events.SetPreferenceScreen; + +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/src/com/android/settingslib/wifi/AccessPoint.java b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java index b9c76013b0d7..22d5d83b702a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java @@ -48,14 +48,12 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; import android.os.UserHandle; -import androidx.annotation.NonNull; -import android.text.Spannable; -import android.text.SpannableString; import android.text.TextUtils; -import android.text.style.TtsSpan; import android.util.ArraySet; import android.util.Log; +import androidx.annotation.NonNull; + import com.android.internal.annotations.VisibleForTesting; import com.android.settingslib.R; import com.android.settingslib.utils.ThreadUtils; @@ -163,6 +161,9 @@ public class AccessPoint implements Comparable<AccessPoint> { public static final int SECURITY_WEP = 1; public static final int SECURITY_PSK = 2; public static final int SECURITY_EAP = 3; + public static final int SECURITY_OWE = 4; + public static final int SECURITY_SAE = 5; + public static final int SECURITY_EAP_SUITE_B = 6; private static final int PSK_UNKNOWN = 0; private static final int PSK_WPA = 1; @@ -435,7 +436,7 @@ public class AccessPoint implements Comparable<AccessPoint> { if (isConnectable()) { builder.append(',').append("connectable"); } - if (security != SECURITY_NONE) { + if ((security != SECURITY_NONE) && (security != SECURITY_OWE)) { builder.append(',').append(securityToString(security, pskType)); } builder.append(",level=").append(getLevel()); @@ -722,6 +723,9 @@ public class AccessPoint implements Comparable<AccessPoint> { case SECURITY_EAP: return concise ? context.getString(R.string.wifi_security_short_eap) : context.getString(R.string.wifi_security_eap); + case SECURITY_EAP_SUITE_B: + return concise ? context.getString(R.string.wifi_security_short_eap_suiteb) : + context.getString(R.string.wifi_security_eap_suiteb); case SECURITY_PSK: switch (pskType) { case PSK_WPA: @@ -741,6 +745,12 @@ public class AccessPoint implements Comparable<AccessPoint> { case SECURITY_WEP: return concise ? context.getString(R.string.wifi_security_short_wep) : context.getString(R.string.wifi_security_wep); + case SECURITY_SAE: + return concise ? context.getString(R.string.wifi_security_short_sae) : + context.getString(R.string.wifi_security_sae); + case SECURITY_OWE: + return concise ? context.getString(R.string.wifi_security_short_owe) : + context.getString(R.string.wifi_security_owe); case SECURITY_NONE: default: return concise ? "" : context.getString(R.string.wifi_security_none); @@ -756,10 +766,7 @@ public class AccessPoint implements Comparable<AccessPoint> { } public CharSequence getSsid() { - final SpannableString str = new SpannableString(ssid); - str.setSpan(new TtsSpan.TelephoneBuilder(ssid).build(), 0, ssid.length(), - Spannable.SPAN_INCLUSIVE_INCLUSIVE); - return str; + return ssid; } public String getConfigName() { @@ -985,13 +992,20 @@ public class AccessPoint implements Comparable<AccessPoint> { * Can only be called for unsecured networks. */ public void generateOpenNetworkConfig() { - if (security != SECURITY_NONE) + if ((security != SECURITY_NONE) && (security != SECURITY_OWE)) { throw new IllegalStateException(); + } if (mConfig != null) return; mConfig = new WifiConfiguration(); mConfig.SSID = AccessPoint.convertToQuotedString(ssid); - mConfig.allowedKeyManagement.set(KeyMgmt.NONE); + + if (security == SECURITY_NONE) { + mConfig.allowedKeyManagement.set(KeyMgmt.NONE); + } else { + mConfig.allowedKeyManagement.set(KeyMgmt.OWE); + mConfig.requirePMF = true; + } } public void saveWifiState(Bundle savedState) { @@ -1293,22 +1307,38 @@ public class AccessPoint implements Comparable<AccessPoint> { private static int getSecurity(ScanResult result) { if (result.capabilities.contains("WEP")) { return SECURITY_WEP; + } else if (result.capabilities.contains("SAE")) { + return SECURITY_SAE; } else if (result.capabilities.contains("PSK")) { return SECURITY_PSK; + } else if (result.capabilities.contains("EAP_SUITE_B_192")) { + return SECURITY_EAP_SUITE_B; } else if (result.capabilities.contains("EAP")) { return SECURITY_EAP; + } else if (result.capabilities.contains("OWE")) { + return SECURITY_OWE; } + return SECURITY_NONE; } static int getSecurity(WifiConfiguration config) { + if (config.allowedKeyManagement.get(KeyMgmt.SAE)) { + return SECURITY_SAE; + } if (config.allowedKeyManagement.get(KeyMgmt.WPA_PSK)) { return SECURITY_PSK; } + if (config.allowedKeyManagement.get(KeyMgmt.SUITE_B_192)) { + return SECURITY_EAP_SUITE_B; + } if (config.allowedKeyManagement.get(KeyMgmt.WPA_EAP) || config.allowedKeyManagement.get(KeyMgmt.IEEE8021X)) { return SECURITY_EAP; } + if (config.allowedKeyManagement.get(KeyMgmt.OWE)) { + return SECURITY_OWE; + } return (config.wepKeys[0] != null) ? SECURITY_WEP : SECURITY_NONE; } @@ -1326,6 +1356,12 @@ public class AccessPoint implements Comparable<AccessPoint> { return "PSK"; } else if (security == SECURITY_EAP) { return "EAP"; + } else if (security == SECURITY_SAE) { + return "SAE"; + } else if (security == SECURITY_EAP_SUITE_B) { + return "SUITE_B"; + } else if (security == SECURITY_OWE) { + return "OWE"; } return "NONE"; } diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPointPreference.java b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPointPreference.java index b07dd8f18354..db364a3b75e5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPointPreference.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPointPreference.java @@ -25,9 +25,6 @@ import android.graphics.drawable.StateListDrawable; import android.net.wifi.WifiConfiguration; import android.os.Looper; import android.os.UserHandle; -import androidx.annotation.VisibleForTesting; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; import android.text.TextUtils; import android.util.AttributeSet; import android.util.SparseArray; @@ -35,9 +32,12 @@ import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.VisibleForTesting; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + import com.android.settingslib.R; import com.android.settingslib.TronUtils; -import com.android.settingslib.TwoTargetPreference; import com.android.settingslib.Utils; import com.android.settingslib.wifi.AccessPoint.Speed; @@ -183,7 +183,7 @@ public class AccessPointPreference extends Preference { Drawable drawable = mIconInjector.getIcon(level); if (!mForSavedNetworks && drawable != null) { - drawable.setTint(Utils.getColorAttr(context, android.R.attr.colorControlNormal)); + drawable.setTintList(Utils.getColorAttr(context, android.R.attr.colorControlNormal)); setIcon(drawable); } else { safeSetDefaultIcon(); @@ -200,7 +200,8 @@ public class AccessPointPreference extends Preference { if (frictionImageView == null || mFrictionSld == null) { return; } - if (mAccessPoint.getSecurity() != AccessPoint.SECURITY_NONE) { + if ((mAccessPoint.getSecurity() != AccessPoint.SECURITY_NONE) + && (mAccessPoint.getSecurity() != AccessPoint.SECURITY_OWE)) { mFrictionSld.setState(STATE_SECURED); } else if (mAccessPoint.isMetered()) { mFrictionSld.setState(STATE_METERED); diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java b/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java index 5862e6f19e16..afea5d2078b0 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java @@ -24,6 +24,7 @@ import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; import android.os.Bundle; import android.os.Parcelable; + import androidx.annotation.Keep; import com.android.settingslib.wifi.AccessPoint.Speed; diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiSavedConfigUtils.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiSavedConfigUtils.java index 159b2a1047d9..19e38081fcad 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiSavedConfigUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiSavedConfigUtils.java @@ -55,8 +55,10 @@ public class WifiSavedConfigUtils { try { List<PasspointConfiguration> savedPasspointConfigs = wifiManager.getPasspointConfigurations(); - for (PasspointConfiguration config : savedPasspointConfigs) { - savedConfigs.add(new AccessPoint(context, config)); + if (savedPasspointConfigs != null) { + for (PasspointConfiguration config : savedPasspointConfigs) { + savedConfigs.add(new AccessPoint(context, config)); + } } } catch (UnsupportedOperationException e) { // Passpoint not supported. diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java index fe0b35b4191c..089f773ec1e9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiStatusTracker.java @@ -163,6 +163,12 @@ public class WifiStatusTracker extends ConnectivityManager.NetworkCallback { ? null : AccessPoint.getSpeedLabel(mContext, scoredNetwork, rssi); } + /** Refresh the status label on Locale changed. */ + public void refreshLocale() { + updateStatusLabel(); + mCallback.run(); + } + private String getValidSsid(WifiInfo info) { String ssid = info.getSSID(); if (ssid != null && !WifiSsid.NONE.equals(ssid)) { diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java index 6211d1214fcc..0dbc037f2298 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTracker.java @@ -41,15 +41,16 @@ import android.os.Message; import android.os.Process; import android.os.SystemClock; import android.provider.Settings; -import androidx.annotation.GuardedBy; -import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.widget.Toast; +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + import com.android.settingslib.R; import com.android.settingslib.core.lifecycle.Lifecycle; import com.android.settingslib.core.lifecycle.LifecycleObserver; @@ -80,7 +81,7 @@ public class WifiTracker implements LifecycleObserver, OnStart, OnStop, OnDestro private static final long DEFAULT_MAX_CACHED_SCORE_AGE_MILLIS = 20 * DateUtils.MINUTE_IN_MILLIS; /** Maximum age of scan results to hold onto while actively scanning. **/ - private static final long MAX_SCAN_RESULT_AGE_MILLIS = 25000; + private static final long MAX_SCAN_RESULT_AGE_MILLIS = 15000; private static final String TAG = "WifiTracker"; private static final boolean DBG() { @@ -133,8 +134,8 @@ public class WifiTracker implements LifecycleObserver, OnStart, OnStop, OnDestro /** * Tracks whether fresh scan results have been received since scanning start. * - * <p>If this variable is false, we will not evict the scan result cache or invoke callbacks - * so that we do not update the UI with stale data / clear out existing UI elements prematurely. + * <p>If this variable is false, we will not invoke callbacks so that we do not + * update the UI with stale data / clear out existing UI elements prematurely. */ private boolean mStaleScanResults = true; @@ -326,7 +327,8 @@ public class WifiTracker implements LifecycleObserver, OnStart, OnStop, OnDestro * <p>Intended to only be invoked within {@link #onStart()}. */ @MainThread - private void forceUpdate() { + @VisibleForTesting + void forceUpdate() { mLastInfo = mWifiManager.getConnectionInfo(); mLastNetworkInfo = mConnectivityManager.getNetworkInfo(mWifiManager.getCurrentNetwork()); @@ -442,10 +444,8 @@ public class WifiTracker implements LifecycleObserver, OnStart, OnStop, OnDestro mScanResultCache.put(newResult.BSSID, newResult); } - // Don't evict old results if no new scan results - if (!mStaleScanResults) { - evictOldScans(); - } + // Evict old results in all conditions + evictOldScans(); ArrayMap<String, List<ScanResult>> scanResultsByApKey = new ArrayMap<>(); for (ScanResult result : mScanResultCache.values()) { diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTrackerFactory.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTrackerFactory.java index e73d952cd645..93e4fce6e74b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTrackerFactory.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiTrackerFactory.java @@ -16,6 +16,7 @@ package com.android.settingslib.wifi; import android.content.Context; + import androidx.annotation.Keep; import androidx.annotation.NonNull; diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java index 4792317c9f1e..4e6c005457c0 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/WifiUtils.java @@ -21,6 +21,7 @@ import android.net.wifi.ScanResult; import android.net.wifi.WifiConfiguration; import android.net.wifi.WifiInfo; import android.os.SystemClock; + import androidx.annotation.VisibleForTesting; import com.android.settingslib.R; diff --git a/packages/SettingsLib/src/com/android/settingslib/wrapper/LocationManagerWrapper.java b/packages/SettingsLib/src/com/android/settingslib/wrapper/LocationManagerWrapper.java deleted file mode 100644 index 1a268a608e5d..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/wrapper/LocationManagerWrapper.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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.wrapper; - -import android.location.LocationManager; -import android.os.UserHandle; - -/** - * This class replicates some methods of android.location.LocationManager that are new and not - * yet available in our current version of Robolectric. It provides a thin wrapper to call the real - * methods in production and a mock in tests. - */ -public class LocationManagerWrapper { - - private LocationManager mLocationManager; - - public LocationManagerWrapper(LocationManager locationManager) { - mLocationManager = locationManager; - } - - /** Returns the real {@code LocationManager} object */ - public LocationManager getLocationManager() { - return mLocationManager; - } - - /** Wraps {@code LocationManager.isProviderEnabled} method */ - public boolean isProviderEnabled(String provider) { - return mLocationManager.isProviderEnabled(provider); - } - - /** Wraps {@code LocationManager.setProviderEnabledForUser} method */ - public void setProviderEnabledForUser(String provider, boolean enabled, UserHandle userHandle) { - mLocationManager.setProviderEnabledForUser(provider, enabled, userHandle); - } - - /** Wraps {@code LocationManager.isLocationEnabled} method */ - public boolean isLocationEnabled() { - return mLocationManager.isLocationEnabled(); - } - - /** Wraps {@code LocationManager.isLocationEnabledForUser} method */ - public boolean isLocationEnabledForUser(UserHandle userHandle) { - return mLocationManager.isLocationEnabledForUser(userHandle); - } - - /** Wraps {@code LocationManager.setLocationEnabledForUser} method */ - public void setLocationEnabledForUser(boolean enabled, UserHandle userHandle) { - mLocationManager.setLocationEnabledForUser(enabled, userHandle); - } -} diff --git a/packages/SettingsLib/src/com/android/settingslib/wrapper/PackageManagerWrapper.java b/packages/SettingsLib/src/com/android/settingslib/wrapper/PackageManagerWrapper.java deleted file mode 100644 index 235daf23a664..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/wrapper/PackageManagerWrapper.java +++ /dev/null @@ -1,253 +0,0 @@ -/* - * 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.wrapper; - -import android.content.ComponentName; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.pm.ApplicationInfo; -import android.content.pm.IPackageDeleteObserver; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.ResolveInfo; -import android.graphics.drawable.Drawable; -import android.os.UserHandle; -import android.os.storage.VolumeInfo; - -import java.util.List; - -/** - * A thin wrapper class that simplifies testing by putting a mockable layer between the application - * and the PackageManager. This class only provides access to the minimum number of functions from - * the PackageManager needed for DeletionHelper to work. - */ -public class PackageManagerWrapper { - - private final PackageManager mPm; - - public PackageManagerWrapper(PackageManager pm) { - mPm = pm; - } - - /** - * Returns the real {@code PackageManager} object. - */ - public PackageManager getPackageManager() { - return mPm; - } - - /** - * Calls {@code PackageManager.getInstalledApplicationsAsUser()}. - * - * @see android.content.pm.PackageManager#getInstalledApplicationsAsUser - */ - public List<ApplicationInfo> getInstalledApplicationsAsUser(int flags, int userId) { - return mPm.getInstalledApplicationsAsUser(flags, userId); - } - - /** - * Calls {@code PackageManager.getInstalledPackagesAsUser} - */ - public List<PackageInfo> getInstalledPackagesAsUser(int flags, int userId) { - return mPm.getInstalledPackagesAsUser(flags, userId); - } - - /** - * Calls {@code PackageManager.hasSystemFeature()}. - * - * @see android.content.pm.PackageManager#hasSystemFeature - */ - public boolean hasSystemFeature(String name) { - return mPm.hasSystemFeature(name); - } - - /** - * Calls {@code PackageManager.queryIntentActivitiesAsUser()}. - * - * @see android.content.pm.PackageManager#queryIntentActivitiesAsUser - */ - public List<ResolveInfo> queryIntentActivitiesAsUser(Intent intent, int flags, int userId) { - return mPm.queryIntentActivitiesAsUser(intent, flags, userId); - } - - /** - * Calls {@code PackageManager.getInstallReason()}. - * - * @see android.content.pm.PackageManager#getInstallReason - */ - public int getInstallReason(String packageName, UserHandle user) { - return mPm.getInstallReason(packageName, user); - } - - /** - * Calls {@code PackageManager.getApplicationInfoAsUser} - */ - public ApplicationInfo getApplicationInfoAsUser(String packageName, int i, int userId) - throws PackageManager.NameNotFoundException { - return mPm.getApplicationInfoAsUser(packageName, i, userId); - } - - /** - * Calls {@code PackageManager.setDefaultBrowserPackageNameAsUser} - */ - public boolean setDefaultBrowserPackageNameAsUser(String packageName, int userId) { - return mPm.setDefaultBrowserPackageNameAsUser(packageName, userId); - } - - /** - * Calls {@code PackageManager.getDefaultBrowserPackageNameAsUser} - */ - public String getDefaultBrowserPackageNameAsUser(int userId) { - return mPm.getDefaultBrowserPackageNameAsUser(userId); - } - - /** - * Calls {@code PackageManager.getHomeActivities} - */ - public ComponentName getHomeActivities(List<ResolveInfo> homeActivities) { - return mPm.getHomeActivities(homeActivities); - } - - /** - * Calls {@code PackageManager.queryIntentServicesAsUser} - */ - public List<ResolveInfo> queryIntentServicesAsUser(Intent intent, int i, int user) { - return mPm.queryIntentServicesAsUser(intent, i, user); - } - - /** - * Calls {@code PackageManager.queryIntentServices} - */ - public List<ResolveInfo> queryIntentServices(Intent intent, int i) { - return mPm.queryIntentServices(intent, i); - } - - /** - * Calls {@code PackageManager.replacePreferredActivity} - */ - public void replacePreferredActivity(IntentFilter homeFilter, int matchCategoryEmpty, - ComponentName[] componentNames, ComponentName component) { - mPm.replacePreferredActivity(homeFilter, matchCategoryEmpty, componentNames, component); - } - - /** - * Gets information about a particular package from the package manager. - * - * @param packageName The name of the package we would like information about. - * @param i additional options flags. see javadoc for - * {@link PackageManager#getPackageInfo(String, int)} - * @return The PackageInfo for the requested package - */ - public PackageInfo getPackageInfo(String packageName, int i) throws NameNotFoundException { - return mPm.getPackageInfo(packageName, i); - } - - /** - * Retrieves the icon associated with this particular set of ApplicationInfo - * - * @param info The ApplicationInfo to retrieve the icon for - * @return The icon as a drawable. - */ - public Drawable getUserBadgedIcon(ApplicationInfo info) { - return mPm.getUserBadgedIcon(mPm.loadUnbadgedItemIcon(info, info), - new UserHandle(UserHandle.getUserId(info.uid))); - } - - /** - * Retrieves the label associated with the particular set of ApplicationInfo - * - * @param app The ApplicationInfo to retrieve the label for - * @return the label as a CharSequence - */ - public CharSequence loadLabel(ApplicationInfo app) { - return app.loadLabel(mPm); - } - - /** - * Retrieve all activities that can be performed for the given intent. - */ - public List<ResolveInfo> queryIntentActivities(Intent intent, int flags) { - return mPm.queryIntentActivities(intent, flags); - } - - /** - * Calls {@code PackageManager.getPrimaryStorageCurrentVolume} - */ - public VolumeInfo getPrimaryStorageCurrentVolume() { - return mPm.getPrimaryStorageCurrentVolume(); - } - - /** - * Calls {@code PackageManager.deletePackageAsUser} - */ - public void deletePackageAsUser(String packageName, IPackageDeleteObserver observer, int flags, - int userId) { - mPm.deletePackageAsUser(packageName, observer, flags, userId); - } - - /** - * Calls {@code PackageManager.getPackageUidAsUser} - */ - public int getPackageUidAsUser(String pkg, int userId) - throws PackageManager.NameNotFoundException { - return mPm.getPackageUidAsUser(pkg, userId); - } - - /** - * Calls {@code PackageManager.setApplicationEnabledSetting} - */ - public void setApplicationEnabledSetting(String packageName, int newState, int flags) { - mPm.setApplicationEnabledSetting(packageName, newState, flags); - } - - /** - * Calls {@code PackageManager.getApplicationEnabledSetting} - */ - public int getApplicationEnabledSetting(String packageName) { - return mPm.getApplicationEnabledSetting(packageName); - } - - /** - * Calls {@code PackageManager.setComponentEnabledSetting} - */ - public void setComponentEnabledSetting(ComponentName componentName, int newState, int flags) { - mPm.setComponentEnabledSetting(componentName, newState, flags); - } - - /** - * Calls {@code PackageManager.getApplicationInfo} - */ - public ApplicationInfo getApplicationInfo(String packageName, int flags) - throws NameNotFoundException { - return mPm.getApplicationInfo(packageName, flags); - } - - /** - * Calls {@code PackageManager.getApplicationLabel} - */ - public CharSequence getApplicationLabel(ApplicationInfo info) { - return mPm.getApplicationLabel(info); - } - - /** - * Calls {@code PackageManager.queryBroadcastReceivers} - */ - public List<ResolveInfo> queryBroadcastReceivers(Intent intent, int flags) { - return mPm.queryBroadcastReceivers(intent, flags); - } -} - |