apps) {
- }
-
- @Override
- public void onPackageIconChanged() {
- }
-
- @Override
- public void onPackageSizeChanged(String packageName) {
- }
-
- @Override
- public void onAllSizesComputed() {
- }
-
- @Override
- public void onLauncherInfoChanged() {
- // when the value of the AppEntry.hasLauncherEntry was changed.
- updateSummary();
- }
-
- @Override
- public void onLoadEntriesCompleted() {
- }
-}
diff --git a/src/com/android/settings/applications/appcompat/RadioWithImagePreference.java b/src/com/android/settings/applications/appcompat/RadioWithImagePreference.java
new file mode 100644
index 0000000000000000000000000000000000000000..77cd86c23dc023ed76b20a08474ec74e125dfac4
--- /dev/null
+++ b/src/com/android/settings/applications/appcompat/RadioWithImagePreference.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2023 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.settings.applications.appcompat;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.preference.CheckBoxPreference;
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.settings.R;
+
+/**
+ * Radio button preference with image at the bottom.
+ *
+ * Layout should stay the same as
+ * {@link com.android.settingslib.widget.SelectorWithWidgetPreference} for consistency.
+ */
+public class RadioWithImagePreference extends CheckBoxPreference {
+
+ /**
+ * Interface definition for a callback to be invoked when the preference is clicked.
+ */
+ public interface OnClickListener {
+ /**
+ * Called when a preference has been clicked.
+ *
+ * @param emiter The clicked preference
+ */
+ void onRadioButtonClicked(RadioWithImagePreference emiter);
+ }
+
+ private OnClickListener mListener = null;
+
+ /**
+ * Performs inflation from XML and apply a class-specific base style.
+ *
+ * @param context The {@link Context} this is associated with, through which it can
+ * access the current theme, resources, {@link SharedPreferences}, etc.
+ * @param attrs The attributes of the XML tag that is inflating the preference
+ * @param defStyle An attribute in the current theme that contains a reference to a style
+ * resource that supplies default values for the view. Can be 0 to not
+ * look for defaults.
+ */
+ public RadioWithImagePreference(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ /**
+ * Performs inflation from XML and apply a class-specific base style.
+ *
+ * @param context The {@link Context} this is associated with, through which it can
+ * access the current theme, resources, {@link SharedPreferences}, etc.
+ * @param attrs The attributes of the XML tag that is inflating the preference
+ */
+ public RadioWithImagePreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ /**
+ * Constructor to create a preference.
+ *
+ * @param context The Context this is associated with.
+ */
+ public RadioWithImagePreference(Context context) {
+ this(context, null);
+ }
+
+ /**
+ * Sets the callback to be invoked when this preference is clicked by the user.
+ *
+ * @param listener The callback to be invoked
+ */
+ public void setOnClickListener(OnClickListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Processes a click on the preference.
+ */
+ @Override
+ public void onClick() {
+ if (mListener != null) {
+ mListener.onRadioButtonClicked(this);
+ }
+ }
+
+ /**
+ * Binds the created View to the data for this preference.
+ *
+ *
This is a good place to grab references to custom Views in the layout and set
+ * properties on them.
+ *
+ *
Make sure to call through to the superclass's implementation.
+ *
+ * @param holder The ViewHolder that provides references to the views to fill in. These views
+ * will be recycled, so you should not hold a reference to them after this method
+ * returns.
+ */
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+
+ View summaryContainer = holder.findViewById(R.id.summary_container);
+ if (summaryContainer != null) {
+ summaryContainer.setVisibility(
+ TextUtils.isEmpty(getSummary()) ? View.GONE : View.VISIBLE);
+ }
+ }
+
+ private void init() {
+ setWidgetLayoutResource(com.android.settingslib.R.layout.preference_widget_radiobutton);
+ setLayoutResource(R.layout.radio_with_image_preference);
+ setIconSpaceReserved(false);
+ }
+}
diff --git a/src/com/android/settings/applications/appcompat/UserAspectRatioAppsPreferenceController.java b/src/com/android/settings/applications/appcompat/UserAspectRatioAppsPreferenceController.java
new file mode 100644
index 0000000000000000000000000000000000000000..4211424351b44cf608752d272dccf23998541be0
--- /dev/null
+++ b/src/com/android/settings/applications/appcompat/UserAspectRatioAppsPreferenceController.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2023 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.settings.applications.appcompat;
+
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+
+/**
+ * Preference controller for
+ * {@link com.android.settings.spa.app.appcompat.UserAspectRatioAppsPageProvider}
+ */
+public class UserAspectRatioAppsPreferenceController extends BasePreferenceController {
+
+ public UserAspectRatioAppsPreferenceController(@NonNull Context context,
+ @NonNull String preferenceKey) {
+ super(context, preferenceKey);
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return UserAspectRatioManager.isFeatureEnabled(mContext)
+ ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ return mContext.getResources().getString(R.string.aspect_ratio_summary_text, Build.MODEL);
+ }
+}
diff --git a/src/com/android/settings/applications/appcompat/UserAspectRatioDetails.java b/src/com/android/settings/applications/appcompat/UserAspectRatioDetails.java
new file mode 100644
index 0000000000000000000000000000000000000000..dfb583ce2fc51f564e9efd1d25674105be5949b8
--- /dev/null
+++ b/src/com/android/settings/applications/appcompat/UserAspectRatioDetails.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2023 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.settings.applications.appcompat;
+
+import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_16_9;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_4_3;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_DISPLAY_SIZE;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
+
+import android.app.ActivityManager;
+import android.app.IActivityManager;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.AlertDialog;
+import androidx.preference.Preference;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.R;
+import com.android.settings.Utils;
+import com.android.settings.applications.AppInfoBase;
+import com.android.settings.widget.EntityHeaderController;
+import com.android.settingslib.applications.AppUtils;
+import com.android.settingslib.widget.ActionButtonsPreference;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * App specific activity to show aspect ratio overrides
+ */
+public class UserAspectRatioDetails extends AppInfoBase implements
+ RadioWithImagePreference.OnClickListener {
+ private static final String TAG = UserAspectRatioDetails.class.getSimpleName();
+
+ private static final String KEY_HEADER_SUMMARY = "app_aspect_ratio_summary";
+ private static final String KEY_HEADER_BUTTONS = "header_view";
+ private static final String KEY_PREF_FULLSCREEN = "fullscreen_pref";
+ private static final String KEY_PREF_HALF_SCREEN = "half_screen_pref";
+ private static final String KEY_PREF_DISPLAY_SIZE = "display_size_pref";
+ private static final String KEY_PREF_16_9 = "16_9_pref";
+ private static final String KEY_PREF_4_3 = "4_3_pref";
+ @VisibleForTesting
+ static final String KEY_PREF_DEFAULT = "app_default_pref";
+ @VisibleForTesting
+ static final String KEY_PREF_3_2 = "3_2_pref";
+
+ private final List mAspectRatioPreferences = new ArrayList<>();
+
+ @NonNull private UserAspectRatioManager mUserAspectRatioManager;
+ @NonNull private String mSelectedKey = KEY_PREF_DEFAULT;
+
+ @Override
+ public void onCreate(@NonNull Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mUserAspectRatioManager = new UserAspectRatioManager(getContext());
+ initPreferences();
+ try {
+ final int userAspectRatio = mUserAspectRatioManager
+ .getUserMinAspectRatioValue(mPackageName, mUserId);
+ mSelectedKey = getSelectedKey(userAspectRatio);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to get user min aspect ratio");
+ }
+ refreshUi();
+ }
+
+ @Override
+ public void onRadioButtonClicked(@NonNull RadioWithImagePreference selected) {
+ final String selectedKey = selected.getKey();
+ if (mSelectedKey.equals(selectedKey)) {
+ return;
+ }
+ final int userAspectRatio = getSelectedUserMinAspectRatio(selectedKey);
+ try {
+ getAspectRatioManager().setUserMinAspectRatio(mPackageName, mUserId, userAspectRatio);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to set user min aspect ratio");
+ return;
+ }
+ // Only update to selected aspect ratio if nothing goes wrong
+ mSelectedKey = selectedKey;
+ updateAllPreferences(mSelectedKey);
+ Log.d(TAG, "Killing application process " + mPackageName);
+ try {
+ final IActivityManager am = ActivityManager.getService();
+ am.stopAppForUser(mPackageName, mUserId);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to stop application " + mPackageName);
+ }
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ // TODO(b/292566895): add metrics for logging
+ return 0;
+ }
+
+ @Override
+ protected boolean refreshUi() {
+ if (mPackageInfo == null || mPackageInfo.applicationInfo == null) {
+ return false;
+ }
+ updateAllPreferences(mSelectedKey);
+ return true;
+ }
+
+ @Override
+ protected AlertDialog createDialog(int id, int errorCode) {
+ return null;
+ }
+
+ private void launchApplication() {
+ Intent launchIntent = mPm.getLaunchIntentForPackage(mPackageName)
+ .addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP);
+ if (launchIntent != null) {
+ getContext().startActivityAsUser(launchIntent, new UserHandle(mUserId));
+ }
+ }
+
+ @PackageManager.UserMinAspectRatio
+ private int getSelectedUserMinAspectRatio(@NonNull String selectedKey) {
+ switch (selectedKey) {
+ case KEY_PREF_FULLSCREEN:
+ return USER_MIN_ASPECT_RATIO_FULLSCREEN;
+ case KEY_PREF_HALF_SCREEN:
+ return USER_MIN_ASPECT_RATIO_SPLIT_SCREEN;
+ case KEY_PREF_DISPLAY_SIZE:
+ return USER_MIN_ASPECT_RATIO_DISPLAY_SIZE;
+ case KEY_PREF_3_2:
+ return USER_MIN_ASPECT_RATIO_3_2;
+ case KEY_PREF_4_3:
+ return USER_MIN_ASPECT_RATIO_4_3;
+ case KEY_PREF_16_9:
+ return USER_MIN_ASPECT_RATIO_16_9;
+ default:
+ return USER_MIN_ASPECT_RATIO_UNSET;
+ }
+ }
+
+ @NonNull
+ private String getSelectedKey(@PackageManager.UserMinAspectRatio int userMinAspectRatio) {
+ switch (userMinAspectRatio) {
+ case USER_MIN_ASPECT_RATIO_FULLSCREEN:
+ return KEY_PREF_FULLSCREEN;
+ case USER_MIN_ASPECT_RATIO_SPLIT_SCREEN:
+ return KEY_PREF_HALF_SCREEN;
+ case USER_MIN_ASPECT_RATIO_DISPLAY_SIZE:
+ return KEY_PREF_DISPLAY_SIZE;
+ case USER_MIN_ASPECT_RATIO_3_2:
+ return KEY_PREF_3_2;
+ case USER_MIN_ASPECT_RATIO_4_3:
+ return KEY_PREF_4_3;
+ case USER_MIN_ASPECT_RATIO_16_9:
+ return KEY_PREF_16_9;
+ default:
+ return KEY_PREF_DEFAULT;
+ }
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ final Preference pref = EntityHeaderController
+ .newInstance(getActivity(), this, null /* header */)
+ .setIcon(Utils.getBadgedIcon(getContext(), mPackageInfo.applicationInfo))
+ .setLabel(mPackageInfo.applicationInfo.loadLabel(mPm))
+ .setIsInstantApp(AppUtils.isInstant(mPackageInfo.applicationInfo))
+ .setPackageName(mPackageName)
+ .setUid(mPackageInfo.applicationInfo.uid)
+ .setHasAppInfoLink(true)
+ .setButtonActions(EntityHeaderController.ActionType.ACTION_NONE,
+ EntityHeaderController.ActionType.ACTION_NONE)
+ .done(getActivity(), getPrefContext());
+
+ getPreferenceScreen().addPreference(pref);
+ }
+
+ private void initPreferences() {
+ addPreferencesFromResource(R.xml.user_aspect_ratio_details);
+
+ final String summary = getContext().getResources().getString(
+ R.string.aspect_ratio_main_summary, Build.MODEL);
+ findPreference(KEY_HEADER_SUMMARY).setTitle(summary);
+
+ ((ActionButtonsPreference) findPreference(KEY_HEADER_BUTTONS))
+ .setButton1Text(R.string.launch_instant_app)
+ .setButton1Icon(R.drawable.ic_settings_open)
+ .setButton1OnClickListener(v -> launchApplication());
+
+ addPreference(KEY_PREF_DEFAULT, USER_MIN_ASPECT_RATIO_UNSET);
+ addPreference(KEY_PREF_FULLSCREEN, USER_MIN_ASPECT_RATIO_FULLSCREEN);
+ addPreference(KEY_PREF_DISPLAY_SIZE, USER_MIN_ASPECT_RATIO_DISPLAY_SIZE);
+ addPreference(KEY_PREF_HALF_SCREEN, USER_MIN_ASPECT_RATIO_SPLIT_SCREEN);
+ addPreference(KEY_PREF_16_9, USER_MIN_ASPECT_RATIO_16_9);
+ addPreference(KEY_PREF_4_3, USER_MIN_ASPECT_RATIO_4_3);
+ addPreference(KEY_PREF_3_2, USER_MIN_ASPECT_RATIO_3_2);
+ }
+
+ private void addPreference(@NonNull String key,
+ @PackageManager.UserMinAspectRatio int aspectRatio) {
+ final RadioWithImagePreference pref = findPreference(key);
+ if (pref == null) {
+ return;
+ }
+ if (!mUserAspectRatioManager.hasAspectRatioOption(aspectRatio, mPackageName)) {
+ pref.setVisible(false);
+ return;
+ }
+ pref.setTitle(mUserAspectRatioManager.getAccessibleEntry(aspectRatio, mPackageName));
+ pref.setOnClickListener(this);
+ mAspectRatioPreferences.add(pref);
+ }
+
+ private void updateAllPreferences(@NonNull String selectedKey) {
+ for (RadioWithImagePreference pref : mAspectRatioPreferences) {
+ pref.setChecked(selectedKey.equals(pref.getKey()));
+ }
+ }
+
+ @VisibleForTesting
+ UserAspectRatioManager getAspectRatioManager() {
+ return mUserAspectRatioManager;
+ }
+}
diff --git a/src/com/android/settings/applications/appcompat/UserAspectRatioManager.java b/src/com/android/settings/applications/appcompat/UserAspectRatioManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..b940dc844a5bb4a599185d0b56b60a04051b262b
--- /dev/null
+++ b/src/com/android/settings/applications/appcompat/UserAspectRatioManager.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2023 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.settings.applications.appcompat;
+
+import static android.os.UserHandle.getUserHandleForUid;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE;
+import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE;
+
+import static java.lang.Boolean.FALSE;
+
+import android.app.AppGlobals;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
+import android.os.RemoteException;
+import android.provider.DeviceConfig;
+import android.util.ArrayMap;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+import com.android.settings.Utils;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.Map;
+
+/**
+ * Helper class for handling app aspect ratio override
+ * {@link PackageManager.UserMinAspectRatio} set by user
+ */
+public class UserAspectRatioManager {
+ private static final Intent LAUNCHER_ENTRY_INTENT =
+ new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER);
+
+ // TODO(b/288142656): Enable user aspect ratio settings by default
+ private static final boolean DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_SETTINGS = true;
+ @VisibleForTesting
+ static final String KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS =
+ "enable_app_compat_aspect_ratio_user_settings";
+ static final String KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN =
+ "enable_app_compat_user_aspect_ratio_fullscreen";
+ private static final boolean DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_FULLSCREEN = true;
+
+ private final Context mContext;
+ private final IPackageManager mIPm;
+ /** Apps that have launcher entry defined in manifest */
+ private final Map mUserAspectRatioMap;
+ private final Map mUserAspectRatioA11yMap;
+
+ public UserAspectRatioManager(@NonNull Context context) {
+ mContext = context;
+ mIPm = AppGlobals.getPackageManager();
+ mUserAspectRatioA11yMap = new ArrayMap<>();
+ mUserAspectRatioMap = getUserMinAspectRatioMapping();
+ }
+
+ /**
+ * Whether user aspect ratio settings is enabled for device.
+ */
+ public static boolean isFeatureEnabled(Context context) {
+ final boolean isBuildTimeFlagEnabled = context.getResources().getBoolean(
+ com.android.internal.R.bool.config_appCompatUserAppAspectRatioSettingsIsEnabled);
+ return getValueFromDeviceConfig(KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS,
+ DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_SETTINGS) && isBuildTimeFlagEnabled;
+ }
+
+ /**
+ * @return user-specific {@link PackageManager.UserMinAspectRatio} override for an app
+ */
+ @PackageManager.UserMinAspectRatio
+ public int getUserMinAspectRatioValue(@NonNull String packageName, int uid)
+ throws RemoteException {
+ final int aspectRatio = mIPm.getUserMinAspectRatio(packageName, uid);
+ return hasAspectRatioOption(aspectRatio, packageName)
+ ? aspectRatio : PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
+ }
+
+ /**
+ * @return corresponding string for {@link PackageManager.UserMinAspectRatio} value
+ */
+ @NonNull
+ public String getUserMinAspectRatioEntry(@PackageManager.UserMinAspectRatio int aspectRatio,
+ String packageName) {
+ if (!hasAspectRatioOption(aspectRatio, packageName)) {
+ return mUserAspectRatioMap.get(PackageManager.USER_MIN_ASPECT_RATIO_UNSET);
+ }
+ return mUserAspectRatioMap.get(aspectRatio);
+ }
+
+ /**
+ * @return corresponding accessible string for {@link PackageManager.UserMinAspectRatio} value
+ */
+ @NonNull
+ public CharSequence getAccessibleEntry(@PackageManager.UserMinAspectRatio int aspectRatio,
+ String packageName) {
+ return mUserAspectRatioA11yMap.getOrDefault(aspectRatio,
+ getUserMinAspectRatioEntry(aspectRatio, packageName));
+ }
+
+ /**
+ * @return corresponding aspect ratio string for package name and user
+ */
+ @NonNull
+ public String getUserMinAspectRatioEntry(@NonNull String packageName, int uid)
+ throws RemoteException {
+ final int aspectRatio = getUserMinAspectRatioValue(packageName, uid);
+ return getUserMinAspectRatioEntry(aspectRatio, packageName);
+ }
+
+ /**
+ * Whether user aspect ratio option is specified in
+ * {@link R.array.config_userAspectRatioOverrideValues}
+ * and is enabled by device config
+ */
+ public boolean hasAspectRatioOption(@PackageManager.UserMinAspectRatio int option,
+ String packageName) {
+ if (option == PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN
+ && !isFullscreenOptionEnabled(packageName)) {
+ return false;
+ }
+ return mUserAspectRatioMap.containsKey(option);
+ }
+
+ /**
+ * Sets user-specified {@link PackageManager.UserMinAspectRatio} override for an app
+ */
+ public void setUserMinAspectRatio(@NonNull String packageName, int uid,
+ @PackageManager.UserMinAspectRatio int aspectRatio) throws RemoteException {
+ mIPm.setUserMinAspectRatio(packageName, uid, aspectRatio);
+ }
+
+ /**
+ * Whether an app's aspect ratio can be overridden by user. Only apps with launcher entry
+ * will be overridable.
+ */
+ public boolean canDisplayAspectRatioUi(@NonNull ApplicationInfo app) {
+ Boolean appAllowsUserAspectRatioOverride = readComponentProperty(
+ mContext.getPackageManager(), app.packageName,
+ PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE);
+ return !FALSE.equals(appAllowsUserAspectRatioOverride) && hasLauncherEntry(app);
+ }
+
+ /**
+ * Whether fullscreen option in per-app user aspect ratio settings is enabled
+ */
+ @VisibleForTesting
+ boolean isFullscreenOptionEnabled(String packageName) {
+ Boolean appAllowsFullscreenOption = readComponentProperty(mContext.getPackageManager(),
+ packageName, PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE);
+ final boolean isBuildTimeFlagEnabled = mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_appCompatUserAppAspectRatioFullscreenIsEnabled);
+ return !FALSE.equals(appAllowsFullscreenOption) && isBuildTimeFlagEnabled
+ && getValueFromDeviceConfig(KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN,
+ DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_FULLSCREEN);
+ }
+
+ LauncherApps getLauncherApps() {
+ return mContext.getSystemService(LauncherApps.class);
+ }
+
+ private boolean hasLauncherEntry(@NonNull ApplicationInfo app) {
+ return !getLauncherApps().getActivityList(app.packageName, getUserHandleForUid(app.uid))
+ .isEmpty();
+ }
+
+ private static boolean getValueFromDeviceConfig(String name, boolean defaultValue) {
+ return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER, name, defaultValue);
+ }
+
+ @NonNull
+ private Map getUserMinAspectRatioMapping() {
+ final String[] userMinAspectRatioStrings = mContext.getResources().getStringArray(
+ R.array.config_userAspectRatioOverrideEntries);
+ final int[] userMinAspectRatioValues = mContext.getResources().getIntArray(
+ R.array.config_userAspectRatioOverrideValues);
+ if (userMinAspectRatioStrings.length != userMinAspectRatioValues.length) {
+ throw new RuntimeException(
+ "config_userAspectRatioOverride options cannot be different length");
+ }
+
+ final Map userMinAspectRatioMap = new ArrayMap<>();
+ for (int i = 0; i < userMinAspectRatioValues.length; i++) {
+ final int aspectRatioVal = userMinAspectRatioValues[i];
+ final String aspectRatioString = getAspectRatioStringOrDefault(
+ userMinAspectRatioStrings[i], aspectRatioVal);
+ boolean containsColon = aspectRatioString.contains(":");
+ switch (aspectRatioVal) {
+ // Only map known values of UserMinAspectRatio and ignore unknown entries
+ case PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN:
+ case PackageManager.USER_MIN_ASPECT_RATIO_UNSET:
+ case PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN:
+ case PackageManager.USER_MIN_ASPECT_RATIO_DISPLAY_SIZE:
+ case PackageManager.USER_MIN_ASPECT_RATIO_4_3:
+ case PackageManager.USER_MIN_ASPECT_RATIO_16_9:
+ case PackageManager.USER_MIN_ASPECT_RATIO_3_2:
+ if (containsColon) {
+ String[] aspectRatioDigits = aspectRatioString.split(":");
+ String accessibleString = getAccessibleOption(aspectRatioDigits[0],
+ aspectRatioDigits[1]);
+ final CharSequence accessibleSequence = Utils.createAccessibleSequence(
+ aspectRatioString, accessibleString);
+ mUserAspectRatioA11yMap.put(aspectRatioVal, accessibleSequence);
+ }
+ userMinAspectRatioMap.put(aspectRatioVal, aspectRatioString);
+ }
+ }
+ if (!userMinAspectRatioMap.containsKey(PackageManager.USER_MIN_ASPECT_RATIO_UNSET)) {
+ throw new RuntimeException("config_userAspectRatioOverrideValues options must have"
+ + " USER_MIN_ASPECT_RATIO_UNSET value");
+ }
+ return userMinAspectRatioMap;
+ }
+
+ @NonNull
+ private String getAccessibleOption(String numerator, String denominator) {
+ return mContext.getResources().getString(R.string.user_aspect_ratio_option_a11y,
+ numerator, denominator);
+ }
+
+ @NonNull
+ private String getAspectRatioStringOrDefault(@Nullable String aspectRatioString,
+ @PackageManager.UserMinAspectRatio int aspectRatioVal) {
+ if (aspectRatioString != null) {
+ return aspectRatioString;
+ }
+ // Options are customized per device and if strings are set to @null, use default
+ switch (aspectRatioVal) {
+ case PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN:
+ return mContext.getString(R.string.user_aspect_ratio_fullscreen);
+ case PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN:
+ return mContext.getString(R.string.user_aspect_ratio_half_screen);
+ case PackageManager.USER_MIN_ASPECT_RATIO_DISPLAY_SIZE:
+ return mContext.getString(R.string.user_aspect_ratio_device_size);
+ case PackageManager.USER_MIN_ASPECT_RATIO_4_3:
+ return mContext.getString(R.string.user_aspect_ratio_4_3);
+ case PackageManager.USER_MIN_ASPECT_RATIO_16_9:
+ return mContext.getString(R.string.user_aspect_ratio_16_9);
+ case PackageManager.USER_MIN_ASPECT_RATIO_3_2:
+ return mContext.getString(R.string.user_aspect_ratio_3_2);
+ default:
+ return mContext.getString(R.string.user_aspect_ratio_app_default);
+ }
+ }
+
+ @Nullable
+ private static Boolean readComponentProperty(PackageManager pm, String packageName,
+ String propertyName) {
+ try {
+ return pm.getProperty(propertyName, packageName).getBoolean();
+ } catch (PackageManager.NameNotFoundException e) {
+ // No such property name
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/settings/applications/manageapplications/ManageApplications.java b/src/com/android/settings/applications/manageapplications/ManageApplications.java
index 548ca553b400281a3183cd8ccd4eff95f3159015..d734a27f0335ee480a99d33f4d0e52c2945e73bf 100644
--- a/src/com/android/settings/applications/manageapplications/ManageApplications.java
+++ b/src/com/android/settings/applications/manageapplications/ManageApplications.java
@@ -269,6 +269,7 @@ public class ManageApplications extends InstrumentedFragment
public static final int LIST_TYPE_CLONED_APPS = 17;
public static final int LIST_TYPE_NFC_TAG_APPS = 18;
public static final int LIST_TYPE_TURN_SCREEN_ON = 19;
+ public static final int LIST_TYPE_USER_ASPECT_RATIO_APPS = 20;
// List types that should show instant apps.
public static final Set LIST_TYPES_WITH_INSTANT = new ArraySet<>(Arrays.asList(
diff --git a/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt b/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt
index 78a4a6bfe54c48377c4f7146225110b57d1d45f2..8313686f29f73a8e8276cbe0d56057acc10c5798 100644
--- a/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt
+++ b/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt
@@ -20,6 +20,7 @@ import android.content.Context
import android.util.FeatureFlagUtils
import com.android.settings.Settings.AlarmsAndRemindersActivity
import com.android.settings.Settings.AppBatteryUsageActivity
+import com.android.settings.Settings.UserAspectRatioAppListActivity
import com.android.settings.Settings.ChangeNfcTagAppsActivity
import com.android.settings.Settings.ChangeWifiStateActivity
import com.android.settings.Settings.ClonedAppsListActivity
@@ -40,6 +41,7 @@ import com.android.settings.applications.appinfo.AppLocaleDetails
import com.android.settings.applications.manageapplications.ManageApplications.LIST_MANAGE_EXTERNAL_STORAGE
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_ALARMS_AND_REMINDERS
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_APPS_LOCALE
+import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_USER_ASPECT_RATIO_APPS
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_BATTERY_OPTIMIZATION
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_CLONED_APPS
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_GAMES
@@ -57,12 +59,14 @@ import com.android.settings.applications.manageapplications.ManageApplications.L
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_WIFI_ACCESS
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_WRITE_SETTINGS
import com.android.settings.spa.app.AllAppListPageProvider
+import com.android.settings.spa.app.appcompat.UserAspectRatioAppsPageProvider
import com.android.settings.spa.app.specialaccess.AlarmsAndRemindersAppListProvider
import com.android.settings.spa.app.specialaccess.AllFilesAccessAppListProvider
import com.android.settings.spa.app.specialaccess.DisplayOverOtherAppsAppListProvider
import com.android.settings.spa.app.specialaccess.InstallUnknownAppsListProvider
import com.android.settings.spa.app.specialaccess.MediaManagementAppsAppListProvider
import com.android.settings.spa.app.specialaccess.ModifySystemSettingsAppListProvider
+import com.android.settings.spa.app.specialaccess.NfcTagAppsSettingsProvider
import com.android.settings.spa.app.specialaccess.WifiControlAppListProvider
import com.android.settings.spa.notification.AppListNotificationsPageProvider
import com.android.settings.spa.system.AppLanguagesPageProvider
@@ -91,6 +95,7 @@ object ManageApplicationsUtil {
ClonedAppsListActivity::class to LIST_TYPE_CLONED_APPS,
ChangeNfcTagAppsActivity::class to LIST_TYPE_NFC_TAG_APPS,
TurnScreenOnSettingsActivity::class to LIST_TYPE_TURN_SCREEN_ON,
+ UserAspectRatioAppListActivity::class to LIST_TYPE_USER_ASPECT_RATIO_APPS,
)
@JvmField
@@ -112,6 +117,8 @@ object ManageApplicationsUtil {
LIST_TYPE_NOTIFICATION -> AppListNotificationsPageProvider.name
LIST_TYPE_APPS_LOCALE -> AppLanguagesPageProvider.name
LIST_TYPE_MAIN -> AllAppListPageProvider.name
+ LIST_TYPE_NFC_TAG_APPS -> NfcTagAppsSettingsProvider.getAppListRoute()
+ LIST_TYPE_USER_ASPECT_RATIO_APPS -> UserAspectRatioAppsPageProvider.name
else -> null
}
}
diff --git a/src/com/android/settings/applications/specialaccess/DataSaverController.kt b/src/com/android/settings/applications/specialaccess/DataSaverController.kt
new file mode 100644
index 0000000000000000000000000000000000000000..baed0aa5ec43e164d49989b2e36cfa16d78f13aa
--- /dev/null
+++ b/src/com/android/settings/applications/specialaccess/DataSaverController.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.settings.applications.specialaccess
+
+import android.content.Context
+import android.net.NetworkPolicyManager
+import android.os.UserHandle
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.preference.Preference
+import androidx.preference.PreferenceScreen
+import com.android.settings.R
+import com.android.settings.core.BasePreferenceController
+import com.android.settingslib.spa.framework.util.formatString
+import com.android.settingslib.spaprivileged.model.app.AppListRepository
+import com.android.settingslib.spaprivileged.model.app.AppListRepositoryImpl
+import com.google.common.annotations.VisibleForTesting
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+class DataSaverController(context: Context, key: String) : BasePreferenceController(context, key) {
+
+ private lateinit var preference: Preference
+
+ @AvailabilityStatus
+ override fun getAvailabilityStatus(): Int = when {
+ mContext.resources.getBoolean(R.bool.config_show_data_saver) -> AVAILABLE
+ else -> UNSUPPORTED_ON_DEVICE
+ }
+
+ override fun displayPreference(screen: PreferenceScreen) {
+ super.displayPreference(screen)
+ preference = screen.findPreference(preferenceKey)!!
+ }
+
+ override fun onViewCreated(viewLifecycleOwner: LifecycleOwner) {
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ preference.summary = getUnrestrictedSummary(mContext)
+ }
+ }
+ }
+
+ companion object {
+ @VisibleForTesting
+ suspend fun getUnrestrictedSummary(
+ context: Context,
+ appListRepository: AppListRepository =
+ AppListRepositoryImpl(context.applicationContext),
+ ) = context.formatString(
+ R.string.data_saver_unrestricted_summary,
+ "count" to getAllowCount(context.applicationContext, appListRepository),
+ )
+
+ private suspend fun getAllowCount(context: Context, appListRepository: AppListRepository) =
+ withContext(Dispatchers.IO) {
+ coroutineScope {
+ val appsDeferred = async {
+ appListRepository.loadAndFilterApps(
+ userId = UserHandle.myUserId(),
+ isSystemApp = false,
+ )
+ }
+ val uidsAllowed = NetworkPolicyManager.from(context)
+ .getUidsWithPolicy(NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND)
+ appsDeferred.await().count { app -> app.uid in uidsAllowed }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/settings/biometrics/BiometricEnrollBase.java b/src/com/android/settings/biometrics/BiometricEnrollBase.java
index 2f852f08b9f3821a381a2ce86ef839434642f6ad..6e110799c463de8b86f54511c701312fdced2931 100644
--- a/src/com/android/settings/biometrics/BiometricEnrollBase.java
+++ b/src/com/android/settings/biometrics/BiometricEnrollBase.java
@@ -133,6 +133,7 @@ public abstract class BiometricEnrollBase extends InstrumentedActivity {
protected long mChallenge;
protected boolean mFromSettingsSummary;
protected FooterBarMixin mFooterBarMixin;
+ protected boolean mShouldSetFooterBarBackground = true;
@Nullable
protected ScreenSizeFoldProvider mScreenSizeFoldProvider;
@Nullable
@@ -191,12 +192,14 @@ public abstract class BiometricEnrollBase extends InstrumentedActivity {
super.onPostCreate(savedInstanceState);
initViews();
- @SuppressLint("VisibleForTests")
- final LinearLayout buttonContainer = mFooterBarMixin != null
- ? mFooterBarMixin.getButtonContainer()
- : null;
- if (buttonContainer != null) {
- buttonContainer.setBackgroundColor(getBackgroundColor());
+ if (mShouldSetFooterBarBackground) {
+ @SuppressLint("VisibleForTests")
+ final LinearLayout buttonContainer = mFooterBarMixin != null
+ ? mFooterBarMixin.getButtonContainer()
+ : null;
+ if (buttonContainer != null) {
+ buttonContainer.setBackgroundColor(getBackgroundColor());
+ }
}
}
@@ -331,7 +334,7 @@ public abstract class BiometricEnrollBase extends InstrumentedActivity {
}
@ColorInt
- private int getBackgroundColor() {
+ public int getBackgroundColor() {
final ColorStateList stateList = Utils.getColorAttr(this, android.R.attr.windowBackground);
return stateList != null ? stateList.getDefaultColor() : Color.TRANSPARENT;
}
diff --git a/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java b/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java
index 2a350f4abd5cf36094d7b1fae9694d6d130d25e1..46f534df364781542ebd97556bdbdf5a93eeadb2 100644
--- a/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/BiometricEnrollIntroduction.java
@@ -236,6 +236,9 @@ public abstract class BiometricEnrollIntroduction extends BiometricEnrollBase
protected void onResume() {
super.onResume();
+ //reset mNextClick to make sure introduction page would be closed correctly
+ mNextClicked = false;
+
final int errorMsg = checkMaxEnrolled();
if (errorMsg == 0) {
mErrorText.setText(null);
diff --git a/src/com/android/settings/biometrics/BiometricEnrollSidecar.java b/src/com/android/settings/biometrics/BiometricEnrollSidecar.java
index 97d46a420e3bf80069cb5e085373d507f0ceb6c5..369fa4b4311317cbad93f1f18c08d4d8a4ee6e97 100644
--- a/src/com/android/settings/biometrics/BiometricEnrollSidecar.java
+++ b/src/com/android/settings/biometrics/BiometricEnrollSidecar.java
@@ -48,11 +48,16 @@ public abstract class BiometricEnrollSidecar extends InstrumentedFragment {
/**
* Called when a pointer down event has occurred.
*/
- default void onPointerDown(int sensorId) { }
+ default void onUdfpsPointerDown(int sensorId) { }
/**
* Called when a pointer up event has occurred.
*/
- default void onPointerUp(int sensorId) { }
+ default void onUdfpsPointerUp(int sensorId) { }
+
+ /**
+ * Called when udfps overlay is shown.
+ */
+ default void onUdfpsOverlayShown() { }
}
private int mEnrollmentSteps = -1;
@@ -126,29 +131,36 @@ public abstract class BiometricEnrollSidecar extends InstrumentedFragment {
}
}
- private class QueuedPointerDown extends QueuedEvent {
+ private class QueuedUdfpsPointerDown extends QueuedEvent {
private final int sensorId;
- public QueuedPointerDown(int sensorId) {
+ QueuedUdfpsPointerDown(int sensorId) {
this.sensorId = sensorId;
}
@Override
public void send(Listener listener) {
- listener.onPointerDown(sensorId);
+ listener.onUdfpsPointerDown(sensorId);
}
}
- private class QueuedPointerUp extends QueuedEvent {
+ private class QueuedUdfpsPointerUp extends QueuedEvent {
private final int sensorId;
- public QueuedPointerUp(int sensorId) {
+ QueuedUdfpsPointerUp(int sensorId) {
this.sensorId = sensorId;
}
@Override
public void send(Listener listener) {
- listener.onPointerUp(sensorId);
+ listener.onUdfpsPointerUp(sensorId);
+ }
+ }
+
+ private class QueuedUdfpsOverlayShown extends QueuedEvent {
+ @Override
+ public void send(Listener listener) {
+ listener.onUdfpsOverlayShown();
}
}
@@ -249,19 +261,27 @@ public abstract class BiometricEnrollSidecar extends InstrumentedFragment {
}
}
- protected void onPointerDown(int sensorId) {
+ protected void onUdfpsPointerDown(int sensorId) {
+ if (mListener != null) {
+ mListener.onUdfpsPointerDown(sensorId);
+ } else {
+ mQueuedEvents.add(new QueuedUdfpsPointerDown(sensorId));
+ }
+ }
+
+ protected void onUdfpsPointerUp(int sensorId) {
if (mListener != null) {
- mListener.onPointerDown(sensorId);
+ mListener.onUdfpsPointerUp(sensorId);
} else {
- mQueuedEvents.add(new QueuedPointerDown(sensorId));
+ mQueuedEvents.add(new QueuedUdfpsPointerUp(sensorId));
}
}
- protected void onPointerUp(int sensorId) {
+ protected void onUdfpsOverlayShown() {
if (mListener != null) {
- mListener.onPointerUp(sensorId);
+ mListener.onUdfpsOverlayShown();
} else {
- mQueuedEvents.add(new QueuedPointerUp(sensorId));
+ mQueuedEvents.add(new QueuedUdfpsOverlayShown());
}
}
diff --git a/src/com/android/settings/biometrics/BiometricUtils.java b/src/com/android/settings/biometrics/BiometricUtils.java
index 3356dfa932226a34d59ed9d473789a3023f681a5..4e1a2f329b4de02b1d119ffa2187311d11f5c7fe 100644
--- a/src/com/android/settings/biometrics/BiometricUtils.java
+++ b/src/com/android/settings/biometrics/BiometricUtils.java
@@ -527,17 +527,18 @@ public class BiometricUtils {
// Assume the flow is "Screen Lock" + "Face" + "Fingerprint"
ssb.append(bidi.unicodeWrap(screenLock));
- if (isFaceSupported) {
+ if (hasFingerprint) {
ssb.append(bidi.unicodeWrap(SEPARATOR));
ssb.append(bidi.unicodeWrap(
- capitalize(context.getString(R.string.keywords_face_settings))));
+ capitalize(context.getString(R.string.security_settings_fingerprint))));
}
- if (hasFingerprint) {
+ if (isFaceSupported) {
ssb.append(bidi.unicodeWrap(SEPARATOR));
ssb.append(bidi.unicodeWrap(
- capitalize(context.getString(R.string.security_settings_fingerprint))));
+ capitalize(context.getString(R.string.keywords_face_settings))));
}
+
return ssb.toString();
}
diff --git a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java
index 487e254a929f5f7461ed513e5eae6027aea59120..69ae9a735e364ab8506ed80f575edd4cb542ec69 100644
--- a/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java
+++ b/src/com/android/settings/biometrics/combination/BiometricsSettingsBase.java
@@ -21,6 +21,7 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRIN
import static com.android.settings.password.ChooseLockPattern.RESULT_FINISHED;
+import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.hardware.biometrics.SensorProperties;
@@ -179,6 +180,12 @@ public abstract class BiometricsSettingsBase extends DashboardFragment {
}
mFaceManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> {
+ final Activity activity = getActivity();
+ if (activity == null || activity.isFinishing()) {
+ Log.e(getLogTag(), "Stop during generating face unlock challenge"
+ + " because activity is null or finishing");
+ return;
+ }
try {
final byte[] token = requestGatekeeperHat(context, mGkPwHandle, mUserId,
challenge);
@@ -215,6 +222,12 @@ public abstract class BiometricsSettingsBase extends DashboardFragment {
}
mFingerprintManager.generateChallenge(mUserId, (sensorId, userId, challenge) -> {
+ final Activity activity = getActivity();
+ if (activity == null || activity.isFinishing()) {
+ Log.e(getLogTag(), "Stop during generating fingerprint challenge"
+ + " because activity is null or finishing");
+ return;
+ }
try {
final byte[] token = requestGatekeeperHat(context, mGkPwHandle, mUserId,
challenge);
diff --git a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
index bff998a4a3e458e7dc9c6359ef8dde7317409966..bea0c3389ec92adddd51e3af443fe9de98676b4a 100644
--- a/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
+++ b/src/com/android/settings/biometrics/face/FaceEnrollIntroduction.java
@@ -120,6 +120,8 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction {
protected void onCreate(Bundle savedInstanceState) {
mFaceManager = getFaceManager();
+ super.onCreate(savedInstanceState);
+
if (savedInstanceState == null
&& !WizardManagerHelper.isAnySetupWizard(getIntent())
&& !getIntent().getBooleanExtra(EXTRA_FROM_SETTINGS_SUMMARY, false)
@@ -130,8 +132,6 @@ public class FaceEnrollIntroduction extends BiometricEnrollIntroduction {
finish();
}
- super.onCreate(savedInstanceState);
-
// Wait super::onCreated() then return because SuperNotCalledExceptio will be thrown
// if we don't wait for it.
if (isFinishing()) {
diff --git a/src/com/android/settings/biometrics/face/FaceSettings.java b/src/com/android/settings/biometrics/face/FaceSettings.java
index 979faa22fc2b8fe53ebd20876cf92e9a002f75e1..2e9440412eca89459c0ab5d02b9239bd0a8bb194 100644
--- a/src/com/android/settings/biometrics/face/FaceSettings.java
+++ b/src/com/android/settings/biometrics/face/FaceSettings.java
@@ -208,6 +208,10 @@ public class FaceSettings extends DashboardFragment {
mRemoveButton = findPreference(FaceSettingsRemoveButtonPreferenceController.KEY);
mEnrollButton = findPreference(FaceSettingsEnrollButtonPreferenceController.KEY);
+ final boolean hasEnrolled = mFaceManager.hasEnrolledTemplates(mUserId);
+ mEnrollButton.setVisible(!hasEnrolled);
+ mRemoveButton.setVisible(hasEnrolled);
+
// There is no better way to do this :/
for (AbstractPreferenceController controller : mControllers) {
if (controller instanceof FaceSettingsPreferenceController) {
diff --git a/src/com/android/settings/biometrics/face/FaceSettingsRemoveButtonPreferenceController.java b/src/com/android/settings/biometrics/face/FaceSettingsRemoveButtonPreferenceController.java
index 7db595848996cdf48e2299c3e4b974a433ab11fa..4b2e3363b6dd1230935c95395824e8e09195431e 100644
--- a/src/com/android/settings/biometrics/face/FaceSettingsRemoveButtonPreferenceController.java
+++ b/src/com/android/settings/biometrics/face/FaceSettingsRemoveButtonPreferenceController.java
@@ -21,6 +21,7 @@ import android.app.Dialog;
import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.DialogInterface;
+import android.content.pm.PackageManager;
import android.hardware.face.Face;
import android.hardware.face.FaceManager;
import android.os.Bundle;
@@ -29,6 +30,7 @@ import android.view.View;
import android.widget.Button;
import android.widget.Toast;
+import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
import com.android.settings.R;
@@ -56,10 +58,18 @@ public class FaceSettingsRemoveButtonPreferenceController extends BasePreference
static final String KEY = "security_settings_face_delete_faces_container";
public static class ConfirmRemoveDialog extends InstrumentedDialogFragment {
-
- private boolean mIsConvenience;
+ private static final String KEY_IS_CONVENIENCE = "is_convenience";
private DialogInterface.OnClickListener mOnClickListener;
+ /** Returns the new instance of the class */
+ public static ConfirmRemoveDialog newInstance(boolean isConvenience) {
+ final ConfirmRemoveDialog dialog = new ConfirmRemoveDialog();
+ final Bundle args = new Bundle();
+ args.putBoolean(KEY_IS_CONVENIENCE, isConvenience);
+ dialog.setArguments(args);
+ return dialog;
+ }
+
@Override
public int getMetricsCategory() {
return SettingsEnums.DIALOG_FACE_REMOVE;
@@ -67,12 +77,26 @@ public class FaceSettingsRemoveButtonPreferenceController extends BasePreference
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
+ boolean isConvenience = getArguments().getBoolean(KEY_IS_CONVENIENCE);
+
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ final PackageManager pm = getContext().getPackageManager();
+ final boolean hasFingerprint = pm.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT);
+ final int dialogMessageRes;
+
+ if (hasFingerprint) {
+ dialogMessageRes = isConvenience
+ ? R.string.security_settings_face_remove_dialog_details_fingerprint_conv
+ : R.string.security_settings_face_remove_dialog_details_fingerprint;
+ } else {
+ dialogMessageRes = isConvenience
+ ? R.string.security_settings_face_settings_remove_dialog_details_convenience
+ : R.string.security_settings_face_settings_remove_dialog_details;
+ }
+
builder.setTitle(R.string.security_settings_face_settings_remove_dialog_title)
- .setMessage(mIsConvenience
- ? R.string.security_settings_face_settings_remove_dialog_details_convenience
- : R.string.security_settings_face_settings_remove_dialog_details)
+ .setMessage(dialogMessageRes)
.setPositiveButton(R.string.delete, mOnClickListener)
.setNegativeButton(R.string.cancel, mOnClickListener);
AlertDialog dialog = builder.create();
@@ -80,10 +104,6 @@ public class FaceSettingsRemoveButtonPreferenceController extends BasePreference
return dialog;
}
- public void setIsConvenience(boolean isConvenience) {
- mIsConvenience = isConvenience;
- }
-
public void setOnClickListener(DialogInterface.OnClickListener listener) {
mOnClickListener = listener;
}
@@ -98,7 +118,8 @@ public class FaceSettingsRemoveButtonPreferenceController extends BasePreference
private Listener mListener;
private SettingsActivity mActivity;
private int mUserId;
- private boolean mRemoving;
+ @VisibleForTesting
+ boolean mRemoving;
private final MetricsFeatureProvider mMetricsFeatureProvider;
private final Context mContext;
@@ -129,7 +150,7 @@ public class FaceSettingsRemoveButtonPreferenceController extends BasePreference
}
};
- private final DialogInterface.OnClickListener mOnClickListener
+ private final DialogInterface.OnClickListener mOnConfirmDialogClickListener
= new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
@@ -183,6 +204,16 @@ public class FaceSettingsRemoveButtonPreferenceController extends BasePreference
mButton.setOnClickListener(this);
+ // If there is already a ConfirmRemoveDialog showing, reset the listener since the
+ // controller has been recreated.
+ ConfirmRemoveDialog removeDialog =
+ (ConfirmRemoveDialog) mActivity.getSupportFragmentManager()
+ .findFragmentByTag(ConfirmRemoveDialog.class.getName());
+ if (removeDialog != null) {
+ mRemoving = true;
+ removeDialog.setOnClickListener(mOnConfirmDialogClickListener);
+ }
+
if (!FaceSettings.isFaceHardwareDetected(mContext)) {
mButton.setEnabled(false);
} else {
@@ -205,10 +236,11 @@ public class FaceSettingsRemoveButtonPreferenceController extends BasePreference
if (v == mButton) {
mMetricsFeatureProvider.logClickedPreference(mPreference, getMetricsCategory());
mRemoving = true;
- ConfirmRemoveDialog dialog = new ConfirmRemoveDialog();
- dialog.setOnClickListener(mOnClickListener);
- dialog.setIsConvenience(BiometricUtils.isConvenience(mFaceManager));
- dialog.show(mActivity.getSupportFragmentManager(), ConfirmRemoveDialog.class.getName());
+ ConfirmRemoveDialog confirmRemoveDialog =
+ ConfirmRemoveDialog.newInstance(BiometricUtils.isConvenience(mFaceManager));
+ confirmRemoveDialog.setOnClickListener(mOnConfirmDialogClickListener);
+ confirmRemoveDialog.show(mActivity.getSupportFragmentManager(),
+ ConfirmRemoveDialog.class.getName());
}
}
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintAuthenticateSidecar.java b/src/com/android/settings/biometrics/fingerprint/FingerprintAuthenticateSidecar.java
index 426405627abd6758f768cf52d1e0c4c7af169e3d..f3c8aba4095462394d78acaf6dab56b396449060 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintAuthenticateSidecar.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintAuthenticateSidecar.java
@@ -21,6 +21,7 @@ import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.FingerprintManager.AuthenticationResult;
import android.os.CancellationSignal;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.settings.core.InstrumentedFragment;
/**
@@ -80,7 +81,6 @@ public class FingerprintAuthenticateSidecar extends InstrumentedFragment {
@Override
public void onAuthenticationError(int errMsgId, CharSequence errString) {
- mCancellationSignal = null;
if (mListener != null) {
mListener.onAuthenticationError(errMsgId, errString);
} else {
@@ -108,10 +108,12 @@ public class FingerprintAuthenticateSidecar extends InstrumentedFragment {
}
public void stopAuthentication() {
- if (mCancellationSignal != null && !mCancellationSignal.isCanceled()) {
+ if (mCancellationSignal != null) {
+ // This will automatically check if the cancel has been sent and if so
+ // it won't send it again.
mCancellationSignal.cancel();
+ mCancellationSignal = null;
}
- mCancellationSignal = null;
}
public void setListener(Listener listener) {
@@ -129,4 +131,9 @@ public class FingerprintAuthenticateSidecar extends InstrumentedFragment {
}
mListener = listener;
}
+
+ @VisibleForTesting
+ boolean isCancelled() {
+ return mCancellationSignal == null || mCancellationSignal.isCanceled();
+ }
}
\ No newline at end of file
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
index 7e764059b9a5559088f846122191917620eed20e..a62bd6723ecf44aef713174b2aa85f23bbdc3215 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java
@@ -32,10 +32,8 @@ import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.content.res.Resources;
-import android.graphics.Point;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
-import android.graphics.Rect;
import android.graphics.drawable.Animatable2;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
@@ -48,22 +46,16 @@ import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.text.TextUtils;
-import android.util.FeatureFlagUtils;
import android.util.Log;
-import android.view.DisplayInfo;
import android.view.MotionEvent;
import android.view.OrientationEventListener;
import android.view.Surface;
import android.view.View;
-import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
@@ -79,25 +71,20 @@ import com.android.settings.biometrics.BiometricUtils;
import com.android.settings.biometrics.BiometricsEnrollEnrolling;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settingslib.display.DisplayDensityUtils;
-import com.android.settingslib.udfps.UdfpsOverlayParams;
-import com.android.settingslib.udfps.UdfpsUtils;
import com.airbnb.lottie.LottieAnimationView;
import com.airbnb.lottie.LottieCompositionFactory;
import com.airbnb.lottie.LottieProperty;
import com.airbnb.lottie.model.KeyPath;
-import com.google.android.setupcompat.template.FooterActionButton;
import com.google.android.setupcompat.template.FooterBarMixin;
import com.google.android.setupcompat.template.FooterButton;
import com.google.android.setupcompat.util.WizardManagerHelper;
-import com.google.android.setupdesign.GlifLayout;
import com.google.android.setupdesign.template.DescriptionMixin;
import com.google.android.setupdesign.template.HeaderMixin;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
-import java.util.Locale;
/**
* Activity which handles the actual enrolling for fingerprint.
@@ -176,8 +163,6 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
@VisibleForTesting
@Nullable
UdfpsEnrollHelper mUdfpsEnrollHelper;
- // TODO(b/260617060): Do not hard-code mScaleFactor, referring to AuthController.
- private float mScaleFactor = 1.0f;
private ObjectAnimator mProgressAnim;
private TextView mErrorText;
private Interpolator mFastOutSlowInInterpolator;
@@ -206,7 +191,7 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
private boolean mHaveShownSfpsLeftEdgeLottie;
private boolean mHaveShownSfpsRightEdgeLottie;
private boolean mShouldShowLottie;
- private UdfpsUtils mUdfpsUtils;
+
private ObjectAnimator mHelpAnimation;
private OrientationEventListener mOrientationEventListener;
@@ -251,82 +236,17 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
mAccessibilityManager = getSystemService(AccessibilityManager.class);
mIsAccessibilityEnabled = mAccessibilityManager.isEnabled();
- mUdfpsUtils = new UdfpsUtils();
- final boolean isLayoutRtl = (TextUtils.getLayoutDirectionFromLocale(
- Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL);
listenOrientationEvent();
if (mCanAssumeUdfps) {
- int rotation = getApplicationContext().getDisplay().getRotation();
- final GlifLayout layout = (GlifLayout) getLayoutInflater().inflate(
- R.layout.udfps_enroll_enrolling, null, false);
- final UdfpsEnrollView udfpsEnrollView = layout.findViewById(R.id.udfps_animation_view);
- updateUdfpsEnrollView(udfpsEnrollView, props.get(0));
- switch (rotation) {
- case Surface.ROTATION_90:
- final LinearLayout layoutContainer = layout.findViewById(
- R.id.layout_container);
- final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
- LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.MATCH_PARENT);
-
- lp.setMarginEnd((int) getResources().getDimension(
- R.dimen.rotation_90_enroll_margin_end));
- layoutContainer.setPaddingRelative((int) getResources().getDimension(
- R.dimen.rotation_90_enroll_padding_start), 0, isLayoutRtl
- ? 0 : (int) getResources().getDimension(
- R.dimen.rotation_90_enroll_padding_end), 0);
- layoutContainer.setLayoutParams(lp);
-
- setOnHoverListener(true, layout, udfpsEnrollView);
- setContentView(layout, lp);
- break;
-
- case Surface.ROTATION_0:
- case Surface.ROTATION_180:
- // In the portrait mode, layout_container's height is 0, so it's
- // always shown at the bottom of the screen.
- final FrameLayout portraitLayoutContainer = layout.findViewById(
- R.id.layout_container);
-
- // In the portrait mode, the title and lottie animation view may
- // overlap when title needs three lines, so adding some paddings
- // between them, and adjusting the fp progress view here accordingly.
- final int layoutLottieAnimationPadding = (int) getResources()
- .getDimension(R.dimen.udfps_lottie_padding_top);
- portraitLayoutContainer.setPadding(0,
- layoutLottieAnimationPadding, 0, 0);
- final ImageView progressView = udfpsEnrollView.findViewById(
- R.id.udfps_enroll_animation_fp_progress_view);
- progressView.setPadding(0, -(layoutLottieAnimationPadding),
- 0, layoutLottieAnimationPadding);
- final ImageView fingerprintView = udfpsEnrollView.findViewById(
- R.id.udfps_enroll_animation_fp_view);
- fingerprintView.setPadding(0, -layoutLottieAnimationPadding,
- 0, layoutLottieAnimationPadding);
-
- // TODO(b/260970216) Instead of hiding the description text view, we should
- // make the header view scrollable if the text is too long.
- // If description text view has overlap with udfps progress view, hide it.
- View view = layout.getDescriptionTextView();
- layout.getViewTreeObserver().addOnDrawListener(() -> {
- if (view.getVisibility() == View.VISIBLE
- && hasOverlap(view, udfpsEnrollView)) {
- view.setVisibility(View.GONE);
- }
- });
-
- setOnHoverListener(false, layout, udfpsEnrollView);
- setContentView(layout);
- break;
+ final UdfpsEnrollEnrollingView layout =
+ (UdfpsEnrollEnrollingView) getLayoutInflater().inflate(
+ R.layout.udfps_enroll_enrolling, null, false);
+ setUdfpsEnrollHelper();
+ layout.initView(props.get(0), mUdfpsEnrollHelper, mAccessibilityManager);
- case Surface.ROTATION_270:
- default:
- setOnHoverListener(true, layout, udfpsEnrollView);
- setContentView(layout);
- break;
- }
+ setContentView(layout);
setDescriptionText(R.string.security_settings_udfps_enroll_start_message);
} else if (mCanAssumeSfps) {
setContentView(R.layout.sfps_enroll_enrolling);
@@ -366,22 +286,11 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
.build()
);
- if (FeatureFlagUtils.isEnabled(getApplicationContext(),
- FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS)) {
- // Remove the space view and make the width of footer button container WRAP_CONTENT
- // to avoid hiding the udfps view progress bar bottom.
- final LinearLayout buttonContainer = mFooterBarMixin.getButtonContainer();
- View spaceView = null;
- for (int i = 0; i < buttonContainer.getChildCount(); i++) {
- if (!(buttonContainer.getChildAt(i) instanceof FooterActionButton)) {
- spaceView = buttonContainer.getChildAt(i);
- break;
- }
- }
- if (spaceView != null) {
- spaceView.setVisibility(View.GONE);
- buttonContainer.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
- }
+ // If it's udfps, set the background color only for secondary button if necessary.
+ if (mCanAssumeUdfps) {
+ mShouldSetFooterBarBackground = false;
+ ((UdfpsEnrollEnrollingView) getLayout()).setSecondaryButtonBackground(
+ getBackgroundColor());
}
final LayerDrawable fingerprintDrawable = mProgressBar != null
@@ -919,19 +828,26 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
}
@Override
- public void onPointerDown(int sensorId) {
+ public void onUdfpsPointerDown(int sensorId) {
if (mUdfpsEnrollHelper != null) {
mUdfpsEnrollHelper.onPointerDown(sensorId);
}
}
@Override
- public void onPointerUp(int sensorId) {
+ public void onUdfpsPointerUp(int sensorId) {
if (mUdfpsEnrollHelper != null) {
mUdfpsEnrollHelper.onPointerUp(sensorId);
}
}
+ @Override
+ public void onUdfpsOverlayShown() {
+ if (mCanAssumeUdfps) {
+ findViewById(R.id.udfps_animation_view).setVisibility(View.VISIBLE);
+ }
+ }
+
private void updateProgress(boolean animate) {
if (mSidecar == null || !mSidecar.isEnrolling()) {
Log.d(TAG, "Enrollment not started yet");
@@ -1185,9 +1101,9 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
}
}
- @SuppressWarnings("MissingSuperCall") // TODO: Fix me
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
maybeHideSfpsText(newConfig);
switch(newConfig.orientation) {
case Configuration.ORIENTATION_LANDSCAPE: {
@@ -1224,30 +1140,7 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
}
}
- private UdfpsEnrollView updateUdfpsEnrollView(UdfpsEnrollView udfpsEnrollView,
- FingerprintSensorPropertiesInternal udfpsProps) {
- DisplayInfo displayInfo = new DisplayInfo();
- getDisplay().getDisplayInfo(displayInfo);
- mScaleFactor = mUdfpsUtils.getScaleFactor(displayInfo);
- Rect udfpsBounds = udfpsProps.getLocation().getRect();
- udfpsBounds.scale(mScaleFactor);
-
- final Rect overlayBounds = new Rect(
- 0, /* left */
- displayInfo.getNaturalHeight() / 2, /* top */
- displayInfo.getNaturalWidth(), /* right */
- displayInfo.getNaturalHeight() /* botom */);
-
- UdfpsOverlayParams params = new UdfpsOverlayParams(
- udfpsBounds,
- overlayBounds,
- displayInfo.getNaturalWidth(),
- displayInfo.getNaturalHeight(),
- mScaleFactor,
- displayInfo.rotation);
-
- udfpsEnrollView.setOverlayParams(params);
-
+ private void setUdfpsEnrollHelper() {
mUdfpsEnrollHelper = (UdfpsEnrollHelper) getSupportFragmentManager().findFragmentByTag(
FingerprintEnrollEnrolling.TAG_UDFPS_HELPER);
if (mUdfpsEnrollHelper == null) {
@@ -1257,57 +1150,6 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling {
.add(mUdfpsEnrollHelper, FingerprintEnrollEnrolling.TAG_UDFPS_HELPER)
.commitAllowingStateLoss();
}
- udfpsEnrollView.setEnrollHelper(mUdfpsEnrollHelper);
-
- return udfpsEnrollView;
- }
-
- private void setOnHoverListener(boolean isLandscape, GlifLayout enrollLayout,
- UdfpsEnrollView udfpsEnrollView) {
- if (!mIsAccessibilityEnabled) return;
-
- final Context context = getApplicationContext();
- final View.OnHoverListener onHoverListener = (v, event) -> {
- // Map the touch to portrait mode if the device is in
- // landscape mode.
- final Point scaledTouch =
- mUdfpsUtils.getTouchInNativeCoordinates(event.getPointerId(0),
- event, udfpsEnrollView.getOverlayParams());
-
- if (mUdfpsUtils.isWithinSensorArea(event.getPointerId(0), event,
- udfpsEnrollView.getOverlayParams())) {
- return false;
- }
-
- final String theStr = mUdfpsUtils.onTouchOutsideOfSensorArea(
- mAccessibilityManager.isTouchExplorationEnabled(), context,
- scaledTouch.x, scaledTouch.y, udfpsEnrollView.getOverlayParams());
- if (theStr != null) {
- v.announceForAccessibility(theStr);
- }
- return false;
- };
-
- enrollLayout.findManagedViewById(isLandscape ? R.id.sud_landscape_content_area
- : R.id.sud_layout_content).setOnHoverListener(onHoverListener);
- }
-
-
- @VisibleForTesting boolean hasOverlap(View view1, View view2) {
- int[] firstPosition = new int[2];
- int[] secondPosition = new int[2];
-
- view1.getLocationOnScreen(firstPosition);
- view2.getLocationOnScreen(secondPosition);
-
- // Rect constructor parameters: left, top, right, bottom
- Rect rectView1 = new Rect(firstPosition[0], firstPosition[1],
- firstPosition[0] + view1.getMeasuredWidth(),
- firstPosition[1] + view1.getMeasuredHeight());
- Rect rectView2 = new Rect(secondPosition[0], secondPosition[1],
- secondPosition[0] + view2.getMeasuredWidth(),
- secondPosition[1] + view2.getMeasuredHeight());
- return rectView1.intersect(rectView2);
}
public static class IconTouchDialog extends InstrumentedDialogFragment {
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java
index 5d04cd6c5ed8d9221cf4be36c355831fad0a7627..493302bd13c8fd2c31d6ecf3beae8d2f68fa8e53 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java
@@ -124,13 +124,18 @@ public class FingerprintEnrollSidecar extends BiometricEnrollSidecar {
}
@Override
- public void onPointerDown(int sensorId) {
- FingerprintEnrollSidecar.super.onPointerDown(sensorId);
+ public void onUdfpsPointerDown(int sensorId) {
+ FingerprintEnrollSidecar.super.onUdfpsPointerDown(sensorId);
}
@Override
- public void onPointerUp(int sensorId) {
- FingerprintEnrollSidecar.super.onPointerUp(sensorId);
+ public void onUdfpsPointerUp(int sensorId) {
+ FingerprintEnrollSidecar.super.onUdfpsPointerUp(sensorId);
+ }
+
+ @Override
+ public void onUdfpsOverlayShown() {
+ FingerprintEnrollSidecar.super.onUdfpsOverlayShown();
}
};
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
index fb3319c3ff62d701465e9f1002b61faa843a7cce..505fe1c1fc4a66562d7ddc2258a2e5ccf7f1e9e8 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
@@ -169,7 +169,8 @@ public class FingerprintSettings extends SubSettings {
private static final String KEY_LAUNCHED_CONFIRM = "launched_confirm";
private static final String KEY_HAS_FIRST_ENROLLED = "has_first_enrolled";
private static final String KEY_IS_ENROLLING = "is_enrolled";
- private static final String KEY_REQUIRE_SCREEN_ON_TO_AUTH =
+ @VisibleForTesting
+ static final String KEY_REQUIRE_SCREEN_ON_TO_AUTH =
"security_settings_require_screen_on_to_auth";
private static final String KEY_FINGERPRINTS_ENROLLED_CATEGORY =
"security_settings_fingerprints_enrolled";
@@ -479,10 +480,8 @@ public class FingerprintSettings extends SubSettings {
R.string.security_settings_fingerprint_enroll_introduction_v3_message,
DeviceHelper.getDeviceName(getActivity()));
column.mLearnMoreClickListener = learnMoreClickListener;
- if (isSfps()) {
- column.mLearnMoreOverrideText = getText(
- R.string.security_settings_fingerprint_settings_footer_learn_more);
- }
+ column.mLearnMoreOverrideText = getText(
+ R.string.security_settings_fingerprint_settings_footer_learn_more);
mFooterColumns.add(column);
}
}
@@ -536,10 +535,6 @@ public class FingerprintSettings extends SubSettings {
private void addFingerprintPreferences(PreferenceGroup root) {
final String fpPrefKey = addFingerprintItemPreferences(root);
- if (isSfps()) {
- scrollToPreference(fpPrefKey);
- addFingerprintUnlockCategory();
- }
for (AbstractPreferenceController controller : mControllers) {
if (controller instanceof FingerprintSettingsPreferenceController) {
((FingerprintSettingsPreferenceController) controller).setUserId(mUserId);
@@ -547,6 +542,14 @@ public class FingerprintSettings extends SubSettings {
((FingerprintUnlockCategoryController) controller).setUserId(mUserId);
}
}
+
+ // This needs to be after setting ids, otherwise
+ // |mRequireScreenOnToAuthPreferenceController.isChecked| is always checking the primary
+ // user instead of the user with |mUserId|.
+ if (isSfps()) {
+ scrollToPreference(fpPrefKey);
+ addFingerprintUnlockCategory();
+ }
createFooterPreference(root);
}
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintUpdater.java b/src/com/android/settings/biometrics/fingerprint/FingerprintUpdater.java
index 36325a7b975c9b0a46098ecfcbd70ab551567366..306b1a3e136faff0412a4c723c64a52e5a0f6b9d 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintUpdater.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintUpdater.java
@@ -98,13 +98,18 @@ public class FingerprintUpdater {
}
@Override
- public void onPointerDown(int sensorId) {
- mCallback.onPointerDown(sensorId);
+ public void onUdfpsPointerDown(int sensorId) {
+ mCallback.onUdfpsPointerDown(sensorId);
}
@Override
- public void onPointerUp(int sensorId) {
- mCallback.onPointerUp(sensorId);
+ public void onUdfpsPointerUp(int sensorId) {
+ mCallback.onUdfpsPointerUp(sensorId);
+ }
+
+ @Override
+ public void onUdfpsOverlayShown() {
+ mCallback.onUdfpsOverlayShown();
}
}
diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollEnrollingView.java b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollEnrollingView.java
new file mode 100644
index 0000000000000000000000000000000000000000..df2f2f77d3b5a9e36a2bf6981670875bdefde423
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollEnrollingView.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2023 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.settings.biometrics.fingerprint;
+
+import android.content.Context;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.DisplayInfo;
+import android.view.Gravity;
+import android.view.Surface;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+
+import androidx.annotation.ColorInt;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.R;
+import com.android.settingslib.udfps.UdfpsOverlayParams;
+import com.android.settingslib.udfps.UdfpsUtils;
+
+import com.google.android.setupcompat.template.FooterBarMixin;
+import com.google.android.setupdesign.GlifLayout;
+import com.google.android.setupdesign.view.BottomScrollView;
+
+import java.util.Locale;
+
+/**
+ * View for udfps enrolling.
+ */
+public class UdfpsEnrollEnrollingView extends GlifLayout {
+ private final UdfpsUtils mUdfpsUtils;
+ private final Context mContext;
+ // We don't need to listen to onConfigurationChanged() for mRotation here because
+ // FingerprintEnrollEnrolling is always recreated once the configuration is changed.
+ private final int mRotation;
+ private final boolean mIsLandscape;
+ private final boolean mShouldUseReverseLandscape;
+ private UdfpsEnrollView mUdfpsEnrollView;
+ private View mHeaderView;
+ private AccessibilityManager mAccessibilityManager;
+
+
+ public UdfpsEnrollEnrollingView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mContext = context;
+ mRotation = mContext.getDisplay().getRotation();
+ mIsLandscape = mRotation == Surface.ROTATION_90 || mRotation == Surface.ROTATION_270;
+ final boolean isLayoutRtl = (TextUtils.getLayoutDirectionFromLocale(Locale.getDefault())
+ == View.LAYOUT_DIRECTION_RTL);
+ mShouldUseReverseLandscape = (mRotation == Surface.ROTATION_90 && isLayoutRtl)
+ || (mRotation == Surface.ROTATION_270 && !isLayoutRtl);
+
+ mUdfpsUtils = new UdfpsUtils();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mHeaderView = findViewById(R.id.sud_landscape_header_area);
+ mUdfpsEnrollView = findViewById(R.id.udfps_animation_view);
+ }
+
+ void initView(FingerprintSensorPropertiesInternal udfpsProps,
+ UdfpsEnrollHelper udfpsEnrollHelper,
+ AccessibilityManager accessibilityManager) {
+ mAccessibilityManager = accessibilityManager;
+ initUdfpsEnrollView(udfpsProps, udfpsEnrollHelper);
+
+ if (!mIsLandscape) {
+ adjustPortraitPaddings();
+ } else if (mShouldUseReverseLandscape) {
+ swapHeaderAndContent();
+ }
+ setOnHoverListener();
+ }
+
+ void setSecondaryButtonBackground(@ColorInt int color) {
+ // Set the button background only when the button is not under udfps overlay to avoid UI
+ // overlap.
+ if (!mIsLandscape || mShouldUseReverseLandscape) {
+ return;
+ }
+ final Button secondaryButtonView =
+ getMixin(FooterBarMixin.class).getSecondaryButtonView();
+ secondaryButtonView.setBackgroundColor(color);
+ if (mRotation == Surface.ROTATION_90) {
+ secondaryButtonView.setGravity(Gravity.START);
+ } else {
+ secondaryButtonView.setGravity(Gravity.END);
+ }
+ mHeaderView.post(() -> {
+ secondaryButtonView.setLayoutParams(
+ new LinearLayout.LayoutParams(mHeaderView.getMeasuredWidth(),
+ ViewGroup.LayoutParams.WRAP_CONTENT));
+ });
+ }
+
+ private void initUdfpsEnrollView(FingerprintSensorPropertiesInternal udfpsProps,
+ UdfpsEnrollHelper udfpsEnrollHelper) {
+ DisplayInfo displayInfo = new DisplayInfo();
+ mContext.getDisplay().getDisplayInfo(displayInfo);
+
+ final float scaleFactor = mUdfpsUtils.getScaleFactor(displayInfo);
+ Rect udfpsBounds = udfpsProps.getLocation().getRect();
+ udfpsBounds.scale(scaleFactor);
+
+ final Rect overlayBounds = new Rect(
+ 0, /* left */
+ displayInfo.getNaturalHeight() / 2, /* top */
+ displayInfo.getNaturalWidth(), /* right */
+ displayInfo.getNaturalHeight() /* botom */);
+
+ UdfpsOverlayParams params = new UdfpsOverlayParams(
+ udfpsBounds,
+ overlayBounds,
+ displayInfo.getNaturalWidth(),
+ displayInfo.getNaturalHeight(),
+ scaleFactor,
+ displayInfo.rotation);
+
+ mUdfpsEnrollView.setOverlayParams(params);
+ mUdfpsEnrollView.setEnrollHelper(udfpsEnrollHelper);
+ }
+
+ private void adjustPortraitPaddings() {
+ // In the portrait mode, layout_container's height is 0, so it's
+ // always shown at the bottom of the screen.
+ final FrameLayout portraitLayoutContainer = findViewById(R.id.layout_container);
+
+ // In the portrait mode, the title and lottie animation view may
+ // overlap when title needs three lines, so adding some paddings
+ // between them, and adjusting the fp progress view here accordingly.
+ final int layoutLottieAnimationPadding = (int) getResources()
+ .getDimension(R.dimen.udfps_lottie_padding_top);
+ portraitLayoutContainer.setPadding(0,
+ layoutLottieAnimationPadding, 0, 0);
+ final ImageView progressView = mUdfpsEnrollView.findViewById(
+ R.id.udfps_enroll_animation_fp_progress_view);
+ progressView.setPadding(0, -(layoutLottieAnimationPadding),
+ 0, layoutLottieAnimationPadding);
+ final ImageView fingerprintView = mUdfpsEnrollView.findViewById(
+ R.id.udfps_enroll_animation_fp_view);
+ fingerprintView.setPadding(0, -layoutLottieAnimationPadding,
+ 0, layoutLottieAnimationPadding);
+
+ // TODO(b/260970216) Instead of hiding the description text view, we should
+ // make the header view scrollable if the text is too long.
+ // If description text view has overlap with udfps progress view, hide it.
+ final View descView = getDescriptionTextView();
+ getViewTreeObserver().addOnDrawListener(() -> {
+ if (descView.getVisibility() == View.VISIBLE
+ && hasOverlap(descView, mUdfpsEnrollView)) {
+ descView.setVisibility(View.GONE);
+ }
+ });
+ }
+
+ private void setOnHoverListener() {
+ if (!mAccessibilityManager.isEnabled()) return;
+
+ final View.OnHoverListener onHoverListener = (v, event) -> {
+ // Map the touch to portrait mode if the device is in
+ // landscape mode.
+ final Point scaledTouch =
+ mUdfpsUtils.getTouchInNativeCoordinates(event.getPointerId(0),
+ event, mUdfpsEnrollView.getOverlayParams());
+
+ if (mUdfpsUtils.isWithinSensorArea(event.getPointerId(0), event,
+ mUdfpsEnrollView.getOverlayParams())) {
+ return false;
+ }
+
+ final String theStr = mUdfpsUtils.onTouchOutsideOfSensorArea(
+ mAccessibilityManager.isTouchExplorationEnabled(), mContext,
+ scaledTouch.x, scaledTouch.y, mUdfpsEnrollView.getOverlayParams());
+ if (theStr != null) {
+ v.announceForAccessibility(theStr);
+ }
+ return false;
+ };
+
+ findManagedViewById(mIsLandscape ? R.id.sud_landscape_content_area
+ : R.id.sud_layout_content).setOnHoverListener(onHoverListener);
+ }
+
+ private void swapHeaderAndContent() {
+ // Reverse header and body
+ ViewGroup parentView = (ViewGroup) mHeaderView.getParent();
+ parentView.removeView(mHeaderView);
+ parentView.addView(mHeaderView);
+
+ // Hide scroll indicators
+ BottomScrollView headerScrollView = mHeaderView.findViewById(R.id.sud_header_scroll_view);
+ headerScrollView.setScrollIndicators(0);
+ }
+
+ @VisibleForTesting
+ boolean hasOverlap(View view1, View view2) {
+ int[] firstPosition = new int[2];
+ int[] secondPosition = new int[2];
+
+ view1.getLocationOnScreen(firstPosition);
+ view2.getLocationOnScreen(secondPosition);
+
+ // Rect constructor parameters: left, top, right, bottom
+ Rect rectView1 = new Rect(firstPosition[0], firstPosition[1],
+ firstPosition[0] + view1.getMeasuredWidth(),
+ firstPosition[1] + view1.getMeasuredHeight());
+ Rect rectView2 = new Rect(secondPosition[0], secondPosition[1],
+ secondPosition[0] + view2.getMeasuredWidth(),
+ secondPosition[1] + view2.getMeasuredHeight());
+ return rectView1.intersect(rectView2);
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollProgressBarDrawable.java b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollProgressBarDrawable.java
index aa3f77010bcde0771cf021a9b5b6c68140506fd4..75251cf381fb5bf1244c2bd994ee5561694c6b6e 100644
--- a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollProgressBarDrawable.java
+++ b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollProgressBarDrawable.java
@@ -202,6 +202,7 @@ public class UdfpsEnrollProgressBarDrawable extends Drawable {
return;
}
+ mShowingHelp = showingHelp;
if (mShowingHelp) {
if (mVibrator != null && mIsAccessibilityEnabled) {
mVibrator.vibrate(Process.myUid(), mContext.getOpPackageName(),
@@ -228,7 +229,6 @@ public class UdfpsEnrollProgressBarDrawable extends Drawable {
}
}
- mShowingHelp = showingHelp;
mRemainingSteps = remainingSteps;
mTotalSteps = totalSteps;
diff --git a/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractor.kt b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractor.kt
new file mode 100644
index 0000000000000000000000000000000000000000..2fbdedfc7e4f3a5f914691911f141ecd93b16404
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/domain/interactor/FingerprintManagerInteractor.kt
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2023 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.settings.biometrics.fingerprint2.domain.interactor
+
+import android.content.Context
+import android.content.Intent
+import android.hardware.fingerprint.FingerprintManager
+import android.hardware.fingerprint.FingerprintManager.GenerateChallengeCallback
+import android.hardware.fingerprint.FingerprintManager.RemovalCallback
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import android.os.CancellationSignal
+import android.util.Log
+import com.android.settings.biometrics.GatekeeperPasswordProvider
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settings.password.ChooseLockSettingsHelper
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+import kotlinx.coroutines.CancellableContinuation
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+
+private const val TAG = "FingerprintManagerInteractor"
+
+/** Encapsulates business logic related to managing fingerprints. */
+interface FingerprintManagerInteractor {
+ /** Returns the list of current fingerprints. */
+ val enrolledFingerprints: Flow>
+
+ /** Returns the max enrollable fingerprints, note during SUW this might be 1 */
+ val maxEnrollableFingerprints: Flow
+
+ /** Runs [FingerprintManager.authenticate] */
+ suspend fun authenticate(): FingerprintAuthAttemptViewModel
+
+ /**
+ * Generates a challenge with the provided [gateKeeperPasswordHandle] and on success returns a
+ * challenge and challenge token. This info can be used for secure operations such as
+ * [FingerprintManager.enroll]
+ *
+ * @param gateKeeperPasswordHandle GateKeeper password handle generated by a Confirm
+ * @return A [Pair] of the challenge and challenge token
+ */
+ suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair
+
+ /** Returns true if a user can enroll a fingerprint false otherwise. */
+ fun canEnrollFingerprints(numFingerprints: Int): Flow
+
+ /**
+ * Removes the given fingerprint, returning true if it was successfully removed and false
+ * otherwise
+ */
+ suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean
+
+ /** Renames the given fingerprint if one exists */
+ suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String)
+
+ /** Indicates if the device has side fingerprint */
+ suspend fun hasSideFps(): Boolean
+
+ /** Indicates if the press to auth feature has been enabled */
+ suspend fun pressToAuthEnabled(): Boolean
+
+ /** Retrieves the sensor properties of a device */
+ suspend fun sensorPropertiesInternal(): List
+}
+
+class FingerprintManagerInteractorImpl(
+ applicationContext: Context,
+ private val backgroundDispatcher: CoroutineDispatcher,
+ private val fingerprintManager: FingerprintManager,
+ private val gatekeeperPasswordProvider: GatekeeperPasswordProvider,
+ private val pressToAuthProvider: () -> Boolean,
+) : FingerprintManagerInteractor {
+
+ private val maxFingerprints =
+ applicationContext.resources.getInteger(
+ com.android.internal.R.integer.config_fingerprintMaxTemplatesPerUser
+ )
+ private val applicationContext = applicationContext.applicationContext
+
+ override suspend fun generateChallenge(gateKeeperPasswordHandle: Long): Pair =
+ suspendCoroutine {
+ val callback = GenerateChallengeCallback { _, userId, challenge ->
+ val intent = Intent()
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, gateKeeperPasswordHandle)
+ val challengeToken =
+ gatekeeperPasswordProvider.requestGatekeeperHat(intent, challenge, userId)
+
+ gatekeeperPasswordProvider.removeGatekeeperPasswordHandle(intent, false)
+ val p = Pair(challenge, challengeToken)
+ it.resume(p)
+ }
+ fingerprintManager.generateChallenge(applicationContext.userId, callback)
+ }
+
+ override val enrolledFingerprints: Flow> = flow {
+ emit(
+ fingerprintManager
+ .getEnrolledFingerprints(applicationContext.userId)
+ .map { (FingerprintViewModel(it.name.toString(), it.biometricId, it.deviceId)) }
+ .toList()
+ )
+ }
+
+ override fun canEnrollFingerprints(numFingerprints: Int): Flow = flow {
+ emit(numFingerprints < maxFingerprints)
+ }
+
+ override val maxEnrollableFingerprints = flow { emit(maxFingerprints) }
+
+ override suspend fun removeFingerprint(fp: FingerprintViewModel): Boolean = suspendCoroutine {
+ val callback =
+ object : RemovalCallback() {
+ override fun onRemovalError(
+ fp: android.hardware.fingerprint.Fingerprint,
+ errMsgId: Int,
+ errString: CharSequence
+ ) {
+ it.resume(false)
+ }
+
+ override fun onRemovalSucceeded(
+ fp: android.hardware.fingerprint.Fingerprint?,
+ remaining: Int
+ ) {
+ it.resume(true)
+ }
+ }
+ fingerprintManager.remove(
+ android.hardware.fingerprint.Fingerprint(fp.name, fp.fingerId, fp.deviceId),
+ applicationContext.userId,
+ callback
+ )
+ }
+
+ override suspend fun renameFingerprint(fp: FingerprintViewModel, newName: String) {
+ withContext(backgroundDispatcher) {
+ fingerprintManager.rename(fp.fingerId, applicationContext.userId, newName)
+ }
+ }
+
+ override suspend fun hasSideFps(): Boolean = suspendCancellableCoroutine {
+ it.resume(fingerprintManager.isPowerbuttonFps)
+ }
+
+ override suspend fun pressToAuthEnabled(): Boolean = suspendCancellableCoroutine {
+ it.resume(pressToAuthProvider())
+ }
+
+ override suspend fun sensorPropertiesInternal(): List =
+ suspendCancellableCoroutine {
+ it.resume(fingerprintManager.sensorPropertiesInternal)
+ }
+
+ override suspend fun authenticate(): FingerprintAuthAttemptViewModel =
+ suspendCancellableCoroutine { c: CancellableContinuation ->
+ val authenticationCallback =
+ object : FingerprintManager.AuthenticationCallback() {
+
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
+ super.onAuthenticationError(errorCode, errString)
+ if (c.isCompleted) {
+ Log.d(TAG, "framework sent down onAuthError after finish")
+ return
+ }
+ c.resume(FingerprintAuthAttemptViewModel.Error(errorCode, errString.toString()))
+ }
+
+ override fun onAuthenticationSucceeded(result: FingerprintManager.AuthenticationResult) {
+ super.onAuthenticationSucceeded(result)
+ if (c.isCompleted) {
+ Log.d(TAG, "framework sent down onAuthError after finish")
+ return
+ }
+ c.resume(FingerprintAuthAttemptViewModel.Success(result.fingerprint?.biometricId ?: -1))
+ }
+ }
+
+ val cancellationSignal = CancellationSignal()
+ c.invokeOnCancellation { cancellationSignal.cancel() }
+ fingerprintManager.authenticate(
+ null,
+ cancellationSignal,
+ authenticationCallback,
+ null,
+ applicationContext.userId
+ )
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/binder/FingerprintSettingsViewBinder.kt b/src/com/android/settings/biometrics/fingerprint2/ui/binder/FingerprintSettingsViewBinder.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d9f3e43fa6fdbd4825926ba277151a3f308c55da
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/binder/FingerprintSettingsViewBinder.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2023 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.settings.biometrics.fingerprint2.ui.binder
+
+import android.hardware.fingerprint.FingerprintManager
+import android.util.Log
+import androidx.lifecycle.LifecycleCoroutineScope
+import com.android.settings.biometrics.fingerprint2.ui.binder.FingerprintSettingsViewBinder.FingerprintView
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollAdditionalFingerprint
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.EnrollFirstFingerprint
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsNavigationViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintStateViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettings
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FinishSettingsWithResult
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchConfirmDeviceCredential
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.LaunchedActivity
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.PreferenceViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.ShowSettings
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.launch
+
+private const val TAG = "FingerprintSettingsViewBinder"
+
+/** Binds a [FingerprintSettingsViewModel] to a [FingerprintView] */
+object FingerprintSettingsViewBinder {
+
+ interface FingerprintView {
+ /**
+ * Helper function to launch fingerprint enrollment(This should be the default behavior when a
+ * user enters their PIN/PATTERN/PASS and no fingerprints are enrolled).
+ */
+ fun launchFullFingerprintEnrollment(
+ userId: Int,
+ gateKeeperPasswordHandle: Long?,
+ challenge: Long?,
+ challengeToken: ByteArray?
+ )
+
+ /** Helper to launch an add fingerprint request */
+ fun launchAddFingerprint(userId: Int, challengeToken: ByteArray?)
+ /**
+ * Helper function that will try and launch confirm lock, if that fails we will prompt user to
+ * choose a PIN/PATTERN/PASS.
+ */
+ fun launchConfirmOrChooseLock(userId: Int)
+
+ /** Used to indicate that FingerprintSettings is finished. */
+ fun finish()
+
+ /** Indicates what result should be set for the returning callee */
+ fun setResultExternal(resultCode: Int)
+ /** Indicates the settings UI should be shown */
+ fun showSettings(state: FingerprintStateViewModel)
+ /** Indicates that a user has been locked out */
+ fun userLockout(authAttemptViewModel: FingerprintAuthAttemptViewModel.Error)
+ /** Indicates a fingerprint preference should be highlighted */
+ suspend fun highlightPref(fingerId: Int)
+ /** Indicates a user should be prompted to delete a fingerprint */
+ suspend fun askUserToDeleteDialog(fingerprintViewModel: FingerprintViewModel): Boolean
+ /** Indicates a user should be asked to renae ma dialog */
+ suspend fun askUserToRenameDialog(
+ fingerprintViewModel: FingerprintViewModel
+ ): Pair?
+ }
+
+ fun bind(
+ view: FingerprintView,
+ viewModel: FingerprintSettingsViewModel,
+ navigationViewModel: FingerprintSettingsNavigationViewModel,
+ lifecycleScope: LifecycleCoroutineScope,
+ ) {
+
+ /** Result listener for launching enrollments **after** a user has reached the settings page. */
+
+ // Settings display flow
+ lifecycleScope.launch {
+ viewModel.fingerprintState.filterNotNull().collect { view.showSettings(it) }
+ }
+
+ // Dialog flow
+ lifecycleScope.launch {
+ viewModel.isShowingDialog.collectLatest {
+ if (it == null) {
+ return@collectLatest
+ }
+ when (it) {
+ is PreferenceViewModel.RenameDialog -> {
+ val willRename = view.askUserToRenameDialog(it.fingerprintViewModel)
+ if (willRename != null) {
+ Log.d(TAG, "renaming fingerprint $it")
+ viewModel.renameFingerprint(willRename.first, willRename.second)
+ }
+ viewModel.onRenameDialogFinished()
+ }
+ is PreferenceViewModel.DeleteDialog -> {
+ if (view.askUserToDeleteDialog(it.fingerprintViewModel)) {
+ Log.d(TAG, "deleting fingerprint $it")
+ viewModel.deleteFingerprint(it.fingerprintViewModel)
+ }
+ viewModel.onDeleteDialogFinished()
+ }
+ }
+ }
+ }
+
+ // Auth flow
+ lifecycleScope.launch {
+ viewModel.authFlow.filterNotNull().collect {
+ when (it) {
+ is FingerprintAuthAttemptViewModel.Success -> {
+ view.highlightPref(it.fingerId)
+ }
+ is FingerprintAuthAttemptViewModel.Error -> {
+ if (it.error == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT) {
+ view.userLockout(it)
+ }
+ }
+ }
+ }
+ }
+
+ // Launch this on Dispatchers.Default and not main.
+ // Otherwise it takes too long for state transitions such as PIN/PATTERN/PASS
+ // to enrollment, which makes gives the user a janky experience.
+ lifecycleScope.launch(Dispatchers.Default) {
+ var settingsShowingJob: Job? = null
+ navigationViewModel.nextStep.filterNotNull().collect { nextStep ->
+ settingsShowingJob?.cancel()
+ settingsShowingJob = null
+ Log.d(TAG, "next step = $nextStep")
+ when (nextStep) {
+ is EnrollFirstFingerprint ->
+ view.launchFullFingerprintEnrollment(
+ nextStep.userId,
+ nextStep.gateKeeperPasswordHandle,
+ nextStep.challenge,
+ nextStep.challengeToken
+ )
+ is EnrollAdditionalFingerprint ->
+ view.launchAddFingerprint(nextStep.userId, nextStep.challengeToken)
+ is LaunchConfirmDeviceCredential -> view.launchConfirmOrChooseLock(nextStep.userId)
+ is FinishSettings -> {
+ Log.d(TAG, "Finishing due to ${nextStep.reason}")
+ view.finish()
+ }
+ is FinishSettingsWithResult -> {
+ Log.d(TAG, "Finishing with result ${nextStep.result} due to ${nextStep.reason}")
+ view.setResultExternal(nextStep.result)
+ view.finish()
+ }
+ is ShowSettings -> Log.d(TAG, "Showing settings")
+ is LaunchedActivity -> Log.d(TAG, "Launched activity, awaiting result")
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintDeletionDialog.kt b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintDeletionDialog.kt
new file mode 100644
index 0000000000000000000000000000000000000000..42e20477acceabff3c14147744a0eb2685d93c4c
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintDeletionDialog.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2023 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.settings.biometrics.fingerprint2.ui.fragment
+
+import android.app.Dialog
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_FINGERPRINT_LAST_DELETE_MESSAGE
+import android.app.admin.DevicePolicyResources.UNDEFINED
+import android.app.settings.SettingsEnums
+import android.content.DialogInterface
+import android.os.Bundle
+import android.os.UserManager
+import androidx.appcompat.app.AlertDialog
+import com.android.settings.R
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment
+import kotlin.coroutines.resume
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+private const val KEY_IS_LAST_FINGERPRINT = "IS_LAST_FINGERPRINT"
+
+class FingerprintDeletionDialog : InstrumentedDialogFragment() {
+ private lateinit var fingerprintViewModel: FingerprintViewModel
+ private var isLastFingerprint: Boolean = false
+ private lateinit var alertDialog: AlertDialog
+ lateinit var onClickListener: DialogInterface.OnClickListener
+ lateinit var onNegativeClickListener: DialogInterface.OnClickListener
+ lateinit var onCancelListener: DialogInterface.OnCancelListener
+
+ override fun getMetricsCategory(): Int {
+ return SettingsEnums.DIALOG_FINGERPINT_EDIT
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ onCancelListener.onCancel(dialog)
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val fp = requireArguments().get(KEY_FINGERPRINT) as android.hardware.fingerprint.Fingerprint
+ fingerprintViewModel = FingerprintViewModel(fp.name.toString(), fp.biometricId, fp.deviceId)
+ isLastFingerprint = requireArguments().getBoolean(KEY_IS_LAST_FINGERPRINT)
+ val title = getString(R.string.fingerprint_delete_title, fingerprintViewModel.name)
+ var message = getString(R.string.fingerprint_v2_delete_message, fingerprintViewModel.name)
+ val context = requireContext()
+
+ if (isLastFingerprint) {
+ val isProfileChallengeUser = UserManager.get(context).isManagedProfile(context.userId)
+ val messageId =
+ if (isProfileChallengeUser) {
+ WORK_PROFILE_FINGERPRINT_LAST_DELETE_MESSAGE
+ } else {
+ UNDEFINED
+ }
+ val defaultMessageId =
+ if (isProfileChallengeUser) {
+ R.string.fingerprint_last_delete_message_profile_challenge
+ } else {
+ R.string.fingerprint_last_delete_message
+ }
+ val devicePolicyManager = requireContext().getSystemService(DevicePolicyManager::class.java)
+ message =
+ devicePolicyManager?.resources?.getString(messageId) {
+ message + "\n\n" + context.getString(defaultMessageId)
+ }
+ ?: ""
+ }
+
+ alertDialog =
+ AlertDialog.Builder(requireActivity())
+ .setTitle(title)
+ .setMessage(message)
+ .setPositiveButton(
+ R.string.security_settings_fingerprint_enroll_dialog_delete,
+ onClickListener
+ )
+ .setNegativeButton(R.string.cancel, onNegativeClickListener)
+ .create()
+ return alertDialog
+ }
+
+ companion object {
+ private const val KEY_FINGERPRINT = "fingerprint"
+ suspend fun showInstance(
+ fp: FingerprintViewModel,
+ lastFingerprint: Boolean,
+ target: FingerprintSettingsV2Fragment,
+ ) = suspendCancellableCoroutine { continuation ->
+ val dialog = FingerprintDeletionDialog()
+ dialog.onClickListener = DialogInterface.OnClickListener { _, _ -> continuation.resume(true) }
+ dialog.onNegativeClickListener =
+ DialogInterface.OnClickListener { _, _ -> continuation.resume(false) }
+ dialog.onCancelListener = DialogInterface.OnCancelListener { continuation.resume(false) }
+
+ continuation.invokeOnCancellation { dialog.dismiss() }
+ val bundle = Bundle()
+ bundle.putObject(
+ KEY_FINGERPRINT,
+ android.hardware.fingerprint.Fingerprint(fp.name, fp.fingerId, fp.deviceId)
+ )
+ bundle.putBoolean(KEY_IS_LAST_FINGERPRINT, lastFingerprint)
+ dialog.arguments = bundle
+ dialog.show(target.parentFragmentManager, FingerprintDeletionDialog::class.java.toString())
+ }
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsPreference.kt b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsPreference.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e12785d12f0d42a0c5cafaba6eb8d932123742aa
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsPreference.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2023 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.settings.biometrics.fingerprint2.ui.fragment
+
+import android.content.Context
+import android.util.Log
+import android.view.View
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.PreferenceViewHolder
+import com.android.settings.R
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settingslib.widget.TwoTargetPreference
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+private const val TAG = "FingerprintSettingsPreference"
+
+class FingerprintSettingsPreference(
+ context: Context,
+ val fingerprintViewModel: FingerprintViewModel,
+ val fragment: FingerprintSettingsV2Fragment,
+ val isLastFingerprint: Boolean
+) : TwoTargetPreference(context) {
+ private lateinit var myView: View
+
+ init {
+ key = "FINGERPRINT_" + fingerprintViewModel.fingerId
+ Log.d(TAG, "FingerprintPreference $this with frag $fragment $key")
+ title = fingerprintViewModel.name
+ isPersistent = false
+ setIcon(R.drawable.ic_fingerprint_24dp)
+ setOnPreferenceClickListener {
+ fragment.lifecycleScope.launch { fragment.onPrefClicked(fingerprintViewModel) }
+ true
+ }
+ }
+
+ override fun onBindViewHolder(view: PreferenceViewHolder) {
+ super.onBindViewHolder(view)
+ myView = view.itemView
+ view.itemView.findViewById(R.id.delete_button)?.setOnClickListener {
+ fragment.lifecycleScope.launch { fragment.onDeletePrefClicked(fingerprintViewModel) }
+ }
+ }
+
+ /** Highlights this dialog. */
+ suspend fun highlight() {
+ fragment.activity?.getDrawable(R.drawable.preference_highlight)?.let { highlight ->
+ val centerX: Float = myView.width / 2.0f
+ val centerY: Float = myView.height / 2.0f
+ highlight.setHotspot(centerX, centerY)
+ myView.background = highlight
+ myView.isPressed = true
+ myView.isPressed = false
+ delay(300)
+ myView.background = null
+ }
+ }
+
+ override fun getSecondTargetResId(): Int {
+ return R.layout.preference_widget_delete
+ }
+
+ suspend fun askUserToDeleteDialog(): Boolean {
+ return FingerprintDeletionDialog.showInstance(fingerprintViewModel, isLastFingerprint, fragment)
+ }
+
+ suspend fun askUserToRenameDialog(): Pair? {
+ return FingerprintSettingsRenameDialog.showInstance(fingerprintViewModel, fragment)
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsRenameDialog.kt b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsRenameDialog.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9542ed83371dba193e7014aece0a217ee552a93e
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsRenameDialog.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2023 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.settings.biometrics.fingerprint2.ui.fragment
+
+import android.app.Dialog
+import android.app.settings.SettingsEnums
+import android.content.DialogInterface
+import android.os.Bundle
+import android.text.InputFilter
+import android.text.Spanned
+import android.text.TextUtils
+import android.util.Log
+import android.widget.ImeAwareEditText
+import androidx.appcompat.app.AlertDialog
+import com.android.settings.R
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment
+import kotlin.coroutines.resume
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+private const val TAG = "FingerprintSettingsRenameDialog"
+
+class FingerprintSettingsRenameDialog : InstrumentedDialogFragment() {
+ lateinit var onClickListener: DialogInterface.OnClickListener
+ lateinit var onCancelListener: DialogInterface.OnCancelListener
+
+ override fun onCancel(dialog: DialogInterface) {
+ Log.d(TAG, "onCancel $dialog")
+ onCancelListener.onCancel(dialog)
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ Log.d(TAG, "onCreateDialog $this")
+ val fp = requireArguments().get(KEY_FINGERPRINT) as android.hardware.fingerprint.Fingerprint
+ val fingerprintViewModel = FingerprintViewModel(fp.name.toString(), fp.biometricId, fp.deviceId)
+
+ val context = requireContext()
+ val alertDialog =
+ AlertDialog.Builder(context)
+ .setView(R.layout.fingerprint_rename_dialog)
+ .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok, onClickListener)
+ .create()
+ alertDialog.setOnShowListener {
+ (dialog?.findViewById(R.id.fingerprint_rename_field) as ImeAwareEditText?)?.apply {
+ val name = fingerprintViewModel.name
+ setText(name)
+ filters = this@FingerprintSettingsRenameDialog.getFilters()
+ selectAll()
+ requestFocus()
+ scheduleShowSoftInput()
+ }
+ }
+
+ return alertDialog
+ }
+
+ private fun getFilters(): Array {
+ val filter: InputFilter =
+ object : InputFilter {
+
+ override fun filter(
+ source: CharSequence,
+ start: Int,
+ end: Int,
+ dest: Spanned?,
+ dstart: Int,
+ dend: Int
+ ): CharSequence? {
+ for (index in start until end) {
+ val c = source[index]
+ // KXMLSerializer does not allow these characters,
+ // see KXmlSerializer.java:162.
+ if (c.code < 0x20) {
+ return ""
+ }
+ }
+ return null
+ }
+ }
+ return arrayOf(filter)
+ }
+
+ override fun getMetricsCategory(): Int {
+ return SettingsEnums.DIALOG_FINGERPINT_EDIT
+ }
+
+ companion object {
+ private const val KEY_FINGERPRINT = "fingerprint"
+
+ suspend fun showInstance(fp: FingerprintViewModel, target: FingerprintSettingsV2Fragment) =
+ suspendCancellableCoroutine { continuation ->
+ val dialog = FingerprintSettingsRenameDialog()
+ val onClick =
+ DialogInterface.OnClickListener { _, _ ->
+ val dialogTextField = dialog.requireDialog()
+ .requireViewById(R.id.fingerprint_rename_field) as ImeAwareEditText
+ val newName = dialogTextField.text.toString()
+ if (!TextUtils.equals(newName, fp.name)) {
+ Log.d(TAG, "rename $fp.name to $newName for $dialog")
+ continuation.resume(Pair(fp, newName))
+ } else {
+ continuation.resume(null)
+ }
+ }
+
+ dialog.onClickListener = onClick
+ dialog.onCancelListener =
+ DialogInterface.OnCancelListener {
+ Log.d(TAG, "onCancelListener clicked $dialog")
+ continuation.resume(null)
+ }
+
+ continuation.invokeOnCancellation {
+ Log.d(TAG, "invokeOnCancellation $dialog")
+ dialog.dismiss()
+ }
+
+ val bundle = Bundle()
+ bundle.putObject(
+ KEY_FINGERPRINT,
+ android.hardware.fingerprint.Fingerprint(fp.name, fp.fingerId, fp.deviceId)
+ )
+ dialog.arguments = bundle
+ Log.d(TAG, "showing dialog $dialog")
+ dialog.show(
+ target.parentFragmentManager,
+ FingerprintSettingsRenameDialog::class.java.toString()
+ )
+ }
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsV2Fragment.kt b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsV2Fragment.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b82f7c1aec375c89dc3f68e23d3e239b3e3b2e88
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/fragment/FingerprintSettingsV2Fragment.kt
@@ -0,0 +1,581 @@
+/*
+ * Copyright (C) 2023 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.settings.biometrics.fingerprint2.ui.fragment
+
+import android.app.Activity
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResources.Strings.Settings.FINGERPRINT_UNLOCK_DISABLED_EXPLANATION
+import android.app.settings.SettingsEnums
+import android.content.Context.FINGERPRINT_SERVICE
+import android.content.Intent
+import android.hardware.fingerprint.FingerprintManager
+import android.os.Bundle
+import android.provider.Settings.Secure
+import android.text.TextUtils
+import android.util.FeatureFlagUtils
+import android.util.Log
+import android.view.View
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.Preference
+import androidx.preference.PreferenceCategory
+import com.android.internal.widget.LockPatternUtils
+import com.android.settings.R
+import com.android.settings.Utils.SETTINGS_PACKAGE_NAME
+import com.android.settings.biometrics.BiometricEnrollBase
+import com.android.settings.biometrics.BiometricEnrollBase.CONFIRM_REQUEST
+import com.android.settings.biometrics.BiometricEnrollBase.EXTRA_FROM_SETTINGS_SUMMARY
+import com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHED
+import com.android.settings.biometrics.GatekeeperPasswordProvider
+import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling
+import com.android.settings.biometrics.fingerprint.FingerprintEnrollIntroductionInternal
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractorImpl
+import com.android.settings.biometrics.fingerprint2.ui.binder.FingerprintSettingsViewBinder
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintAuthAttemptViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsNavigationViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintSettingsViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintStateViewModel
+import com.android.settings.biometrics.fingerprint2.ui.viewmodel.FingerprintViewModel
+import com.android.settings.core.SettingsBaseActivity
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment
+import com.android.settings.dashboard.DashboardFragment
+import com.android.settings.password.ChooseLockGeneric
+import com.android.settings.password.ChooseLockSettingsHelper
+import com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE
+import com.android.settingslib.HelpUtils
+import com.android.settingslib.RestrictedLockUtils
+import com.android.settingslib.RestrictedLockUtilsInternal
+import com.android.settingslib.transition.SettingsTransitionHelper
+import com.android.settingslib.widget.FooterPreference
+import com.google.android.setupdesign.util.DeviceHelper
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+private const val TAG = "FingerprintSettingsV2Fragment"
+private const val KEY_FINGERPRINTS_ENROLLED_CATEGORY = "security_settings_fingerprints_enrolled"
+private const val KEY_FINGERPRINT_SIDE_FPS_CATEGORY =
+ "security_settings_fingerprint_unlock_category"
+private const val KEY_FINGERPRINT_ADD = "key_fingerprint_add"
+private const val KEY_FINGERPRINT_SIDE_FPS_SCREEN_ON_TO_AUTH =
+ "security_settings_require_screen_on_to_auth"
+private const val KEY_FINGERPRINT_FOOTER = "security_settings_fingerprint_footer"
+
+/**
+ * A class responsible for showing FingerprintSettings. Typical activity Flows are
+ * 1. Settings > FingerprintSettings > PIN/PATTERN/PASS -> FingerprintSettings
+ * 2. FingerprintSettings -> FingerprintEnrollment fow
+ *
+ * This page typically allows for
+ * 1. Fingerprint deletion
+ * 2. Fingerprint enrollment
+ * 3. Renaming a fingerprint
+ * 4. Enabling/Disabling a feature
+ */
+class FingerprintSettingsV2Fragment :
+ DashboardFragment(), FingerprintSettingsViewBinder.FingerprintView {
+ private lateinit var settingsViewModel: FingerprintSettingsViewModel
+ private lateinit var navigationViewModel: FingerprintSettingsNavigationViewModel
+
+ /** Result listener for ChooseLock activity flow. */
+ private val confirmDeviceResultListener =
+ registerForActivityResult(StartActivityForResult()) { result ->
+ val resultCode = result.resultCode
+ val data = result.data
+ onConfirmDevice(resultCode, data)
+ }
+
+ /** Result listener for launching enrollments **after** a user has reached the settings page. */
+ private val launchAdditionalFingerprintListener: ActivityResultLauncher =
+ registerForActivityResult(StartActivityForResult()) { result ->
+ lifecycleScope.launch {
+ val resultCode = result.resultCode
+ Log.d(TAG, "onEnrollAdditionalFingerprint($resultCode)")
+
+ if (resultCode == BiometricEnrollBase.RESULT_TIMEOUT) {
+ navigationViewModel.onEnrollAdditionalFailure()
+ } else {
+ navigationViewModel.onEnrollSuccess()
+ }
+ }
+ }
+
+ /** Initial listener for the first enrollment request */
+ private val launchFirstEnrollmentListener: ActivityResultLauncher =
+ registerForActivityResult(StartActivityForResult()) { result ->
+ lifecycleScope.launch {
+ val resultCode = result.resultCode
+ val data = result.data
+
+ Log.d(TAG, "onEnrollFirstFingerprint($resultCode, $data)")
+ if (resultCode != RESULT_FINISHED || data == null) {
+ if (resultCode == BiometricEnrollBase.RESULT_TIMEOUT) {
+ navigationViewModel.onEnrollFirstFailure(
+ "Received RESULT_TIMEOUT when enrolling",
+ resultCode
+ )
+ } else {
+ navigationViewModel.onEnrollFirstFailure(
+ "Incorrect resultCode or data was null",
+ resultCode
+ )
+ }
+ } else {
+ val token = data.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
+ val challenge = data.getExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE) as Long?
+ navigationViewModel.onEnrollFirst(token, challenge)
+ }
+ }
+ }
+
+ override fun userLockout(authAttemptViewModel: FingerprintAuthAttemptViewModel.Error) {
+ Toast.makeText(activity, authAttemptViewModel.message, Toast.LENGTH_SHORT).show()
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ // This is needed to support ChooseLockSettingBuilder...show(). All other activity
+ // calls should use the registerForActivity method call.
+ super.onActivityResult(requestCode, resultCode, data)
+ onConfirmDevice(resultCode, data)
+ }
+
+ override fun onCreate(icicle: Bundle?) {
+ super.onCreate(icicle)
+
+ if (icicle != null) {
+ Log.d(TAG, "onCreateWithSavedState")
+ } else {
+ Log.d(TAG, "onCreate()")
+ }
+
+ if (
+ !FeatureFlagUtils.isEnabled(
+ context,
+ FeatureFlagUtils.SETTINGS_BIOMETRICS2_FINGERPRINT_SETTINGS
+ )
+ ) {
+ Log.d(TAG, "Finishing due to feature not being enabled")
+ finish()
+ return
+ }
+
+ val context = requireContext()
+ val userId = context.userId
+
+ preferenceScreen.isVisible = false
+
+ val fingerprintManager = context.getSystemService(FINGERPRINT_SERVICE) as FingerprintManager
+
+ val backgroundDispatcher = Dispatchers.IO
+ val activity = requireActivity()
+ val userHandle = activity.user.identifier
+
+ val interactor =
+ FingerprintManagerInteractorImpl(
+ context.applicationContext,
+ backgroundDispatcher,
+ fingerprintManager,
+ GatekeeperPasswordProvider(LockPatternUtils(context.applicationContext))
+ ) {
+ var toReturn: Int =
+ Secure.getIntForUser(
+ context.contentResolver,
+ Secure.SFPS_PERFORMANT_AUTH_ENABLED,
+ -1,
+ userHandle,
+ )
+ if (toReturn == -1) {
+ toReturn =
+ if (
+ context.resources.getBoolean(com.android.internal.R.bool.config_performantAuthDefault)
+ ) {
+ 1
+ } else {
+ 0
+ }
+ Secure.putIntForUser(
+ context.contentResolver,
+ Secure.SFPS_PERFORMANT_AUTH_ENABLED,
+ toReturn,
+ userHandle
+ )
+ }
+
+ toReturn == 1
+ }
+
+ val token = intent.getByteArrayExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN)
+ val challenge = intent.getLongExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, -1L)
+
+ navigationViewModel =
+ ViewModelProvider(
+ this,
+ FingerprintSettingsNavigationViewModel.FingerprintSettingsNavigationModelFactory(
+ userId,
+ interactor,
+ backgroundDispatcher,
+ token,
+ challenge
+ )
+ )[FingerprintSettingsNavigationViewModel::class.java]
+
+ settingsViewModel =
+ ViewModelProvider(
+ this,
+ FingerprintSettingsViewModel.FingerprintSettingsViewModelFactory(
+ userId,
+ interactor,
+ backgroundDispatcher,
+ navigationViewModel,
+ )
+ )[FingerprintSettingsViewModel::class.java]
+
+ FingerprintSettingsViewBinder.bind(
+ this,
+ settingsViewModel,
+ navigationViewModel,
+ lifecycleScope,
+ )
+ }
+
+ override fun getMetricsCategory(): Int {
+ return SettingsEnums.FINGERPRINT
+ }
+
+ override fun getPreferenceScreenResId(): Int {
+ return R.xml.security_settings_fingerprint_limbo
+ }
+
+ override fun getLogTag(): String {
+ return TAG
+ }
+
+ override fun onStop() {
+ super.onStop()
+ navigationViewModel.maybeFinishActivity(requireActivity().isChangingConfigurations)
+ }
+
+ override fun onPause() {
+ super.onPause()
+ settingsViewModel.shouldAuthenticate(false)
+ val transaction = parentFragmentManager.beginTransaction()
+ for (frag in parentFragmentManager.fragments) {
+ if (frag is InstrumentedDialogFragment) {
+ Log.d(TAG, "removing dialog settings fragment $frag")
+ frag.dismiss()
+ transaction.remove(frag)
+ }
+ }
+ transaction.commit()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ settingsViewModel.shouldAuthenticate(true)
+ }
+
+ /** Used to indicate that preference has been clicked */
+ fun onPrefClicked(fingerprintViewModel: FingerprintViewModel) {
+ Log.d(TAG, "onPrefClicked(${fingerprintViewModel})")
+ settingsViewModel.onPrefClicked(fingerprintViewModel)
+ }
+
+ /** Used to indicate that a delete pref has been clicked */
+ fun onDeletePrefClicked(fingerprintViewModel: FingerprintViewModel) {
+ Log.d(TAG, "onDeletePrefClicked(${fingerprintViewModel})")
+ settingsViewModel.onDeleteClicked(fingerprintViewModel)
+ }
+
+ override fun showSettings(state: FingerprintStateViewModel) {
+ val category =
+ this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINTS_ENROLLED_CATEGORY)
+ as PreferenceCategory?
+
+ category?.removeAll()
+
+ state.fingerprintViewModels.forEach { fingerprint ->
+ category?.addPreference(
+ FingerprintSettingsPreference(
+ requireContext(),
+ fingerprint,
+ this@FingerprintSettingsV2Fragment,
+ state.fingerprintViewModels.size == 1,
+ )
+ )
+ }
+ category?.isVisible = true
+
+ createFingerprintsFooterPreference(state.canEnroll, state.maxFingerprints)
+ preferenceScreen.isVisible = true
+
+ val sideFpsPref =
+ this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINT_SIDE_FPS_CATEGORY)
+ as PreferenceCategory?
+ sideFpsPref?.isVisible = false
+
+ if (state.hasSideFps) {
+ sideFpsPref?.isVisible = state.fingerprintViewModels.isNotEmpty()
+ val otherPref =
+ this@FingerprintSettingsV2Fragment.findPreference(
+ KEY_FINGERPRINT_SIDE_FPS_SCREEN_ON_TO_AUTH
+ ) as Preference?
+ otherPref?.isVisible = state.fingerprintViewModels.isNotEmpty()
+ }
+ addFooter(state.hasSideFps)
+ }
+ private fun addFooter(hasSideFps: Boolean) {
+ val footer =
+ this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINT_FOOTER)
+ as PreferenceCategory?
+ val admin =
+ RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled(
+ activity,
+ DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT,
+ requireActivity().userId
+ )
+ val activity = requireActivity()
+ val helpIntent =
+ HelpUtils.getHelpIntent(activity, getString(helpResource), activity::class.java.name)
+ val learnMoreClickListener =
+ View.OnClickListener { v: View? -> activity.startActivityForResult(helpIntent, 0) }
+
+ class FooterColumn {
+ var title: CharSequence? = null
+ var learnMoreOverrideText: CharSequence? = null
+ var learnMoreOnClickListener: View.OnClickListener? = null
+ }
+
+ var footerColumns = mutableListOf()
+ if (admin != null) {
+ val devicePolicyManager = getSystemService(DevicePolicyManager::class.java)
+ val column1 = FooterColumn()
+ column1.title =
+ devicePolicyManager.resources.getString(FINGERPRINT_UNLOCK_DISABLED_EXPLANATION) {
+ getString(R.string.security_fingerprint_disclaimer_lockscreen_disabled_1)
+ }
+
+ column1.learnMoreOnClickListener =
+ View.OnClickListener { _ ->
+ RestrictedLockUtils.sendShowAdminSupportDetailsIntent(activity, admin)
+ }
+ column1.learnMoreOverrideText = getText(R.string.admin_support_more_info)
+ footerColumns.add(column1)
+ val column2 = FooterColumn()
+ column2.title = getText(R.string.security_fingerprint_disclaimer_lockscreen_disabled_2)
+ if (hasSideFps) {
+ column2.learnMoreOverrideText =
+ getText(R.string.security_settings_fingerprint_settings_footer_learn_more)
+ }
+ column2.learnMoreOnClickListener = learnMoreClickListener
+ footerColumns.add(column2)
+ } else {
+ val column = FooterColumn()
+ column.title =
+ getString(
+ R.string.security_settings_fingerprint_enroll_introduction_v3_message,
+ DeviceHelper.getDeviceName(requireActivity())
+ )
+ column.learnMoreOnClickListener = learnMoreClickListener
+ if (hasSideFps) {
+ column.learnMoreOverrideText =
+ getText(R.string.security_settings_fingerprint_settings_footer_learn_more)
+ }
+ footerColumns.add(column)
+ }
+
+ footer?.removeAll()
+ for (i in 0 until footerColumns.size) {
+ val column = footerColumns[i]
+ val footerPrefToAdd: FooterPreference =
+ FooterPreference.Builder(requireContext()).setTitle(column.title).build()
+ if (i > 0) {
+ footerPrefToAdd.setIconVisibility(View.GONE)
+ }
+ if (column.learnMoreOnClickListener != null) {
+ footerPrefToAdd.setLearnMoreAction(column.learnMoreOnClickListener)
+ if (!TextUtils.isEmpty(column.learnMoreOverrideText)) {
+ footerPrefToAdd.setLearnMoreText(column.learnMoreOverrideText)
+ }
+ }
+ footer?.addPreference(footerPrefToAdd)
+ }
+ }
+
+ override suspend fun askUserToDeleteDialog(fingerprintViewModel: FingerprintViewModel): Boolean {
+ Log.d(TAG, "showing delete dialog for (${fingerprintViewModel})")
+
+ try {
+ val willDelete =
+ fingerprintPreferences()
+ .first { it?.fingerprintViewModel == fingerprintViewModel }
+ ?.askUserToDeleteDialog()
+ ?: false
+ if (willDelete) {
+ mMetricsFeatureProvider.action(
+ context,
+ SettingsEnums.ACTION_FINGERPRINT_DELETE,
+ fingerprintViewModel.fingerId
+ )
+ }
+ return willDelete
+ } catch (exception: Exception) {
+ Log.d(TAG, "askUserToDeleteDialog exception $exception")
+ return false
+ }
+ }
+
+ override suspend fun askUserToRenameDialog(
+ fingerprintViewModel: FingerprintViewModel
+ ): Pair? {
+ Log.d(TAG, "showing rename dialog for (${fingerprintViewModel})")
+ try {
+ val toReturn =
+ fingerprintPreferences()
+ .first { it?.fingerprintViewModel == fingerprintViewModel }
+ ?.askUserToRenameDialog()
+ if (toReturn != null) {
+ mMetricsFeatureProvider.action(
+ context,
+ SettingsEnums.ACTION_FINGERPRINT_RENAME,
+ toReturn.first.fingerId
+ )
+ }
+ return toReturn
+ } catch (exception: Exception) {
+ Log.d(TAG, "askUserToRenameDialog exception $exception")
+ return null
+ }
+ }
+
+ override suspend fun highlightPref(fingerId: Int) {
+ fingerprintPreferences()
+ .first { pref -> pref?.fingerprintViewModel?.fingerId == fingerId }
+ ?.highlight()
+ }
+
+ override fun launchConfirmOrChooseLock(userId: Int) {
+ lifecycleScope.launch(Dispatchers.Default) {
+ navigationViewModel.setStepToLaunched()
+ val intent = Intent()
+ val builder =
+ ChooseLockSettingsHelper.Builder(requireActivity(), this@FingerprintSettingsV2Fragment)
+ val launched =
+ builder
+ .setRequestCode(CONFIRM_REQUEST)
+ .setTitle(getString(R.string.security_settings_fingerprint_preference_title))
+ .setRequestGatekeeperPasswordHandle(true)
+ .setUserId(userId)
+ .setForegroundOnly(true)
+ .setReturnCredentials(true)
+ .show()
+ if (!launched) {
+ intent.setClassName(SETTINGS_PACKAGE_NAME, ChooseLockGeneric::class.java.name)
+ intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, true)
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true)
+ intent.putExtra(Intent.EXTRA_USER_ID, userId)
+ confirmDeviceResultListener.launch(intent)
+ }
+ }
+ }
+
+ override fun launchFullFingerprintEnrollment(
+ userId: Int,
+ gateKeeperPasswordHandle: Long?,
+ challenge: Long?,
+ challengeToken: ByteArray?,
+ ) {
+ navigationViewModel.setStepToLaunched()
+ Log.d(TAG, "launchFullFingerprintEnrollment")
+ val intent = Intent()
+ intent.setClassName(
+ SETTINGS_PACKAGE_NAME,
+ FingerprintEnrollIntroductionInternal::class.java.name
+ )
+ intent.putExtra(EXTRA_FROM_SETTINGS_SUMMARY, true)
+ intent.putExtra(
+ SettingsBaseActivity.EXTRA_PAGE_TRANSITION_TYPE,
+ SettingsTransitionHelper.TransitionType.TRANSITION_SLIDE
+ )
+
+ intent.putExtra(Intent.EXTRA_USER_ID, userId)
+
+ if (gateKeeperPasswordHandle != null) {
+ intent.putExtra(EXTRA_KEY_GK_PW_HANDLE, gateKeeperPasswordHandle)
+ } else {
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, challengeToken)
+ intent.putExtra(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, challenge)
+ }
+ launchFirstEnrollmentListener.launch(intent)
+ }
+
+ override fun setResultExternal(resultCode: Int) {
+ setResult(resultCode)
+ }
+
+ override fun launchAddFingerprint(userId: Int, challengeToken: ByteArray?) {
+ navigationViewModel.setStepToLaunched()
+ val intent = Intent()
+ intent.setClassName(
+ SETTINGS_PACKAGE_NAME,
+ FingerprintEnrollEnrolling::class.qualifiedName.toString()
+ )
+ intent.putExtra(Intent.EXTRA_USER_ID, userId)
+ intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, challengeToken)
+ launchAdditionalFingerprintListener.launch(intent)
+ }
+
+ private fun onConfirmDevice(resultCode: Int, data: Intent?) {
+ val wasSuccessful = resultCode == RESULT_FINISHED || resultCode == Activity.RESULT_OK
+ val gateKeeperPasswordHandle = data?.getExtra(EXTRA_KEY_GK_PW_HANDLE) as Long?
+ lifecycleScope.launch {
+ navigationViewModel.onConfirmDevice(wasSuccessful, gateKeeperPasswordHandle)
+ }
+ }
+
+ private fun createFingerprintsFooterPreference(canEnroll: Boolean, maxFingerprints: Int) {
+ val pref = this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINT_ADD)
+ val maxSummary = context?.getString(R.string.fingerprint_add_max, maxFingerprints) ?: ""
+ pref?.summary = maxSummary
+ pref?.isEnabled = canEnroll
+ pref?.setOnPreferenceClickListener {
+ navigationViewModel.onAddFingerprintClicked()
+ true
+ }
+ pref?.isVisible = true
+ }
+
+ private fun fingerprintPreferences(): List {
+ val category =
+ this@FingerprintSettingsV2Fragment.findPreference(KEY_FINGERPRINTS_ENROLLED_CATEGORY)
+ as PreferenceCategory?
+
+ return category?.let { cat ->
+ cat.childrenToList().map { it as FingerprintSettingsPreference? }
+ }
+ ?: emptyList()
+ }
+
+ private fun PreferenceCategory.childrenToList(): List {
+ val mutable: MutableList = mutableListOf()
+ for (i in 0 until this.preferenceCount) {
+ mutable.add(this.getPreference(i))
+ }
+ return mutable.toList()
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsNavigationViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsNavigationViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..a3a5d3c9746b9baf7de3a5d6fc5108d30cdf25a1
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsNavigationViewModel.kt
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2023 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.settings.biometrics.fingerprint2.ui.viewmodel
+
+import android.hardware.fingerprint.FingerprintManager
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.android.settings.biometrics.BiometricEnrollBase
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+/** A Viewmodel that represents the navigation of the FingerprintSettings activity. */
+class FingerprintSettingsNavigationViewModel(
+ private val userId: Int,
+ private val fingerprintManagerInteractor: FingerprintManagerInteractor,
+ private val backgroundDispatcher: CoroutineDispatcher,
+ tokenInit: ByteArray?,
+ challengeInit: Long?,
+) : ViewModel() {
+
+ private var token = tokenInit
+ private var challenge = challengeInit
+
+ private val _nextStep: MutableStateFlow = MutableStateFlow(null)
+ /** This flow represents the high level state for the FingerprintSettingsV2Fragment. */
+ val nextStep: StateFlow = _nextStep.asStateFlow()
+
+ init {
+ if (challengeInit == null || tokenInit == null) {
+ _nextStep.update { LaunchConfirmDeviceCredential(userId) }
+ } else {
+ viewModelScope.launch { showSettingsHelper() }
+ }
+ }
+
+ /** Used to indicate that FingerprintSettings is complete. */
+ fun finish() {
+ _nextStep.update { null }
+ }
+
+ /** Used to finish settings in certain cases. */
+ fun maybeFinishActivity(changingConfig: Boolean) {
+ val isConfirmingOrEnrolling =
+ _nextStep.value is LaunchConfirmDeviceCredential ||
+ _nextStep.value is EnrollAdditionalFingerprint ||
+ _nextStep.value is EnrollFirstFingerprint ||
+ _nextStep.value is LaunchedActivity
+ if (!isConfirmingOrEnrolling && !changingConfig)
+ _nextStep.update {
+ FinishSettingsWithResult(BiometricEnrollBase.RESULT_TIMEOUT, "onStop finishing settings")
+ }
+ }
+
+ /** Used to indicate that we have launched another activity and we should await its result. */
+ fun setStepToLaunched() {
+ _nextStep.update { LaunchedActivity }
+ }
+
+ /** Indicates a successful enroll has occurred */
+ fun onEnrollSuccess() {
+ showSettingsHelper()
+ }
+
+ /** Add fingerprint clicked */
+ fun onAddFingerprintClicked() {
+ _nextStep.update { EnrollAdditionalFingerprint(userId, token) }
+ }
+
+ /** Enrolling of an additional fingerprint failed */
+ fun onEnrollAdditionalFailure() {
+ launchFinishSettings("Failed to enroll additional fingerprint")
+ }
+
+ /** The first fingerprint enrollment failed */
+ fun onEnrollFirstFailure(reason: String) {
+ launchFinishSettings(reason)
+ }
+
+ /** The first fingerprint enrollment failed with a result code */
+ fun onEnrollFirstFailure(reason: String, resultCode: Int) {
+ launchFinishSettings(reason, resultCode)
+ }
+
+ /** Notifies that a users first enrollment succeeded. */
+ fun onEnrollFirst(theToken: ByteArray?, theChallenge: Long?) {
+ if (theToken == null) {
+ launchFinishSettings("Error, empty token")
+ return
+ }
+ if (theChallenge == null) {
+ launchFinishSettings("Error, empty keyChallenge")
+ return
+ }
+ token = theToken!!
+ challenge = theChallenge!!
+
+ showSettingsHelper()
+ }
+
+ /**
+ * Indicates to the view model that a confirm device credential action has been completed with a
+ * [theGateKeeperPasswordHandle] which will be used for [FingerprintManager] operations such as
+ * [FingerprintManager.enroll].
+ */
+ suspend fun onConfirmDevice(wasSuccessful: Boolean, theGateKeeperPasswordHandle: Long?) {
+ if (!wasSuccessful) {
+ launchFinishSettings("ConfirmDeviceCredential was unsuccessful")
+ return
+ }
+ if (theGateKeeperPasswordHandle == null) {
+ launchFinishSettings("ConfirmDeviceCredential gatekeeper password was null")
+ return
+ }
+
+ launchEnrollNextStep(theGateKeeperPasswordHandle)
+ }
+
+ private fun showSettingsHelper() {
+ _nextStep.update { ShowSettings }
+ }
+
+ private suspend fun launchEnrollNextStep(gateKeeperPasswordHandle: Long?) {
+ fingerprintManagerInteractor.enrolledFingerprints.collect {
+ if (it.isEmpty()) {
+ _nextStep.update { EnrollFirstFingerprint(userId, gateKeeperPasswordHandle, null, null) }
+ } else {
+ viewModelScope.launch(backgroundDispatcher) {
+ val challengePair =
+ fingerprintManagerInteractor.generateChallenge(gateKeeperPasswordHandle!!)
+ challenge = challengePair.first
+ token = challengePair.second
+
+ showSettingsHelper()
+ }
+ }
+ }
+ }
+
+ private fun launchFinishSettings(reason: String) {
+ _nextStep.update { FinishSettings(reason) }
+ }
+
+ private fun launchFinishSettings(reason: String, errorCode: Int) {
+ _nextStep.update { FinishSettingsWithResult(errorCode, reason) }
+ }
+ class FingerprintSettingsNavigationModelFactory(
+ private val userId: Int,
+ private val interactor: FingerprintManagerInteractor,
+ private val backgroundDispatcher: CoroutineDispatcher,
+ private val token: ByteArray?,
+ private val challenge: Long?,
+ ) : ViewModelProvider.Factory {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun create(
+ modelClass: Class,
+ ): T {
+
+ return FingerprintSettingsNavigationViewModel(
+ userId,
+ interactor,
+ backgroundDispatcher,
+ token,
+ challenge,
+ )
+ as T
+ }
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..554f336a7ffe4c30b8fa0e1a9dffeb987fbe9200
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintSettingsViewModel.kt
@@ -0,0 +1,324 @@
+/*
+ * Copyright (C) 2023 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.settings.biometrics.fingerprint2.ui.viewmodel
+
+import android.hardware.fingerprint.FingerprintManager
+import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL
+import android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewModelScope
+import com.android.settings.biometrics.fingerprint2.domain.interactor.FingerprintManagerInteractor
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.combineTransform
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.last
+import kotlinx.coroutines.flow.sample
+import kotlinx.coroutines.flow.transformLatest
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+private const val TAG = "FingerprintSettingsViewModel"
+private const val DEBUG = false
+
+/** Models the UI state for fingerprint settings. */
+class FingerprintSettingsViewModel(
+ private val userId: Int,
+ private val fingerprintManagerInteractor: FingerprintManagerInteractor,
+ private val backgroundDispatcher: CoroutineDispatcher,
+ private val navigationViewModel: FingerprintSettingsNavigationViewModel,
+) : ViewModel() {
+
+ private val _consumerShouldAuthenticate: MutableStateFlow = MutableStateFlow(false)
+
+ private val fingerprintSensorPropertiesInternal:
+ MutableStateFlow?> =
+ MutableStateFlow(null)
+
+ private val _isShowingDialog: MutableStateFlow = MutableStateFlow(null)
+ val isShowingDialog =
+ _isShowingDialog.combine(navigationViewModel.nextStep) { dialogFlow, nextStep ->
+ if (nextStep is ShowSettings) {
+ return@combine dialogFlow
+ } else {
+ return@combine null
+ }
+ }
+
+ init {
+ viewModelScope.launch {
+ fingerprintSensorPropertiesInternal.update {
+ fingerprintManagerInteractor.sensorPropertiesInternal()
+ }
+ }
+
+ viewModelScope.launch {
+ navigationViewModel.nextStep.filterNotNull().collect {
+ _isShowingDialog.update { null }
+ if (it is ShowSettings) {
+ // reset state
+ updateSettingsData()
+ }
+ }
+ }
+ }
+
+ private val _fingerprintStateViewModel: MutableStateFlow =
+ MutableStateFlow(null)
+ val fingerprintState: Flow =
+ _fingerprintStateViewModel.combineTransform(navigationViewModel.nextStep) {
+ settingsShowingViewModel,
+ currStep ->
+ if (currStep != null && currStep is ShowSettings) {
+ emit(settingsShowingViewModel)
+ }
+ }
+
+ private val _isLockedOut: MutableStateFlow =
+ MutableStateFlow(null)
+
+ private val _authSucceeded: MutableSharedFlow =
+ MutableSharedFlow()
+
+ private val attemptsSoFar: MutableStateFlow = MutableStateFlow(0)
+
+ /**
+ * This is a very tricky flow. The current fingerprint manager APIs are not robust, and a proper
+ * implementation would take quite a lot of code to implement, it might be easier to rewrite
+ * FingerprintManager.
+ *
+ * The hack to note is the sample(400), if we call authentications in too close of proximity
+ * without waiting for a response, the fingerprint manager will send us the results of the
+ * previous attempt.
+ */
+ private val canAuthenticate: Flow =
+ combine(
+ _isShowingDialog,
+ navigationViewModel.nextStep,
+ _consumerShouldAuthenticate,
+ _fingerprintStateViewModel,
+ _isLockedOut,
+ attemptsSoFar,
+ fingerprintSensorPropertiesInternal
+ ) { dialogShowing, step, resume, fingerprints, isLockedOut, attempts, sensorProps ->
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "canAuthenticate(isShowingDialog=${dialogShowing != null}," +
+ "nextStep=${step}," +
+ "resumed=${resume}," +
+ "fingerprints=${fingerprints}," +
+ "lockedOut=${isLockedOut}," +
+ "attempts=${attempts}," +
+ "sensorProps=${sensorProps}"
+ )
+ }
+ if (sensorProps.isNullOrEmpty()) {
+ return@combine false
+ }
+ val sensorType = sensorProps[0].sensorType
+ if (listOf(TYPE_UDFPS_OPTICAL, TYPE_UDFPS_ULTRASONIC).contains(sensorType)) {
+ return@combine false
+ }
+
+ if (step != null && step is ShowSettings) {
+ if (fingerprints?.fingerprintViewModels?.isNotEmpty() == true) {
+ return@combine dialogShowing == null && isLockedOut == null && resume && attempts < 15
+ }
+ }
+ false
+ }
+ .sample(400)
+ .distinctUntilChanged()
+
+ /** Represents a consistent stream of authentication attempts. */
+ val authFlow: Flow =
+ canAuthenticate
+ .transformLatest {
+ try {
+ Log.d(TAG, "canAuthenticate $it")
+ while (it && navigationViewModel.nextStep.value is ShowSettings) {
+ Log.d(TAG, "canAuthenticate authing")
+ attemptingAuth()
+ when (val authAttempt = fingerprintManagerInteractor.authenticate()) {
+ is FingerprintAuthAttemptViewModel.Success -> {
+ onAuthSuccess(authAttempt)
+ emit(authAttempt)
+ }
+ is FingerprintAuthAttemptViewModel.Error -> {
+ if (authAttempt.error == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT) {
+ lockout(authAttempt)
+ emit(authAttempt)
+ return@transformLatest
+ }
+ }
+ }
+ }
+ } catch (exception: Exception) {
+ Log.d(TAG, "shouldAuthenticate exception $exception")
+ }
+ }
+ .flowOn(backgroundDispatcher)
+
+ /** The rename dialog has finished */
+ fun onRenameDialogFinished() {
+ _isShowingDialog.update { null }
+ }
+
+ /** The delete dialog has finished */
+ fun onDeleteDialogFinished() {
+ _isShowingDialog.update { null }
+ }
+
+ override fun toString(): String {
+ return "userId: $userId\n" + "fingerprintState: ${_fingerprintStateViewModel.value}\n"
+ }
+
+ /** The fingerprint delete button has been clicked. */
+ fun onDeleteClicked(fingerprintViewModel: FingerprintViewModel) {
+ viewModelScope.launch {
+ if (_isShowingDialog.value == null || navigationViewModel.nextStep.value != ShowSettings) {
+ _isShowingDialog.tryEmit(PreferenceViewModel.DeleteDialog(fingerprintViewModel))
+ } else {
+ Log.d(TAG, "Ignoring onDeleteClicked due to dialog showing ${_isShowingDialog.value}")
+ }
+ }
+ }
+
+ /** The rename fingerprint dialog has been clicked. */
+ fun onPrefClicked(fingerprintViewModel: FingerprintViewModel) {
+ viewModelScope.launch {
+ if (_isShowingDialog.value == null || navigationViewModel.nextStep.value != ShowSettings) {
+ _isShowingDialog.tryEmit(PreferenceViewModel.RenameDialog(fingerprintViewModel))
+ } else {
+ Log.d(TAG, "Ignoring onPrefClicked due to dialog showing ${_isShowingDialog.value}")
+ }
+ }
+ }
+
+ /** A request to delete a fingerprint */
+ fun deleteFingerprint(fp: FingerprintViewModel) {
+ viewModelScope.launch(backgroundDispatcher) {
+ if (fingerprintManagerInteractor.removeFingerprint(fp)) {
+ updateSettingsData()
+ }
+ }
+ }
+
+ /** A request to rename a fingerprint */
+ fun renameFingerprint(fp: FingerprintViewModel, newName: String) {
+ viewModelScope.launch {
+ fingerprintManagerInteractor.renameFingerprint(fp, newName)
+ updateSettingsData()
+ }
+ }
+
+ private fun attemptingAuth() {
+ attemptsSoFar.update { it + 1 }
+ }
+
+ private suspend fun onAuthSuccess(success: FingerprintAuthAttemptViewModel.Success) {
+ _authSucceeded.emit(success)
+ attemptsSoFar.update { 0 }
+ }
+
+ private fun lockout(attemptViewModel: FingerprintAuthAttemptViewModel.Error) {
+ _isLockedOut.update { attemptViewModel }
+ }
+
+ /**
+ * This function is sort of a hack, it's used whenever we want to check for fingerprint state
+ * updates.
+ */
+ private suspend fun updateSettingsData() {
+ Log.d(TAG, "update settings data called")
+ val fingerprints = fingerprintManagerInteractor.enrolledFingerprints.last()
+ val canEnrollFingerprint =
+ fingerprintManagerInteractor.canEnrollFingerprints(fingerprints.size).last()
+ val maxFingerprints = fingerprintManagerInteractor.maxEnrollableFingerprints.last()
+ val hasSideFps = fingerprintManagerInteractor.hasSideFps()
+ val pressToAuthEnabled = fingerprintManagerInteractor.pressToAuthEnabled()
+ _fingerprintStateViewModel.update {
+ FingerprintStateViewModel(
+ fingerprints,
+ canEnrollFingerprint,
+ maxFingerprints,
+ hasSideFps,
+ pressToAuthEnabled
+ )
+ }
+ }
+
+ /** Used to indicate whether the consumer of the view model is ready for authentication. */
+ fun shouldAuthenticate(authenticate: Boolean) {
+ _consumerShouldAuthenticate.update { authenticate }
+ }
+
+ class FingerprintSettingsViewModelFactory(
+ private val userId: Int,
+ private val interactor: FingerprintManagerInteractor,
+ private val backgroundDispatcher: CoroutineDispatcher,
+ private val navigationViewModel: FingerprintSettingsNavigationViewModel,
+ ) : ViewModelProvider.Factory {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun create(
+ modelClass: Class,
+ ): T {
+
+ return FingerprintSettingsViewModel(
+ userId,
+ interactor,
+ backgroundDispatcher,
+ navigationViewModel,
+ )
+ as T
+ }
+ }
+}
+
+private inline fun combine(
+ flow: Flow,
+ flow2: Flow,
+ flow3: Flow,
+ flow4: Flow,
+ flow5: Flow,
+ flow6: Flow,
+ flow7: Flow,
+ crossinline transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R
+): Flow {
+ return combine(flow, flow2, flow3, flow4, flow5, flow6, flow7) { args: Array<*> ->
+ @Suppress("UNCHECKED_CAST")
+ transform(
+ args[0] as T1,
+ args[1] as T2,
+ args[2] as T3,
+ args[3] as T4,
+ args[4] as T5,
+ args[5] as T6,
+ args[6] as T7,
+ )
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1df0e34f060f2094a7c306ef202430080ab88c76
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/FingerprintViewModel.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 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.settings.biometrics.fingerprint2.ui.viewmodel
+
+/** Represents the fingerprint data nad the relevant state. */
+data class FingerprintStateViewModel(
+ val fingerprintViewModels: List,
+ val canEnroll: Boolean,
+ val maxFingerprints: Int,
+ val hasSideFps: Boolean,
+ val pressToAuth: Boolean,
+)
+
+data class FingerprintViewModel(
+ val name: String,
+ val fingerId: Int,
+ val deviceId: Long,
+)
+
+sealed class FingerprintAuthAttemptViewModel {
+ data class Success(
+ val fingerId: Int,
+ ) : FingerprintAuthAttemptViewModel()
+
+ data class Error(
+ val error: Int,
+ val message: String,
+ ) : FingerprintAuthAttemptViewModel()
+}
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/NextStepViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/NextStepViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f9dbbffda33ef003264c2776476312e73c9300ab
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/NextStepViewModel.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 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.settings.biometrics.fingerprint2.ui.viewmodel
+
+/**
+ * A class to represent a high level step for FingerprintSettings. This is typically to perform an
+ * action like launching an activity.
+ */
+sealed class NextStepViewModel
+
+data class EnrollFirstFingerprint(
+ val userId: Int,
+ val gateKeeperPasswordHandle: Long?,
+ val challenge: Long?,
+ val challengeToken: ByteArray?,
+) : NextStepViewModel()
+
+data class EnrollAdditionalFingerprint(
+ val userId: Int,
+ val challengeToken: ByteArray?,
+) : NextStepViewModel()
+
+data class FinishSettings(val reason: String) : NextStepViewModel()
+
+data class FinishSettingsWithResult(val result: Int, val reason: String) : NextStepViewModel()
+
+object ShowSettings : NextStepViewModel()
+
+object LaunchedActivity : NextStepViewModel()
+
+data class LaunchConfirmDeviceCredential(val userId: Int) : NextStepViewModel()
diff --git a/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/PreferenceViewModel.kt b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/PreferenceViewModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..05764a217f1547ce0b9d7583320bebfe31862756
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint2/ui/viewmodel/PreferenceViewModel.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2023 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.settings.biometrics.fingerprint2.ui.viewmodel
+
+/** Classed use to represent a Dialogs state. */
+sealed class PreferenceViewModel {
+ data class RenameDialog(
+ val fingerprintViewModel: FingerprintViewModel,
+ ) : PreferenceViewModel()
+
+ data class DeleteDialog(
+ val fingerprintViewModel: FingerprintViewModel,
+ ) : PreferenceViewModel()
+}
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java
index d77d9d3f7e8086d7e31262d5db2ff494319f9b38..7074288716e32d5d34ed5a676931f66a62def452 100644
--- a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java
+++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java
@@ -103,12 +103,12 @@ public class FingerprintEnrollProgressViewModel extends AndroidViewModel {
}
@Override
- public void onPointerDown(int sensorId) {
+ public void onUdfpsPointerDown(int sensorId) {
mPointerDownLiveData.postValue(sensorId);
}
@Override
- public void onPointerUp(int sensorId) {
+ public void onUdfpsPointerUp(int sensorId) {
mPointerUpLiveData.postValue(sensorId);
}
};
diff --git a/src/com/android/settings/bluetooth/BlockingPrefWithSliceController.java b/src/com/android/settings/bluetooth/BlockingPrefWithSliceController.java
index 93a2747cac072197161bd7c49ed2478adf459d41..0690186b9722b9f04d3566b8dbfeabc9d1abbf95 100644
--- a/src/com/android/settings/bluetooth/BlockingPrefWithSliceController.java
+++ b/src/com/android/settings/bluetooth/BlockingPrefWithSliceController.java
@@ -59,7 +59,7 @@ import java.util.Optional;
* until {@link Slice} is fully loaded.
*/
public class BlockingPrefWithSliceController extends BasePreferenceController implements
- LifecycleObserver, OnStart, OnStop, Observer, BasePreferenceController.UiBlocker{
+ LifecycleObserver, OnStart, OnStop, Observer, BasePreferenceController.UiBlocker {
private static final String TAG = "BlockingPrefWithSliceController";
private static final String PREFIX_KEY = "slice_preference_item_";
@@ -225,7 +225,8 @@ public class BlockingPrefWithSliceController extends BasePreferenceController im
} else {
expectedActivityIntent = intentFromSliceAction;
}
- if (expectedActivityIntent != null) {
+ if (expectedActivityIntent != null && expectedActivityIntent.resolveActivity(
+ mContext.getPackageManager()) != null) {
Log.d(TAG, "setIntent: ActivityIntent" + expectedActivityIntent);
// Since UI needs to support the Settings' 2 panel feature, the intent can't use the
// FLAG_ACTIVITY_NEW_TASK. The above intent may have the FLAG_ACTIVITY_NEW_TASK
@@ -234,6 +235,7 @@ public class BlockingPrefWithSliceController extends BasePreferenceController im
preference.setIntent(expectedActivityIntent);
} else {
Log.d(TAG, "setIntent: Intent is null");
+ preference.setSelectable(false);
}
}
diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeController.java b/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeController.java
new file mode 100644
index 0000000000000000000000000000000000000000..9571767253c4ba3514b050c817bdcff7dadb900d
--- /dev/null
+++ b/src/com/android/settings/bluetooth/BluetoothDetailsAudioDeviceTypeController.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2022 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.settings.bluetooth;
+
+import static android.bluetooth.BluetoothDevice.DEVICE_TYPE_LE;
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_CARKIT;
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_HEADPHONES;
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_HEARING_AID;
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_OTHER;
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_SPEAKER;
+import static android.media.AudioManager.AUDIO_DEVICE_CATEGORY_UNKNOWN;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.AudioManager.AudioDeviceCategory;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.ListPreference;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.R;
+import com.android.settingslib.bluetooth.A2dpProfile;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.LeAudioProfile;
+import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+/**
+ * Controller responsible for the bluetooth audio device type selection
+ */
+public class BluetoothDetailsAudioDeviceTypeController extends BluetoothDetailsController
+ implements Preference.OnPreferenceChangeListener {
+ private static final String TAG = "BluetoothDetailsAudioDeviceTypeController";
+
+ private static final boolean DEBUG = false;
+
+ private static final String KEY_BT_AUDIO_DEVICE_TYPE_GROUP =
+ "bluetooth_audio_device_type_group";
+ private static final String KEY_BT_AUDIO_DEVICE_TYPE = "bluetooth_audio_device_type";
+
+ private final AudioManager mAudioManager;
+
+ private ListPreference mAudioDeviceTypePreference;
+
+ private final LocalBluetoothProfileManager mProfileManager;
+
+ @VisibleForTesting
+ PreferenceCategory mProfilesContainer;
+
+ public BluetoothDetailsAudioDeviceTypeController(
+ Context context,
+ PreferenceFragmentCompat fragment,
+ LocalBluetoothManager manager,
+ CachedBluetoothDevice device,
+ Lifecycle lifecycle) {
+ super(context, fragment, device, lifecycle);
+ mAudioManager = context.getSystemService(AudioManager.class);
+ mProfileManager = manager.getProfileManager();
+ }
+
+ @Override
+ public boolean isAvailable() {
+ // Available only for A2DP and BLE devices.
+ A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
+ boolean a2dpProfileEnabled = false;
+ if (a2dpProfile != null) {
+ a2dpProfileEnabled = a2dpProfile.isEnabled(mCachedDevice.getDevice());
+ }
+
+ LeAudioProfile leAudioProfile = mProfileManager.getLeAudioProfile();
+ boolean leAudioProfileEnabled = false;
+ if (leAudioProfile != null) {
+ leAudioProfileEnabled = leAudioProfile.isEnabled(mCachedDevice.getDevice());
+ }
+
+ return a2dpProfileEnabled || leAudioProfileEnabled;
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (preference instanceof ListPreference) {
+ final ListPreference pref = (ListPreference) preference;
+ final String key = pref.getKey();
+ if (key.equals(KEY_BT_AUDIO_DEVICE_TYPE)) {
+ if (newValue instanceof String) {
+ final String value = (String) newValue;
+ final int index = pref.findIndexOfValue(value);
+ if (index >= 0) {
+ pref.setSummary(pref.getEntries()[index]);
+ mAudioManager.setBluetoothAudioDeviceCategory(mCachedDevice.getAddress(),
+ mCachedDevice.getDevice().getType() == DEVICE_TYPE_LE,
+ Integer.parseInt(value));
+ mCachedDevice.onAudioDeviceCategoryChanged();
+ }
+ }
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return KEY_BT_AUDIO_DEVICE_TYPE_GROUP;
+ }
+
+ @Override
+ protected void init(PreferenceScreen screen) {
+ mProfilesContainer = screen.findPreference(getPreferenceKey());
+ refresh();
+ }
+
+ @Override
+ protected void refresh() {
+ mAudioDeviceTypePreference = mProfilesContainer.findPreference(
+ KEY_BT_AUDIO_DEVICE_TYPE);
+ if (mAudioDeviceTypePreference == null) {
+ createAudioDeviceTypePreference(mProfilesContainer.getContext());
+ mProfilesContainer.addPreference(mAudioDeviceTypePreference);
+ }
+ }
+
+ @VisibleForTesting
+ void createAudioDeviceTypePreference(Context context) {
+ mAudioDeviceTypePreference = new ListPreference(context);
+ mAudioDeviceTypePreference.setKey(KEY_BT_AUDIO_DEVICE_TYPE);
+ mAudioDeviceTypePreference.setTitle(
+ mContext.getString(R.string.bluetooth_details_audio_device_types_title));
+ mAudioDeviceTypePreference.setEntries(new CharSequence[]{
+ mContext.getString(R.string.bluetooth_details_audio_device_type_unknown),
+ mContext.getString(R.string.bluetooth_details_audio_device_type_speaker),
+ mContext.getString(R.string.bluetooth_details_audio_device_type_headphones),
+ mContext.getString(R.string.bluetooth_details_audio_device_type_carkit),
+ mContext.getString(R.string.bluetooth_details_audio_device_type_hearing_aid),
+ mContext.getString(R.string.bluetooth_details_audio_device_type_other),
+ });
+ mAudioDeviceTypePreference.setEntryValues(new CharSequence[]{
+ Integer.toString(AUDIO_DEVICE_CATEGORY_UNKNOWN),
+ Integer.toString(AUDIO_DEVICE_CATEGORY_SPEAKER),
+ Integer.toString(AUDIO_DEVICE_CATEGORY_HEADPHONES),
+ Integer.toString(AUDIO_DEVICE_CATEGORY_CARKIT),
+ Integer.toString(AUDIO_DEVICE_CATEGORY_HEARING_AID),
+ Integer.toString(AUDIO_DEVICE_CATEGORY_OTHER),
+ });
+
+ @AudioDeviceCategory final int deviceCategory =
+ mAudioManager.getBluetoothAudioDeviceCategory(mCachedDevice.getAddress(),
+ mCachedDevice.getDevice().getType() == DEVICE_TYPE_LE);
+ if (DEBUG) {
+ Log.v(TAG, "getBluetoothAudioDeviceCategory() device: "
+ + mCachedDevice.getDevice().getAnonymizedAddress()
+ + ", has audio device category: " + deviceCategory);
+ }
+ mAudioDeviceTypePreference.setValue(Integer.toString(deviceCategory));
+
+ mAudioDeviceTypePreference.setSummary(mAudioDeviceTypePreference.getEntry());
+ mAudioDeviceTypePreference.setOnPreferenceChangeListener(this);
+ }
+
+ @VisibleForTesting
+ ListPreference getAudioDeviceTypePreference() {
+ return mAudioDeviceTypePreference;
+ }
+}
diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsDataSyncController.java b/src/com/android/settings/bluetooth/BluetoothDetailsDataSyncController.java
new file mode 100644
index 0000000000000000000000000000000000000000..0d74f3ce5ec5080b3f8587d26c84310eecfc315c
--- /dev/null
+++ b/src/com/android/settings/bluetooth/BluetoothDetailsDataSyncController.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2023 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.settings.bluetooth;
+
+import android.companion.AssociationInfo;
+import android.companion.CompanionDeviceManager;
+import android.companion.datatransfer.PermissionSyncRequest;
+import android.content.Context;
+import android.provider.Settings;
+
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceScreen;
+import androidx.preference.SwitchPreference;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.settings.R;
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.core.lifecycle.Lifecycle;
+
+import com.google.common.base.Objects;
+
+import java.util.Comparator;
+
+/**
+ * The controller of the CDM data sync in the bluetooth detail settings.
+ */
+public class BluetoothDetailsDataSyncController extends BluetoothDetailsController
+ implements Preference.OnPreferenceClickListener {
+
+ private static final int DUMMY_ASSOCIATION_ID = -1;
+ private static final String TAG = "BTDataSyncController";
+ private static final String KEY_DATA_SYNC_GROUP = "data_sync_group";
+ private static final String KEY_PERM_SYNC = "perm_sync";
+
+ @VisibleForTesting
+ PreferenceCategory mPreferenceCategory;
+ @VisibleForTesting
+ int mAssociationId = DUMMY_ASSOCIATION_ID;
+
+ private CachedBluetoothDevice mCachedDevice;
+ private CompanionDeviceManager mCompanionDeviceManager;
+
+ public BluetoothDetailsDataSyncController(Context context,
+ PreferenceFragmentCompat fragment,
+ CachedBluetoothDevice device,
+ Lifecycle lifecycle) {
+ super(context, fragment, device, lifecycle);
+ mCachedDevice = device;
+ mCompanionDeviceManager = context.getSystemService(CompanionDeviceManager.class);
+
+ mCompanionDeviceManager.getAllAssociations().stream().filter(
+ a -> a.getDeviceMacAddress() != null).filter(
+ a -> Objects.equal(mCachedDevice.getAddress(),
+ a.getDeviceMacAddress().toString().toUpperCase())).max(
+ Comparator.comparingLong(AssociationInfo::getTimeApprovedMs)).ifPresent(
+ a -> mAssociationId = a.getId());
+ }
+
+ @Override
+ public boolean isAvailable() {
+ if (mAssociationId == DUMMY_ASSOCIATION_ID) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ SwitchPreference switchPreference = (SwitchPreference) preference;
+ String key = switchPreference.getKey();
+ if (key.equals(KEY_PERM_SYNC)) {
+ if (switchPreference.isChecked()) {
+ mCompanionDeviceManager.enablePermissionsSync(mAssociationId);
+ } else {
+ mCompanionDeviceManager.disablePermissionsSync(mAssociationId);
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return KEY_DATA_SYNC_GROUP;
+ }
+
+ @Override
+ protected void init(PreferenceScreen screen) {
+ mPreferenceCategory = screen.findPreference(getPreferenceKey());
+ refresh();
+ }
+
+ @Override
+ protected void refresh() {
+ SwitchPreference permSyncPref = mPreferenceCategory.findPreference(KEY_PERM_SYNC);
+ if (permSyncPref == null) {
+ permSyncPref = createPermSyncPreference(mPreferenceCategory.getContext());
+ mPreferenceCategory.addPreference(permSyncPref);
+ }
+
+ if (mAssociationId == DUMMY_ASSOCIATION_ID) {
+ permSyncPref.setVisible(false);
+ return;
+ }
+
+ boolean visible = false;
+ boolean checked = false;
+ PermissionSyncRequest request = mCompanionDeviceManager.getPermissionSyncRequest(
+ mAssociationId);
+ if (request != null) {
+ visible = true;
+ if (request.isUserConsented()) {
+ checked = true;
+ }
+ }
+ permSyncPref.setVisible(visible);
+ permSyncPref.setChecked(checked);
+ }
+
+ @VisibleForTesting
+ SwitchPreference createPermSyncPreference(Context context) {
+ SwitchPreference pref = new SwitchPreference(context);
+ pref.setKey(KEY_PERM_SYNC);
+ pref.setTitle(context.getString(R.string.bluetooth_details_permissions_sync_title));
+ pref.setSummary(context.getString(R.string.bluetooth_details_permissions_sync_summary,
+ mCachedDevice.getName(),
+ Settings.Global.getString(context.getContentResolver(), "device_name")));
+ pref.setOnPreferenceClickListener(this);
+ return pref;
+ }
+}
diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java b/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java
index 724947c2fd6e9e12f6e5bf9624074c77bb049a93..e76b92e237bbc08ef944a41737f7cf92abbd4726 100644
--- a/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java
+++ b/src/com/android/settings/bluetooth/BluetoothDetailsProfilesController.java
@@ -69,6 +69,9 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll
private static final String ENABLE_DUAL_MODE_AUDIO =
"persist.bluetooth.enable_dual_mode_audio";
private static final String CONFIG_LE_AUDIO_ENABLED_BY_DEFAULT = "le_audio_enabled_by_default";
+ private static final boolean LE_AUDIO_TOGGLE_VISIBLE_DEFAULT_VALUE = true;
+ private static final String LE_AUDIO_TOGGLE_VISIBLE_PROPERTY =
+ "persist.bluetooth.leaudio.toggle_visible";
private LocalBluetoothManager mManager;
private LocalBluetoothProfileManager mProfileManager;
@@ -88,7 +91,7 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll
mManager = manager;
mProfileManager = mManager.getProfileManager();
mCachedDevice = device;
- mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mContext, mCachedDevice);
+ mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice);
lifecycle.addObserver(this);
}
@@ -96,12 +99,6 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll
protected void init(PreferenceScreen screen) {
mProfilesContainer = (PreferenceCategory)screen.findPreference(getPreferenceKey());
mProfilesContainer.setLayoutResource(R.layout.preference_bluetooth_profile_category);
- mIsLeContactSharingEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
- SettingsUIDeviceConfig.BT_LE_AUDIO_CONTACT_SHARING_ENABLED, true);
- mIsLeAudioToggleEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
- SettingsUIDeviceConfig.BT_LE_AUDIO_DEVICE_DETAIL_ENABLED, false)
- || DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH,
- CONFIG_LE_AUDIO_ENABLED_BY_DEFAULT, false);
// Call refresh here even though it will get called later in onResume, to avoid the
// list of switches appearing to "pop" into the page.
refresh();
@@ -151,8 +148,8 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll
profilePref.setEnabled(!mCachedDevice.isBusy());
}
- if (profile instanceof LeAudioProfile && !mIsLeAudioToggleEnabled) {
- profilePref.setVisible(false);
+ if (profile instanceof LeAudioProfile) {
+ profilePref.setVisible(mIsLeAudioToggleEnabled);
}
if (profile instanceof MapProfile) {
@@ -329,11 +326,16 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll
return;
}
+ LocalBluetoothProfile asha = mProfileManager.getHearingAidProfile();
+
for (CachedBluetoothDevice leAudioDevice : mProfileDeviceMap.get(profile.toString())) {
Log.d(TAG,
"device:" + leAudioDevice.getDevice().getAnonymizedAddress()
+ "disable LE profile");
profile.setEnabled(leAudioDevice.getDevice(), false);
+ if (asha != null) {
+ asha.setEnabled(leAudioDevice.getDevice(), true);
+ }
}
if (!SystemProperties.getBoolean(ENABLE_DUAL_MODE_AUDIO, false)) {
@@ -359,12 +361,16 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll
disableProfileBeforeUserEnablesLeAudio(mProfileManager.getA2dpProfile());
disableProfileBeforeUserEnablesLeAudio(mProfileManager.getHeadsetProfile());
}
+ LocalBluetoothProfile asha = mProfileManager.getHearingAidProfile();
for (CachedBluetoothDevice leAudioDevice : mProfileDeviceMap.get(profile.toString())) {
Log.d(TAG,
"device:" + leAudioDevice.getDevice().getAnonymizedAddress()
+ "enable LE profile");
profile.setEnabled(leAudioDevice.getDevice(), true);
+ if (asha != null) {
+ asha.setEnabled(leAudioDevice.getDevice(), false);
+ }
}
}
@@ -381,6 +387,12 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll
+ profile.toString() + " profile is disabled. Do nothing.");
}
}
+ } else {
+ if (profile == null) {
+ Log.w(TAG, "profile is null");
+ } else {
+ Log.w(TAG, profile.toString() + " is not in " + mProfileDeviceMap);
+ }
}
}
@@ -397,6 +409,12 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll
+ profile.toString() + " profile is enabled. Do nothing.");
}
}
+ } else {
+ if (profile == null) {
+ Log.w(TAG, "profile is null");
+ } else {
+ Log.w(TAG, profile.toString() + " is not in " + mProfileDeviceMap);
+ }
}
}
@@ -437,6 +455,7 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll
@Override
public void onResume() {
+ updateLeAudioConfig();
for (CachedBluetoothDevice item : mAllOfCachedDevices) {
item.registerCallback(this);
}
@@ -444,12 +463,25 @@ public class BluetoothDetailsProfilesController extends BluetoothDetailsControll
refresh();
}
+ private void updateLeAudioConfig() {
+ mIsLeContactSharingEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
+ SettingsUIDeviceConfig.BT_LE_AUDIO_CONTACT_SHARING_ENABLED, true);
+ boolean isLeAudioToggleVisible = SystemProperties.getBoolean(
+ LE_AUDIO_TOGGLE_VISIBLE_PROPERTY, LE_AUDIO_TOGGLE_VISIBLE_DEFAULT_VALUE);
+ boolean isLeEnabledByDefault = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_BLUETOOTH,
+ CONFIG_LE_AUDIO_ENABLED_BY_DEFAULT, false);
+ mIsLeAudioToggleEnabled = isLeAudioToggleVisible || isLeEnabledByDefault;
+ Log.d(TAG, "BT_LE_AUDIO_CONTACT_SHARING_ENABLED:" + mIsLeContactSharingEnabled
+ + ", LE_AUDIO_TOGGLE_VISIBLE_PROPERTY:" + isLeAudioToggleVisible
+ + ", CONFIG_LE_AUDIO_ENABLED_BY_DEFAULT:" + isLeEnabledByDefault);
+ }
+
@Override
public void onDeviceAttributesChanged() {
for (CachedBluetoothDevice item : mAllOfCachedDevices) {
item.unregisterCallback(this);
}
- mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mContext, mCachedDevice);
+ mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice);
for (CachedBluetoothDevice item : mAllOfCachedDevices) {
item.registerCallback(this);
}
diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioController.java b/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioController.java
index a1e133e6104b3d34637cb652715d94f169b1a2ec..c431ceeb0ae744ddab64039601da2dbcf30baefa 100644
--- a/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioController.java
+++ b/src/com/android/settings/bluetooth/BluetoothDetailsSpatialAudioController.java
@@ -16,6 +16,8 @@
package com.android.settings.bluetooth;
+import static android.media.Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
+
import android.content.Context;
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceInfo;
@@ -51,9 +53,7 @@ public class BluetoothDetailsSpatialAudioController extends BluetoothDetailsCont
@VisibleForTesting
PreferenceCategory mProfilesContainer;
@VisibleForTesting
- AudioDeviceAttributes mAudioDevice;
-
- private boolean mIsAvailable;
+ AudioDeviceAttributes mAudioDevice = null;
public BluetoothDetailsSpatialAudioController(
Context context,
@@ -63,13 +63,11 @@ public class BluetoothDetailsSpatialAudioController extends BluetoothDetailsCont
super(context, fragment, device, lifecycle);
AudioManager audioManager = context.getSystemService(AudioManager.class);
mSpatializer = audioManager.getSpatializer();
- getAvailableDevice();
-
}
@Override
public boolean isAvailable() {
- return mIsAvailable;
+ return mSpatializer.getImmersiveAudioLevel() != SPATIALIZER_IMMERSIVE_LEVEL_NONE;
}
@Override
@@ -77,15 +75,11 @@ public class BluetoothDetailsSpatialAudioController extends BluetoothDetailsCont
SwitchPreference switchPreference = (SwitchPreference) preference;
String key = switchPreference.getKey();
if (TextUtils.equals(key, KEY_SPATIAL_AUDIO)) {
- if (switchPreference.isChecked()) {
- mSpatializer.addCompatibleAudioDevice(mAudioDevice);
- } else {
- mSpatializer.removeCompatibleAudioDevice(mAudioDevice);
- }
- refresh();
+ updateSpatializerEnabled(switchPreference.isChecked());
+ refreshSpatialAudioEnabled(switchPreference);
return true;
} else if (TextUtils.equals(key, KEY_HEAD_TRACKING)) {
- mSpatializer.setHeadTrackerEnabled(switchPreference.isChecked(), mAudioDevice);
+ updateSpatializerHeadTracking(switchPreference.isChecked());
return true;
} else {
Log.w(TAG, "invalid key name.");
@@ -93,6 +87,26 @@ public class BluetoothDetailsSpatialAudioController extends BluetoothDetailsCont
}
}
+ private void updateSpatializerEnabled(boolean enabled) {
+ if (mAudioDevice == null) {
+ Log.w(TAG, "cannot update spatializer enabled for null audio device.");
+ return;
+ }
+ if (enabled) {
+ mSpatializer.addCompatibleAudioDevice(mAudioDevice);
+ } else {
+ mSpatializer.removeCompatibleAudioDevice(mAudioDevice);
+ }
+ }
+
+ private void updateSpatializerHeadTracking(boolean enabled) {
+ if (mAudioDevice == null) {
+ Log.w(TAG, "cannot update spatializer head tracking for null audio device.");
+ return;
+ }
+ mSpatializer.setHeadTrackerEnabled(enabled, mAudioDevice);
+ }
+
@Override
public String getPreferenceKey() {
return KEY_SPATIAL_AUDIO_GROUP;
@@ -106,12 +120,31 @@ public class BluetoothDetailsSpatialAudioController extends BluetoothDetailsCont
@Override
protected void refresh() {
+ if (mAudioDevice == null) {
+ getAvailableDevice();
+ }
+
SwitchPreference spatialAudioPref = mProfilesContainer.findPreference(KEY_SPATIAL_AUDIO);
- if (spatialAudioPref == null) {
+ if (spatialAudioPref == null && mAudioDevice != null) {
spatialAudioPref = createSpatialAudioPreference(mProfilesContainer.getContext());
mProfilesContainer.addPreference(spatialAudioPref);
+ } else if (mAudioDevice == null || !mSpatializer.isAvailableForDevice(mAudioDevice)) {
+ if (spatialAudioPref != null) {
+ mProfilesContainer.removePreference(spatialAudioPref);
+ }
+ final SwitchPreference headTrackingPref =
+ mProfilesContainer.findPreference(KEY_HEAD_TRACKING);
+ if (headTrackingPref != null) {
+ mProfilesContainer.removePreference(headTrackingPref);
+ }
+ mAudioDevice = null;
+ return;
}
+ refreshSpatialAudioEnabled(spatialAudioPref);
+ }
+
+ private void refreshSpatialAudioEnabled(SwitchPreference spatialAudioPref) {
boolean isSpatialAudioOn = mSpatializer.getCompatibleAudioDevices().contains(mAudioDevice);
Log.d(TAG, "refresh() isSpatialAudioOn : " + isSpatialAudioOn);
spatialAudioPref.setChecked(isSpatialAudioOn);
@@ -121,9 +154,13 @@ public class BluetoothDetailsSpatialAudioController extends BluetoothDetailsCont
headTrackingPref = createHeadTrackingPreference(mProfilesContainer.getContext());
mProfilesContainer.addPreference(headTrackingPref);
}
+ refreshHeadTracking(spatialAudioPref, headTrackingPref);
+ }
+ private void refreshHeadTracking(SwitchPreference spatialAudioPref,
+ SwitchPreference headTrackingPref) {
boolean isHeadTrackingAvailable =
- isSpatialAudioOn && mSpatializer.hasHeadTracker(mAudioDevice);
+ spatialAudioPref.isChecked() && mSpatializer.hasHeadTracker(mAudioDevice);
Log.d(TAG, "refresh() has head tracker : " + mSpatializer.hasHeadTracker(mAudioDevice));
headTrackingPref.setVisible(isHeadTrackingAvailable);
if (isHeadTrackingAvailable) {
@@ -173,7 +210,6 @@ public class BluetoothDetailsSpatialAudioController extends BluetoothDetailsCont
AudioDeviceInfo.TYPE_HEARING_AID,
mCachedDevice.getAddress());
- mIsAvailable = true;
if (mSpatializer.isAvailableForDevice(bleHeadsetDevice)) {
mAudioDevice = bleHeadsetDevice;
} else if (mSpatializer.isAvailableForDevice(bleSpeakerDevice)) {
@@ -182,20 +218,20 @@ public class BluetoothDetailsSpatialAudioController extends BluetoothDetailsCont
mAudioDevice = bleBroadcastDevice;
} else if (mSpatializer.isAvailableForDevice(a2dpDevice)) {
mAudioDevice = a2dpDevice;
- } else {
- mIsAvailable = mSpatializer.isAvailableForDevice(hearingAidDevice);
+ } else if (mSpatializer.isAvailableForDevice(hearingAidDevice)) {
mAudioDevice = hearingAidDevice;
+ } else {
+ mAudioDevice = null;
}
Log.d(TAG, "getAvailableDevice() device : "
+ mCachedDevice.getDevice().getAnonymizedAddress()
- + ", type : " + mAudioDevice.getType()
- + ", is available : " + mIsAvailable);
+ + ", is available : " + (mAudioDevice != null)
+ + ", type : " + (mAudioDevice == null ? "no type" : mAudioDevice.getType()));
}
@VisibleForTesting
void setAvailableDevice(AudioDeviceAttributes audioDevice) {
mAudioDevice = audioDevice;
- mIsAvailable = mSpatializer.isAvailableForDevice(audioDevice);
}
}
diff --git a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
index 99f3e3187cb699351398c409f8ba7ba58710b6a1..ae022aa6d88ddeab4e576bd333746cfee25f38d5 100644
--- a/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
+++ b/src/com/android/settings/bluetooth/BluetoothDeviceDetailsFragment.java
@@ -300,6 +300,8 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
lifecycle));
controllers.add(new BluetoothDetailsCompanionAppsController(context, this,
mCachedDevice, lifecycle));
+ controllers.add(new BluetoothDetailsAudioDeviceTypeController(context, this, mManager,
+ mCachedDevice, lifecycle));
controllers.add(new BluetoothDetailsSpatialAudioController(context, this, mCachedDevice,
lifecycle));
controllers.add(new BluetoothDetailsProfilesController(context, this, mManager,
@@ -314,6 +316,8 @@ public class BluetoothDeviceDetailsFragment extends RestrictedDashboardFragment
lifecycle));
controllers.add(new BluetoothDetailsHearingDeviceControlsController(context, this,
mCachedDevice, lifecycle));
+ controllers.add(new BluetoothDetailsDataSyncController(context, this,
+ mCachedDevice, lifecycle));
}
return controllers;
}
diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java b/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java
index 7ee61ee249dc959e1132dd52c32a45ea89f92e04..f2bc6fcfde603b7477051ea57761340413cada82 100644
--- a/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java
+++ b/src/com/android/settings/bluetooth/BluetoothDevicePairingDetailBase.java
@@ -128,7 +128,7 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere
if (device != null && mSelectedList.contains(device)) {
setResult(RESULT_OK);
finish();
- } else if (mDevicePreferenceMap.containsKey(cachedDevice)) {
+ } else {
onDeviceDeleted(cachedDevice);
}
}
@@ -175,8 +175,6 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere
public void updateContent(int bluetoothState) {
switch (bluetoothState) {
case BluetoothAdapter.STATE_ON:
- mDevicePreferenceMap.clear();
- clearPreferenceGroupCache();
mBluetoothAdapter.enable();
enableScanning();
break;
@@ -187,14 +185,6 @@ public abstract class BluetoothDevicePairingDetailBase extends DeviceListPrefere
}
}
- /**
- * Clears all cached preferences in {@code preferenceGroup}.
- */
- private void clearPreferenceGroupCache() {
- cacheRemoveAllPrefs(mAvailableDevicesCategory);
- removeCachedPrefs(mAvailableDevicesCategory);
- }
-
@VisibleForTesting
void showBluetoothTurnedOnToast() {
Toast.makeText(getContext(), R.string.connected_device_bluetooth_turned_on_toast,
diff --git a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java
index 5256f3d65964c4da66691630fd37e420efc0e84d..039080b26bae2d63a9bbee6e9df120892f265d8f 100644
--- a/src/com/android/settings/bluetooth/BluetoothDevicePreference.java
+++ b/src/com/android/settings/bluetooth/BluetoothDevicePreference.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2008 The Android Open Source Project
+ * Copyright (C) 2023 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.
@@ -35,6 +35,8 @@ import android.view.View;
import android.widget.ImageView;
import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
@@ -52,6 +54,7 @@ import java.lang.annotation.RetentionPolicy;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.atomic.AtomicInteger;
/**
* BluetoothDevicePreference is the preference type used to display each remote
@@ -79,7 +82,9 @@ public final class BluetoothDevicePreference extends GearPreference {
@VisibleForTesting
BluetoothAdapter mBluetoothAdapter;
private final boolean mShowDevicesWithoutNames;
- private final long mCurrentTime;
+ @NonNull
+ private static final AtomicInteger sNextId = new AtomicInteger();
+ private final int mId;
private final int mType;
private AlertDialog mDisconnectDialog;
@@ -127,8 +132,9 @@ public final class BluetoothDevicePreference extends GearPreference {
mCachedDevice = cachedDevice;
mCallback = new BluetoothDevicePreferenceCallback();
- mCurrentTime = System.currentTimeMillis();
+ mId = sNextId.getAndIncrement();
mType = type;
+ setVisible(false);
onPreferenceAttributesChanged();
}
@@ -229,35 +235,41 @@ public final class BluetoothDevicePreference extends GearPreference {
@SuppressWarnings("FutureReturnValueIgnored")
void onPreferenceAttributesChanged() {
- Pair pair = mCachedDevice.getDrawableWithDescription();
- setIcon(pair.first);
- contentDescription = pair.second;
-
- /*
- * The preference framework takes care of making sure the value has
- * changed before proceeding. It will also call notifyChanged() if
- * any preference info has changed from the previous value.
- */
- setTitle(mCachedDevice.getName());
try {
ThreadUtils.postOnBackgroundThread(() -> {
+ @Nullable String name = mCachedDevice.getName();
// Null check is done at the framework
- ThreadUtils.postOnMainThread(() -> setSummary(getConnectionSummary()));
+ @Nullable String connectionSummary = getConnectionSummary();
+ @NonNull Pair pair = mCachedDevice.getDrawableWithDescription();
+ boolean isBusy = mCachedDevice.isBusy();
+ // Device is only visible in the UI if it has a valid name besides MAC address or
+ // when user allows showing devices without user-friendly name in developer settings
+ boolean isVisible =
+ mShowDevicesWithoutNames || mCachedDevice.hasHumanReadableName();
+
+ ThreadUtils.postOnMainThread(() -> {
+ /*
+ * The preference framework takes care of making sure the value has
+ * changed before proceeding. It will also call notifyChanged() if
+ * any preference info has changed from the previous value.
+ */
+ setTitle(name);
+ setSummary(connectionSummary);
+ setIcon(pair.first);
+ contentDescription = pair.second;
+ // Used to gray out the item
+ setEnabled(!isBusy);
+ setVisible(isVisible);
+
+ // This could affect ordering, so notify that
+ if (mNeedNotifyHierarchyChanged) {
+ notifyHierarchyChanged();
+ }
+ });
});
} catch (RejectedExecutionException e) {
Log.w(TAG, "Handler thread unavailable, skipping getConnectionSummary!");
}
- // Used to gray out the item
- setEnabled(!mCachedDevice.isBusy());
-
- // Device is only visible in the UI if it has a valid name besides MAC address or when user
- // allows showing devices without user-friendly name in developer settings
- setVisible(mShowDevicesWithoutNames || mCachedDevice.hasHumanReadableName());
-
- // This could affect ordering, so notify that
- if (mNeedNotifyHierarchyChanged) {
- notifyHierarchyChanged();
- }
}
@Override
@@ -311,7 +323,7 @@ public final class BluetoothDevicePreference extends GearPreference {
return mCachedDevice
.compareTo(((BluetoothDevicePreference) another).mCachedDevice);
case SortType.TYPE_FIFO:
- return mCurrentTime > ((BluetoothDevicePreference) another).mCurrentTime ? 1 : -1;
+ return mId > ((BluetoothDevicePreference) another).mId ? 1 : -1;
default:
return super.compareTo(another);
}
diff --git a/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java b/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java
index d435639ddd955a7533645f0d3ed9b98d4ce0e501..cffd68c88e55794462a2391b8baf09affad32ff8 100644
--- a/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java
+++ b/src/com/android/settings/bluetooth/BluetoothFindBroadcastsFragment.java
@@ -131,6 +131,10 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment
Log.w(TAG, "onSourceAdded: mSelectedPreference == null!");
return;
}
+ if (mLeBroadcastAssistant != null
+ && mLeBroadcastAssistant.isSearchInProgress()) {
+ mLeBroadcastAssistant.stopSearchingForSources();
+ }
getActivity().runOnUiThread(() -> updateListCategoryFromBroadcastMetadata(
mSelectedPreference.getBluetoothLeBroadcastMetadata(), true));
}
@@ -238,6 +242,9 @@ public class BluetoothFindBroadcastsFragment extends RestrictedDashboardFragment
public void onStop() {
super.onStop();
if (mLeBroadcastAssistant != null) {
+ if (mLeBroadcastAssistant.isSearchInProgress()) {
+ mLeBroadcastAssistant.stopSearchingForSources();
+ }
mLeBroadcastAssistant.unregisterServiceCallBack(mBroadcastAssistantCallback);
}
}
diff --git a/src/com/android/settings/bluetooth/BluetoothPairingDetail.java b/src/com/android/settings/bluetooth/BluetoothPairingDetail.java
index a78bf27101c514e65852648f95279cd12eebfdb4..234d6d2eb45ed10bb6e4e336074a56780a130d8f 100644
--- a/src/com/android/settings/bluetooth/BluetoothPairingDetail.java
+++ b/src/com/android/settings/bluetooth/BluetoothPairingDetail.java
@@ -101,10 +101,8 @@ public class BluetoothPairingDetail extends BluetoothDevicePairingDetailBase imp
if (bluetoothState == BluetoothAdapter.STATE_ON) {
if (mInitialScanStarted) {
// Don't show bonded devices when screen turned back on
- setFilter(BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER);
- addCachedDevices();
+ addCachedDevices(BluetoothDeviceFilter.UNBONDED_DEVICE_FILTER);
}
- setFilter(BluetoothDeviceFilter.ALL_FILTER);
updateFooterPreference(mFooterPreference);
mAlwaysDiscoverable.start();
}
diff --git a/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java b/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java
deleted file mode 100644
index a4a9891797413e9ae29f168edeb585e096a92b5b..0000000000000000000000000000000000000000
--- a/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.java
+++ /dev/null
@@ -1,351 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settings.bluetooth;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.le.BluetoothLeScanner;
-import android.bluetooth.le.ScanCallback;
-import android.bluetooth.le.ScanFilter;
-import android.bluetooth.le.ScanResult;
-import android.bluetooth.le.ScanSettings;
-import android.os.Bundle;
-import android.os.SystemProperties;
-import android.text.BidiFormatter;
-import android.util.Log;
-
-import androidx.annotation.VisibleForTesting;
-import androidx.preference.Preference;
-import androidx.preference.PreferenceCategory;
-import androidx.preference.PreferenceGroup;
-
-import com.android.settings.R;
-import com.android.settings.dashboard.RestrictedDashboardFragment;
-import com.android.settingslib.bluetooth.BluetoothCallback;
-import com.android.settingslib.bluetooth.BluetoothDeviceFilter;
-import com.android.settingslib.bluetooth.CachedBluetoothDevice;
-import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
-import com.android.settingslib.bluetooth.LocalBluetoothManager;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-
-/**
- * Parent class for settings fragments that contain a list of Bluetooth
- * devices.
- *
- * @see DevicePickerFragment
- */
-// TODO: Refactor this fragment
-public abstract class DeviceListPreferenceFragment extends
- RestrictedDashboardFragment implements BluetoothCallback {
-
- private static final String TAG = "DeviceListPreferenceFragment";
-
- private static final String KEY_BT_SCAN = "bt_scan";
-
- // Copied from BluetoothDeviceNoNamePreferenceController.java
- private static final String BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY =
- "persist.bluetooth.showdeviceswithoutnames";
-
- private BluetoothDeviceFilter.Filter mFilter;
- private List mLeScanFilters;
- private ScanCallback mScanCallback;
-
- @VisibleForTesting
- protected boolean mScanEnabled;
-
- protected BluetoothDevice mSelectedDevice;
-
- protected BluetoothAdapter mBluetoothAdapter;
- protected LocalBluetoothManager mLocalManager;
- protected CachedBluetoothDeviceManager mCachedDeviceManager;
-
- @VisibleForTesting
- protected PreferenceGroup mDeviceListGroup;
-
- protected final HashMap mDevicePreferenceMap =
- new HashMap<>();
- protected final List mSelectedList = new ArrayList<>();
-
- protected boolean mShowDevicesWithoutNames;
-
- public DeviceListPreferenceFragment(String restrictedKey) {
- super(restrictedKey);
- mFilter = BluetoothDeviceFilter.ALL_FILTER;
- }
-
- protected final void setFilter(BluetoothDeviceFilter.Filter filter) {
- mFilter = filter;
- }
-
- protected final void setFilter(int filterType) {
- mFilter = BluetoothDeviceFilter.getFilter(filterType);
- }
-
- /**
- * Sets the bluetooth device scanning filter with {@link ScanFilter}s. It will change to start
- * {@link BluetoothLeScanner} which will scan BLE device only.
- *
- * @param leScanFilters list of settings to filter scan result
- */
- protected void setFilter(List leScanFilters) {
- mFilter = null;
- mLeScanFilters = leScanFilters;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- mLocalManager = Utils.getLocalBtManager(getActivity());
- if (mLocalManager == null) {
- Log.e(TAG, "Bluetooth is not supported on this device");
- return;
- }
- mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
- mCachedDeviceManager = mLocalManager.getCachedDeviceManager();
- mShowDevicesWithoutNames = SystemProperties.getBoolean(
- BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false);
-
- initPreferencesFromPreferenceScreen();
-
- mDeviceListGroup = (PreferenceCategory) findPreference(getDeviceListKey());
- }
-
- /** find and update preference that already existed in preference screen */
- protected abstract void initPreferencesFromPreferenceScreen();
-
- @Override
- public void onStart() {
- super.onStart();
- if (mLocalManager == null || isUiRestricted()) return;
-
- mLocalManager.setForegroundActivity(getActivity());
- mLocalManager.getEventManager().registerCallback(this);
- }
-
- @Override
- public void onStop() {
- super.onStop();
- if (mLocalManager == null || isUiRestricted()) {
- return;
- }
-
- removeAllDevices();
- mLocalManager.setForegroundActivity(null);
- mLocalManager.getEventManager().unregisterCallback(this);
- }
-
- void removeAllDevices() {
- mDevicePreferenceMap.clear();
- mDeviceListGroup.removeAll();
- }
-
- void addCachedDevices() {
- Collection cachedDevices =
- mCachedDeviceManager.getCachedDevicesCopy();
- for (CachedBluetoothDevice cachedDevice : cachedDevices) {
- onDeviceAdded(cachedDevice);
- }
- }
-
- @Override
- public boolean onPreferenceTreeClick(Preference preference) {
- if (KEY_BT_SCAN.equals(preference.getKey())) {
- startScanning();
- return true;
- }
-
- if (preference instanceof BluetoothDevicePreference) {
- BluetoothDevicePreference btPreference = (BluetoothDevicePreference) preference;
- CachedBluetoothDevice device = btPreference.getCachedDevice();
- mSelectedDevice = device.getDevice();
- mSelectedList.add(mSelectedDevice);
- onDevicePreferenceClick(btPreference);
- return true;
- }
-
- return super.onPreferenceTreeClick(preference);
- }
-
- protected void onDevicePreferenceClick(BluetoothDevicePreference btPreference) {
- btPreference.onClicked();
- }
-
- @Override
- public void onDeviceAdded(CachedBluetoothDevice cachedDevice) {
- if (mDevicePreferenceMap.get(cachedDevice) != null) {
- return;
- }
-
- // Prevent updates while the list shows one of the state messages
- if (mBluetoothAdapter.getState() != BluetoothAdapter.STATE_ON) {
- return;
- }
-
- if (mFilter != null && mFilter.matches(cachedDevice.getDevice())) {
- createDevicePreference(cachedDevice);
- }
- }
-
- void createDevicePreference(CachedBluetoothDevice cachedDevice) {
- if (mDeviceListGroup == null) {
- Log.w(TAG, "Trying to create a device preference before the list group/category "
- + "exists!");
- return;
- }
-
- String key = cachedDevice.getDevice().getAddress();
- BluetoothDevicePreference preference = (BluetoothDevicePreference) getCachedPreference(key);
-
- if (preference == null) {
- preference = new BluetoothDevicePreference(getPrefContext(), cachedDevice,
- mShowDevicesWithoutNames, BluetoothDevicePreference.SortType.TYPE_FIFO);
- preference.setKey(key);
- //Set hideSecondTarget is true if it's bonded device.
- preference.hideSecondTarget(true);
- mDeviceListGroup.addPreference(preference);
- }
-
- initDevicePreference(preference);
- mDevicePreferenceMap.put(cachedDevice, preference);
- }
-
- protected void initDevicePreference(BluetoothDevicePreference preference) {
- // Does nothing by default
- }
-
- @VisibleForTesting
- void updateFooterPreference(Preference myDevicePreference) {
- final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
-
- myDevicePreference.setTitle(getString(
- R.string.bluetooth_footer_mac_message,
- bidiFormatter.unicodeWrap(mBluetoothAdapter.getAddress())));
- }
-
- @Override
- public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) {
- BluetoothDevicePreference preference = mDevicePreferenceMap.remove(cachedDevice);
- if (preference != null) {
- mDeviceListGroup.removePreference(preference);
- }
- }
-
- @VisibleForTesting
- protected void enableScanning() {
- // BluetoothAdapter already handles repeated scan requests
- if (!mScanEnabled) {
- startScanning();
- mScanEnabled = true;
- }
- }
-
- @VisibleForTesting
- protected void disableScanning() {
- if (mScanEnabled) {
- stopScanning();
- mScanEnabled = false;
- }
- }
-
- @Override
- public void onScanningStateChanged(boolean started) {
- if (!started && mScanEnabled) {
- startScanning();
- }
- }
-
- /**
- * Return the key of the {@link PreferenceGroup} that contains the bluetooth devices
- */
- public abstract String getDeviceListKey();
-
- public boolean shouldShowDevicesWithoutNames() {
- return mShowDevicesWithoutNames;
- }
-
- @VisibleForTesting
- void startScanning() {
- if (mFilter != null) {
- startClassicScanning();
- } else if (mLeScanFilters != null) {
- startLeScanning();
- }
-
- }
-
- @VisibleForTesting
- void stopScanning() {
- if (mFilter != null) {
- stopClassicScanning();
- } else if (mLeScanFilters != null) {
- stopLeScanning();
- }
- }
-
- private void startClassicScanning() {
- if (!mBluetoothAdapter.isDiscovering()) {
- mBluetoothAdapter.startDiscovery();
- }
- }
-
- private void stopClassicScanning() {
- if (mBluetoothAdapter.isDiscovering()) {
- mBluetoothAdapter.cancelDiscovery();
- }
- }
-
- private void startLeScanning() {
- final BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner();
- final ScanSettings settings = new ScanSettings.Builder()
- .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
- .build();
- mScanCallback = new ScanCallback() {
- @Override
- public void onScanResult(int callbackType, ScanResult result) {
- final BluetoothDevice device = result.getDevice();
- CachedBluetoothDevice cachedDevice = mCachedDeviceManager.findDevice(device);
- if (cachedDevice == null) {
- cachedDevice = mCachedDeviceManager.addDevice(device);
- }
- // Only add device preference when it's not found in the map and there's no other
- // state message showing in the list
- if (mDevicePreferenceMap.get(cachedDevice) == null
- && mBluetoothAdapter.getState() == BluetoothAdapter.STATE_ON) {
- createDevicePreference(cachedDevice);
- }
- }
-
- @Override
- public void onScanFailed(int errorCode) {
- Log.w(TAG, "BLE Scan failed with error code " + errorCode);
- }
- };
- scanner.startScan(mLeScanFilters, settings, mScanCallback);
- }
-
- private void stopLeScanning() {
- final BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner();
- if (scanner != null) {
- scanner.stopScan(mScanCallback);
- }
- }
-}
diff --git a/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.kt b/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f18ae46e18b7f231dcb950a001713c6152e41c51
--- /dev/null
+++ b/src/com/android/settings/bluetooth/DeviceListPreferenceFragment.kt
@@ -0,0 +1,356 @@
+/*
+ * Copyright (C) 2023 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.settings.bluetooth
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.le.BluetoothLeScanner
+import android.bluetooth.le.ScanCallback
+import android.bluetooth.le.ScanFilter
+import android.bluetooth.le.ScanResult
+import android.bluetooth.le.ScanSettings
+import android.os.Bundle
+import android.os.SystemProperties
+import android.text.BidiFormatter
+import android.util.Log
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.lifecycleScope
+import androidx.preference.Preference
+import androidx.preference.PreferenceCategory
+import androidx.preference.PreferenceGroup
+import com.android.settings.R
+import com.android.settings.dashboard.RestrictedDashboardFragment
+import com.android.settingslib.bluetooth.BluetoothCallback
+import com.android.settingslib.bluetooth.BluetoothDeviceFilter
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import java.util.concurrent.ConcurrentHashMap
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+/**
+ * Parent class for settings fragments that contain a list of Bluetooth devices.
+ *
+ * @see DevicePickerFragment
+ *
+ * TODO: Refactor this fragment
+ */
+abstract class DeviceListPreferenceFragment(restrictedKey: String?) :
+ RestrictedDashboardFragment(restrictedKey), BluetoothCallback {
+
+ private var filter: BluetoothDeviceFilter.Filter? = BluetoothDeviceFilter.ALL_FILTER
+ private var leScanFilters: List? = null
+
+ @JvmField
+ @VisibleForTesting
+ var mScanEnabled = false
+
+ @JvmField
+ var mSelectedDevice: BluetoothDevice? = null
+
+ @JvmField
+ var mBluetoothAdapter: BluetoothAdapter? = null
+
+ @JvmField
+ var mLocalManager: LocalBluetoothManager? = null
+
+ @JvmField
+ var mCachedDeviceManager: CachedBluetoothDeviceManager? = null
+
+ @JvmField
+ @VisibleForTesting
+ var mDeviceListGroup: PreferenceGroup? = null
+
+ @VisibleForTesting
+ val devicePreferenceMap =
+ ConcurrentHashMap()
+
+ @JvmField
+ val mSelectedList: MutableList = ArrayList()
+
+ @VisibleForTesting
+ var lifecycleScope: CoroutineScope? = null
+
+ private var showDevicesWithoutNames = false
+
+ protected fun setFilter(filterType: Int) {
+ filter = BluetoothDeviceFilter.getFilter(filterType)
+ }
+
+ /**
+ * Sets the bluetooth device scanning filter with [ScanFilter]s. It will change to start
+ * [BluetoothLeScanner] which will scan BLE device only.
+ *
+ * @param leScanFilters list of settings to filter scan result
+ */
+ fun setFilter(leScanFilters: List?) {
+ filter = null
+ this.leScanFilters = leScanFilters
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ mLocalManager = Utils.getLocalBtManager(activity)
+ if (mLocalManager == null) {
+ Log.e(TAG, "Bluetooth is not supported on this device")
+ return
+ }
+ mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
+ mCachedDeviceManager = mLocalManager!!.cachedDeviceManager
+ showDevicesWithoutNames = SystemProperties.getBoolean(
+ BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY, false
+ )
+ initPreferencesFromPreferenceScreen()
+ mDeviceListGroup = findPreference(deviceListKey) as PreferenceCategory
+ }
+
+ /** find and update preference that already existed in preference screen */
+ protected abstract fun initPreferencesFromPreferenceScreen()
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ lifecycleScope = viewLifecycleOwner.lifecycleScope
+ }
+
+ override fun onStart() {
+ super.onStart()
+ if (mLocalManager == null || isUiRestricted) return
+ mLocalManager!!.foregroundActivity = activity
+ mLocalManager!!.eventManager.registerCallback(this)
+ }
+
+ override fun onStop() {
+ super.onStop()
+ if (mLocalManager == null || isUiRestricted) {
+ return
+ }
+ removeAllDevices()
+ mLocalManager!!.foregroundActivity = null
+ mLocalManager!!.eventManager.unregisterCallback(this)
+ }
+
+ fun removeAllDevices() {
+ devicePreferenceMap.clear()
+ mDeviceListGroup!!.removeAll()
+ }
+
+ @JvmOverloads
+ fun addCachedDevices(filterForCachedDevices: BluetoothDeviceFilter.Filter? = null) {
+ lifecycleScope?.launch {
+ withContext(Dispatchers.Default) {
+ mCachedDeviceManager!!.cachedDevicesCopy
+ .filter {
+ filterForCachedDevices == null || filterForCachedDevices.matches(it.device)
+ }
+ .forEach(::onDeviceAdded)
+ }
+ }
+ }
+
+ override fun onPreferenceTreeClick(preference: Preference): Boolean {
+ if (KEY_BT_SCAN == preference.key) {
+ startScanning()
+ return true
+ }
+ if (preference is BluetoothDevicePreference) {
+ val device = preference.cachedDevice.device
+ mSelectedDevice = device
+ mSelectedList.add(device)
+ onDevicePreferenceClick(preference)
+ return true
+ }
+ return super.onPreferenceTreeClick(preference)
+ }
+
+ protected open fun onDevicePreferenceClick(btPreference: BluetoothDevicePreference) {
+ btPreference.onClicked()
+ }
+
+ override fun onDeviceAdded(cachedDevice: CachedBluetoothDevice) {
+ lifecycleScope?.launch {
+ addDevice(cachedDevice)
+ }
+ }
+
+ private suspend fun addDevice(cachedDevice: CachedBluetoothDevice) =
+ withContext(Dispatchers.Default) {
+ // TODO(b/289189853): Replace checking if `filter` is null or not to decide which type
+ // of Bluetooth scanning method will be used
+ val filterMatched = filter == null || filter!!.matches(cachedDevice.device) == true
+ // Prevent updates while the list shows one of the state messages
+ if (mBluetoothAdapter!!.state == BluetoothAdapter.STATE_ON && filterMatched) {
+ createDevicePreference(cachedDevice)
+ }
+ }
+
+ private suspend fun createDevicePreference(cachedDevice: CachedBluetoothDevice) {
+ if (mDeviceListGroup == null) {
+ Log.w(
+ TAG,
+ "Trying to create a device preference before the list group/category exists!",
+ )
+ return
+ }
+ // Only add device preference when it's not found in the map and there's no other state
+ // message showing in the list
+ val preference = devicePreferenceMap.computeIfAbsent(cachedDevice) {
+ BluetoothDevicePreference(
+ prefContext,
+ cachedDevice,
+ showDevicesWithoutNames,
+ BluetoothDevicePreference.SortType.TYPE_FIFO,
+ ).apply {
+ key = cachedDevice.device.address
+ //Set hideSecondTarget is true if it's bonded device.
+ hideSecondTarget(true)
+ }
+ }
+ withContext(Dispatchers.Main) {
+ mDeviceListGroup!!.addPreference(preference)
+ initDevicePreference(preference)
+ }
+ }
+
+ protected open fun initDevicePreference(preference: BluetoothDevicePreference?) {
+ // Does nothing by default
+ }
+
+ @VisibleForTesting
+ fun updateFooterPreference(myDevicePreference: Preference) {
+ val bidiFormatter = BidiFormatter.getInstance()
+ myDevicePreference.title = getString(
+ R.string.bluetooth_footer_mac_message,
+ bidiFormatter.unicodeWrap(mBluetoothAdapter!!.address)
+ )
+ }
+
+ override fun onDeviceDeleted(cachedDevice: CachedBluetoothDevice) {
+ devicePreferenceMap.remove(cachedDevice)?.let {
+ mDeviceListGroup!!.removePreference(it)
+ }
+ }
+
+ @VisibleForTesting
+ open fun enableScanning() {
+ // BluetoothAdapter already handles repeated scan requests
+ if (!mScanEnabled) {
+ startScanning()
+ mScanEnabled = true
+ }
+ }
+
+ @VisibleForTesting
+ fun disableScanning() {
+ if (mScanEnabled) {
+ stopScanning()
+ mScanEnabled = false
+ }
+ }
+
+ override fun onScanningStateChanged(started: Boolean) {
+ if (!started && mScanEnabled) {
+ startScanning()
+ }
+ }
+
+ /**
+ * Return the key of the [PreferenceGroup] that contains the bluetooth devices
+ */
+ abstract val deviceListKey: String
+
+ @VisibleForTesting
+ open fun startScanning() {
+ if (filter != null) {
+ startClassicScanning()
+ } else if (leScanFilters != null) {
+ startLeScanning()
+ }
+ }
+
+ @VisibleForTesting
+ open fun stopScanning() {
+ if (filter != null) {
+ stopClassicScanning()
+ } else if (leScanFilters != null) {
+ stopLeScanning()
+ }
+ }
+
+ private fun startClassicScanning() {
+ if (!mBluetoothAdapter!!.isDiscovering) {
+ mBluetoothAdapter!!.startDiscovery()
+ }
+ }
+
+ private fun stopClassicScanning() {
+ if (mBluetoothAdapter!!.isDiscovering) {
+ mBluetoothAdapter!!.cancelDiscovery()
+ }
+ }
+
+ private val leScanCallback = object : ScanCallback() {
+ override fun onScanResult(callbackType: Int, result: ScanResult) {
+ handleLeScanResult(result)
+ }
+
+ override fun onBatchScanResults(results: MutableList?) {
+ for (result in results.orEmpty()) {
+ handleLeScanResult(result)
+ }
+ }
+
+ override fun onScanFailed(errorCode: Int) {
+ Log.w(TAG, "BLE Scan failed with error code $errorCode")
+ }
+ }
+
+ private fun startLeScanning() {
+ val scanner = mBluetoothAdapter!!.bluetoothLeScanner
+ val settings = ScanSettings.Builder()
+ .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
+ .build()
+ scanner.startScan(leScanFilters, settings, leScanCallback)
+ }
+
+ private fun stopLeScanning() {
+ val scanner = mBluetoothAdapter!!.bluetoothLeScanner
+ scanner?.stopScan(leScanCallback)
+ }
+
+ private fun handleLeScanResult(result: ScanResult) {
+ lifecycleScope?.launch {
+ withContext(Dispatchers.Default) {
+ val device = result.device
+ val cachedDevice = mCachedDeviceManager!!.findDevice(device)
+ ?: mCachedDeviceManager!!.addDevice(device, leScanFilters)
+ addDevice(cachedDevice)
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "DeviceListPreferenceFragment"
+ private const val KEY_BT_SCAN = "bt_scan"
+
+ // Copied from BluetoothDeviceNoNamePreferenceController.java
+ private const val BLUETOOTH_SHOW_DEVICES_WITHOUT_NAMES_PROPERTY =
+ "persist.bluetooth.showdeviceswithoutnames"
+ }
+}
diff --git a/src/com/android/settings/bluetooth/ForgetDeviceDialogFragment.java b/src/com/android/settings/bluetooth/ForgetDeviceDialogFragment.java
index 1da8672b760f19ad57d3a8445b98e6d6d9cde9cf..60d63c637df52e0a18f5a1f362679ffd84fb0cc3 100644
--- a/src/com/android/settings/bluetooth/ForgetDeviceDialogFragment.java
+++ b/src/com/android/settings/bluetooth/ForgetDeviceDialogFragment.java
@@ -23,6 +23,7 @@ import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
+import android.util.Log;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
@@ -63,6 +64,13 @@ public class ForgetDeviceDialogFragment extends InstrumentedDialogFragment {
@Override
public Dialog onCreateDialog(Bundle inState) {
+ Context context = getContext();
+ mDevice = getDevice(context);
+ if (mDevice == null) {
+ Log.e(TAG, "onCreateDialog: Device is null.");
+ return null;
+ }
+
DialogInterface.OnClickListener onConfirm = (dialog, which) -> {
mDevice.unpair();
Activity activity = getActivity();
@@ -70,9 +78,6 @@ public class ForgetDeviceDialogFragment extends InstrumentedDialogFragment {
activity.finish();
}
};
- Context context = getContext();
- mDevice = getDevice(context);
-
AlertDialog dialog = new AlertDialog.Builder(context)
.setPositiveButton(R.string.bluetooth_unpair_dialog_forget_confirm_button,
onConfirm)
diff --git a/src/com/android/settings/bluetooth/LeAudioBluetoothDetailsHeaderController.java b/src/com/android/settings/bluetooth/LeAudioBluetoothDetailsHeaderController.java
index e30bbfb398ace88b638746c38f13cf7088ca4eca..f72494f271c5bfe6c5a13c2b1497b6789d751660 100644
--- a/src/com/android/settings/bluetooth/LeAudioBluetoothDetailsHeaderController.java
+++ b/src/com/android/settings/bluetooth/LeAudioBluetoothDetailsHeaderController.java
@@ -88,6 +88,7 @@ public class LeAudioBluetoothDetailsHeaderController extends BasePreferenceContr
@VisibleForTesting
LayoutPreference mLayoutPreference;
+ LocalBluetoothManager mManager;
private CachedBluetoothDevice mCachedDevice;
private List mAllOfCachedDevices;
@VisibleForTesting
@@ -152,8 +153,9 @@ public class LeAudioBluetoothDetailsHeaderController extends BasePreferenceContr
public void init(CachedBluetoothDevice cachedBluetoothDevice,
LocalBluetoothManager bluetoothManager) {
mCachedDevice = cachedBluetoothDevice;
+ mManager = bluetoothManager;
mProfileManager = bluetoothManager.getProfileManager();
- mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mContext, mCachedDevice);
+ mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice);
}
@VisibleForTesting
@@ -300,7 +302,7 @@ public class LeAudioBluetoothDetailsHeaderController extends BasePreferenceContr
for (CachedBluetoothDevice item : mAllOfCachedDevices) {
item.unregisterCallback(this);
}
- mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mContext, mCachedDevice);
+ mAllOfCachedDevices = Utils.getAllOfCachedBluetoothDevices(mManager, mCachedDevice);
for (CachedBluetoothDevice item : mAllOfCachedDevices) {
item.registerCallback(this);
}
diff --git a/src/com/android/settings/bluetooth/QrCodeScanModeActivity.java b/src/com/android/settings/bluetooth/QrCodeScanModeActivity.java
index 92786c9b8135c1730e3a907ea557befdf5b96639..a0b249dee873b0dcbf13719faee0139c9329b084 100644
--- a/src/com/android/settings/bluetooth/QrCodeScanModeActivity.java
+++ b/src/com/android/settings/bluetooth/QrCodeScanModeActivity.java
@@ -98,7 +98,7 @@ public class QrCodeScanModeActivity extends QrCodeScanModeBaseActivity {
BluetoothBroadcastUtils.TAG_FRAGMENT_QR_CODE_SCANNER);
if (fragment == null) {
- fragment = new QrCodeScanModeFragment(mIsGroupOp, mSink);
+ fragment = new QrCodeScanModeFragment();
} else {
if (fragment.isVisible()) {
return;
diff --git a/src/com/android/settings/bluetooth/QrCodeScanModeFragment.java b/src/com/android/settings/bluetooth/QrCodeScanModeFragment.java
index f89dac6f935c442dc4ccbf12f52431aac71806c6..80aedd7142aa4ba59f4d345786984ffec668862d 100644
--- a/src/com/android/settings/bluetooth/QrCodeScanModeFragment.java
+++ b/src/com/android/settings/bluetooth/QrCodeScanModeFragment.java
@@ -18,7 +18,6 @@ package com.android.settings.bluetooth;
import android.app.Activity;
import android.app.settings.SettingsEnums;
-import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.Intent;
import android.graphics.Matrix;
@@ -71,9 +70,7 @@ public class QrCodeScanModeFragment extends InstrumentedFragment implements
public static final String KEY_BROADCAST_METADATA = "key_broadcast_metadata";
- private boolean mIsGroupOp;
private int mCornerRadius;
- private BluetoothDevice mSink;
private String mBroadcastMetadata;
private Context mContext;
private QrCamera mCamera;
@@ -81,11 +78,6 @@ public class QrCodeScanModeFragment extends InstrumentedFragment implements
private TextView mSummary;
private TextView mErrorMessage;
- public QrCodeScanModeFragment(boolean isGroupOp, BluetoothDevice sink) {
- mIsGroupOp = isGroupOp;
- mSink = sink;
- }
-
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
diff --git a/src/com/android/settings/bluetooth/Utils.java b/src/com/android/settings/bluetooth/Utils.java
index 79a2de0b38a59d2f7275151a44c9ad2e98f67e87..f1d6b20b51972e37ce789e47d634ef075fb67955 100644
--- a/src/com/android/settings/bluetooth/Utils.java
+++ b/src/com/android/settings/bluetooth/Utils.java
@@ -235,7 +235,8 @@ public final class Utils {
* @param cachedBluetoothDevice The main cachedBluetoothDevice.
* @return all cachedBluetoothDevices with the same groupId.
*/
- public static List getAllOfCachedBluetoothDevices(Context context,
+ public static List getAllOfCachedBluetoothDevices(
+ LocalBluetoothManager localBtMgr,
CachedBluetoothDevice cachedBluetoothDevice) {
List cachedBluetoothDevices = new ArrayList<>();
if (cachedBluetoothDevice == null) {
@@ -248,7 +249,6 @@ public final class Utils {
return cachedBluetoothDevices;
}
- final LocalBluetoothManager localBtMgr = Utils.getLocalBtManager(context);
if (localBtMgr == null) {
Log.e(TAG, "getAllOfCachedBluetoothDevices: no LocalBluetoothManager");
return cachedBluetoothDevices;
diff --git a/src/com/android/settings/connecteddevice/stylus/StylusDevicesController.java b/src/com/android/settings/connecteddevice/stylus/StylusDevicesController.java
index c93a1c6e6596b975e81f5647b32ab182d0050d72..985c8b713dc578d69350f31a73d4789337134ae9 100644
--- a/src/com/android/settings/connecteddevice/stylus/StylusDevicesController.java
+++ b/src/com/android/settings/connecteddevice/stylus/StylusDevicesController.java
@@ -16,12 +16,17 @@
package com.android.settings.connecteddevice.stylus;
+import android.app.Dialog;
import android.app.role.RoleManager;
import android.bluetooth.BluetoothDevice;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.content.pm.UserInfo;
+import android.os.Process;
+import android.os.UserHandle;
+import android.os.UserManager;
import android.provider.Settings;
import android.provider.Settings.Secure;
import android.text.TextUtils;
@@ -38,6 +43,9 @@ import androidx.preference.PreferenceScreen;
import androidx.preference.SwitchPreference;
import com.android.settings.R;
+import com.android.settings.dashboard.profileselector.ProfileSelectDialog;
+import com.android.settings.dashboard.profileselector.UserAdapter;
+import com.android.settingslib.PrimarySwitchPreference;
import com.android.settingslib.bluetooth.BluetoothUtils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.settingslib.core.AbstractPreferenceController;
@@ -45,13 +53,15 @@ import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
import com.android.settingslib.core.lifecycle.events.OnResume;
+import java.util.ArrayList;
import java.util.List;
/**
* This class adds stylus preferences.
*/
public class StylusDevicesController extends AbstractPreferenceController implements
- Preference.OnPreferenceClickListener, LifecycleObserver, OnResume {
+ Preference.OnPreferenceClickListener, Preference.OnPreferenceChangeListener,
+ LifecycleObserver, OnResume {
@VisibleForTesting
static final String KEY_STYLUS = "device_stylus";
@@ -73,6 +83,9 @@ public class StylusDevicesController extends AbstractPreferenceController implem
@VisibleForTesting
PreferenceCategory mPreferencesContainer;
+ @VisibleForTesting
+ Dialog mDialog;
+
public StylusDevicesController(Context context, InputDevice inputDevice,
CachedBluetoothDevice cachedBluetoothDevice, Lifecycle lifecycle) {
super(context);
@@ -100,8 +113,8 @@ public class StylusDevicesController extends AbstractPreferenceController implem
pref.setOnPreferenceClickListener(this);
pref.setEnabled(true);
- List roleHolders = rm.getRoleHoldersAsUser(RoleManager.ROLE_NOTES,
- mContext.getUser());
+ UserHandle user = getDefaultNoteTaskProfile();
+ List roleHolders = rm.getRoleHoldersAsUser(RoleManager.ROLE_NOTES, user);
if (roleHolders.isEmpty()) {
pref.setSummary(R.string.default_app_none);
return pref;
@@ -113,19 +126,29 @@ public class StylusDevicesController extends AbstractPreferenceController implem
try {
ApplicationInfo ai = pm.getApplicationInfo(packageName,
PackageManager.ApplicationInfoFlags.of(0));
- appName = ai == null ? packageName : pm.getApplicationLabel(ai).toString();
+ appName = ai == null ? "" : pm.getApplicationLabel(ai).toString();
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Notes role package not found.");
}
- pref.setSummary(appName);
+
+ if (mContext.getSystemService(UserManager.class).isManagedProfile(user.getIdentifier())) {
+ pref.setSummary(
+ mContext.getString(R.string.stylus_default_notes_summary_work, appName));
+ } else {
+ pref.setSummary(appName);
+ }
return pref;
}
- private SwitchPreference createOrUpdateHandwritingPreference(SwitchPreference preference) {
- SwitchPreference pref = preference == null ? new SwitchPreference(mContext) : preference;
+ private PrimarySwitchPreference createOrUpdateHandwritingPreference(
+ PrimarySwitchPreference preference) {
+ PrimarySwitchPreference pref = preference == null ? new PrimarySwitchPreference(mContext)
+ : preference;
pref.setKey(KEY_HANDWRITING);
pref.setTitle(mContext.getString(R.string.stylus_textfield_handwriting));
pref.setIcon(R.drawable.ic_text_fields_alt);
+ // Using a two-target preference, clicking will send an intent and change will toggle.
+ pref.setOnPreferenceChangeListener(this);
pref.setOnPreferenceClickListener(this);
pref.setChecked(Settings.Secure.getInt(mContext.getContentResolver(),
Settings.Secure.STYLUS_HANDWRITING_ENABLED,
@@ -148,30 +171,28 @@ public class StylusDevicesController extends AbstractPreferenceController implem
@Override
public boolean onPreferenceClick(Preference preference) {
String key = preference.getKey();
-
switch (key) {
case KEY_DEFAULT_NOTES:
PackageManager pm = mContext.getPackageManager();
String packageName = pm.getPermissionControllerPackageName();
Intent intent = new Intent(Intent.ACTION_MANAGE_DEFAULT_APP).setPackage(
packageName).putExtra(Intent.EXTRA_ROLE_NAME, RoleManager.ROLE_NOTES);
- mContext.startActivity(intent);
+
+ List users = getUserAndManagedProfiles();
+ if (users.size() <= 1) {
+ mContext.startActivity(intent);
+ } else {
+ createAndShowProfileSelectDialog(intent, users);
+ }
break;
case KEY_HANDWRITING:
- Settings.Secure.putInt(mContext.getContentResolver(),
- Settings.Secure.STYLUS_HANDWRITING_ENABLED,
- ((SwitchPreference) preference).isChecked() ? 1 : 0);
-
- if (((SwitchPreference) preference).isChecked()) {
- InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
- InputMethodInfo inputMethod = imm.getCurrentInputMethodInfo();
- if (inputMethod == null) break;
-
- Intent handwritingIntent =
- inputMethod.createStylusHandwritingSettingsActivityIntent();
- if (handwritingIntent != null) {
- mContext.startActivity(handwritingIntent);
- }
+ InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
+ InputMethodInfo inputMethod = imm.getCurrentInputMethodInfo();
+ if (inputMethod == null) break;
+ Intent handwritingIntent =
+ inputMethod.createStylusHandwritingSettingsActivityIntent();
+ if (handwritingIntent != null) {
+ mContext.startActivity(handwritingIntent);
}
break;
case KEY_IGNORE_BUTTON:
@@ -183,6 +204,19 @@ public class StylusDevicesController extends AbstractPreferenceController implem
return true;
}
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ String key = preference.getKey();
+ switch (key) {
+ case KEY_HANDWRITING:
+ Settings.Secure.putInt(mContext.getContentResolver(),
+ Settings.Secure.STYLUS_HANDWRITING_ENABLED,
+ (boolean) newValue ? 1 : 0);
+ break;
+ }
+ return true;
+ }
+
@Override
public final void displayPreference(PreferenceScreen screen) {
mPreferencesContainer = (PreferenceCategory) screen.findPreference(getPreferenceKey());
@@ -210,7 +244,7 @@ public class StylusDevicesController extends AbstractPreferenceController implem
mPreferencesContainer.addPreference(notesPref);
}
- SwitchPreference currHandwritingPref = mPreferencesContainer.findPreference(
+ PrimarySwitchPreference currHandwritingPref = mPreferencesContainer.findPreference(
KEY_HANDWRITING);
Preference handwritingPref = createOrUpdateHandwritingPreference(currHandwritingPref);
if (currHandwritingPref == null) {
@@ -229,6 +263,56 @@ public class StylusDevicesController extends AbstractPreferenceController implem
return inputMethod != null && inputMethod.supportsStylusHandwriting();
}
+ private List getUserAndManagedProfiles() {
+ UserManager um = mContext.getSystemService(UserManager.class);
+ final List userManagedProfiles = new ArrayList<>();
+ // Add the current user, then add all the associated managed profiles.
+ final UserHandle currentUser = Process.myUserHandle();
+ userManagedProfiles.add(currentUser);
+
+ final List userInfos = um.getUsers();
+ for (UserInfo info : userInfos) {
+ int userId = info.id;
+ if (um.isManagedProfile(userId)
+ && um.getProfileParent(userId).id == currentUser.getIdentifier()) {
+ userManagedProfiles.add(UserHandle.of(userId));
+ }
+ }
+ return userManagedProfiles;
+ }
+
+ private UserHandle getDefaultNoteTaskProfile() {
+ final int userId = Secure.getInt(
+ mContext.getContentResolver(),
+ Secure.DEFAULT_NOTE_TASK_PROFILE,
+ UserHandle.myUserId());
+ return UserHandle.of(userId);
+ }
+
+ @VisibleForTesting
+ UserAdapter.OnClickListener createProfileDialogClickCallback(
+ Intent intent, List users) {
+ // TODO(b/281659827): improve UX flow for when activity is cancelled
+ return (int position) -> {
+ intent.putExtra(Intent.EXTRA_USER, users.get(position));
+
+ Secure.putInt(mContext.getContentResolver(),
+ Secure.DEFAULT_NOTE_TASK_PROFILE,
+ users.get(position).getIdentifier());
+ mContext.startActivity(intent);
+
+ mDialog.dismiss();
+ };
+ }
+
+ private void createAndShowProfileSelectDialog(Intent intent, List users) {
+ mDialog = ProfileSelectDialog.createDialog(
+ mContext,
+ users,
+ createProfileDialogClickCallback(intent, users));
+ mDialog.show();
+ }
+
/**
* Identifies whether a device is a stylus using the associated {@link InputDevice} or
* {@link CachedBluetoothDevice}.
@@ -255,5 +339,4 @@ public class StylusDevicesController extends AbstractPreferenceController implem
return false;
}
-
}
diff --git a/src/com/android/settings/connecteddevice/stylus/StylusFeatureProvider.java b/src/com/android/settings/connecteddevice/stylus/StylusFeatureProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..7ca35d87f45b2bc1bccb82f132e6a354b792c126
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/stylus/StylusFeatureProvider.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.stylus;
+
+import android.content.Context;
+import android.hardware.usb.UsbDevice;
+
+import androidx.preference.Preference;
+
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+/** FeatureProvider for USB settings */
+public interface StylusFeatureProvider {
+
+ /**
+ * Returns whether the current attached USB device allows firmware updates.
+ *
+ * @param usbDevice The USB device to check
+ */
+ boolean isUsbFirmwareUpdateEnabled(UsbDevice usbDevice);
+
+ /**
+ * Returns a list of preferences for the connected USB device if exists. If not, returns
+ * null. If an update is not available but firmware update feature is enabled for the device,
+ * the list will contain only the preference showing the current firmware version.
+ *
+ * @param context The context
+ * @param usbDevice The USB device for which to generate preferences.
+ */
+ @Nullable
+ List getUsbFirmwareUpdatePreferences(Context context, UsbDevice usbDevice);
+}
diff --git a/src/com/android/settings/connecteddevice/stylus/StylusFeatureProviderImpl.java b/src/com/android/settings/connecteddevice/stylus/StylusFeatureProviderImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..be5ae407d0717f05b8afcb74fc8add6a69a7a99e
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/stylus/StylusFeatureProviderImpl.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.stylus;
+
+import android.content.Context;
+import android.hardware.usb.UsbDevice;
+
+import androidx.preference.Preference;
+
+import java.util.List;
+
+/** Default implementation for StylusFeatureProvider */
+public class StylusFeatureProviderImpl implements StylusFeatureProvider {
+ @Override
+ public boolean isUsbFirmwareUpdateEnabled(UsbDevice usbDevice) {
+ return false;
+ }
+
+ @Override
+ public List getUsbFirmwareUpdatePreferences(Context context, UsbDevice usbDevice) {
+ return null;
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/stylus/StylusUsbFirmwareController.java b/src/com/android/settings/connecteddevice/stylus/StylusUsbFirmwareController.java
new file mode 100644
index 0000000000000000000000000000000000000000..9c567a46c81cddd6c6d2a52d2fe12e61df089098
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/stylus/StylusUsbFirmwareController.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.stylus;
+
+import android.content.Context;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
+import androidx.preference.PreferenceScreen;
+
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.overlay.FeatureFactory;
+import com.android.settingslib.core.lifecycle.LifecycleObserver;
+import com.android.settingslib.core.lifecycle.events.OnStart;
+import com.android.settingslib.core.lifecycle.events.OnStop;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Preference controller for stylus firmware updates via USB */
+public class StylusUsbFirmwareController extends BasePreferenceController
+ implements LifecycleObserver, OnStart, OnStop {
+ private static final String TAG = StylusUsbFirmwareController.class.getSimpleName();
+ @Nullable
+ private UsbDevice mStylusUsbDevice;
+ private final UsbStylusBroadcastReceiver mUsbStylusBroadcastReceiver;
+
+ private PreferenceScreen mPreferenceScreen;
+ private PreferenceCategory mPreference;
+
+ @VisibleForTesting
+ UsbStylusBroadcastReceiver.UsbStylusConnectionListener mUsbConnectionListener =
+ (stylusUsbDevice, attached) -> {
+ refresh();
+ };
+
+ public StylusUsbFirmwareController(Context context, String key) {
+ super(context, key);
+ mUsbStylusBroadcastReceiver = new UsbStylusBroadcastReceiver(context,
+ mUsbConnectionListener);
+ }
+
+ @Override
+ public void displayPreference(PreferenceScreen screen) {
+ mPreferenceScreen = screen;
+ refresh();
+ super.displayPreference(screen);
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ // always available, preferences will be added or
+ // removed according to the connected usb device
+ return AVAILABLE;
+ }
+
+ private void refresh() {
+ if (mPreferenceScreen == null) return;
+
+ UsbDevice device = getStylusUsbDevice();
+ if (device == mStylusUsbDevice) {
+ return;
+ }
+ mStylusUsbDevice = device;
+ mPreference = mPreferenceScreen.findPreference(getPreferenceKey());
+ if (mPreference != null) {
+ mPreferenceScreen.removePreference(mPreference);
+ }
+ if (hasUsbStylusFirmwareUpdateFeature(mStylusUsbDevice)) {
+ StylusFeatureProvider featureProvider = FeatureFactory.getFactory(
+ mContext).getStylusFeatureProvider();
+ List preferences =
+ featureProvider.getUsbFirmwareUpdatePreferences(mContext, mStylusUsbDevice);
+
+ if (preferences != null) {
+ mPreference = new PreferenceCategory(mContext);
+ mPreference.setKey(getPreferenceKey());
+ mPreferenceScreen.addPreference(mPreference);
+
+ for (Preference preference : preferences) {
+ mPreference.addPreference(preference);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onStart() {
+ mUsbStylusBroadcastReceiver.register();
+ }
+
+ @Override
+ public void onStop() {
+ mUsbStylusBroadcastReceiver.unregister();
+ }
+
+ private UsbDevice getStylusUsbDevice() {
+ UsbManager usbManager = mContext.getSystemService(UsbManager.class);
+
+ if (usbManager == null) {
+ return null;
+ }
+
+ List devices = new ArrayList<>(usbManager.getDeviceList().values());
+ if (devices.isEmpty()) {
+ return null;
+ }
+
+ UsbDevice usbDevice = devices.get(0);
+ if (hasUsbStylusFirmwareUpdateFeature(usbDevice)) {
+ return usbDevice;
+ }
+ return null;
+ }
+
+ static boolean hasUsbStylusFirmwareUpdateFeature(UsbDevice usbDevice) {
+ if (usbDevice == null) return false;
+
+ StylusFeatureProvider featureProvider = FeatureFactory.getFactory(
+ FeatureFactory.getAppContext()).getStylusFeatureProvider();
+
+ return featureProvider.isUsbFirmwareUpdateEnabled(usbDevice);
+ }
+}
diff --git a/src/com/android/settings/connecteddevice/stylus/StylusUsiDetailsFragment.java b/src/com/android/settings/connecteddevice/stylus/StylusUsiDetailsFragment.java
index 5e68a537e5d4282acb1b6286a41ae6c37275f544..ea9781e3ce76bcd73546871c922a557294d6c6ad 100644
--- a/src/com/android/settings/connecteddevice/stylus/StylusUsiDetailsFragment.java
+++ b/src/com/android/settings/connecteddevice/stylus/StylusUsiDetailsFragment.java
@@ -54,7 +54,6 @@ public class StylusUsiDetailsFragment extends DashboardFragment {
}
}
-
@Override
public int getMetricsCategory() {
return SettingsEnums.USI_DEVICE_DETAILS;
diff --git a/src/com/android/settings/connecteddevice/stylus/UsbStylusBroadcastReceiver.java b/src/com/android/settings/connecteddevice/stylus/UsbStylusBroadcastReceiver.java
new file mode 100644
index 0000000000000000000000000000000000000000..41d88d2691d90049612eda522435ff2ed967646a
--- /dev/null
+++ b/src/com/android/settings/connecteddevice/stylus/UsbStylusBroadcastReceiver.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 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.settings.connecteddevice.stylus;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.usb.UsbDevice;
+import android.hardware.usb.UsbManager;
+
+/** Broadcast receiver for styluses connected via USB */
+public class UsbStylusBroadcastReceiver extends BroadcastReceiver {
+ private Context mContext;
+ private UsbStylusConnectionListener mUsbConnectionListener;
+ private boolean mListeningToUsbEvents;
+
+ public UsbStylusBroadcastReceiver(Context context,
+ UsbStylusConnectionListener usbConnectionListener) {
+ mContext = context;
+ mUsbConnectionListener = usbConnectionListener;
+ }
+
+ /** Registers the receiver. */
+ public void register() {
+ if (!mListeningToUsbEvents) {
+ final IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
+ intentFilter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
+ intentFilter.addAction(UsbManager.ACTION_USB_STATE);
+ final Intent intent = mContext.registerReceiver(this, intentFilter);
+ if (intent != null) {
+ onReceive(mContext, intent);
+ }
+ mListeningToUsbEvents = true;
+ }
+ }
+
+ /** Unregisters the receiver. */
+ public void unregister() {
+ if (mListeningToUsbEvents) {
+ mContext.unregisterReceiver(this);
+ mListeningToUsbEvents = false;
+ }
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE, UsbDevice.class);
+ if (StylusUsbFirmwareController.hasUsbStylusFirmwareUpdateFeature(usbDevice)) {
+ mUsbConnectionListener.onUsbStylusConnectionChanged(usbDevice,
+ intent.getAction().equals(UsbManager.ACTION_USB_DEVICE_ATTACHED));
+ }
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when stylus usb connection is changed.
+ */
+ interface UsbStylusConnectionListener {
+ void onUsbStylusConnectionChanged(UsbDevice device, boolean connected);
+ }
+}
diff --git a/src/com/android/settings/core/SettingsUIDeviceConfig.java b/src/com/android/settings/core/SettingsUIDeviceConfig.java
index 404b0b4ef3934ce14716ee0eb919dcd42c572283..94074dfa3354bb0b307c3b63d7d1ac7bd39d13ce 100644
--- a/src/com/android/settings/core/SettingsUIDeviceConfig.java
+++ b/src/com/android/settings/core/SettingsUIDeviceConfig.java
@@ -42,9 +42,4 @@ public class SettingsUIDeviceConfig {
* {@code true} whether or not event_log for generic actions is enabled. Default is true.
*/
public static final String GENERIC_EVENT_LOGGING_ENABLED = "event_logging_enabled";
- /**
- * {@code true} whether to show LE Audio toggle in device detail page. Default is false.
- */
- public static final String BT_LE_AUDIO_DEVICE_DETAIL_ENABLED =
- "bt_le_audio_device_detail_enabled";
}
diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java
index ff4b47fec0279d43e33c5edce687a13aef0e83e2..7c8a3cc1c0a0672d82358f77c648320720c0ce6a 100644
--- a/src/com/android/settings/core/gateway/SettingsGateway.java
+++ b/src/com/android/settings/core/gateway/SettingsGateway.java
@@ -42,6 +42,7 @@ import com.android.settings.applications.AppDashboardFragment;
import com.android.settings.applications.ProcessStatsSummary;
import com.android.settings.applications.ProcessStatsUi;
import com.android.settings.applications.UsageAccessDetails;
+import com.android.settings.applications.appcompat.UserAspectRatioDetails;
import com.android.settings.applications.appinfo.AlarmsAndRemindersDetails;
import com.android.settings.applications.appinfo.AppInfoDashboardFragment;
import com.android.settings.applications.appinfo.AppLocaleDetails;
@@ -72,6 +73,7 @@ import com.android.settings.biometrics.combination.CombinedBiometricProfileSetti
import com.android.settings.biometrics.combination.CombinedBiometricSettings;
import com.android.settings.biometrics.face.FaceSettings;
import com.android.settings.biometrics.fingerprint.FingerprintSettings;
+import com.android.settings.biometrics.fingerprint2.ui.fragment.FingerprintSettingsV2Fragment;
import com.android.settings.bluetooth.BluetoothBroadcastDialog;
import com.android.settings.bluetooth.BluetoothDeviceDetailsFragment;
import com.android.settings.bluetooth.BluetoothFindBroadcastsFragment;
@@ -94,6 +96,7 @@ import com.android.settings.deviceinfo.PrivateVolumeForget;
import com.android.settings.deviceinfo.PublicVolumeSettings;
import com.android.settings.deviceinfo.StorageDashboardFragment;
import com.android.settings.deviceinfo.aboutphone.MyDeviceInfoFragment;
+import com.android.settings.deviceinfo.batteryinfo.BatteryInfoFragment;
import com.android.settings.deviceinfo.firmwareversion.FirmwareVersionSettings;
import com.android.settings.deviceinfo.legal.ModuleLicensesDashboard;
import com.android.settings.display.AutoBrightnessSettings;
@@ -263,6 +266,7 @@ public class SettingsGateway {
AssistGestureSettings.class.getName(),
FaceSettings.class.getName(),
FingerprintSettings.FingerprintSettingsFragment.class.getName(),
+ FingerprintSettingsV2Fragment.class.getName(),
CombinedBiometricSettings.class.getName(),
CombinedBiometricProfileSettings.class.getName(),
SwipeToNotificationSettings.class.getName(),
@@ -369,7 +373,9 @@ public class SettingsGateway {
NfcAndPaymentFragment.class.getName(),
ColorAndMotionFragment.class.getName(),
LongBackgroundTasksDetails.class.getName(),
- RegionalPreferencesEntriesFragment.class.getName()
+ RegionalPreferencesEntriesFragment.class.getName(),
+ BatteryInfoFragment.class.getName(),
+ UserAspectRatioDetails.class.getName()
};
public static final String[] SETTINGS_FOR_RESTRICTED = {
diff --git a/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java b/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java
index 4dc8f1af2078ed20a4154abd89d7faab1bd0b35c..578493a47d216ee42258f8ca966111a1307d1348 100644
--- a/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java
+++ b/src/com/android/settings/dashboard/DashboardFeatureProviderImpl.java
@@ -33,6 +33,7 @@ import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SWIT
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE;
import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI;
+import android.app.PendingIntent;
import android.app.settings.SettingsEnums;
import android.content.ComponentName;
import android.content.Context;
@@ -75,6 +76,8 @@ import com.android.settingslib.drawer.TileUtils;
import com.android.settingslib.utils.ThreadUtils;
import com.android.settingslib.widget.AdaptiveIcon;
+import com.google.common.collect.Iterables;
+
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -152,7 +155,14 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider {
}
bindIcon(pref, tile, forceRoundedIcon);
- if (tile instanceof ActivityTile) {
+ if (tile.hasPendingIntent()) {
+ // Pending intent cannot be launched within the settings app panel, and will thus always
+ // be executed directly.
+ pref.setOnPreferenceClickListener(preference -> {
+ launchPendingIntentOrSelectProfile(activity, tile, fragment.getMetricsCategory());
+ return true;
+ });
+ } else if (tile instanceof ActivityTile) {
final int sourceMetricsCategory = fragment.getMetricsCategory();
final Bundle metadata = tile.getMetaData();
String clsName = null;
@@ -441,6 +451,33 @@ public class DashboardFeatureProviderImpl implements DashboardFeatureProvider {
preference.setIcon(iconDrawable);
}
+ private void launchPendingIntentOrSelectProfile(FragmentActivity activity, Tile tile,
+ int sourceMetricCategory) {
+ ProfileSelectDialog.updatePendingIntentsIfNeeded(mContext, tile);
+
+ if (tile.pendingIntentMap.isEmpty()) {
+ Log.w(TAG, "Cannot resolve pendingIntent, skipping. " + tile.getIntent());
+ return;
+ }
+
+ mMetricsFeatureProvider.logSettingsTileClick(tile.getKey(mContext), sourceMetricCategory);
+
+ // Launch the pending intent directly if there's only one available.
+ if (tile.pendingIntentMap.size() == 1) {
+ PendingIntent pendingIntent = Iterables.getOnlyElement(tile.pendingIntentMap.values());
+ try {
+ pendingIntent.send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.w(TAG, "Failed executing pendingIntent. " + pendingIntent.getIntent(), e);
+ }
+ return;
+ }
+
+ ProfileSelectDialog.show(activity.getSupportFragmentManager(), tile,
+ sourceMetricCategory, /* onShowListener= */ null,
+ /* onDismissListener= */ null, /* onCancelListener= */ null);
+ }
+
private void launchIntentOrSelectProfile(FragmentActivity activity, Tile tile, Intent intent,
int sourceMetricCategory, TopLevelHighlightMixin highlightMixin,
boolean isDuplicateClick) {
diff --git a/src/com/android/settings/dashboard/DashboardFragment.java b/src/com/android/settings/dashboard/DashboardFragment.java
index 6076a25ddc1eb3c733fcfd469c7daa2ccfc97821..d4acfa11c570bb3e985dcfa19445d2962acd8715 100644
--- a/src/com/android/settings/dashboard/DashboardFragment.java
+++ b/src/com/android/settings/dashboard/DashboardFragment.java
@@ -25,12 +25,16 @@ import android.preference.PreferenceManager.OnActivityResultListener;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
+import android.view.View;
import androidx.annotation.CallSuper;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LifecycleObserver;
+import androidx.lifecycle.LifecycleOwner;
import androidx.preference.Preference;
+import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceGroup;
import androidx.preference.PreferenceManager;
import androidx.preference.PreferenceScreen;
@@ -47,7 +51,6 @@ import com.android.settingslib.PrimarySwitchPreference;
import com.android.settingslib.core.AbstractPreferenceController;
import com.android.settingslib.core.lifecycle.Lifecycle;
import com.android.settingslib.drawer.DashboardCategory;
-import com.android.settingslib.drawer.ProviderTile;
import com.android.settingslib.drawer.Tile;
import com.android.settingslib.search.Indexable;
@@ -55,6 +58,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -168,6 +172,15 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment
}
}
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ LifecycleOwner viewLifecycleOwner = getViewLifecycleOwner();
+ for (AbstractPreferenceController controller : mControllers) {
+ controller.onViewCreated(viewLifecycleOwner);
+ }
+ }
+
@Override
public void onCategoriesChanged(Set categories) {
final String categoryKey = getCategoryKey();
@@ -504,6 +517,10 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment
// Install dashboard tiles and collect pending observers.
final boolean forceRoundedIcons = shouldForceRoundedIcon();
final List pendingObservers = new ArrayList<>();
+
+ // Move group tiles to the beginning of the list to ensure they are created before the
+ // other tiles.
+ tiles.sort(Comparator.comparingInt(tile -> tile.getType() == Tile.Type.GROUP ? 0 : 1));
for (Tile tile : tiles) {
final String key = mDashboardFeatureProvider.getDashboardKeyForTile(tile);
if (TextUtils.isEmpty(key)) {
@@ -526,7 +543,14 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment
observers = mDashboardFeatureProvider.bindPreferenceToTileAndGetObservers(
getActivity(), this, forceRoundedIcons, pref, tile, key,
mPlaceholderPreferenceController.getOrder());
- screen.addPreference(pref);
+ if (tile.hasGroupKey() && mDashboardTilePrefKeys.containsKey(tile.getGroupKey())) {
+ final Preference group = screen.findPreference(tile.getGroupKey());
+ if (group instanceof PreferenceCategory) {
+ ((PreferenceCategory) group).addPreference(pref);
+ }
+ } else {
+ screen.addPreference(pref);
+ }
registerDynamicDataObservers(observers);
mDashboardTilePrefKeys.put(key, observers);
}
@@ -569,11 +593,28 @@ public abstract class DashboardFragment extends SettingsPreferenceFragment
}
protected Preference createPreference(Tile tile) {
- return tile instanceof ProviderTile
- ? new SwitchPreference(getPrefContext())
- : tile.hasSwitch()
- ? new PrimarySwitchPreference(getPrefContext())
- : new Preference(getPrefContext());
+ switch (tile.getType()) {
+ case EXTERNAL_ACTION:
+ Preference externalActionPreference = new Preference(getPrefContext());
+ externalActionPreference
+ .setWidgetLayoutResource(R.layout.preference_external_action_icon);
+ return externalActionPreference;
+ case SWITCH:
+ return new SwitchPreference(getPrefContext());
+ case SWITCH_WITH_ACTION:
+ return new PrimarySwitchPreference(getPrefContext());
+ case GROUP:
+ mMetricsFeatureProvider.action(
+ mMetricsFeatureProvider.getAttribution(getActivity()),
+ SettingsEnums.ACTION_SETTINGS_GROUP_TILE_ADDED_TO_SCREEN,
+ getMetricsCategory(),
+ tile.getKey(getContext()),
+ /* value= */ 0);
+ return new PreferenceCategory((getPrefContext()));
+ case ACTION:
+ default:
+ return new Preference(getPrefContext());
+ }
}
@VisibleForTesting
diff --git a/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java b/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java
index ef6ad8324770e0f1ecd3d3bbf67cf2c1a249dfb9..58a51cb32dcbfa3a73f626cc0560984405551bc2 100644
--- a/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java
+++ b/src/com/android/settings/dashboard/profileselector/ProfileSelectDialog.java
@@ -17,6 +17,7 @@
package com.android.settings.dashboard.profileselector;
import android.app.Dialog;
+import android.app.PendingIntent;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
@@ -127,13 +128,25 @@ public class ProfileSelectDialog extends DialogFragment implements UserAdapter.O
@Override
public void onClick(int position) {
final UserHandle user = mSelectedTile.userHandle.get(position);
- // Show menu on top level items.
- final Intent intent = new Intent(mSelectedTile.getIntent());
- FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider()
- .logStartedIntentWithProfile(intent, mSourceMetricCategory,
- position == 1 /* isWorkProfile */);
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
- getActivity().startActivityAsUser(intent, user);
+ if (!mSelectedTile.hasPendingIntent()) {
+ final Intent intent = new Intent(mSelectedTile.getIntent());
+ FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider()
+ .logStartedIntentWithProfile(intent, mSourceMetricCategory,
+ position == 1 /* isWorkProfile */);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ getActivity().startActivityAsUser(intent, user);
+ } else {
+ PendingIntent pendingIntent = mSelectedTile.pendingIntentMap.get(user);
+ FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider()
+ .logSettingsTileClickWithProfile(mSelectedTile.getKey(getContext()),
+ mSourceMetricCategory,
+ position == 1 /* isWorkProfile */);
+ try {
+ pendingIntent.send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.w(TAG, "Failed executing pendingIntent. " + pendingIntent.getIntent(), e);
+ }
+ }
dismiss();
}
@@ -178,4 +191,36 @@ public class ProfileSelectDialog extends DialogFragment implements UserAdapter.O
}
}
}
+
+ /**
+ * Checks the userHandle and pendingIntentMap in the provided tile, and remove the invalid
+ * entries if any.
+ */
+ public static void updatePendingIntentsIfNeeded(Context context, Tile tile) {
+ if (tile.userHandle == null || tile.userHandle.size() <= 1
+ || tile.pendingIntentMap.size() <= 1) {
+ return;
+ }
+ for (UserHandle userHandle : List.copyOf(tile.userHandle)) {
+ if (!tile.pendingIntentMap.containsKey(userHandle)) {
+ if (DEBUG) {
+ Log.d(TAG, "Delete the user without pending intent: "
+ + userHandle.getIdentifier());
+ }
+ tile.userHandle.remove(userHandle);
+ }
+ }
+
+ final UserManager userManager = UserManager.get(context);
+ for (UserHandle userHandle : List.copyOf(tile.pendingIntentMap.keySet())) {
+ UserInfo userInfo = userManager.getUserInfo(userHandle.getIdentifier());
+ if (userInfo == null || userInfo.isCloneProfile()) {
+ if (DEBUG) {
+ Log.d(TAG, "Delete the user: " + userHandle.getIdentifier());
+ }
+ tile.userHandle.remove(userHandle);
+ tile.pendingIntentMap.remove(userHandle);
+ }
+ }
+ }
}
diff --git a/src/com/android/settings/datausage/BillingCycleSettings.java b/src/com/android/settings/datausage/BillingCycleSettings.java
index 3047d73dddf76e8fc3f5242222940affd5339870..c3ddb2eac2dce25710e51628a2847857375e28a5 100644
--- a/src/com/android/settings/datausage/BillingCycleSettings.java
+++ b/src/com/android/settings/datausage/BillingCycleSettings.java
@@ -22,8 +22,6 @@ import android.app.settings.SettingsEnums;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
-import android.icu.text.MeasureFormat;
-import android.icu.util.MeasureUnit;
import android.net.NetworkPolicy;
import android.net.NetworkTemplate;
import android.os.Bundle;
@@ -322,14 +320,10 @@ public class BillingCycleSettings extends DataUsageBaseFragment implements
final boolean isLimit = getArguments().getBoolean(EXTRA_LIMIT);
final long bytes = isLimit ? editor.getPolicyLimitBytes(template)
: editor.getPolicyWarningBytes(template);
- final long limitDisabled = isLimit ? LIMIT_DISABLED : WARNING_DISABLED;
- final MeasureFormat formatter = MeasureFormat.getInstance(
- getContext().getResources().getConfiguration().locale,
- MeasureFormat.FormatWidth.SHORT);
final String[] unitNames = new String[] {
- formatter.getUnitDisplayName(MeasureUnit.MEGABYTE),
- formatter.getUnitDisplayName(MeasureUnit.GIGABYTE)
+ DataUsageFormatter.INSTANCE.getBytesDisplayUnit(getResources(), MIB_IN_BYTES),
+ DataUsageFormatter.INSTANCE.getBytesDisplayUnit(getResources(), GIB_IN_BYTES),
};
final ArrayAdapter adapter = new ArrayAdapter(
getContext(), android.R.layout.simple_spinner_item, unitNames);
diff --git a/src/com/android/settings/datausage/DataSaverBackend.java b/src/com/android/settings/datausage/DataSaverBackend.java
index e47ecbdc997e77bf00de8894c6db0a6891bc7011..6a392341122d1feff172e8fa9827e2a5c63e3847 100644
--- a/src/com/android/settings/datausage/DataSaverBackend.java
+++ b/src/com/android/settings/datausage/DataSaverBackend.java
@@ -196,8 +196,10 @@ public class DataSaverBackend {
public interface Listener {
void onDataSaverChanged(boolean isDataSaving);
- void onAllowlistStatusChanged(int uid, boolean isAllowlisted);
+ /** This is called when allow list status is changed. */
+ default void onAllowlistStatusChanged(int uid, boolean isAllowlisted) {}
- void onDenylistStatusChanged(int uid, boolean isDenylisted);
+ /** This is called when deny list status is changed. */
+ default void onDenylistStatusChanged(int uid, boolean isDenylisted) {}
}
}
diff --git a/src/com/android/settings/datausage/DataSaverSummary.kt b/src/com/android/settings/datausage/DataSaverSummary.kt
index 1d9cbb73a6683a018a034807a1223e3cd08cfeb8..0828d3624105a3a0e72e1fee3ce1ceae22e6cae4 100644
--- a/src/com/android/settings/datausage/DataSaverSummary.kt
+++ b/src/com/android/settings/datausage/DataSaverSummary.kt
@@ -15,33 +15,22 @@
*/
package com.android.settings.datausage
-import android.app.Application
import android.app.settings.SettingsEnums
import android.content.Context
import android.os.Bundle
import android.telephony.SubscriptionManager
import android.widget.Switch
-import androidx.lifecycle.lifecycleScope
-import androidx.preference.Preference
import com.android.settings.R
import com.android.settings.SettingsActivity
-import com.android.settings.SettingsPreferenceFragment
-import com.android.settings.applications.AppStateBaseBridge
-import com.android.settings.datausage.AppStateDataUsageBridge.DataUsageState
+import com.android.settings.dashboard.DashboardFragment
import com.android.settings.search.BaseSearchIndexProvider
import com.android.settings.widget.SettingsMainSwitchBar
-import com.android.settingslib.applications.ApplicationsState
import com.android.settingslib.search.SearchIndexable
-import com.android.settingslib.spa.framework.util.formatString
-import kotlinx.coroutines.launch
@SearchIndexable
-class DataSaverSummary : SettingsPreferenceFragment() {
+class DataSaverSummary : DashboardFragment() {
private lateinit var switchBar: SettingsMainSwitchBar
private lateinit var dataSaverBackend: DataSaverBackend
- private lateinit var unrestrictedAccess: Preference
- private var dataUsageBridge: AppStateDataUsageBridge? = null
- private var session: ApplicationsState.Session? = null
// Flag used to avoid infinite loop due if user switch it on/off too quick.
private var switching = false
@@ -54,8 +43,6 @@ class DataSaverSummary : SettingsPreferenceFragment() {
return
}
- addPreferencesFromResource(R.xml.data_saver)
- unrestrictedAccess = findPreference(KEY_UNRESTRICTED_ACCESS)!!
dataSaverBackend = DataSaverBackend(requireContext())
}
@@ -72,27 +59,12 @@ class DataSaverSummary : SettingsPreferenceFragment() {
override fun onResume() {
super.onResume()
- dataSaverBackend.refreshAllowlist()
- dataSaverBackend.refreshDenylist()
dataSaverBackend.addListener(dataSaverBackendListener)
- dataUsageBridge?.resume(/* forceLoadAllApps= */ true)
- ?: viewLifecycleOwner.lifecycleScope.launch {
- val applicationsState = ApplicationsState.getInstance(
- requireContext().applicationContext as Application
- )
- dataUsageBridge = AppStateDataUsageBridge(
- applicationsState, dataUsageBridgeCallbacks, dataSaverBackend
- )
- session =
- applicationsState.newSession(applicationsStateCallbacks, settingsLifecycle)
- dataUsageBridge?.resume(/* forceLoadAllApps= */ true)
- }
}
override fun onPause() {
super.onPause()
dataSaverBackend.remListener(dataSaverBackendListener)
- dataUsageBridge?.pause()
}
private fun onSwitchChanged(isChecked: Boolean) {
@@ -104,9 +76,10 @@ class DataSaverSummary : SettingsPreferenceFragment() {
}
}
+ override fun getPreferenceScreenResId() = R.xml.data_saver
override fun getMetricsCategory() = SettingsEnums.DATA_SAVER_SUMMARY
-
override fun getHelpResource() = R.string.help_url_data_saver
+ override fun getLogTag() = TAG
private val dataSaverBackendListener = object : DataSaverBackend.Listener {
override fun onDataSaverChanged(isDataSaving: Boolean) {
@@ -115,51 +88,10 @@ class DataSaverSummary : SettingsPreferenceFragment() {
switching = false
}
}
-
- override fun onAllowlistStatusChanged(uid: Int, isAllowlisted: Boolean) {}
-
- override fun onDenylistStatusChanged(uid: Int, isDenylisted: Boolean) {}
- }
-
- private val dataUsageBridgeCallbacks = AppStateBaseBridge.Callback {
- updateUnrestrictedAccessSummary()
- }
-
- private val applicationsStateCallbacks = object : ApplicationsState.Callbacks {
- override fun onRunningStateChanged(running: Boolean) {}
-
- override fun onPackageListChanged() {}
-
- override fun onRebuildComplete(apps: ArrayList?) {}
-
- override fun onPackageIconChanged() {}
-
- override fun onPackageSizeChanged(packageName: String?) {}
-
- override fun onAllSizesComputed() {
- updateUnrestrictedAccessSummary()
- }
-
- override fun onLauncherInfoChanged() {
- updateUnrestrictedAccessSummary()
- }
-
- override fun onLoadEntriesCompleted() {}
- }
-
- private fun updateUnrestrictedAccessSummary() {
- if (!isAdded || isFinishingOrDestroyed) return
- val allApps = session?.allApps ?: return
- val count = allApps.count {
- ApplicationsState.FILTER_DOWNLOADED_AND_LAUNCHER.filterApp(it) &&
- (it.extraInfo as? DataUsageState)?.isDataSaverAllowlisted == true
- }
- unrestrictedAccess.summary =
- resources.formatString(R.string.data_saver_unrestricted_summary, "count" to count)
}
companion object {
- private const val KEY_UNRESTRICTED_ACCESS = "unrestricted_access"
+ private const val TAG = "DataSaverSummary"
private fun Context.isDataSaverVisible(): Boolean =
resources.getBoolean(R.bool.config_show_data_saver)
diff --git a/src/com/android/settings/datausage/DataUsageFormatter.kt b/src/com/android/settings/datausage/DataUsageFormatter.kt
new file mode 100644
index 0000000000000000000000000000000000000000..16a9ae8b6b0069fea80bc2ef584d4f906770d330
--- /dev/null
+++ b/src/com/android/settings/datausage/DataUsageFormatter.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 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.settings.datausage
+
+import android.content.res.Resources
+import android.text.format.Formatter
+
+object DataUsageFormatter {
+
+ /**
+ * Gets the display unit of the given bytes.
+ *
+ * Similar to MeasureFormat.getUnitDisplayName(), but with the expected result for the bytes in
+ * Settings, and align with other places in Settings.
+ */
+ fun Resources.getBytesDisplayUnit(bytes: Long): String =
+ Formatter.formatBytes(this, bytes, Formatter.FLAG_IEC_UNITS).units
+}
\ No newline at end of file
diff --git a/src/com/android/settings/development/BluetoothLeAudioDeviceDetailsPreferenceController.java b/src/com/android/settings/development/BluetoothLeAudioDeviceDetailsPreferenceController.java
index a54c594c84d68e9634b5a2d40feb7492f4145590..980bdaa8f5e7ac12dfbaf39079e35bb640508d45 100644
--- a/src/com/android/settings/development/BluetoothLeAudioDeviceDetailsPreferenceController.java
+++ b/src/com/android/settings/development/BluetoothLeAudioDeviceDetailsPreferenceController.java
@@ -20,6 +20,7 @@ import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothStatusCodes;
import android.content.Context;
+import android.os.SystemProperties;
import android.provider.DeviceConfig;
import androidx.annotation.VisibleForTesting;
@@ -27,7 +28,6 @@ import androidx.preference.Preference;
import androidx.preference.SwitchPreference;
import com.android.settings.core.PreferenceControllerMixin;
-import com.android.settings.core.SettingsUIDeviceConfig;
import com.android.settingslib.development.DeveloperOptionsPreferenceController;
/**
@@ -40,8 +40,12 @@ public class BluetoothLeAudioDeviceDetailsPreferenceController
private static final String PREFERENCE_KEY = "bluetooth_show_leaudio_device_details";
private static final String CONFIG_LE_AUDIO_ENABLED_BY_DEFAULT = "le_audio_enabled_by_default";
+ private static final boolean LE_AUDIO_TOGGLE_VISIBLE_DEFAULT_VALUE = true;
static int sLeAudioSupportedStateCache = BluetoothStatusCodes.ERROR_UNKNOWN;
+ static final String LE_AUDIO_TOGGLE_VISIBLE_PROPERTY =
+ "persist.bluetooth.leaudio.toggle_visible";
+
@VisibleForTesting
BluetoothAdapter mBluetoothAdapter;
@@ -72,10 +76,7 @@ public class BluetoothLeAudioDeviceDetailsPreferenceController
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final boolean isEnabled = (Boolean) newValue;
- DeviceConfig.setProperty(
- DeviceConfig.NAMESPACE_SETTINGS_UI,
- SettingsUIDeviceConfig.BT_LE_AUDIO_DEVICE_DETAIL_ENABLED,
- isEnabled ? "true" : "false", false);
+ SystemProperties.set(LE_AUDIO_TOGGLE_VISIBLE_PROPERTY, Boolean.toString(isEnabled));
return true;
}
@@ -85,23 +86,13 @@ public class BluetoothLeAudioDeviceDetailsPreferenceController
return;
}
- final boolean leAudioDeviceDetailEnabled = DeviceConfig.getBoolean(
- DeviceConfig.NAMESPACE_SETTINGS_UI,
- SettingsUIDeviceConfig.BT_LE_AUDIO_DEVICE_DETAIL_ENABLED, false);
+ final boolean isLeAudioToggleVisible = SystemProperties.getBoolean(
+ LE_AUDIO_TOGGLE_VISIBLE_PROPERTY, LE_AUDIO_TOGGLE_VISIBLE_DEFAULT_VALUE);
final boolean leAudioEnabledByDefault = DeviceConfig.getBoolean(
DeviceConfig.NAMESPACE_BLUETOOTH, CONFIG_LE_AUDIO_ENABLED_BY_DEFAULT, false);
mPreference.setEnabled(!leAudioEnabledByDefault);
- ((SwitchPreference) mPreference).setChecked(leAudioDeviceDetailEnabled
+ ((SwitchPreference) mPreference).setChecked(isLeAudioToggleVisible
|| leAudioEnabledByDefault);
}
-
- @Override
- protected void onDeveloperOptionsSwitchDisabled() {
- super.onDeveloperOptionsSwitchDisabled();
- // Reset the toggle to null when the developer option is disabled
- DeviceConfig.setProperty(
- DeviceConfig.NAMESPACE_SETTINGS_UI,
- SettingsUIDeviceConfig.BT_LE_AUDIO_DEVICE_DETAIL_ENABLED, "null", false);
- }
}
diff --git a/src/com/android/settings/development/DevelopmentOptionsActivityRequestCodes.java b/src/com/android/settings/development/DevelopmentOptionsActivityRequestCodes.java
index 0d91fdd1df3fadc233090f17e07112b73ea0b5ec..b7b27591df970999ecc395e3e7640218a0b29c82 100644
--- a/src/com/android/settings/development/DevelopmentOptionsActivityRequestCodes.java
+++ b/src/com/android/settings/development/DevelopmentOptionsActivityRequestCodes.java
@@ -25,12 +25,4 @@ public interface DevelopmentOptionsActivityRequestCodes {
int REQUEST_CODE_DEBUG_APP = 1;
int REQUEST_MOCK_LOCATION_APP = 2;
-
- int REQUEST_CODE_ANGLE_ALL_USE_ANGLE = 3;
-
- int REQUEST_CODE_ANGLE_DRIVER_PKGS = 4;
-
- int REQUEST_CODE_ANGLE_DRIVER_VALUES = 5;
-
- int REQUEST_COMPAT_CHANGE_APP = 6;
}
diff --git a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
index f7be1aa47b119456e64a35222d89dc0f1f5115f2..047b2194be8e629fbef9c3deca1d5acdfe2f26e6 100644
--- a/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
+++ b/src/com/android/settings/development/DevelopmentSettingsDashboardFragment.java
@@ -675,6 +675,7 @@ public class DevelopmentSettingsDashboardFragment extends RestrictedDashboardFra
controllers.add(new NfcVerboseVendorLogPreferenceController(context, fragment));
controllers.add(new ShowTapsPreferenceController(context));
controllers.add(new PointerLocationPreferenceController(context));
+ controllers.add(new ShowKeyPressesPreferenceController(context));
controllers.add(new ShowSurfaceUpdatesPreferenceController(context));
controllers.add(new ShowLayoutBoundsPreferenceController(context));
controllers.add(new ShowRefreshRatePreferenceController(context));
diff --git a/src/com/android/settings/development/EnableVerboseVendorLoggingPreferenceController.java b/src/com/android/settings/development/EnableVerboseVendorLoggingPreferenceController.java
index 051cede54b551b07bf5d1a5e1c55c8c3ba9d329e..f13143dfdce7d05b6f01ac13f1394b79fbc448ce 100644
--- a/src/com/android/settings/development/EnableVerboseVendorLoggingPreferenceController.java
+++ b/src/com/android/settings/development/EnableVerboseVendorLoggingPreferenceController.java
@@ -29,6 +29,7 @@ import androidx.preference.SwitchPreference;
import com.android.settings.core.PreferenceControllerMixin;
import com.android.settingslib.development.DeveloperOptionsPreferenceController;
+import com.android.settingslib.utils.ThreadUtils;
import java.util.NoSuchElementException;
@@ -66,23 +67,34 @@ public class EnableVerboseVendorLoggingPreferenceController
return isIDumpstateDeviceAidlServiceAvailable() || isIDumpstateDeviceV1_1ServiceAvailable();
}
+ @SuppressWarnings("FutureReturnValueIgnored")
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final boolean isEnabled = (Boolean) newValue;
- setVerboseLoggingEnabled(isEnabled);
+ // IDumpstateDevice IPC may be blocking when system is extremely heavily-loaded.
+ // Post to background thread to avoid ANR. Ignore the returned Future.
+ ThreadUtils.postOnBackgroundThread(() ->
+ setVerboseLoggingEnabled(isEnabled));
return true;
}
+ @SuppressWarnings("FutureReturnValueIgnored")
@Override
public void updateState(Preference preference) {
- final boolean enabled = getVerboseLoggingEnabled();
- ((SwitchPreference) mPreference).setChecked(enabled);
+ ThreadUtils.postOnBackgroundThread(() -> {
+ final boolean enabled = getVerboseLoggingEnabled();
+ ThreadUtils.getUiThreadHandler().post(() ->
+ ((SwitchPreference) mPreference).setChecked(enabled));
+ }
+ );
}
+ @SuppressWarnings("FutureReturnValueIgnored")
@Override
protected void onDeveloperOptionsSwitchDisabled() {
super.onDeveloperOptionsSwitchDisabled();
- setVerboseLoggingEnabled(false);
+ ThreadUtils.postOnBackgroundThread(() ->
+ setVerboseLoggingEnabled(false));
((SwitchPreference) mPreference).setChecked(false);
}
diff --git a/src/com/android/settings/development/ShowKeyPressesPreferenceController.java b/src/com/android/settings/development/ShowKeyPressesPreferenceController.java
new file mode 100644
index 0000000000000000000000000000000000000000..247f59a53858c31770b8df6de968378b59038cdb
--- /dev/null
+++ b/src/com/android/settings/development/ShowKeyPressesPreferenceController.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2023 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.settings.development;
+
+import android.content.Context;
+import android.provider.Settings;
+
+import androidx.annotation.VisibleForTesting;
+import androidx.preference.Preference;
+import androidx.preference.SwitchPreference;
+
+import com.android.settings.core.PreferenceControllerMixin;
+import com.android.settingslib.development.DeveloperOptionsPreferenceController;
+
+/** PreferenceController that controls the "Show key presses" developer option. */
+public class ShowKeyPressesPreferenceController extends
+ DeveloperOptionsPreferenceController implements
+ Preference.OnPreferenceChangeListener, PreferenceControllerMixin {
+
+ private static final String SHOW_KEY_PRESSES_KEY = "show_key_presses";
+
+ @VisibleForTesting
+ static final int SETTING_VALUE_ON = 1;
+ @VisibleForTesting
+ static final int SETTING_VALUE_OFF = 0;
+
+ public ShowKeyPressesPreferenceController(Context context) {
+ super(context);
+ }
+
+ @Override
+ public String getPreferenceKey() {
+ return SHOW_KEY_PRESSES_KEY;
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ final boolean isEnabled = (Boolean) newValue;
+ Settings.System.putInt(mContext.getContentResolver(),
+ Settings.System.SHOW_KEY_PRESSES, isEnabled ? SETTING_VALUE_ON : SETTING_VALUE_OFF);
+ return true;
+ }
+
+ @Override
+ public void updateState(Preference preference) {
+ int showKeyPresses = Settings.System.getInt(mContext.getContentResolver(),
+ Settings.System.SHOW_KEY_PRESSES, SETTING_VALUE_OFF);
+ ((SwitchPreference) mPreference).setChecked(showKeyPresses != SETTING_VALUE_OFF);
+ }
+
+ @Override
+ protected void onDeveloperOptionsSwitchDisabled() {
+ super.onDeveloperOptionsSwitchDisabled();
+ Settings.System.putInt(mContext.getContentResolver(), Settings.System.SHOW_KEY_PRESSES,
+ SETTING_VALUE_OFF);
+ ((SwitchPreference) mPreference).setChecked(false);
+ }
+}
diff --git a/src/com/android/settings/development/compat/PlatformCompatDashboard.java b/src/com/android/settings/development/compat/PlatformCompatDashboard.java
index f8cbf21b36bef54fa260e8d1f79789e712dda145..3f0ffc79acaedbb6f68074a9b53fe50bf38e735d 100644
--- a/src/com/android/settings/development/compat/PlatformCompatDashboard.java
+++ b/src/com/android/settings/development/compat/PlatformCompatDashboard.java
@@ -17,21 +17,16 @@
package com.android.settings.development.compat;
import static com.android.internal.compat.OverrideAllowedState.ALLOWED;
-import static com.android.settings.development.DevelopmentOptionsActivityRequestCodes.REQUEST_COMPAT_CHANGE_APP;
-import android.app.Activity;
-import android.app.AlertDialog;
import android.app.settings.SettingsEnums;
import android.compat.Compatibility.ChangeConfig;
import android.content.Context;
-import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ServiceManager;
-import android.text.TextUtils;
import android.util.ArraySet;
import androidx.annotation.VisibleForTesting;
@@ -40,35 +35,28 @@ import androidx.preference.Preference.OnPreferenceChangeListener;
import androidx.preference.PreferenceCategory;
import androidx.preference.SwitchPreference;
-import com.android.internal.compat.AndroidBuildClassifier;
import com.android.internal.compat.CompatibilityChangeConfig;
import com.android.internal.compat.CompatibilityChangeInfo;
import com.android.internal.compat.IPlatformCompat;
import com.android.settings.R;
import com.android.settings.dashboard.DashboardFragment;
-import com.android.settings.development.AppPicker;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
-
/**
* Dashboard for Platform Compat preferences.
*/
public class PlatformCompatDashboard extends DashboardFragment {
private static final String TAG = "PlatformCompatDashboard";
- private static final String COMPAT_APP = "compat_app";
+ public static final String COMPAT_APP = "compat_app";
private IPlatformCompat mPlatformCompat;
private CompatibilityChangeInfo[] mChanges;
- private AndroidBuildClassifier mAndroidBuildClassifier = new AndroidBuildClassifier();
-
- private boolean mShouldStartAppPickerOnResume = true;
-
@VisibleForTesting
String mSelectedApp;
@@ -108,32 +96,6 @@ public class PlatformCompatDashboard extends DashboardFragment {
} catch (RemoteException e) {
throw new RuntimeException("Could not list changes!", e);
}
- if (icicle != null) {
- mShouldStartAppPickerOnResume = false;
- mSelectedApp = icicle.getString(COMPAT_APP);
- }
- }
-
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- if (requestCode == REQUEST_COMPAT_CHANGE_APP) {
- mShouldStartAppPickerOnResume = false;
- switch (resultCode) {
- case Activity.RESULT_OK:
- mSelectedApp = data.getAction();
- break;
- case Activity.RESULT_CANCELED:
- if (TextUtils.isEmpty(mSelectedApp)) {
- finish();
- }
- break;
- case AppPicker.RESULT_NO_MATCHING_APPS:
- mSelectedApp = null;
- break;
- }
- return;
- }
- super.onActivityResult(requestCode, resultCode, data);
}
@Override
@@ -142,33 +104,18 @@ public class PlatformCompatDashboard extends DashboardFragment {
if (isFinishingOrDestroyed()) {
return;
}
- if (!mShouldStartAppPickerOnResume) {
- if (TextUtils.isEmpty(mSelectedApp)) {
- new AlertDialog.Builder(getContext())
- .setTitle(R.string.platform_compat_dialog_title_no_apps)
- .setMessage(R.string.platform_compat_dialog_text_no_apps)
- .setPositiveButton(R.string.okay, (dialog, which) -> finish())
- .setOnDismissListener(dialog -> finish())
- .setCancelable(false)
- .show();
- return;
- }
- try {
- final ApplicationInfo applicationInfo = getApplicationInfo();
- addPreferences(applicationInfo);
- return;
- } catch (PackageManager.NameNotFoundException e) {
- mShouldStartAppPickerOnResume = true;
- mSelectedApp = null;
- }
+ Bundle arguments = getArguments();
+ if (arguments == null) {
+ finish();
+ return;
+ }
+ mSelectedApp = arguments.getString(COMPAT_APP);
+ try {
+ final ApplicationInfo applicationInfo = getApplicationInfo();
+ addPreferences(applicationInfo);
+ } catch (PackageManager.NameNotFoundException ignored) {
+ finish();
}
- startAppPicker();
- }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putString(COMPAT_APP, mSelectedApp);
}
private void addPreferences(ApplicationInfo applicationInfo) {
@@ -266,12 +213,6 @@ public class PlatformCompatDashboard extends DashboardFragment {
appPreference.setIcon(icon);
appPreference.setSummary(getString(R.string.platform_compat_selected_app_summary,
mSelectedApp, applicationInfo.targetSdkVersion));
- appPreference.setKey(mSelectedApp);
- appPreference.setOnPreferenceClickListener(
- preference -> {
- startAppPicker();
- return true;
- });
return appPreference;
}
@@ -294,17 +235,6 @@ public class PlatformCompatDashboard extends DashboardFragment {
}
}
- private void startAppPicker() {
- final Intent intent = new Intent(getContext(), AppPicker.class)
- .putExtra(AppPicker.EXTRA_INCLUDE_NOTHING, false);
- // If build is neither userdebug nor eng, only include debuggable apps
- final boolean debuggableBuild = mAndroidBuildClassifier.isDebuggableBuild();
- if (!debuggableBuild) {
- intent.putExtra(AppPicker.EXTRA_DEBUGGABLE, true /* value */);
- }
- startActivityForResult(intent, REQUEST_COMPAT_CHANGE_APP);
- }
-
private class CompatChangePreferenceChangeListener implements OnPreferenceChangeListener {
private final long changeId;
diff --git a/src/com/android/settings/deviceinfo/TopLevelStoragePreferenceController.java b/src/com/android/settings/deviceinfo/TopLevelStoragePreferenceController.java
index e6827832a8ed9fc84605fcc0c69e07c790808817..ccae7e92ab1ae87bb60caeb7edae556f212118c8 100644
--- a/src/com/android/settings/deviceinfo/TopLevelStoragePreferenceController.java
+++ b/src/com/android/settings/deviceinfo/TopLevelStoragePreferenceController.java
@@ -74,10 +74,12 @@ public class TopLevelStoragePreferenceController extends BasePreferenceControlle
return ThreadUtils.postOnBackgroundThread(() -> {
final PrivateStorageInfo info = PrivateStorageInfo.getPrivateStorageInfo(
getStorageManagerVolumeProvider());
- storageCacheHelper.cacheUsedSize(info.totalBytes - info.freeBytes);
+
+ long usedBytes = info.totalBytes - info.freeBytes;
+ storageCacheHelper.cacheUsedSize(usedBytes);
ThreadUtils.postOnMainThread(() -> {
preference.setSummary(
- getSummary(info.totalBytes - info.freeBytes, info.totalBytes));
+ getSummary(usedBytes, info.totalBytes));
});
});
}
diff --git a/src/com/android/settings/deviceinfo/batteryinfo/BatteryCycleCountPreferenceController.java b/src/com/android/settings/deviceinfo/batteryinfo/BatteryCycleCountPreferenceController.java
new file mode 100644
index 0000000000000000000000000000000000000000..b022fcf7df296d0a5651f14508fb98d071b202f0
--- /dev/null
+++ b/src/com/android/settings/deviceinfo/batteryinfo/BatteryCycleCountPreferenceController.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 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.settings.deviceinfo.batteryinfo;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.BatteryManager;
+
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+import com.android.settingslib.fuelgauge.BatteryUtils;
+
+/**
+ * A controller that manages the information about battery cycle count.
+ */
+public class BatteryCycleCountPreferenceController extends BasePreferenceController {
+
+ public BatteryCycleCountPreferenceController(Context context,
+ String preferenceKey) {
+ super(context, preferenceKey);
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return AVAILABLE;
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ final Intent batteryIntent = BatteryUtils.getBatteryIntent(mContext);
+ final int cycleCount = batteryIntent.getIntExtra(BatteryManager.EXTRA_CYCLE_COUNT, -1);
+
+ return cycleCount == -1
+ ? mContext.getText(R.string.battery_cycle_count_not_available)
+ : Integer.toString(cycleCount);
+ }
+}
diff --git a/src/com/android/settings/deviceinfo/batteryinfo/BatteryFirstUseDatePreferenceController.java b/src/com/android/settings/deviceinfo/batteryinfo/BatteryFirstUseDatePreferenceController.java
new file mode 100644
index 0000000000000000000000000000000000000000..6c7a743ed12c0bd6a4be1e5b7af25a254a74d87d
--- /dev/null
+++ b/src/com/android/settings/deviceinfo/batteryinfo/BatteryFirstUseDatePreferenceController.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2023 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.settings.deviceinfo.batteryinfo;
+
+import android.content.Context;
+import android.os.BatteryManager;
+
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.fuelgauge.BatterySettingsFeatureProvider;
+import com.android.settings.fuelgauge.BatteryUtils;
+import com.android.settings.overlay.FeatureFactory;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A controller that manages the information about battery first use date.
+ */
+public class BatteryFirstUseDatePreferenceController extends BasePreferenceController {
+
+ private final BatterySettingsFeatureProvider mBatterySettingsFeatureProvider;
+ private final BatteryManager mBatteryManager;
+
+ private long mFirstUseDateInMs;
+
+ public BatteryFirstUseDatePreferenceController(Context context, String preferenceKey) {
+ super(context, preferenceKey);
+ mBatterySettingsFeatureProvider = FeatureFactory.getFactory(
+ context).getBatterySettingsFeatureProvider();
+ mBatteryManager = mContext.getSystemService(BatteryManager.class);
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return mBatterySettingsFeatureProvider.isFirstUseDateAvailable(mContext, getFirstUseDate())
+ ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ return isAvailable()
+ ? BatteryUtils.getBatteryInfoFormattedDate(mFirstUseDateInMs)
+ : null;
+ }
+
+ private long getFirstUseDate() {
+ if (mFirstUseDateInMs == 0L) {
+ final long firstUseDateInSec = mBatteryManager.getLongProperty(
+ BatteryManager.BATTERY_PROPERTY_FIRST_USAGE_DATE);
+ mFirstUseDateInMs = TimeUnit.MILLISECONDS.convert(firstUseDateInSec, TimeUnit.SECONDS);
+ }
+ return mFirstUseDateInMs;
+ }
+}
diff --git a/src/com/android/settings/deviceinfo/batteryinfo/BatteryInfoFragment.java b/src/com/android/settings/deviceinfo/batteryinfo/BatteryInfoFragment.java
new file mode 100644
index 0000000000000000000000000000000000000000..1731212fa075b3e06003da39ce93fe076cdb0a91
--- /dev/null
+++ b/src/com/android/settings/deviceinfo/batteryinfo/BatteryInfoFragment.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 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.settings.deviceinfo.batteryinfo;
+
+import android.app.settings.SettingsEnums;
+
+import com.android.settings.R;
+import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.search.BaseSearchIndexProvider;
+import com.android.settingslib.search.SearchIndexable;
+
+/**
+ * A fragment that shows battery hardware information.
+ */
+@SearchIndexable
+public class BatteryInfoFragment extends DashboardFragment {
+
+ public static final String TAG = "BatteryInfo";
+
+ @Override
+ public int getMetricsCategory() {
+ return SettingsEnums.SETTINGS_BATTERY_INFORMATION;
+ }
+
+ @Override
+ protected int getPreferenceScreenResId() {
+ return R.xml.battery_info;
+ }
+
+ @Override
+ protected String getLogTag() {
+ return TAG;
+ }
+
+ public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
+ new BaseSearchIndexProvider(R.xml.battery_info);
+}
diff --git a/src/com/android/settings/deviceinfo/batteryinfo/BatteryManufactureDatePreferenceController.java b/src/com/android/settings/deviceinfo/batteryinfo/BatteryManufactureDatePreferenceController.java
new file mode 100644
index 0000000000000000000000000000000000000000..ff54c77052b9a4fe2cf0a7cda59dbc3408587415
--- /dev/null
+++ b/src/com/android/settings/deviceinfo/batteryinfo/BatteryManufactureDatePreferenceController.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 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.settings.deviceinfo.batteryinfo;
+
+import android.content.Context;
+import android.os.BatteryManager;
+
+import com.android.settings.core.BasePreferenceController;
+import com.android.settings.fuelgauge.BatterySettingsFeatureProvider;
+import com.android.settings.fuelgauge.BatteryUtils;
+import com.android.settings.overlay.FeatureFactory;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A controller that manages the information about battery manufacture date.
+ */
+public class BatteryManufactureDatePreferenceController extends BasePreferenceController {
+
+ private final BatterySettingsFeatureProvider mBatterySettingsFeatureProvider;
+ private final BatteryManager mBatteryManager;
+
+ private long mManufactureDateInMs;
+
+ public BatteryManufactureDatePreferenceController(Context context, String preferenceKey) {
+ super(context, preferenceKey);
+ mBatterySettingsFeatureProvider = FeatureFactory.getFactory(
+ context).getBatterySettingsFeatureProvider();
+ mBatteryManager = mContext.getSystemService(BatteryManager.class);
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return mBatterySettingsFeatureProvider.isManufactureDateAvailable(mContext,
+ getManufactureDate())
+ ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ return isAvailable()
+ ? BatteryUtils.getBatteryInfoFormattedDate(mManufactureDateInMs)
+ : null;
+ }
+
+ private long getManufactureDate() {
+ if (mManufactureDateInMs == 0L) {
+ final long manufactureDateInSec = mBatteryManager.getLongProperty(
+ BatteryManager.BATTERY_PROPERTY_MANUFACTURING_DATE);
+ mManufactureDateInMs = TimeUnit.MILLISECONDS.convert(manufactureDateInSec,
+ TimeUnit.SECONDS);
+ }
+ return mManufactureDateInMs;
+ }
+}
diff --git a/src/com/android/settings/deviceinfo/regulatory/RegulatoryInfo.kt b/src/com/android/settings/deviceinfo/regulatory/RegulatoryInfo.kt
new file mode 100644
index 0000000000000000000000000000000000000000..e26e0610502b16976fcb7a570c0b94e14693c51e
--- /dev/null
+++ b/src/com/android/settings/deviceinfo/regulatory/RegulatoryInfo.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 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.settings.deviceinfo.regulatory
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.drawable.Drawable
+import android.os.SystemProperties
+import androidx.annotation.DrawableRes
+import androidx.annotation.VisibleForTesting
+import com.android.settings.R
+
+
+
+/** To load Regulatory Info from device. */
+object RegulatoryInfo {
+ private const val REGULATORY_INFO_RESOURCE = "regulatory_info"
+
+ @VisibleForTesting
+ const val KEY_COO = "ro.boot.hardware.coo"
+
+ @VisibleForTesting
+ const val KEY_SKU = "ro.boot.hardware.sku"
+
+ /** Gets the regulatory drawable. */
+ fun Context.getRegulatoryInfo(): Drawable? {
+ val sku = getSku()
+ if (sku.isNotBlank()) {
+ // When hardware coo property exists, use regulatory_info__ resource if valid.
+ val coo = getCoo()
+ if (coo.isNotBlank()) {
+ getRegulatoryInfo("${REGULATORY_INFO_RESOURCE}_${sku}_$coo")?.let { return it }
+ }
+ // Use regulatory_info_ resource if valid.
+ getRegulatoryInfo("${REGULATORY_INFO_RESOURCE}_$sku")?.let { return it }
+ }
+ return getRegulatoryInfo(REGULATORY_INFO_RESOURCE)
+ }
+
+ private fun getCoo(): String = SystemProperties.get(KEY_COO).lowercase()
+
+ private fun getSku(): String = SystemProperties.get(KEY_SKU).lowercase()
+
+ private fun Context.getRegulatoryInfo(fileName: String): Drawable? {
+ val overlayPackageName =
+ resources.getString(R.string.config_regulatory_info_overlay_package_name)
+ .ifBlank { packageName }
+ val resources = packageManager.getResourcesForApplication(overlayPackageName)
+ val id = resources.getIdentifier(fileName, "drawable", overlayPackageName)
+ return if (id > 0) resources.getRegulatoryInfo(id) else null
+ }
+
+ private fun Resources.getRegulatoryInfo(@DrawableRes resId: Int): Drawable? = try {
+ getDrawable(resId, null).takeIf {
+ // Ignore the placeholder image
+ it.intrinsicWidth > 10 && it.intrinsicHeight > 10
+ }
+ } catch (_: Resources.NotFoundException) {
+ null
+ }
+}
diff --git a/src/com/android/settings/display/FoldLockBehaviorPreferenceController.java b/src/com/android/settings/display/FoldLockBehaviorPreferenceController.java
new file mode 100644
index 0000000000000000000000000000000000000000..88e78e80a95d7774bd897f8f10d21ba9735cf84c
--- /dev/null
+++ b/src/com/android/settings/display/FoldLockBehaviorPreferenceController.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2023 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.settings.display;
+
+import static android.provider.Settings.System.FOLD_LOCK_BEHAVIOR;
+
+import static com.android.settings.display.FoldLockBehaviorSettings.SETTING_VALUES;
+import static com.android.settings.display.FoldLockBehaviorSettings.SETTING_VALUE_SELECTIVE_STAY_AWAKE;
+import static com.android.settings.display.FoldLockBehaviorSettings.SETTING_VALUE_SLEEP_ON_FOLD;
+import static com.android.settings.display.FoldLockBehaviorSettings.SETTING_VALUE_STAY_AWAKE_ON_FOLD;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.UserHandle;
+import android.provider.Settings;
+
+import androidx.preference.Preference;
+
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A preference controller for the @link android.provider.Settings.System#FOLD_LOCK_BEHAVIOR
+ * setting.
+ *
+ * This preference controller allows users to control whether or not the device
+ * stays awake when it is folded.
+ */
+public class FoldLockBehaviorPreferenceController extends BasePreferenceController {
+
+ private final Resources mResources;
+
+ private static Map KEY_TO_TEXT = new HashMap<>();
+
+ public FoldLockBehaviorPreferenceController(Context context, String key) {
+ this(context, key, context.getResources());
+ }
+
+ public FoldLockBehaviorPreferenceController(Context context, String key, Resources resources) {
+ super(context, key);
+ mResources = resources;
+ KEY_TO_TEXT.put(SETTING_VALUE_STAY_AWAKE_ON_FOLD,
+ resourceToString(R.string.stay_awake_on_fold_title));
+ KEY_TO_TEXT.put(SETTING_VALUE_SELECTIVE_STAY_AWAKE,
+ resourceToString(R.string.selective_stay_awake_title));
+ KEY_TO_TEXT.put(SETTING_VALUE_SLEEP_ON_FOLD,
+ resourceToString(R.string.sleep_on_fold_title));
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return mResources.getBoolean(com.android.internal.R.bool.config_fold_lock_behavior)
+ ? AVAILABLE : UNSUPPORTED_ON_DEVICE;
+ }
+
+ @Override
+ public void updateState(Preference preference) {
+ String summary = KEY_TO_TEXT.get(getFoldSettingValue());
+ preference.setSummary(summary);
+ }
+
+ private String getFoldSettingValue() {
+ String foldSettingValue = Settings.System.getStringForUser(mContext.getContentResolver(),
+ FOLD_LOCK_BEHAVIOR, UserHandle.USER_CURRENT);
+ return (foldSettingValue != null && SETTING_VALUES.contains(foldSettingValue))
+ ? foldSettingValue : SETTING_VALUE_SELECTIVE_STAY_AWAKE;
+ }
+
+ @Override
+ public int getSliceHighlightMenuRes() {
+ return R.string.menu_key_display;
+ }
+
+ private String resourceToString(int resource) {
+ return mContext.getText(resource).toString();
+ }
+
+}
diff --git a/src/com/android/settings/display/FoldLockBehaviorSettings.java b/src/com/android/settings/display/FoldLockBehaviorSettings.java
new file mode 100644
index 0000000000000000000000000000000000000000..beda52e8696e3c4915492d1bee398fee95a466f3
--- /dev/null
+++ b/src/com/android/settings/display/FoldLockBehaviorSettings.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2023 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.settings.display;
+
+import static android.provider.Settings.System.FOLD_LOCK_BEHAVIOR;
+
+import android.app.settings.SettingsEnums;
+import android.content.Context;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.settings.R;
+import com.android.settings.support.actionbar.HelpResourceProvider;
+import com.android.settings.utils.CandidateInfoExtra;
+import com.android.settings.widget.RadioButtonPickerFragment;
+import com.android.settingslib.widget.CandidateInfo;
+import com.android.settingslib.widget.SelectorWithWidgetPreference;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Fragment that is used to control fold setting.
+ *
+ * Keep the setting values in this class in sync with the values in
+ * {@link com.android.server.utils.FoldSettingProvider}
+ */
+public class FoldLockBehaviorSettings extends RadioButtonPickerFragment implements
+ HelpResourceProvider {
+
+ public static final String SETTING_VALUE_STAY_AWAKE_ON_FOLD = "stay_awake_on_fold_key";
+ public static final String SETTING_VALUE_SELECTIVE_STAY_AWAKE = "selective_stay_awake_key";
+ public static final String SETTING_VALUE_SLEEP_ON_FOLD = "sleep_on_fold_key";
+ private static final String SETTING_VALUE_DEFAULT = SETTING_VALUE_SELECTIVE_STAY_AWAKE;
+ public static final String TAG = "FoldLockBehaviorSetting";
+ public static final HashSet SETTING_VALUES = new HashSet<>(
+ Set.of(SETTING_VALUE_STAY_AWAKE_ON_FOLD, SETTING_VALUE_SELECTIVE_STAY_AWAKE,
+ SETTING_VALUE_SLEEP_ON_FOLD));
+
+ private Context mContext;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ mContext = context;
+ }
+
+ @Override
+ protected List extends CandidateInfo> getCandidates() {
+ List candidates = new ArrayList<>();
+ candidates.add(new CandidateInfoExtra(
+ resourceToString(R.string.stay_awake_on_fold_title),
+ resourceToString(R.string.stay_awake_on_fold_summary),
+ SETTING_VALUE_STAY_AWAKE_ON_FOLD, /* enabled */ true));
+ candidates.add(new CandidateInfoExtra(
+ resourceToString(R.string.selective_stay_awake_title),
+ resourceToString(R.string.selective_stay_awake_summary),
+ SETTING_VALUE_SELECTIVE_STAY_AWAKE, /* enabled */ true));
+ candidates.add(new CandidateInfoExtra(
+ resourceToString(R.string.sleep_on_fold_title),
+ resourceToString(R.string.sleep_on_fold_summary),
+ SETTING_VALUE_SLEEP_ON_FOLD, /* enabled */ true));
+ return candidates;
+ }
+
+ @Override
+ public void bindPreferenceExtra(SelectorWithWidgetPreference pref,
+ String key, CandidateInfo info, String defaultKey, String systemDefaultKey) {
+ if (!(info instanceof CandidateInfoExtra)) {
+ return;
+ }
+
+ pref.setSummary(((CandidateInfoExtra) info).loadSummary());
+ }
+
+ @Override
+ protected String getDefaultKey() {
+ String foldSettingValue = getCurrentFoldSettingValue();
+ foldSettingValue = (foldSettingValue != null) ? foldSettingValue : SETTING_VALUE_DEFAULT;
+ if (!SETTING_VALUES.contains(foldSettingValue)) {
+ Log.e(TAG,
+ "getDefaultKey: Invalid setting value, returning default setting value");
+ foldSettingValue = SETTING_VALUE_DEFAULT;
+ }
+
+ return foldSettingValue;
+ }
+
+ @Override
+ protected boolean setDefaultKey(String key) {
+ if (!SETTING_VALUES.contains(key)) {
+ Log.e(TAG, "setDefaultKey: Can not set invalid key: " + key);
+ key = SETTING_VALUE_SELECTIVE_STAY_AWAKE;
+ }
+ setCurrentFoldSettingValue(key);
+ return true;
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ return SettingsEnums.FOLD_LOCK_BEHAVIOR;
+ }
+
+ @Override
+ protected int getPreferenceScreenResId() {
+ return R.xml.fold_lock_behavior_settings;
+ }
+
+ private String getCurrentFoldSettingValue() {
+ return Settings.System.getStringForUser(mContext.getContentResolver(),
+ FOLD_LOCK_BEHAVIOR,
+ UserHandle.USER_CURRENT);
+ }
+
+ private void setCurrentFoldSettingValue(String key) {
+ Settings.System.putStringForUser(mContext.getContentResolver(),
+ FOLD_LOCK_BEHAVIOR,
+ key,
+ UserHandle.USER_CURRENT);
+ }
+
+ private String resourceToString(int resource) {
+ return mContext.getText(resource).toString();
+ }
+}
diff --git a/src/com/android/settings/display/ScreenResolutionFragment.java b/src/com/android/settings/display/ScreenResolutionFragment.java
index de7d25fefb9dffe6fc18f3c7278863580677a97f..daf1793f11f9e879554b9d768c28da1efc719f91 100644
--- a/src/com/android/settings/display/ScreenResolutionFragment.java
+++ b/src/com/android/settings/display/ScreenResolutionFragment.java
@@ -369,6 +369,12 @@ public class ScreenResolutionFragment extends RadioButtonPickerFragment {
private void restoreDensity() {
final DisplayDensityUtils density = new DisplayDensityUtils(mContext);
+ /* If current density is the same as a default density of other resolutions,
+ * then mCurrentIndex may be out of boundary.
+ */
+ if (density.getDefaultDisplayDensityValues().length <= mCurrentIndex) {
+ mCurrentIndex = density.getCurrentIndexForDefaultDisplay();
+ }
if (density.getDefaultDisplayDensityValues()[mCurrentIndex]
!= density.getDefaultDensityForDefaultDisplay()) {
density.setForcedDisplayDensity(mCurrentIndex);
diff --git a/src/com/android/settings/dream/WhenToDreamPicker.java b/src/com/android/settings/dream/WhenToDreamPicker.java
index 13cdadf19819c55d7d03c60de212688c24b16be3..3052d20ba711e2d73da4e92b428f3f89eef3d6e4 100644
--- a/src/com/android/settings/dream/WhenToDreamPicker.java
+++ b/src/com/android/settings/dream/WhenToDreamPicker.java
@@ -50,7 +50,7 @@ public class WhenToDreamPicker extends RadioButtonPickerFragment {
@Override
public int getMetricsCategory() {
- return SettingsEnums.DREAM;
+ return SettingsEnums.SETTINGS_WHEN_TO_DREAM;
}
@Override
diff --git a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
index 79e01940ecd42547d2619a83d83d66d8fe22d8e8..d38dede68470a912a4b357eb4bc4dc47e93e9c52 100644
--- a/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
+++ b/src/com/android/settings/fuelgauge/AdvancedPowerUsageDetail.java
@@ -16,6 +16,8 @@
package com.android.settings.fuelgauge;
+import static com.android.settings.fuelgauge.batteryusage.ConvertUtils.isUserConsumer;
+
import android.app.Activity;
import android.app.ActivityManager;
import android.app.backup.BackupManager;
@@ -41,7 +43,6 @@ import com.android.settings.dashboard.DashboardFragment;
import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
import com.android.settings.fuelgauge.batteryusage.BatteryDiffEntry;
import com.android.settings.fuelgauge.batteryusage.BatteryEntry;
-import com.android.settings.fuelgauge.batteryusage.BatteryHistEntry;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.EntityHeaderController;
import com.android.settingslib.HelpUtils;
@@ -149,14 +150,13 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements
Context context, int sourceMetricsCategory,
BatteryDiffEntry diffEntry, String usagePercent, String slotInformation,
boolean showTimeInformation) {
- final BatteryHistEntry histEntry = diffEntry.mBatteryHistEntry;
final LaunchBatteryDetailPageArgs launchArgs = new LaunchBatteryDetailPageArgs();
// configure the launch argument.
launchArgs.mUsagePercent = usagePercent;
launchArgs.mPackageName = diffEntry.getPackageName();
launchArgs.mAppLabel = diffEntry.getAppLabel();
launchArgs.mSlotInformation = slotInformation;
- launchArgs.mUid = (int) histEntry.mUid;
+ launchArgs.mUid = (int) diffEntry.mUid;
launchArgs.mIconId = diffEntry.getAppIconId();
launchArgs.mConsumedPower = (int) diffEntry.mConsumePower;
if (showTimeInformation) {
@@ -164,7 +164,7 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements
launchArgs.mBackgroundTimeMs = diffEntry.mBackgroundUsageTimeInMs;
launchArgs.mScreenOnTimeMs = diffEntry.mScreenOnTimeInMs;
}
- launchArgs.mIsUserEntry = histEntry.isUserEntry();
+ launchArgs.mIsUserEntry = isUserConsumer(diffEntry.mConsumerType);
startBatteryDetailPage(context, sourceMetricsCategory, launchArgs);
}
@@ -289,12 +289,14 @@ public class AdvancedPowerUsageDetail extends DashboardFragment implements
mLogStringBuilder.append(", onPause mode = ").append(selectedPreference);
logMetricCategory(selectedPreference);
- BatteryHistoricalLogUtil.writeLog(
- getContext().getApplicationContext(),
- Action.LEAVE,
- BatteryHistoricalLogUtil.getPackageNameWithUserId(
- mBatteryOptimizeUtils.getPackageName(), UserHandle.myUserId()),
- mLogStringBuilder.toString());
+ mExecutor.execute(() -> {
+ BatteryOptimizeLogUtils.writeLog(
+ getContext().getApplicationContext(),
+ Action.LEAVE,
+ BatteryOptimizeLogUtils.getPackageNameWithUserId(
+ mBatteryOptimizeUtils.getPackageName(), UserHandle.myUserId()),
+ mLogStringBuilder.toString());
+ });
Log.d(TAG, "Leave with mode: " + selectedPreference);
}
diff --git a/src/com/android/settings/fuelgauge/BatteryBackupHelper.java b/src/com/android/settings/fuelgauge/BatteryBackupHelper.java
index 66ffc90c8775c901f3a61863780929e8e765495d..50f1b90df777a65f122d691267419cacd246e411 100644
--- a/src/com/android/settings/fuelgauge/BatteryBackupHelper.java
+++ b/src/com/android/settings/fuelgauge/BatteryBackupHelper.java
@@ -199,7 +199,7 @@ public final class BatteryBackupHelper implements BackupHelper {
info.packageName + DELIMITER_MODE + optimizationMode;
builder.append(packageOptimizeMode + DELIMITER);
Log.d(TAG, "backupOptimizationMode: " + packageOptimizeMode);
- BatteryHistoricalLogUtil.writeLog(
+ BatteryOptimizeLogUtils.writeLog(
sharedPreferences, Action.BACKUP, info.packageName,
/* actionDescription */ "mode: " + optimizationMode);
backupCount++;
@@ -275,7 +275,7 @@ public final class BatteryBackupHelper implements BackupHelper {
/** Dump the app optimization mode backup history data. */
public static void dumpHistoricalData(Context context, PrintWriter writer) {
- BatteryHistoricalLogUtil.printBatteryOptimizeHistoricalLog(
+ BatteryOptimizeLogUtils.printBatteryOptimizeHistoricalLog(
getSharedPreferences(context), writer);
}
diff --git a/src/com/android/settings/fuelgauge/BatteryHistoricalLogUtil.java b/src/com/android/settings/fuelgauge/BatteryOptimizeLogUtils.java
similarity index 90%
rename from src/com/android/settings/fuelgauge/BatteryHistoricalLogUtil.java
rename to src/com/android/settings/fuelgauge/BatteryOptimizeLogUtils.java
index f82b70317491b553a9df306eb8460a7d30af5bc3..d093d35debcccd549f97bcc8e783435dbc49b2ec 100644
--- a/src/com/android/settings/fuelgauge/BatteryHistoricalLogUtil.java
+++ b/src/com/android/settings/fuelgauge/BatteryOptimizeLogUtils.java
@@ -20,23 +20,25 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.util.Base64;
+import androidx.annotation.VisibleForTesting;
+
import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry.Action;
import com.android.settings.fuelgauge.batteryusage.ConvertUtils;
-import com.google.common.annotations.VisibleForTesting;
-
import java.io.PrintWriter;
import java.util.List;
/** Writes and reads a historical log of battery related state change events. */
-public final class BatteryHistoricalLogUtil {
+public final class BatteryOptimizeLogUtils {
+ private static final String TAG = "BatteryOptimizeLogUtils";
private static final String BATTERY_OPTIMIZE_FILE_NAME = "battery_optimize_historical_logs";
private static final String LOGS_KEY = "battery_optimize_logs_key";
- private static final String TAG = "BatteryHistoricalLogUtil";
@VisibleForTesting
static final int MAX_ENTRIES = 40;
+ private BatteryOptimizeLogUtils() {}
+
/** Writes a log entry for battery optimization mode. */
static void writeLog(
Context context, Action action, String packageName, String actionDescription) {
@@ -67,7 +69,7 @@ public final class BatteryHistoricalLogUtil {
newLogBuilder.addLogEntry(logEntry);
String loggingContent =
- Base64.encodeToString(newLogBuilder.build().toByteArray(), Base64.DEFAULT);
+ Base64.encodeToString(newLogBuilder.build().toByteArray(), Base64.DEFAULT);
sharedPreferences
.edit()
.putString(LOGS_KEY, loggingContent)
@@ -94,7 +96,7 @@ public final class BatteryHistoricalLogUtil {
if (logEntryList.isEmpty()) {
writer.println("\tnothing to dump");
} else {
- writer.println("0:UNKNOWN 1:RESTRICTED 2:UNRESTRICTED 3:OPTIMIZED");
+ writer.println("0:UNKNOWN 1:RESTRICTED 2:UNRESTRICTED 3:OPTIMIZED");
logEntryList.forEach(entry -> writer.println(toString(entry)));
}
}
@@ -113,6 +115,7 @@ public final class BatteryHistoricalLogUtil {
@VisibleForTesting
static SharedPreferences getSharedPreferences(Context context) {
- return context.getSharedPreferences(BATTERY_OPTIMIZE_FILE_NAME, Context.MODE_PRIVATE);
+ return context.getApplicationContext()
+ .getSharedPreferences(BATTERY_OPTIMIZE_FILE_NAME, Context.MODE_PRIVATE);
}
}
diff --git a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
index 589e1fd40552ff0fb91bc50d4822889de2e3e3e8..124840e1e01dd2708a2fa0071a11113799dacc9c 100644
--- a/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
+++ b/src/com/android/settings/fuelgauge/BatteryOptimizeUtils.java
@@ -245,7 +245,7 @@ public class BatteryOptimizeUtils {
Context context, int appStandbyMode, boolean allowListed, int uid, String packageName,
BatteryUtils batteryUtils, PowerAllowlistBackend powerAllowlistBackend,
Action action) {
- final String packageNameKey = BatteryHistoricalLogUtil
+ final String packageNameKey = BatteryOptimizeLogUtils
.getPackageNameWithUserId(packageName, UserHandle.myUserId());
try {
batteryUtils.setForceAppStandby(uid, packageName, appStandbyMode);
@@ -259,7 +259,7 @@ public class BatteryOptimizeUtils {
appStandbyMode = -1;
Log.e(TAG, "set OPTIMIZATION MODE failed for " + packageName, e);
}
- BatteryHistoricalLogUtil.writeLog(
+ BatteryOptimizeLogUtils.writeLog(
context,
action,
packageNameKey,
diff --git a/src/com/android/settings/fuelgauge/BatterySettingsFeatureProvider.java b/src/com/android/settings/fuelgauge/BatterySettingsFeatureProvider.java
index f6efb24e02498cfd02a1d143184198cb4628e865..260fde0e6673d5aaccf4ba688b8119a446d316d5 100644
--- a/src/com/android/settings/fuelgauge/BatterySettingsFeatureProvider.java
+++ b/src/com/android/settings/fuelgauge/BatterySettingsFeatureProvider.java
@@ -16,9 +16,14 @@
package com.android.settings.fuelgauge;
-import android.content.ComponentName;
+import android.content.Context;
/** Feature provider for battery settings usage. */
public interface BatterySettingsFeatureProvider {
+ /** Returns true if manufacture date should be shown */
+ boolean isManufactureDateAvailable(Context context, long manufactureDateMs);
+
+ /** Returns true if first use date should be shown */
+ boolean isFirstUseDateAvailable(Context context, long firstUseDateMs);
}
diff --git a/src/com/android/settings/fuelgauge/BatterySettingsFeatureProviderImpl.java b/src/com/android/settings/fuelgauge/BatterySettingsFeatureProviderImpl.java
index 39fe118e872c8ef7fa240404b30585d35e2304f2..6b456b7629e55b1ace2348a87ed12137c37c847d 100644
--- a/src/com/android/settings/fuelgauge/BatterySettingsFeatureProviderImpl.java
+++ b/src/com/android/settings/fuelgauge/BatterySettingsFeatureProviderImpl.java
@@ -21,9 +21,13 @@ import android.content.Context;
/** Feature provider implementation for battery settings usage. */
public class BatterySettingsFeatureProviderImpl implements BatterySettingsFeatureProvider {
- protected Context mContext;
+ @Override
+ public boolean isManufactureDateAvailable(Context context, long manufactureDateMs) {
+ return false;
+ }
- public BatterySettingsFeatureProviderImpl(Context context) {
- mContext = context.getApplicationContext();
+ @Override
+ public boolean isFirstUseDateAvailable(Context context, long firstUseDateMs) {
+ return false;
}
}
diff --git a/src/com/android/settings/fuelgauge/BatterySettingsMigrateChecker.java b/src/com/android/settings/fuelgauge/BatterySettingsMigrateChecker.java
index 4b9e6efaff05630468929bdb90dcf73bce5f7cd4..8697e438888d56d80443748ad25a5a09b2259879 100644
--- a/src/com/android/settings/fuelgauge/BatterySettingsMigrateChecker.java
+++ b/src/com/android/settings/fuelgauge/BatterySettingsMigrateChecker.java
@@ -16,8 +16,8 @@
package com.android.settings.fuelgauge;
-import android.content.ContentResolver;
import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.provider.Settings;
@@ -25,8 +25,6 @@ import android.util.Log;
import androidx.annotation.VisibleForTesting;
-import com.android.settings.R;
-import com.android.settings.fuelgauge.BatteryOptimizeHistoricalLogEntry;
import com.android.settings.fuelgauge.batterysaver.BatterySaverScheduleRadioButtonsController;
import com.android.settingslib.fuelgauge.BatterySaverUtils;
@@ -41,6 +39,7 @@ public final class BatterySettingsMigrateChecker extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "onReceive: " + intent + " owner: " + BatteryBackupHelper.isOwner());
if (intent != null
&& Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())
&& BatteryBackupHelper.isOwner()) {
diff --git a/src/com/android/settings/fuelgauge/BatteryUtils.java b/src/com/android/settings/fuelgauge/BatteryUtils.java
index 12760b18b0aef3f473df3ef5778586736521e175..1f7e3ec282ba44f1d7f599313baa11da210a4cb5 100644
--- a/src/com/android/settings/fuelgauge/BatteryUtils.java
+++ b/src/com/android/settings/fuelgauge/BatteryUtils.java
@@ -64,8 +64,10 @@ import com.google.protobuf.MessageLite;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import java.time.Duration;
import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.FormatStyle;
import java.util.List;
/**
@@ -353,7 +355,7 @@ public class BatteryUtils {
@SuppressWarnings("unchecked")
public static T parseProtoFromString(
String serializedProto, T protoClass) {
- if (serializedProto.isEmpty()) {
+ if (serializedProto == null || serializedProto.isEmpty()) {
return (T) protoClass.getDefaultInstanceForType();
}
try {
@@ -451,12 +453,10 @@ public class BatteryUtils {
@VisibleForTesting
Estimate getEnhancedEstimate() {
- Estimate estimate = null;
- // Get enhanced prediction if available
- if (Duration.between(Estimate.getLastCacheUpdateTime(mContext), Instant.now())
- .compareTo(Duration.ofSeconds(10)) < 0) {
- estimate = Estimate.getCachedEstimateIfAvailable(mContext);
- } else if (mPowerUsageFeatureProvider != null &&
+ // Align the same logic in the BatteryControllerImpl.updateEstimate()
+ Estimate estimate = Estimate.getCachedEstimateIfAvailable(mContext);
+ if (estimate == null &&
+ mPowerUsageFeatureProvider != null &&
mPowerUsageFeatureProvider.isEnhancedBatteryPredictionEnabled(mContext)) {
estimate = mPowerUsageFeatureProvider.getEnhancedBatteryPrediction(mContext);
if (estimate != null) {
@@ -673,6 +673,14 @@ public class BatteryUtils {
}
return summary.toString();
}
+ /** Format the date of battery related info */
+ public static CharSequence getBatteryInfoFormattedDate(long dateInMs) {
+ final Instant instant = Instant.ofEpochMilli(dateInMs);
+ final String localDate = instant.atZone(ZoneId.systemDefault()).toLocalDate().format(
+ DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG));
+
+ return localDate;
+ }
/** Builds the battery usage time information for one timestamp. */
private static String buildBatteryUsageTimeInfo(final Context context, long timeInMs,
diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
index 0b0e2430c90311c468fbbc312fca266646ec433e..4253ca6db55fd6b2606ae6357b622bbbba411a4a 100644
--- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
+++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProvider.java
@@ -18,9 +18,11 @@ package com.android.settings.fuelgauge;
import android.content.Context;
import android.content.Intent;
+import android.os.Bundle;
import android.util.ArrayMap;
import android.util.SparseIntArray;
+import com.android.settings.fuelgauge.batteryusage.PowerAnomalyEventList;
import com.android.settingslib.fuelgauge.Estimate;
import java.util.List;
@@ -36,6 +38,11 @@ public interface PowerUsageFeatureProvider {
*/
boolean isBatteryUsageEnabled();
+ /**
+ * Check whether the battery tips card is enabled in the battery usage page
+ */
+ boolean isBatteryTipsEnabled();
+
/**
* Returns a threshold (in milliseconds) for the minimal screen on time in battery usage list
*/
@@ -128,6 +135,16 @@ public interface PowerUsageFeatureProvider {
*/
boolean delayHourlyJobWhenBooting();
+ /**
+ * Insert settings configuration data for anomaly detection
+ */
+ void insertSettingsData(Context context, double displayDrain);
+
+ /**
+ * Returns {@link Bundle} for settings anomaly detection result
+ */
+ PowerAnomalyEventList detectSettingsAnomaly(Context context, double displayDrain);
+
/**
* Gets an intent for one time bypass charge limited to resume charging.
*/
diff --git a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
index 1d0ba18b40fa64ebc2a96604aea6c8bcf6d29088..5931e206b1d126d9cb0b8eff4074f8c690679216 100644
--- a/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
+++ b/src/com/android/settings/fuelgauge/PowerUsageFeatureProviderImpl.java
@@ -27,6 +27,7 @@ import android.util.ArraySet;
import android.util.SparseIntArray;
import com.android.internal.util.ArrayUtils;
+import com.android.settings.fuelgauge.batteryusage.PowerAnomalyEventList;
import com.android.settingslib.fuelgauge.Estimate;
import java.util.ArrayList;
@@ -74,6 +75,11 @@ public class PowerUsageFeatureProviderImpl implements PowerUsageFeatureProvider
return true;
}
+ @Override
+ public boolean isBatteryTipsEnabled() {
+ return false;
+ }
+
@Override
public double getBatteryUsageListScreenOnTimeThresholdInMs() {
return 0;
@@ -160,6 +166,14 @@ public class PowerUsageFeatureProviderImpl implements PowerUsageFeatureProvider
return true;
}
+ @Override
+ public void insertSettingsData(Context context, double displayDrain) {}
+
+ @Override
+ public PowerAnomalyEventList detectSettingsAnomaly(Context context, double displayDrain) {
+ return null;
+ }
+
@Override
public Set getOthersSystemComponentSet() {
return new ArraySet<>();
diff --git a/src/com/android/settings/fuelgauge/TopLevelBatteryPreferenceController.java b/src/com/android/settings/fuelgauge/TopLevelBatteryPreferenceController.java
index 254cf046bb8bd385fc6d80ed394cbbd99f6de18a..e08f4ba56db8ea79ce6d62febad9d5411d90b4dd 100644
--- a/src/com/android/settings/fuelgauge/TopLevelBatteryPreferenceController.java
+++ b/src/com/android/settings/fuelgauge/TopLevelBatteryPreferenceController.java
@@ -18,6 +18,7 @@ package com.android.settings.fuelgauge;
import android.content.ComponentName;
import android.content.Context;
+import android.os.BatteryManager;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
@@ -139,7 +140,10 @@ public class TopLevelBatteryPreferenceController extends BasePreferenceControlle
if (Utils.containsIncompatibleChargers(mContext, TAG)) {
return mContext.getString(R.string.battery_info_status_not_charging);
}
- if (!info.discharging && info.chargeLabel != null) {
+ if (info.batteryStatus == BatteryManager.BATTERY_STATUS_NOT_CHARGING) {
+ // Present status only if no remaining time or status anomalous
+ return info.statusLabel;
+ } else if (!info.discharging && info.chargeLabel != null) {
return info.chargeLabel;
} else if (info.remainingLabel == null) {
return info.batteryPercentString;
diff --git a/src/com/android/settings/fuelgauge/batterytip/detectors/LowBatteryDetector.java b/src/com/android/settings/fuelgauge/batterytip/detectors/LowBatteryDetector.java
index ed8cc62de59ff0254d0bfa4e03fda4d05d4c1096..9e970d29aed3774044fa3dcd74bb41b0f41556c7 100644
--- a/src/com/android/settings/fuelgauge/batterytip/detectors/LowBatteryDetector.java
+++ b/src/com/android/settings/fuelgauge/batterytip/detectors/LowBatteryDetector.java
@@ -23,8 +23,6 @@ import com.android.settings.fuelgauge.batterytip.BatteryTipPolicy;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
import com.android.settings.fuelgauge.batterytip.tips.LowBatteryTip;
-import java.util.concurrent.TimeUnit;
-
/**
* Detect whether the battery is too low
*/
@@ -46,9 +44,7 @@ public class LowBatteryDetector implements BatteryTipDetector {
@Override
public BatteryTip detect() {
- final boolean lowBattery = mBatteryInfo.batteryLevel <= mWarningLevel
- || (mBatteryInfo.discharging && mBatteryInfo.remainingTimeUs != 0
- && mBatteryInfo.remainingTimeUs < TimeUnit.HOURS.toMicros(mPolicy.lowBatteryHour));
+ final boolean lowBattery = mBatteryInfo.batteryLevel <= mWarningLevel;
final boolean lowBatteryEnabled = mPolicy.lowBatteryEnabled && !mIsPowerSaveMode;
final boolean dischargingLowBatteryState =
mPolicy.testLowBatteryTip || (mBatteryInfo.discharging && lowBattery);
diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java
index 8aabc37c6dce58206288d6707bcb5c4e4717c376..fdafca65c0cda662d460ad17f267497a8c7d1e66 100644
--- a/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java
+++ b/src/com/android/settings/fuelgauge/batterytip/tips/BatteryTip.java
@@ -20,9 +20,8 @@ import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.SparseIntArray;
-import android.view.View;
-import androidx.annotation.IdRes;
+import androidx.annotation.DrawableRes;
import androidx.annotation.IntDef;
import androidx.annotation.VisibleForTesting;
import androidx.preference.Preference;
@@ -134,7 +133,8 @@ public abstract class BatteryTip implements Comparable, Parcelable {
public abstract CharSequence getSummary(Context context);
- @IdRes
+ /** Gets the drawable resource id for the icon. */
+ @DrawableRes
public abstract int getIconId();
/**
@@ -162,21 +162,12 @@ public abstract class BatteryTip implements Comparable, Parcelable {
preference.setTitle(getTitle(context));
preference.setSummary(getSummary(context));
preference.setIcon(getIconId());
- @IdRes int iconTintColorId = getIconTintColorId();
- if (iconTintColorId != View.NO_ID) {
- preference.getIcon().setTint(context.getColor(iconTintColorId));
- }
final CardPreference cardPreference = castToCardPreferenceSafely(preference);
if (cardPreference != null) {
cardPreference.resetLayoutState();
}
}
- /** Returns the color resid for tinting {@link #getIconId()} or {@link View#NO_ID} if none. */
- public @IdRes int getIconTintColorId() {
- return View.NO_ID;
- }
-
public boolean shouldShowDialog() {
return mShowDialog;
}
diff --git a/src/com/android/settings/fuelgauge/batterytip/tips/IncompatibleChargerTip.java b/src/com/android/settings/fuelgauge/batterytip/tips/IncompatibleChargerTip.java
index 1c5616f867555b4294372c1b0155ef443bf34436..48cfb7a48896c8f3bdbd7f6cf63a8e020e68aaf4 100644
--- a/src/com/android/settings/fuelgauge/batterytip/tips/IncompatibleChargerTip.java
+++ b/src/com/android/settings/fuelgauge/batterytip/tips/IncompatibleChargerTip.java
@@ -52,7 +52,7 @@ public final class IncompatibleChargerTip extends BatteryTip {
@Override
public int getIconId() {
- return R.drawable.ic_battery_alert_theme;
+ return R.drawable.ic_battery_charger;
}
@Override
diff --git a/src/com/android/settings/fuelgauge/batteryusage/AnomalyAppItemPreference.java b/src/com/android/settings/fuelgauge/batteryusage/AnomalyAppItemPreference.java
new file mode 100644
index 0000000000000000000000000000000000000000..2f139ecaabc55d09d60611806486ebe3aded3aeb
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/AnomalyAppItemPreference.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2023 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.settings.fuelgauge.batteryusage;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.settings.R;
+
+class AnomalyAppItemPreference extends PowerGaugePreference {
+
+ private static final String TAG = "AnomalyAppItemPreference";
+
+ private CharSequence mAnomalyHintText;
+
+ AnomalyAppItemPreference(Context context) {
+ super(context, /* attrs */ null);
+ setLayoutResource(R.layout.anomaly_app_item_preference);
+ }
+
+ void setAnomalyHint(CharSequence anomalyHintText) {
+ if (!TextUtils.equals(mAnomalyHintText, anomalyHintText)) {
+ mAnomalyHintText = anomalyHintText;
+ notifyChanged();
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(PreferenceViewHolder viewHolder) {
+ super.onBindViewHolder(viewHolder);
+ final LinearLayout warningChipView =
+ (LinearLayout) viewHolder.findViewById(R.id.warning_chip);
+
+ if (!TextUtils.isEmpty(mAnomalyHintText)) {
+ ((TextView) warningChipView.findViewById(R.id.warning_info)).setText(mAnomalyHintText);
+ warningChipView.setVisibility(View.VISIBLE);
+ } else {
+ warningChipView.setVisibility(View.GONE);
+ }
+ }
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/AnomalyEventWrapper.java b/src/com/android/settings/fuelgauge/batteryusage/AnomalyEventWrapper.java
new file mode 100644
index 0000000000000000000000000000000000000000..d5354900b055df44d85c13b9e9ec544e4dec3470
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batteryusage/AnomalyEventWrapper.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2023 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.settings.fuelgauge.batteryusage;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import com.android.settings.R;
+import com.android.settings.SettingsActivity;
+import com.android.settings.core.SubSettingLauncher;
+
+import java.util.function.Function;
+
+final class AnomalyEventWrapper {
+ private static final String TAG = "AnomalyEventWrapper";
+
+ private final Context mContext;
+ private final PowerAnomalyEvent mPowerAnomalyEvent;
+
+ private final int mCardStyleId;
+ private final int mResourceIndex;
+
+ private SubSettingLauncher mSubSettingLauncher = null;
+ private Pair mHighlightSlotPair = null;
+ private BatteryDiffEntry mRelatedBatteryDiffEntry = null;
+
+ AnomalyEventWrapper(Context context, PowerAnomalyEvent powerAnomalyEvent) {
+ mContext = context;
+ mPowerAnomalyEvent = powerAnomalyEvent;
+ // Set basic battery tips card info
+ mCardStyleId = mPowerAnomalyEvent.getType().getNumber();
+ mResourceIndex = mPowerAnomalyEvent.getKey().getNumber();
+ }
+
+ private T getInfo(Function warningBannerInfoSupplier,
+ Function warningItemInfoSupplier) {
+ if (warningBannerInfoSupplier != null && mPowerAnomalyEvent.hasWarningBannerInfo()) {
+ return warningBannerInfoSupplier.apply(mPowerAnomalyEvent.getWarningBannerInfo());
+ } else if (warningItemInfoSupplier != null && mPowerAnomalyEvent.hasWarningItemInfo()) {
+ return warningItemInfoSupplier.apply(mPowerAnomalyEvent.getWarningItemInfo());
+ }
+ return null;
+ }
+
+ private int getResourceId(int resourceId, int resourceIndex, String defType) {
+ final String key = getStringFromArrayResource(resourceId, resourceIndex);
+ return TextUtils.isEmpty(key) ? 0
+ : mContext.getResources().getIdentifier(key, defType, mContext.getPackageName());
+ }
+
+ private String getString(Function warningBannerInfoSupplier,
+ Function warningItemInfoSupplier,
+ int resourceId, int resourceIndex) {
+ final String string = getInfo(warningBannerInfoSupplier, warningItemInfoSupplier);
+ return (!TextUtils.isEmpty(string) || resourceId <= 0) ? string
+ : getStringFromArrayResource(resourceId, resourceIndex);
+ }
+
+ private String getStringFromArrayResource(int resourceId, int resourceIndex) {
+ if (resourceId <= 0 || resourceIndex < 0) {
+ return null;
+ }
+ final String[] stringArray = mContext.getResources().getStringArray(resourceId);
+ return (resourceIndex >= 0 && resourceIndex < stringArray.length)
+ ? stringArray[resourceIndex] : null;
+ }
+
+ void setRelatedBatteryDiffEntry(BatteryDiffEntry batteryDiffEntry) {
+ mRelatedBatteryDiffEntry = batteryDiffEntry;
+ }
+
+ String getEventId() {
+ return mPowerAnomalyEvent.hasEventId() ? mPowerAnomalyEvent.getEventId() : null;
+ }
+
+ int getIconResId() {
+ return getResourceId(R.array.battery_tips_card_icons, mCardStyleId, "drawable");
+ }
+
+ int getColorResId() {
+ return getResourceId(R.array.battery_tips_card_colors, mCardStyleId, "color");
+ }
+
+ String getTitleString() {
+ final String protoTitleString = getInfo(WarningBannerInfo::getTitleString,
+ WarningItemInfo::getTitleString);
+ if (!TextUtils.isEmpty(protoTitleString)) {
+ return protoTitleString;
+ }
+ final int titleFormatResId = getResourceId(R.array.power_anomaly_title_ids,
+ mResourceIndex, "string");
+ if (mPowerAnomalyEvent.hasWarningBannerInfo()) {
+ return mContext.getString(titleFormatResId);
+ } else if (mPowerAnomalyEvent.hasWarningItemInfo() && mRelatedBatteryDiffEntry != null) {
+ final String appLabel = mRelatedBatteryDiffEntry.getAppLabel();
+ return mContext.getString(titleFormatResId, appLabel);
+ }
+ return null;
+ }
+
+ String getMainBtnString() {
+ return getString(WarningBannerInfo::getMainButtonString,
+ WarningItemInfo::getMainButtonString,
+ R.array.power_anomaly_main_btn_strings, mResourceIndex);
+ }
+
+ String getDismissBtnString() {
+ return getString(WarningBannerInfo::getCancelButtonString,
+ WarningItemInfo::getCancelButtonString,
+ R.array.power_anomaly_dismiss_btn_strings, mResourceIndex);
+ }
+
+ String getAnomalyHintString() {
+ return getStringFromArrayResource(R.array.power_anomaly_hint_messages, mResourceIndex);
+ }
+
+ String getDismissRecordKey() {
+ return mPowerAnomalyEvent.getDismissRecordKey();
+ }
+
+ boolean hasAnomalyEntryKey() {
+ return getAnomalyEntryKey() != null;
+ }
+
+ String getAnomalyEntryKey() {
+ return mPowerAnomalyEvent.hasWarningItemInfo()
+ && mPowerAnomalyEvent.getWarningItemInfo().hasItemKey()
+ ? mPowerAnomalyEvent.getWarningItemInfo().getItemKey() : null;
+ }
+
+ boolean hasSubSettingLauncher() {
+ if (mSubSettingLauncher == null) {
+ mSubSettingLauncher = getSubSettingLauncher();
+ }
+ return mSubSettingLauncher != null;
+ }
+
+ SubSettingLauncher getSubSettingLauncher() {
+ if (mSubSettingLauncher != null) {
+ return mSubSettingLauncher;
+ }
+ final String destinationClassName = getInfo(
+ WarningBannerInfo::getMainButtonDestination, null);
+ if (!TextUtils.isEmpty(destinationClassName)) {
+ final Integer sourceMetricsCategory = getInfo(
+ WarningBannerInfo::getMainButtonSourceMetricsCategory, null);
+ final String preferenceHighlightKey = getInfo(
+ WarningBannerInfo::getMainButtonSourceHighlightKey, null);
+ Bundle arguments = Bundle.EMPTY;
+ if (!TextUtils.isEmpty(preferenceHighlightKey)) {
+ arguments = new Bundle(1);
+ arguments.putString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY,
+ preferenceHighlightKey);
+ }
+ mSubSettingLauncher = new SubSettingLauncher(mContext)
+ .setDestination(destinationClassName)
+ .setSourceMetricsCategory(sourceMetricsCategory)
+ .setArguments(arguments);
+ }
+ return mSubSettingLauncher;
+ }
+
+ boolean hasHighlightSlotPair(BatteryLevelData batteryLevelData) {
+ if (mHighlightSlotPair == null) {
+ mHighlightSlotPair = getHighlightSlotPair(batteryLevelData);
+ }
+ return mHighlightSlotPair != null;
+ }
+
+ Pair getHighlightSlotPair(BatteryLevelData batteryLevelData) {
+ if (mHighlightSlotPair != null) {
+ return mHighlightSlotPair;
+ }
+ if (!mPowerAnomalyEvent.hasWarningItemInfo()) {
+ return null;
+ }
+ final WarningItemInfo warningItemInfo = mPowerAnomalyEvent.getWarningItemInfo();
+ final Long startTimestamp = warningItemInfo.hasStartTimestamp()
+ ? warningItemInfo.getStartTimestamp() : null;
+ final Long endTimestamp = warningItemInfo.hasEndTimestamp()
+ ? warningItemInfo.getEndTimestamp() : null;
+ if (startTimestamp != null && endTimestamp != null) {
+ mHighlightSlotPair = batteryLevelData
+ .getIndexByTimestamps(startTimestamp, endTimestamp);
+ if (mHighlightSlotPair.first == BatteryChartViewModel.SELECTED_INDEX_INVALID
+ || mHighlightSlotPair.second == BatteryChartViewModel.SELECTED_INDEX_INVALID) {
+ // Drop invalid mHighlightSlotPair index
+ mHighlightSlotPair = null;
+ }
+ }
+ return mHighlightSlotPair;
+ }
+
+ boolean updateTipsCardPreference(BatteryTipsCardPreference preference) {
+ final String titleString = getTitleString();
+ if (TextUtils.isEmpty(titleString)) {
+ return false;
+ }
+ preference.setTitle(titleString);
+ preference.setIconResourceId(getIconResId());
+ preference.setMainButtonStrokeColorResourceId(getColorResId());
+ preference.setMainButtonLabel(getMainBtnString());
+ preference.setDismissButtonLabel(getDismissBtnString());
+ return true;
+ }
+
+ boolean launchSubSetting() {
+ if (!hasSubSettingLauncher()) {
+ return false;
+ }
+ // Navigate to sub setting page
+ mSubSettingLauncher.launch();
+ return true;
+ }
+}
diff --git a/src/com/android/settings/fuelgauge/batteryusage/AppUsageDataLoader.java b/src/com/android/settings/fuelgauge/batteryusage/AppUsageDataLoader.java
deleted file mode 100644
index c336fcdfc6594086e6f9a6468203f4b67a08f28f..0000000000000000000000000000000000000000
--- a/src/com/android/settings/fuelgauge/batteryusage/AppUsageDataLoader.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2022 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.settings.fuelgauge.batteryusage;
-
-import android.app.usage.UsageEvents;
-import android.content.Context;
-import android.os.AsyncTask;
-import android.util.Log;
-
-import androidx.annotation.VisibleForTesting;
-
-import java.util.List;
-import java.util.Map;
-import java.util.function.Supplier;
-
-/** Load app usage events data in the background. */
-public final class AppUsageDataLoader {
- private static final String TAG = "AppUsageDataLoader";
-
- // For testing only.
- @VisibleForTesting
- static Supplier