diff --git a/res/drawable/ic_check_list_24dp.xml b/res/drawable/ic_check_list_24dp.xml
new file mode 100644
index 00000000000..4d8955cbccb
--- /dev/null
+++ b/res/drawable/ic_check_list_24dp.xml
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/res/layout/fingerprint_check_enrolled_dialog.xml b/res/layout/fingerprint_check_enrolled_dialog.xml
new file mode 100644
index 00000000000..5565829d825
--- /dev/null
+++ b/res/layout/fingerprint_check_enrolled_dialog.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 236a66183ce..0ddab5bba51 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -938,6 +938,8 @@
When using Fingerprint Unlock
Fingerprint for work
+
+ Check enrolled fingerprints
Add fingerprint
@@ -991,7 +993,10 @@
For best results, use a screen protector that\u2019s Made for Google certified. With other screen protectors, your child\u2019s fingerprint may not work.
-
+
+ Touch the fingerprint sensor
+
+ Fingerprint not recognized
Watch Unlock
@@ -2035,6 +2040,11 @@
%1$s app will no longer connect to your %2$s
Experimental. Improves audio quality.
+
+ Battery
+
+ Battery, charging
+
Forget device
diff --git a/src/com/android/settings/SettingsApplication.java b/src/com/android/settings/SettingsApplication.java
index f2257d4dfdf..442e3c2559a 100644
--- a/src/com/android/settings/SettingsApplication.java
+++ b/src/com/android/settings/SettingsApplication.java
@@ -44,7 +44,7 @@ import com.android.settings.spa.SettingsSpaEnvironment;
import com.android.settingslib.applications.AppIconCacheManager;
import com.android.settingslib.datastore.BackupRestoreStorageManager;
import com.android.settingslib.metadata.FixedArrayMap;
-import com.android.settingslib.metadata.PreferenceScreenMetadataCreator;
+import com.android.settingslib.metadata.PreferenceScreenMetadataFactory;
import com.android.settingslib.metadata.PreferenceScreenRegistry;
import com.android.settingslib.metadata.ProvidePreferenceScreenOptions;
import com.android.settingslib.preference.PreferenceBindingFactory;
@@ -75,8 +75,8 @@ public class SettingsApplication extends Application {
super.onCreate();
if (Flags.catalyst()) {
- PreferenceScreenRegistry.INSTANCE.setPreferenceScreenMetadataCreators(
- getPreferenceScreenCreators());
+ PreferenceScreenRegistry.INSTANCE.setPreferenceScreenMetadataFactories(
+ preferenceScreenFactories());
PreferenceBindingFactory.setDefaultFactory(new SettingsPreferenceBindingFactory());
}
@@ -106,8 +106,8 @@ public class SettingsApplication extends Application {
registerActivityLifecycleCallbacks(new DeveloperOptionsActivityLifecycle());
}
- /** Returns the creators of preference screen metadata. */
- protected FixedArrayMap getPreferenceScreenCreators() {
+ /** Returns the factories of preference screen metadata. */
+ protected FixedArrayMap preferenceScreenFactories() {
// PreferenceScreenCollector is generated by annotation processor from classes annotated
// with @ProvidePreferenceScreen
return PreferenceScreenCollector.get();
diff --git a/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java b/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java
index 1712e85259c..432b71121de 100644
--- a/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java
+++ b/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java
@@ -572,7 +572,7 @@ public class AppInfoDashboardFragment extends DashboardFragment
showIt = false;
} else if (mPackageInfo == null || mDpm.packageHasActiveAdmins(mPackageInfo.packageName)) {
showIt = false;
- } else if (UserHandle.myUserId() != 0) {
+ } else if (!mUserManager.isAdminUser()) {
showIt = false;
} else if (mUserManager.getUsers().size() < 2) {
showIt = false;
diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
index f649a40bb2c..8d6f3cb0306 100644
--- a/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
+++ b/src/com/android/settings/biometrics/fingerprint/FingerprintSettings.java
@@ -21,6 +21,7 @@ import static android.app.admin.DevicePolicyResources.Strings.Settings.FINGERPRI
import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_FINGERPRINT_LAST_DELETE_MESSAGE;
import static android.app.admin.DevicePolicyResources.UNDEFINED;
import static android.hardware.biometrics.Flags.screenOffUnlockUdfps;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static com.android.settings.Utils.SETTINGS_PACKAGE_NAME;
import static com.android.settings.Utils.isPrivateProfile;
@@ -40,6 +41,7 @@ import android.hardware.fingerprint.Fingerprint;
import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.os.Bundle;
+import android.os.CancellationSignal;
import android.os.Handler;
import android.os.UserHandle;
import android.os.UserManager;
@@ -49,8 +51,15 @@ import android.text.InputFilter;
import android.text.Spanned;
import android.text.TextUtils;
import android.util.Log;
+import android.view.LayoutInflater;
import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowInsetsController;
+import android.view.WindowManager;
import android.widget.ImeAwareEditText;
+import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -76,6 +85,7 @@ import com.android.settings.biometrics.IdentityCheckBiometricErrorDialog;
import com.android.settings.core.SettingsBaseActivity;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settings.dashboard.DashboardFragment;
+import com.android.settings.flags.Flags;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.password.ChooseLockGeneric;
import com.android.settings.password.ChooseLockSettingsHelper;
@@ -235,6 +245,9 @@ public class FingerprintSettings extends SubSettings {
private static final String TAG = "FingerprintSettings";
private static final String KEY_FINGERPRINT_ITEM_PREFIX = "key_fingerprint_item";
+
+ private static final String KEY_FINGERPRINT_CHECK_ENROLLED =
+ "key_fingerprint_check_enrolled";
@VisibleForTesting
static final String KEY_FINGERPRINT_ADD = "key_fingerprint_add";
private static final String KEY_FINGERPRINT_ENABLE_KEYGUARD_TOGGLE =
@@ -691,6 +704,17 @@ public class FingerprintSettings extends SubSettings {
mFingerprintsEnrolledCategory.addPreference(pref);
pref.setOnPreferenceChangeListener(this);
}
+ if (Flags.biometricsOnboardingEducation() && isUdfps() && fingerprintCount > 0) {
+ // Setup check enrolled fingerprints preference
+ Preference pref = new Preference(root.getContext());
+ pref.setKey(KEY_FINGERPRINT_CHECK_ENROLLED);
+ pref.setTitle(root.getContext().getString(
+ R.string.fingerprint_check_enrolled_title));
+ pref.setIcon(R.drawable.ic_check_list_24dp);
+ pref.setVisible(true);
+ mFingerprintsEnrolledCategory.addPreference(pref);
+ pref.setOnPreferenceChangeListener(this);
+ }
mAddFingerprintPreference = findPreference(KEY_FINGERPRINT_ADD);
setupAddFingerprintPreference();
return keyToReturn;
@@ -916,6 +940,8 @@ public class FingerprintSettings extends SubSettings {
FingerprintPreference fpref = (FingerprintPreference) pref;
final Fingerprint fp = fpref.getFingerprint();
showRenameDialog(fp);
+ } else if (KEY_FINGERPRINT_CHECK_ENROLLED.equals(key)) {
+ showCheckEnrolledDialog();
}
return super.onPreferenceTreeClick(pref);
}
@@ -968,6 +994,16 @@ public class FingerprintSettings extends SubSettings {
mAuthenticateSidecar.stopAuthentication();
}
+ private void showCheckEnrolledDialog() {
+ final CheckEnrolledDialog checkEnrolledDialog = new CheckEnrolledDialog();
+ final Bundle args = new Bundle();
+ args.putInt(CheckEnrolledDialog.KEY_USER_ID, mUserId);
+ args.putParcelable(CheckEnrolledDialog.KEY_SENSOR_PROPERTIES, mSensorProperties.get(0));
+ checkEnrolledDialog.setArguments(args);
+ checkEnrolledDialog.setTargetFragment(this, 0);
+ checkEnrolledDialog.show(getFragmentManager(), CheckEnrolledDialog.class.getName());
+ }
+
@Override
public boolean onPreferenceChange(Preference preference, Object value) {
boolean result = true;
@@ -1344,6 +1380,121 @@ public class FingerprintSettings extends SubSettings {
return new InputFilter[]{filter};
}
+ public static class CheckEnrolledDialog extends InstrumentedDialogFragment {
+
+ private static final String KEY_USER_ID = "user_id";
+ private static final String KEY_SENSOR_PROPERTIES = "sensor_properties";
+ private int mUserId;
+ private @Nullable CancellationSignal mCancellationSignal;
+ private @Nullable FingerprintSensorPropertiesInternal mSensorPropertiesInternal;
+
+ @Override
+ public @NonNull View onCreateView(
+ @NonNull LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(
+ R.layout.fingerprint_check_enrolled_dialog, container, false);
+ }
+
+ @Override
+ public @NonNull Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
+ final Dialog dialog = super.onCreateDialog(savedInstanceState);
+ if (dialog != null) {
+ mUserId = getArguments().getInt(KEY_USER_ID);
+ mSensorPropertiesInternal =
+ getArguments().getParcelable(KEY_SENSOR_PROPERTIES);
+
+ // Remove the default dialog title bar
+ dialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE);
+
+ dialog.setOnShowListener(dialogInterface -> {
+ final UdfpsCheckEnrolledView v =
+ dialog.findViewById(R.id.udfps_check_enrolled_view);
+ v.setSensorProperties(mSensorPropertiesInternal);
+ });
+ }
+
+ return dialog;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ if (getDialog() == null) {
+ return;
+ }
+
+ final Dialog dialog = getDialog();
+ Window window = dialog.getWindow();
+ WindowManager.LayoutParams params = window.getAttributes();
+
+ // Make the dialog fullscreen
+ params.width = WindowManager.LayoutParams.MATCH_PARENT;
+ params.height = WindowManager.LayoutParams.MATCH_PARENT;
+ params.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+ params.setFitInsetsTypes(0);
+ window.setAttributes(params);
+ window.getDecorView().getWindowInsetsController().hide(
+ WindowInsets.Type.statusBars());
+ window.getDecorView().getWindowInsetsController().setSystemBarsBehavior(
+ WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
+ window.setBackgroundDrawableResource(android.R.color.black);
+
+ final TextView message =
+ dialog.findViewById(R.id.udfps_fingerprint_sensor_message);
+ final Vibrator vibrator = getContext().getSystemService(Vibrator.class);
+ final FingerprintManager fpm = Utils.getFingerprintManagerOrNull(getContext());
+ mCancellationSignal = new CancellationSignal();
+ fpm.authenticate(
+ null /* crypto */,
+ mCancellationSignal,
+ new FingerprintManager.AuthenticationCallback() {
+ @Override
+ public void onAuthenticationError(
+ int errorCode, @NonNull CharSequence errString) {
+ dialog.dismiss();
+ }
+
+ @Override
+ public void onAuthenticationSucceeded(
+ @NonNull FingerprintManager.AuthenticationResult result) {
+ int fingerId = result.getFingerprint().getBiometricId();
+ FingerprintSettingsFragment parent =
+ (FingerprintSettingsFragment) getTargetFragment();
+ parent.highlightFingerprintItem(fingerId);
+ dialog.dismiss();
+ }
+
+ @Override
+ public void onAuthenticationFailed() {
+ vibrator.vibrate(
+ VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK));
+ message.setText(R.string.fingerprint_check_enroll_not_recognized);
+ message.postDelayed(() -> {
+ message.setText(R.string.fingerprint_check_enroll_touch_sensor);
+ }, 2000);
+ }
+ },
+ null /* handler */,
+ mUserId);
+ }
+
+ @Override
+ public void onDismiss(@NonNull DialogInterface dialog) {
+ super.onDismiss(dialog);
+ if (mCancellationSignal != null) {
+ mCancellationSignal.cancel();
+ mCancellationSignal = null;
+ }
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ return 0;
+ }
+ }
+
public static class RenameDialog extends InstrumentedDialogFragment {
private Fingerprint mFp;
diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsCheckEnrolledView.java b/src/com/android/settings/biometrics/fingerprint/UdfpsCheckEnrolledView.java
new file mode 100644
index 00000000000..52a28c75ea1
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint/UdfpsCheckEnrolledView.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2024 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.Rect;
+import android.graphics.RectF;
+import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.RotationUtils;
+import android.view.DisplayInfo;
+import android.view.Surface;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+import com.android.systemui.biometrics.UdfpsUtils;
+import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams;
+
+/**
+ * View corresponding with fingerprint_check_enrolled_dialog.xml
+ */
+public class UdfpsCheckEnrolledView extends RelativeLayout {
+ private static final String TAG = "UdfpsCheckEnrolledView";
+ @NonNull
+ private final UdfpsFingerprintDrawable mFingerprintDrawable;
+ private ImageView mFingerprintView;
+ private UdfpsUtils mUdfpsUtils;
+
+ private @Nullable Rect mSensorRect;
+ private @Nullable UdfpsOverlayParams mOverlayParams;
+ private @Nullable FingerprintSensorPropertiesInternal mSensorProperties;
+
+
+ public UdfpsCheckEnrolledView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ mFingerprintDrawable = new UdfpsFingerprintDrawable(mContext, attrs);
+ mUdfpsUtils = new UdfpsUtils();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mFingerprintView = findViewById(R.id.udfps_fingerprint_sensor_view);
+ mFingerprintView.setImageDrawable(mFingerprintDrawable);
+ }
+
+ /**
+ * setup SensorProperties
+ */
+ public void setSensorProperties(@Nullable FingerprintSensorPropertiesInternal properties) {
+ mSensorProperties = properties;
+ updateOverlayParams();
+ }
+
+ private void onSensorRectUpdated() {
+ updateDimensions();
+
+ if (mSensorRect == null || mOverlayParams == null) {
+ Log.e(TAG, "Fail to onSensorRectUpdated, mSensorRect/mOverlayParams null");
+ return;
+ }
+
+ // Updates sensor rect in relation to the overlay view
+ mSensorRect.set(0, 0,
+ mOverlayParams.getSensorBounds().width(),
+ mOverlayParams.getSensorBounds().height());
+ mFingerprintDrawable.onSensorRectUpdated(new RectF(mSensorRect));
+ }
+
+ private void updateDimensions() {
+ if (mOverlayParams == null) {
+ Log.e(TAG, "Fail to updateDimensions for " + this + ", mOverlayParams null");
+ return;
+ }
+ // Original sensorBounds assume portrait mode.
+ final Rect rotatedBounds = new Rect(mOverlayParams.getSensorBounds());
+ int rotation = mOverlayParams.getRotation();
+ if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) {
+ RotationUtils.rotateBounds(
+ rotatedBounds,
+ mOverlayParams.getNaturalDisplayWidth(),
+ mOverlayParams.getNaturalDisplayHeight(),
+ rotation
+ );
+ }
+
+ RelativeLayout parent = ((RelativeLayout) getParent());
+ if (parent == null) {
+ Log.e(TAG, "Fail to updateDimensions for " + this + ", parent null");
+ return;
+ }
+ final int[] coords = parent.getLocationOnScreen();
+ final int parentLeft = coords[0];
+ final int parentTop = coords[1];
+ final int parentRight = parentLeft + parent.getWidth();
+
+ // Update container view LayoutParams
+ RelativeLayout.LayoutParams checkEnrolledViewLp =
+ new RelativeLayout.LayoutParams(getWidth(), getHeight());
+ checkEnrolledViewLp.addRule(RelativeLayout.ALIGN_PARENT_TOP);
+ if (rotation == Surface.ROTATION_90) {
+ checkEnrolledViewLp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
+ checkEnrolledViewLp.width =
+ rotatedBounds.width() + 2 * (parentRight - rotatedBounds.right);
+ } else {
+ checkEnrolledViewLp.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
+ checkEnrolledViewLp.width = rotatedBounds.width() + 2 * rotatedBounds.left;
+ }
+ setLayoutParams(checkEnrolledViewLp);
+
+ // Update fingerprint view LayoutParams
+ RelativeLayout.LayoutParams fingerprintViewLp = new RelativeLayout.LayoutParams(
+ rotatedBounds.width(), rotatedBounds.height());
+ fingerprintViewLp.addRule(RelativeLayout.ALIGN_PARENT_TOP);
+ fingerprintViewLp.topMargin = rotatedBounds.top - parentTop;
+ if (rotation == Surface.ROTATION_90) {
+ fingerprintViewLp.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
+ fingerprintViewLp.rightMargin = parentRight - rotatedBounds.right;
+ } else {
+ fingerprintViewLp.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
+ fingerprintViewLp.leftMargin = rotatedBounds.left - parentLeft;
+ }
+ mFingerprintView.setLayoutParams(fingerprintViewLp);
+ }
+
+ private void updateOverlayParams() {
+
+ if (mSensorProperties == null) {
+ android.util.Log.e(TAG, "There is no sensor info!");
+ return;
+ }
+
+ DisplayInfo displayInfo = new DisplayInfo();
+ if (getDisplay() == null) {
+ android.util.Log.e(TAG, "Can not get display");
+ return;
+ }
+ getDisplay().getDisplayInfo(displayInfo);
+ Rect udfpsBounds = mSensorProperties.getLocation().getRect();
+ float scaleFactor = mUdfpsUtils.getScaleFactor(displayInfo);
+ udfpsBounds.scale(scaleFactor);
+
+ final Rect overlayBounds = new Rect(
+ 0, /* left */
+ displayInfo.getNaturalHeight() / 2, /* top */
+ displayInfo.getNaturalWidth(), /* right */
+ displayInfo.getNaturalHeight() /* botom */);
+
+ mOverlayParams = new UdfpsOverlayParams(
+ udfpsBounds,
+ overlayBounds,
+ displayInfo.getNaturalWidth(),
+ displayInfo.getNaturalHeight(),
+ scaleFactor,
+ displayInfo.rotation,
+ mSensorProperties.sensorType);
+
+ post(() -> {
+ if (mOverlayParams == null) {
+ Log.e(TAG, "Fail to updateOverlayParams, mOverlayParams null");
+ return;
+ }
+ mSensorRect = new Rect(mOverlayParams.getSensorBounds());
+ onSensorRectUpdated();
+ });
+ }
+}
diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsFingerprintDrawable.java b/src/com/android/settings/biometrics/fingerprint/UdfpsFingerprintDrawable.java
new file mode 100644
index 00000000000..e5ed6e16cc6
--- /dev/null
+++ b/src/com/android/settings/biometrics/fingerprint/UdfpsFingerprintDrawable.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2024 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.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.PathShape;
+import android.util.AttributeSet;
+import android.util.PathParser;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settings.R;
+
+/**
+ * UDFPS fingerprint drawable
+ */
+public class UdfpsFingerprintDrawable extends Drawable {
+ private static final String TAG = "UdfpsFingerprintDrawable";
+
+ private static final float DEFAULT_STROKE_WIDTH = 3f;
+
+ @NonNull
+ private final Paint mSensorOutlinePaint;
+ @NonNull
+ private final ShapeDrawable mFingerprintDrawable;
+ private int mAlpha;
+
+ @Nullable
+ private RectF mSensorRect;
+ private int mEnrollIcon;
+ private int mOutlineColor;
+
+ UdfpsFingerprintDrawable(@NonNull Context context, @Nullable AttributeSet attrs) {
+ mFingerprintDrawable = defaultFactory(context);
+
+ loadResources(context, attrs);
+ mSensorOutlinePaint = new Paint(0 /* flags */);
+ mSensorOutlinePaint.setAntiAlias(true);
+ mSensorOutlinePaint.setColor(mOutlineColor);
+ mSensorOutlinePaint.setStyle(Paint.Style.FILL);
+
+ mFingerprintDrawable.setTint(mEnrollIcon);
+
+ setAlpha(255);
+ }
+
+ /** The [sensorRect] coordinates for the sensor area. */
+ void onSensorRectUpdated(@NonNull RectF sensorRect) {
+ int margin = ((int) sensorRect.height()) / 8;
+ Rect bounds = new Rect((int) (sensorRect.left) + margin, (int) (sensorRect.top) + margin,
+ (int) (sensorRect.right) - margin, (int) (sensorRect.bottom) - margin);
+ updateFingerprintIconBounds(bounds);
+ mSensorRect = sensorRect;
+ }
+
+ void updateFingerprintIconBounds(@NonNull Rect bounds) {
+ mFingerprintDrawable.setBounds(bounds);
+ invalidateSelf();
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ if (mSensorRect != null) {
+ canvas.drawOval(mSensorRect, mSensorOutlinePaint);
+ }
+ mFingerprintDrawable.draw(canvas);
+ mFingerprintDrawable.setAlpha(getAlpha());
+ mSensorOutlinePaint.setAlpha(getAlpha());
+ }
+
+ @Override
+ public void setAlpha(int alpha) {
+ mAlpha = alpha;
+ mFingerprintDrawable.setAlpha(alpha);
+ mSensorOutlinePaint.setAlpha(alpha);
+ invalidateSelf();
+ }
+
+ @Override
+ public int getAlpha() {
+ return mAlpha;
+ }
+
+ @Override
+ public void setColorFilter(@Nullable ColorFilter colorFilter) {
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.OPAQUE;
+ }
+
+ private ShapeDrawable defaultFactory(Context context) {
+ String fpPath = context.getResources().getString(R.string.config_udfpsIcon);
+ ShapeDrawable drawable = new ShapeDrawable(
+ new PathShape(PathParser.createPathFromPathData(fpPath), 72f, 72f)
+ );
+ drawable.mutate();
+ drawable.getPaint().setStyle(Paint.Style.STROKE);
+ drawable.getPaint().setStrokeCap(Paint.Cap.ROUND);
+ drawable.getPaint().setStrokeWidth(DEFAULT_STROKE_WIDTH);
+ return drawable;
+ }
+
+ private void loadResources(Context context, @Nullable AttributeSet attrs) {
+ final TypedArray ta = context.obtainStyledAttributes(attrs,
+ R.styleable.BiometricsEnrollView, R.attr.biometricsEnrollStyle,
+ R.style.BiometricsEnrollStyle);
+ mEnrollIcon = ta.getColor(R.styleable.BiometricsEnrollView_biometricsEnrollIcon, 0);
+ mOutlineColor = ta.getColor(
+ R.styleable.BiometricsEnrollView_biometricsMovingTargetFill, 0);
+ ta.recycle();
+ }
+}
diff --git a/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java b/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java
index 02b8813c225..d8e834dfbf8 100644
--- a/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java
+++ b/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderController.java
@@ -626,6 +626,11 @@ public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceCont
imageView.setLayoutParams(layoutParams);
} else {
imageView.setImageDrawable(createBtBatteryIcon(mContext, level, charging));
+ imageView.setContentDescription(
+ mContext.getString(
+ charging
+ ? R.string.device_details_battery_charging
+ : R.string.device_details_battery));
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
imageView.setLayoutParams(layoutParams);
diff --git a/src/com/android/settings/password/SetupChooseLockGeneric.java b/src/com/android/settings/password/SetupChooseLockGeneric.java
index 1b771a2f84a..a5a36da8129 100644
--- a/src/com/android/settings/password/SetupChooseLockGeneric.java
+++ b/src/com/android/settings/password/SetupChooseLockGeneric.java
@@ -269,9 +269,7 @@ public class SetupChooseLockGeneric extends ChooseLockGeneric {
public void onViewCreated(@NotNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
GlifPreferenceLayout layout = (GlifPreferenceLayout) view;
- int titleResource = R.string.lock_settings_picker_new_lock_title;
- layout.setHeaderText(titleResource);
setDivider(new ColorDrawable(Color.TRANSPARENT));
setDividerHeight(0);
getHeaderView().setVisible(false);
diff --git a/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptions.kt b/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptions.kt
index e05340271f5..1a3dd6eaa1b 100644
--- a/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptions.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptions.kt
@@ -167,7 +167,7 @@ private fun ApplicationInfo.isShowUninstallUpdates(context: Context): Boolean =
private fun ApplicationInfo.isShowUninstallForAllUsers(
userManager: UserManager,
packageManagers: IPackageManagers,
-): Boolean = userId == 0 && !isSystemApp && !isInstantApp &&
+): Boolean = userManager.isUserAdmin(userId) && !isSystemApp && !isInstantApp &&
isOtherUserHasInstallPackage(userManager, packageManagers)
private fun ApplicationInfo.isOtherUserHasInstallPackage(
diff --git a/src/com/android/settings/spa/network/MobileDataSwitchPreference.kt b/src/com/android/settings/spa/network/MobileDataSwitchPreference.kt
new file mode 100644
index 00000000000..e178dc378a6
--- /dev/null
+++ b/src/com/android/settings/spa/network/MobileDataSwitchPreference.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2024 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.spa.network
+
+import androidx.annotation.VisibleForTesting
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.settings.R
+import com.android.settings.network.telephony.MobileDataRepository
+import com.android.settings.network.telephony.subscriptionManager
+import com.android.settingslib.spa.framework.compose.rememberContext
+import com.android.settingslib.spa.widget.preference.SwitchPreference
+import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+
+@Composable
+fun MobileDataSwitchPreference(subId: Int) {
+ MobileDataSwitchPreference(
+ subId = subId,
+ mobileDataRepository = rememberContext(::MobileDataRepository),
+ setMobileData = setMobileDataImpl(subId),
+ )
+}
+
+@VisibleForTesting
+@Composable
+fun MobileDataSwitchPreference(
+ subId: Int,
+ mobileDataRepository: MobileDataRepository,
+ setMobileData: (newChecked: Boolean) -> Unit,
+) {
+ val mobileDataSummary = stringResource(id = R.string.mobile_data_settings_summary)
+ val isMobileDataEnabled by
+ remember(subId) { mobileDataRepository.isMobileDataEnabledFlow(subId) }
+ .collectAsStateWithLifecycle(initialValue = null)
+
+ SwitchPreference(
+ object : SwitchPreferenceModel {
+ override val title = stringResource(id = R.string.mobile_data_settings_title)
+ override val summary = { mobileDataSummary }
+ override val checked = { isMobileDataEnabled }
+ override val onCheckedChange = setMobileData
+ }
+ )
+}
+
+@Composable
+private fun setMobileDataImpl(subId: Int): (newChecked: Boolean) -> Unit {
+ val context = LocalContext.current
+ val coroutineScope = rememberCoroutineScope()
+ val wifiPickerTrackerHelper = rememberWifiPickerTrackerHelper()
+ return { newEnabled ->
+ coroutineScope.launch(Dispatchers.Default) {
+ setMobileData(
+ context = context,
+ subscriptionManager = context.subscriptionManager,
+ wifiPickerTrackerHelper = wifiPickerTrackerHelper,
+ subId = subId,
+ enabled = newEnabled,
+ )
+ }
+ }
+}
diff --git a/src/com/android/settings/spa/network/MobileDataSwitchingPreference.kt b/src/com/android/settings/spa/network/MobileDataSwitchingPreference.kt
deleted file mode 100644
index 4b95d448b5f..00000000000
--- a/src/com/android/settings/spa/network/MobileDataSwitchingPreference.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Copyright (C) 2024 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.spa.network
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.res.stringResource
-import com.android.settings.R
-import com.android.settingslib.spa.widget.preference.SwitchPreference
-import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-
-@Composable
-fun MobileDataSwitchingPreference(
- isMobileDataEnabled: () -> Boolean?,
- setMobileDataEnabled: (newEnabled: Boolean) -> Unit,
-) {
- val mobileDataSummary = stringResource(id = R.string.mobile_data_settings_summary)
- val coroutineScope = rememberCoroutineScope()
- SwitchPreference(
- object : SwitchPreferenceModel {
- override val title = stringResource(id = R.string.mobile_data_settings_title)
- override val summary = { mobileDataSummary }
- override val checked = { isMobileDataEnabled() }
- override val onCheckedChange: (Boolean) -> Unit = { newEnabled ->
- coroutineScope.launch(Dispatchers.Default) {
- setMobileDataEnabled(newEnabled)
- }
- }
- override val changeable:() -> Boolean = {true}
- }
- )
-}
diff --git a/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt b/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt
index f60ba81fc4c..4bdb0442f1d 100644
--- a/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt
+++ b/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt
@@ -29,6 +29,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Message
import androidx.compose.material.icons.outlined.DataUsage
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableIntState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
@@ -40,7 +41,6 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
-import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -60,7 +60,6 @@ import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.rememberContext
-import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.scaffold.RegularScaffold
@@ -110,51 +109,48 @@ open class NetworkCellularGroupProvider : SettingsPageProvider, SearchablePage {
var textsSelectedId = rememberSaveable {
mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
}
- var mobileDataSelectedId = rememberSaveable {
- mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
- }
+ val mobileDataSelectedId = rememberSaveable { mutableStateOf(null) }
var nonDdsRemember = rememberSaveable {
mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
}
- var showMobileDataSection = rememberSaveable {
- mutableStateOf(false)
- }
val subscriptionViewModel = viewModel()
CollectAirplaneModeAndFinishIfOn()
- remember {
- allOfFlows(context, subscriptionViewModel.selectableSubscriptionInfoListFlow)
- }.collectLatestWithLifecycle(LocalLifecycleOwner.current) {
- callsSelectedId.intValue = defaultVoiceSubId
- textsSelectedId.intValue = defaultSmsSubId
- mobileDataSelectedId.intValue = defaultDataSubId
- nonDdsRemember.intValue = nonDds
+ LaunchedEffect(Unit) {
+ allOfFlows(context, subscriptionViewModel.selectableSubscriptionInfoListFlow).collect {
+ callsSelectedId.intValue = defaultVoiceSubId
+ textsSelectedId.intValue = defaultSmsSubId
+ mobileDataSelectedId.value = defaultDataSubId
+ nonDdsRemember.intValue = nonDds
+ }
}
val selectableSubscriptionInfoList by subscriptionViewModel
.selectableSubscriptionInfoListFlow
.collectAsStateWithLifecycle(initialValue = emptyList())
- showMobileDataSection.value = selectableSubscriptionInfoList
- .filter { subInfo -> subInfo.simSlotIndex > -1 }
- .size > 0
- val stringSims = stringResource(R.string.provider_network_settings_title)
- RegularScaffold(title = stringSims) {
+
+ RegularScaffold(title = stringResource(R.string.provider_network_settings_title)) {
SimsSection(selectableSubscriptionInfoList)
- if(showMobileDataSection.value) {
- MobileDataSectionImpl(
- mobileDataSelectedId,
- nonDdsRemember,
+ val mobileDataSelectedIdValue = mobileDataSelectedId.value
+ // Avoid draw mobile data UI before data ready to reduce flaky
+ if (mobileDataSelectedIdValue != null) {
+ val showMobileDataSection =
+ selectableSubscriptionInfoList.any { subInfo -> subInfo.simSlotIndex > -1 }
+ if (showMobileDataSection) {
+ MobileDataSectionImpl(mobileDataSelectedIdValue, nonDdsRemember.intValue)
+ }
+
+ PrimarySimSectionImpl(
+ subscriptionViewModel.selectableSubscriptionInfoListFlow,
+ callsSelectedId,
+ textsSelectedId,
+ remember(mobileDataSelectedIdValue) {
+ mutableIntStateOf(mobileDataSelectedIdValue)
+ },
)
}
- PrimarySimSectionImpl(
- subscriptionViewModel.selectableSubscriptionInfoListFlow,
- callsSelectedId,
- textsSelectedId,
- mobileDataSelectedId,
- )
-
OtherSection()
}
}
@@ -217,46 +213,23 @@ open class NetworkCellularGroupProvider : SettingsPageProvider, SearchablePage {
}
@Composable
-fun MobileDataSectionImpl(
- mobileDataSelectedId: MutableIntState,
- nonDds: MutableIntState,
-) {
- val context = LocalContext.current
- val localLifecycleOwner = LocalLifecycleOwner.current
+fun MobileDataSectionImpl(mobileDataSelectedId: Int, nonDds: Int) {
val mobileDataRepository = rememberContext(::MobileDataRepository)
Category(title = stringResource(id = R.string.mobile_data_settings_title)) {
- val isAutoDataEnabled by remember(nonDds.intValue) {
+ MobileDataSwitchPreference(subId = mobileDataSelectedId)
+
+ val isAutoDataEnabled by remember(nonDds) {
mobileDataRepository.isMobileDataPolicyEnabledFlow(
- subId = nonDds.intValue,
+ subId = nonDds,
policy = TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH
)
}.collectAsStateWithLifecycle(initialValue = null)
-
- val mobileDataStateChanged by remember(mobileDataSelectedId.intValue) {
- mobileDataRepository.isMobileDataEnabledFlow(mobileDataSelectedId.intValue)
- }.collectAsStateWithLifecycle(initialValue = false)
- val coroutineScope = rememberCoroutineScope()
-
- MobileDataSwitchingPreference(
- isMobileDataEnabled = { mobileDataStateChanged },
- setMobileDataEnabled = { newEnabled ->
- coroutineScope.launch {
- setMobileData(
- context,
- context.getSystemService(SubscriptionManager::class.java),
- getWifiPickerTrackerHelper(context, localLifecycleOwner),
- mobileDataSelectedId.intValue,
- newEnabled
- )
- }
- },
- )
- if (nonDds.intValue != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+ if (SubscriptionManager.isValidSubscriptionId(nonDds)) {
AutomaticDataSwitchingPreference(
isAutoDataEnabled = { isAutoDataEnabled },
setAutoDataEnabled = { newEnabled ->
- mobileDataRepository.setAutoDataSwitch(nonDds.intValue, newEnabled)
+ mobileDataRepository.setAutoDataSwitch(nonDds, newEnabled)
},
)
}
@@ -328,9 +301,6 @@ fun PrimarySimSectionImpl(
mobileDataSelectedId: MutableIntState,
) {
val context = LocalContext.current
- val localLifecycleOwner = LocalLifecycleOwner.current
- val wifiPickerTrackerHelper = getWifiPickerTrackerHelper(context, localLifecycleOwner)
-
val primarySimInfo = remember(subscriptionInfoListFlow) {
subscriptionInfoListFlow
.map { subscriptionInfoList ->
@@ -346,7 +316,7 @@ fun PrimarySimSectionImpl(
callsSelectedId,
textsSelectedId,
mobileDataSelectedId,
- wifiPickerTrackerHelper
+ rememberWifiPickerTrackerHelper()
)
}
}
@@ -354,22 +324,21 @@ fun PrimarySimSectionImpl(
@Composable
fun CollectAirplaneModeAndFinishIfOn() {
val context = LocalContext.current
- context.settingsGlobalBooleanFlow(Settings.Global.AIRPLANE_MODE_ON)
- .collectLatestWithLifecycle(LocalLifecycleOwner.current) { isAirplaneModeOn ->
+ LaunchedEffect(Unit) {
+ context.settingsGlobalBooleanFlow(Settings.Global.AIRPLANE_MODE_ON).collect {
+ isAirplaneModeOn ->
if (isAirplaneModeOn) {
context.getActivity()?.finish()
}
}
+ }
}
-private fun getWifiPickerTrackerHelper(
- context: Context,
- lifecycleOwner: LifecycleOwner
-): WifiPickerTrackerHelper {
- return WifiPickerTrackerHelper(
- LifecycleRegistry(lifecycleOwner), context,
- null /* WifiPickerTrackerCallback */
- )
+@Composable
+fun rememberWifiPickerTrackerHelper(): WifiPickerTrackerHelper {
+ val context = LocalContext.current
+ val lifecycleOwner = LocalLifecycleOwner.current
+ return remember { WifiPickerTrackerHelper(LifecycleRegistry(lifecycleOwner), context, null) }
}
private fun Context.defaultVoiceSubscriptionFlow(): Flow =
diff --git a/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceController.java b/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceController.java
index d61b3d043cd..dbe62d433e9 100644
--- a/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceController.java
+++ b/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceController.java
@@ -20,7 +20,9 @@ import android.app.settings.SettingsEnums;
import android.content.Context;
import android.net.wifi.SoftApConfiguration;
import android.text.TextUtils;
+import android.widget.EditText;
+import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.preference.EditTextPreference;
import androidx.preference.Preference;
@@ -36,7 +38,8 @@ import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
* Controller for logic pertaining to the password of Wi-Fi tethering.
*/
public class WifiTetherPasswordPreferenceController extends WifiTetherBasePreferenceController
- implements ValidatedEditTextPreference.Validator {
+ implements ValidatedEditTextPreference.Validator,
+ EditTextPreference.OnBindEditTextListener {
private static final String PREF_KEY = "wifi_tether_network_password";
@@ -80,6 +83,7 @@ public class WifiTetherPasswordPreferenceController extends WifiTetherBasePrefer
((ValidatedEditTextPreference) mPreference).setValidator(this);
((ValidatedEditTextPreference) mPreference).setIsPassword(true);
((ValidatedEditTextPreference) mPreference).setIsSummaryPassword(true);
+ ((EditTextPreference) mPreference).setOnBindEditTextListener(this);
updatePasswordDisplay((EditTextPreference) mPreference);
}
@@ -143,4 +147,9 @@ public class WifiTetherPasswordPreferenceController extends WifiTetherBasePrefer
pref.setVisible(false);
}
}
+
+ @Override
+ public void onBindEditText(@NonNull EditText editText) {
+ editText.setHint(R.string.wifi_hotspot_password_title);
+ }
}
diff --git a/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceController.java b/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceController.java
index d2d26ab84fa..a57768bbb19 100644
--- a/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceController.java
+++ b/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceController.java
@@ -22,18 +22,22 @@ import android.content.Intent;
import android.net.wifi.SoftApConfiguration;
import android.text.TextUtils;
import android.util.Log;
+import android.widget.EditText;
+import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.preference.EditTextPreference;
import androidx.preference.Preference;
+import com.android.settings.R;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.widget.ValidatedEditTextPreference;
import com.android.settings.wifi.dpp.WifiDppUtils;
import com.android.settingslib.core.instrumentation.MetricsFeatureProvider;
public class WifiTetherSSIDPreferenceController extends WifiTetherBasePreferenceController
- implements ValidatedEditTextPreference.Validator {
+ implements ValidatedEditTextPreference.Validator,
+ EditTextPreference.OnBindEditTextListener {
private static final String TAG = "WifiTetherSsidPref";
private static final String PREF_KEY = "wifi_tether_network_name";
@@ -93,6 +97,7 @@ public class WifiTetherSSIDPreferenceController extends WifiTetherBasePreference
((WifiTetherSsidPreference) mPreference).setButtonVisible(false);
}
+ ((EditTextPreference) mPreference).setOnBindEditTextListener(this);
updateSsidDisplay((EditTextPreference) mPreference);
}
@@ -138,4 +143,9 @@ public class WifiTetherSSIDPreferenceController extends WifiTetherBasePreference
boolean isQrCodeButtonAvailable() {
return ((WifiTetherSsidPreference) mPreference).isQrCodeButtonAvailable();
}
+
+ @Override
+ public void onBindEditText(@NonNull EditText editText) {
+ editText.setHint(R.string.wifi_hotspot_name_title);
+ }
}
diff --git a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java
index 1086f85d45c..a570baadd59 100644
--- a/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/biometrics/fingerprint/FingerprintSettingsFragmentTest.java
@@ -17,6 +17,7 @@
package com.android.settings.biometrics.fingerprint;
import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR;
import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL;
import static com.android.settings.biometrics.BiometricEnrollBase.BIOMETRIC_AUTH_REQUEST;
@@ -354,6 +355,48 @@ public class FingerprintSettingsFragmentTest {
assertThat(addPref.isEnabled()).isTrue();
}
+ @Test
+ @EnableFlags(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION)
+ public void testCheckEnrolledShown_whenAtLeastOneFingerprintEnrolled_Udfps() {
+ final Fingerprint fingerprint = new Fingerprint("Test", 0, 0);
+ doReturn(List.of(fingerprint)).when(mFingerprintManager).getEnrolledFingerprints(anyInt());
+ setUpFragment(false, PRIMARY_USER_ID, TYPE_UDFPS_OPTICAL, 5);
+
+ shadowOf(Looper.getMainLooper()).idle();
+
+ final Preference checkEnrolledPerf =
+ mFragment.findPreference("key_fingerprint_check_enrolled");
+ assertThat(checkEnrolledPerf).isNotNull();
+ assertThat(checkEnrolledPerf.isVisible()).isTrue();
+ }
+
+ @Test
+ @EnableFlags(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION)
+ public void testCheckEnrolledHide_whenNoFingerprintEnrolled_Udfps() {
+ doReturn(List.of()).when(mFingerprintManager).getEnrolledFingerprints(anyInt());
+ setUpFragment(false, PRIMARY_USER_ID, TYPE_UDFPS_OPTICAL, 5);
+
+ shadowOf(Looper.getMainLooper()).idle();
+
+ final Preference checkEnrolledPerf =
+ mFragment.findPreference("key_fingerprint_check_enrolled");
+ assertThat(checkEnrolledPerf).isNull();
+ }
+
+ @Test
+ @EnableFlags(com.android.settings.flags.Flags.FLAG_BIOMETRICS_ONBOARDING_EDUCATION)
+ public void testCheckEnrolledHide_nonUdfps() {
+ final Fingerprint fingerprint = new Fingerprint("Test", 0, 0);
+ doReturn(List.of(fingerprint)).when(mFingerprintManager).getEnrolledFingerprints(anyInt());
+ setUpFragment(false, PRIMARY_USER_ID, TYPE_REAR, 5);
+
+ shadowOf(Looper.getMainLooper()).idle();
+
+ final Preference checkEnrolledPerf =
+ mFragment.findPreference("key_fingerprint_check_enrolled");
+ assertThat(checkEnrolledPerf).isNull();
+ }
+
private void setSensor(@FingerprintSensorProperties.SensorType int sensorType,
int maxFingerprints) {
final ArrayList props = new ArrayList<>();
diff --git a/tests/robotests/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderControllerTest.java
index db8c862342f..fc1df5a0f69 100644
--- a/tests/robotests/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderControllerTest.java
+++ b/tests/robotests/src/com/android/settings/bluetooth/AdvancedBluetoothDetailsHeaderControllerTest.java
@@ -329,11 +329,16 @@ public class AdvancedBluetoothDetailsHeaderControllerTest {
mController.refresh();
- assertBatteryIcon(mLayoutPreference.findViewById(R.id.layout_left),
- R.drawable.ic_battery_alert_24dp);
- assertBatteryIcon(mLayoutPreference.findViewById(R.id.layout_right), /* resId= */-1);
- assertBatteryIcon(mLayoutPreference.findViewById(R.id.layout_middle),
- R.drawable.ic_battery_alert_24dp);
+ assertBatteryIcon(
+ mLayoutPreference.findViewById(R.id.layout_left),
+ R.drawable.ic_battery_alert_24dp,
+ false);
+ assertBatteryIcon(
+ mLayoutPreference.findViewById(R.id.layout_right), /* resId= */ -1, true);
+ assertBatteryIcon(
+ mLayoutPreference.findViewById(R.id.layout_middle),
+ R.drawable.ic_battery_alert_24dp,
+ false);
}
@Test
@@ -546,10 +551,15 @@ public class AdvancedBluetoothDetailsHeaderControllerTest {
}
}
- private void assertBatteryIcon(LinearLayout linearLayout, int resId) {
+ private void assertBatteryIcon(LinearLayout linearLayout, int resId, boolean charging) {
final ImageView imageView = linearLayout.findViewById(R.id.bt_battery_icon);
- assertThat(shadowOf(imageView.getDrawable()).getCreatedFromResId())
- .isEqualTo(resId);
+ if (charging) {
+ assertThat(imageView.getContentDescription().toString())
+ .isEqualTo(mContext.getString(R.string.device_details_battery_charging));
+ } else {
+ assertThat(imageView.getContentDescription().toString())
+ .isEqualTo(mContext.getString(R.string.device_details_battery));
+ }
+ assertThat(shadowOf(imageView.getDrawable()).getCreatedFromResId()).isEqualTo(resId);
}
-
}
diff --git a/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceControllerTest.java
index 500e31bc7d6..3dc0a1a5752 100644
--- a/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherPasswordPreferenceControllerTest.java
@@ -18,10 +18,13 @@ package com.android.settings.wifi.tether;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -32,6 +35,7 @@ import android.content.Context;
import android.net.TetheringManager;
import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiManager;
+import android.widget.EditText;
import androidx.preference.PreferenceScreen;
@@ -81,7 +85,7 @@ public class WifiTetherPasswordPreferenceControllerTest {
when(featureFactory.getWifiFeatureProvider().getWifiHotspotRepository())
.thenReturn(mWifiHotspotRepository);
- mPreference = new ValidatedEditTextPreference(RuntimeEnvironment.application);
+ mPreference = spy(new ValidatedEditTextPreference(RuntimeEnvironment.application));
mConfig = new SoftApConfiguration.Builder().setSsid("test_1234")
.setPassphrase(INITIAL_PASSWORD, SoftApConfiguration.SECURITY_TYPE_WPA2_PSK)
.build();
@@ -179,4 +183,20 @@ public class WifiTetherPasswordPreferenceControllerTest {
mController.updateDisplay();
assertThat(mPreference.isPassword()).isTrue();
}
+
+ @Test
+ public void updateDisplay_shouldSetOnBindEditTextListener() {
+ mController.displayPreference(mScreen);
+
+ verify(mPreference).setOnBindEditTextListener(any());
+ }
+
+ @Test
+ public void onBindEditText_shouldSetHint() {
+ EditText editText = mock(EditText.class);
+
+ mController.onBindEditText(editText);
+
+ verify(editText).setHint(anyInt());
+ }
}
diff --git a/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceControllerTest.java
index 07d57623745..e8ef9366f06 100644
--- a/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/wifi/tether/WifiTetherSSIDPreferenceControllerTest.java
@@ -18,10 +18,13 @@ package com.android.settings.wifi.tether;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -32,6 +35,7 @@ import android.content.Context;
import android.net.TetheringManager;
import android.net.wifi.SoftApConfiguration;
import android.net.wifi.WifiManager;
+import android.widget.EditText;
import androidx.preference.PreferenceScreen;
@@ -68,7 +72,7 @@ public class WifiTetherSSIDPreferenceControllerTest {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
- mPreference = new WifiTetherSsidPreference(RuntimeEnvironment.application);
+ mPreference = spy(new WifiTetherSsidPreference(RuntimeEnvironment.application));
doReturn(mock(DevicePolicyManager.class)).when(mContext)
.getSystemService(Context.DEVICE_POLICY_SERVICE);
@@ -147,12 +151,19 @@ public class WifiTetherSSIDPreferenceControllerTest {
assertThat(mPreference.getSummary()).isEqualTo(config.getSsid());
}
+ @Test
+ public void updateDisplay_shouldSetOnBindEditTextListener() {
+ mController.displayPreference(mScreen);
+
+ verify(mPreference).setOnBindEditTextListener(any());
+ }
+
@Test
public void displayPreference_wifiApDisabled_shouldHideQrCodeIcon() {
when(mWifiManager.isWifiApEnabled()).thenReturn(false);
final SoftApConfiguration config = new SoftApConfiguration.Builder()
.setSsid("test_1234").setPassphrase("test_password",
- SoftApConfiguration.SECURITY_TYPE_WPA2_PSK).build();
+ SoftApConfiguration.SECURITY_TYPE_WPA2_PSK).build();
when(mWifiManager.getSoftApConfiguration()).thenReturn(config);
mController.displayPreference(mScreen);
@@ -164,10 +175,19 @@ public class WifiTetherSSIDPreferenceControllerTest {
when(mWifiManager.isWifiApEnabled()).thenReturn(true);
final SoftApConfiguration config = new SoftApConfiguration.Builder()
.setSsid("test_1234").setPassphrase("test_password",
- SoftApConfiguration.SECURITY_TYPE_WPA2_PSK).build();
+ SoftApConfiguration.SECURITY_TYPE_WPA2_PSK).build();
when(mWifiManager.getSoftApConfiguration()).thenReturn(config);
mController.displayPreference(mScreen);
assertThat(mController.isQrCodeButtonAvailable()).isEqualTo(true);
}
+
+ @Test
+ public void onBindEditText_shouldSetHint() {
+ EditText editText = mock(EditText.class);
+
+ mController.onBindEditText(editText);
+
+ verify(editText).setHint(anyInt());
+ }
}
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptionsTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptionsTest.kt
index d4a989cc3ab..11ba67f75f6 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptionsTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptionsTest.kt
@@ -159,6 +159,7 @@ class AppInfoSettingsMoreOptionsTest {
packageName = PACKAGE_NAME
uid = UID
}
+ whenever(userManager.isUserAdmin(app.userId)).thenReturn(true)
whenever(userManager.aliveUsers).thenReturn(listOf(OTHER_USER))
whenever(packageManagers.isPackageInstalledAsUser(PACKAGE_NAME, OTHER_USER_ID))
.thenReturn(true)
@@ -171,12 +172,30 @@ class AppInfoSettingsMoreOptionsTest {
)
}
+ @Test
+ fun uninstallForAllUsers_NotAdminUser_notDisplayed() {
+ val app = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ uid = UID
+ }
+ whenever(userManager.isUserAdmin(app.userId)).thenReturn(false)
+ whenever(userManager.aliveUsers).thenReturn(listOf(OTHER_USER))
+ whenever(packageManagers.isPackageInstalledAsUser(PACKAGE_NAME, OTHER_USER_ID))
+ .thenReturn(true)
+
+ setContent(app)
+ composeTestRule.onRoot().performClick()
+
+ composeTestRule.onRoot().assertIsNotDisplayed()
+ }
+
@Test
fun uninstallForAllUsers_appHiddenNotInQuietModeAndPrimaryUser_displayed() {
val app = ApplicationInfo().apply {
packageName = PACKAGE_NAME
uid = UID
}
+ whenever(userManager.isUserAdmin(app.userId)).thenReturn(true)
whenever(userManager.aliveUsers).thenReturn(listOf(OTHER_USER))
whenever(packageManagers
.isPackageInstalledAsUser(PACKAGE_NAME, OTHER_USER_ID))
@@ -198,6 +217,7 @@ class AppInfoSettingsMoreOptionsTest {
packageName = PACKAGE_NAME
uid = UID
}
+ whenever(userManager.isUserAdmin(app.userId)).thenReturn(true)
whenever(userManager.aliveUsers).thenReturn(listOf(OTHER_USER))
whenever(packageManagers
.isPackageInstalledAsUser(PACKAGE_NAME, OTHER_USER_ID))
diff --git a/tests/spa_unit/src/com/android/settings/spa/network/MobileDataSwitchPreferenceTest.kt b/tests/spa_unit/src/com/android/settings/spa/network/MobileDataSwitchPreferenceTest.kt
new file mode 100644
index 00000000000..3334db9fd95
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/network/MobileDataSwitchPreferenceTest.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2024 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.spa.network
+
+import android.content.Context
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.R
+import com.android.settings.network.telephony.MobileDataRepository
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flowOf
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.stub
+
+@RunWith(AndroidJUnit4::class)
+class MobileDataSwitchPreferenceTest {
+ @get:Rule val composeTestRule = createComposeRule()
+
+ private val context: Context = spy(ApplicationProvider.getApplicationContext()) {}
+
+ private val mockMobileDataRepository =
+ mock { on { isMobileDataEnabledFlow(any()) } doReturn emptyFlow() }
+
+ @Test
+ fun title_displayed() {
+ composeTestRule.setContent {
+ CompositionLocalProvider(LocalContext provides context) {
+ MobileDataSwitchPreference(SUB_ID, mockMobileDataRepository) {}
+ }
+ }
+
+ composeTestRule
+ .onNodeWithText(context.getString(R.string.mobile_data_settings_title))
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun summary_displayed() {
+ composeTestRule.setContent {
+ CompositionLocalProvider(LocalContext provides context) {
+ MobileDataSwitchPreference(SUB_ID, mockMobileDataRepository) {}
+ }
+ }
+
+ composeTestRule
+ .onNodeWithText(context.getString(R.string.mobile_data_settings_summary))
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun onClick_whenOff_turnedOn() {
+ mockMobileDataRepository.stub {
+ on { isMobileDataEnabledFlow(SUB_ID) } doReturn flowOf(false)
+ }
+ var newCheckedCalled: Boolean? = null
+ composeTestRule.setContent {
+ CompositionLocalProvider(LocalContext provides context) {
+ MobileDataSwitchPreference(SUB_ID, mockMobileDataRepository) {
+ newCheckedCalled = it
+ }
+ }
+ }
+
+ composeTestRule
+ .onNodeWithText(context.getString(R.string.mobile_data_settings_title))
+ .performClick()
+
+ assertThat(newCheckedCalled).isTrue()
+ }
+
+ private companion object {
+ const val SUB_ID = 12
+ }
+}