Merge "Fix b/265746746: Announce "Battery usage for [slot_timestamp]" instead of changing focus when Talk Back on."
diff --git a/src/com/android/settings/biometrics2/data/repository/AccessibilityRepository.java b/src/com/android/settings/biometrics2/data/repository/AccessibilityRepository.java
new file mode 100644
index 0000000..5353f89
--- /dev/null
+++ b/src/com/android/settings/biometrics2/data/repository/AccessibilityRepository.java
@@ -0,0 +1,47 @@
+/*
+ * 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.data.repository;
+
+import android.view.accessibility.AccessibilityManager;
+
+/**
+ * This repository is used to call all APIs in {@link AccessibilityManager}
+ */
+public class AccessibilityRepository {
+
+ private final AccessibilityManager mAccessibilityManager;
+
+ public AccessibilityRepository(AccessibilityManager accessibilityManager) {
+ mAccessibilityManager = accessibilityManager;
+ }
+
+ /**
+ * Requests interruption of the accessibility feedback from all accessibility services.
+ */
+ public void interrupt() {
+ mAccessibilityManager.interrupt();
+ }
+
+ /**
+ * Returns if the {@link AccessibilityManager} is enabled.
+ *
+ * @return True if this {@link AccessibilityManager} is enabled, false otherwise.
+ */
+ public boolean isEnabled() {
+ return mAccessibilityManager.isEnabled();
+ }
+}
diff --git a/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java b/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java
index 64bf898..8f432e6 100644
--- a/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java
+++ b/src/com/android/settings/biometrics2/data/repository/FingerprintRepository.java
@@ -42,7 +42,8 @@
public class FingerprintRepository {
private static final String TAG = "FingerprintRepository";
- @NonNull private final FingerprintManager mFingerprintManager;
+ @NonNull
+ private final FingerprintManager mFingerprintManager;
private List<FingerprintSensorPropertiesInternal> mSensorPropertiesCache;
@@ -130,4 +131,18 @@
return RestrictedLockUtilsInternal.checkIfKeyguardFeaturesDisabled(
context, DevicePolicyManager.KEYGUARD_DISABLE_FINGERPRINT, userId) != null;
}
+
+ /**
+ * Get fingerprint enroll stage threshold
+ */
+ public float getEnrollStageThreshold(int index) {
+ return mFingerprintManager.getEnrollStageThreshold(index);
+ }
+
+ /**
+ * Get fingerprint enroll stage count
+ */
+ public int getEnrollStageCount() {
+ return mFingerprintManager.getEnrollStageCount();
+ }
}
diff --git a/src/com/android/settings/biometrics2/data/repository/PackageManagerRepository.java b/src/com/android/settings/biometrics2/data/repository/PackageManagerRepository.java
new file mode 100644
index 0000000..ae00221
--- /dev/null
+++ b/src/com/android/settings/biometrics2/data/repository/PackageManagerRepository.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.data.repository;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+
+/**
+ * This repository is used to call all APIs in {@link PackageManager}
+ */
+public class PackageManagerRepository {
+
+ private final PackageManager mPackageManager;
+
+ public PackageManagerRepository(PackageManager packageManager) {
+ mPackageManager = packageManager;
+ }
+
+ /**
+ * Set the enabled setting for a package component (activity, receiver, service, provider).
+ * This setting will override any enabled state which may have been set by the component in its
+ * manifest.
+ */
+ public void setComponentEnabledSetting(@NonNull ComponentName componentName,
+ @PackageManager.EnabledState int newState, @PackageManager.EnabledFlags int flags) {
+ mPackageManager.setComponentEnabledSetting(componentName, newState, flags);
+ }
+}
diff --git a/src/com/android/settings/biometrics2/data/repository/VibratorRepository.java b/src/com/android/settings/biometrics2/data/repository/VibratorRepository.java
new file mode 100644
index 0000000..cccafff
--- /dev/null
+++ b/src/com/android/settings/biometrics2/data/repository/VibratorRepository.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.biometrics2.data.repository;
+
+import android.annotation.NonNull;
+import android.os.VibrationAttributes;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+
+/**
+ * This repository is used to call all APIs in {@link Vibrator}
+ */
+public class VibratorRepository {
+
+ private final Vibrator mVibrator;
+
+ public VibratorRepository(Vibrator vibrator) {
+ mVibrator = vibrator;
+ }
+
+ /**
+ * Like {@link #vibrate(VibrationEffect, VibrationAttributes)}, but allows the
+ * caller to specify the vibration is owned by someone else and set a reason for vibration.
+ */
+ public void vibrate(int uid, String opPkg, @NonNull VibrationEffect vibe,
+ String reason, @NonNull VibrationAttributes attributes) {
+ mVibrator.vibrate(uid, opPkg, vibe, reason, attributes);
+ }
+}
diff --git a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java
index fdc5745..8e17ba4 100644
--- a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java
+++ b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProvider.java
@@ -21,7 +21,9 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.android.settings.biometrics2.data.repository.AccessibilityRepository;
import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.data.repository.VibratorRepository;
/**
* Interface for BiometricsRepositoryProvider
@@ -33,4 +35,16 @@
*/
@Nullable
FingerprintRepository getFingerprintRepository(@NonNull Application application);
+
+ /**
+ * Get VibtatorRepository
+ */
+ @Nullable
+ VibratorRepository getVibratorRepository(@NonNull Application application);
+
+ /**
+ * Get AccessibilityRepository
+ */
+ @Nullable
+ AccessibilityRepository getAccessibilityRepository(@NonNull Application application);
}
diff --git a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java
index 22409c8..7b1fe16 100644
--- a/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java
+++ b/src/com/android/settings/biometrics2/factory/BiometricsRepositoryProviderImpl.java
@@ -18,12 +18,16 @@
import android.app.Application;
import android.hardware.fingerprint.FingerprintManager;
+import android.os.Vibrator;
+import android.view.accessibility.AccessibilityManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.settings.Utils;
+import com.android.settings.biometrics2.data.repository.AccessibilityRepository;
import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.data.repository.VibratorRepository;
/**
* Implementation for BiometricsRepositoryProvider
@@ -31,6 +35,8 @@
public class BiometricsRepositoryProviderImpl implements BiometricsRepositoryProvider {
private static volatile FingerprintRepository sFingerprintRepository;
+ private static volatile VibratorRepository sVibratorRepository;
+ private static volatile AccessibilityRepository sAccessibilityRepository;
/**
* Get FingerprintRepository
@@ -52,4 +58,49 @@
}
return sFingerprintRepository;
}
+
+ /**
+ * Get VibratorRepository
+ */
+ @Nullable
+ @Override
+ public VibratorRepository getVibratorRepository(@NonNull Application application) {
+
+ final Vibrator vibrator = application.getSystemService(Vibrator.class);
+ if (vibrator == null) {
+ return null;
+ }
+
+ if (sVibratorRepository == null) {
+ synchronized (VibratorRepository.class) {
+ if (sVibratorRepository == null) {
+ sVibratorRepository = new VibratorRepository(vibrator);
+ }
+ }
+ }
+ return sVibratorRepository;
+ }
+
+ /**
+ * Get AccessibilityRepository
+ */
+ @Nullable
+ @Override
+ public AccessibilityRepository getAccessibilityRepository(@NonNull Application application) {
+
+ final AccessibilityManager accessibilityManager = application.getSystemService(
+ AccessibilityManager.class);
+ if (accessibilityManager == null) {
+ return null;
+ }
+
+ if (sAccessibilityRepository == null) {
+ synchronized (AccessibilityRepository.class) {
+ if (sAccessibilityRepository == null) {
+ sAccessibilityRepository = new AccessibilityRepository(accessibilityManager);
+ }
+ }
+ }
+ return sAccessibilityRepository;
+ }
}
diff --git a/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java b/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java
index 0b84f4c..7bf9d53 100644
--- a/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java
+++ b/src/com/android/settings/biometrics2/factory/BiometricsViewModelFactory.java
@@ -28,12 +28,15 @@
import com.android.internal.widget.LockPatternUtils;
import com.android.settings.biometrics.fingerprint.FingerprintUpdater;
+import com.android.settings.biometrics2.data.repository.AccessibilityRepository;
import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.data.repository.VibratorRepository;
import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
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.FingerprintEnrollEnrollingViewModel;
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFindSensorViewModel;
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollIntroViewModel;
import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel;
@@ -109,6 +112,16 @@
return (T) new FingerprintEnrollProgressViewModel(application,
new FingerprintUpdater(application), userId);
}
+ } else if (modelClass.isAssignableFrom(FingerprintEnrollEnrollingViewModel.class)) {
+ final FingerprintRepository fingerprint = provider.getFingerprintRepository(
+ application);
+ final AccessibilityRepository accessibility = provider.getAccessibilityRepository(
+ application);
+ final VibratorRepository vibrator = provider.getVibratorRepository(application);
+ if (fingerprint != null && accessibility != null && vibrator != null) {
+ return (T) new FingerprintEnrollEnrollingViewModel(application, fingerprint,
+ accessibility, vibrator);
+ }
}
return create(modelClass);
}
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingRfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingRfpsFragment.java
new file mode 100644
index 0000000..30b66a2
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingRfpsFragment.java
@@ -0,0 +1,272 @@
+/*
+ * 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.view;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Animatable2;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.settings.R;
+import com.android.settings.biometrics.BiometricUtils;
+import com.android.settings.biometrics2.ui.model.EnrollmentProgress;
+import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel;
+import com.android.settingslib.display.DisplayDensityUtils;
+
+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 is used to handle enrolling process for rfps
+ */
+public class FingerprintEnrollEnrollingRfpsFragment extends Fragment {
+
+ private static final String TAG = FingerprintEnrollEnrollingRfpsFragment.class.getSimpleName();
+
+ private static final long ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN = 500;
+ private static final int ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3;
+ private static final int HINT_TIMEOUT_DURATION = 2500;
+
+ private FingerprintEnrollEnrollingViewModel mEnrollingViewModel;
+ private DeviceRotationViewModel mRotationViewModel;
+ private FingerprintEnrollProgressViewModel mProgressViewModel;
+
+ private Interpolator mFastOutSlowInInterpolator;
+ private Interpolator mLinearOutSlowInInterpolator;
+ private Interpolator mFastOutLinearInInterpolator;
+ private boolean mAnimationCancelled;
+
+ private View mView;
+ private ProgressBar mProgressBar;
+ private TextView mErrorText;
+ private FooterBarMixin mFooterBarMixin;
+ private AnimatedVectorDrawable mIconAnimationDrawable;
+ private AnimatedVectorDrawable mIconBackgroundBlinksDrawable;
+
+ private LottieAnimationView mIllustrationLottie;
+ private boolean mShouldShowLottie;
+ private boolean mIsAccessibilityEnabled;
+
+ private boolean mHaveShownSfpsNoAnimationLottie;
+ private boolean mHaveShownSfpsCenterLottie;
+ private boolean mHaveShownSfpsTipLottie;
+ private boolean mHaveShownSfpsLeftEdgeLottie;
+ private boolean mHaveShownSfpsRightEdgeLottie;
+
+ private final View.OnClickListener mOnSkipClickListener =
+ (v) -> mEnrollingViewModel.onSkipButtonClick();
+
+ private int mIconTouchCount;
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ final FragmentActivity activity = getActivity();
+ final ViewModelProvider provider = new ViewModelProvider(activity);
+ mEnrollingViewModel = provider.get(FingerprintEnrollEnrollingViewModel.class);
+ mRotationViewModel = provider.get(DeviceRotationViewModel.class);
+ mProgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class);
+ super.onAttach(context);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mIsAccessibilityEnabled = mEnrollingViewModel.isAccessibilityEnabled();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mView = initRfpsLayout(inflater, container);
+ return mView;
+ }
+
+ private View initRfpsLayout(LayoutInflater inflater, ViewGroup container) {
+ final View containView = inflater.inflate(R.layout.sfps_enroll_enrolling, container, false);
+ final Activity activity = getActivity();
+ final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity,
+ (GlifLayout) containView);
+ glifLayoutHelper.setDescriptionText(
+ R.string.security_settings_fingerprint_enroll_start_message);
+ glifLayoutHelper.setHeaderText(R.string.security_settings_fingerprint_enroll_repeat_title);
+
+ mShouldShowLottie = shouldShowLottie();
+ boolean isLandscape = BiometricUtils.isReverseLandscape(activity)
+ || BiometricUtils.isLandscape(activity);
+ updateOrientation((isLandscape
+ ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT));
+
+ mErrorText = containView.findViewById(R.id.error_text);
+ mProgressBar = containView.findViewById(R.id.fingerprint_progress_bar);
+ mFooterBarMixin = ((GlifLayout) containView).getMixin(FooterBarMixin.class);
+ mFooterBarMixin.setSecondaryButton(
+ new FooterButton.Builder(activity)
+ .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
+ .setListener(mOnSkipClickListener)
+ .setButtonType(FooterButton.ButtonType.SKIP)
+ .setTheme(R.style.SudGlifButton_Secondary)
+ .build()
+ );
+
+ final LayerDrawable fingerprintDrawable = mProgressBar != null
+ ? (LayerDrawable) mProgressBar.getBackground() : null;
+ if (fingerprintDrawable != null) {
+ mIconAnimationDrawable = (AnimatedVectorDrawable)
+ fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_animation);
+ mIconBackgroundBlinksDrawable = (AnimatedVectorDrawable)
+ fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_background);
+ mIconAnimationDrawable.registerAnimationCallback(mIconAnimationCallback);
+ }
+
+ mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
+ activity, android.R.interpolator.fast_out_slow_in);
+ mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
+ activity, android.R.interpolator.linear_out_slow_in);
+ mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
+ activity, android.R.interpolator.fast_out_linear_in);
+
+ if (mProgressBar != null) {
+ mProgressBar.setProgressBackgroundTintMode(PorterDuff.Mode.SRC);
+ mProgressBar.setOnTouchListener((v, event) -> {
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mIconTouchCount++;
+ if (mIconTouchCount == ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN) {
+ showIconTouchDialog();
+ } else {
+ mProgressBar.postDelayed(mShowDialogRunnable,
+ ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN);
+ }
+ } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
+ || event.getActionMasked() == MotionEvent.ACTION_UP) {
+ mProgressBar.removeCallbacks(mShowDialogRunnable);
+ }
+ return true;
+ });
+ }
+
+ return containView;
+ }
+
+ private void updateOrientation(int orientation) {
+ switch (orientation) {
+ case Configuration.ORIENTATION_LANDSCAPE: {
+ mIllustrationLottie = null;
+ break;
+ }
+ case Configuration.ORIENTATION_PORTRAIT: {
+ if (mShouldShowLottie) {
+ mIllustrationLottie = mView.findViewById(R.id.illustration_lottie);
+ }
+ break;
+ }
+ default:
+ Log.e(TAG, "Error unhandled configuration change");
+ break;
+ }
+ }
+
+ private void updateTitleAndDescription() {
+ final Activity activity = getActivity();
+ final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity,
+ (GlifLayout) mView);
+
+ EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue();
+ if (progressLiveData == null || progressLiveData.getSteps() == -1) {
+ glifLayoutHelper.setDescriptionText(
+ R.string.security_settings_fingerprint_enroll_start_message);
+ } else {
+ glifLayoutHelper.setDescriptionText(
+ R.string.security_settings_fingerprint_enroll_repeat_message);
+ }
+ }
+
+ private void startIconAnimation() {
+ if (mIconAnimationDrawable != null) {
+ mIconAnimationDrawable.start();
+ }
+ }
+
+ private void stopIconAnimation() {
+ mAnimationCancelled = true;
+ if (mIconAnimationDrawable != null) {
+ mIconAnimationDrawable.stop();
+ }
+ }
+
+ private void showIconTouchDialog() {
+ mIconTouchCount = 0;
+ //TODO EnrollingActivity should observe live data and add dialog fragment
+ mEnrollingViewModel.onIconTouchDialogShow();
+ }
+
+ private boolean shouldShowLottie() {
+ DisplayDensityUtils displayDensity = new DisplayDensityUtils(getContext());
+ int currentDensityIndex = displayDensity.getCurrentIndexForDefaultDisplay();
+ final int currentDensity = displayDensity.getDefaultDisplayDensityValues()
+ [currentDensityIndex];
+ final int defaultDensity = displayDensity.getDefaultDensityForDefaultDisplay();
+ return defaultDensity == currentDensity;
+ }
+
+ private final Runnable mShowDialogRunnable = new Runnable() {
+ @Override
+ public void run() {
+ showIconTouchDialog();
+ }
+ };
+
+ private final Animatable2.AnimationCallback mIconAnimationCallback =
+ new Animatable2.AnimationCallback() {
+ @Override
+ public void onAnimationEnd(Drawable d) {
+ if (mAnimationCancelled) {
+ return;
+ }
+
+ // Start animation after it has ended.
+ mProgressBar.post(new Runnable() {
+ @Override
+ public void run() {
+ startIconAnimation();
+ }
+ });
+ }
+ };
+}
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingSfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingSfpsFragment.java
new file mode 100644
index 0000000..ddeb465
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingSfpsFragment.java
@@ -0,0 +1,410 @@
+/*
+ * 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.view;
+
+import android.annotation.RawRes;
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Animatable2;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.settings.R;
+import com.android.settings.biometrics.BiometricUtils;
+import com.android.settings.biometrics2.ui.model.EnrollmentProgress;
+import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel;
+import com.android.settingslib.display.DisplayDensityUtils;
+
+import com.airbnb.lottie.LottieAnimationView;
+import com.airbnb.lottie.LottieCompositionFactory;
+import com.google.android.setupcompat.template.FooterBarMixin;
+import com.google.android.setupcompat.template.FooterButton;
+import com.google.android.setupdesign.GlifLayout;
+import com.google.android.setupdesign.template.DescriptionMixin;
+import com.google.android.setupdesign.template.HeaderMixin;
+
+/**
+ * Fragment is used to handle enrolling process for sfps
+ */
+public class FingerprintEnrollEnrollingSfpsFragment extends Fragment {
+
+ private static final String TAG = FingerprintEnrollEnrollingSfpsFragment.class.getSimpleName();
+
+ private static final long ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN = 500;
+ private static final int ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3;
+ private static final int HINT_TIMEOUT_DURATION = 2500;
+
+ private static final int STAGE_UNKNOWN = -1;
+ private static final int SFPS_STAGE_NO_ANIMATION = 0;
+ private static final int SFPS_STAGE_CENTER = 1;
+ private static final int SFPS_STAGE_FINGERTIP = 2;
+ private static final int SFPS_STAGE_LEFT_EDGE = 3;
+ private static final int SFPS_STAGE_RIGHT_EDGE = 4;
+
+ private FingerprintEnrollEnrollingViewModel mEnrollingViewModel;
+ private DeviceRotationViewModel mRotationViewModel;
+ private FingerprintEnrollProgressViewModel mProgressViewModel;
+
+ private Interpolator mFastOutSlowInInterpolator;
+ private Interpolator mLinearOutSlowInInterpolator;
+ private Interpolator mFastOutLinearInInterpolator;
+ private boolean mAnimationCancelled;
+
+ private View mView;
+ private ProgressBar mProgressBar;
+ private TextView mErrorText;
+ private FooterBarMixin mFooterBarMixin;
+ private AnimatedVectorDrawable mIconAnimationDrawable;
+ private AnimatedVectorDrawable mIconBackgroundBlinksDrawable;
+
+ private LottieAnimationView mIllustrationLottie;
+ private boolean mShouldShowLottie;
+ private boolean mIsAccessibilityEnabled;
+
+ private boolean mHaveShownSfpsNoAnimationLottie;
+ private boolean mHaveShownSfpsCenterLottie;
+ private boolean mHaveShownSfpsTipLottie;
+ private boolean mHaveShownSfpsLeftEdgeLottie;
+ private boolean mHaveShownSfpsRightEdgeLottie;
+
+ private final View.OnClickListener mOnSkipClickListener =
+ (v) -> mEnrollingViewModel.onSkipButtonClick();
+
+ private int mIconTouchCount;
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ final FragmentActivity activity = getActivity();
+ final ViewModelProvider provider = new ViewModelProvider(activity);
+ mEnrollingViewModel = provider.get(FingerprintEnrollEnrollingViewModel.class);
+ mRotationViewModel = provider.get(DeviceRotationViewModel.class);
+ mProgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class);
+ super.onAttach(context);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mIsAccessibilityEnabled = mEnrollingViewModel.isAccessibilityEnabled();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mView = initSfpsLayout(inflater, container);
+ final Configuration config = getActivity().getResources().getConfiguration();
+ maybeHideSfpsText(config);
+ return mView;
+ }
+
+ private View initSfpsLayout(LayoutInflater inflater, ViewGroup container) {
+ final View containView = inflater.inflate(R.layout.sfps_enroll_enrolling, container, false);
+ final Activity activity = getActivity();
+ final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity,
+ (GlifLayout) containView);
+ glifLayoutHelper.setDescriptionText(
+ R.string.security_settings_fingerprint_enroll_start_message);
+ updateTitleAndDescription();
+
+ mShouldShowLottie = shouldShowLottie();
+ boolean isLandscape = BiometricUtils.isReverseLandscape(activity)
+ || BiometricUtils.isLandscape(activity);
+ updateOrientation((isLandscape
+ ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT));
+
+ mErrorText = containView.findViewById(R.id.error_text);
+ mProgressBar = containView.findViewById(R.id.fingerprint_progress_bar);
+ mFooterBarMixin = ((GlifLayout) containView).getMixin(FooterBarMixin.class);
+ mFooterBarMixin.setSecondaryButton(
+ new FooterButton.Builder(activity)
+ .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
+ .setListener(mOnSkipClickListener)
+ .setButtonType(FooterButton.ButtonType.SKIP)
+ .setTheme(R.style.SudGlifButton_Secondary)
+ .build()
+ );
+
+ final LayerDrawable fingerprintDrawable = mProgressBar != null
+ ? (LayerDrawable) mProgressBar.getBackground() : null;
+ if (fingerprintDrawable != null) {
+ mIconAnimationDrawable = (AnimatedVectorDrawable)
+ fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_animation);
+ mIconBackgroundBlinksDrawable = (AnimatedVectorDrawable)
+ fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_background);
+ mIconAnimationDrawable.registerAnimationCallback(mIconAnimationCallback);
+ }
+
+ mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
+ activity, android.R.interpolator.fast_out_slow_in);
+ mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
+ activity, android.R.interpolator.linear_out_slow_in);
+ mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
+ activity, android.R.interpolator.fast_out_linear_in);
+
+ if (mProgressBar != null) {
+ mProgressBar.setProgressBackgroundTintMode(PorterDuff.Mode.SRC);
+ mProgressBar.setOnTouchListener((v, event) -> {
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mIconTouchCount++;
+ if (mIconTouchCount == ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN) {
+ showIconTouchDialog();
+ } else {
+ mProgressBar.postDelayed(mShowDialogRunnable,
+ ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN);
+ }
+ } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
+ || event.getActionMasked() == MotionEvent.ACTION_UP) {
+ mProgressBar.removeCallbacks(mShowDialogRunnable);
+ }
+ return true;
+ });
+ }
+
+ return containView;
+ }
+
+ private void updateTitleAndDescription() {
+
+ final Activity activity = getActivity();
+ final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity,
+ (GlifLayout) mView);
+
+ if (mIsAccessibilityEnabled) {
+ mEnrollingViewModel.clearTalkback();
+ ((GlifLayout) mView).getDescriptionTextView().setAccessibilityLiveRegion(
+ View.ACCESSIBILITY_LIVE_REGION_POLITE);
+ }
+ switch (getCurrentSfpsStage()) {
+ case SFPS_STAGE_NO_ANIMATION:
+ glifLayoutHelper.setHeaderText(
+ R.string.security_settings_fingerprint_enroll_repeat_title);
+ if (!mHaveShownSfpsNoAnimationLottie && mIllustrationLottie != null) {
+ mHaveShownSfpsNoAnimationLottie = true;
+ mIllustrationLottie.setContentDescription(
+ getString(
+ R.string.security_settings_sfps_animation_a11y_label,
+ 0
+ )
+ );
+ configureEnrollmentStage(
+ getString(R.string.security_settings_sfps_enroll_start_message),
+ R.raw.sfps_lottie_no_animation
+ );
+ }
+ break;
+
+ case SFPS_STAGE_CENTER:
+ glifLayoutHelper.setHeaderText(
+ R.string.security_settings_sfps_enroll_finger_center_title);
+ if (!mHaveShownSfpsCenterLottie && mIllustrationLottie != null) {
+ mHaveShownSfpsCenterLottie = true;
+ configureEnrollmentStage(
+ getString(R.string.security_settings_sfps_enroll_start_message),
+ R.raw.sfps_lottie_pad_center
+ );
+ }
+ break;
+
+ case SFPS_STAGE_FINGERTIP:
+ glifLayoutHelper.setHeaderText(
+ R.string.security_settings_sfps_enroll_fingertip_title);
+ if (!mHaveShownSfpsTipLottie && mIllustrationLottie != null) {
+ mHaveShownSfpsTipLottie = true;
+ configureEnrollmentStage("", R.raw.sfps_lottie_tip);
+ }
+ break;
+
+ case SFPS_STAGE_LEFT_EDGE:
+ glifLayoutHelper.setHeaderText(
+ R.string.security_settings_sfps_enroll_left_edge_title);
+ if (!mHaveShownSfpsLeftEdgeLottie && mIllustrationLottie != null) {
+ mHaveShownSfpsLeftEdgeLottie = true;
+ configureEnrollmentStage("", R.raw.sfps_lottie_left_edge);
+ }
+ break;
+
+ case SFPS_STAGE_RIGHT_EDGE:
+ glifLayoutHelper.setHeaderText(
+ R.string.security_settings_sfps_enroll_right_edge_title);
+ if (!mHaveShownSfpsRightEdgeLottie && mIllustrationLottie != null) {
+ mHaveShownSfpsRightEdgeLottie = true;
+ configureEnrollmentStage("", R.raw.sfps_lottie_right_edge);
+ }
+ break;
+
+ case STAGE_UNKNOWN:
+ default:
+ // Don't use BiometricEnrollBase#setHeaderText, since that invokes setTitle,
+ // which gets announced for a11y upon entering the page. For SFPS, we want to
+ // announce a different string for a11y upon entering the page.
+ glifLayoutHelper.setHeaderText(
+ R.string.security_settings_sfps_enroll_find_sensor_title);
+ glifLayoutHelper.setDescriptionText(
+ R.string.security_settings_sfps_enroll_start_message);
+ final CharSequence description = getString(
+ R.string.security_settings_sfps_enroll_find_sensor_message);
+ ((GlifLayout) mView).getHeaderTextView().setContentDescription(description);
+ activity.setTitle(description);
+ break;
+
+ }
+ }
+
+ private void maybeHideSfpsText(@android.annotation.NonNull Configuration newConfig) {
+ final HeaderMixin headerMixin = ((GlifLayout) mView).getMixin(HeaderMixin.class);
+ final DescriptionMixin descriptionMixin = ((GlifLayout) mView).getMixin(
+ DescriptionMixin.class);
+ final boolean isLandscape = newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE;
+
+ if (isLandscape) {
+ headerMixin.setAutoTextSizeEnabled(true);
+ headerMixin.getTextView().setMinLines(0);
+ headerMixin.getTextView().setMaxLines(10);
+ descriptionMixin.getTextView().setMinLines(0);
+ descriptionMixin.getTextView().setMaxLines(10);
+ } else {
+ headerMixin.setAutoTextSizeEnabled(false);
+ headerMixin.getTextView().setLines(4);
+ // hide the description
+ descriptionMixin.getTextView().setLines(0);
+ }
+
+ }
+
+ private int getCurrentSfpsStage() {
+ EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue();
+
+ if (progressLiveData == null || progressLiveData.getSteps() == -1) {
+ return STAGE_UNKNOWN;
+ }
+
+ final int progressSteps = progressLiveData.getSteps() - progressLiveData.getRemaining();
+ if (progressSteps < getStageThresholdSteps(0)) {
+ return SFPS_STAGE_NO_ANIMATION;
+ } else if (progressSteps < getStageThresholdSteps(1)) {
+ return SFPS_STAGE_CENTER;
+ } else if (progressSteps < getStageThresholdSteps(2)) {
+ return SFPS_STAGE_FINGERTIP;
+ } else if (progressSteps < getStageThresholdSteps(3)) {
+ return SFPS_STAGE_LEFT_EDGE;
+ } else {
+ return SFPS_STAGE_RIGHT_EDGE;
+ }
+ }
+
+ private int getStageThresholdSteps(int index) {
+
+ EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue();
+
+ if (progressLiveData == null || progressLiveData.getSteps() == -1) {
+ Log.w(TAG, "getStageThresholdSteps: Enrollment not started yet");
+ return 1;
+ }
+ return Math.round(progressLiveData.getSteps()
+ * mEnrollingViewModel.getEnrollStageThreshold(index));
+ }
+
+ private void updateOrientation(int orientation) {
+ mIllustrationLottie = mView.findViewById(R.id.illustration_lottie);
+ }
+
+ private boolean shouldShowLottie() {
+ DisplayDensityUtils displayDensity = new DisplayDensityUtils(getContext());
+ int currentDensityIndex = displayDensity.getCurrentIndexForDefaultDisplay();
+ final int currentDensity = displayDensity.getDefaultDisplayDensityValues()
+ [currentDensityIndex];
+ final int defaultDensity = displayDensity.getDefaultDensityForDefaultDisplay();
+ return defaultDensity == currentDensity;
+ }
+
+
+ private void startIconAnimation() {
+ if (mIconAnimationDrawable != null) {
+ mIconAnimationDrawable.start();
+ }
+ }
+
+ private void stopIconAnimation() {
+ mAnimationCancelled = true;
+ if (mIconAnimationDrawable != null) {
+ mIconAnimationDrawable.stop();
+ }
+ }
+
+ private void showIconTouchDialog() {
+ mIconTouchCount = 0;
+ //TODO EnrollingActivity should observe live data and add dialog fragment
+ mEnrollingViewModel.onIconTouchDialogShow();
+ }
+
+ private void configureEnrollmentStage(CharSequence description, @RawRes int lottie) {
+ final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(getActivity(),
+ (GlifLayout) mView);
+ glifLayoutHelper.setDescriptionText(description);
+ LottieCompositionFactory.fromRawRes(getActivity(), lottie)
+ .addListener((c) -> {
+ mIllustrationLottie.setComposition(c);
+ mIllustrationLottie.setVisibility(View.VISIBLE);
+ mIllustrationLottie.playAnimation();
+ });
+ }
+
+ private final Runnable mShowDialogRunnable = new Runnable() {
+ @Override
+ public void run() {
+ showIconTouchDialog();
+ }
+ };
+
+ private final Animatable2.AnimationCallback mIconAnimationCallback =
+ new Animatable2.AnimationCallback() {
+ @Override
+ public void onAnimationEnd(Drawable d) {
+ if (mAnimationCancelled) {
+ return;
+ }
+
+ // Start animation after it has ended.
+ mProgressBar.post(new Runnable() {
+ @Override
+ public void run() {
+ startIconAnimation();
+ }
+ });
+ }
+ };
+}
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingUdfpsFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingUdfpsFragment.java
new file mode 100644
index 0000000..89b061f
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollEnrollingUdfpsFragment.java
@@ -0,0 +1,470 @@
+/*
+ * 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.view;
+
+import android.annotation.RawRes;
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Animatable2;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.Surface;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.settings.R;
+import com.android.settings.biometrics.BiometricUtils;
+import com.android.settings.biometrics2.ui.model.EnrollmentProgress;
+import com.android.settings.biometrics2.ui.viewmodel.DeviceRotationViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollEnrollingViewModel;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollProgressViewModel;
+import com.android.settingslib.display.DisplayDensityUtils;
+
+import com.airbnb.lottie.LottieAnimationView;
+import com.airbnb.lottie.LottieCompositionFactory;
+import com.google.android.setupcompat.template.FooterBarMixin;
+import com.google.android.setupcompat.template.FooterButton;
+import com.google.android.setupdesign.GlifLayout;
+
+import java.util.Locale;
+
+/**
+ * Fragment is used to handle enrolling process for udfps
+ */
+public class FingerprintEnrollEnrollingUdfpsFragment extends Fragment {
+
+ private static final String TAG = FingerprintEnrollEnrollingUdfpsFragment.class.getSimpleName();
+
+ private static final long ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN = 500;
+ private static final int ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN = 3;
+ private static final int HINT_TIMEOUT_DURATION = 2500;
+
+ private static final int STAGE_UNKNOWN = -1;
+ private static final int STAGE_CENTER = 0;
+ private static final int STAGE_GUIDED = 1;
+ private static final int STAGE_FINGERTIP = 2;
+ private static final int STAGE_LEFT_EDGE = 3;
+ private static final int STAGE_RIGHT_EDGE = 4;
+
+ private FingerprintEnrollEnrollingViewModel mEnrollingViewModel;
+ private DeviceRotationViewModel mRotationViewModel;
+ private FingerprintEnrollProgressViewModel mProgressViewModel;
+
+ private Interpolator mFastOutSlowInInterpolator;
+ private Interpolator mLinearOutSlowInInterpolator;
+ private Interpolator mFastOutLinearInInterpolator;
+ private boolean mAnimationCancelled;
+
+ private LottieAnimationView mIllustrationLottie;
+ private boolean mHaveShownUdfpsTipLottie;
+ private boolean mHaveShownUdfpsLeftEdgeLottie;
+ private boolean mHaveShownUdfpsRightEdgeLottie;
+ private boolean mHaveShownUdfpsCenterLottie;
+ private boolean mHaveShownUdfpsGuideLottie;
+
+ private View mView;
+ private ProgressBar mProgressBar;
+ private TextView mErrorText;
+ private FooterBarMixin mFooterBarMixin;
+ private AnimatedVectorDrawable mIconAnimationDrawable;
+ private AnimatedVectorDrawable mIconBackgroundBlinksDrawable;
+
+ private boolean mShouldShowLottie;
+ private boolean mIsAccessibilityEnabled;
+
+ private final View.OnClickListener mOnSkipClickListener =
+ (v) -> mEnrollingViewModel.onSkipButtonClick();
+
+ private int mIconTouchCount;
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ final FragmentActivity activity = getActivity();
+ final ViewModelProvider provider = new ViewModelProvider(activity);
+ mEnrollingViewModel = provider.get(FingerprintEnrollEnrollingViewModel.class);
+ mRotationViewModel = provider.get(DeviceRotationViewModel.class);
+ mProgressViewModel = provider.get(FingerprintEnrollProgressViewModel.class);
+ super.onAttach(context);
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mIsAccessibilityEnabled = mEnrollingViewModel.isAccessibilityEnabled();
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mView = initUdfpsLayout(inflater, container);
+ return mView;
+ }
+
+ private View initUdfpsLayout(LayoutInflater inflater, ViewGroup container) {
+ final View containView = inflater.inflate(R.layout.udfps_enroll_enrolling, container,
+ false);
+
+ final Activity activity = getActivity();
+ final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity,
+ (GlifLayout) containView);
+ final int rotation = mRotationViewModel.getLiveData().getValue();
+ final boolean isLayoutRtl = (TextUtils.getLayoutDirectionFromLocale(
+ Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL);
+
+
+ //TODO implement b/20653554
+ if (rotation == Surface.ROTATION_90) {
+ final LinearLayout layoutContainer = containView.findViewById(
+ R.id.layout_container);
+ final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.MATCH_PARENT);
+ lp.setMarginEnd((int) getResources().getDimension(
+ R.dimen.rotation_90_enroll_margin_end));
+ layoutContainer.setPaddingRelative((int) getResources().getDimension(
+ R.dimen.rotation_90_enroll_padding_start), 0, isLayoutRtl
+ ? 0 : (int) getResources().getDimension(
+ R.dimen.rotation_90_enroll_padding_end), 0);
+ layoutContainer.setLayoutParams(lp);
+ containView.setLayoutParams(lp);
+ }
+ glifLayoutHelper.setDescriptionText(R.string.security_settings_udfps_enroll_start_message);
+ updateTitleAndDescription();
+
+ mShouldShowLottie = shouldShowLottie();
+ boolean isLandscape = BiometricUtils.isReverseLandscape(activity)
+ || BiometricUtils.isLandscape(activity);
+ updateOrientation((isLandscape
+ ? Configuration.ORIENTATION_LANDSCAPE : Configuration.ORIENTATION_PORTRAIT));
+
+ mErrorText = containView.findViewById(R.id.error_text);
+ mProgressBar = containView.findViewById(R.id.fingerprint_progress_bar);
+ mFooterBarMixin = ((GlifLayout) containView).getMixin(FooterBarMixin.class);
+ mFooterBarMixin.setSecondaryButton(
+ new FooterButton.Builder(activity)
+ .setText(R.string.security_settings_fingerprint_enroll_enrolling_skip)
+ .setListener(mOnSkipClickListener)
+ .setButtonType(FooterButton.ButtonType.SKIP)
+ .setTheme(R.style.SudGlifButton_Secondary)
+ .build()
+ );
+
+ final LayerDrawable fingerprintDrawable = mProgressBar != null
+ ? (LayerDrawable) mProgressBar.getBackground() : null;
+ if (fingerprintDrawable != null) {
+ mIconAnimationDrawable = (AnimatedVectorDrawable)
+ fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_animation);
+ mIconBackgroundBlinksDrawable = (AnimatedVectorDrawable)
+ fingerprintDrawable.findDrawableByLayerId(R.id.fingerprint_background);
+ mIconAnimationDrawable.registerAnimationCallback(mIconAnimationCallback);
+ }
+
+ mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
+ activity, android.R.interpolator.fast_out_slow_in);
+ mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
+ activity, android.R.interpolator.linear_out_slow_in);
+ mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(
+ activity, android.R.interpolator.fast_out_linear_in);
+
+ if (mProgressBar != null) {
+ mProgressBar.setProgressBackgroundTintMode(PorterDuff.Mode.SRC);
+ mProgressBar.setOnTouchListener((v, event) -> {
+ if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+ mIconTouchCount++;
+ if (mIconTouchCount == ICON_TOUCH_COUNT_SHOW_UNTIL_DIALOG_SHOWN) {
+ showIconTouchDialog();
+ } else {
+ mProgressBar.postDelayed(mShowDialogRunnable,
+ ICON_TOUCH_DURATION_UNTIL_DIALOG_SHOWN);
+ }
+ } else if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
+ || event.getActionMasked() == MotionEvent.ACTION_UP) {
+ mProgressBar.removeCallbacks(mShowDialogRunnable);
+ }
+ return true;
+ });
+ }
+
+ return containView;
+ }
+
+ private void updateTitleAndDescription() {
+
+ final Activity activity = getActivity();
+ final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity,
+ (GlifLayout) mView);
+
+ switch (getCurrentStage()) {
+ case STAGE_CENTER:
+ glifLayoutHelper.setHeaderText(
+ R.string.security_settings_fingerprint_enroll_repeat_title);
+ if (mIsAccessibilityEnabled || mIllustrationLottie == null) {
+ glifLayoutHelper.setDescriptionText(
+ R.string.security_settings_udfps_enroll_start_message);
+ } else if (!mHaveShownUdfpsCenterLottie && mIllustrationLottie != null) {
+ mHaveShownUdfpsCenterLottie = true;
+ // Note: Update string reference when differentiate in between udfps & sfps
+ mIllustrationLottie.setContentDescription(
+ getString(R.string.security_settings_sfps_enroll_finger_center_title)
+ );
+ configureEnrollmentStage("", R.raw.udfps_center_hint_lottie);
+ }
+ break;
+
+ case STAGE_GUIDED:
+ glifLayoutHelper.setHeaderText(
+ R.string.security_settings_fingerprint_enroll_repeat_title);
+ if (mIsAccessibilityEnabled || mIllustrationLottie == null) {
+ glifLayoutHelper.setDescriptionText(
+ R.string.security_settings_udfps_enroll_repeat_a11y_message);
+ } else if (!mHaveShownUdfpsGuideLottie && mIllustrationLottie != null) {
+ mHaveShownUdfpsGuideLottie = true;
+ mIllustrationLottie.setContentDescription(
+ getString(R.string.security_settings_fingerprint_enroll_repeat_message)
+ );
+ // TODO(b/228100413) Could customize guided lottie animation
+ configureEnrollmentStage("", R.raw.udfps_center_hint_lottie);
+ }
+ break;
+ case STAGE_FINGERTIP:
+ glifLayoutHelper.setHeaderText(
+ R.string.security_settings_udfps_enroll_fingertip_title);
+ if (!mHaveShownUdfpsTipLottie && mIllustrationLottie != null) {
+ mHaveShownUdfpsTipLottie = true;
+ mIllustrationLottie.setContentDescription(
+ getString(R.string.security_settings_udfps_tip_fingerprint_help)
+ );
+ configureEnrollmentStage("", R.raw.udfps_tip_hint_lottie);
+ }
+ break;
+ case STAGE_LEFT_EDGE:
+ glifLayoutHelper.setHeaderText(
+ R.string.security_settings_udfps_enroll_left_edge_title);
+ if (!mHaveShownUdfpsLeftEdgeLottie && mIllustrationLottie != null) {
+ mHaveShownUdfpsLeftEdgeLottie = true;
+ mIllustrationLottie.setContentDescription(
+ getString(R.string.security_settings_udfps_side_fingerprint_help)
+ );
+ configureEnrollmentStage("", R.raw.udfps_left_edge_hint_lottie);
+ } else if (mIllustrationLottie == null) {
+ if (isStageHalfCompleted()) {
+ glifLayoutHelper.setDescriptionText(
+ R.string.security_settings_fingerprint_enroll_repeat_message);
+ } else {
+ glifLayoutHelper.setDescriptionText(
+ R.string.security_settings_udfps_enroll_edge_message);
+ }
+ }
+ break;
+ case STAGE_RIGHT_EDGE:
+ glifLayoutHelper.setHeaderText(
+ R.string.security_settings_udfps_enroll_right_edge_title);
+ if (!mHaveShownUdfpsRightEdgeLottie && mIllustrationLottie != null) {
+ mHaveShownUdfpsRightEdgeLottie = true;
+ mIllustrationLottie.setContentDescription(
+ getString(R.string.security_settings_udfps_side_fingerprint_help)
+ );
+ configureEnrollmentStage("", R.raw.udfps_right_edge_hint_lottie);
+
+ } else if (mIllustrationLottie == null) {
+ if (isStageHalfCompleted()) {
+ glifLayoutHelper.setDescriptionText(
+ R.string.security_settings_fingerprint_enroll_repeat_message);
+ } else {
+ glifLayoutHelper.setDescriptionText(
+ R.string.security_settings_udfps_enroll_edge_message);
+ }
+ }
+ break;
+
+ case STAGE_UNKNOWN:
+ default:
+ // setHeaderText(R.string.security_settings_fingerprint_enroll_udfps_title);
+ // Don't use BiometricEnrollBase#setHeaderText, since that invokes setTitle,
+ // which gets announced for a11y upon entering the page. For UDFPS, we want to
+ // announce a different string for a11y upon entering the page.
+ glifLayoutHelper.setHeaderText(
+ R.string.security_settings_fingerprint_enroll_udfps_title);
+ glifLayoutHelper.setDescriptionText(
+ R.string.security_settings_udfps_enroll_start_message);
+ final CharSequence description = getString(
+ R.string.security_settings_udfps_enroll_a11y);
+ ((GlifLayout) mView).getHeaderTextView().setContentDescription(description);
+ activity.setTitle(description);
+ break;
+
+ }
+ }
+
+ private boolean shouldShowLottie() {
+ DisplayDensityUtils displayDensity = new DisplayDensityUtils(getContext());
+ int currentDensityIndex = displayDensity.getCurrentIndexForDefaultDisplay();
+ final int currentDensity = displayDensity.getDefaultDisplayDensityValues()
+ [currentDensityIndex];
+ final int defaultDensity = displayDensity.getDefaultDensityForDefaultDisplay();
+ return defaultDensity == currentDensity;
+ }
+
+ private void updateOrientation(int orientation) {
+ switch (orientation) {
+ case Configuration.ORIENTATION_LANDSCAPE: {
+ mIllustrationLottie = null;
+ break;
+ }
+ case Configuration.ORIENTATION_PORTRAIT: {
+ if (mShouldShowLottie) {
+ mIllustrationLottie = mView.findViewById(R.id.illustration_lottie);
+ }
+ break;
+ }
+ default:
+ Log.e(TAG, "Error unhandled configuration change");
+ break;
+ }
+ }
+
+ private void startIconAnimation() {
+ if (mIconAnimationDrawable != null) {
+ mIconAnimationDrawable.start();
+ }
+ }
+
+ private void stopIconAnimation() {
+ mAnimationCancelled = true;
+ if (mIconAnimationDrawable != null) {
+ mIconAnimationDrawable.stop();
+ }
+ }
+
+ private int getCurrentStage() {
+ EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue();
+
+ if (progressLiveData == null || progressLiveData.getSteps() == -1) {
+ return STAGE_UNKNOWN;
+ }
+
+ final int progressSteps = progressLiveData.getSteps() - progressLiveData.getRemaining();
+ if (progressSteps < getStageThresholdSteps(0)) {
+ return STAGE_CENTER;
+ } else if (progressSteps < getStageThresholdSteps(1)) {
+ return STAGE_GUIDED;
+ } else if (progressSteps < getStageThresholdSteps(2)) {
+ return STAGE_FINGERTIP;
+ } else if (progressSteps < getStageThresholdSteps(3)) {
+ return STAGE_LEFT_EDGE;
+ } else {
+ return STAGE_RIGHT_EDGE;
+ }
+ }
+
+ private boolean isStageHalfCompleted() {
+
+ EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue();
+ if (progressLiveData == null || progressLiveData.getSteps() == -1) {
+ return false;
+ }
+
+ final int progressSteps = progressLiveData.getSteps() - progressLiveData.getRemaining();
+ int prevThresholdSteps = 0;
+ for (int i = 0; i < mEnrollingViewModel.getEnrollStageCount(); i++) {
+ final int thresholdSteps = getStageThresholdSteps(i);
+ if (progressSteps >= prevThresholdSteps && progressSteps < thresholdSteps) {
+ final int adjustedProgress = progressSteps - prevThresholdSteps;
+ final int adjustedThreshold = thresholdSteps - prevThresholdSteps;
+ return adjustedProgress >= adjustedThreshold / 2;
+ }
+ prevThresholdSteps = thresholdSteps;
+ }
+
+ // After last enrollment step.
+ return true;
+ }
+
+ private int getStageThresholdSteps(int index) {
+
+ EnrollmentProgress progressLiveData = mProgressViewModel.getProgressLiveData().getValue();
+
+ if (progressLiveData == null || progressLiveData.getSteps() == -1) {
+ Log.w(TAG, "getStageThresholdSteps: Enrollment not started yet");
+ return 1;
+ }
+ return Math.round(progressLiveData.getSteps()
+ * mEnrollingViewModel.getEnrollStageThreshold(index));
+ }
+
+ private void showIconTouchDialog() {
+ mIconTouchCount = 0;
+ //TODO EnrollingActivity should observe live data and add dialog fragment
+ mEnrollingViewModel.onIconTouchDialogShow();
+ }
+
+ private void configureEnrollmentStage(CharSequence description, @RawRes int lottie) {
+ final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(getActivity(),
+ (GlifLayout) mView);
+ glifLayoutHelper.setDescriptionText(description);
+ LottieCompositionFactory.fromRawRes(getActivity(), lottie)
+ .addListener((c) -> {
+ mIllustrationLottie.setComposition(c);
+ mIllustrationLottie.setVisibility(View.VISIBLE);
+ mIllustrationLottie.playAnimation();
+ });
+ }
+
+ private final Runnable mShowDialogRunnable = new Runnable() {
+ @Override
+ public void run() {
+ showIconTouchDialog();
+ }
+ };
+
+ private final Animatable2.AnimationCallback mIconAnimationCallback =
+ new Animatable2.AnimationCallback() {
+ @Override
+ public void onAnimationEnd(Drawable d) {
+ if (mAnimationCancelled) {
+ return;
+ }
+
+ // Start animation after it has ended.
+ mProgressBar.post(new Runnable() {
+ @Override
+ public void run() {
+ startIconAnimation();
+ }
+ });
+ }
+ };
+
+}
diff --git a/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFinishFragment.java b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFinishFragment.java
new file mode 100644
index 0000000..02aa5f2
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/view/FingerprintEnrollFinishFragment.java
@@ -0,0 +1,128 @@
+/*
+ * 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.view;
+
+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.fragment.app.FragmentActivity;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.settings.R;
+import com.android.settings.biometrics2.ui.viewmodel.FingerprintEnrollFinishViewModel;
+
+import com.google.android.setupcompat.template.FooterBarMixin;
+import com.google.android.setupcompat.template.FooterButton;
+import com.google.android.setupdesign.GlifLayout;
+
+
+/**
+ * Fragment which concludes fingerprint enrollment.
+ */
+public class FingerprintEnrollFinishFragment extends Fragment {
+
+ private static final String TAG = FingerprintEnrollFinishFragment.class.getSimpleName();
+
+ private FingerprintEnrollFinishViewModel mFingerprintEnrollFinishViewModel;
+ private boolean mCanAssumeSfps;
+
+ private View mView;
+ private FooterBarMixin mFooterBarMixin;
+
+ private final View.OnClickListener mAddButtonClickListener =
+ (v) -> mFingerprintEnrollFinishViewModel.onAddButtonClick();
+ private final View.OnClickListener mNextButtonClickListener =
+ (v) -> mFingerprintEnrollFinishViewModel.onNextButtonClick();
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+ final FragmentActivity activity = getActivity();
+ final ViewModelProvider provider = new ViewModelProvider(activity);
+ mFingerprintEnrollFinishViewModel = provider.get(FingerprintEnrollFinishViewModel.class);
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mCanAssumeSfps = mFingerprintEnrollFinishViewModel.canAssumeSfps();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+
+ if (mCanAssumeSfps) {
+ mView = inflater.inflate(R.layout.sfps_enroll_finish, container, false);
+ } else {
+ mView = inflater.inflate(R.layout.fingerprint_enroll_finish, container, false);
+ }
+
+ final Activity activity = getActivity();
+ final GlifLayoutHelper glifLayoutHelper = new GlifLayoutHelper(activity,
+ (GlifLayout) mView);
+
+ glifLayoutHelper.setHeaderText(R.string.security_settings_fingerprint_enroll_finish_title);
+ glifLayoutHelper.setDescriptionText(
+ R.string.security_settings_fingerprint_enroll_finish_v2_message);
+
+ final int maxEnrollments = mFingerprintEnrollFinishViewModel.getMaxFingerprints();
+ final int enrolled = mFingerprintEnrollFinishViewModel.getNumOfEnrolledFingerprintsSize();
+ if (mCanAssumeSfps) {
+ if (enrolled < maxEnrollments) {
+ glifLayoutHelper.setDescriptionText(R.string
+ .security_settings_fingerprint_enroll_finish_v2_add_fingerprint_message);
+ }
+ }
+
+ mFooterBarMixin = ((GlifLayout) mView).getMixin(FooterBarMixin.class);
+ mFooterBarMixin.setSecondaryButton(
+ new FooterButton.Builder(getActivity())
+ .setText(R.string.fingerprint_enroll_button_add)
+ .setButtonType(FooterButton.ButtonType.SKIP)
+ .setTheme(R.style.SudGlifButton_Secondary)
+ .build()
+ );
+
+ mFooterBarMixin.setPrimaryButton(
+ new FooterButton.Builder(getActivity())
+ .setText(R.string.security_settings_fingerprint_enroll_done)
+ .setListener(mNextButtonClickListener)
+ .setButtonType(FooterButton.ButtonType.NEXT)
+ .setTheme(R.style.SudGlifButton_Primary)
+ .build()
+ );
+
+ FooterButton addButton = mFooterBarMixin.getSecondaryButton();
+ if (enrolled >= maxEnrollments) {
+ addButton.setVisibility(View.INVISIBLE);
+ } else {
+ addButton.setOnClickListener(mAddButtonClickListener);
+ }
+
+ return mView;
+ }
+
+}
diff --git a/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java b/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java
index a1645d2..814f579 100644
--- a/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java
+++ b/src/com/android/settings/biometrics2/ui/view/GlifLayoutHelper.java
@@ -67,4 +67,16 @@
mGlifLayout.setDescriptionText(description);
}
}
+
+ /**
+ * Sets description resId to GlifLayout
+ */
+ public void setDescriptionText(int resId) {
+ CharSequence previousDescription = mGlifLayout.getDescriptionText();
+ CharSequence description = mActivity.getString(resId);
+ // Prevent a11y for re-reading the same string
+ if (!TextUtils.equals(previousDescription, description)) {
+ mGlifLayout.setDescriptionText(resId);
+ }
+ }
}
diff --git a/src/com/android/settings/biometrics2/ui/view/IconTouchDialog.java b/src/com/android/settings/biometrics2/ui/view/IconTouchDialog.java
new file mode 100644
index 0000000..38d6a5b
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/view/IconTouchDialog.java
@@ -0,0 +1,63 @@
+/*
+ * 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.view;
+
+import android.app.Dialog;
+import android.app.settings.SettingsEnums;
+import android.content.DialogInterface;
+import android.os.Bundle;
+
+import androidx.appcompat.app.AlertDialog;
+
+import com.android.settings.R;
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+
+/**
+ * Icon Touch dialog
+ */
+public class IconTouchDialog extends InstrumentedDialogFragment {
+
+// private FingerprintEnrollEnrollingViewModel mViewModel;
+//
+// @Override
+// public void onAttach(Context context) {
+// mViewModel = new ViewModelProvider(getActivity()).get(
+// FingerprintEnrollEnrollingViewModel.class);
+// super.onAttach(context);
+// }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(),
+ R.style.Theme_AlertDialog);
+ builder.setTitle(R.string.security_settings_fingerprint_enroll_touch_dialog_title)
+ .setMessage(R.string.security_settings_fingerprint_enroll_touch_dialog_message)
+ .setPositiveButton(R.string.security_settings_fingerprint_enroll_dialog_ok,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ });
+ return builder.create();
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ return SettingsEnums.DIALOG_FINGERPRINT_ICON_TOUCH;
+ }
+}
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollEnrollingViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollEnrollingViewModel.java
new file mode 100644
index 0000000..058b50b
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollEnrollingViewModel.java
@@ -0,0 +1,161 @@
+/*
+ * 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.app.Application;
+import android.os.VibrationAttributes;
+import android.os.VibrationEffect;
+import android.util.Log;
+import android.view.accessibility.AccessibilityManager;
+
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.settings.biometrics2.data.repository.AccessibilityRepository;
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.data.repository.VibratorRepository;
+import com.android.settings.biometrics2.ui.model.EnrollmentRequest;
+
+/**
+ * ViewModel explaining the fingerprint enrolling page
+ */
+public class FingerprintEnrollEnrollingViewModel extends AndroidViewModel
+ implements DefaultLifecycleObserver {
+
+ private static final String TAG = FingerprintEnrollEnrollingViewModel.class.getSimpleName();
+ private static final boolean DEBUG = false;
+
+ 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);
+
+ //Enrolling skip
+ public static final int FINGERPRINT_ENROLL_ENROLLING_ACTION_SKIP = 0;
+
+ //Icon touch dialog show
+ public static final int FINGERPRINT_ENROLL_ENROLLING_ACTION_SHOW_DIALOG = 0;
+
+ //Icon touch dialog dismiss
+ public static final int FINGERPRINT_ENROLL_ENROLLING_ACTION_DISMISS_DIALOG = 1;
+
+ private final FingerprintRepository mFingerprintRepository;
+ private final AccessibilityRepository mAccessibilityRepository;
+ private final VibratorRepository mVibratorRepository;
+
+ private EnrollmentRequest mEnrollmentRequest = null;
+ private final MutableLiveData<Integer> mEnrollingLiveData = new MutableLiveData<>();
+ private final MutableLiveData<Integer> mIconTouchDialogLiveData = new MutableLiveData<>();
+
+
+ public FingerprintEnrollEnrollingViewModel(Application application,
+ FingerprintRepository fingerprintRepository,
+ AccessibilityRepository accessibilityRepository,
+ VibratorRepository vibratorRepository) {
+ super(application);
+ mFingerprintRepository = fingerprintRepository;
+ mAccessibilityRepository = accessibilityRepository;
+ mVibratorRepository = vibratorRepository;
+ }
+
+ /**
+ * User clicks skip button
+ */
+ public void onSkipButtonClick() {
+ final int action = FINGERPRINT_ENROLL_ENROLLING_ACTION_SKIP;
+ if (DEBUG) {
+ Log.d(TAG, "onSkipButtonClick, post action " + action);
+ }
+ mEnrollingLiveData.postValue(action);
+ }
+
+ /**
+ * Icon touch dialog show
+ */
+ public void onIconTouchDialogShow() {
+ final int action = FINGERPRINT_ENROLL_ENROLLING_ACTION_SHOW_DIALOG;
+ if (DEBUG) {
+ Log.d(TAG, "onIconTouchDialogShow, post action " + action);
+ }
+ mIconTouchDialogLiveData.postValue(action);
+ }
+
+ /**
+ * Icon touch dialog dismiss
+ */
+ public void onIconTouchDialogDismiss() {
+ final int action = FINGERPRINT_ENROLL_ENROLLING_ACTION_DISMISS_DIALOG;
+ if (DEBUG) {
+ Log.d(TAG, "onIconTouchDialogDismiss, post action " + action);
+ }
+ mIconTouchDialogLiveData.postValue(action);
+ }
+
+ /**
+ * get enroll stage threshold
+ */
+ public float getEnrollStageThreshold(int index) {
+ return mFingerprintRepository.getEnrollStageThreshold(index);
+ }
+
+ /**
+ * Get enroll stage count
+ */
+ public int getEnrollStageCount() {
+ return mFingerprintRepository.getEnrollStageCount();
+ }
+
+ /**
+ * The first sensor type is UDFPS sensor or not
+ */
+ public boolean canAssumeUdfps() {
+ return mFingerprintRepository.canAssumeUdfps();
+ }
+
+ /**
+ * The first sensor type is SFPS sensor or not
+ */
+ public boolean canAssumeSfps() {
+ return mFingerprintRepository.canAssumeSfps();
+ }
+
+ /**
+ * Requests interruption of the accessibility feedback from all accessibility services.
+ */
+ public void clearTalkback() {
+ mAccessibilityRepository.interrupt();
+ }
+
+ /**
+ * Returns if the {@link AccessibilityManager} is enabled.
+ *
+ * @return True if this {@link AccessibilityManager} is enabled, false otherwise.
+ */
+ public boolean isAccessibilityEnabled() {
+ return mAccessibilityRepository.isEnabled();
+ }
+
+ /**
+ * Like {@link #vibrate(VibrationEffect, VibrationAttributes)}, but allows the
+ * caller to specify the vibration is owned by someone else and set a reason for vibration.
+ */
+ public void vibrateError(int uid, String opPkg, String reason) {
+ mVibratorRepository.vibrate(uid, opPkg, VIBRATE_EFFECT_ERROR, reason,
+ FINGERPRINT_ENROLLING_SONFICATION_ATTRIBUTES);
+ }
+}
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollFinishViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollFinishViewModel.java
new file mode 100644
index 0000000..2cf0b47
--- /dev/null
+++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollFinishViewModel.java
@@ -0,0 +1,123 @@
+/*
+ * 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.app.Application;
+import android.content.ComponentName;
+import android.content.pm.PackageManager;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.MutableLiveData;
+
+import com.android.settings.biometrics2.data.repository.FingerprintRepository;
+import com.android.settings.biometrics2.data.repository.PackageManagerRepository;
+
+/**
+ * Finish ViewModel handles the state of the fingerprint renroll final stage
+ */
+public class FingerprintEnrollFinishViewModel extends AndroidViewModel {
+
+ private static final String TAG = FingerprintEnrollFinishViewModel.class.getSimpleName();
+
+ private static final String FINGERPRINT_SUGGESTION_ACTIVITY =
+ "com.android.settings.SetupFingerprintSuggestionActivity";
+
+ private static final int ACTION_NONE = -1;
+ private static final int ACTION_ADD_BUTTON_CLICK = 0;
+ private static final int ACTION_NEXT_BUTTON_CLICK = 1;
+
+ private final FingerprintRepository mFingerprintRepository;
+ private final PackageManagerRepository mPackageManagerRepository;
+ private final int mUserId;
+
+ private final MutableLiveData<Integer> mActionLiveData = new MutableLiveData<>();
+
+ public FingerprintEnrollFinishViewModel(@NonNull Application application,
+ FingerprintRepository fingerprintRepository,
+ PackageManagerRepository packageManagerRepository,
+ int userId) {
+ super(application);
+ mFingerprintRepository = fingerprintRepository;
+ mPackageManagerRepository = packageManagerRepository;
+ mUserId = userId;
+ mActionLiveData.setValue(ACTION_NONE);
+ }
+
+ /**
+ * The first sensor type is Side fps sensor or not
+ */
+ public boolean canAssumeSfps() {
+ return mFingerprintRepository.canAssumeSfps();
+ }
+
+ /**
+ * Get number of fingerprints that this user enrolled.
+ */
+ public int getNumOfEnrolledFingerprintsSize() {
+ return mFingerprintRepository.getNumOfEnrolledFingerprintsSize(mUserId);
+ }
+
+ /**
+ * Get max possible number of fingerprints for a user
+ */
+ public int getMaxFingerprints() {
+ return mFingerprintRepository.getMaxFingerprints();
+ }
+
+ /**
+ * Clear life data
+ */
+ public void clearLiveData() {
+ mActionLiveData.setValue(ACTION_NONE);
+ }
+
+ /**
+ * Handle add button Click
+ */
+ public void onAddButtonClick() {
+ mActionLiveData.postValue(ACTION_ADD_BUTTON_CLICK);
+ }
+
+ /**
+ * Handle next button Click
+ */
+ public void onNextButtonClick() {
+ updateFingerprintSuggestionEnableState();
+ mActionLiveData.postValue(ACTION_NEXT_BUTTON_CLICK);
+ }
+
+ /**
+ * Handle back key pressed
+ */
+ public void onBackKeyPressed() {
+ updateFingerprintSuggestionEnableState();
+ }
+
+ private void updateFingerprintSuggestionEnableState() {
+ final int enrollNum = mFingerprintRepository.getNumOfEnrolledFingerprintsSize(mUserId);
+ final int flag = (enrollNum == 1) ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
+
+ ComponentName componentName = new ComponentName(getApplication(),
+ FINGERPRINT_SUGGESTION_ACTIVITY);
+
+ mPackageManagerRepository.setComponentEnabledSetting(componentName, flag,
+ PackageManager.DONT_KILL_APP);
+ }
+}
diff --git a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java
index cbc74c0..532e2cc 100644
--- a/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java
+++ b/src/com/android/settings/biometrics2/ui/viewmodel/FingerprintEnrollProgressViewModel.java
@@ -56,6 +56,10 @@
private final MutableLiveData<EnrollmentStatusMessage> mErrorMessageLiveData =
new MutableLiveData<>();
+ private final MutableLiveData<Boolean> mAcquireLiveData = new MutableLiveData<>();
+ private final MutableLiveData<Integer> mPointerDownLiveData = new MutableLiveData<>();
+ private final MutableLiveData<Integer> mPointerUpLiveData = new MutableLiveData<>();
+
private byte[] mToken = null;
private final int mUserId;
@@ -86,6 +90,21 @@
public void onEnrollmentError(int errMsgId, CharSequence errString) {
mErrorMessageLiveData.postValue(new EnrollmentStatusMessage(errMsgId, errString));
}
+
+ @Override
+ public void onAcquired(boolean isAcquiredGood) {
+ mAcquireLiveData.postValue(isAcquiredGood);
+ }
+
+ @Override
+ public void onPointerDown(int sensorId) {
+ mPointerDownLiveData.postValue(sensorId);
+ }
+
+ @Override
+ public void onPointerUp(int sensorId) {
+ mPointerUpLiveData.postValue(sensorId);
+ }
};
public FingerprintEnrollProgressViewModel(@NonNull Application application,
@@ -132,6 +151,19 @@
return mErrorMessageLiveData;
}
+ public MutableLiveData<Boolean> getAcquireLiveData() {
+ return mAcquireLiveData;
+ }
+
+ public MutableLiveData<Integer> getPointerDownLiveData() {
+ return mPointerDownLiveData;
+ }
+
+ public MutableLiveData<Integer> getPointerUpLiveData() {
+ return mPointerUpLiveData;
+ }
+
+
/**
* Starts enrollment and return latest isEnrolling() result
*/
diff --git a/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java b/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java
index 5e432cf..ecc4ea0 100644
--- a/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java
+++ b/src/com/android/settings/fuelgauge/BatteryBroadcastReceiver.java
@@ -20,6 +20,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.hardware.usb.UsbManager;
import android.os.BatteryManager;
import android.os.PowerManager;
import android.util.Log;
@@ -99,6 +100,7 @@
intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
intentFilter.addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
intentFilter.addAction(BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION);
+ intentFilter.addAction(UsbManager.ACTION_USB_PORT_COMPLIANCE_CHANGED);
final Intent intent = mContext.registerReceiver(this, intentFilter,
Context.RECEIVER_EXPORTED);
@@ -110,33 +112,36 @@
}
private void updateBatteryStatus(Intent intent, boolean forceUpdate) {
- if (intent != null && mBatteryListener != null) {
- if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {
- final String batteryLevel = Utils.getBatteryPercentage(intent);
- final String batteryStatus =
- Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false);
- final int batteryHealth = intent.getIntExtra(
- BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_UNKNOWN);
- if (!Utils.isBatteryPresent(intent)) {
- Log.w(TAG, "Problem reading the battery meter.");
- mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_NOT_PRESENT);
- } else if (forceUpdate) {
- mBatteryListener.onBatteryChanged(BatteryUpdateType.MANUAL);
- } else if (batteryHealth != mBatteryHealth) {
- mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_HEALTH);
- } else if(!batteryLevel.equals(mBatteryLevel)) {
- mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_LEVEL);
- } else if (!batteryStatus.equals(mBatteryStatus)) {
- mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_STATUS);
- }
- mBatteryLevel = batteryLevel;
- mBatteryStatus = batteryStatus;
- mBatteryHealth = batteryHealth;
- } else if (PowerManager.ACTION_POWER_SAVE_MODE_CHANGED.equals(intent.getAction())) {
- mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_SAVER);
- } else if (BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION.equals(intent.getAction())) {
+ if (intent == null || mBatteryListener == null) {
+ return;
+ }
+ final String action = intent.getAction();
+ if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
+ final String batteryLevel = Utils.getBatteryPercentage(intent);
+ final String batteryStatus =
+ Utils.getBatteryStatus(mContext, intent, /* compactStatus= */ false);
+ final int batteryHealth = intent.getIntExtra(
+ BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_UNKNOWN);
+ if (!Utils.isBatteryPresent(intent)) {
+ Log.w(TAG, "Problem reading the battery meter.");
+ mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_NOT_PRESENT);
+ } else if (forceUpdate) {
+ mBatteryListener.onBatteryChanged(BatteryUpdateType.MANUAL);
+ } else if (batteryHealth != mBatteryHealth) {
+ mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_HEALTH);
+ } else if(!batteryLevel.equals(mBatteryLevel)) {
+ mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_LEVEL);
+ } else if (!batteryStatus.equals(mBatteryStatus)) {
mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_STATUS);
}
+ mBatteryLevel = batteryLevel;
+ mBatteryStatus = batteryStatus;
+ mBatteryHealth = batteryHealth;
+ } else if (PowerManager.ACTION_POWER_SAVE_MODE_CHANGED.equals(action)) {
+ mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_SAVER);
+ } else if (BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION.equals(action)
+ || UsbManager.ACTION_USB_PORT_COMPLIANCE_CHANGED.equals(action)) {
+ mBatteryListener.onBatteryChanged(BatteryUpdateType.BATTERY_STATUS);
}
}
}
diff --git a/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java b/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java
index da646cb..437ffda 100644
--- a/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java
+++ b/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoader.java
@@ -26,6 +26,7 @@
import com.android.settings.fuelgauge.batterytip.detectors.BatteryDefenderDetector;
import com.android.settings.fuelgauge.batterytip.detectors.DockDefenderDetector;
import com.android.settings.fuelgauge.batterytip.detectors.HighUsageDetector;
+import com.android.settings.fuelgauge.batterytip.detectors.IncompatibleChargerDetector;
import com.android.settings.fuelgauge.batterytip.detectors.LowBatteryDetector;
import com.android.settings.fuelgauge.batterytip.detectors.SmartBatteryDetector;
import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
@@ -59,15 +60,15 @@
final List<BatteryTip> tips = new ArrayList<>();
final BatteryTipPolicy policy = new BatteryTipPolicy(getContext());
final BatteryInfo batteryInfo = mBatteryUtils.getBatteryInfo(TAG);
- final Context context = getContext();
+ final Context context = getContext().getApplicationContext();
tips.add(new LowBatteryDetector(context, policy, batteryInfo).detect());
tips.add(new HighUsageDetector(context, policy, mBatteryUsageStats, batteryInfo).detect());
tips.add(new SmartBatteryDetector(
context, policy, batteryInfo, context.getContentResolver()).detect());
- tips.add(new BatteryDefenderDetector(
- batteryInfo, context.getApplicationContext()).detect());
- tips.add(new DockDefenderDetector(batteryInfo, context.getApplicationContext()).detect());
+ tips.add(new BatteryDefenderDetector(batteryInfo, context).detect());
+ tips.add(new DockDefenderDetector(batteryInfo, context).detect());
+ tips.add(new IncompatibleChargerDetector(context, batteryInfo).detect());
Collections.sort(tips);
return tips;
}
diff --git a/src/com/android/settings/fuelgauge/batterytip/detectors/IncompatibleChargerDetector.java b/src/com/android/settings/fuelgauge/batterytip/detectors/IncompatibleChargerDetector.java
new file mode 100644
index 0000000..483e37d
--- /dev/null
+++ b/src/com/android/settings/fuelgauge/batterytip/detectors/IncompatibleChargerDetector.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.fuelgauge.batterytip.detectors;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.android.settings.fuelgauge.BatteryInfo;
+import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
+import com.android.settings.fuelgauge.batterytip.tips.IncompatibleChargerTip;
+import com.android.settingslib.Utils;
+
+/** Detect whether it is in the incompatible charging state */
+public final class IncompatibleChargerDetector implements BatteryTipDetector {
+ private static final String TAG = "IncompatibleChargerDetector";
+
+ private final Context mContext;
+ private final BatteryInfo mBatteryInfo;
+
+ public IncompatibleChargerDetector(Context context, BatteryInfo batteryInfo) {
+ mContext = context;
+ mBatteryInfo = batteryInfo;
+ }
+
+ @Override
+ public BatteryTip detect() {
+ int state = BatteryTip.StateType.INVISIBLE;
+ boolean isIncompatibleCharging = false;
+
+ // Check incompatible charging state if the device is plugged.
+ if (mBatteryInfo.pluggedStatus != 0) {
+ isIncompatibleCharging = Utils.containsIncompatibleChargers(mContext, TAG);
+ if (isIncompatibleCharging) {
+ state = BatteryTip.StateType.NEW;
+ }
+ }
+ Log.d(TAG, "detect() state= " + state + " isIncompatibleCharging: "
+ + isIncompatibleCharging);
+ return new IncompatibleChargerTip(state);
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/BatteryBroadcastReceiverTest.java b/tests/robotests/src/com/android/settings/fuelgauge/BatteryBroadcastReceiverTest.java
index 4bfb15b..a829c40 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/BatteryBroadcastReceiverTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/BatteryBroadcastReceiverTest.java
@@ -21,6 +21,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
@@ -29,6 +30,8 @@
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.usb.UsbManager;
import android.os.BatteryManager;
import android.os.PowerManager;
@@ -37,6 +40,7 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;
@@ -55,7 +59,6 @@
private BatteryBroadcastReceiver mBatteryBroadcastReceiver;
private Context mContext;
private Intent mChargingIntent;
- private Intent mDockDefenderBypassIntent;
@Before
public void setUp() {
@@ -73,12 +76,10 @@
mChargingIntent.putExtra(BatteryManager.EXTRA_SCALE, BATTERY_INTENT_SCALE);
mChargingIntent
.putExtra(BatteryManager.EXTRA_STATUS, BatteryManager.BATTERY_STATUS_CHARGING);
- mDockDefenderBypassIntent = new Intent(BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION);
-
}
@Test
- public void testOnReceive_batteryLevelChanged_dataUpdated() {
+ public void onReceive_batteryLevelChanged_dataUpdated() {
mBatteryBroadcastReceiver.onReceive(mContext, mChargingIntent);
assertThat(mBatteryBroadcastReceiver.mBatteryLevel)
@@ -89,7 +90,7 @@
}
@Test
- public void testOnReceive_batteryHealthChanged_dataUpdated() {
+ public void onReceive_batteryHealthChanged_dataUpdated() {
mChargingIntent
.putExtra(BatteryManager.EXTRA_HEALTH, BatteryManager.BATTERY_HEALTH_OVERHEAT);
mBatteryBroadcastReceiver.onReceive(mContext, mChargingIntent);
@@ -109,7 +110,7 @@
}
@Test
- public void testOnReceive_powerSaveModeChanged_listenerInvoked() {
+ public void onReceive_powerSaveModeChanged_listenerInvoked() {
mBatteryBroadcastReceiver.onReceive(mContext,
new Intent(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED));
@@ -117,7 +118,7 @@
}
@Test
- public void testOnReceive_batteryDataNotChanged_listenerNotInvoked() {
+ public void onReceive_batteryDataNotChanged_listenerNotInvoked() {
final String batteryLevel = Utils.getBatteryPercentage(mChargingIntent);
final String batteryStatus =
Utils.getBatteryStatus(mContext, mChargingIntent, /* compactStatus= */ false);
@@ -134,14 +135,23 @@
}
@Test
- public void testOnReceive_dockDefenderBypassed_listenerInvoked() {
- mBatteryBroadcastReceiver.onReceive(mContext, mDockDefenderBypassIntent);
+ public void onReceive_dockDefenderBypassed_listenerInvoked() {
+ mBatteryBroadcastReceiver.onReceive(mContext,
+ new Intent(BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION));
verify(mBatteryListener).onBatteryChanged(BatteryUpdateType.BATTERY_STATUS);
}
@Test
- public void testRegister_updateBatteryStatus() {
+ public void onReceive_usbPortComplianceChanged_listenerInvoked() {
+ mBatteryBroadcastReceiver.onReceive(mContext,
+ new Intent(UsbManager.ACTION_USB_PORT_COMPLIANCE_CHANGED));
+
+ verify(mBatteryListener).onBatteryChanged(BatteryUpdateType.BATTERY_STATUS);
+ }
+
+ @Test
+ public void register_updateBatteryStatus() {
doReturn(mChargingIntent).when(mContext).registerReceiver(any(), any(), anyInt());
mBatteryBroadcastReceiver.register();
@@ -156,4 +166,23 @@
// 2 times because register will force update the battery
verify(mBatteryListener, times(2)).onBatteryChanged(BatteryUpdateType.MANUAL);
}
+
+ @Test
+ public void register_registerExpectedIntent() {
+ mBatteryBroadcastReceiver.register();
+
+ ArgumentCaptor<IntentFilter> captor = ArgumentCaptor.forClass(IntentFilter.class);
+ verify(mContext).registerReceiver(
+ eq(mBatteryBroadcastReceiver),
+ captor.capture(),
+ eq(Context.RECEIVER_EXPORTED));
+ assertAction(captor, Intent.ACTION_BATTERY_CHANGED);
+ assertAction(captor, PowerManager.ACTION_POWER_SAVE_MODE_CHANGED);
+ assertAction(captor, BatteryUtils.BYPASS_DOCK_DEFENDER_ACTION);
+ assertAction(captor, UsbManager.ACTION_USB_PORT_COMPLIANCE_CHANGED);
+ }
+
+ private void assertAction(ArgumentCaptor<IntentFilter> captor, String action) {
+ assertThat(captor.getValue().hasAction(action)).isTrue();
+ }
}
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoaderTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoaderTest.java
index 0e1c458..5b0ae04 100644
--- a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoaderTest.java
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/BatteryTipLoaderTest.java
@@ -53,6 +53,7 @@
BatteryTip.TipType.LOW_BATTERY,
BatteryTip.TipType.BATTERY_DEFENDER,
BatteryTip.TipType.DOCK_DEFENDER,
+ BatteryTip.TipType.INCOMPATIBLE_CHARGER,
BatteryTip.TipType.HIGH_DEVICE_USAGE,
BatteryTip.TipType.SMART_BATTERY_MANAGER};
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
diff --git a/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/IncompatibleChargerDetectorTest.java b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/IncompatibleChargerDetectorTest.java
new file mode 100644
index 0000000..1dfe6e2
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/fuelgauge/batterytip/detectors/IncompatibleChargerDetectorTest.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.fuelgauge.batterytip.detectors;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.hardware.usb.UsbManager;
+import android.hardware.usb.UsbPort;
+import android.hardware.usb.UsbPortStatus;
+
+import com.android.settings.fuelgauge.BatteryInfo;
+import com.android.settings.fuelgauge.batterytip.tips.BatteryTip;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@RunWith(RobolectricTestRunner.class)
+public final class IncompatibleChargerDetectorTest {
+
+ @Mock private BatteryInfo mBatteryInfo;
+ @Mock private UsbPort mUsbPort;
+ @Mock private UsbManager mUsbManager;
+ @Mock private UsbPortStatus mUsbPortStatus;
+
+ private Context mContext;
+ private IncompatibleChargerDetector mIncompatibleChargerDetector;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mContext = spy(RuntimeEnvironment.application);
+ when(mContext.getSystemService(UsbManager.class)).thenReturn(mUsbManager);
+ mIncompatibleChargerDetector =
+ new IncompatibleChargerDetector(mContext, mBatteryInfo);
+ }
+
+ @Test
+ public void detect_unplugDevice_shouldNotShowTip() {
+ mBatteryInfo.pluggedStatus = 0;
+
+ BatteryTip batteryTip = mIncompatibleChargerDetector.detect();
+
+ assertThat(batteryTip.isVisible()).isFalse();
+ assertThat(batteryTip.getState()).isEqualTo(BatteryTip.StateType.INVISIBLE);
+ }
+
+ @Test
+ public void detect_plugDeviceWithoutIncompatibleCharger_shouldNotShowTip() {
+ mBatteryInfo.pluggedStatus = 1;
+
+ BatteryTip batteryTip = mIncompatibleChargerDetector.detect();
+
+ assertThat(batteryTip.isVisible()).isFalse();
+ assertThat(batteryTip.getState()).isEqualTo(BatteryTip.StateType.INVISIBLE);
+ }
+
+ @Test
+ public void detect_plugDeviceWithIncompatibleCharger_showTip() {
+ mBatteryInfo.pluggedStatus = 1;
+ setupIncompatibleCharging();
+
+ BatteryTip batteryTip = mIncompatibleChargerDetector.detect();
+
+ assertThat(batteryTip.isVisible()).isTrue();
+ assertThat(batteryTip.getState()).isEqualTo(BatteryTip.StateType.NEW);
+ }
+
+ private void setupIncompatibleCharging() {
+ final List<UsbPort> usbPorts = new ArrayList<>();
+ usbPorts.add(mUsbPort);
+ when(mUsbManager.getPorts()).thenReturn(usbPorts);
+ when(mUsbPort.getStatus()).thenReturn(mUsbPortStatus);
+ when(mUsbPort.supportsComplianceWarnings()).thenReturn(true);
+ when(mUsbPortStatus.isConnected()).thenReturn(true);
+ when(mUsbPortStatus.getComplianceWarnings()).thenReturn(new int[]{1});
+ }
+}