diff --git a/AndroidManifest.xml b/AndroidManifest.xml index ccd6b77599a..e4a434c199a 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -993,6 +993,22 @@ android:value="true"/> + + + + + + + + + + diff --git a/res/drawable/ic_enrollment_fingerprint.xml b/res/drawable/ic_enrollment_fingerprint.xml new file mode 100644 index 00000000000..e5c4c6bcf5a --- /dev/null +++ b/res/drawable/ic_enrollment_fingerprint.xml @@ -0,0 +1,39 @@ + + + + + \ No newline at end of file diff --git a/res/drawable/udfps_enroll_checkmark.xml b/res/drawable/udfps_enroll_checkmark.xml new file mode 100644 index 00000000000..f8169d377f1 --- /dev/null +++ b/res/drawable/udfps_enroll_checkmark.xml @@ -0,0 +1,35 @@ + + + + + + + + diff --git a/res/layout/udfps_enroll_enrolling.xml b/res/layout/udfps_enroll_enrolling.xml index e9337538629..c97591d6d52 100644 --- a/res/layout/udfps_enroll_enrolling.xml +++ b/res/layout/udfps_enroll_enrolling.xml @@ -39,6 +39,7 @@ android:orientation="vertical"> + + + + + + + + + + + diff --git a/res/values-night/colors.xml b/res/values-night/colors.xml index 1fbd5aa8ef6..959bc172793 100644 --- a/res/values-night/colors.xml +++ b/res/values-night/colors.xml @@ -57,5 +57,15 @@ @android:color/white + + + + #7DA7F1 + #475670 + + #80475670 + #7DA7F1 + #607DA7F1 + #FFEE675C diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 599a145bd1d..90a308eee9f 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -184,4 +184,14 @@ + + + + + + + + + + diff --git a/res/values/colors.xml b/res/values/colors.xml index a4e6f7065cb..5d98e94f083 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -172,4 +172,13 @@ ?android:attr/textColorPrimary + + + #699FF3 + #C2D7F7 + + #80C2D7F7 + #699FF3 + #70699FF3 + #FFEE675C diff --git a/res/values/config.xml b/res/values/config.xml index 356d084560f..db62b186f86 100755 --- a/res/values/config.xml +++ b/res/values/config.xml @@ -578,13 +578,7 @@ @string/screen_resolution_option_high - @string/screen_resolution_option_highest - - - - - @string/screen_resolution_summary_high - @string/screen_resolution_summary_highest + @string/screen_resolution_option_full @@ -649,4 +643,25 @@ true + + + + 280 + + + + + M25.5,16.3283C28.47,14.8433 31.9167,14 35.5834,14C39.2501,14 42.6968,14.8433 45.6668,16.3283 + M20,28.6669C22.7683,24.3402 28.7084,21.3335 35.5834,21.3335C42.4585,21.3335 48.3985, + 24.3402 51.1669,28.6669 + M22.8607,47.0002C21.834,44.3235 21.834,41.5002 21.834,41.5002C21.834, + 34.4051 27.7374,28.6667 35.5841,28.6667C43.4308,28.6667 49.3341,34.4051 49.3341,41.5002 + M49.3344,41.5003V42.0319C49.3344,44.7636 47.1161,47.0003 44.3661,47.0003C41.9461, + 47.0003 39.8744,45.2403 39.471,42.857L38.9577, + 39.7769C38.591,37.5953 36.7027,36.0002 34.5027, + 36.0002C26.5826,36.0002 29.846,49.1087 35.291,50.6487 + M44.9713,54.6267C42.5513,56.7167 39.2879,58.0001 35.5846,58.0001C32.2296, + 58.0001 29.2229,56.9551 26.8945,55.195 + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 9d3bf70010e..4edc3b505ea 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -2116,14 +2116,10 @@ Screen resolution - + High resolution - - Full resolution - - 1080p FHD+ - - 1440p QHD+ + + Full resolution Full resolution uses more of your battery. Switching your resolution may cause some apps to restart. @@ -2452,7 +2448,9 @@ EID (sim slot %1$d) - IMEI (sim slot %1$d) + IMEI (sim slot %1$d) + + IMEI (sim slot %1$d) (primary) To view, choose saved network @@ -2468,7 +2466,9 @@ PRL version - MEID (sim slot %1$d) + MEID (sim slot %1$d) + + MEID (sim slot %1$d) (primary) MEID @@ -10786,6 +10786,9 @@ System default + + None + This choice is no longer valid. Try again. @@ -11564,4 +11567,7 @@ =1 {Apps installed more than # month ago} other {Apps installed more than # months ago} } + + + Fingerprint sensor diff --git a/res/values/styles.xml b/res/values/styles.xml index 1bd9c4ad599..352779bfce2 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -925,4 +925,13 @@ false true + + diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index cea4cc67146..0c58fff09e0 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -114,6 +114,8 @@ public class Settings extends SettingsActivity { public static class LocalePickerActivity extends SettingsActivity { /* empty */ } public static class LanguageAndInputSettingsActivity extends SettingsActivity { /* empty */ } public static class LanguageSettingsActivity extends SettingsActivity { /* empty */ } + /** Activity for the regional preferences settings. */ + public static class RegionalPreferencesActivity extends SettingsActivity { /* empty */ } public static class KeyboardSettingsActivity extends SettingsActivity { /* empty */ } public static class UserDictionarySettingsActivity extends SettingsActivity { /* empty */ } public static class DarkThemeSettingsActivity extends SettingsActivity { /* empty */ } diff --git a/src/com/android/settings/biometrics/BiometricEnrollSidecar.java b/src/com/android/settings/biometrics/BiometricEnrollSidecar.java index cedbec1a0ac..9a89f7a349f 100644 --- a/src/com/android/settings/biometrics/BiometricEnrollSidecar.java +++ b/src/com/android/settings/biometrics/BiometricEnrollSidecar.java @@ -40,6 +40,11 @@ public abstract class BiometricEnrollSidecar extends InstrumentedFragment { void onEnrollmentHelp(int helpMsgId, CharSequence helpString); void onEnrollmentError(int errMsgId, CharSequence errString); void onEnrollmentProgressChange(int steps, int remaining); + /** + * Called when a fingerprint image has been acquired. + * @param isAcquiredGood whether the fingerprint image was good. + */ + default void onAcquired(boolean isAcquiredGood) { } } private int mEnrollmentSteps = -1; @@ -100,6 +105,19 @@ public abstract class BiometricEnrollSidecar extends InstrumentedFragment { } } + private class QueuedAcquired extends QueuedEvent { + private final boolean isAcquiredGood; + + public QueuedAcquired(boolean isAcquiredGood) { + this.isAcquiredGood = isAcquiredGood; + } + + @Override + public void send(Listener listener) { + listener.onAcquired(isAcquiredGood); + } + } + private final Runnable mTimeoutRunnable = new Runnable() { @Override public void run() { @@ -189,6 +207,14 @@ public abstract class BiometricEnrollSidecar extends InstrumentedFragment { mEnrolling = false; } + protected void onAcquired(boolean isAcquiredGood) { + if (mListener != null) { + mListener.onAcquired(isAcquiredGood); + } else { + mQueuedEvents.add(new QueuedAcquired(isAcquiredGood)); + } + } + public void setListener(Listener listener) { mListener = listener; if (mListener != null) { diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java index 5b8cd680ab1..6d144abb051 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollEnrolling.java @@ -34,11 +34,13 @@ import android.content.res.Configuration; import android.content.res.Resources; 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; import android.graphics.drawable.LayerDrawable; import android.hardware.fingerprint.FingerprintManager; +import android.hardware.fingerprint.FingerprintSensorProperties; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.os.Bundle; import android.os.Process; @@ -46,15 +48,21 @@ import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.Vibrator; import android.text.TextUtils; +import android.util.DisplayUtils; +import android.util.FeatureFlagUtils; import android.util.Log; +import android.view.Display; +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.AnimationUtils; import android.view.animation.Interpolator; +import android.widget.FrameLayout; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.TextView; @@ -157,6 +165,9 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling { private boolean mCanAssumeUdfps; private boolean mCanAssumeSfps; @Nullable private ProgressBar mProgressBar; + @Nullable private 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; @@ -245,7 +256,8 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling { listenOrientationEvent(); if (mCanAssumeUdfps) { - switch(getApplicationContext().getDisplay().getRotation()) { + int rotation = getApplicationContext().getDisplay().getRotation(); + switch (rotation) { case Surface.ROTATION_90: final GlifLayout layout = (GlifLayout) getLayoutInflater().inflate( R.layout.udfps_enroll_enrolling, null, false); @@ -260,8 +272,12 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling { 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); + R.dimen.rotation_90_enroll_padding_end), 0); layoutContainer.setLayoutParams(lp); + if (FeatureFlagUtils.isEnabled(getApplicationContext(), + FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS)) { + layout.addView(addUdfpsEnrollView(props.get(0))); + } setContentView(layout, lp); break; @@ -269,7 +285,31 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling { case Surface.ROTATION_180: case Surface.ROTATION_270: default: - setContentView(R.layout.udfps_enroll_enrolling); + final GlifLayout defaultLayout = (GlifLayout) getLayoutInflater().inflate( + R.layout.udfps_enroll_enrolling, null, false); + if (FeatureFlagUtils.isEnabled(getApplicationContext(), + FeatureFlagUtils.SETTINGS_SHOW_UDFPS_ENROLL_IN_SETTINGS)) { + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) { + // In the portrait mode, set layout_container's height 0, so it's + // always shown at the bottom of the screen. + // Add udfps enroll view into layout_container instead of + // udfps_enroll_enrolling, so that when the content is too long to + // make udfps_enroll_enrolling larger than the screen, udfps enroll + // view could still be set to right position by setting bottom margin to + // its parent view (layout_container) because it's always at the + // bottom of the screen. + final FrameLayout portraitLayoutContainer = defaultLayout.findViewById( + R.id.layout_container); + final ViewGroup.LayoutParams containerLp = + portraitLayoutContainer.getLayoutParams(); + containerLp.height = 0; + portraitLayoutContainer.addView(addUdfpsEnrollView(props.get(0))); + } else if (rotation == Surface.ROTATION_270) { + defaultLayout.addView(addUdfpsEnrollView(props.get(0))); + } + } + + setContentView(defaultLayout); break; } setDescriptionText(R.string.security_settings_udfps_enroll_start_message); @@ -766,6 +806,8 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling { mErrorText.removeCallbacks(mTouchAgainRunnable); } showError(helpString); + + if (mUdfpsEnrollHelper != null) mUdfpsEnrollHelper.onEnrollmentHelp(); } } @@ -813,6 +855,13 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling { } } + @Override + public void onAcquired(boolean isAcquiredGood) { + if (mUdfpsEnrollHelper != null) { + mUdfpsEnrollHelper.onAcquired(isAcquiredGood); + } + } + private void updateProgress(boolean animate) { if (mSidecar == null || !mSidecar.isEnrolling()) { Log.d(TAG, "Enrollment not started yet"); @@ -826,6 +875,12 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling { if (mProgressBar != null && mProgressBar.getProgress() < progress) { clearError(); } + + if (mUdfpsEnrollHelper != null) { + mUdfpsEnrollHelper.onEnrollmentProgress(mSidecar.getEnrollmentSteps(), + mSidecar.getEnrollmentRemaining()); + } + if (animate) { animateProgress(progress); } else { @@ -1097,6 +1152,50 @@ public class FingerprintEnrollEnrolling extends BiometricsEnrollEnrolling { } } + private UdfpsEnrollView addUdfpsEnrollView(FingerprintSensorPropertiesInternal udfpsProps) { + UdfpsEnrollView udfpsEnrollView = (UdfpsEnrollView) getLayoutInflater().inflate( + R.layout.udfps_enroll_view, null, false); + + DisplayInfo displayInfo = new DisplayInfo(); + getDisplay().getDisplayInfo(displayInfo); + final Display.Mode maxDisplayMode = + DisplayUtils.getMaximumResolutionDisplayMode(displayInfo.supportedModes); + final float scaleFactor = android.util.DisplayUtils.getPhysicalPixelDisplaySizeRatio( + maxDisplayMode.getPhysicalWidth(), maxDisplayMode.getPhysicalHeight(), + displayInfo.getNaturalWidth(), displayInfo.getNaturalHeight()); + if (scaleFactor == Float.POSITIVE_INFINITY) { + mScaleFactor = 1f; + } else { + mScaleFactor = scaleFactor; + } + + 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 */); + + // TODO(b/260617060): Extract this logic into a 3rd party library for both Settings and + // SysUI to depend on. + UdfpsOverlayParams params = new UdfpsOverlayParams( + udfpsBounds, + overlayBounds, + displayInfo.getNaturalWidth(), + displayInfo.getNaturalHeight(), + mScaleFactor, + displayInfo.rotation, + udfpsProps.sensorType == FingerprintSensorProperties.TYPE_UDFPS_OPTICAL); + + udfpsEnrollView.setOverlayParams(params); + mUdfpsEnrollHelper = new UdfpsEnrollHelper(getApplicationContext(), mFingerprintManager); + udfpsEnrollView.setEnrollHelper(mUdfpsEnrollHelper); + + return udfpsEnrollView; + } + public static class IconTouchDialog extends InstrumentedDialogFragment { @Override diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java index b3b99753cf7..3adfc28b113 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintEnrollSidecar.java @@ -108,6 +108,11 @@ public class FingerprintEnrollSidecar extends BiometricEnrollSidecar { FingerprintEnrollSidecar.super.onEnrollmentProgress(remaining); } + @Override + public void onAcquired(boolean isAcquiredGood) { + FingerprintEnrollSidecar.super.onAcquired(isAcquiredGood); + } + @Override public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { FingerprintEnrollSidecar.super.onEnrollmentHelp(helpMsgId, helpString); diff --git a/src/com/android/settings/biometrics/fingerprint/FingerprintUpdater.java b/src/com/android/settings/biometrics/fingerprint/FingerprintUpdater.java index 66ed0856828..25689374da8 100644 --- a/src/com/android/settings/biometrics/fingerprint/FingerprintUpdater.java +++ b/src/com/android/settings/biometrics/fingerprint/FingerprintUpdater.java @@ -91,6 +91,11 @@ public class FingerprintUpdater { BiometricsSafetySource.onBiometricsChanged(mContext); // biometrics data changed } } + + @Override + public void onAcquired(boolean isAcquiredGood) { + mCallback.onAcquired(isAcquiredGood); + } } /** diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollDrawable.java b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollDrawable.java new file mode 100644 index 00000000000..480b17de8ae --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollDrawable.java @@ -0,0 +1,309 @@ +/* + * 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.biometrics.fingerprint; + + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PointF; +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 android.view.animation.AccelerateDecelerateInterpolator; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settings.R; + +/** + * UDFPS fingerprint drawable that is shown when enrolling + */ +public class UdfpsEnrollDrawable extends Drawable { + private static final String TAG = "UdfpsAnimationEnroll"; + + private static final long TARGET_ANIM_DURATION_LONG = 800L; + private static final long TARGET_ANIM_DURATION_SHORT = 600L; + // 1 + SCALE_MAX is the maximum that the moving target will animate to + private static final float SCALE_MAX = 0.25f; + private static final float DEFAULT_STROKE_WIDTH = 3f; + + @NonNull + private final Drawable mMovingTargetFpIcon; + @NonNull + private final Paint mSensorOutlinePaint; + @NonNull + private final Paint mBlueFill; + @NonNull + private final ShapeDrawable mFingerprintDrawable; + + private int mAlpha; + private boolean mSkipDraw = false; + + @Nullable + private RectF mSensorRect; + @Nullable + private UdfpsEnrollHelper mEnrollHelper; + + // Moving target animator set + @Nullable + AnimatorSet mTargetAnimatorSet; + // Moving target location + float mCurrentX; + float mCurrentY; + // Moving target size + float mCurrentScale = 1.f; + + @NonNull + private final Animator.AnimatorListener mTargetAnimListener; + + private boolean mShouldShowTipHint = false; + private boolean mShouldShowEdgeHint = false; + + private int mEnrollIcon; + private int mMovingTargetFill; + + UdfpsEnrollDrawable(@NonNull Context context, @Nullable AttributeSet attrs) { + mFingerprintDrawable = defaultFactory(context); + + loadResources(context, attrs); + mSensorOutlinePaint = new Paint(0 /* flags */); + mSensorOutlinePaint.setAntiAlias(true); + mSensorOutlinePaint.setColor(mMovingTargetFill); + mSensorOutlinePaint.setStyle(Paint.Style.FILL); + + mBlueFill = new Paint(0 /* flags */); + mBlueFill.setAntiAlias(true); + mBlueFill.setColor(mMovingTargetFill); + mBlueFill.setStyle(Paint.Style.FILL); + + mMovingTargetFpIcon = context.getResources() + .getDrawable(R.drawable.ic_enrollment_fingerprint, null); + mMovingTargetFpIcon.setTint(mEnrollIcon); + mMovingTargetFpIcon.mutate(); + + mFingerprintDrawable.setTint(mEnrollIcon); + + setAlpha(255); + mTargetAnimListener = new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + } + + @Override + public void onAnimationEnd(Animator animation) { + updateTipHintVisibility(); + } + + @Override + public void onAnimationCancel(Animator animation) { + } + + @Override + public void onAnimationRepeat(Animator animation) { + } + }; + } + + /** 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 setEnrollHelper(@NonNull UdfpsEnrollHelper helper) { + mEnrollHelper = helper; + } + + void setShouldSkipDraw(boolean skipDraw) { + if (mSkipDraw == skipDraw) { + return; + } + mSkipDraw = skipDraw; + invalidateSelf(); + } + + void updateFingerprintIconBounds(@NonNull Rect bounds) { + mFingerprintDrawable.setBounds(bounds); + invalidateSelf(); + mMovingTargetFpIcon.setBounds(bounds); + invalidateSelf(); + } + + void onEnrollmentProgress(int remaining, int totalSteps) { + if (mEnrollHelper == null) { + return; + } + + if (!mEnrollHelper.isCenterEnrollmentStage()) { + if (mTargetAnimatorSet != null && mTargetAnimatorSet.isRunning()) { + mTargetAnimatorSet.end(); + } + + final PointF point = mEnrollHelper.getNextGuidedEnrollmentPoint(); + if (mCurrentX != point.x || mCurrentY != point.y) { + final ValueAnimator x = ValueAnimator.ofFloat(mCurrentX, point.x); + x.addUpdateListener(animation -> { + mCurrentX = (float) animation.getAnimatedValue(); + invalidateSelf(); + }); + + final ValueAnimator y = ValueAnimator.ofFloat(mCurrentY, point.y); + y.addUpdateListener(animation -> { + mCurrentY = (float) animation.getAnimatedValue(); + invalidateSelf(); + }); + + final boolean isMovingToCenter = point.x == 0f && point.y == 0f; + final long duration = isMovingToCenter + ? TARGET_ANIM_DURATION_SHORT + : TARGET_ANIM_DURATION_LONG; + + final ValueAnimator scale = ValueAnimator.ofFloat(0, (float) Math.PI); + scale.setDuration(duration); + scale.addUpdateListener(animation -> { + // Grow then shrink + mCurrentScale = 1 + + SCALE_MAX * (float) Math.sin((float) animation.getAnimatedValue()); + invalidateSelf(); + }); + + mTargetAnimatorSet = new AnimatorSet(); + + mTargetAnimatorSet.setInterpolator(new AccelerateDecelerateInterpolator()); + mTargetAnimatorSet.setDuration(duration); + mTargetAnimatorSet.addListener(mTargetAnimListener); + mTargetAnimatorSet.playTogether(x, y, scale); + mTargetAnimatorSet.start(); + } else { + updateTipHintVisibility(); + } + } else { + updateTipHintVisibility(); + } + + updateEdgeHintVisibility(); + } + + @Override + public void draw(@NonNull Canvas canvas) { + if (mSkipDraw) { + return; + } + + // Draw moving target + if (mEnrollHelper != null && !mEnrollHelper.isCenterEnrollmentStage()) { + canvas.save(); + canvas.translate(mCurrentX, mCurrentY); + + if (mSensorRect != null) { + canvas.scale(mCurrentScale, mCurrentScale, + mSensorRect.centerX(), mSensorRect.centerY()); + canvas.drawOval(mSensorRect, mBlueFill); + } + + mMovingTargetFpIcon.draw(canvas); + canvas.restore(); + } else { + 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); + mBlueFill.setAlpha(alpha); + mMovingTargetFpIcon.setAlpha(alpha); + invalidateSelf(); + } + + @Override + public int getAlpha() { + return mAlpha; + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + } + + @Override + public int getOpacity() { + return 0; + } + + private void updateTipHintVisibility() { + final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isTipEnrollmentStage(); + // With the new update, we will git rid of most of this code, and instead + // we will change the fingerprint icon. + if (mShouldShowTipHint == shouldShow) { + return; + } + mShouldShowTipHint = shouldShow; + } + + private void updateEdgeHintVisibility() { + final boolean shouldShow = mEnrollHelper != null && mEnrollHelper.isEdgeEnrollmentStage(); + if (mShouldShowEdgeHint == shouldShow) { + return; + } + mShouldShowEdgeHint = shouldShow; + } + + 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); + mMovingTargetFill = ta.getColor( + R.styleable.BiometricsEnrollView_biometricsMovingTargetFill, 0); + ta.recycle(); + } + +} diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollHelper.java b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollHelper.java new file mode 100644 index 00000000000..f42b8eca2fc --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollHelper.java @@ -0,0 +1,231 @@ +/* + * 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.biometrics.fingerprint; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.PointF; +import android.hardware.fingerprint.FingerprintManager; +import android.os.Build; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Log; +import android.util.TypedValue; +import android.view.accessibility.AccessibilityManager; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helps keep track of enrollment state and animates the progress bar accordingly. + */ +public class UdfpsEnrollHelper { + private static final String TAG = "UdfpsEnrollHelper"; + + private static final String SCALE_OVERRIDE = + "com.android.systemui.biometrics.UdfpsEnrollHelper.scale"; + private static final float SCALE = 0.5f; + + private static final String NEW_COORDS_OVERRIDE = + "com.android.systemui.biometrics.UdfpsNewCoords"; + + interface Listener { + void onEnrollmentProgress(int remaining, int totalSteps); + + void onEnrollmentHelp(int remaining, int totalSteps); + + void onAcquired(boolean animateIfLastStepGood); + } + + @NonNull + private final Context mContext; + @NonNull + private final FingerprintManager mFingerprintManager; + private final boolean mAccessibilityEnabled; + @NonNull + private final List mGuidedEnrollmentPoints; + + private int mTotalSteps = -1; + private int mRemainingSteps = -1; + + // Note that this is actually not equal to "mTotalSteps - mRemainingSteps", because the + // interface makes no promises about monotonically increasing by one each time. + private int mLocationsEnrolled = 0; + + private int mCenterTouchCount = 0; + + @Nullable + UdfpsEnrollHelper.Listener mListener; + + public UdfpsEnrollHelper(@NonNull Context context, + @NonNull FingerprintManager fingerprintManager) { + + mContext = context; + mFingerprintManager = fingerprintManager; + + final AccessibilityManager am = context.getSystemService(AccessibilityManager.class); + mAccessibilityEnabled = am.isEnabled(); + + mGuidedEnrollmentPoints = new ArrayList<>(); + + // Number of pixels per mm + float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1, + context.getResources().getDisplayMetrics()); + boolean useNewCoords = Settings.Secure.getIntForUser(mContext.getContentResolver(), + NEW_COORDS_OVERRIDE, 0, + UserHandle.USER_CURRENT) != 0; + if (useNewCoords && (Build.IS_ENG || Build.IS_USERDEBUG)) { + Log.v(TAG, "Using new coordinates"); + mGuidedEnrollmentPoints.add(new PointF(-0.15f * px, -1.02f * px)); + mGuidedEnrollmentPoints.add(new PointF(-0.15f * px, 1.02f * px)); + mGuidedEnrollmentPoints.add(new PointF(0.29f * px, 0.00f * px)); + mGuidedEnrollmentPoints.add(new PointF(2.17f * px, -2.35f * px)); + mGuidedEnrollmentPoints.add(new PointF(1.07f * px, -3.96f * px)); + mGuidedEnrollmentPoints.add(new PointF(-0.37f * px, -4.31f * px)); + mGuidedEnrollmentPoints.add(new PointF(-1.69f * px, -3.29f * px)); + mGuidedEnrollmentPoints.add(new PointF(-2.48f * px, -1.23f * px)); + mGuidedEnrollmentPoints.add(new PointF(-2.48f * px, 1.23f * px)); + mGuidedEnrollmentPoints.add(new PointF(-1.69f * px, 3.29f * px)); + mGuidedEnrollmentPoints.add(new PointF(-0.37f * px, 4.31f * px)); + mGuidedEnrollmentPoints.add(new PointF(1.07f * px, 3.96f * px)); + mGuidedEnrollmentPoints.add(new PointF(2.17f * px, 2.35f * px)); + mGuidedEnrollmentPoints.add(new PointF(2.58f * px, 0.00f * px)); + } else { + Log.v(TAG, "Using old coordinates"); + mGuidedEnrollmentPoints.add(new PointF(2.00f * px, 0.00f * px)); + mGuidedEnrollmentPoints.add(new PointF(0.87f * px, -2.70f * px)); + mGuidedEnrollmentPoints.add(new PointF(-1.80f * px, -1.31f * px)); + mGuidedEnrollmentPoints.add(new PointF(-1.80f * px, 1.31f * px)); + mGuidedEnrollmentPoints.add(new PointF(0.88f * px, 2.70f * px)); + mGuidedEnrollmentPoints.add(new PointF(3.94f * px, -1.06f * px)); + mGuidedEnrollmentPoints.add(new PointF(2.90f * px, -4.14f * px)); + mGuidedEnrollmentPoints.add(new PointF(-0.52f * px, -5.95f * px)); + mGuidedEnrollmentPoints.add(new PointF(-3.33f * px, -3.33f * px)); + mGuidedEnrollmentPoints.add(new PointF(-3.99f * px, -0.35f * px)); + mGuidedEnrollmentPoints.add(new PointF(-3.62f * px, 2.54f * px)); + mGuidedEnrollmentPoints.add(new PointF(-1.49f * px, 5.57f * px)); + mGuidedEnrollmentPoints.add(new PointF(2.29f * px, 4.92f * px)); + mGuidedEnrollmentPoints.add(new PointF(3.82f * px, 1.78f * px)); + } + } + + void onEnrollmentProgress(int totalSteps, int remaining) { + if (mTotalSteps == -1) { + mTotalSteps = totalSteps; + } + + if (remaining != mRemainingSteps) { + mLocationsEnrolled++; + if (isCenterEnrollmentStage()) { + mCenterTouchCount++; + } + } + + mRemainingSteps = remaining; + + if (mListener != null && mTotalSteps != -1) { + mListener.onEnrollmentProgress(remaining, mTotalSteps); + } + } + + void onEnrollmentHelp() { + if (mListener != null && mTotalSteps != -1) { + mListener.onEnrollmentHelp(mRemainingSteps, mTotalSteps); + } + } + + void onAcquired(boolean isAcquiredGood) { + if (mListener != null && mTotalSteps != -1) { + mListener.onAcquired(isAcquiredGood && animateIfLastStep()); + } + } + + void setListener(UdfpsEnrollHelper.Listener listener) { + mListener = listener; + + // Only notify during setListener if enrollment is already in progress, so the progress + // bar can be updated. If enrollment has not started yet, the progress bar will be empty + // anyway. + if (mListener != null && mTotalSteps != -1) { + mListener.onEnrollmentProgress(mRemainingSteps, mTotalSteps); + } + } + + boolean isCenterEnrollmentStage() { + if (mTotalSteps == -1 || mRemainingSteps == -1) { + return true; + } + return mTotalSteps - mRemainingSteps < getStageThresholdSteps(mTotalSteps, 0); + } + + boolean isTipEnrollmentStage() { + if (mTotalSteps == -1 || mRemainingSteps == -1) { + return false; + } + final int progressSteps = mTotalSteps - mRemainingSteps; + return progressSteps >= getStageThresholdSteps(mTotalSteps, 1) + && progressSteps < getStageThresholdSteps(mTotalSteps, 2); + } + + boolean isEdgeEnrollmentStage() { + if (mTotalSteps == -1 || mRemainingSteps == -1) { + return false; + } + return mTotalSteps - mRemainingSteps >= getStageThresholdSteps(mTotalSteps, 2); + } + + @NonNull + PointF getNextGuidedEnrollmentPoint() { + if (mAccessibilityEnabled || !isGuidedEnrollmentStage()) { + return new PointF(0f, 0f); + } + + float scale = SCALE; + if (Build.IS_ENG || Build.IS_USERDEBUG) { + scale = Settings.Secure.getFloatForUser(mContext.getContentResolver(), + SCALE_OVERRIDE, SCALE, + UserHandle.USER_CURRENT); + } + final int index = mLocationsEnrolled - mCenterTouchCount; + final PointF originalPoint = mGuidedEnrollmentPoints + .get(index % mGuidedEnrollmentPoints.size()); + return new PointF(originalPoint.x * scale, originalPoint.y * scale); + } + + boolean animateIfLastStep() { + if (mListener == null) { + Log.e(TAG, "animateIfLastStep, null listener"); + return false; + } + + return mRemainingSteps <= 2 && mRemainingSteps >= 0; + } + + private int getStageThresholdSteps(int totalSteps, int stageIndex) { + return Math.round(totalSteps * mFingerprintManager.getEnrollStageThreshold(stageIndex)); + } + + private boolean isGuidedEnrollmentStage() { + if (mAccessibilityEnabled || mTotalSteps == -1 || mRemainingSteps == -1) { + return false; + } + final int progressSteps = mTotalSteps - mRemainingSteps; + return progressSteps >= getStageThresholdSteps(mTotalSteps, 0) + && progressSteps < getStageThresholdSteps(mTotalSteps, 1); + } +} diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollProgressBarDrawable.java b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollProgressBarDrawable.java new file mode 100644 index 00000000000..52f30f5b79c --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollProgressBarDrawable.java @@ -0,0 +1,419 @@ +/* + * Copyright (C) 2021 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.animation.ValueAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.os.Process; +import android.os.VibrationAttributes; +import android.os.VibrationEffect; +import android.os.Vibrator; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.accessibility.AccessibilityManager; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.view.animation.OvershootInterpolator; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settings.R; + +/** + * UDFPS enrollment progress bar. + */ +public class UdfpsEnrollProgressBarDrawable extends Drawable { + private static final String TAG = "UdfpsProgressBar"; + + private static final long CHECKMARK_ANIMATION_DELAY_MS = 200L; + private static final long CHECKMARK_ANIMATION_DURATION_MS = 300L; + private static final long FILL_COLOR_ANIMATION_DURATION_MS = 350L; + private static final long PROGRESS_ANIMATION_DURATION_MS = 400L; + private static final float STROKE_WIDTH_DP = 12f; + private static final Interpolator DEACCEL = new DecelerateInterpolator(); + + private static final VibrationEffect VIBRATE_EFFECT_ERROR = + VibrationEffect.createWaveform(new long[]{0, 5, 55, 60}, -1); + private static final VibrationAttributes FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES = + VibrationAttributes.createForUsage(VibrationAttributes.USAGE_ACCESSIBILITY); + + private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES = + VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK); + + private static final VibrationEffect SUCCESS_VIBRATION_EFFECT = + VibrationEffect.get(VibrationEffect.EFFECT_CLICK); + + private final float mStrokeWidthPx; + @ColorInt + private final int mProgressColor; + @ColorInt + private final int mHelpColor; + @ColorInt + private final int mOnFirstBucketFailedColor; + @NonNull + private final Drawable mCheckmarkDrawable; + @NonNull + private final Interpolator mCheckmarkInterpolator; + @NonNull + private final Paint mBackgroundPaint; + @NonNull + private final Paint mFillPaint; + @NonNull + private final Vibrator mVibrator; + @NonNull + private final boolean mIsAccessibilityEnabled; + @NonNull + private final Context mContext; + + private boolean mAfterFirstTouch; + + private int mRemainingSteps = 0; + private int mTotalSteps = 0; + private float mProgress = 0f; + @Nullable + private ValueAnimator mProgressAnimator; + @NonNull + private final ValueAnimator.AnimatorUpdateListener mProgressUpdateListener; + + private boolean mShowingHelp = false; + @Nullable + private ValueAnimator mFillColorAnimator; + @NonNull + private final ValueAnimator.AnimatorUpdateListener mFillColorUpdateListener; + + @Nullable + private ValueAnimator mBackgroundColorAnimator; + @NonNull + private final ValueAnimator.AnimatorUpdateListener mBackgroundColorUpdateListener; + + private boolean mComplete = false; + private float mCheckmarkScale = 0f; + @Nullable + private ValueAnimator mCheckmarkAnimator; + @NonNull + private final ValueAnimator.AnimatorUpdateListener mCheckmarkUpdateListener; + + private int mMovingTargetFill; + private int mMovingTargetFillError; + private int mEnrollProgress; + private int mEnrollProgressHelp; + private int mEnrollProgressHelpWithTalkback; + + public UdfpsEnrollProgressBarDrawable(@NonNull Context context, @Nullable AttributeSet attrs) { + mContext = context; + + loadResources(context, attrs); + float density = context.getResources().getDisplayMetrics().densityDpi; + mStrokeWidthPx = STROKE_WIDTH_DP * (density / DisplayMetrics.DENSITY_DEFAULT); + mProgressColor = mEnrollProgress; + final AccessibilityManager am = context.getSystemService(AccessibilityManager.class); + mIsAccessibilityEnabled = am.isTouchExplorationEnabled(); + mOnFirstBucketFailedColor = mMovingTargetFillError; + if (!mIsAccessibilityEnabled) { + mHelpColor = mEnrollProgressHelp; + } else { + mHelpColor = mEnrollProgressHelpWithTalkback; + } + mCheckmarkDrawable = context.getDrawable(R.drawable.udfps_enroll_checkmark); + mCheckmarkDrawable.mutate(); + mCheckmarkInterpolator = new OvershootInterpolator(); + + mBackgroundPaint = new Paint(); + mBackgroundPaint.setStrokeWidth(mStrokeWidthPx); + mBackgroundPaint.setColor(mMovingTargetFill); + mBackgroundPaint.setAntiAlias(true); + mBackgroundPaint.setStyle(Paint.Style.STROKE); + mBackgroundPaint.setStrokeCap(Paint.Cap.ROUND); + + // Progress fill should *not* use the extracted system color. + mFillPaint = new Paint(); + mFillPaint.setStrokeWidth(mStrokeWidthPx); + mFillPaint.setColor(mProgressColor); + mFillPaint.setAntiAlias(true); + mFillPaint.setStyle(Paint.Style.STROKE); + mFillPaint.setStrokeCap(Paint.Cap.ROUND); + + mVibrator = mContext.getSystemService(Vibrator.class); + + mProgressUpdateListener = animation -> { + mProgress = (float) animation.getAnimatedValue(); + invalidateSelf(); + }; + + mFillColorUpdateListener = animation -> { + mFillPaint.setColor((int) animation.getAnimatedValue()); + invalidateSelf(); + }; + + mCheckmarkUpdateListener = animation -> { + mCheckmarkScale = (float) animation.getAnimatedValue(); + invalidateSelf(); + }; + + mBackgroundColorUpdateListener = animation -> { + mBackgroundPaint.setColor((int) animation.getAnimatedValue()); + invalidateSelf(); + }; + } + + void onEnrollmentProgress(int remaining, int totalSteps) { + mAfterFirstTouch = true; + updateState(remaining, totalSteps, false /* showingHelp */); + } + + void onEnrollmentHelp(int remaining, int totalSteps) { + updateState(remaining, totalSteps, true /* showingHelp */); + } + + void onLastStepAcquired() { + updateState(0, mTotalSteps, false /* showingHelp */); + } + + private void updateState(int remainingSteps, int totalSteps, boolean showingHelp) { + updateProgress(remainingSteps, totalSteps, showingHelp); + updateFillColor(showingHelp); + } + + private void updateProgress(int remainingSteps, int totalSteps, boolean showingHelp) { + if (mRemainingSteps == remainingSteps && mTotalSteps == totalSteps) { + return; + } + + if (mShowingHelp) { + if (mVibrator != null && mIsAccessibilityEnabled) { + mVibrator.vibrate(Process.myUid(), mContext.getOpPackageName(), + VIBRATE_EFFECT_ERROR, getClass().getSimpleName() + "::onEnrollmentHelp", + FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES); + } + } else { + // If the first touch is an error, remainingSteps will be -1 and the callback + // doesn't come from onEnrollmentHelp. If we are in the accessibility flow, + // we still would like to vibrate. + if (mVibrator != null) { + if (remainingSteps == -1 && mIsAccessibilityEnabled) { + mVibrator.vibrate(Process.myUid(), mContext.getOpPackageName(), + VIBRATE_EFFECT_ERROR, + getClass().getSimpleName() + "::onFirstTouchError", + FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES); + } else if (remainingSteps != -1 && !mIsAccessibilityEnabled) { + mVibrator.vibrate(Process.myUid(), + mContext.getOpPackageName(), + SUCCESS_VIBRATION_EFFECT, + getClass().getSimpleName() + "::OnEnrollmentProgress", + HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES); + } + } + } + + mShowingHelp = showingHelp; + mRemainingSteps = remainingSteps; + mTotalSteps = totalSteps; + + final int progressSteps = Math.max(0, totalSteps - remainingSteps); + + // If needed, add 1 to progress and total steps to account for initial touch. + final int adjustedSteps = mAfterFirstTouch ? progressSteps + 1 : progressSteps; + final int adjustedTotal = mAfterFirstTouch ? mTotalSteps + 1 : mTotalSteps; + + final float targetProgress = Math.min(1f, (float) adjustedSteps / (float) adjustedTotal); + + if (mProgressAnimator != null && mProgressAnimator.isRunning()) { + mProgressAnimator.cancel(); + } + + mProgressAnimator = ValueAnimator.ofFloat(mProgress, targetProgress); + mProgressAnimator.setDuration(PROGRESS_ANIMATION_DURATION_MS); + mProgressAnimator.addUpdateListener(mProgressUpdateListener); + mProgressAnimator.start(); + + if (remainingSteps == 0) { + startCompletionAnimation(); + } else if (remainingSteps > 0) { + rollBackCompletionAnimation(); + } + } + + private void animateBackgroundColor() { + if (mBackgroundColorAnimator != null && mBackgroundColorAnimator.isRunning()) { + mBackgroundColorAnimator.end(); + } + mBackgroundColorAnimator = ValueAnimator.ofArgb(mBackgroundPaint.getColor(), + mOnFirstBucketFailedColor); + mBackgroundColorAnimator.setDuration(FILL_COLOR_ANIMATION_DURATION_MS); + mBackgroundColorAnimator.setRepeatCount(1); + mBackgroundColorAnimator.setRepeatMode(ValueAnimator.REVERSE); + mBackgroundColorAnimator.setInterpolator(DEACCEL); + mBackgroundColorAnimator.addUpdateListener(mBackgroundColorUpdateListener); + mBackgroundColorAnimator.start(); + } + + private void updateFillColor(boolean showingHelp) { + if (!mAfterFirstTouch && showingHelp) { + // If we are on the first touch, animate the background color + // instead of the progress color. + animateBackgroundColor(); + return; + } + + if (mFillColorAnimator != null && mFillColorAnimator.isRunning()) { + mFillColorAnimator.end(); + } + + @ColorInt final int targetColor = showingHelp ? mHelpColor : mProgressColor; + mFillColorAnimator = ValueAnimator.ofArgb(mFillPaint.getColor(), targetColor); + mFillColorAnimator.setDuration(FILL_COLOR_ANIMATION_DURATION_MS); + mFillColorAnimator.setRepeatCount(1); + mFillColorAnimator.setRepeatMode(ValueAnimator.REVERSE); + mFillColorAnimator.setInterpolator(DEACCEL); + mFillColorAnimator.addUpdateListener(mFillColorUpdateListener); + mFillColorAnimator.start(); + } + + private void startCompletionAnimation() { + if (mComplete) { + return; + } + mComplete = true; + + if (mCheckmarkAnimator != null && mCheckmarkAnimator.isRunning()) { + mCheckmarkAnimator.cancel(); + } + + mCheckmarkAnimator = ValueAnimator.ofFloat(mCheckmarkScale, 1f); + mCheckmarkAnimator.setStartDelay(CHECKMARK_ANIMATION_DELAY_MS); + mCheckmarkAnimator.setDuration(CHECKMARK_ANIMATION_DURATION_MS); + mCheckmarkAnimator.setInterpolator(mCheckmarkInterpolator); + mCheckmarkAnimator.addUpdateListener(mCheckmarkUpdateListener); + mCheckmarkAnimator.start(); + } + + private void rollBackCompletionAnimation() { + if (!mComplete) { + return; + } + mComplete = false; + + // Adjust duration based on how much of the completion animation has played. + final float animatedFraction = mCheckmarkAnimator != null + ? mCheckmarkAnimator.getAnimatedFraction() + : 0f; + final long durationMs = Math.round(CHECKMARK_ANIMATION_DELAY_MS * animatedFraction); + + if (mCheckmarkAnimator != null && mCheckmarkAnimator.isRunning()) { + mCheckmarkAnimator.cancel(); + } + + mCheckmarkAnimator = ValueAnimator.ofFloat(mCheckmarkScale, 0f); + mCheckmarkAnimator.setDuration(durationMs); + mCheckmarkAnimator.addUpdateListener(mCheckmarkUpdateListener); + mCheckmarkAnimator.start(); + } + + private void loadResources(Context context, @Nullable AttributeSet attrs) { + final TypedArray ta = context.obtainStyledAttributes(attrs, + R.styleable.BiometricsEnrollView, R.attr.biometricsEnrollStyle, + R.style.BiometricsEnrollStyle); + mMovingTargetFill = ta.getColor( + R.styleable.BiometricsEnrollView_biometricsMovingTargetFill, 0); + mMovingTargetFillError = ta.getColor( + R.styleable.BiometricsEnrollView_biometricsMovingTargetFillError, 0); + mEnrollProgress = ta.getColor( + R.styleable.BiometricsEnrollView_biometricsEnrollProgress, 0); + mEnrollProgressHelp = ta.getColor( + R.styleable.BiometricsEnrollView_biometricsEnrollProgressHelp, 0); + mEnrollProgressHelpWithTalkback = ta.getColor( + R.styleable.BiometricsEnrollView_biometricsEnrollProgressHelpWithTalkback, 0); + ta.recycle(); + } + + @Override + public void draw(@NonNull Canvas canvas) { + canvas.save(); + + // Progress starts from the top, instead of the right + canvas.rotate(-90f, getBounds().centerX(), getBounds().centerY()); + + final float halfPaddingPx = mStrokeWidthPx / 2f; + + if (mProgress < 1f) { + // Draw the background color of the progress circle. + canvas.drawArc( + halfPaddingPx, + halfPaddingPx, + getBounds().right - halfPaddingPx, + getBounds().bottom - halfPaddingPx, + 0f /* startAngle */, + 360f /* sweepAngle */, + false /* useCenter */, + mBackgroundPaint); + } + + if (mProgress > 0f) { + // Draw the filled portion of the progress circle. + canvas.drawArc( + halfPaddingPx, + halfPaddingPx, + getBounds().right - halfPaddingPx, + getBounds().bottom - halfPaddingPx, + 0f /* startAngle */, + 360f * mProgress /* sweepAngle */, + false /* useCenter */, + mFillPaint); + } + + canvas.restore(); + + if (mCheckmarkScale > 0f) { + final float offsetScale = (float) Math.sqrt(2) / 2f; + final float centerXOffset = (getBounds().width() - mStrokeWidthPx) / 2f * offsetScale; + final float centerYOffset = (getBounds().height() - mStrokeWidthPx) / 2f * offsetScale; + final float centerX = getBounds().centerX() + centerXOffset; + final float centerY = getBounds().centerY() + centerYOffset; + + final float boundsXOffset = + mCheckmarkDrawable.getIntrinsicWidth() / 2f * mCheckmarkScale; + final float boundsYOffset = + mCheckmarkDrawable.getIntrinsicHeight() / 2f * mCheckmarkScale; + + final int left = Math.round(centerX - boundsXOffset); + final int top = Math.round(centerY - boundsYOffset); + final int right = Math.round(centerX + boundsXOffset); + final int bottom = Math.round(centerY + boundsYOffset); + mCheckmarkDrawable.setBounds(left, top, right, bottom); + mCheckmarkDrawable.draw(canvas); + } + } + + @Override + public void setAlpha(int alpha) {} + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + } + + @Override + public int getOpacity() { + return 0; + } +} diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollView.java b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollView.java new file mode 100644 index 00000000000..3e3fd0e436d --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint/UdfpsEnrollView.java @@ -0,0 +1,204 @@ +/* + * 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.biometrics.fingerprint; + +import android.content.Context; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import android.util.RotationUtils; +import android.view.Gravity; +import android.view.Surface; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.settings.R; + +/** + * View corresponding with udfps_enroll_view.xml + */ +public class UdfpsEnrollView extends FrameLayout implements UdfpsEnrollHelper.Listener { + @NonNull + private final UdfpsEnrollDrawable mFingerprintDrawable; + @NonNull + private final UdfpsEnrollProgressBarDrawable mFingerprintProgressDrawable; + @NonNull + private final Handler mHandler; + + @NonNull + private ImageView mFingerprintProgressView; + + private int mProgressBarRadius; + + // sensorRect may be bigger than the sensor. True sensor dimensions are defined in + // overlayParams.sensorBounds + private Rect mSensorRect; + private UdfpsOverlayParams mOverlayParams; + + public UdfpsEnrollView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + mFingerprintDrawable = new UdfpsEnrollDrawable(mContext, attrs); + mFingerprintProgressDrawable = new UdfpsEnrollProgressBarDrawable(context, attrs); + mHandler = new Handler(Looper.getMainLooper()); + } + + @Override + protected void onFinishInflate() { + ImageView fingerprintView = findViewById(R.id.udfps_enroll_animation_fp_view); + fingerprintView.setImageDrawable(mFingerprintDrawable); + mFingerprintProgressView = findViewById(R.id.udfps_enroll_animation_fp_progress_view); + mFingerprintProgressView.setImageDrawable(mFingerprintProgressDrawable); + } + + // Implements UdfpsEnrollHelper.Listener + @Override + public void onEnrollmentProgress(int remaining, int totalSteps) { + mHandler.post(() -> { + mFingerprintProgressDrawable.onEnrollmentProgress(remaining, totalSteps); + mFingerprintDrawable.onEnrollmentProgress(remaining, totalSteps); + }); + } + + @Override + public void onEnrollmentHelp(int remaining, int totalSteps) { + mHandler.post(() -> mFingerprintProgressDrawable.onEnrollmentHelp(remaining, totalSteps)); + } + + @Override + public void onAcquired(boolean animateIfLastStepGood) { + mHandler.post(() -> { + if (animateIfLastStepGood) mFingerprintProgressDrawable.onLastStepAcquired(); + }); + } + + void setOverlayParams(UdfpsOverlayParams params) { + mOverlayParams = params; + + post(() -> { + mProgressBarRadius = + (int) (mOverlayParams.getScaleFactor() * getContext().getResources().getInteger( + R.integer.config_udfpsEnrollProgressBar)); + mSensorRect = mOverlayParams.getSensorBounds(); + + onSensorRectUpdated(); + }); + } + + void setEnrollHelper(UdfpsEnrollHelper enrollHelper) { + mFingerprintDrawable.setEnrollHelper(enrollHelper); + enrollHelper.setListener(this); + } + + private void onSensorRectUpdated() { + updateDimensions(); + updateAccessibilityViewLocation(); + + // Updates sensor rect in relation to the overlay view + mSensorRect.set(getPaddingX(), getPaddingY(), + (mOverlayParams.getSensorBounds().width() + getPaddingX()), + (mOverlayParams.getSensorBounds().height() + getPaddingY())); + mFingerprintDrawable.onSensorRectUpdated(new RectF(mSensorRect)); + } + + private void updateDimensions() { + // Original sensorBounds assume portrait mode. + Rect rotatedBounds = mOverlayParams.getSensorBounds(); + int rotation = mOverlayParams.getRotation(); + if (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) { + RotationUtils.rotateBounds( + rotatedBounds, + mOverlayParams.getNaturalDisplayWidth(), + mOverlayParams.getNaturalDisplayHeight(), + rotation + ); + } + + // Use parent view's and rotatedBound's absolute coordinates to decide the margins of + // UdfpsEnrollView, so that its center keeps consistent with sensor rect's. + ViewGroup parentView = (ViewGroup) getParent(); + int[] coords = parentView.getLocationOnScreen(); + int parentLeft = coords[0]; + int parentTop = coords[1]; + int parentRight = parentLeft + parentView.getWidth(); + int parentBottom = parentTop + parentView.getHeight(); + MarginLayoutParams marginLayoutParams = (MarginLayoutParams) getLayoutParams(); + FrameLayout.LayoutParams params = (LayoutParams) getLayoutParams(); + + switch (rotation) { + case Surface.ROTATION_0: + case Surface.ROTATION_180: + params.gravity = Gravity.RIGHT | Gravity.TOP; + marginLayoutParams.rightMargin = parentRight - rotatedBounds.right - getPaddingX(); + marginLayoutParams.topMargin = rotatedBounds.top - parentTop - getPaddingY(); + break; + case Surface.ROTATION_90: + params.gravity = Gravity.RIGHT | Gravity.BOTTOM; + marginLayoutParams.rightMargin = parentRight - rotatedBounds.right - getPaddingX(); + marginLayoutParams.bottomMargin = + parentBottom - rotatedBounds.bottom - getPaddingY(); + break; + case Surface.ROTATION_270: + params.gravity = Gravity.LEFT | Gravity.BOTTOM; + marginLayoutParams.leftMargin = rotatedBounds.left - parentLeft - getPaddingX(); + marginLayoutParams.bottomMargin = + parentBottom - rotatedBounds.bottom - getPaddingY(); + break; + } + + params.height = rotatedBounds.height() + 2 * getPaddingX(); + params.width = rotatedBounds.width() + 2 * getPaddingY(); + setLayoutParams(params); + } + + private void updateAccessibilityViewLocation() { + View fingerprintAccessibilityView = findViewById(R.id.udfps_enroll_accessibility_view); + ViewGroup.LayoutParams params = fingerprintAccessibilityView.getLayoutParams(); + params.width = mOverlayParams.getSensorBounds().width(); + params.height = mOverlayParams.getSensorBounds().height(); + fingerprintAccessibilityView.setLayoutParams(params); + fingerprintAccessibilityView.requestLayout(); + } + + private void onFingerDown() { + if (mOverlayParams.isOptical()) { + mFingerprintDrawable.setShouldSkipDraw(true); + mFingerprintDrawable.invalidateSelf(); + } + } + + private void onFingerUp() { + if (mOverlayParams.isOptical()) { + mFingerprintDrawable.setShouldSkipDraw(false); + mFingerprintDrawable.invalidateSelf(); + } + } + + private int getPaddingX() { + return mProgressBarRadius; + } + + private int getPaddingY() { + return mProgressBarRadius; + } +} diff --git a/src/com/android/settings/biometrics/fingerprint/UdfpsOverlayParams.java b/src/com/android/settings/biometrics/fingerprint/UdfpsOverlayParams.java new file mode 100644 index 00000000000..9b52ad62302 --- /dev/null +++ b/src/com/android/settings/biometrics/fingerprint/UdfpsOverlayParams.java @@ -0,0 +1,95 @@ +/* + * 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.biometrics.fingerprint; + +import android.graphics.Rect; + +import androidx.annotation.NonNull; + +/** + * Collection of parameters that define an under-display fingerprint sensor (UDFPS) overlay. + * + * [sensorBounds] coordinates of the bounding box around the sensor in natural orientation, in + * pixels, for the current resolution. + * + * [overlayBounds] coordinates of the UI overlay in natural orientation, in pixels, for the current + * resolution. + * + * [naturalDisplayWidth] width of the physical display in natural orientation, in pixels, for the + * current resolution. + * + * [naturalDisplayHeight] height of the physical display in natural orientation, in pixels, for the + * current resolution. + * + * [scaleFactor] ratio of a dimension in the current resolution to the corresponding dimension in + * the native resolution. + * + * [rotation] current rotation of the display. + */ +public final class UdfpsOverlayParams { + @NonNull + private final Rect mSensorBounds; + @NonNull + private final Rect mOverlayBounds; + private final int mNaturalDisplayWidth; + private final int mNaturalDisplayHeight; + private final float mScaleFactor; + private final int mRotation; + private final boolean mIsOptical; + + public UdfpsOverlayParams(@NonNull Rect sensorBounds, @NonNull Rect overlayBounds, + int naturalDisplayWidth, int naturalDisplayHeight, float scaleFactor, int rotation, + boolean isOptical) { + mSensorBounds = sensorBounds; + mOverlayBounds = overlayBounds; + mNaturalDisplayWidth = naturalDisplayWidth; + mNaturalDisplayHeight = naturalDisplayHeight; + mScaleFactor = scaleFactor; + mRotation = rotation; + mIsOptical = isOptical; + } + + @NonNull + public Rect getSensorBounds() { + return mSensorBounds; + } + + @NonNull + public Rect getOverlayBounds() { + return mOverlayBounds; + } + + public int getNaturalDisplayWidth() { + return mNaturalDisplayWidth; + } + + public int getNaturalDisplayHeight() { + return mNaturalDisplayHeight; + } + + public float getScaleFactor() { + return mScaleFactor; + } + + public int getRotation() { + return mRotation; + } + + public boolean isOptical() { + return mIsOptical; + } +} diff --git a/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java b/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java index f58175af520..c7229e3eabe 100644 --- a/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java +++ b/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java @@ -53,6 +53,14 @@ public class FingerprintRepository { return prop != null && prop.isAnyUdfpsType(); } + /** + * The first sensor type is Side fps sensor or not + */ + public boolean canAssumeSfps() { + FingerprintSensorPropertiesInternal prop = getFirstFingerprintSensorPropertiesInternal(); + return prop != null && prop.isAnySidefpsType(); + } + /** * Get max possible number of fingerprints for a user */ @@ -78,6 +86,7 @@ public class FingerprintRepository { @Nullable private FingerprintSensorPropertiesInternal getFirstFingerprintSensorPropertiesInternal() { + // TODO(b/264827022) use API addAuthenticatorsRegisteredCallback final List props = mFingerprintManager.getSensorPropertiesInternal(); return props.size() > 0 ? props.get(0) : null; diff --git a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java index 87b41e93080..22409c849d7 100644 --- a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java +++ b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java @@ -30,6 +30,8 @@ import com.android.settings.biometrics2.data.repository.FingerprintRepository; */ public class BiometricsRepositoryProviderImpl implements BiometricsRepositoryProvider { + private static volatile FingerprintRepository sFingerprintRepository; + /** * Get FingerprintRepository */ @@ -41,6 +43,13 @@ public class BiometricsRepositoryProviderImpl implements BiometricsRepositoryPro if (fingerprintManager == null) { return null; } - return new FingerprintRepository(fingerprintManager); + if (sFingerprintRepository == null) { + synchronized (FingerprintRepository.class) { + if (sFingerprintRepository == null) { + sFingerprintRepository = new FingerprintRepository(fingerprintManager); + } + } + } + return sFingerprintRepository; } } diff --git a/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java b/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java index a9f43561ed8..15b187d9e0a 100644 --- a/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java +++ b/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java @@ -27,12 +27,18 @@ import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory; import androidx.lifecycle.viewmodel.CreationExtras; import com.android.internal.widget.LockPatternUtils; +import com.android.settings.biometrics.fingerprint.FingerprintUpdater; import com.android.settings.biometrics2.data.repository.FingerprintRepository; import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel; import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.ChallengeGenerator; +import com.android.settings.biometrics2.ui.viewmodel.DeviceFoldedViewModel; +import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel; import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel; import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollmentViewModel; import com.android.settings.overlay.FeatureFactory; +import com.android.systemui.unfold.compat.ScreenSizeFoldProvider; /** * View model factory for biometric enrollment fragment @@ -42,7 +48,7 @@ public class BiometricsViewModelFactory implements ViewModelProvider.Factory { private static final String TAG = "BiometricsViewModelFact"; public static final CreationExtras.Key CHALLENGE_GENERATOR = - new CreationExtras.Key() {}; + new CreationExtras.Key<>() {}; @NonNull @Override @@ -59,7 +65,22 @@ public class BiometricsViewModelFactory implements ViewModelProvider.Factory { final BiometricsRepositoryProvider provider = FeatureFactory.getFactory(application) .getBiometricsRepositoryProvider(); - if (modelClass.isAssignableFrom(FingerprintEnrollIntroViewModel.class)) { + if (modelClass.isAssignableFrom(AutoCredentialViewModel.class)) { + final LockPatternUtils lockPatternUtils = + featureFactory.getSecurityFeatureProvider().getLockPatternUtils(application); + final ChallengeGenerator challengeGenerator = extras.get(CHALLENGE_GENERATOR); + if (challengeGenerator != null) { + return (T) new AutoCredentialViewModel(application, lockPatternUtils, + challengeGenerator); + } + } else if (modelClass.isAssignableFrom(DeviceFoldedViewModel.class)) { + return (T) new DeviceFoldedViewModel(new ScreenSizeFoldProvider(application), + application.getMainExecutor()); + } else if (modelClass.isAssignableFrom(DeviceRotationViewModel.class)) { + return (T) new DeviceRotationViewModel(application); + } else if (modelClass.isAssignableFrom(FingerprintEnrollFindSensorViewModel.class)) { + return (T) new FingerprintEnrollFindSensorViewModel(application); + } else if (modelClass.isAssignableFrom(FingerprintEnrollIntroViewModel.class)) { final FingerprintRepository repository = provider.getFingerprintRepository(application); if (repository != null) { return (T) new FingerprintEnrollIntroViewModel(application, repository); @@ -70,14 +91,9 @@ public class BiometricsViewModelFactory implements ViewModelProvider.Factory { return (T) new FingerprintEnrollmentViewModel(application, repository, application.getSystemService(KeyguardManager.class)); } - } else if (modelClass.isAssignableFrom(AutoCredentialViewModel.class)) { - final LockPatternUtils lockPatternUtils = - featureFactory.getSecurityFeatureProvider().getLockPatternUtils(application); - final ChallengeGenerator challengeGenerator = extras.get(CHALLENGE_GENERATOR); - if (challengeGenerator != null) { - return (T) new AutoCredentialViewModel(application, lockPatternUtils, - challengeGenerator); - } + } else if (modelClass.isAssignableFrom(FingerprintEnrollProgressViewModel.class)) { + return (T) new FingerprintEnrollProgressViewModel(application, + new FingerprintUpdater(application)); } return create(modelClass); } diff --git a/src/com/android/settings/biometrics2/ui/model/EnrollmentProgress.java b/src/com/android/settings/biometrics2/ui/model/EnrollmentProgress.java new file mode 100644 index 00000000000..c62d670283a --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/model/EnrollmentProgress.java @@ -0,0 +1,52 @@ +/* + * 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.biometrics2.ui.model; + +/** + * Biometric Enrollment progress + */ +public final class EnrollmentProgress { + + public static final int INITIAL_STEPS = -1; + public static final int INITIAL_REMAINING = 0; + + private final int mSteps; + private final int mRemaining; + + public EnrollmentProgress(int steps, int remaining) { + mSteps = steps; + mRemaining = remaining; + } + + public int getSteps() { + return mSteps; + } + + public int getRemaining() { + return mRemaining; + } + + public boolean isInitialStep() { + return mSteps == INITIAL_STEPS; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()) + + "{steps:" + mSteps + ", remaining:" + mRemaining + "}"; + } +} diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindRfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindRfpsFragment.java new file mode 100644 index 00000000000..d7537847f44 --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindRfpsFragment.java @@ -0,0 +1,209 @@ +/* + * 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.biometrics2.ui.view; + +import static android.hardware.fingerprint.FingerprintManager.ENROLL_FIND_SENSOR; +import static android.view.View.OnClickListener; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; + +import com.android.settings.R; +import com.android.settings.biometrics.fingerprint.FingerprintFindSensorAnimation; +import com.android.settings.biometrics2.ui.model.EnrollmentProgress; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel; + +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupdesign.GlifLayout; + +/** + * Fragment explaining the side fingerprint sensor location for fingerprint enrollment. + * It interacts with ProgressViewModel, and FingerprintFindSensorAnimation. + *
+ | Has                 | UDFPS | SFPS | Other (Rear FPS) |
+ |---------------------|-------|------|------------------|
+ | Primary button      | Yes   | No   | No               |
+ | Illustration Lottie | Yes   | Yes  | No               |
+ | Animation           | No    | No   | Depend on layout |
+ | Progress ViewModel  | No    | Yes  | Yes              |
+ | Orientation detect  | No    | Yes  | No               |
+ | Foldable detect     | No    | Yes  | No               |
+ 
+ */ +public class FingerprintEnrollFindRfpsFragment extends Fragment { + + private static final boolean DEBUG = false; + private static final String TAG = "FingerprintEnrollFindRfpsFragment"; + + private FingerprintEnrollFindSensorViewModel mViewModel; + private FingerprintEnrollProgressViewModel mPorgressViewModel; + + private View mView; + private GlifLayout mGlifLayout; + private FooterBarMixin mFooterBarMixin; + private final OnClickListener mOnSkipClickListener = (v) -> mViewModel.onSkipButtonClick(); + @Nullable private FingerprintFindSensorAnimation mAnimation; + + private final Observer mProgressObserver = progress -> { + if (DEBUG) { + Log.d(TAG, "mProgressObserver(" + progress + ")"); + } + if (progress != null && !progress.isInitialStep()) { + mViewModel.onStartButtonClick(); + } + }; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + final Context context = inflater.getContext(); + mView = inflater.inflate(R.layout.fingerprint_enroll_find_sensor, container, false); + mGlifLayout = mView.findViewById(R.id.setup_wizard_layout); + mFooterBarMixin = mGlifLayout.getMixin(FooterBarMixin.class); + mFooterBarMixin.setSecondaryButton( + new FooterButton.Builder(context) + .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip) + .setButtonType(FooterButton.ButtonType.SKIP) + .setTheme(R.style.SudGlifButton_Secondary) + .build() + ); + View animationView = mView.findViewById(R.id.fingerprint_sensor_location_animation); + if (animationView instanceof FingerprintFindSensorAnimation) { + mAnimation = (FingerprintFindSensorAnimation) animationView; + } + return mView; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + final Activity activity = getActivity(); + final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity, mGlifLayout); + glifLayoutHelper.setHeaderText( + R.string.security_settings_fingerprint_enroll_find_sensor_title); + glifLayoutHelper.setDescriptionText( + getText(R.string.security_settings_fingerprint_enroll_find_sensor_message)); + mFooterBarMixin.getSecondaryButton().setOnClickListener(mOnSkipClickListener); + } + + @Override + public void onStart() { + super.onStart(); + + if (DEBUG) { + Log.d(TAG, "onStart(), start looking for fingerprint, animation exist:" + + (mAnimation != null)); + } + startLookingForFingerprint(); + } + + @Override + public void onResume() { + if (mAnimation != null) { + if (DEBUG) { + Log.d(TAG, "onResume(), start animation"); + } + mAnimation.startAnimation(); + } + super.onResume(); + } + + @Override + public void onPause() { + if (mAnimation != null) { + if (DEBUG) { + Log.d(TAG, "onPause(), pause animation"); + } + mAnimation.pauseAnimation(); + } + super.onPause(); + } + + @Override + public void onStop() { + super.onStop(); + if (DEBUG) { + Log.d(TAG, "onStop(), stop looking for fingerprint, animation exist:" + + (mAnimation != null)); + } + stopLookingForFingerprint(); + } + + private void startLookingForFingerprint() { + if (mPorgressViewModel.isEnrolling()) { + Log.d(TAG, "startLookingForFingerprint(), failed because isEnrolling is true before" + + " starting"); + return; + } + + mPorgressViewModel.clearProgressLiveData(); + mPorgressViewModel.getProgressLiveData().observe(this, mProgressObserver); + final boolean startResult = mPorgressViewModel.startEnrollment(ENROLL_FIND_SENSOR); + if (!startResult) { + Log.e(TAG, "startLookingForFingerprint(), failed to start enrollment"); + } + } + + private void stopLookingForFingerprint() { + if (!mPorgressViewModel.isEnrolling()) { + Log.d(TAG, "stopLookingForFingerprint(), failed because isEnrolling is false before" + + " stopping"); + return; + } + + mPorgressViewModel.getProgressLiveData().removeObserver(mProgressObserver); + final boolean cancelResult = mPorgressViewModel.cancelEnrollment(); + if (!cancelResult) { + Log.e(TAG, "stopLookingForFingerprint(), failed to cancel enrollment"); + } + } + + @Override + public void onDestroy() { + if (mAnimation != null) { + if (DEBUG) { + Log.d(TAG, "onDestroy(), stop animation"); + } + mAnimation.stopAnimation(); + } + super.onDestroy(); + } + + @Override + public void onAttach(@NonNull Context context) { + final FragmentActivity activity = getActivity(); + final ViewModelProvider provider = new ViewModelProvider(activity); + mViewModel = provider.get(FingerprintEnrollFindSensorViewModel.class); + mPorgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class); + super.onAttach(context); + } +} diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindSfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindSfpsFragment.java new file mode 100644 index 00000000000..c363f04e1e5 --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindSfpsFragment.java @@ -0,0 +1,251 @@ +/* + * 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.biometrics2.ui.view; + +import static android.hardware.fingerprint.FingerprintManager.ENROLL_FIND_SENSOR; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.Surface; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RawRes; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModelProvider; + +import com.android.settings.R; +import com.android.settings.biometrics2.ui.model.EnrollmentProgress; +import com.android.settings.biometrics2.ui.viewmodel.DeviceFoldedViewModel; +import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel; +import com.android.settingslib.widget.LottieColorUtils; + +import com.airbnb.lottie.LottieAnimationView; +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupdesign.GlifLayout; + +/** + * Fragment explaining the side fingerprint sensor location for fingerprint enrollment. + * It interacts with ProgressViewModel, FoldCallback (for different lottie), and + * LottieAnimationView. + *
+ | Has                 | UDFPS | SFPS | Other (Rear FPS) |
+ |---------------------|-------|------|------------------|
+ | Primary button      | Yes   | No   | No               |
+ | Illustration Lottie | Yes   | Yes  | No               |
+ | Animation           | No    | No   | Depend on layout |
+ | Progress ViewModel  | No    | Yes  | Yes              |
+ | Orientation detect  | No    | Yes  | No               |
+ | Foldable detect     | No    | Yes  | No               |
+ 
+ */ +public class FingerprintEnrollFindSfpsFragment extends Fragment { + + private static final boolean DEBUG = false; + private static final String TAG = "FingerprintEnrollFindSfpsFragment"; + + private FingerprintEnrollFindSensorViewModel mViewModel; + private FingerprintEnrollProgressViewModel mProgressViewModel; + private DeviceRotationViewModel mRotationViewModel; + private DeviceFoldedViewModel mFoldedViewModel; + + private final Observer mRotationObserver = rotation -> { + if (DEBUG) { + Log.d(TAG, "rotationObserver " + rotation); + } + if (rotation == null) { + return; + } + onRotationChanged(rotation); + }; + + @Surface.Rotation private int mAnimationRotation = -1; + + private View mView; + private GlifLayout mGlifLayout; + private FooterBarMixin mFooterBarMixin; + private final OnClickListener mOnSkipClickListener = (v) -> mViewModel.onSkipButtonClick(); + private LottieAnimationView mIllustrationLottie; + + private final Observer mProgressObserver = progress -> { + if (DEBUG) { + Log.d(TAG, "mProgressObserver(" + progress + ")"); + } + if (progress != null && !progress.isInitialStep()) { + mViewModel.onStartButtonClick(); + } + }; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + final Context context = inflater.getContext(); + mView = inflater.inflate(R.layout.sfps_enroll_find_sensor_layout, container, false); + mGlifLayout = mView.findViewById(R.id.setup_wizard_layout); + mIllustrationLottie = mView.findViewById(R.id.illustration_lottie); + mFooterBarMixin = mGlifLayout.getMixin(FooterBarMixin.class); + mFooterBarMixin.setSecondaryButton( + new FooterButton.Builder(context) + .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip) + .setButtonType(FooterButton.ButtonType.SKIP) + .setTheme(R.style.SudGlifButton_Secondary) + .build() + ); + return mView; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + final Activity activity = getActivity(); + final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity, mGlifLayout); + glifLayoutHelper.setHeaderText(R.string.security_settings_sfps_enroll_find_sensor_title); + glifLayoutHelper.setDescriptionText( + getText(R.string.security_settings_sfps_enroll_find_sensor_message)); + mFooterBarMixin.getSecondaryButton().setOnClickListener(mOnSkipClickListener); + } + + @Override + public void onStart() { + super.onStart(); + + if (DEBUG) { + Log.d(TAG, "onStart(), start looking for fingerprint"); + } + startLookingForFingerprint(); + } + + @Override + public void onResume() { + super.onResume(); + final LiveData rotationLiveData = mRotationViewModel.getLiveData(); + playLottieAnimation(rotationLiveData.getValue()); + rotationLiveData.observe(this, mRotationObserver); + } + + @Override + public void onPause() { + mRotationViewModel.getLiveData().removeObserver(mRotationObserver); + super.onPause(); + } + + @Override + public void onStop() { + super.onStop(); + if (DEBUG) { + Log.d(TAG, "onStop(), stop looking for fingerprint"); + } + stopLookingForFingerprint(); + } + + private void startLookingForFingerprint() { + if (mProgressViewModel.isEnrolling()) { + Log.d(TAG, "startLookingForFingerprint(), failed because isEnrolling is true before" + + " starting"); + return; + } + + mProgressViewModel.clearProgressLiveData(); + mProgressViewModel.getProgressLiveData().observe(this, mProgressObserver); + final boolean startResult = mProgressViewModel.startEnrollment(ENROLL_FIND_SENSOR); + if (!startResult) { + Log.e(TAG, "startLookingForFingerprint(), failed to start enrollment"); + } + } + + private void stopLookingForFingerprint() { + if (!mProgressViewModel.isEnrolling()) { + Log.d(TAG, "stopLookingForFingerprint(), failed because isEnrolling is false before" + + " stopping"); + return; + } + + mProgressViewModel.getProgressLiveData().removeObserver(mProgressObserver); + final boolean cancelResult = mProgressViewModel.cancelEnrollment(); + if (!cancelResult) { + Log.e(TAG, "stopLookingForFingerprint(), failed to cancel enrollment"); + } + } + + private void onRotationChanged(@Surface.Rotation int newRotation) { + if (DEBUG) { + Log.d(TAG, "onRotationChanged() from " + mAnimationRotation + " to " + newRotation); + } + if ((newRotation + 2) % 4 == mAnimationRotation) { + playLottieAnimation(newRotation); + } + // Fragment will be re-created if it's changed between landscape and portrait, so no need to + // handle other cases. + } + + private void playLottieAnimation(@Surface.Rotation int rotation) { + @RawRes final int animationRawRes = getSfpsLottieAnimationRawRes(rotation); + if (DEBUG) { + Log.d(TAG, "play lottie animation " + animationRawRes + + ", previous rotation:" + mAnimationRotation + ", new rotation:" + rotation); + } + + mAnimationRotation = rotation; + mIllustrationLottie.setAnimation(animationRawRes); + LottieColorUtils.applyDynamicColors(getActivity(), mIllustrationLottie); + mIllustrationLottie.setVisibility(View.VISIBLE); + mIllustrationLottie.playAnimation(); + } + + @RawRes + private int getSfpsLottieAnimationRawRes(@Surface.Rotation int rotation) { + final boolean isFolded = !Boolean.FALSE.equals(mFoldedViewModel.getLiveData().getValue()); + switch (rotation) { + case Surface.ROTATION_90: + return isFolded ? R.raw.fingerprint_edu_lottie_folded_top_left + : R.raw.fingerprint_edu_lottie_portrait_top_left; + case Surface.ROTATION_180 : + return isFolded ? R.raw.fingerprint_edu_lottie_folded_bottom_left + : R.raw.fingerprint_edu_lottie_landscape_bottom_left; + case Surface.ROTATION_270 : + return isFolded ? R.raw.fingerprint_edu_lottie_folded_bottom_right + : R.raw.fingerprint_edu_lottie_portrait_bottom_right; + default : + return isFolded ? R.raw.fingerprint_edu_lottie_folded_top_right + : R.raw.fingerprint_edu_lottie_landscape_top_right; + } + } + + @Override + public void onAttach(@NonNull Context context) { + final FragmentActivity activity = getActivity(); + final ViewModelProvider provider = new ViewModelProvider(activity); + mViewModel = provider.get(FingerprintEnrollFindSensorViewModel.class); + mProgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class); + mRotationViewModel = provider.get(DeviceRotationViewModel.class); + mFoldedViewModel = provider.get(DeviceFoldedViewModel.class); + super.onAttach(context); + } +} diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindUdfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindUdfpsFragment.java new file mode 100644 index 00000000000..565f017b703 --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFindUdfpsFragment.java @@ -0,0 +1,115 @@ +/* + * 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.biometrics2.ui.view; + +import static android.view.View.OnClickListener; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; + +import com.android.settings.R; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel; + +import com.airbnb.lottie.LottieAnimationView; +import com.google.android.setupcompat.template.FooterBarMixin; +import com.google.android.setupcompat.template.FooterButton; +import com.google.android.setupdesign.GlifLayout; + +/** + * Fragment explaining the under-display fingerprint sensor location for fingerprint enrollment. + * It interacts with Primary button, and LottieAnimationView. + *
+ | Has                 | UDFPS | SFPS | Other (Rear FPS) |
+ |---------------------|-------|------|------------------|
+ | Primary button      | Yes   | No   | No               |
+ | Illustration Lottie | Yes   | Yes  | No               |
+ | Animation           | No    | No   | Depend on layout |
+ | Progress ViewModel  | No    | Yes  | Yes              |
+ | Orientation detect  | No    | Yes  | No               |
+ | Foldable detect     | No    | Yes  | No               |
+ 
+ */ +public class FingerprintEnrollFindUdfpsFragment extends Fragment { + + private FingerprintEnrollFindSensorViewModel mViewModel; + + private View mView; + private GlifLayout mGlifLayout; + private FooterBarMixin mFooterBarMixin; + private final OnClickListener mOnSkipClickListener = (v) -> mViewModel.onSkipButtonClick(); + private final OnClickListener mOnStartClickListener = (v) -> mViewModel.onStartButtonClick(); + private LottieAnimationView mIllustrationLottie; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + final Context context = inflater.getContext(); + mView = inflater.inflate(R.layout.udfps_enroll_find_sensor_layout, container, false); + mGlifLayout = mView.findViewById(R.id.setup_wizard_layout); + mIllustrationLottie = mView.findViewById(R.id.illustration_lottie); + mFooterBarMixin = mGlifLayout.getMixin(FooterBarMixin.class); + mFooterBarMixin.setSecondaryButton( + new FooterButton.Builder(context) + .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip) + .setButtonType(FooterButton.ButtonType.SKIP) + .setTheme(R.style.SudGlifButton_Secondary) + .build() + ); + mFooterBarMixin.setPrimaryButton( + new FooterButton.Builder(context) + .setText(R.string.security_settings_udfps_enroll_find_sensor_start_button) + .setButtonType(FooterButton.ButtonType.NEXT) + .setTheme(R.style.SudGlifButton_Primary) + .build() + ); + return mView; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + final Activity activity = getActivity(); + final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity, mGlifLayout); + glifLayoutHelper.setHeaderText(R.string.security_settings_udfps_enroll_find_sensor_title); + glifLayoutHelper.setDescriptionText( + getText(R.string.security_settings_udfps_enroll_find_sensor_message)); + mFooterBarMixin.getSecondaryButton().setOnClickListener(mOnSkipClickListener); + mFooterBarMixin.getPrimaryButton().setOnClickListener(mOnStartClickListener); + mIllustrationLottie.setOnClickListener(mOnStartClickListener); + + if (mViewModel.isAccessibilityEnabled()) { + mIllustrationLottie.setAnimation(R.raw.udfps_edu_a11y_lottie); + } + } + + @Override + public void onAttach(@NonNull Context context) { + mViewModel = new ViewModelProvider(getActivity()).get( + FingerprintEnrollFindSensorViewModel.class); + super.onAttach(context); + } +} diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.java index ecec36f50de..f6e06f9d938 100644 --- a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.java +++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollIntroFragment.java @@ -24,18 +24,17 @@ import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroSt import static com.google.android.setupdesign.util.DynamicColorPalette.ColorType.ACCENT; -import android.app.Activity; import android.app.admin.DevicePolicyManager; import android.content.Context; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.os.Bundle; import android.text.Html; -import android.text.TextUtils; import android.text.method.LinkMovementMethod; import android.util.Log; import android.view.LayoutInflater; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; @@ -69,6 +68,9 @@ public class FingerprintEnrollIntroFragment extends Fragment { private View mView = null; private FooterButton mPrimaryFooterButton = null; private FooterButton mSecondaryFooterButton = null; + private final OnClickListener mOnNextClickListener = (v) -> mViewModel.onNextButtonClick(); + private final OnClickListener mOnSkipOrCancelClickListener = + (v) -> mViewModel.onSkipOrCancelButtonClick(); private ImageView mIconShield = null; private TextView mFooterMessage6 = null; @Nullable private PorterDuffColorFilter mIconColorFilter; @@ -150,8 +152,8 @@ public class FingerprintEnrollIntroFragment extends Fragment { final Context context = view.getContext(); - mPrimaryFooterButton.setOnClickListener(mViewModel::onNextButtonClick); - mSecondaryFooterButton.setOnClickListener(mViewModel::onSkipOrCancelButtonClick); + mPrimaryFooterButton.setOnClickListener(mOnNextClickListener); + mSecondaryFooterButton.setOnClickListener(mOnSkipOrCancelClickListener); if (mViewModel.canAssumeUdfps()) { mFooterMessage6.setVisibility(View.VISIBLE); @@ -165,15 +167,15 @@ public class FingerprintEnrollIntroFragment extends Fragment { ? R.string.security_settings_fingerprint_enroll_introduction_cancel : R.string.security_settings_fingerprint_enroll_introduction_no_thanks); + final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(getActivity(), getLayout()); if (mViewModel.isBiometricUnlockDisabledByAdmin() && !mViewModel.isParentalConsentRequired()) { - setHeaderText( - getActivity(), + glifLayoutHelper.setHeaderText( R.string.security_settings_fingerprint_enroll_introduction_title_unlock_disabled ); - getLayout().setDescriptionText(getDescriptionDisabledByAdmin(context)); + glifLayoutHelper.setDescriptionText(getDescriptionDisabledByAdmin(context)); } else { - setHeaderText(getActivity(), + glifLayoutHelper.setHeaderText( R.string.security_settings_fingerprint_enroll_introduction_title); } observePageStatusLiveDataIfNeed(); @@ -192,7 +194,7 @@ public class FingerprintEnrollIntroFragment extends Fragment { final RequireScrollMixin requireScrollMixin = getLayout() .getMixin(RequireScrollMixin.class); requireScrollMixin.requireScrollWithButton(getActivity(), mPrimaryFooterButton, - getMoreButtonTextRes(), mViewModel::onNextButtonClick); + getMoreButtonTextRes(), mOnNextClickListener); // Always set true to setHasScrolledToBottom() before registering listener through // setOnRequireScrollStateChangedListener(), because listener will not be called if first @@ -253,21 +255,6 @@ public class FingerprintEnrollIntroFragment extends Fragment { } } - private void setHeaderText(@NonNull Activity activity, int resId) { - TextView layoutTitle = getLayout().getHeaderTextView(); - CharSequence previousTitle = layoutTitle.getText(); - CharSequence title = activity.getText(resId); - if (previousTitle != title) { - if (!TextUtils.isEmpty(previousTitle)) { - layoutTitle.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); - } - getLayout().setHeaderText(title); - getLayout().getHeaderTextView().setContentDescription(title); - activity.setTitle(title); - } - getLayout().getHeaderTextView().setContentDescription(activity.getText(resId)); - } - void updateFooterButtons(@NonNull FingerprintEnrollIntroStatus status) { @StringRes final int scrollToBottomPrimaryResId = status.getEnrollableStatus() == FINGERPRINT_ENROLLABLE_OK diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java index cd4c539b361..366b7acef18 100644 --- a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java +++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivity.java @@ -16,6 +16,7 @@ package com.android.settings.biometrics2.ui.view; +import static androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; import static androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY; import static com.android.settings.biometrics2.factory.BiometricsViewModelFactory.CHALLENGE_GENERATOR; @@ -23,24 +24,34 @@ import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewMo import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK; import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_IS_GENERATING_CHALLENGE; import static com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.CREDENTIAL_VALID; +import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel.FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG; +import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel.FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP; +import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel.FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START; +import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel.FingerprintEnrollFindSensorAction; import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL; import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH; import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL; +import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel.FingerprintEnrollIntroAction; +import android.annotation.StyleRes; import android.app.Application; import android.content.Intent; import android.content.res.ColorStateList; +import android.content.res.Resources; import android.graphics.Color; import android.os.Bundle; import android.util.Log; import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.viewmodel.CreationExtras; import androidx.lifecycle.viewmodel.MutableCreationExtras; @@ -48,14 +59,16 @@ import androidx.lifecycle.viewmodel.MutableCreationExtras; import com.android.settings.R; import com.android.settings.Utils; import com.android.settings.biometrics.BiometricEnrollBase; -import com.android.settings.biometrics.fingerprint.FingerprintEnrollFindSensor; -import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollFindSensor; +import com.android.settings.biometrics.fingerprint.FingerprintEnrollEnrolling; +import com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollEnrolling; import com.android.settings.biometrics2.data.repository.FingerprintRepository; import com.android.settings.biometrics2.factory.BiometricsViewModelFactory; import com.android.settings.biometrics2.ui.model.EnrollmentRequest; import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel; import com.android.settings.biometrics2.ui.viewmodel.AutoCredentialViewModel.FingerprintChallengeGenerator; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel; import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel; import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollmentViewModel; import com.android.settings.overlay.FeatureFactory; @@ -66,37 +79,57 @@ import com.google.android.setupdesign.util.ThemeHelper; */ public class FingerprintEnrollmentActivity extends FragmentActivity { + private static final boolean DEBUG = false; private static final String TAG = "FingerprintEnrollmentActivity"; + private static final String INTRO_TAG = "enroll-intro"; + private static final String FIND_UDFPS_TAG = "enroll-find-udfps"; + private static final String FIND_SFPS_TAG = "enroll-find-sfps"; + private static final String FIND_RFPS_TAG = "enroll-find-rfps"; + private static final String SKIP_SETUP_FIND_FPS_DIALOG_TAG = "skip-setup-dialog"; + protected static final int LAUNCH_CONFIRM_LOCK_ACTIVITY = 1; + private ViewModelProvider mViewModelProvider; private FingerprintEnrollmentViewModel mViewModel; private AutoCredentialViewModel mAutoCredentialViewModel; - private ActivityResultLauncher mNextActivityLauncher; - private ActivityResultLauncher mChooseLockLauncher; + private final Observer mIntroActionObserver = action -> { + if (DEBUG) { + Log.d(TAG, "mIntroActionObserver(" + action + ")"); + } + if (action != null) { + onIntroAction(action); + } + }; + private final Observer mFindSensorActionObserver = action -> { + if (DEBUG) { + Log.d(TAG, "mFindSensorActionObserver(" + action + ")"); + } + if (action != null) { + onFindSensorAction(action); + } + }; + private final ActivityResultCallback mNextActivityResultCallback = + result -> mViewModel.onContinueEnrollActivityResult(result, + mAutoCredentialViewModel.getUserId()); + private final ActivityResultLauncher mNextActivityLauncher = + registerForActivityResult(new StartActivityForResult(), mNextActivityResultCallback); + private final ActivityResultCallback mChooseLockResultCallback = + result -> onChooseOrConfirmLockResult(true /* isChooseLock */, result); + private final ActivityResultLauncher mChooseLockLauncher = + registerForActivityResult(new StartActivityForResult(), mChooseLockResultCallback); @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mNextActivityLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - (it) -> mViewModel.onContinueEnrollActivityResult( - it, - mAutoCredentialViewModel.getUserId()) - ); - mChooseLockLauncher = registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - (it) -> onChooseOrConfirmLockResult(true, it) - ); + mViewModelProvider = new ViewModelProvider(this); - ViewModelProvider viewModelProvider = new ViewModelProvider(this); - - mViewModel = viewModelProvider.get(FingerprintEnrollmentViewModel.class); + mViewModel = mViewModelProvider.get(FingerprintEnrollmentViewModel.class); mViewModel.setRequest(new EnrollmentRequest(getIntent(), getApplicationContext())); mViewModel.setSavedInstanceState(savedInstanceState); - mAutoCredentialViewModel = viewModelProvider.get(AutoCredentialViewModel.class); + mAutoCredentialViewModel = mViewModelProvider.get(AutoCredentialViewModel.class); mAutoCredentialViewModel.setCredentialModel(savedInstanceState, getIntent()); // Theme @@ -106,20 +139,37 @@ public class FingerprintEnrollmentActivity extends FragmentActivity { // fragment setContentView(R.layout.biometric_enrollment_container); - final FingerprintEnrollIntroViewModel introViewModel = - viewModelProvider.get(FingerprintEnrollIntroViewModel.class); - introViewModel.setEnrollmentRequest(mViewModel.getRequest()); - introViewModel.setUserId(mAutoCredentialViewModel.getUserId()); + if (DEBUG) { + Log.e(TAG, "onCreate() has savedInstance:" + (savedInstanceState != null)); + } if (savedInstanceState == null) { checkCredential(); - - final String tag = "FingerprintEnrollIntroFragment"; - getSupportFragmentManager().beginTransaction() - .setReorderingAllowed(true) - .add(R.id.fragment_container_view, FingerprintEnrollIntroFragment.class, null, - tag) - .commit(); + startIntroFragment(); + } else { + final FragmentManager manager = getSupportFragmentManager(); + String[] tags = new String[] { + FIND_UDFPS_TAG, + FIND_SFPS_TAG, + FIND_RFPS_TAG, + INTRO_TAG + }; + for (String tag: tags) { + final Fragment fragment = manager.findFragmentByTag(tag); + if (fragment == null) { + continue; + } + if (DEBUG) { + Log.e(TAG, "onCreate() currentFragment:" + tag); + } + if (tag.equals(INTRO_TAG)) { + attachIntroViewModel(); + } else { // FIND_UDFPS_TAG, FIND_SFPS_TAG, FIND_RFPS_TAG + attachFindSensorViewModel(); + attachIntroViewModel(); + } + break; + } } // observe LiveData @@ -128,11 +178,80 @@ public class FingerprintEnrollmentActivity extends FragmentActivity { mAutoCredentialViewModel.getGenerateChallengeFailedLiveData().observe(this, this::onGenerateChallengeFailed); + } + + private void startIntroFragment() { + attachIntroViewModel(); + getSupportFragmentManager().beginTransaction() + .setReorderingAllowed(true) + .replace(R.id.fragment_container_view, FingerprintEnrollIntroFragment.class, null, + INTRO_TAG) + .commit(); + } + + private void attachIntroViewModel() { + final FingerprintEnrollIntroViewModel introViewModel = + mViewModelProvider.get(FingerprintEnrollIntroViewModel.class); + + introViewModel.setEnrollmentRequest(mViewModel.getRequest()); + introViewModel.setUserId(mAutoCredentialViewModel.getUserId()); // Clear ActionLiveData in FragmentViewModel to prevent getting previous action during - // recreate, like press 'I agree' then press 'back' in FingerprintEnrollFindSensor activity. + // recreate, like press 'Agree' then press 'back' in FingerprintEnrollFindSensor activity. introViewModel.clearActionLiveData(); - introViewModel.getActionLiveData().observe(this, this::observeIntroAction); + introViewModel.getActionLiveData().observe(this, mIntroActionObserver); + } + + // We need to make sure token is valid before entering find sensor page + private void startFindSensorFragment() { + attachFindSensorViewModel(); + if (mViewModel.canAssumeUdfps()) { + // UDFPS does not need to start real fingerprint enrolling during finding sensor + startFindFpsFragmentWithProgressViewModel(FingerprintEnrollFindUdfpsFragment.class, + FIND_UDFPS_TAG, false /* initProgressViewModel */); + } else if (mViewModel.canAssumeSfps()) { + startFindFpsFragmentWithProgressViewModel(FingerprintEnrollFindSfpsFragment.class, + FIND_SFPS_TAG, true /* initProgressViewModel */); + } else { + startFindFpsFragmentWithProgressViewModel(FingerprintEnrollFindRfpsFragment.class, + FIND_RFPS_TAG, true /* initProgressViewModel */); + } + } + + private void startFindFpsFragmentWithProgressViewModel( + @NonNull Class findFpsClass, @NonNull String tag, + boolean initProgressViewModel) { + if (initProgressViewModel) { + final FingerprintEnrollProgressViewModel progressViewModel = + mViewModelProvider.get(FingerprintEnrollProgressViewModel.class); + progressViewModel.setUserId(mAutoCredentialViewModel.getUserId()); + progressViewModel.setToken(mAutoCredentialViewModel.getToken()); + } + final FingerprintEnrollFindSensorViewModel findSensorViewModel = + mViewModelProvider.get(FingerprintEnrollFindSensorViewModel.class); + findSensorViewModel.setIsSuw(mViewModel.getRequest().isSuw()); + getSupportFragmentManager().beginTransaction() + .setReorderingAllowed(true) + .setCustomAnimations(R.anim.sud_slide_next_in, R.anim.sud_slide_next_out, + R.anim.sud_slide_back_in, R.anim.sud_slide_back_out) + .replace(R.id.fragment_container_view, findFpsClass, null, tag) + .addToBackStack(tag) + .commit(); + } + + private void attachFindSensorViewModel() { + final FingerprintEnrollFindSensorViewModel findSensorViewModel = + mViewModelProvider.get(FingerprintEnrollFindSensorViewModel.class); + + // Clear ActionLiveData in FragmentViewModel to prevent getting previous action during + // recreate, like press 'Start' then press 'back' in FingerprintEnrollEnrolling activity. + findSensorViewModel.clearActionLiveData(); + findSensorViewModel.getActionLiveData().observe(this, mFindSensorActionObserver); + } + + private void startSkipSetupFindFpsDialog() { + new SkipSetupFindFpsDialog().show(getSupportFragmentManager(), + SKIP_SETUP_FIND_FPS_DIALOG_TAG); } private void onGenerateChallengeFailed(@NonNull Boolean ignoredBoolean) { @@ -217,10 +336,7 @@ public class FingerprintEnrollmentActivity extends FragmentActivity { } } - private void observeIntroAction(@Nullable Integer action) { - if (action == null) { - return; - } + private void onIntroAction(@FingerprintEnrollIntroAction int action) { switch (action) { case FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH: { onSetActivityResult( @@ -233,13 +349,30 @@ public class FingerprintEnrollmentActivity extends FragmentActivity { return; } case FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL: { + startFindSensorFragment(); + } + } + } + + private void onFindSensorAction(@FingerprintEnrollFindSensorAction int action) { + switch (action) { + case FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP: { + onSetActivityResult( + new ActivityResult(BiometricEnrollBase.RESULT_SKIP, null)); + return; + } + case FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG: { + startSkipSetupFindFpsDialog(); + return; + } + case FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START: { final boolean isSuw = mViewModel.getRequest().isSuw(); if (!mViewModel.isWaitingActivityResult().compareAndSet(false, true)) { Log.w(TAG, "startNext, isSuw:" + isSuw + ", fail to set isWaiting flag"); } - final Intent intent = new Intent(this, isSuw - ? SetupFingerprintEnrollFindSensor.class - : FingerprintEnrollFindSensor.class); + Intent intent = new Intent(this, isSuw + ? SetupFingerprintEnrollEnrolling.class + : FingerprintEnrollEnrolling.class); intent.putExtras(mAutoCredentialViewModel.createCredentialIntentExtra()); intent.putExtras(mViewModel.getNextActivityBaseIntentExtras()); mNextActivityLauncher.launch(intent); @@ -253,6 +386,12 @@ public class FingerprintEnrollmentActivity extends FragmentActivity { mViewModel.checkFinishActivityDuringOnPause(isFinishing(), isChangingConfigurations()); } + @Override + protected void onApplyThemeResource(Resources.Theme theme, @StyleRes int resid, boolean first) { + theme.applyStyle(R.style.SetupWizardPartnerResource, true); + super.onApplyThemeResource(theme, resid, first); + } + @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { if (requestCode == LAUNCH_CONFIRM_LOCK_ACTIVITY) { diff --git a/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java b/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java new file mode 100644 index 00000000000..a1645d2f7ab --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java @@ -0,0 +1,70 @@ +/* + * 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.biometrics2.ui.view; + +import android.app.Activity; +import android.text.TextUtils; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +import com.google.android.setupdesign.GlifLayout; + +/** + * Utils class for GlifLayout + */ +public class GlifLayoutHelper { + + @NonNull private final Activity mActivity; + @NonNull private final GlifLayout mGlifLayout; + + public GlifLayoutHelper(@NonNull Activity activity, @NonNull GlifLayout glifLayout) { + mActivity = activity; + mGlifLayout = glifLayout; + } + + /** + * Sets header text to GlifLayout + */ + public void setHeaderText(@StringRes int textResId) { + TextView layoutTitle = mGlifLayout.getHeaderTextView(); + CharSequence previousTitle = layoutTitle.getText(); + CharSequence title = mActivity.getText(textResId); + if (previousTitle != title) { + if (!TextUtils.isEmpty(previousTitle)) { + layoutTitle.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE); + } + mGlifLayout.setHeaderText(title); + mGlifLayout.getHeaderTextView().setContentDescription(title); + mActivity.setTitle(title); + } + mGlifLayout.getHeaderTextView().setContentDescription(title); + } + + /** + * Sets description text to GlifLayout + */ + public void setDescriptionText(CharSequence description) { + CharSequence previousDescription = mGlifLayout.getDescriptionText(); + // Prevent a11y for re-reading the same string + if (!TextUtils.equals(previousDescription, description)) { + mGlifLayout.setDescriptionText(description); + } + } +} diff --git a/src/com/android/settings/biometrics2/ui/view/SkipSetupFindFpsDialog.java b/src/com/android/settings/biometrics2/ui/view/SkipSetupFindFpsDialog.java new file mode 100644 index 00000000000..b011267c1b2 --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/view/SkipSetupFindFpsDialog.java @@ -0,0 +1,71 @@ +/* + * 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.biometrics2.ui.view; + +import static android.content.DialogInterface.OnClickListener; + +import android.app.Dialog; +import android.app.settings.SettingsEnums; +import android.content.Context; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.lifecycle.ViewModelProvider; + +import com.android.settings.R; +import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel; +import com.android.settings.core.instrumentation.InstrumentedDialogFragment; + +/** + * Skip dialog which shows when user clicks "Do it later" button in FingerprintFindSensor page. + */ +public class SkipSetupFindFpsDialog extends InstrumentedDialogFragment { + + private FingerprintEnrollFindSensorViewModel mViewModel; + private final OnClickListener mOnSkipClickListener = + (d, w) -> mViewModel.onSkipDialogButtonClick(); + + @Override + public int getMetricsCategory() { + return SettingsEnums.DIALOG_FINGERPRINT_SKIP_SETUP; + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return onCreateDialogBuilder().create(); + } + + /** + * Returns builder for this dialog + */ + @NonNull + private AlertDialog.Builder onCreateDialogBuilder() { + return new AlertDialog.Builder(getActivity(), R.style.Theme_AlertDialog) + .setTitle(R.string.setup_fingerprint_enroll_skip_title) + .setPositiveButton(R.string.skip_anyway_button_label, mOnSkipClickListener) + .setNegativeButton(R.string.go_back_button_label, null) + .setMessage(R.string.setup_fingerprint_enroll_skip_after_adding_lock_text); + } + + @Override + public void onAttach(Context context) { + mViewModel = new ViewModelProvider(getActivity()).get( + FingerprintEnrollFindSensorViewModel.class); + super.onAttach(context); + } +} diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java index a4f94b5771f..aa20f23dab9 100644 --- a/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java +++ b/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModel.java @@ -253,7 +253,11 @@ public class AutoCredentialViewModel extends AndroidViewModel { if (isUnspecifiedPassword()) { return CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK; } else if (mCredentialModel.isValidGkPwHandle()) { - generateChallenge(mCredentialModel.getGkPwHandle()); + final long gkPwHandle = mCredentialModel.getGkPwHandle(); + mCredentialModel.clearGkPwHandle(); + // GkPwHandle is got through caller activity, we shall not revoke it after + // generateChallenge(). Let caller activity to make decision. + generateChallenge(gkPwHandle, false /* revokeGkPwHandle */); mIsGeneratingChallengeDuringCheckingCredential = true; return CREDENTIAL_IS_GENERATING_CHALLENGE; } else { @@ -261,7 +265,7 @@ public class AutoCredentialViewModel extends AndroidViewModel { } } - private void generateChallenge(long gkPwHandle) { + private void generateChallenge(long gkPwHandle, boolean revokeGkPwHandle) { mChallengeGenerator.setCallback((sensorId, userId, challenge) -> { try { final byte[] newToken = requestGatekeeperHat(gkPwHandle, challenge, userId); @@ -274,11 +278,13 @@ public class AutoCredentialViewModel extends AndroidViewModel { return; } - mLockPatternUtils.removeGatekeeperPasswordHandle(gkPwHandle); - mCredentialModel.clearGkPwHandle(); + if (revokeGkPwHandle) { + mLockPatternUtils.removeGatekeeperPasswordHandle(gkPwHandle); + } if (DEBUG) { - Log.d(TAG, "generateChallenge " + mCredentialModel); + Log.d(TAG, "generateChallenge(), model:" + mCredentialModel + + ", revokeGkPwHandle:" + revokeGkPwHandle); } // Check credential again @@ -314,7 +320,9 @@ public class AutoCredentialViewModel extends AndroidViewModel { if (data != null) { final long gkPwHandle = result.getData().getLongExtra( EXTRA_KEY_GK_PW_HANDLE, INVALID_GK_PW_HANDLE); - generateChallenge(gkPwHandle); + // Revoke self requested GkPwHandle because it shall only used once inside this + // activity lifecycle. + generateChallenge(gkPwHandle, true /* revokeGkPwHandle */); return true; } } @@ -328,6 +336,14 @@ public class AutoCredentialViewModel extends AndroidViewModel { return mCredentialModel.getUserId(); } + /** + * Get userId for this credential + */ + @Nullable + public byte[] getToken() { + return mCredentialModel.getToken(); + } + @Nullable private byte[] requestGatekeeperHat(long gkPwHandle, long challenge, int userId) throws IllegalStateException { diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/DeviceFoldedViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/DeviceFoldedViewModel.java new file mode 100644 index 00000000000..6ea60b149a1 --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/viewmodel/DeviceFoldedViewModel.java @@ -0,0 +1,66 @@ +/* + * 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.biometrics2.ui.viewmodel; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +import com.android.systemui.unfold.compat.ScreenSizeFoldProvider; +import com.android.systemui.unfold.updates.FoldProvider; + +import java.util.concurrent.Executor; + +/** + * ViewModel explaining the fingerprint sensor location for fingerprint enrollment. + */ +public class DeviceFoldedViewModel extends ViewModel { + + private static final String TAG = "DeviceFoldedViewModel"; + + @NonNull private final MutableLiveData mLiveData = + new MutableLiveData<>(null); + + private final ScreenSizeFoldProvider mScreenSizeFoldProvider; + private final FoldProvider.FoldCallback mIsFoldedCallback = isFolded -> { + Log.d(TAG, "onFoldUpdated= " + isFolded); + mLiveData.postValue(isFolded); + }; + + public DeviceFoldedViewModel(@NonNull ScreenSizeFoldProvider screenSizeFoldProvider, + @NonNull Executor executor) { + super(); + mScreenSizeFoldProvider = screenSizeFoldProvider; + mScreenSizeFoldProvider.registerCallback(mIsFoldedCallback, executor); + } + + /** + * Returns FoldedLiveData + */ + public LiveData getLiveData() { + return mLiveData; + } + + @Override + protected void onCleared() { + mScreenSizeFoldProvider.unregisterCallback(mIsFoldedCallback); + super.onCleared(); + } +} diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/DeviceRotationViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/DeviceRotationViewModel.java new file mode 100644 index 00000000000..c9135fb6f0b --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/viewmodel/DeviceRotationViewModel.java @@ -0,0 +1,92 @@ +/* + * 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.biometrics2.ui.viewmodel; + +import static android.hardware.display.DisplayManager.DisplayListener; + +import android.app.Application; +import android.hardware.display.DisplayManager; +import android.util.Log; +import android.view.DisplayInfo; +import android.view.Surface; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +/** + * ViewModel explaining the fingerprint sensor location for fingerprint enrollment. + */ +public class DeviceRotationViewModel extends AndroidViewModel { + + private static final boolean DEBUG = false; + private static final String TAG = "RotationViewModel"; + + private final DisplayManager mDisplayManager; + @NonNull private final DisplayInfo mDisplayInfo = new DisplayInfo(); + private final DisplayListener mDisplayListener = new DisplayListener() { + @Override + public void onDisplayAdded(int displayId) { + } + + @Override + public void onDisplayRemoved(int displayId) { + } + + @Override + public void onDisplayChanged(int displayId) { + final int rotation = getRotation(); + if (DEBUG) { + Log.d(TAG, "onDisplayChanged(" + displayId + "), rotation:" + rotation); + } + mLiveData.postValue(rotation); + } + }; + + @NonNull private final MutableLiveData mLiveData = + new MutableLiveData<>(getRotation()); + + public DeviceRotationViewModel(@NonNull Application application) { + super(application); + mDisplayManager = application.getSystemService(DisplayManager.class); + mDisplayManager.registerDisplayListener(mDisplayListener, + application.getMainThreadHandler()); + } + + /** + * Returns current rotation + */ + @Surface.Rotation + private int getRotation() { + getApplication().getDisplay().getDisplayInfo(mDisplayInfo); + return mDisplayInfo.rotation; + } + + /** + * Returns RotationLiveData + */ + public LiveData getLiveData() { + return mLiveData; + } + + @Override + protected void onCleared() { + mDisplayManager.unregisterDisplayListener(mDisplayListener); + super.onCleared(); + } +} diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollFindSensorViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollFindSensorViewModel.java new file mode 100644 index 00000000000..a871fe120a8 --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollFindSensorViewModel.java @@ -0,0 +1,135 @@ +/* + * 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.biometrics2.ui.viewmodel; + +import android.annotation.IntDef; +import android.app.Application; +import android.util.Log; +import android.view.accessibility.AccessibilityManager; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * ViewModel explaining the fingerprint sensor location for fingerprint enrollment. + */ +public class FingerprintEnrollFindSensorViewModel extends AndroidViewModel { + + private static final boolean DEBUG = false; + private static final String TAG = "FingerprintEnrollFindSensorViewModel"; + + /** + * User clicks 'Skip' button on this page in Settings + */ + public static final int FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP = 0; + + /** + * User clicks 'Skip' button on this page in SetupWizard flow + */ + public static final int FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG = 1; + + /** + * User clicks 'Start' button on this page + */ + public static final int FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START = 2; + + @IntDef(prefix = { "FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_" }, value = { + FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP, + FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG, + FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START + }) + @Retention(RetentionPolicy.SOURCE) + public @interface FingerprintEnrollFindSensorAction {} + + private final AccessibilityManager mAccessibilityManager; + + private boolean mIsSuw = false; + @NonNull private final MutableLiveData mActionLiveData = new MutableLiveData<>(); + + public FingerprintEnrollFindSensorViewModel(@NonNull Application application) { + super(application); + mAccessibilityManager = application.getSystemService(AccessibilityManager.class); + } + + /** + * Sets isSetupWizard or not + */ + public void setIsSuw(boolean isSuw) { + mIsSuw = isSuw; + } + + /** + * Returns action live data that user chooses + */ + public LiveData getActionLiveData() { + return mActionLiveData; + } + + /** + * Clear ActionLiveData to prevent get obsolete data + */ + public void clearActionLiveData() { + mActionLiveData.setValue(null); + } + + /** + * User clicks skip button on dialog + */ + public void onSkipDialogButtonClick() { + final int action = FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP; + if (DEBUG) { + Log.d(TAG, "onSkipDialogButtonClick, post " + action); + } + mActionLiveData.postValue(action); + } + + /** + * User clicks skip button + */ + public void onSkipButtonClick() { + final int action = mIsSuw + ? FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_DIALOG + : FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_SKIP; + if (DEBUG) { + Log.d(TAG, "onSkipButtonClick, post action " + action); + } + mActionLiveData.postValue(action); + } + + /** + * User clicks start button + */ + public void onStartButtonClick() { + final int action = FINGERPRINT_ENROLL_FIND_SENSOR_ACTION_START; + if (DEBUG) { + Log.d(TAG, "onStartButtonClick, post action " + action); + } + mActionLiveData.postValue(action); + } + + /** + * Returns the info about accessibility is enabled or not + */ + public boolean isAccessibilityEnabled() { + return mAccessibilityManager.isEnabled(); + } +} diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java index 72611a7c791..228ea19a8d4 100644 --- a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java +++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModel.java @@ -24,7 +24,6 @@ import android.annotation.IntDef; import android.app.Application; import android.os.UserHandle; import android.util.Log; -import android.view.View; import androidx.annotation.NonNull; import androidx.lifecycle.AndroidViewModel; @@ -189,7 +188,7 @@ public class FingerprintEnrollIntroViewModel extends AndroidViewModel /** * User clicks next button */ - public void onNextButtonClick(View ignoredView) { + public void onNextButtonClick() { final Integer status = mEnrollableStatusLiveData.getValue(); switch (status != null ? status : ENROLLABLE_STATUS_DEFAULT) { case FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX: @@ -206,7 +205,7 @@ public class FingerprintEnrollIntroViewModel extends AndroidViewModel /** * User clicks skip/cancel button */ - public void onSkipOrCancelButtonClick(View ignoredView) { + public void onSkipOrCancelButtonClick() { mActionLiveData.postValue(FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL); } diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java new file mode 100644 index 00000000000..cec94751f73 --- /dev/null +++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java @@ -0,0 +1,231 @@ +/* + * 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.biometrics2.ui.viewmodel; + +import static com.android.settings.biometrics2.ui.model.EnrollmentProgress.INITIAL_REMAINING; +import static com.android.settings.biometrics2.ui.model.EnrollmentProgress.INITIAL_STEPS; + +import android.app.Application; +import android.content.res.Resources; +import android.hardware.fingerprint.FingerprintManager.EnrollReason; +import android.hardware.fingerprint.FingerprintManager.EnrollmentCallback; +import android.os.CancellationSignal; +import android.os.SystemClock; +import android.os.UserHandle; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.android.settings.R; +import com.android.settings.biometrics.fingerprint.FingerprintUpdater; +import com.android.settings.biometrics.fingerprint.MessageDisplayController; +import com.android.settings.biometrics2.ui.model.EnrollmentProgress; + +/** + * Progress ViewModel handles the state around biometric enrollment. It manages the state of + * enrollment throughout the activity lifecycle so the app can continue after an event like + * rotation. + */ +public class FingerprintEnrollProgressViewModel extends AndroidViewModel { + + private static final boolean DEBUG = false; + private static final String TAG = "FingerprintEnrollProgressViewModel"; + + private final MutableLiveData mProgressLiveData = new MutableLiveData<>( + new EnrollmentProgress(INITIAL_STEPS, INITIAL_REMAINING)); + + private byte[] mToken = null; + private int mUserId = UserHandle.myUserId(); + + private final FingerprintUpdater mFingerprintUpdater; + private final MessageDisplayController mMessageDisplayController; + private EnrollmentHelper mEnrollmentHelper; + private final EnrollmentCallback mEnrollmentCallback = new EnrollmentCallback() { + + @Override + public void onEnrollmentProgress(int remaining) { + final int currentSteps = getSteps(); + final EnrollmentProgress progress = new EnrollmentProgress( + currentSteps == INITIAL_STEPS ? remaining : getSteps(), remaining); + if (DEBUG) { + Log.d(TAG, "onEnrollmentProgress(" + remaining + "), steps: " + currentSteps + + ", post progress as " + progress); + } + mProgressLiveData.postValue(progress); + // TODO set enrolling to false when remaining is 0 during implementing b/260957933 + } + + @Override + public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { + // TODO add LiveData for help message during implementing b/260957933 + } + + @Override + public void onEnrollmentError(int errMsgId, CharSequence errString) { + // TODO add LiveData for error message during implementing b/260957933 + } + }; + + public FingerprintEnrollProgressViewModel(@NonNull Application application, + @NonNull FingerprintUpdater fingerprintUpdater) { + super(application); + mFingerprintUpdater = fingerprintUpdater; + final Resources res = application.getResources(); + mMessageDisplayController = + res.getBoolean(R.bool.enrollment_message_display_controller_flag) + ? new MessageDisplayController( + application.getMainThreadHandler(), + mEnrollmentCallback, + SystemClock.elapsedRealtimeClock(), + res.getInteger(R.integer.enrollment_help_minimum_time_display), + res.getInteger(R.integer.enrollment_progress_minimum_time_display), + res.getBoolean(R.bool.enrollment_progress_priority_over_help), + res.getBoolean(R.bool.enrollment_prioritize_acquire_messages), + res.getInteger(R.integer.enrollment_collect_time)) : null; + } + + public void setToken(byte[] token) { + mToken = token; + } + + public void setUserId(int userId) { + mUserId = userId; + } + + /** + * clear progress + */ + public void clearProgressLiveData() { + mProgressLiveData.setValue(new EnrollmentProgress(INITIAL_STEPS, INITIAL_REMAINING)); + } + + public LiveData getProgressLiveData() { + return mProgressLiveData; + } + + /** + * Starts enrollment and return latest isEnrolling() result + */ + public boolean startEnrollment(@EnrollReason int reason) { + if (mToken == null) { + Log.e(TAG, "Null hardware auth token for enroll"); + return false; + } + if (isEnrolling()) { + Log.w(TAG, "Enrolling has started, shall not start again"); + return true; + } + + mEnrollmentHelper = new EnrollmentHelper( + mMessageDisplayController != null + ? mMessageDisplayController + : mEnrollmentCallback); + mEnrollmentHelper.startEnrollment(mFingerprintUpdater, mToken, mUserId, reason); + return true; + } + + /** + * Cancels enrollment and return latest isEnrolling result + */ + public boolean cancelEnrollment() { + if (!isEnrolling() || mEnrollmentHelper == null) { + Log.e(TAG, "Fail to cancel enrollment, enrollmentController exist:" + + (mEnrollmentHelper != null)); + return false; + } + + mEnrollmentHelper.cancelEnrollment(); + mEnrollmentHelper = null; + return true; + } + + public boolean isEnrolling() { + return (mEnrollmentHelper != null); + } + + private int getSteps() { + return mProgressLiveData.getValue().getSteps(); + } + + /** + * This class is used to stop latest message from onEnrollmentError() after user cancelled + * enrollment. This class will not forward message anymore after mCancellationSignal is sent. + */ + private static class EnrollmentHelper extends EnrollmentCallback { + + @NonNull private final EnrollmentCallback mEnrollmentCallback; + @Nullable private CancellationSignal mCancellationSignal = new CancellationSignal(); + + EnrollmentHelper(@NonNull EnrollmentCallback enrollmentCallback) { + mEnrollmentCallback = enrollmentCallback; + } + + @Override + public void onEnrollmentError(int errMsgId, CharSequence errString) { + if (mCancellationSignal == null) { + return; + } + mEnrollmentCallback.onEnrollmentError(errMsgId, errString); + } + + @Override + public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) { + if (mCancellationSignal == null) { + return; + } + mEnrollmentCallback.onEnrollmentHelp(helpMsgId, helpString); + } + + @Override + public void onEnrollmentProgress(int remaining) { + if (mCancellationSignal == null) { + return; + } + mEnrollmentCallback.onEnrollmentProgress(remaining); + } + + /** + * Starts enrollment + */ + public boolean startEnrollment(@NonNull FingerprintUpdater fingerprintUpdater, + @NonNull byte[] token, int userId, @EnrollReason int reason) { + if (mCancellationSignal == null) { + // Not allow enrolling twice as same instance. Allocate a new instance for second + // enrollment. + return false; + } + fingerprintUpdater.enroll(token, mCancellationSignal, userId, this, reason); + return true; + } + + /** + * Cancels current enrollment + */ + public void cancelEnrollment() { + final CancellationSignal cancellationSignal = mCancellationSignal; + mCancellationSignal = null; + + if (cancellationSignal != null) { + cancellationSignal.cancel(); + } + } + } +} diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.java index 4b862731da5..30ed2032edd 100644 --- a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.java +++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModel.java @@ -124,8 +124,6 @@ public class FingerprintEnrollmentViewModel extends AndroidViewModel implements } } - - private boolean isKeyguardSecure() { return mKeyguardManager != null && mKeyguardManager.isKeyguardSecure(); } @@ -182,4 +180,18 @@ public class FingerprintEnrollmentViewModel extends AndroidViewModel implements public void onSaveInstanceState(@NonNull Bundle outState) { outState.putBoolean(SAVED_STATE_IS_WAITING_ACTIVITY_RESULT, mIsWaitingActivityResult.get()); } + + /** + * The first sensor type is UDFPS sensor or not + */ + public boolean canAssumeUdfps() { + return mFingerprintRepository.canAssumeUdfps(); + } + + /** + * The first sensor type is side fps sensor or not + */ + public boolean canAssumeSfps() { + return mFingerprintRepository.canAssumeSfps(); + } } diff --git a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsController.java b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsController.java index 585a5f9a2d0..6425814a9e3 100644 --- a/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsController.java +++ b/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsController.java @@ -19,6 +19,7 @@ package com.android.settings.bluetooth; import static com.android.settings.bluetooth.BluetoothDeviceDetailsFragment.FEATURE_HEARING_DEVICE_CONTROLS_ORDER; import android.content.Context; +import android.text.TextUtils; import android.util.FeatureFlagUtils; import androidx.preference.Preference; @@ -27,16 +28,22 @@ import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceScreen; import com.android.settings.R; +import com.android.settings.accessibility.AccessibilityHearingAidsFragment; +import com.android.settings.core.SubSettingLauncher; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.core.lifecycle.Lifecycle; +import com.google.common.annotations.VisibleForTesting; + /** * The controller of the hearing device controls in the bluetooth detail settings. */ -public class BluetoothDetailsHearingDeviceControlsController extends BluetoothDetailsController { +public class BluetoothDetailsHearingDeviceControlsController extends BluetoothDetailsController + implements Preference.OnPreferenceClickListener { private static final String KEY_FEATURE_CONTROLS_GROUP = "feature_controls_group"; - private static final String KEY_HEARING_DEVICE_CONTROLS = "hearing_device_controls"; + @VisibleForTesting + static final String KEY_HEARING_DEVICE_CONTROLS = "hearing_device_controls"; public BluetoothDetailsHearingDeviceControlsController(Context context, PreferenceFragmentCompat fragment, CachedBluetoothDevice device, Lifecycle lifecycle) { @@ -70,12 +77,30 @@ public class BluetoothDetailsHearingDeviceControlsController extends BluetoothDe return KEY_FEATURE_CONTROLS_GROUP; } + @Override + public boolean onPreferenceClick(Preference preference) { + if (TextUtils.equals(preference.getKey(), KEY_HEARING_DEVICE_CONTROLS)) { + launchAccessibilityHearingDeviceSettings(); + return true; + } + return false; + } + private Preference createHearingDeviceControlsPreference(Context context) { final Preference preference = new Preference(context); preference.setKey(KEY_HEARING_DEVICE_CONTROLS); preference.setTitle(context.getString(R.string.bluetooth_device_controls_title)); preference.setSummary(context.getString(R.string.bluetooth_device_controls_summary)); + preference.setOnPreferenceClickListener(this); return preference; } + + private void launchAccessibilityHearingDeviceSettings() { + new SubSettingLauncher(mContext) + .setDestination(AccessibilityHearingAidsFragment.class.getName()) + .setSourceMetricsCategory( + ((BluetoothDeviceDetailsFragment) mFragment).getMetricsCategory()) + .launch(); + } } diff --git a/src/com/android/settings/communal/CommunalPreferenceController.java b/src/com/android/settings/communal/CommunalPreferenceController.java index e16dcc3435a..5cfedc45de6 100644 --- a/src/com/android/settings/communal/CommunalPreferenceController.java +++ b/src/com/android/settings/communal/CommunalPreferenceController.java @@ -17,6 +17,7 @@ package com.android.settings.communal; import android.content.Context; +import android.os.UserManager; import com.android.settings.R; import com.android.settings.core.BasePreferenceController; @@ -31,7 +32,14 @@ public class CommunalPreferenceController extends BasePreferenceController { @Override public int getAvailabilityStatus() { - return mContext.getResources().getBoolean(R.bool.config_show_communal_settings) + // TODO(b/257333623): Allow the communal user to be non-SystemUser user in HSUM. + return (mContext.getResources().getBoolean(R.bool.config_show_communal_settings) + && isSystemUser()) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } + + private boolean isSystemUser() { + final UserManager userManager = mContext.getSystemService(UserManager.class); + return userManager != null && userManager.isSystemUser(); + } } diff --git a/src/com/android/settings/connecteddevice/stylus/StylusDevicesController.java b/src/com/android/settings/connecteddevice/stylus/StylusDevicesController.java index 7ed02491ca3..e04251a218e 100644 --- a/src/com/android/settings/connecteddevice/stylus/StylusDevicesController.java +++ b/src/com/android/settings/connecteddevice/stylus/StylusDevicesController.java @@ -85,16 +85,24 @@ public class StylusDevicesController extends AbstractPreferenceController implem } @Nullable - private Preference createDefaultNotesPreference() { + private Preference createOrUpdateDefaultNotesPreference(@Nullable Preference preference) { RoleManager rm = mContext.getSystemService(RoleManager.class); - if (rm == null) { + if (rm == null || !rm.isRoleAvailable(RoleManager.ROLE_NOTES)) { return null; } + Preference pref = preference == null ? new Preference(mContext) : preference; + pref.setKey(KEY_DEFAULT_NOTES); + pref.setTitle(mContext.getString(R.string.stylus_default_notes_app)); + pref.setIcon(R.drawable.ic_article); + pref.setOnPreferenceClickListener(this); + pref.setEnabled(true); + List roleHolders = rm.getRoleHoldersAsUser(RoleManager.ROLE_NOTES, mContext.getUser()); if (roleHolders.isEmpty()) { - return null; + pref.setSummary(R.string.default_app_none); + return pref; } String packageName = roleHolders.get(0); @@ -107,13 +115,6 @@ public class StylusDevicesController extends AbstractPreferenceController implem } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Notes role package not found."); } - - Preference pref = new Preference(mContext); - pref.setKey(KEY_DEFAULT_NOTES); - pref.setTitle(mContext.getString(R.string.stylus_default_notes_app)); - pref.setIcon(R.drawable.ic_article); - pref.setOnPreferenceClickListener(this); - pref.setEnabled(true); pref.setSummary(appName); return pref; } @@ -187,12 +188,10 @@ public class StylusDevicesController extends AbstractPreferenceController implem private void refresh() { if (!isAvailable()) return; - Preference notesPref = mPreferencesContainer.findPreference(KEY_DEFAULT_NOTES); - if (notesPref == null) { - notesPref = createDefaultNotesPreference(); - if (notesPref != null) { - mPreferencesContainer.addPreference(notesPref); - } + Preference currNotesPref = mPreferencesContainer.findPreference(KEY_DEFAULT_NOTES); + Preference notesPref = createOrUpdateDefaultNotesPreference(currNotesPref); + if (currNotesPref == null && notesPref != null) { + mPreferencesContainer.addPreference(notesPref); } Preference handwritingPref = mPreferencesContainer.findPreference(KEY_HANDWRITING); diff --git a/src/com/android/settings/core/gateway/SettingsGateway.java b/src/com/android/settings/core/gateway/SettingsGateway.java index 2243f5764e5..5d80193a208 100644 --- a/src/com/android/settings/core/gateway/SettingsGateway.java +++ b/src/com/android/settings/core/gateway/SettingsGateway.java @@ -164,6 +164,7 @@ import com.android.settings.print.PrintJobSettingsFragment; import com.android.settings.print.PrintSettingsFragment; import com.android.settings.privacy.PrivacyControlsFragment; import com.android.settings.privacy.PrivacyDashboardFragment; +import com.android.settings.regionalpreferences.RegionalPreferencesEntriesFragment; import com.android.settings.security.LockscreenDashboardFragment; import com.android.settings.security.MemtagPage; import com.android.settings.security.SecurityAdvancedSettings; @@ -367,7 +368,8 @@ public class SettingsGateway { TurnScreenOnDetails.class.getName(), NfcAndPaymentFragment.class.getName(), ColorAndMotionFragment.class.getName(), - LongBackgroundTasksDetails.class.getName() + LongBackgroundTasksDetails.class.getName(), + RegionalPreferencesEntriesFragment.class.getName() }; public static final String[] SETTINGS_FOR_RESTRICTED = { diff --git a/src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoFragment.java b/src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoFragment.java index 991ad25a457..5e5e2abce82 100644 --- a/src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoFragment.java +++ b/src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoFragment.java @@ -55,6 +55,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.function.Consumer; @SearchIndexable public class MyDeviceInfoFragment extends DashboardFragment @@ -79,7 +80,6 @@ public class MyDeviceInfoFragment extends DashboardFragment @Override public void onAttach(Context context) { super.onAttach(context); - use(ImeiInfoPreferenceController.class).setHost(this /* parent */); use(DeviceNamePreferenceController.class).setHost(this /* parent */); mBuildNumberPreferenceController = use(BuildNumberPreferenceController.class); mBuildNumberPreferenceController.setHost(this /* parent */); @@ -126,12 +126,27 @@ public class MyDeviceInfoFragment extends DashboardFragment controllers.add(new FccEquipmentIdPreferenceController(context)); controllers.add(new UptimePreferenceController(context, lifecycle)); + Consumer imeiInfoList = imeiKey -> { + ImeiInfoPreferenceController imeiRecord = + new ImeiInfoPreferenceController(context, imeiKey); + imeiRecord.init(fragment, slotSimStatus); + controllers.add(imeiRecord); + }; + + if (fragment != null) { + imeiInfoList.accept(ImeiInfoPreferenceController.DEFAULT_KEY); + } + for (int slotIndex = 0; slotIndex < slotSimStatus.size(); slotIndex ++) { SimStatusPreferenceController slotRecord = new SimStatusPreferenceController(context, slotSimStatus.getPreferenceKey(slotIndex)); slotRecord.init(fragment, slotSimStatus); controllers.add(slotRecord); + + if (fragment != null) { + imeiInfoList.accept(ImeiInfoPreferenceController.DEFAULT_KEY + (1 + slotIndex)); + } } EidStatus eidStatus = new EidStatus(slotSimStatus, context, executor); diff --git a/src/com/android/settings/deviceinfo/imei/ImeiInfoPreferenceController.java b/src/com/android/settings/deviceinfo/imei/ImeiInfoPreferenceController.java index a5019f896cf..13c5989a1dd 100644 --- a/src/com/android/settings/deviceinfo/imei/ImeiInfoPreferenceController.java +++ b/src/com/android/settings/deviceinfo/imei/ImeiInfoPreferenceController.java @@ -23,6 +23,7 @@ import android.os.UserManager; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; +import android.util.Log; import androidx.annotation.VisibleForTesting; import androidx.fragment.app.Fragment; @@ -33,70 +34,80 @@ import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.core.BasePreferenceController; import com.android.settings.deviceinfo.PhoneNumberSummaryPreference; +import com.android.settings.deviceinfo.simstatus.SlotSimStatus; import com.android.settings.network.SubscriptionUtil; import com.android.settingslib.Utils; -import java.util.ArrayList; -import java.util.List; - /** * Controller that manages preference for single and multi sim devices. */ public class ImeiInfoPreferenceController extends BasePreferenceController { - private static final String KEY_PREFERENCE_CATEGORY = "device_detail_category"; + private static final String TAG = "ImeiInfoPreferenceController"; - private final boolean mIsMultiSim; - private final TelephonyManager mTelephonyManager; - private final List mPreferenceList = new ArrayList<>(); + private static final String KEY_PREFERENCE_CATEGORY = "device_detail_category"; + public static final String DEFAULT_KEY = "imei_info"; + + private TelephonyManager mTelephonyManager; private Fragment mFragment; + private SlotSimStatus mSlotSimStatus; public ImeiInfoPreferenceController(Context context, String key) { super(context, key); - mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - mIsMultiSim = mTelephonyManager.getPhoneCount() > 1; } - public void setHost(Fragment fragment) { + public void init(Fragment fragment, SlotSimStatus slotSimStatus) { mFragment = fragment; + mSlotSimStatus = slotSimStatus; + } + + private boolean isMultiSim() { + return (mSlotSimStatus != null) && (mSlotSimStatus.size() > 1); + } + + private int keyToSlotIndex(String key) { + int simSlot = SubscriptionManager.INVALID_SIM_SLOT_INDEX; + try { + simSlot = Integer.valueOf(key.replace(DEFAULT_KEY, "")) - 1; + } catch (Exception exception) { + Log.i(TAG, "Invalid key : " + key); + } + return simSlot; + } + + private SubscriptionInfo getSubscriptionInfo(int simSlot) { + return (mSlotSimStatus == null) ? null : mSlotSimStatus.getSubscriptionInfo(simSlot); } @Override public void displayPreference(PreferenceScreen screen) { super.displayPreference(screen); - if (!SubscriptionUtil.isSimHardwareVisible(mContext)) { + if ((!SubscriptionUtil.isSimHardwareVisible(mContext)) || (mSlotSimStatus == null)) { return; } - final Preference preference = screen.findPreference(getPreferenceKey()); - final PreferenceCategory category = screen.findPreference(KEY_PREFERENCE_CATEGORY); - - mPreferenceList.add(preference); - updatePreference(preference, 0 /* simSlot */); - - final int imeiPreferenceOrder = preference.getOrder(); - // Add additional preferences for each sim in the device - for (int simSlotNumber = 1; simSlotNumber < mTelephonyManager.getPhoneCount(); - simSlotNumber++) { - final Preference multiSimPreference = createNewPreference(screen.getContext()); - multiSimPreference.setCopyingEnabled(true); - multiSimPreference.setOrder(imeiPreferenceOrder + simSlotNumber); - multiSimPreference.setKey(getPreferenceKey() + simSlotNumber); - category.addPreference(multiSimPreference); - mPreferenceList.add(multiSimPreference); - updatePreference(multiSimPreference, simSlotNumber); + mTelephonyManager = mContext.getSystemService(TelephonyManager.class); + Preference preference = screen.findPreference(DEFAULT_KEY); + if (!isAvailable() || preference == null || !preference.isVisible()) { + return; } + PreferenceCategory category = screen.findPreference(KEY_PREFERENCE_CATEGORY); + + int imeiPreferenceOrder = preference.getOrder(); + screen.removePreference(preference); + preference.setVisible(false); + + // Add additional preferences for each imei slot in the device + for (int simSlotNumber = 0; simSlotNumber < mSlotSimStatus.size(); simSlotNumber++) { + Preference multiImeiPreference = createNewPreference(screen.getContext()); + multiImeiPreference.setOrder(imeiPreferenceOrder + 1 + simSlotNumber); + multiImeiPreference.setKey(DEFAULT_KEY + (1 + simSlotNumber)); + category.addPreference(multiImeiPreference); + } } @Override public void updateState(Preference preference) { - if (preference == null) { - return; - } - int size = mPreferenceList.size(); - for (int i = 0; i < size; i++) { - Preference pref = mPreferenceList.get(i); - updatePreference(pref, i); - } + updatePreference(preference, keyToSlotIndex(preference.getKey())); } @Override @@ -112,8 +123,8 @@ public class ImeiInfoPreferenceController extends BasePreferenceController { @Override public boolean handlePreferenceTreeClick(Preference preference) { - final int simSlot = mPreferenceList.indexOf(preference); - if (simSlot == -1) { + final int simSlot = keyToSlotIndex(preference.getKey()); + if (simSlot < 0) { return false; } @@ -124,9 +135,10 @@ public class ImeiInfoPreferenceController extends BasePreferenceController { @Override public int getAvailabilityStatus() { - return SubscriptionUtil.isSimHardwareVisible(mContext) && - mContext.getSystemService(UserManager.class).isAdminUser() - && !Utils.isWifiOnly(mContext) ? AVAILABLE : UNSUPPORTED_ON_DEVICE; + boolean isAvailable = SubscriptionUtil.isSimHardwareVisible(mContext) && + mContext.getSystemService(UserManager.class).isAdminUser() && + !Utils.isWifiOnly(mContext); + return isAvailable ? AVAILABLE : UNSUPPORTED_ON_DEVICE; } @Override @@ -135,29 +147,48 @@ public class ImeiInfoPreferenceController extends BasePreferenceController { } private void updatePreference(Preference preference, int simSlot) { + SubscriptionInfo subInfo = getSubscriptionInfo(simSlot); + preference.setEnabled(subInfo != null); + preference.setCopyingEnabled(subInfo != null); preference.setTitle(getTitle(simSlot)); preference.setSummary(getSummary()); } - private CharSequence getTitleForGsmPhone(int simSlot) { - return mIsMultiSim ? mContext.getString(R.string.imei_multi_sim, simSlot + 1) + private CharSequence getTitleForGsmPhone(int simSlot, boolean isPrimaryImei) { + int titleId = isPrimaryImei ? R.string.imei_multi_sim_primary : R.string.imei_multi_sim; + return isMultiSim() ? mContext.getString(titleId, simSlot + 1) : mContext.getString(R.string.status_imei); } - private CharSequence getTitleForCdmaPhone(int simSlot) { - return mIsMultiSim ? mContext.getString(R.string.meid_multi_sim, simSlot + 1) + private CharSequence getTitleForCdmaPhone(int simSlot, boolean isPrimaryImei) { + int titleId = isPrimaryImei ? R.string.meid_multi_sim_primary : R.string.meid_multi_sim; + return isMultiSim() ? mContext.getString(titleId, simSlot + 1) : mContext.getString(R.string.status_meid_number); } - private CharSequence getTitle(int simSlot) { - final int phoneType = getPhoneType(simSlot); - return phoneType == PHONE_TYPE_CDMA ? getTitleForCdmaPhone(simSlot) - : getTitleForGsmPhone(simSlot); + protected boolean isPrimaryImei(int simSlot) { + CharSequence imei = getSummary(simSlot); + if (imei == null) { + return false; + } + String primaryImei = null; + try { + primaryImei = mTelephonyManager.getPrimaryImei(); + } catch (Exception exception) { + Log.i(TAG, "PrimaryImei not available. " + exception); + } + return (primaryImei != null) && primaryImei.equals(imei.toString()); } - private int getPhoneType(int slotIndex) { - SubscriptionInfo subInfo = SubscriptionManager.from(mContext) - .getActiveSubscriptionInfoForSimSlotIndex(slotIndex); + private CharSequence getTitle(int simSlot) { + boolean isPrimaryImei = isMultiSim() && isPrimaryImei(simSlot); + final int phoneType = getPhoneType(simSlot); + return phoneType == PHONE_TYPE_CDMA ? getTitleForCdmaPhone(simSlot, isPrimaryImei) + : getTitleForGsmPhone(simSlot, isPrimaryImei); + } + + public int getPhoneType(int slotIndex) { + SubscriptionInfo subInfo = getSubscriptionInfo(slotIndex); return mTelephonyManager.getCurrentPhoneType(subInfo != null ? subInfo.getSubscriptionId() : SubscriptionManager.DEFAULT_SUBSCRIPTION_ID); } diff --git a/src/com/android/settings/display/ScreenResolutionController.java b/src/com/android/settings/display/ScreenResolutionController.java index dca12757e18..a3433d405a5 100644 --- a/src/com/android/settings/display/ScreenResolutionController.java +++ b/src/com/android/settings/display/ScreenResolutionController.java @@ -17,7 +17,9 @@ package com.android.settings.display; import android.content.Context; +import android.graphics.Point; import android.hardware.display.DisplayManager; +import android.util.Log; import android.view.Display; import androidx.annotation.VisibleForTesting; @@ -25,32 +27,63 @@ import androidx.annotation.VisibleForTesting; import com.android.settings.R; import com.android.settings.core.BasePreferenceController; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + /** Controller that switch the screen resolution. */ public class ScreenResolutionController extends BasePreferenceController { - - static final int FHD_WIDTH = 1080; - static final int QHD_WIDTH = 1440; + private static final String TAG = "ScreenResolutionController"; + static final int HIGHRESOLUTION_IDX = 0; + static final int FULLRESOLUTION_IDX = 1; private Display mDisplay; + private Set mSupportedResolutions = null; + private int mHighWidth = 0; + private int mFullWidth = 0; + private int mHighHeight = 0; + private int mFullHeight = 0; public ScreenResolutionController(Context context, String key) { super(context, key); mDisplay = mContext.getSystemService(DisplayManager.class).getDisplay(Display.DEFAULT_DISPLAY); + + initSupportedResolutionData(); } - /** Check if the width is supported by the display. */ - private boolean isSupportedMode(int width) { + /** + * Initialize the resolution data. So far, we support two resolution switching. Save the width + * and the height for high resolution and full resolution. + */ + private void initSupportedResolutionData() { + // Collect and filter the resolutions + Set resolutions = new HashSet<>(); for (Display.Mode mode : getSupportedModes()) { - if (mode.getPhysicalWidth() == width) return true; + resolutions.add(new Point(mode.getPhysicalWidth(), mode.getPhysicalHeight())); } - return false; + mSupportedResolutions = resolutions; + + // Get the width and height for high resolution and full resolution + List resolutionList = new ArrayList<>(resolutions); + if (resolutionList == null || resolutionList.size() != 2) { + Log.e(TAG, "No support"); + return; + } + + Collections.sort(resolutionList, (p1, p2) -> p1.x * p1.y - p2.x * p2.y); + mHighWidth = resolutionList.get(HIGHRESOLUTION_IDX).x; + mHighHeight = resolutionList.get(HIGHRESOLUTION_IDX).y; + mFullWidth = resolutionList.get(FULLRESOLUTION_IDX).x; + mFullHeight = resolutionList.get(FULLRESOLUTION_IDX).y; } /** Return true if the device contains two (or more) resolutions. */ protected boolean checkSupportedResolutions() { - return isSupportedMode(FHD_WIDTH) && isSupportedMode(QHD_WIDTH); + return getHighWidth() != 0 && getFullWidth() != 0; } @Override @@ -61,20 +94,43 @@ public class ScreenResolutionController extends BasePreferenceController { @Override public CharSequence getSummary() { String summary = null; - switch (getDisplayWidth()) { - case FHD_WIDTH: - summary = mContext.getString(R.string.screen_resolution_summary_high); - break; - case QHD_WIDTH: - summary = mContext.getString(R.string.screen_resolution_summary_highest); - break; - default: - summary = mContext.getString(R.string.screen_resolution_title); + int width = getDisplayWidth(); + if (width == mHighWidth) { + summary = mContext.getString(R.string.screen_resolution_option_high); + } else if (width == mFullWidth) { + summary = mContext.getString(R.string.screen_resolution_option_full); + } else { + summary = mContext.getString(R.string.screen_resolution_title); } return summary; } + /** Return all supported resolutions of the device. */ + public Set getAllSupportedResolutions() { + return this.mSupportedResolutions; + } + + /** Return the high resolution width of the device. */ + public int getHighWidth() { + return this.mHighWidth; + } + + /** Return the full resolution width of the device. */ + public int getFullWidth() { + return this.mFullWidth; + } + + /** Return the high resolution height of the device. */ + public int getHighHeight() { + return this.mHighHeight; + } + + /** Return the full resolution height of the device. */ + public int getFullHeight() { + return this.mFullHeight; + } + @VisibleForTesting public int getDisplayWidth() { return mDisplay.getMode().getPhysicalWidth(); diff --git a/src/com/android/settings/display/ScreenResolutionFragment.java b/src/com/android/settings/display/ScreenResolutionFragment.java index 665f6b08d69..687fdb88fea 100644 --- a/src/com/android/settings/display/ScreenResolutionFragment.java +++ b/src/com/android/settings/display/ScreenResolutionFragment.java @@ -16,9 +16,6 @@ package com.android.settings.display; -import static com.android.settings.display.ScreenResolutionController.FHD_WIDTH; -import static com.android.settings.display.ScreenResolutionController.QHD_WIDTH; - import android.annotation.Nullable; import android.app.settings.SettingsEnums; import android.content.Context; @@ -48,7 +45,6 @@ import com.android.settingslib.widget.IllustrationPreference; import com.android.settingslib.widget.SelectorWithWidgetPreference; import java.util.ArrayList; -import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; @@ -59,8 +55,6 @@ public class ScreenResolutionFragment extends RadioButtonPickerFragment { private static final String TAG = "ScreenResolution"; private Resources mResources; - private static final int FHD_INDEX = 0; - private static final int QHD_INDEX = 1; private static final String SCREEN_RESOLUTION = "user_selected_resolution"; private Display mDefaultDisplay; private String[] mScreenResolutionOptions; @@ -71,6 +65,9 @@ public class ScreenResolutionFragment extends RadioButtonPickerFragment { private DisplayObserver mDisplayObserver; private AccessibilityManager mAccessibilityManager; + private int mHighWidth; + private int mFullWidth; + @Override public void onAttach(Context context) { super.onAttach(context); @@ -81,11 +78,18 @@ public class ScreenResolutionFragment extends RadioButtonPickerFragment { mResources = context.getResources(); mScreenResolutionOptions = mResources.getStringArray(R.array.config_screen_resolution_options_strings); - mScreenResolutionSummaries = - mResources.getStringArray(R.array.config_screen_resolution_summaries_strings); - mResolutions = getAllSupportedResolution(); mImagePreference = new IllustrationPreference(context); mDisplayObserver = new DisplayObserver(context); + ScreenResolutionController mController = + new ScreenResolutionController(context, "fragment"); + mResolutions = mController.getAllSupportedResolutions(); + mHighWidth = mController.getHighWidth(); + mFullWidth = mController.getFullWidth(); + mScreenResolutionSummaries = + new String[] { + mHighWidth + " x " + mController.getHighHeight(), + mFullWidth + " x " + mController.getFullHeight() + }; } @Override @@ -133,16 +137,6 @@ public class ScreenResolutionFragment extends RadioButtonPickerFragment { return candidates; } - /** Get all supported resolutions on the device. */ - private Set getAllSupportedResolution() { - Set resolutions = new HashSet<>(); - for (Display.Mode mode : mDefaultDisplay.getSupportedModes()) { - resolutions.add(new Point(mode.getPhysicalWidth(), mode.getPhysicalHeight())); - } - - return resolutions; - } - /** Get prefer display mode. */ private Display.Mode getPreferMode(int width) { for (Point resolution : mResolutions) { @@ -177,6 +171,7 @@ public class ScreenResolutionFragment extends RadioButtonPickerFragment { try { /** Apply the resolution change. */ + Log.i(TAG, "setUserPreferredDisplayMode: " + mode); mDefaultDisplay.setUserPreferredDisplayMode(mode); } catch (Exception e) { Log.e(TAG, "setUserPreferredDisplayMode() failed", e); @@ -194,16 +189,19 @@ public class ScreenResolutionFragment extends RadioButtonPickerFragment { /** Get the key corresponding to the resolution. */ @VisibleForTesting String getKeyForResolution(int width) { - return width == FHD_WIDTH - ? mScreenResolutionOptions[FHD_INDEX] - : width == QHD_WIDTH ? mScreenResolutionOptions[QHD_INDEX] : null; + return width == mHighWidth + ? mScreenResolutionOptions[ScreenResolutionController.HIGHRESOLUTION_IDX] + : width == mFullWidth + ? mScreenResolutionOptions[ScreenResolutionController.FULLRESOLUTION_IDX] + : null; } /** Get the width corresponding to the resolution key. */ int getWidthForResoluitonKey(String key) { - return mScreenResolutionOptions[FHD_INDEX].equals(key) - ? FHD_WIDTH - : mScreenResolutionOptions[QHD_INDEX].equals(key) ? QHD_WIDTH : -1; + return mScreenResolutionOptions[ScreenResolutionController.HIGHRESOLUTION_IDX].equals(key) + ? mHighWidth + : mScreenResolutionOptions[ScreenResolutionController.FULLRESOLUTION_IDX].equals( + key) ? mFullWidth : -1; } @Override @@ -248,9 +246,11 @@ public class ScreenResolutionFragment extends RadioButtonPickerFragment { private void updateIllustrationImage(IllustrationPreference preference) { String key = getDefaultKey(); - if (TextUtils.equals(mScreenResolutionOptions[FHD_INDEX], key)) { + if (TextUtils.equals( + mScreenResolutionOptions[ScreenResolutionController.HIGHRESOLUTION_IDX], key)) { preference.setLottieAnimationResId(R.drawable.screen_resolution_1080p); - } else if (TextUtils.equals(mScreenResolutionOptions[QHD_INDEX], key)) { + } else if (TextUtils.equals( + mScreenResolutionOptions[ScreenResolutionController.FULLRESOLUTION_IDX], key)) { preference.setLottieAnimationResId(R.drawable.screen_resolution_1440p); } } @@ -407,6 +407,8 @@ public class ScreenResolutionFragment extends RadioButtonPickerFragment { return false; } + Log.i(TAG, + "resolution changed from " + mPreviousWidth.get() + " to " + getCurrentWidth()); return true; } } diff --git a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java index 4b1bdf4eac3..004c8bf6638 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java +++ b/src/com/android/settings/fuelgauge/batteryusage/BatteryChartPreferenceController.java @@ -233,6 +233,7 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll batteryCallbackData -> { mBatteryUsageMap = batteryCallbackData.getBatteryUsageMap(); mScreenOnTimeMap = batteryCallbackData.getDeviceScreenOnTime(); + logScreenUsageTime(); refreshUi(); }); Log.d(TAG, "getBatteryLevelData: " + batteryLevelData); @@ -495,6 +496,42 @@ public class BatteryChartPreferenceController extends AbstractPreferenceControll }; } + private void logScreenUsageTime() { + if (mBatteryUsageMap == null || mScreenOnTimeMap == null) { + return; + } + final long totalScreenOnTime = + mScreenOnTimeMap + .get(BatteryChartViewModel.SELECTED_INDEX_ALL) + .get(BatteryChartViewModel.SELECTED_INDEX_ALL); + mMetricsFeatureProvider.action( + mPrefContext, + SettingsEnums.ACTION_BATTERY_USAGE_SCREEN_ON_TIME, + (int) totalScreenOnTime); + mMetricsFeatureProvider.action( + mPrefContext, + SettingsEnums.ACTION_BATTERY_USAGE_FOREGROUND_USAGE_TIME, + (int) getTotalForegroundUsageTime()); + } + + private long getTotalForegroundUsageTime() { + if (mBatteryUsageMap == null) { + return 0; + } + final BatteryDiffData totalBatteryUsageDiffData = + mBatteryUsageMap + .get(BatteryChartViewModel.SELECTED_INDEX_ALL) + .get(BatteryChartViewModel.SELECTED_INDEX_ALL); + if (totalBatteryUsageDiffData == null) { + return 0; + } + long totalValue = 0; + for (final BatteryDiffEntry entry : totalBatteryUsageDiffData.getAppDiffEntryList()) { + totalValue += entry.mForegroundUsageTimeInMs; + } + return totalValue; + } + private boolean isBatteryLevelDataInOneDay() { return mHourlyViewModels != null && mHourlyViewModels.size() == 1; } diff --git a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java index 649adbe8069..97363e07489 100644 --- a/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java +++ b/src/com/android/settings/fuelgauge/batteryusage/DataProcessor.java @@ -1792,6 +1792,8 @@ public final class DataProcessor { systemAppsDiffEntry = new BatteryDiffEntry.SystemAppsBatteryDiffEntry(context); } systemAppsDiffEntry.mConsumePower += batteryDiffEntry.mConsumePower; + systemAppsDiffEntry.mForegroundUsageTimeInMs += + batteryDiffEntry.mForegroundUsageTimeInMs; systemAppsDiffEntry.setTotalConsumePower( batteryDiffEntry.getTotalConsumePower()); appListIterator.remove(); diff --git a/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt b/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt index 2d2dddf9f5d..43add1ffb91 100644 --- a/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt +++ b/src/com/android/settings/spa/app/specialaccess/InstallUnknownApps.kt @@ -19,7 +19,6 @@ package com.android.settings.spa.app.specialaccess import android.Manifest import android.app.AppGlobals import android.app.AppOpsManager.MODE_DEFAULT -import android.app.AppOpsManager.MODE_ERRORED import android.app.AppOpsManager.OP_REQUEST_INSTALL_PACKAGES import android.content.Context import android.content.pm.ApplicationInfo @@ -65,7 +64,6 @@ class InstallUnknownAppsListModel(private val context: Context) : context = context, app = app, op = OP_REQUEST_INSTALL_PACKAGES, - modeForNotAllowed = MODE_ERRORED ), ) diff --git a/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt b/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt index 994ee92ba53..56c16df1dae 100644 --- a/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt +++ b/src/com/android/settings/spa/app/specialaccess/PictureInPicture.kt @@ -16,7 +16,6 @@ package com.android.settings.spa.app.specialaccess -import android.app.AppOpsManager.MODE_ERRORED import android.app.AppOpsManager.OP_PICTURE_IN_PICTURE import android.content.Context import android.content.pm.ActivityInfo @@ -85,7 +84,6 @@ class PictureInPictureListModel(private val context: Context) : context = context, app = app, op = OP_PICTURE_IN_PICTURE, - modeForNotAllowed = MODE_ERRORED, ), ) diff --git a/src/com/android/settings/spa/app/specialaccess/WifiControlApps.kt b/src/com/android/settings/spa/app/specialaccess/WifiControlApps.kt index 69f7b1e1688..50bb5c9b9f6 100644 --- a/src/com/android/settings/spa/app/specialaccess/WifiControlApps.kt +++ b/src/com/android/settings/spa/app/specialaccess/WifiControlApps.kt @@ -32,8 +32,8 @@ object WifiControlAppListProvider : TogglePermissionAppListProvider { } class WifiControlAppListModel( - private val context: Context, - private val packageManagers: IPackageManagers = PackageManagers + context: Context, + packageManagers: IPackageManagers = PackageManagers ) : AppOpPermissionListModel(context, packageManagers) { override val pageTitleResId = R.string.change_wifi_state_title override val switchTitleResId = R.string.change_wifi_state_app_detail_switch @@ -44,6 +44,6 @@ class WifiControlAppListModel( /** NETWORK_SETTINGS permission trumps CHANGE_WIFI_CONFIG. */ override val broaderPermission = Manifest.permission.NETWORK_SETTINGS - override val permissionHasAppopFlag = false + override val permissionHasAppOpFlag = false override val modeForNotAllowed = MODE_IGNORED } diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsControllerTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsControllerTest.java index 7590d617075..bf4e055c493 100644 --- a/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsControllerTest.java +++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothDetailsHearingDeviceControlsControllerTest.java @@ -18,13 +18,24 @@ package com.android.settings.bluetooth; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.content.Context; +import android.content.Intent; import android.util.FeatureFlagUtils; +import androidx.preference.Preference; + +import com.android.settings.SettingsActivity; +import com.android.settings.accessibility.AccessibilityHearingAidsFragment; +import com.android.settings.testutils.FakeFeatureFactory; + import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.robolectric.RobolectricTestRunner; @@ -36,14 +47,18 @@ public class BluetoothDetailsHearingDeviceControlsControllerTest extends @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + @Captor + private ArgumentCaptor mIntentArgumentCaptor; private BluetoothDetailsHearingDeviceControlsController mController; @Override public void setUp() { super.setUp(); - mController = new BluetoothDetailsHearingDeviceControlsController(mContext, mFragment, + FakeFeatureFactory.setupForTest(); + mController = new BluetoothDetailsHearingDeviceControlsController(mActivity, mFragment, mCachedDevice, mLifecycle); + when(mCachedDevice.isHearingAidDevice()).thenReturn(true); } @Test @@ -63,4 +78,23 @@ public class BluetoothDetailsHearingDeviceControlsControllerTest extends assertThat(mController.isAvailable()).isFalse(); } + + @Test + public void onPreferenceClick_hearingDeviceControlsKey_LaunchExpectedFragment() { + final Preference hearingControlsKeyPreference = new Preference(mContext); + hearingControlsKeyPreference.setKey( + BluetoothDetailsHearingDeviceControlsController.KEY_HEARING_DEVICE_CONTROLS); + + mController.onPreferenceClick(hearingControlsKeyPreference); + + assertStartActivityWithExpectedFragment(mActivity, + AccessibilityHearingAidsFragment.class.getName()); + } + + private void assertStartActivityWithExpectedFragment(Context mockContext, String fragmentName) { + verify(mockContext).startActivity(mIntentArgumentCaptor.capture()); + assertThat(mIntentArgumentCaptor.getValue() + .getStringExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT)) + .isEqualTo(fragmentName); + } } diff --git a/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusDevicesControllerTest.java b/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusDevicesControllerTest.java index be41787882a..d81722fd5fc 100644 --- a/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusDevicesControllerTest.java +++ b/tests/robotests/src/com/android/settings/connecteddevice/stylus/StylusDevicesControllerTest.java @@ -101,6 +101,7 @@ public class StylusDevicesControllerTest { when(mRm.getRoleHoldersAsUser(eq(RoleManager.ROLE_NOTES), any(UserHandle.class))) .thenReturn(Collections.singletonList(NOTES_PACKAGE_NAME)); + when(mRm.isRoleAvailable(RoleManager.ROLE_NOTES)).thenReturn(true); when(mContext.getPackageManager()).thenReturn(mPm); when(mPm.getApplicationInfo(eq(NOTES_PACKAGE_NAME), any(PackageManager.ApplicationInfoFlags.class))).thenReturn(new ApplicationInfo()); @@ -235,16 +236,26 @@ public class StylusDevicesControllerTest { } @Test - public void defaultNotesPreference_noRoleHolder_hidesNotesRoleApp() { + public void defaultNotesPreference_roleHolderChanges_updatesPreference() { + showScreen(mController); + Preference defaultNotesPref = mPreferenceContainer.getPreference(0); when(mRm.getRoleHoldersAsUser(eq(RoleManager.ROLE_NOTES), any(UserHandle.class))) .thenReturn(Collections.emptyList()); showScreen(mController); - for (int i = 0; i < mPreferenceContainer.getPreferenceCount(); i++) { - Preference pref = mPreferenceContainer.getPreference(i); - assertThat(pref.getTitle().toString()).isNotEqualTo( - mContext.getString(R.string.stylus_default_notes_app)); - } + assertThat(defaultNotesPref.getSummary().toString()).isEqualTo( + mContext.getString(R.string.default_app_none)); + } + + @Test + public void defaultNotesPreference_noRoleHolder_showNoneInSummary() { + when(mRm.getRoleHoldersAsUser(eq(RoleManager.ROLE_NOTES), any(UserHandle.class))) + .thenReturn(Collections.emptyList()); + showScreen(mController); + + Preference defaultNotesPref = mPreferenceContainer.getPreference(0); + assertThat(defaultNotesPref.getSummary().toString()).isEqualTo( + mContext.getString(R.string.default_app_none)); } @Test diff --git a/tests/robotests/src/com/android/settings/deviceinfo/imei/ImeiInfoPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/deviceinfo/imei/ImeiInfoPreferenceControllerTest.java index f7970cf9ce8..f3a935820ac 100644 --- a/tests/robotests/src/com/android/settings/deviceinfo/imei/ImeiInfoPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/deviceinfo/imei/ImeiInfoPreferenceControllerTest.java @@ -18,10 +18,9 @@ package com.android.settings.deviceinfo.imei; import static android.telephony.TelephonyManager.PHONE_TYPE_CDMA; import static android.telephony.TelephonyManager.PHONE_TYPE_GSM; - +import static android.telephony.TelephonyManager.PHONE_TYPE_NONE; import static com.android.settings.core.BasePreferenceController.AVAILABLE; - -import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -32,15 +31,13 @@ import android.content.Context; import android.content.res.Resources; import android.os.UserManager; import android.telephony.TelephonyManager; - import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.preference.Preference; import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceScreen; - import com.android.settings.R; - +import com.android.settings.deviceinfo.simstatus.SlotSimStatus; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -50,7 +47,6 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.robolectric.RobolectricTestRunner; import org.robolectric.RuntimeEnvironment; -import org.robolectric.util.ReflectionHelpers; @RunWith(RobolectricTestRunner.class) public class ImeiInfoPreferenceControllerTest { @@ -72,7 +68,9 @@ public class ImeiInfoPreferenceControllerTest { private Context mContext; private Resources mResources; + private ImeiInfoPreferenceController mDefaultController; private ImeiInfoPreferenceController mController; + private ImeiInfoPreferenceController mSecondController; @Before public void setUp() { @@ -83,56 +81,77 @@ public class ImeiInfoPreferenceControllerTest { when(mContext.getResources()).thenReturn(mResources); when(mResources.getBoolean(R.bool.config_show_sim_info)).thenReturn(true); - doReturn(mUserManager).when(mContext).getSystemService(UserManager.class); - mController = spy(new ImeiInfoPreferenceController(mContext, "imei_info")); - mController.setHost(mFragment); - doReturn(AVAILABLE).when(mController).getAvailabilityStatus(); + mockService(Context.TELEPHONY_SERVICE, TelephonyManager.class, mTelephonyManager); + mockService(Context.USER_SERVICE, UserManager.class, mUserManager); + when(mScreen.getContext()).thenReturn(mContext); - doReturn(mSecondSimPreference).when(mController).createNewPreference(mContext); final String categoryKey = "device_detail_category"; when(mScreen.findPreference(categoryKey)).thenReturn(mCategory); - ReflectionHelpers.setField(mController, "mTelephonyManager", mTelephonyManager); - when(mScreen.findPreference(mController.getPreferenceKey())).thenReturn(mPreference); - final String prefKey = mController.getPreferenceKey(); - when(mPreference.getKey()).thenReturn(prefKey); - when(mPreference.isVisible()).thenReturn(true); } - @Test - public void displayPreference_multiSimGsm_shouldAddSecondPreference() { - ReflectionHelpers.setField(mController, "mIsMultiSim", true); - when(mTelephonyManager.getPhoneCount()).thenReturn(2); - when(mTelephonyManager.getCurrentPhoneType(anyInt())).thenReturn(PHONE_TYPE_GSM); + private ImeiInfoPreferenceController createPreferenceController(SlotSimStatus slotSimStatus, + String key, Preference preference, int phoneType) { + ImeiInfoPreferenceController controller = + spy(new ImeiInfoPreferenceController(mContext, key) { + public int getPhoneType(int slotId) { + return phoneType; + } + }); + controller.init(mFragment, slotSimStatus); + doReturn(AVAILABLE).when(controller).getAvailabilityStatus(); + doReturn(preference).when(controller).createNewPreference(mContext); - mController.displayPreference(mScreen); + when(mScreen.findPreference(key)).thenReturn(preference); + when(preference.getKey()).thenReturn(key); + when(preference.isVisible()).thenReturn(true); + return controller; + } - verify(mCategory).addPreference(mSecondSimPreference); + private void setupPhoneCount(int count, int phoneType1, int phoneType2) { + when(mTelephonyManager.getPhoneCount()).thenReturn(count); + + SlotSimStatus slotSimStatus = new SlotSimStatus(mContext); + mController = createPreferenceController(slotSimStatus, + "imei_info1", mPreference, phoneType1); + mSecondController = createPreferenceController(slotSimStatus, + "imei_info2", mSecondSimPreference, phoneType2); } @Ignore + @Test + public void displayPreference_multiSimGsm_shouldAddSecondPreference() { + setupPhoneCount(2, PHONE_TYPE_GSM, PHONE_TYPE_GSM); + + mDefaultController.displayPreference(mScreen); + + verify(mCategory).addPreference(mSecondSimPreference); + } + @Test public void displayPreference_singleSimCdmaPhone_shouldSetSingleSimCdmaTitleAndMeid() { - ReflectionHelpers.setField(mController, "mIsMultiSim", false); - final String meid = "125132215123"; - when(mTelephonyManager.getCurrentPhoneType(anyInt())).thenReturn(PHONE_TYPE_CDMA); + setupPhoneCount(1, PHONE_TYPE_CDMA, PHONE_TYPE_NONE); + + final String meid = "Tap to show info"; when(mTelephonyManager.getMeid(anyInt())).thenReturn(meid); mController.displayPreference(mScreen); + mController.updateState(mPreference); verify(mPreference).setTitle(mContext.getString(R.string.status_meid_number)); verify(mPreference).setSummary(meid); } - @Ignore @Test public void displayPreference_multiSimCdmaPhone_shouldSetMultiSimCdmaTitleAndMeid() { - ReflectionHelpers.setField(mController, "mIsMultiSim", true); - final String meid = "125132215123"; - when(mTelephonyManager.getPhoneCount()).thenReturn(2); - when(mTelephonyManager.getCurrentPhoneType(anyInt())).thenReturn(PHONE_TYPE_CDMA); + setupPhoneCount(2, PHONE_TYPE_CDMA, PHONE_TYPE_CDMA); + + final String meid = "Tap to show info"; when(mTelephonyManager.getMeid(anyInt())).thenReturn(meid); mController.displayPreference(mScreen); + mController.updateState(mPreference); + mSecondController.displayPreference(mScreen); + mSecondController.updateState(mSecondSimPreference); verify(mPreference).setTitle(mContext.getString(R.string.meid_multi_sim, 1 /* sim slot */)); verify(mSecondSimPreference).setTitle( @@ -141,30 +160,31 @@ public class ImeiInfoPreferenceControllerTest { verify(mSecondSimPreference).setSummary(meid); } - @Ignore @Test public void displayPreference_singleSimGsmPhone_shouldSetSingleSimGsmTitleAndImei() { - ReflectionHelpers.setField(mController, "mIsMultiSim", false); - final String imei = "125132215123"; - when(mTelephonyManager.getCurrentPhoneType(anyInt())).thenReturn(PHONE_TYPE_GSM); + setupPhoneCount(1, PHONE_TYPE_GSM, PHONE_TYPE_NONE); + + final String imei = "Tap to show info"; when(mTelephonyManager.getImei(anyInt())).thenReturn(imei); mController.displayPreference(mScreen); + mController.updateState(mPreference); verify(mPreference).setTitle(mContext.getString(R.string.status_imei)); verify(mPreference).setSummary(imei); } - @Ignore @Test public void displayPreference_multiSimGsmPhone_shouldSetMultiSimGsmTitleAndImei() { - ReflectionHelpers.setField(mController, "mIsMultiSim", true); - final String imei = "125132215123"; - when(mTelephonyManager.getPhoneCount()).thenReturn(2); - when(mTelephonyManager.getCurrentPhoneType(anyInt())).thenReturn(PHONE_TYPE_GSM); + setupPhoneCount(2, PHONE_TYPE_GSM, PHONE_TYPE_GSM); + + final String imei = "Tap to show info"; when(mTelephonyManager.getImei(anyInt())).thenReturn(imei); mController.displayPreference(mScreen); + mController.updateState(mPreference); + mSecondController.displayPreference(mScreen); + mSecondController.updateState(mSecondSimPreference); verify(mPreference).setTitle(mContext.getString(R.string.imei_multi_sim, 1 /* sim slot */)); verify(mSecondSimPreference).setTitle( @@ -175,6 +195,8 @@ public class ImeiInfoPreferenceControllerTest { @Test public void handlePreferenceTreeClick_shouldStartDialogFragment() { + setupPhoneCount(1, PHONE_TYPE_GSM, PHONE_TYPE_NONE); + when(mFragment.getChildFragmentManager()) .thenReturn(mock(FragmentManager.class, Answers.RETURNS_DEEP_STUBS)); when(mPreference.getTitle()).thenReturn("SomeTitle"); @@ -184,4 +206,9 @@ public class ImeiInfoPreferenceControllerTest { verify(mFragment).getChildFragmentManager(); } + + private void mockService(String serviceName, Class serviceClass, T service) { + when(mContext.getSystemServiceName(serviceClass)).thenReturn(serviceName); + when(mContext.getSystemService(serviceName)).thenReturn(service); + } } diff --git a/tests/uitests/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivityTest.java b/tests/uitests/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivityTest.java index 24f4f89e5bc..b074090afae 100644 --- a/tests/uitests/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivityTest.java +++ b/tests/uitests/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollmentActivityTest.java @@ -58,26 +58,30 @@ public class FingerprintEnrollmentActivityTest { } @Test - public void lunchWithoutCredential() { + public void launchWithoutCredential() { launchFingerprintEnrollActivity(true); Assert.assertNotNull(mDevice.wait(Until.hasObject( By.text("Choose your backup screen lock method")), IDLE_TIMEOUT)); } @Test - public void lunchWithCredential() { + public void launchWithCredential() { LockScreenUtil.setLockscreen(LockScreenUtil.LockscreenType.PIN, "1234", true); launchFingerprintEnrollActivity(true); - Assert.assertNotNull(mDevice.wait(Until.hasObject( - By.text("More")), IDLE_TIMEOUT)); + for (long i = 0; i < IDLE_TIMEOUT; i += 100L) { + if (mDevice.wait(Until.hasObject(By.text("More")), 50L) != null) { + break; + } else if (mDevice.wait(Until.hasObject(By.text("I agree")), 50L) != null) { + break; + } + } - //click more btn twice and the introduction should stay in the last page - UiObject2 moreBtn = mDevice.findObject(By.text("More")); - moreBtn.click(); - Assert.assertNotNull(mDevice.wait(Until.hasObject( - By.text("More")), IDLE_TIMEOUT)); - moreBtn = mDevice.findObject(By.text("More")); - moreBtn.click(); + //click more btn at most twice and the introduction should stay in the last page + UiObject2 moreBtn; + for (int i = 0; i < 2 && (moreBtn = mDevice.findObject(By.text("More"))) != null; ++i) { + moreBtn.click(); + mDevice.wait(Until.hasObject(By.text("More")), IDLE_TIMEOUT); + } Assert.assertNotNull(mDevice.wait(Until.hasObject( By.text("I agree")), IDLE_TIMEOUT)); @@ -114,5 +118,4 @@ public class FingerprintEnrollmentActivityTest { intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivity(intent); } - } diff --git a/tests/unit/src/com/android/settings/biometrics2/data/repository/FingerprintRepositoryTest.java b/tests/unit/src/com/android/settings/biometrics2/data/repository/FingerprintRepositoryTest.java index e5920f35b4a..6fb86903316 100644 --- a/tests/unit/src/com/android/settings/biometrics2/data/repository/FingerprintRepositoryTest.java +++ b/tests/unit/src/com/android/settings/biometrics2/data/repository/FingerprintRepositoryTest.java @@ -23,16 +23,17 @@ import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFP import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC; import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UNKNOWN; -import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintEnrolledFingerprints; -import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintFirstSensor; - import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.when; import android.content.Context; import android.content.res.Resources; +import android.hardware.biometrics.SensorProperties; +import android.hardware.fingerprint.Fingerprint; import android.hardware.fingerprint.FingerprintManager; +import android.hardware.fingerprint.FingerprintSensorProperties; +import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; @@ -48,6 +49,8 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.ArrayList; + @RunWith(AndroidJUnit4.class) public class FingerprintRepositoryTest { @@ -66,31 +69,49 @@ public class FingerprintRepositoryTest { } @Test - public void testCanAssumeSensorType() { + public void testCanAssumeSensorType_forUnknownSensor() { setupFingerprintFirstSensor(mFingerprintManager, TYPE_UNKNOWN, 1); assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse(); + assertThat(mFingerprintRepository.canAssumeSfps()).isFalse(); + } + @Test + public void testCanAssumeSensorType_forRearSensor() { setupFingerprintFirstSensor(mFingerprintManager, TYPE_REAR, 1); assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse(); + assertThat(mFingerprintRepository.canAssumeSfps()).isFalse(); + } + @Test + public void testCanAssumeSensorType_forUdfpsUltrasonicSensor() { setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_ULTRASONIC, 1); assertThat(mFingerprintRepository.canAssumeUdfps()).isTrue(); + assertThat(mFingerprintRepository.canAssumeSfps()).isFalse(); + } + @Test + public void testCanAssumeSensorType_forUdfpsOpticalSensor() { setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_OPTICAL, 1); assertThat(mFingerprintRepository.canAssumeUdfps()).isTrue(); + assertThat(mFingerprintRepository.canAssumeSfps()).isFalse(); + } + @Test + public void testCanAssumeSensorType_forPowerButtonSensor() { setupFingerprintFirstSensor(mFingerprintManager, TYPE_POWER_BUTTON, 1); assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse(); + assertThat(mFingerprintRepository.canAssumeSfps()).isTrue(); + } + @Test + public void testCanAssumeSensorType_forHomeButtonSensor() { setupFingerprintFirstSensor(mFingerprintManager, TYPE_HOME_BUTTON, 1); assertThat(mFingerprintRepository.canAssumeUdfps()).isFalse(); + assertThat(mFingerprintRepository.canAssumeSfps()).isFalse(); } @Test public void testGetMaxFingerprints() { - setupFingerprintFirstSensor(mFingerprintManager, TYPE_UNKNOWN, 44); - assertThat(mFingerprintRepository.getMaxFingerprints()).isEqualTo(44); - setupFingerprintFirstSensor(mFingerprintManager, TYPE_UNKNOWN, 999); assertThat(mFingerprintRepository.getMaxFingerprints()).isEqualTo(999); } @@ -122,4 +143,31 @@ public class FingerprintRepositoryTest { "suw_max_fingerprints_enrollable"); when(mockedResources.getInteger(resId)).thenReturn(numOfFp); } + + public static void setupFingerprintFirstSensor( + @NonNull FingerprintManager mockedFingerprintManager, + @FingerprintSensorProperties.SensorType int sensorType, + int maxEnrollmentsPerUser) { + + final ArrayList props = new ArrayList<>(); + props.add(new FingerprintSensorPropertiesInternal( + 0 /* sensorId */, + SensorProperties.STRENGTH_STRONG, + maxEnrollmentsPerUser, + new ArrayList<>() /* componentInfo */, + sensorType, + true /* resetLockoutRequiresHardwareAuthToken */)); + when(mockedFingerprintManager.getSensorPropertiesInternal()).thenReturn(props); + } + + public static void setupFingerprintEnrolledFingerprints( + @NonNull FingerprintManager mockedFingerprintManager, + int userId, + int enrolledFingerprints) { + final ArrayList ret = new ArrayList<>(); + for (int i = 0; i < enrolledFingerprints; ++i) { + ret.add(new Fingerprint("name", 0, 0, 0L)); + } + when(mockedFingerprintManager.getEnrolledFingerprints(userId)).thenReturn(ret); + } } diff --git a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.java b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.java index 4db005ec609..024607d7381 100644 --- a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.java +++ b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/AutoCredentialViewModelTest.java @@ -45,6 +45,7 @@ import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_G import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.when; import android.annotation.NonNull; @@ -71,6 +72,8 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.concurrent.atomic.AtomicBoolean; + @RunWith(AndroidJUnit4.class) public class AutoCredentialViewModelTest { @@ -299,13 +302,18 @@ public class AutoCredentialViewModelTest { when(mLockPatternUtils.verifyGatekeeperPasswordHandle(gkPwHandle, newChallenge, userId)) .thenReturn(newGoodCredential(gkPwHandle, new byte[] { 1 })); + final AtomicBoolean hasCalledRemoveGkPwHandle = new AtomicBoolean(); + doAnswer(invocation -> { + hasCalledRemoveGkPwHandle.set(true); + return null; + }).when(mLockPatternUtils).removeGatekeeperPasswordHandle(gkPwHandle); + // Run credential check @CredentialAction final int action = mViewModel.checkCredential(); // Check viewModel behavior assertThat(action).isEqualTo(CREDENTIAL_IS_GENERATING_CHALLENGE); assertThat(mViewModel.getGenerateChallengeFailedLiveData().getValue()).isNull(); - assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1); // Check data inside CredentialModel final Bundle extras = mViewModel.createCredentialIntentExtra(); @@ -314,6 +322,8 @@ public class AutoCredentialViewModelTest { assertThat(extras.getByteArray(EXTRA_KEY_CHALLENGE_TOKEN)).isNotNull(); assertThat(extras.getLong(EXTRA_KEY_GK_PW_HANDLE)).isEqualTo(INVALID_GK_PW_HANDLE); assertThat(extras.getLong(EXTRA_KEY_CHALLENGE)).isNotEqualTo(INVALID_CHALLENGE); + assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1); + assertThat(hasCalledRemoveGkPwHandle.get()).isFalse(); // Check createGeneratingChallengeExtras() final Bundle generatingChallengeExtras = mViewModel.createGeneratingChallengeExtras(); @@ -511,6 +521,12 @@ public class AutoCredentialViewModelTest { when(mLockPatternUtils.verifyGatekeeperPasswordHandle(gkPwHandle, newChallenge, userId)) .thenReturn(newGoodCredential(gkPwHandle, new byte[] { 1 })); + final AtomicBoolean hasCalledRemoveGkPwHandle = new AtomicBoolean(); + doAnswer(invocation -> { + hasCalledRemoveGkPwHandle.set(true); + return null; + }).when(mLockPatternUtils).removeGatekeeperPasswordHandle(gkPwHandle); + // Run checkNewCredentialFromActivityResult() final Intent intent = new Intent().putExtra(EXTRA_KEY_GK_PW_HANDLE, gkPwHandle); final boolean ret = mViewModel.checkNewCredentialFromActivityResult(true, @@ -524,6 +540,7 @@ public class AutoCredentialViewModelTest { assertThat(extras.getByteArray(EXTRA_KEY_CHALLENGE_TOKEN)).isNotNull(); assertThat(extras.getLong(EXTRA_KEY_GK_PW_HANDLE)).isEqualTo(INVALID_GK_PW_HANDLE); assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1); + assertThat(hasCalledRemoveGkPwHandle.get()).isTrue(); } @Test @@ -541,6 +558,12 @@ public class AutoCredentialViewModelTest { when(mLockPatternUtils.verifyGatekeeperPasswordHandle(gkPwHandle, newChallenge, userId)) .thenReturn(newGoodCredential(gkPwHandle, new byte[] { 1 })); + final AtomicBoolean hasCalledRemoveGkPwHandle = new AtomicBoolean(); + doAnswer(invocation -> { + hasCalledRemoveGkPwHandle.set(true); + return null; + }).when(mLockPatternUtils).removeGatekeeperPasswordHandle(gkPwHandle); + // Run checkNewCredentialFromActivityResult() final Intent intent = new Intent().putExtra(EXTRA_KEY_GK_PW_HANDLE, gkPwHandle); final boolean ret = mViewModel.checkNewCredentialFromActivityResult(false, @@ -554,6 +577,7 @@ public class AutoCredentialViewModelTest { assertThat(extras.getByteArray(EXTRA_KEY_CHALLENGE_TOKEN)).isNotNull(); assertThat(extras.getLong(EXTRA_KEY_GK_PW_HANDLE)).isEqualTo(INVALID_GK_PW_HANDLE); assertThat(mChallengeGenerator.mCallbackRunCount).isEqualTo(1); + assertThat(hasCalledRemoveGkPwHandle.get()).isTrue(); } public static class TestChallengeGenerator implements ChallengeGenerator { diff --git a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.java b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.java index a9536cffcbf..192a3050088 100644 --- a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.java +++ b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollIntroViewModelTest.java @@ -20,6 +20,8 @@ import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_OPTICAL; import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_UDFPS_ULTRASONIC; +import static com.android.settings.biometrics2.data.repository.FingerprintRepositoryTest.setupFingerprintEnrolledFingerprints; +import static com.android.settings.biometrics2.data.repository.FingerprintRepositoryTest.setupFingerprintFirstSensor; import static com.android.settings.biometrics2.data.repository.FingerprintRepositoryTest.setupSuwMaxFingerprintsEnrollable; import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX; import static com.android.settings.biometrics2.ui.model.FingerprintEnrollIntroStatus.FINGERPRINT_ENROLLABLE_OK; @@ -32,8 +34,6 @@ import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsS import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwPortalRequest; import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwRequest; import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwSuggestedActionFlowRequest; -import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintEnrolledFingerprints; -import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintFirstSensor; import static com.google.common.truth.Truth.assertThat; @@ -134,26 +134,47 @@ public class FingerprintEnrollIntroViewModelTest { } @Test - public void testOnStartToUpdateEnrollableStatus_isNotSuw() { - testOnStartToUpdateEnrollableStatus(newAllFalseRequest(mApplication)); + public void testOnStartToUpdateEnrollableStatusOk_isNotSuw() { + testOnStartToUpdateEnrollableStatusOk(newAllFalseRequest(mApplication)); } @Test - public void testOnStartToUpdateEnrollableStatus_isSuwDeferred() { - testOnStartToUpdateEnrollableStatus(newIsSuwDeferredRequest(mApplication)); + public void testOnStartToUpdateEnrollableStatusReachMax_isNotSuw() { + testOnStartToUpdateEnrollableStatusReachMax(newAllFalseRequest(mApplication)); } @Test - public void testOnStartToUpdateEnrollableStatus_isSuwPortal() { - testOnStartToUpdateEnrollableStatus(newIsSuwPortalRequest(mApplication)); + public void testOnStartToUpdateEnrollableStatusOk_isSuwDeferred() { + testOnStartToUpdateEnrollableStatusOk(newIsSuwDeferredRequest(mApplication)); } @Test - public void testOnStartToUpdateEnrollableStatus_isSuwSuggestedActionFlow() { - testOnStartToUpdateEnrollableStatus(newIsSuwSuggestedActionFlowRequest(mApplication)); + public void testOnStartToUpdateEnrollableStatusReachMax_isSuwDeferred() { + testOnStartToUpdateEnrollableStatusReachMax(newIsSuwDeferredRequest(mApplication)); } - private void testOnStartToUpdateEnrollableStatus(@NonNull EnrollmentRequest request) { + @Test + public void testOnStartToUpdateEnrollableStatusOk_isSuwPortal() { + testOnStartToUpdateEnrollableStatusOk(newIsSuwPortalRequest(mApplication)); + } + + @Test + public void testOnStartToUpdateEnrollableStatusReachMax_isSuwPortal() { + testOnStartToUpdateEnrollableStatusReachMax(newIsSuwPortalRequest(mApplication)); + } + + @Test + public void testOnStartToUpdateEnrollableStatusOk_isSuwSuggestedActionFlow() { + testOnStartToUpdateEnrollableStatusOk(newIsSuwSuggestedActionFlowRequest(mApplication)); + } + + @Test + public void testOnStartToUpdateEnrollableStatusReachMax_isSuwSuggestedActionFlow() { + testOnStartToUpdateEnrollableStatusReachMax( + newIsSuwSuggestedActionFlowRequest(mApplication)); + } + + private void testOnStartToUpdateEnrollableStatusOk(@NonNull EnrollmentRequest request) { final int userId = 45; mViewModel.setUserId(userId); mViewModel.setEnrollmentRequest(request); @@ -163,19 +184,28 @@ public class FingerprintEnrollIntroViewModelTest { mViewModel.onStart(mLifecycleOwner); FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue(); assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK); + } + + private void testOnStartToUpdateEnrollableStatusReachMax(@NonNull EnrollmentRequest request) { + final int userId = 45; + mViewModel.setUserId(userId); + mViewModel.setEnrollmentRequest(request); setupFingerprintEnrolledFingerprints(mFingerprintManager, userId, 5); setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_OPTICAL, 5); mViewModel.onStart(mLifecycleOwner); - status = mViewModel.getPageStatusLiveData().getValue(); + FingerprintEnrollIntroStatus status = mViewModel.getPageStatusLiveData().getValue(); assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX); } @Test - public void textCanAssumeUdfps() { + public void textCanAssumeUdfps_forUdfpsUltrasonicSensor() { setupFingerprintFirstSensor(mFingerprintManager, TYPE_UDFPS_ULTRASONIC, 1); assertThat(mViewModel.canAssumeUdfps()).isEqualTo(true); + } + @Test + public void textCanAssumeUdfps_forRearSensor() { setupFingerprintFirstSensor(mFingerprintManager, TYPE_REAR, 1); assertThat(mViewModel.canAssumeUdfps()).isEqualTo(false); } @@ -238,7 +268,7 @@ public class FingerprintEnrollIntroViewModelTest { assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_OK); // Perform click on `next` - mViewModel.onNextButtonClick(null); + mViewModel.onNextButtonClick(); assertThat(mViewModel.getActionLiveData().getValue()) .isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_CONTINUE_ENROLL); @@ -258,7 +288,7 @@ public class FingerprintEnrollIntroViewModelTest { assertThat(status.getEnrollableStatus()).isEqualTo(FINGERPRINT_ENROLLABLE_ERROR_REACH_MAX); // Perform click on `next` - mViewModel.onNextButtonClick(null); + mViewModel.onNextButtonClick(); assertThat(mViewModel.getActionLiveData().getValue()) .isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_DONE_AND_FINISH); @@ -266,7 +296,7 @@ public class FingerprintEnrollIntroViewModelTest { @Test public void testOnSkipOrCancelButtonClick() { - mViewModel.onSkipOrCancelButtonClick(null); + mViewModel.onSkipOrCancelButtonClick(); assertThat(mViewModel.getActionLiveData().getValue()) .isEqualTo(FINGERPRINT_ENROLL_INTRO_ACTION_SKIP_OR_CANCEL); diff --git a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModelTest.java b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModelTest.java index b1d55aa332d..736e1ae5b39 100644 --- a/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModelTest.java +++ b/tests/unit/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollmentViewModelTest.java @@ -20,10 +20,10 @@ import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_FINISHE import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_SKIP; import static com.android.settings.biometrics.BiometricEnrollBase.RESULT_TIMEOUT; import static com.android.settings.biometrics.fingerprint.SetupFingerprintEnrollIntroduction.EXTRA_FINGERPRINT_ENROLLED_COUNT; +import static com.android.settings.biometrics2.data.repository.FingerprintRepositoryTest.setupFingerprintEnrolledFingerprints; import static com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollmentViewModel.SAVED_STATE_IS_WAITING_ACTIVITY_RESULT; import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newAllFalseRequest; import static com.android.settings.biometrics2.util.EnrollmentRequestUtil.newIsSuwRequest; -import static com.android.settings.biometrics2.util.FingerprintManagerUtil.setupFingerprintEnrolledFingerprints; import static com.google.common.truth.Truth.assertThat; diff --git a/tests/unit/src/com/android/settings/biometrics2/util/FingerprintManagerUtil.java b/tests/unit/src/com/android/settings/biometrics2/util/FingerprintManagerUtil.java deleted file mode 100644 index cb45fa4c6c2..00000000000 --- a/tests/unit/src/com/android/settings/biometrics2/util/FingerprintManagerUtil.java +++ /dev/null @@ -1,58 +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.biometrics2.util; - -import static org.mockito.Mockito.when; - -import android.hardware.biometrics.SensorProperties; -import android.hardware.fingerprint.Fingerprint; -import android.hardware.fingerprint.FingerprintManager; -import android.hardware.fingerprint.FingerprintSensorProperties; -import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; - -import androidx.annotation.NonNull; - -import java.util.ArrayList; - -public class FingerprintManagerUtil { - - public static void setupFingerprintFirstSensor( - @NonNull FingerprintManager mockedFingerprintManager, - @FingerprintSensorProperties.SensorType int sensorType, - int maxEnrollmentsPerUser) { - final ArrayList props = new ArrayList<>(); - props.add(new FingerprintSensorPropertiesInternal( - 0 /* sensorId */, - SensorProperties.STRENGTH_STRONG, - maxEnrollmentsPerUser, - new ArrayList<>() /* componentInfo */, - sensorType, - true /* resetLockoutRequiresHardwareAuthToken */)); - when(mockedFingerprintManager.getSensorPropertiesInternal()).thenReturn(props); - } - - public static void setupFingerprintEnrolledFingerprints( - @NonNull FingerprintManager mockedFingerprintManager, - int userId, - int enrolledFingerprints) { - final ArrayList ret = new ArrayList<>(); - for (int i = 0; i < enrolledFingerprints; ++i) { - ret.add(new Fingerprint("name", 0, 0, 0L)); - } - when(mockedFingerprintManager.getEnrolledFingerprints(userId)).thenReturn(ret); - } -} diff --git a/tests/unit/src/com/android/settings/communal/CommunalPreferenceControllerTest.java b/tests/unit/src/com/android/settings/communal/CommunalPreferenceControllerTest.java new file mode 100644 index 00000000000..e8421d0b826 --- /dev/null +++ b/tests/unit/src/com/android/settings/communal/CommunalPreferenceControllerTest.java @@ -0,0 +1,88 @@ +/* + * 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.communal; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; +import android.os.UserManager; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.settings.testutils.ResourcesUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class CommunalPreferenceControllerTest { + @Mock + private UserManager mUserManager; + + private Resources mResources; + private CommunalPreferenceController mController; + + private static final String PREF_KEY = "test_key"; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + final Context context = spy(ApplicationProvider.getApplicationContext()); + mResources = spy(context.getResources()); + + mController = new CommunalPreferenceController(context, PREF_KEY); + + when(context.getResources()).thenReturn(mResources); + when(context.getSystemService(UserManager.class)).thenReturn(mUserManager); + } + + @Test + public void isAvailable_communalEnabled_shouldBeTrueForSystemUser() { + setCommunalEnabled(true); + when(mUserManager.isSystemUser()).thenReturn(true); + assertTrue(mController.isAvailable()); + } + + @Test + public void isAvailable_communalEnabled_shouldBeFalseForNonSystemUser() { + setCommunalEnabled(true); + when(mUserManager.isSystemUser()).thenReturn(false); + assertFalse(mController.isAvailable()); + } + + @Test + public void isAvailable_communalDisabled_shouldBeFalseForSystemUser() { + setCommunalEnabled(false); + when(mUserManager.isSystemUser()).thenReturn(true); + assertFalse(mController.isAvailable()); + } + + private void setCommunalEnabled(boolean enabled) { + final int boolId = ResourcesUtils.getResourcesId( + ApplicationProvider.getApplicationContext(), "bool", + "config_show_communal_settings"); + when(mResources.getBoolean(boolId)).thenReturn(enabled); + } +}