diff options
209 files changed, 4961 insertions, 1111 deletions
diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index e23bbc6c347d..d3bde4b4b8a8 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -20,6 +20,7 @@ import static com.android.internal.util.function.pooled.PooledLambda.obtainRunna  import android.annotation.NonNull;  import android.content.Context; +import android.graphics.ImageFormat;  import android.hardware.ICameraService;  import android.hardware.camera2.CameraAccessException;  import android.hardware.camera2.CameraCaptureSession; @@ -1478,6 +1479,12 @@ public class CameraDeviceImpl extends CameraDevice              }          } +        // Allow RAW formats, even when not advertised. +        if (inputFormat == ImageFormat.RAW_PRIVATE || inputFormat == ImageFormat.RAW10 +                || inputFormat == ImageFormat.RAW12 || inputFormat == ImageFormat.RAW_SENSOR) { +            return true; +        } +          if (validFormat == false) {              return false;          } diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java index 01977f6195ff..619544366b02 100644 --- a/core/java/android/hardware/fingerprint/FingerprintManager.java +++ b/core/java/android/hardware/fingerprint/FingerprintManager.java @@ -103,6 +103,7 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing      private static final int MSG_UDFPS_POINTER_DOWN = 108;      private static final int MSG_UDFPS_POINTER_UP = 109;      private static final int MSG_POWER_BUTTON_PRESSED = 110; +    private static final int MSG_UDFPS_OVERLAY_SHOWN = 111;      /**       * @hide @@ -121,6 +122,24 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing      public @interface EnrollReason {}      /** +     * Udfps ui event of overlay is shown on the screen. +     * @hide +     */ +    public static final int UDFPS_UI_OVERLAY_SHOWN = 1; +    /** +     * Udfps ui event of the udfps UI being ready (e.g. HBM illumination is enabled). +     * @hide +     */ +    public static final int UDFPS_UI_READY = 2; + +    /** +     * @hide +     */ +    @IntDef({UDFPS_UI_OVERLAY_SHOWN, UDFPS_UI_READY}) +    @Retention(RetentionPolicy.SOURCE) +    public @interface UdfpsUiEvent{} + +    /**       * Request authentication with any single sensor.       * @hide       */ @@ -475,12 +494,17 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing          /**           * Called when a pointer down event has occurred.           */ -        public void onPointerDown(int sensorId){ } +        public void onUdfpsPointerDown(int sensorId){ }          /**           * Called when a pointer up event has occurred.           */ -        public void onPointerUp(int sensorId){ } +        public void onUdfpsPointerUp(int sensorId){ } + +        /** +         * Called when udfps overlay is shown. +         */ +        public void onUdfpsOverlayShown() { }      }      /** @@ -1112,14 +1136,14 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing       * @hide       */      @RequiresPermission(USE_BIOMETRIC_INTERNAL) -    public void onUiReady(long requestId, int sensorId) { +    public void onUdfpsUiEvent(@UdfpsUiEvent int event, long requestId, int sensorId) {          if (mService == null) { -            Slog.w(TAG, "onUiReady: no fingerprint service"); +            Slog.w(TAG, "onUdfpsUiEvent: no fingerprint service");              return;          }          try { -            mService.onUiReady(requestId, sensorId); +            mService.onUdfpsUiEvent(event, requestId, sensorId);          } catch (RemoteException e) {              throw e.rethrowFromSystemServer();          } @@ -1365,6 +1389,8 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing                  case MSG_POWER_BUTTON_PRESSED:                      sendPowerPressed();                      break; +                case MSG_UDFPS_OVERLAY_SHOWN: +                    sendUdfpsOverlayShown();                  default:                      Slog.w(TAG, "Unknown message: " + msg.what); @@ -1489,7 +1515,7 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing          }          if (mEnrollmentCallback != null) { -            mEnrollmentCallback.onPointerDown(sensorId); +            mEnrollmentCallback.onUdfpsPointerDown(sensorId);          }      } @@ -1500,7 +1526,7 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing              mAuthenticationCallback.onUdfpsPointerUp(sensorId);          }          if (mEnrollmentCallback != null) { -            mEnrollmentCallback.onPointerUp(sensorId); +            mEnrollmentCallback.onUdfpsPointerUp(sensorId);          }      } @@ -1512,6 +1538,12 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing          }      } +    private void sendUdfpsOverlayShown() { +        if (mEnrollmentCallback != null) { +            mEnrollmentCallback.onUdfpsOverlayShown(); +        } +    } +      /**       * @hide       */ @@ -1787,6 +1819,11 @@ public class FingerprintManager implements BiometricAuthenticator, BiometricFing          public void onUdfpsPointerUp(int sensorId) {              mHandler.obtainMessage(MSG_UDFPS_POINTER_UP, sensorId, 0).sendToTarget();          } + +        @Override +        public void onUdfpsOverlayShown() { +            mHandler.obtainMessage(MSG_UDFPS_OVERLAY_SHOWN).sendToTarget(); +        }      };  } diff --git a/core/java/android/hardware/fingerprint/FingerprintServiceReceiver.java b/core/java/android/hardware/fingerprint/FingerprintServiceReceiver.java index a9779b51321b..89d710d4adfe 100644 --- a/core/java/android/hardware/fingerprint/FingerprintServiceReceiver.java +++ b/core/java/android/hardware/fingerprint/FingerprintServiceReceiver.java @@ -75,4 +75,9 @@ public class FingerprintServiceReceiver extends IFingerprintServiceReceiver.Stub      public void onUdfpsPointerUp(int sensorId) throws RemoteException {      } + +    @Override +    public void onUdfpsOverlayShown() throws RemoteException { + +    }  } diff --git a/core/java/android/hardware/fingerprint/IFingerprintService.aidl b/core/java/android/hardware/fingerprint/IFingerprintService.aidl index ec5749ed4f05..ff2f313ac3df 100644 --- a/core/java/android/hardware/fingerprint/IFingerprintService.aidl +++ b/core/java/android/hardware/fingerprint/IFingerprintService.aidl @@ -193,7 +193,7 @@ interface IFingerprintService {      // Notifies about the fingerprint UI being ready (e.g. HBM illumination is enabled).      @EnforcePermission("USE_BIOMETRIC_INTERNAL") -    void onUiReady(long requestId, int sensorId); +    void onUdfpsUiEvent(int event, long requestId, int sensorId);      // Sets the controller for managing the UDFPS overlay.      @EnforcePermission("USE_BIOMETRIC_INTERNAL") diff --git a/core/java/android/hardware/fingerprint/IFingerprintServiceReceiver.aidl b/core/java/android/hardware/fingerprint/IFingerprintServiceReceiver.aidl index 9cea1fed629d..91a32d78314c 100644 --- a/core/java/android/hardware/fingerprint/IFingerprintServiceReceiver.aidl +++ b/core/java/android/hardware/fingerprint/IFingerprintServiceReceiver.aidl @@ -32,4 +32,5 @@ oneway interface IFingerprintServiceReceiver {      void onChallengeGenerated(int sensorId, int userId, long challenge);      void onUdfpsPointerDown(int sensorId);      void onUdfpsPointerUp(int sensorId); +    void onUdfpsOverlayShown();  } diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index a95ce64ecec8..7f53cb433c98 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -771,7 +771,7 @@ public class ZygoteInit {              Zygote.applyInvokeWithSystemProperty(parsedArgs);              if (Zygote.nativeSupportsMemoryTagging()) { -                String mode = SystemProperties.get("arm64.memtag.process.system_server", ""); +                String mode = SystemProperties.get("persist.arm64.memtag.system_server", "");                  if (mode.isEmpty()) {                    /* The system server has ASYNC MTE by default, in order to allow                     * system services to specify their own MTE level later, as you diff --git a/core/res/res/layout/shutdown_dialog.xml b/core/res/res/layout/shutdown_dialog.xml index ec67aa86bcc9..726c25540e6f 100644 --- a/core/res/res/layout/shutdown_dialog.xml +++ b/core/res/res/layout/shutdown_dialog.xml @@ -40,7 +40,7 @@          android:fontFamily="@string/config_headlineFontFamily"/>      <TextView -        android:id="@+id/text2" +        android:id="@id/text2"          android:layout_width="wrap_content"          android:layout_height="32sp"          android:text="@string/shutdown_progress" diff --git a/core/res/res/values-af/strings.xml b/core/res/res/values-af/strings.xml index 6a7c06581d8a..a941dc7a7f99 100644 --- a/core/res/res/values-af/strings.xml +++ b/core/res/res/values-af/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometriese hardeware is nie beskikbaar nie"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Stawing is gekanselleer"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Nie herken nie"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Gesig word nie herken nie"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Stawing is gekanselleer"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Geen PIN, patroon of wagwoord is gestel nie"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Kon nie staaf nie"</string> diff --git a/core/res/res/values-am/strings.xml b/core/res/res/values-am/strings.xml index ecab978bee6c..f97768ebdd16 100644 --- a/core/res/res/values-am/strings.xml +++ b/core/res/res/values-am/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"ባዮሜትራዊ ሃርድዌር አይገኝም"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"ማረጋገጥ ተሰርዟል"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"አልታወቀም"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"መልክ አልታወቀም"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"ማረጋገጥ ተሰርዟል"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"ምንም ፒን፣ ሥርዓተ ጥለት ወይም የይለፍ ቃል አልተቀናበረም"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"ማረጋገጥ ላይ ስህተት"</string> diff --git a/core/res/res/values-ar/strings.xml b/core/res/res/values-ar/strings.xml index 87df6bb1e466..a812ac0932da 100644 --- a/core/res/res/values-ar/strings.xml +++ b/core/res/res/values-ar/strings.xml @@ -624,6 +624,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"معدّات المقاييس الحيوية غير متاحة."</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"تم إلغاء المصادقة."</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"لم يتم التعرف عليها."</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"لم يتم التعرّف على الوجه."</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"تم إلغاء المصادقة."</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"لم يتم ضبط رقم تعريف شخصي أو نقش أو كلمة مرور."</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"خطأ في المصادقة"</string> diff --git a/core/res/res/values-as/strings.xml b/core/res/res/values-as/strings.xml index 2542163edfc0..626c3d6e930a 100644 --- a/core/res/res/values-as/strings.xml +++ b/core/res/res/values-as/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"বায়োমেট্ৰিক হাৰ্ডৱেৰ উপলব্ধ নহয়"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"বিশ্বাসযোগ্যতাৰ প্ৰমাণীকৰণ বাতিল কৰা হৈছে"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"চিনাক্ত কৰিব পৰা নাই"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"মুখাৱয়ব চিনি পোৱা নাই"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"বিশ্বাসযোগ্যতাৰ প্ৰমাণীকৰণ বাতিল কৰা হৈছে"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"কোনো পিন, আৰ্হি বা পাছৱৰ্ড ছেট কৰা নাই"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"আসোঁৱাহৰ বিশ্বাসযোগ্যতা প্ৰমাণীকৰণ কৰি থকা হৈছে"</string> diff --git a/core/res/res/values-az/strings.xml b/core/res/res/values-az/strings.xml index 3d7e48779ce8..5a41074a0cf7 100644 --- a/core/res/res/values-az/strings.xml +++ b/core/res/res/values-az/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometrik proqram əlçatan deyil"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Doğrulama ləğv edildi"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Tanınmır"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Üz tanınmadı"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Doğrulama ləğv edildi"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Pin, nümunə və ya parol ayarlanmayıb"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Doğrulama zamanı xəta baş verdi"</string> diff --git a/core/res/res/values-b+sr+Latn/strings.xml b/core/res/res/values-b+sr+Latn/strings.xml index 1e731e8813ea..1baf387eeab9 100644 --- a/core/res/res/values-b+sr+Latn/strings.xml +++ b/core/res/res/values-b+sr+Latn/strings.xml @@ -621,6 +621,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometrijski hardver nije dostupan"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Potvrda identiteta je otkazana"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Nije prepoznato"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Lice nije prepoznato"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Potvrda identiteta je otkazana"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Niste podesili ni PIN, ni šablon, ni lozinku"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Greška pri potvrdi identiteta"</string> diff --git a/core/res/res/values-be/strings.xml b/core/res/res/values-be/strings.xml index b23022fc0e49..7de0b63f574c 100644 --- a/core/res/res/values-be/strings.xml +++ b/core/res/res/values-be/strings.xml @@ -622,6 +622,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Біяметрычнае абсталяванне недаступнае"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Аўтэнтыфікацыя скасавана"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Не распазнана"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Твар не распазнаны"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Аўтэнтыфікацыя скасавана"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Не заданы PIN-код, узор разблакіроўкі або пароль"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Памылка аўтэнтыфікацыі"</string> diff --git a/core/res/res/values-bg/strings.xml b/core/res/res/values-bg/strings.xml index 65c883a6eab5..b4ba000938ce 100644 --- a/core/res/res/values-bg/strings.xml +++ b/core/res/res/values-bg/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Биометричният хардуер не е налице"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Удостоверяването бе анулирано"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Не е разпознато"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Лицето не е разпознато"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Удостоверяването бе анулирано"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Няма зададен ПИН код, фигура или парола"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Грешка при удостоверяването"</string> diff --git a/core/res/res/values-bn/strings.xml b/core/res/res/values-bn/strings.xml index fe686db71c4e..25c0d42853e8 100644 --- a/core/res/res/values-bn/strings.xml +++ b/core/res/res/values-bn/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"বায়োমেট্রিক হার্ডওয়্যার পাওয়া যাবে না"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"যাচাইকরণ বাতিল হয়েছে"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"স্বীকৃত নয়"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"ফেস চেনা যায়নি"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"যাচাইকরণ বাতিল হয়েছে"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"পিন, প্যাটার্ন অথবা পাসওয়ার্ড সেট করা নেই"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"যাচাইকরণে সমস্যা হয়েছে"</string> diff --git a/core/res/res/values-bs/strings.xml b/core/res/res/values-bs/strings.xml index 63fff4f2a920..e824ecc213ca 100644 --- a/core/res/res/values-bs/strings.xml +++ b/core/res/res/values-bs/strings.xml @@ -621,6 +621,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometrijski hardver nije dostupan"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Autentifikacija je otkazana"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Nije prepoznato"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Lice nije prepoznato"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Autentifikacija je otkazana"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Nije postavljen PIN, uzorak niti lozinka"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Greška pri autentifikaciji"</string> diff --git a/core/res/res/values-ca/strings.xml b/core/res/res/values-ca/strings.xml index b581d49384b2..46fc3a3a1114 100644 --- a/core/res/res/values-ca/strings.xml +++ b/core/res/res/values-ca/strings.xml @@ -621,6 +621,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Maquinari biomètric no disponible"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"S\'ha cancel·lat l\'autenticació"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"No s\'ha reconegut"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"No s\'ha reconegut la cara"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"S\'ha cancel·lat l\'autenticació"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"No s\'ha definit cap PIN, patró o contrasenya"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Error en l\'autenticació"</string> diff --git a/core/res/res/values-cs/strings.xml b/core/res/res/values-cs/strings.xml index bb07a799eadb..2d83ccdb5cae 100644 --- a/core/res/res/values-cs/strings.xml +++ b/core/res/res/values-cs/strings.xml @@ -622,6 +622,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometrický hardware není k dispozici"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Ověření bylo zrušeno"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Nerozpoznáno"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Obličej nebyl rozpoznán"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Ověření bylo zrušeno"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Není nastaven žádný PIN, gesto ani heslo"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Při ověřování došlo k chybě"</string> diff --git a/core/res/res/values-da/strings.xml b/core/res/res/values-da/strings.xml index 43f6a8cdae03..c1f0e711c1d3 100644 --- a/core/res/res/values-da/strings.xml +++ b/core/res/res/values-da/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometrisk hardware er ikke tilgængelig"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Godkendelsen blev annulleret"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Ikke genkendt"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Ansigt blev ikke genkendt"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Godkendelsen blev annulleret"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Der er ikke angivet pinkode, mønster eller adgangskode"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Der opstod fejl i forbindelse med godkendelse"</string> diff --git a/core/res/res/values-de/strings.xml b/core/res/res/values-de/strings.xml index 55e845586ef1..ad6cfbf6042e 100644 --- a/core/res/res/values-de/strings.xml +++ b/core/res/res/values-de/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometrische Hardware nicht verfügbar"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Authentifizierung abgebrochen"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Nicht erkannt"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Gesicht nicht erkannt"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Authentifizierung abgebrochen"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Keine PIN, kein Muster und kein Passwort festgelegt"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Fehler bei der Authentifizierung"</string> diff --git a/core/res/res/values-el/strings.xml b/core/res/res/values-el/strings.xml index 5f7306ade927..4ea1eab5cd74 100644 --- a/core/res/res/values-el/strings.xml +++ b/core/res/res/values-el/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Δεν υπάρχει διαθέσιμος βιομετρικός εξοπλισμός"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Ο έλεγχος ταυτότητας ακυρώθηκε"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Δεν αναγνωρίστηκε"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Το πρόσωπο δεν αναγνωρίστηκε"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Ο έλεγχος ταυτότητας ακυρώθηκε"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Δεν έχει οριστεί PIN, μοτίβο ή κωδικός πρόσβασης"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Σφάλμα κατά τον έλεγχο ταυτότητας"</string> diff --git a/core/res/res/values-en-rAU/strings.xml b/core/res/res/values-en-rAU/strings.xml index 73b2968ab9cc..675add93a146 100644 --- a/core/res/res/values-en-rAU/strings.xml +++ b/core/res/res/values-en-rAU/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometric hardware unavailable"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Authentication cancelled"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Not recognised"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Face not recognised"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Authentication cancelled"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"No pin, pattern or password set"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Error while authenticating"</string> diff --git a/core/res/res/values-en-rCA/strings.xml b/core/res/res/values-en-rCA/strings.xml index 73017c11ac25..7b6ccf31f886 100644 --- a/core/res/res/values-en-rCA/strings.xml +++ b/core/res/res/values-en-rCA/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometric hardware unavailable"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Authentication canceled"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Not recognized"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Face not recognized"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Authentication canceled"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"No pin, pattern, or password set"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Error authenticating"</string> diff --git a/core/res/res/values-en-rGB/strings.xml b/core/res/res/values-en-rGB/strings.xml index aeabd0096974..a1c7872e5110 100644 --- a/core/res/res/values-en-rGB/strings.xml +++ b/core/res/res/values-en-rGB/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometric hardware unavailable"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Authentication cancelled"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Not recognised"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Face not recognised"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Authentication cancelled"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"No pin, pattern or password set"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Error while authenticating"</string> diff --git a/core/res/res/values-en-rIN/strings.xml b/core/res/res/values-en-rIN/strings.xml index 92f989728902..7c467e8c841f 100644 --- a/core/res/res/values-en-rIN/strings.xml +++ b/core/res/res/values-en-rIN/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometric hardware unavailable"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Authentication cancelled"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Not recognised"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Face not recognised"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Authentication cancelled"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"No pin, pattern or password set"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Error while authenticating"</string> diff --git a/core/res/res/values-en-rXC/strings.xml b/core/res/res/values-en-rXC/strings.xml index b560c017b7f8..4f6c12069810 100644 --- a/core/res/res/values-en-rXC/strings.xml +++ b/core/res/res/values-en-rXC/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometric hardware unavailable"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Authentication canceled"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Not recognized"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Face not recognized"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Authentication canceled"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"No pin, pattern, or password set"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Error authenticating"</string> diff --git a/core/res/res/values-es-rUS/strings.xml b/core/res/res/values-es-rUS/strings.xml index 655e123ef6ac..372c28c3d0c2 100644 --- a/core/res/res/values-es-rUS/strings.xml +++ b/core/res/res/values-es-rUS/strings.xml @@ -621,6 +621,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"No hay hardware biométrico disponible"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Se canceló la autenticación"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"No se reconoció"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"No se reconoció el rostro"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Se canceló la autenticación"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"No se estableció ningún PIN, patrón ni contraseña"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Error de autenticación"</string> diff --git a/core/res/res/values-es/strings.xml b/core/res/res/values-es/strings.xml index d5dc1bc0a369..ec46619b17db 100644 --- a/core/res/res/values-es/strings.xml +++ b/core/res/res/values-es/strings.xml @@ -621,6 +621,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Hardware biométrico no disponible"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Autenticación cancelada"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"No se reconoce"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Cara no reconocida"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Autenticación cancelada"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"No se ha definido el PIN, el patrón o la contraseña"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"No se ha podido autenticar"</string> diff --git a/core/res/res/values-et/strings.xml b/core/res/res/values-et/strings.xml index e4c99482166d..c50f3ba19831 100644 --- a/core/res/res/values-et/strings.xml +++ b/core/res/res/values-et/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biomeetriline riistvara ei ole saadaval"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Autentimine tühistati"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Ei tuvastatud"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Nägu ei tuvastatud"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Autentimine tühistati"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"PIN-koodi, mustrit ega parooli pole määratud"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Viga autentimisel"</string> diff --git a/core/res/res/values-eu/strings.xml b/core/res/res/values-eu/strings.xml index a165af18472e..51f41ef5535b 100644 --- a/core/res/res/values-eu/strings.xml +++ b/core/res/res/values-eu/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Hardware biometrikoa ez dago erabilgarri"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Utzi da autentifikazioa"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Ez da ezagutu"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Ez da ezagutu aurpegia"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Utzi egin da autentifikazioa"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Ez da ezarri PIN koderik, eredurik edo pasahitzik"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Errorea autentifikatzean"</string> diff --git a/core/res/res/values-fa/strings.xml b/core/res/res/values-fa/strings.xml index 1620ad586334..b0293b04f466 100644 --- a/core/res/res/values-fa/strings.xml +++ b/core/res/res/values-fa/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"سختافزار زیستسنجی دردسترس نیست"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"اصالتسنجی لغو شد"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"شناسایی نشد"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"چهره شناسایی نشد"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"اصالتسنجی لغو شد"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"پین، الگو یا گذرواژهای تنظیم نشده است"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"خطا هنگام اصالتسنجی"</string> diff --git a/core/res/res/values-fi/strings.xml b/core/res/res/values-fi/strings.xml index 5f120c58d766..edfae7648bb9 100644 --- a/core/res/res/values-fi/strings.xml +++ b/core/res/res/values-fi/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometrinen laitteisto ei käytettävissä"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Todennus peruutettu"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Ei tunnistettu"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Kasvoja ei tunnistettu"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Todennus peruutettu"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"PIN-koodia, kuviota tai salasanaa ei ole asetettu"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Virhe todennuksessa"</string> diff --git a/core/res/res/values-fr-rCA/strings.xml b/core/res/res/values-fr-rCA/strings.xml index e72b41e2bf39..bca6087c94a4 100644 --- a/core/res/res/values-fr-rCA/strings.xml +++ b/core/res/res/values-fr-rCA/strings.xml @@ -621,6 +621,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Matériel biométrique indisponible"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Authentification annulée"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Données biométriques non reconnues"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Visage non reconnu"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Authentification annulée"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Aucun NIP, schéma ou mot de passe défini"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Erreur d\'authentification"</string> diff --git a/core/res/res/values-fr/strings.xml b/core/res/res/values-fr/strings.xml index c162672c7ce0..221c526732ee 100644 --- a/core/res/res/values-fr/strings.xml +++ b/core/res/res/values-fr/strings.xml @@ -621,6 +621,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Matériel biométrique indisponible"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Authentification annulée"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Non reconnue"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Visage non reconnu"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Authentification annulée"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Aucun code, schéma ni mot de passe n\'est défini"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Erreur d\'authentification"</string> diff --git a/core/res/res/values-gl/strings.xml b/core/res/res/values-gl/strings.xml index cd1ee9366949..c80fcfb55b0d 100644 --- a/core/res/res/values-gl/strings.xml +++ b/core/res/res/values-gl/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"O hardware biométrico non está dispoñible"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Cancelouse a autenticación"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Non se recoñeceu"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Non se recoñeceu a cara"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Cancelouse a autenticación"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Non se estableceu ningún PIN, padrón ou contrasinal"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Produciuse un erro ao realizar a autenticación"</string> diff --git a/core/res/res/values-gu/strings.xml b/core/res/res/values-gu/strings.xml index ab3859e39800..703bb440f403 100644 --- a/core/res/res/values-gu/strings.xml +++ b/core/res/res/values-gu/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"બાયોમેટ્રિક હાર્ડવેર ઉપલબ્ધ નથી"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"પ્રમાણીકરણ રદ કર્યું"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"ઓળખાયેલ નથી"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"ચહેરો ઓળખાયો નથી"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"પ્રમાણીકરણ રદ કર્યું"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"કોઈ પિન, પૅટર્ન અથવા પાસવર્ડ સેટ કરેલો નથી"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"પ્રમાણિત કરવામાં ભૂલ આવી"</string> diff --git a/core/res/res/values-hi/strings.xml b/core/res/res/values-hi/strings.xml index 4c66168f36a8..bfa4f3943953 100644 --- a/core/res/res/values-hi/strings.xml +++ b/core/res/res/values-hi/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"बायोमेट्रिक हार्डवेयर उपलब्ध नहीं है"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"प्रमाणीकरण रद्द किया गया"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"पहचान नहीं हो पाई"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"चेहरा नहीं पहचाना गया"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"प्रमाणीकरण रद्द किया गया"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"पिन, पैटर्न या पासवर्ड सेट नहीं है"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"गड़बड़ी की पुष्टि की जा रही है"</string> diff --git a/core/res/res/values-hr/strings.xml b/core/res/res/values-hr/strings.xml index eb8d11118970..d655dd4beed0 100644 --- a/core/res/res/values-hr/strings.xml +++ b/core/res/res/values-hr/strings.xml @@ -621,6 +621,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometrijski hardver nije dostupan"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Autentifikacija otkazana"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Nije prepoznato"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Lice nije prepoznato"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Autentifikacija otkazana"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Nisu postavljeni PIN, uzorak ni zaporka"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Pogreška prilikom autentifikacije"</string> diff --git a/core/res/res/values-hu/strings.xml b/core/res/res/values-hu/strings.xml index 793fc8699d1e..e5efd16f3b3c 100644 --- a/core/res/res/values-hu/strings.xml +++ b/core/res/res/values-hu/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometrikus hardver nem áll rendelkezésre"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Hitelesítés megszakítva"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Nem ismerhető fel"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Sikertelen arcfelismerés"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Hitelesítés megszakítva"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Nem állított be PIN-kódot, mintát vagy jelszót."</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Hiba történt a hitelesítés közben"</string> diff --git a/core/res/res/values-hy/strings.xml b/core/res/res/values-hy/strings.xml index da1b5dc71889..f831700c5721 100644 --- a/core/res/res/values-hy/strings.xml +++ b/core/res/res/values-hy/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Կենսաչափական սարքը հասանելի չէ"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Նույնականացումը չեղարկվեց"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Չհաջողվեց ճանաչել"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Դեմքը չի ճանաչվել"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Նույնականացումը չեղարկվեց"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Ավելացրեք PIN կոդ, նախշ կամ գաղտնաբառ։"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Չհաջողվեց նույնականացնել"</string> diff --git a/core/res/res/values-in/strings.xml b/core/res/res/values-in/strings.xml index af5e5fe1d70f..92d85c7a8ad3 100644 --- a/core/res/res/values-in/strings.xml +++ b/core/res/res/values-in/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Hardware biometrik tidak tersedia"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Autentikasi dibatalkan"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Tidak dikenali"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Wajah tidak dikenali"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Autentikasi dibatalkan"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Tidak ada PIN, pola, atau sandi yang disetel"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Error saat mengautentikasi"</string> diff --git a/core/res/res/values-is/strings.xml b/core/res/res/values-is/strings.xml index b43935f17844..1c7f5b3e0352 100644 --- a/core/res/res/values-is/strings.xml +++ b/core/res/res/values-is/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Lífkennavélbúnaður ekki tiltækur"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Hætt við auðkenningu"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Þekktist ekki"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Andlit þekkist ekki"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Hætt við auðkenningu"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Ekkert PIN-númer, mynstur eða aðgangsorð stillt"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Villa við auðkenningu"</string> diff --git a/core/res/res/values-it/strings.xml b/core/res/res/values-it/strings.xml index f34b103360cd..fe663ddebe71 100644 --- a/core/res/res/values-it/strings.xml +++ b/core/res/res/values-it/strings.xml @@ -621,6 +621,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Hardware biometrico non disponibile"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Autenticazione annullata"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Non riconosciuto"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Volto non riconosciuto"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Autenticazione annullata"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Non hai impostato PIN, sequenza o password"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Errore durante l\'autenticazione"</string> diff --git a/core/res/res/values-iw/strings.xml b/core/res/res/values-iw/strings.xml index cc032849fc28..1bba6e51580c 100644 --- a/core/res/res/values-iw/strings.xml +++ b/core/res/res/values-iw/strings.xml @@ -621,6 +621,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"חומרה ביומטרית לא זמינה"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"האימות בוטל"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"לא זוהתה"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"הפנים לא זוהו"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"האימות בוטל"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"עוד לא הוגדרו קוד אימות, קו ביטול נעילה או סיסמה"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"שגיאה באימות"</string> diff --git a/core/res/res/values-ja/strings.xml b/core/res/res/values-ja/strings.xml index 66d6852e0e2e..26a3ff7cd1ad 100644 --- a/core/res/res/values-ja/strings.xml +++ b/core/res/res/values-ja/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"生体認証ハードウェアが利用できません"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"認証をキャンセルしました"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"認識されませんでした"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"顔を認識できません"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"認証をキャンセルしました"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"PIN、パターン、パスワードが設定されていません"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"エラー認証"</string> diff --git a/core/res/res/values-ka/strings.xml b/core/res/res/values-ka/strings.xml index c7fa7d0b2671..e09aa157a328 100644 --- a/core/res/res/values-ka/strings.xml +++ b/core/res/res/values-ka/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"ბიომეტრიული აპარატურა მიუწვდომელია"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"ავტორიზაცია გაუქმდა"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"არ არის ამოცნობილი"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"სახის ამოცნობა ვერ მოხერხდა"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"ავტორიზაცია გაუქმდა"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"PIN-კოდი, ნიმუში ან პაროლი დაყენებული არ არის"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"შეცდომა ავთენტიკაციისას"</string> diff --git a/core/res/res/values-kk/strings.xml b/core/res/res/values-kk/strings.xml index 09c9bfb3244f..b90fd51281f8 100644 --- a/core/res/res/values-kk/strings.xml +++ b/core/res/res/values-kk/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Биометрикалық жабдық жоқ"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Аутентификациядан бас тартылды."</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Танылмады"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Бет танылмады."</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Аутентификациядан бас тартылды."</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Ешқандай PIN коды, өрнек немесе құпия сөз орнатылмаған."</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Аутентификациялауда қате шықты."</string> diff --git a/core/res/res/values-km/strings.xml b/core/res/res/values-km/strings.xml index 65c19838910b..bca6e52ae8c2 100644 --- a/core/res/res/values-km/strings.xml +++ b/core/res/res/values-km/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"មិនអាចប្រើឧបករណ៍ស្កេនស្នាមម្រាមដៃបានទេ"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"បានបោះបង់ការផ្ទៀងផ្ទាត់"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"មិនអាចសម្គាល់បានទេ"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"មិនស្គាល់មុខ"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"បានបោះបង់ការផ្ទៀងផ្ទាត់"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"គ្មានការកំណត់កូដ pin លំនាំ ឬពាក្យសម្ងាត់ទេ"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"មានបញ្ហាក្នុងការផ្ទៀងផ្ទាត់"</string> diff --git a/core/res/res/values-kn/strings.xml b/core/res/res/values-kn/strings.xml index 1f0cbfcd4767..26aa63d0396b 100644 --- a/core/res/res/values-kn/strings.xml +++ b/core/res/res/values-kn/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"ಬಯೋಮೆಟ್ರಿಕ್ ಹಾರ್ಡ್ವೇರ್ ಲಭ್ಯವಿಲ್ಲ"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"ಪ್ರಮಾಣೀಕರಣವನ್ನು ರದ್ದುಗೊಳಿಸಲಾಗಿದೆ"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"ಗುರುತಿಸಲಾಗಿಲ್ಲ"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"ಮುಖವನ್ನು ಗುರುತಿಸಲಾಗಿಲ್ಲ"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"ಪ್ರಮಾಣೀಕರಣವನ್ನು ರದ್ದುಗೊಳಿಸಲಾಗಿದೆ"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"ಪಿನ್, ಪ್ಯಾಟರ್ನ್ ಅಥವಾ ಪಾಸ್ವರ್ಡ್ ಸೆಟ್ ಮಾಡಿಲ್ಲ"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"ದೃಢೀಕರಿಸುವಾಗ ದೋಷ ಎದುರಾಗಿದೆ"</string> diff --git a/core/res/res/values-ko/strings.xml b/core/res/res/values-ko/strings.xml index e6979ae283f8..18fe689d4466 100644 --- a/core/res/res/values-ko/strings.xml +++ b/core/res/res/values-ko/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"생체 인식 하드웨어를 사용할 수 없음"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"인증이 취소되었습니다."</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"인식할 수 없음"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"얼굴을 인식할 수 없습니다."</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"인증이 취소되었습니다."</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"PIN, 패턴, 비밀번호가 설정되지 않음"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"인증 오류"</string> diff --git a/core/res/res/values-ky/strings.xml b/core/res/res/values-ky/strings.xml index 9fe0dfe503c6..d7e5e9df4141 100644 --- a/core/res/res/values-ky/strings.xml +++ b/core/res/res/values-ky/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Биометрикалык аппарат жеткиликсиз"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Аныктыгын текшерүү жокко чыгарылды"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Таанылган жок"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Жүз таанылган жок"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Аныктыгын текшерүү жокко чыгарылды"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"PIN код, графикалык ачкыч же сырсөз коюлган жок"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Аутентификация катасы"</string> diff --git a/core/res/res/values-lo/strings.xml b/core/res/res/values-lo/strings.xml index 15517b5ee3e3..b600d2edcbd1 100644 --- a/core/res/res/values-lo/strings.xml +++ b/core/res/res/values-lo/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"ຮາດແວຊີວະມິຕິບໍ່ສາມາດໃຊ້ໄດ້"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"ຍົກເລີກການຮັບຮອງຄວາມຖືກຕ້ອງແລ້ວ"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"ບໍ່ຮັບຮູ້"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"ບໍ່ສາມາດຈຳແນກໜ້າໄດ້"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"ຍົກເລີກການຮັບຮອງຄວາມຖືກຕ້ອງແລ້ວ"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"ບໍ່ໄດ້ຕັ້ງ PIN, ຮູບແບບປົດລັອກ ຫຼື ລະຫັດຜ່ານ"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"ເກີດຄວາມຜິດພາດໃນການພິສູດຢືນຢັນ"</string> diff --git a/core/res/res/values-lt/strings.xml b/core/res/res/values-lt/strings.xml index 6a12170004c8..e3f1952f3ec2 100644 --- a/core/res/res/values-lt/strings.xml +++ b/core/res/res/values-lt/strings.xml @@ -622,6 +622,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometrinė aparatinė įranga nepasiekiama"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Autentifikavimas atšauktas"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Neatpažinta"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Veidas neatpažintas"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Autentifikavimas atšauktas"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Nenustatytas PIN kodas, atrakinimo piešinys arba slaptažodis"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Autentifikuojant įvyko klaida"</string> diff --git a/core/res/res/values-lv/strings.xml b/core/res/res/values-lv/strings.xml index fade02ed8dac..d874fddf5d5a 100644 --- a/core/res/res/values-lv/strings.xml +++ b/core/res/res/values-lv/strings.xml @@ -621,6 +621,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometrisko datu aparatūra nav pieejama"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Autentifikācija ir atcelta"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Dati nav atpazīti"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Seja netika atpazīta"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Autentifikācija ir atcelta"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"PIN, kombinācija vai parole nav iestatīta"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Autentifikācijas kļūda"</string> diff --git a/core/res/res/values-mk/strings.xml b/core/res/res/values-mk/strings.xml index 4fb53a7e510a..ef26b78f97a5 100644 --- a/core/res/res/values-mk/strings.xml +++ b/core/res/res/values-mk/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Биометрискиот хардвер е недостапен"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Проверката е откажана"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Непознат"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Ликот не е препознаен"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Проверката е откажана"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Не е поставен PIN, шема или лозинка"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Грешка при проверката"</string> diff --git a/core/res/res/values-ml/strings.xml b/core/res/res/values-ml/strings.xml index c3b86e382399..0ca9b379cbb1 100644 --- a/core/res/res/values-ml/strings.xml +++ b/core/res/res/values-ml/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"ബയോമെട്രിക് ഹാർഡ്വെയർ ലഭ്യമല്ല"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"പരിശോധിച്ചുറപ്പിക്കൽ റദ്ദാക്കി"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"തിരിച്ചറിഞ്ഞില്ല"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"മുഖം തിരിച്ചറിഞ്ഞിട്ടില്ല"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"പരിശോധിച്ചുറപ്പിക്കൽ റദ്ദാക്കി"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"പിന്നോ പാറ്റേണോ പാസ്വേഡോ സജ്ജീകരിച്ചിട്ടില്ല"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"പിശക് പരിശോധിച്ചുറപ്പിക്കുന്നു"</string> diff --git a/core/res/res/values-mn/strings.xml b/core/res/res/values-mn/strings.xml index b69fb83dde35..8df63ece2264 100644 --- a/core/res/res/values-mn/strings.xml +++ b/core/res/res/values-mn/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Биометрийн техник хангамж боломжгүй байна"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Нотолгоог цуцаллаа"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Таниагүй"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Царайг таньсангүй"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Нотолгоог цуцаллаа"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Тохируулсан пин, хээ эсвэл нууц үг алга"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Баталгаажуулахад алдаа гарлаа"</string> diff --git a/core/res/res/values-mr/strings.xml b/core/res/res/values-mr/strings.xml index 5f6b35dfb887..432ffce831b2 100644 --- a/core/res/res/values-mr/strings.xml +++ b/core/res/res/values-mr/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"बायोमेट्रिक हार्डवेअर उपलब्ध नाही"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"ऑथेंटिकेशन रद्द केले"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"ओळखले नाही"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"चेहरा ओळखता आला नाही"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"ऑथेंटिकेशन रद्द केले"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"कोणताही पिन, पॅटर्न किंवा पासवर्ड सेट केलेला नाही"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"एरर ऑथेंटिकेट करत आहे"</string> diff --git a/core/res/res/values-ms/strings.xml b/core/res/res/values-ms/strings.xml index f62c70690f05..ec737c916c92 100644 --- a/core/res/res/values-ms/strings.xml +++ b/core/res/res/values-ms/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Perkakasan biometrik tidak tersedia"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Pengesahan dibatalkan"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Tidak dikenali"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Wajah tidak dikenali"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Pengesahan dibatalkan"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Pin, corak atau kata laluan tidak ditetapkan"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Ralat semasa membuat pengesahan"</string> diff --git a/core/res/res/values-my/strings.xml b/core/res/res/values-my/strings.xml index d2fc38fe01ab..8a34f9323eef 100644 --- a/core/res/res/values-my/strings.xml +++ b/core/res/res/values-my/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"ဇီဝအချက်အလက်သုံး ကွန်ပျူတာစက်ပစ္စည်း မရရှိနိုင်ပါ"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"အထောက်အထားစိစစ်ခြင်းကို ပယ်ဖျက်လိုက်သည်"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"မသိ"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"မျက်နှာကို မသိရှိပါ"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"အထောက်အထားစိစစ်ခြင်းကို ပယ်ဖျက်လိုက်သည်"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"ပင်နံပါတ်၊ လော့ခ်ပုံစံ သို့မဟုတ် စကားဝှက် သတ်မှတ်မထားပါ"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"အထောက်အထားစိစစ်ရာတွင် အမှားအယွင်းရှိနေသည်"</string> diff --git a/core/res/res/values-nb/strings.xml b/core/res/res/values-nb/strings.xml index 15ef328cfbb0..f8c8aaad345b 100644 --- a/core/res/res/values-nb/strings.xml +++ b/core/res/res/values-nb/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometrisk maskinvare er utilgjengelig"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Autentiseringen er avbrutt"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Ikke gjenkjent"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Gjenkjenner ikke ansiktet"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Autentiseringen er avbrutt"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"PIN-kode, mønster eller passord er ikke angitt"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Feil under autentiseringen"</string> diff --git a/core/res/res/values-ne/strings.xml b/core/res/res/values-ne/strings.xml index 92ee0a1cad69..bbb19925b661 100644 --- a/core/res/res/values-ne/strings.xml +++ b/core/res/res/values-ne/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"बायोमेट्रिक हार्डवेयर उपलब्ध छैन"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"प्रमाणीकरण रद्द गरियो"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"पहिचान भएन"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"अनुहार पहिचान गर्न सकिएन"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"प्रमाणीकरण रद्द गरियो"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"कुनै पनि PIN, ढाँचा वा पासवर्ड सेट गरिएको छैन"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"प्रमाणित गर्ने क्रममा त्रुटि भयो"</string> diff --git a/core/res/res/values-nl/strings.xml b/core/res/res/values-nl/strings.xml index 80082c86cc7b..5457872a29f6 100644 --- a/core/res/res/values-nl/strings.xml +++ b/core/res/res/values-nl/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometrische hardware niet beschikbaar"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Verificatie geannuleerd"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Niet herkend"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Gezicht niet herkend"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Verificatie geannuleerd"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Geen pincode, patroon of wachtwoord ingesteld"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Fout bij verificatie"</string> diff --git a/core/res/res/values-or/strings.xml b/core/res/res/values-or/strings.xml index 4dba4ab8621e..04fd4da5fc23 100644 --- a/core/res/res/values-or/strings.xml +++ b/core/res/res/values-or/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"ବାୟୋମେଟ୍ରିକ୍ ହାର୍ଡୱେର୍ ଉପଲବ୍ଧ ନାହିଁ"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"ପ୍ରାମାଣିକତାକୁ ବାତିଲ୍ କରାଯାଇଛି"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"ଚିହ୍ନଟ ହେଲାନାହିଁ"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"ଫେସ ଚିହ୍ନଟ କରାଯାଇନାହିଁ"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"ପ୍ରାମାଣିକତାକୁ ବାତିଲ୍ କରାଯାଇଛି"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"କୌଣସି ପିନ୍, ପେଟେର୍ନ ବା ପାସ୍ୱର୍ଡ ସେଟ୍ ନାହିଁ"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"ପ୍ରାମାଣିକରଣ କରିବା ସମୟରେ ତ୍ରୁଟି"</string> diff --git a/core/res/res/values-pa/strings.xml b/core/res/res/values-pa/strings.xml index 9c61e5b3f03c..0d0e0c899f6d 100644 --- a/core/res/res/values-pa/strings.xml +++ b/core/res/res/values-pa/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"ਬਾਇਓਮੈਟ੍ਰਿਕ ਹਾਰਡਵੇਅਰ ਉਪਲਬਧ ਨਹੀਂ ਹੈ"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"ਪ੍ਰਮਾਣੀਕਰਨ ਰੱਦ ਕੀਤਾ ਗਿਆ"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"ਪਛਾਣ ਨਹੀਂ ਹੋਈ"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"ਚਿਹਰੇ ਦੀ ਪਛਾਣ ਨਹੀਂ ਹੋਈ"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"ਪ੍ਰਮਾਣੀਕਰਨ ਰੱਦ ਕੀਤਾ ਗਿਆ"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"ਕੋਈ ਪਿੰਨ, ਪੈਟਰਨ ਜਾਂ ਪਾਸਵਰਡ ਸੈੱਟ ਨਹੀਂ ਕੀਤਾ ਗਿਆ"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"ਗੜਬੜ ਨੂੰ ਪ੍ਰਮਾਣਿਤ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string> diff --git a/core/res/res/values-pl/strings.xml b/core/res/res/values-pl/strings.xml index 7a8684f4e8bc..f72ff68499fa 100644 --- a/core/res/res/values-pl/strings.xml +++ b/core/res/res/values-pl/strings.xml @@ -622,6 +622,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Sprzęt biometryczny niedostępny"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Anulowano uwierzytelnianie"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Nie rozpoznano"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Nie rozpoznano twarzy"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Anulowano uwierzytelnianie"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Nie ustawiono kodu PIN, wzoru ani hasła"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Podczas uwierzytelniania wystąpił błąd"</string> diff --git a/core/res/res/values-pt-rBR/strings.xml b/core/res/res/values-pt-rBR/strings.xml index 0a6f769a13dc..72cb00e15929 100644 --- a/core/res/res/values-pt-rBR/strings.xml +++ b/core/res/res/values-pt-rBR/strings.xml @@ -621,6 +621,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Hardware biométrico indisponível"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Autenticação cancelada"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Não reconhecido"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Rosto não reconhecido"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Autenticação cancelada"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Nenhum PIN, padrão ou senha configurado"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Erro na autenticação"</string> diff --git a/core/res/res/values-pt-rPT/strings.xml b/core/res/res/values-pt-rPT/strings.xml index 96242e16dcae..2158dfdc20e5 100644 --- a/core/res/res/values-pt-rPT/strings.xml +++ b/core/res/res/values-pt-rPT/strings.xml @@ -621,6 +621,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Hardware biométrico indisponível."</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Autenticação cancelada"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Não reconhecido."</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Rosto não reconhecido"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Autenticação cancelada"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Nenhum PIN, padrão ou palavra-passe definidos."</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Erro ao autenticar."</string> diff --git a/core/res/res/values-pt/strings.xml b/core/res/res/values-pt/strings.xml index 0a6f769a13dc..72cb00e15929 100644 --- a/core/res/res/values-pt/strings.xml +++ b/core/res/res/values-pt/strings.xml @@ -621,6 +621,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Hardware biométrico indisponível"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Autenticação cancelada"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Não reconhecido"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Rosto não reconhecido"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Autenticação cancelada"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Nenhum PIN, padrão ou senha configurado"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Erro na autenticação"</string> diff --git a/core/res/res/values-ro/strings.xml b/core/res/res/values-ro/strings.xml index 2c58ed5ac323..10daa6a9ff88 100644 --- a/core/res/res/values-ro/strings.xml +++ b/core/res/res/values-ro/strings.xml @@ -621,6 +621,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Hardware biometric indisponibil"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Autentificarea a fost anulată"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Nu este recunoscut"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Fața nu a fost recunoscută"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Autentificarea a fost anulată"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Nu este setat un cod PIN, un model sau o parolă"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Eroare la autentificare"</string> diff --git a/core/res/res/values-ru/strings.xml b/core/res/res/values-ru/strings.xml index e48c48711a3b..f8f323ffc805 100644 --- a/core/res/res/values-ru/strings.xml +++ b/core/res/res/values-ru/strings.xml @@ -622,6 +622,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Биометрическое оборудование недоступно"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Аутентификация отменена"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Не распознано"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Лицо не распознано."</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Аутентификация отменена"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Укажите PIN-код, пароль или графический ключ"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Ошибка аутентификации."</string> diff --git a/core/res/res/values-si/strings.xml b/core/res/res/values-si/strings.xml index 0df5c652a35a..a62caebb5966 100644 --- a/core/res/res/values-si/strings.xml +++ b/core/res/res/values-si/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"ජීවමිතික දෘඪාංග ලබා ගත නොහැකිය"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"සත්යාපනය අවලංගු කළා"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"හඳුනා නොගන්නා ලදී"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"මුහුණ හඳුනා නොගන්නා ලදි"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"සත්යාපනය අවලංගු කළා"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"රහස් අංක, රටා, හෝ මුරපද කිසිවක් සකසා නැත"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"සත්යාපනය කිරීමේ දෝෂයකි"</string> diff --git a/core/res/res/values-sk/strings.xml b/core/res/res/values-sk/strings.xml index d4cfcf9b5524..fc18330cdecf 100644 --- a/core/res/res/values-sk/strings.xml +++ b/core/res/res/values-sk/strings.xml @@ -622,6 +622,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometrický hardvér nie je k dispozícii"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Overenie bolo zrušené"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Nerozpoznané"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Tvár nebola rozpoznaná"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Overenie bolo zrušené"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Nie je nastavený PIN, vzor ani heslo"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Chyba overenia"</string> diff --git a/core/res/res/values-sl/strings.xml b/core/res/res/values-sl/strings.xml index 1b06b0f31e0a..4bd350c88453 100644 --- a/core/res/res/values-sl/strings.xml +++ b/core/res/res/values-sl/strings.xml @@ -622,6 +622,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Strojna oprema za biometrične podatke ni na voljo"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Preverjanje pristnosti je preklicano"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Ni prepoznano"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Obraz ni prepoznan"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Preverjanje pristnosti je preklicano"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Nastavljena ni nobena koda PIN, vzorec ali geslo"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Napaka pri preverjanju pristnosti"</string> diff --git a/core/res/res/values-sq/strings.xml b/core/res/res/values-sq/strings.xml index 82d92c1d9039..512dbc7b37b6 100644 --- a/core/res/res/values-sq/strings.xml +++ b/core/res/res/values-sq/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Nuk ofrohet harduer biometrik"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Vërtetimi u anulua"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Nuk njihet"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Fytyra nuk njihet"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Vërtetimi u anulua"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Nuk është vendosur kod PIN, motiv ose fjalëkalim"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Gabim gjatë vërtetimit"</string> diff --git a/core/res/res/values-sr/strings.xml b/core/res/res/values-sr/strings.xml index ef2456e24bae..ccd638e058de 100644 --- a/core/res/res/values-sr/strings.xml +++ b/core/res/res/values-sr/strings.xml @@ -621,6 +621,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Биометријски хардвер није доступан"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Потврда идентитета је отказана"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Није препознато"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Лице није препознато"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Потврда идентитета је отказана"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Нисте подесили ни PIN, ни шаблон, ни лозинку"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Грешка при потврди идентитета"</string> diff --git a/core/res/res/values-sv/strings.xml b/core/res/res/values-sv/strings.xml index c40b2055986d..4b5d45e79d75 100644 --- a/core/res/res/values-sv/strings.xml +++ b/core/res/res/values-sv/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometrisk maskinvara är inte tillgänglig"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Autentiseringen avbröts"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Identifierades inte"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Ansiktet känns inte igen"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Autentiseringen avbröts"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Pinkod, mönster eller lösenord har inte angetts"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Ett fel uppstod vid autentiseringen"</string> diff --git a/core/res/res/values-sw/strings.xml b/core/res/res/values-sw/strings.xml index caf09cbd81c6..5e30e0d29bc9 100644 --- a/core/res/res/values-sw/strings.xml +++ b/core/res/res/values-sw/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Maunzi ya bayometriki hayapatikani"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Imeghairi uthibitishaji"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Hayatambuliki"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Imeshindwa kutambua uso"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Imeghairi uthibitishaji"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Hujaweka pin, mchoro au nenosiri"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Hitilafu imetokea wakati wa uthibitishaji"</string> diff --git a/core/res/res/values-ta/strings.xml b/core/res/res/values-ta/strings.xml index 4c765ed52773..26442bf553e9 100644 --- a/core/res/res/values-ta/strings.xml +++ b/core/res/res/values-ta/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"பயோமெட்ரிக் வன்பொருள் இல்லை"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"அங்கீகரிப்பு ரத்தானது"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"அடையாளங்காணபடவில்லை"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"முகத்தைக் கண்டறிய முடியவில்லை"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"அங்கீகரிப்பு ரத்தானது"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"பின்னோ, பேட்டர்னோ, கடவுச்சொல்லோ அமைக்கப்படவில்லை"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"அங்கீகரிப்பதில் பிழை"</string> diff --git a/core/res/res/values-te/strings.xml b/core/res/res/values-te/strings.xml index 351acb294467..4dfcc5f2c1e4 100644 --- a/core/res/res/values-te/strings.xml +++ b/core/res/res/values-te/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"బయోమెట్రిక్ హార్డ్వేర్ అందుబాటులో లేదు"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"ప్రమాణీకరణ రద్దు చేయబడింది"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"గుర్తించలేదు"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"ముఖం గుర్తించబడలేదు"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"ప్రమాణీకరణ రద్దు చేయబడింది"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"పిన్, ఆకృతి లేదా పాస్వర్డ్ సెట్ చేయబడలేదు"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"ప్రామాణీకరిస్తున్నప్పుడు ఎర్రర్ ఏర్పడింది"</string> diff --git a/core/res/res/values-th/strings.xml b/core/res/res/values-th/strings.xml index 107a66a77710..916e857acc77 100644 --- a/core/res/res/values-th/strings.xml +++ b/core/res/res/values-th/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"ฮาร์ดแวร์ไบโอเมตริกไม่พร้อมใช้งาน"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"ยกเลิกการตรวจสอบสิทธิ์แล้ว"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"ไม่รู้จัก"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"ไม่รู้จักใบหน้า"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"ยกเลิกการตรวจสอบสิทธิ์แล้ว"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"ไม่ได้ตั้ง PIN, รูปแบบ หรือรหัสผ่าน"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"การตรวจสอบข้อผิดพลาด"</string> diff --git a/core/res/res/values-tl/strings.xml b/core/res/res/values-tl/strings.xml index 549882b8480d..786c324642a8 100644 --- a/core/res/res/values-tl/strings.xml +++ b/core/res/res/values-tl/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Walang biometric hardware"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Nakansela ang pag-authenticate"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Hindi nakilala"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Hindi nakilala ang mukha"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Nakansela ang pag-authenticate"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Walang itinakdang pin, pattern, o password"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Nagkaroon ng error sa pag-authenticate"</string> diff --git a/core/res/res/values-tr/strings.xml b/core/res/res/values-tr/strings.xml index f9f9c3a81931..ac714ed6e891 100644 --- a/core/res/res/values-tr/strings.xml +++ b/core/res/res/values-tr/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biyometrik donanım kullanılamıyor"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Kimlik doğrulama iptal edildi"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Tanınmadı"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Yüz tanınmadı"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Kimlik doğrulama iptal edildi"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"PIN, desen veya şifre seti yok"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Kimlik doğrulama sırasında hata oluştu"</string> diff --git a/core/res/res/values-uk/strings.xml b/core/res/res/values-uk/strings.xml index c6ba301a0a7f..e25d4561f36b 100644 --- a/core/res/res/values-uk/strings.xml +++ b/core/res/res/values-uk/strings.xml @@ -622,6 +622,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Біометричне апаратне забезпечення недоступне"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Автентифікацію скасовано"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Не розпізнано"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Обличчя не розпізнано"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Автентифікацію скасовано"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Не вказано PIN-код, ключ або пароль"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Помилка автентифікації"</string> diff --git a/core/res/res/values-ur/strings.xml b/core/res/res/values-ur/strings.xml index e692be7d9f7c..e1c275296885 100644 --- a/core/res/res/values-ur/strings.xml +++ b/core/res/res/values-ur/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"بایومیٹرک ہارڈ ویئر دستیاب نہیں ہے"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"تصدیق کا عمل منسوخ ہو گیا"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"تسلیم شدہ نہیں ہے"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"چہرے کی شناخت نہیں ہو سکی"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"تصدیق کا عمل منسوخ ہو گیا"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"کوئی پن، پیٹرن، یا پاس ورڈ سیٹ نہیں ہے"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"خرابی کی توثیق ہو رہی ہے"</string> diff --git a/core/res/res/values-uz/strings.xml b/core/res/res/values-uz/strings.xml index bb9cbd28b680..93ad0ceec86b 100644 --- a/core/res/res/values-uz/strings.xml +++ b/core/res/res/values-uz/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Biometrik sensor ishlamayapti"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Autentifikatsiya bekor qilindi"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Aniqlanmadi"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Yuz aniqlanmadi"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Autentifikatsiya bekor qilindi"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"PIN kod, grafik kalit yoki parol sozlanmagan"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Autentifikatsiya amalga oshmadi"</string> diff --git a/core/res/res/values-vi/strings.xml b/core/res/res/values-vi/strings.xml index 83730d5862df..bfe15619d87f 100644 --- a/core/res/res/values-vi/strings.xml +++ b/core/res/res/values-vi/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"Không có phần cứng sinh trắc học"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Đã hủy xác thực"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Không nhận dạng được"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Không nhận dạng được khuôn mặt"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Đã hủy xác thực"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Chưa đặt mã PIN, hình mở khóa hoặc mật khẩu"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Lỗi khi xác thực"</string> diff --git a/core/res/res/values-zh-rCN/strings.xml b/core/res/res/values-zh-rCN/strings.xml index 80558133a287..d7101f417c9f 100644 --- a/core/res/res/values-zh-rCN/strings.xml +++ b/core/res/res/values-zh-rCN/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"生物识别硬件无法使用"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"身份验证已取消"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"无法识别"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"无法识别面孔"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"身份验证已取消"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"未设置任何 PIN 码、图案和密码"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"进行身份验证时出错"</string> diff --git a/core/res/res/values-zh-rHK/strings.xml b/core/res/res/values-zh-rHK/strings.xml index 305560032d00..a471a42c5d37 100644 --- a/core/res/res/values-zh-rHK/strings.xml +++ b/core/res/res/values-zh-rHK/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"無法使用生物識別硬件"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"已取消驗證"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"未能識別"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"無法辨識面孔"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"已取消驗證"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"未設定 PIN、圖案或密碼"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"驗證時發生錯誤"</string> diff --git a/core/res/res/values-zh-rTW/strings.xml b/core/res/res/values-zh-rTW/strings.xml index 828ab59d7657..d911c531aa4f 100644 --- a/core/res/res/values-zh-rTW/strings.xml +++ b/core/res/res/values-zh-rTW/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"無法使用生物特徵辨識硬體"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"已取消驗證"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"無法辨識"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"無法辨識臉孔"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"已取消驗證"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"未設定 PIN 碼、解鎖圖案或密碼"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"驗證時發生錯誤"</string> diff --git a/core/res/res/values-zu/strings.xml b/core/res/res/values-zu/strings.xml index 270cc4bf1d61..72b440499996 100644 --- a/core/res/res/values-zu/strings.xml +++ b/core/res/res/values-zu/strings.xml @@ -620,6 +620,7 @@      <string name="biometric_error_hw_unavailable" msgid="2494077380540615216">"I-Biometric hardware ayitholakali"</string>      <string name="biometric_error_user_canceled" msgid="6732303949695293730">"Ukufakazela ubuqiniso kukhanseliwe"</string>      <string name="biometric_not_recognized" msgid="5106687642694635888">"Akwaziwa"</string> +    <string name="biometric_face_not_recognized" msgid="5535599455744525200">"Ubuso abaziwa"</string>      <string name="biometric_error_canceled" msgid="8266582404844179778">"Ukufakazela ubuqiniso kukhanseliwe"</string>      <string name="biometric_error_device_not_secured" msgid="3129845065043995924">"Ayikho iphinikhodi, iphethini, noma iphasiwedi esethiwe"</string>      <string name="biometric_error_generic" msgid="6784371929985434439">"Iphutha lokufakazela ubuqiniso"</string> diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml index fd7418542a2b..4ae54a052859 100644 --- a/core/res/res/values/config_telephony.xml +++ b/core/res/res/values/config_telephony.xml @@ -168,8 +168,9 @@      <bool name="ignore_emergency_number_routing_from_db">false</bool>      <java-symbol type="bool" name="ignore_emergency_number_routing_from_db" /> -    <!-- Whether "Virtual DSDA", i.e. in-call IMS connectivity can be provided on both subs with -         only single logical modem, by using its data connection in addition to cellular IMS. --> -    <bool name="config_enable_virtual_dsda">false</bool> -    <java-symbol type="bool" name="config_enable_virtual_dsda" /> +    <!-- Boolean indicating whether allow sending null to modem to clear the previous initial attach +         data profile --> +    <bool name="allow_clear_initial_attach_data_profile">false</bool> +    <java-symbol type="bool" name="allow_clear_initial_attach_data_profile" /> +  </resources> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index a5b2b853fddd..04fef58e973e 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -1789,6 +1789,8 @@      <string name="biometric_error_user_canceled">Authentication canceled</string>      <!-- Message shown by the biometric dialog when biometric is not recognized -->      <string name="biometric_not_recognized">Not recognized</string> +    <!-- Message shown by the biometric dialog when face is not recognized [CHAR LIMIT=50] --> +    <string name="biometric_face_not_recognized">Face not recognized</string>      <!-- Message shown when biometric authentication has been canceled [CHAR LIMIT=50] -->      <string name="biometric_error_canceled">Authentication canceled</string>      <!-- Message returned to applications if BiometricPrompt setAllowDeviceCredentials is enabled but no pin, pattern, or password is set. [CHAR LIMIT=NONE] --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index dc4eafd2e00e..80a997717cc3 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2570,6 +2570,7 @@    <java-symbol type="string" name="biometric_error_hw_unavailable" />    <java-symbol type="string" name="biometric_error_user_canceled" />    <java-symbol type="string" name="biometric_not_recognized" /> +  <java-symbol type="string" name="biometric_face_not_recognized" />    <java-symbol type="string" name="biometric_error_canceled" />    <java-symbol type="string" name="biometric_error_device_not_secured" />    <java-symbol type="string" name="biometric_error_generic" /> @@ -5020,6 +5021,7 @@    <java-symbol type="bool" name="config_batteryStatsResetOnUnplugHighBatteryLevel" />    <java-symbol type="bool" name="config_batteryStatsResetOnUnplugAfterSignificantCharge" /> +    <java-symbol name="materialColorOnSecondaryFixedVariant" type="attr"/>    <java-symbol name="materialColorOnTertiaryFixedVariant" type="attr"/>    <java-symbol name="materialColorSurfaceContainerLowest" type="attr"/> diff --git a/packages/SettingsLib/Spa/tests/Android.bp b/packages/SettingsLib/Spa/tests/Android.bp index b4c67ccda6f2..f9e64aee1513 100644 --- a/packages/SettingsLib/Spa/tests/Android.bp +++ b/packages/SettingsLib/Spa/tests/Android.bp @@ -31,7 +31,6 @@ android_test {          "SpaLib",          "SpaLibTestUtils",          "androidx.compose.runtime_runtime", -        "androidx.lifecycle_lifecycle-runtime-testing",          "androidx.test.ext.junit",          "androidx.test.runner",          "mockito-target-minus-junit4", diff --git a/packages/SettingsLib/Spa/testutils/Android.bp b/packages/SettingsLib/Spa/testutils/Android.bp index 2c1e1c2abc2c..e4d56cc4f2a0 100644 --- a/packages/SettingsLib/Spa/testutils/Android.bp +++ b/packages/SettingsLib/Spa/testutils/Android.bp @@ -29,6 +29,7 @@ android_library {          "androidx.compose.runtime_runtime",          "androidx.compose.ui_ui-test-junit4",          "androidx.compose.ui_ui-test-manifest", +        "androidx.lifecycle_lifecycle-runtime-testing",          "mockito",          "truth-prebuilt",      ], diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/DynamicSummary.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/DynamicSummary.java index c9d9b57c7170..5b7899b1e3af 100644 --- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/DynamicSummary.java +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/DynamicSummary.java @@ -16,7 +16,7 @@  package com.android.settingslib.drawer; -/** Interface for {@link SwitchController} whose instances support dynamic summary */ +/** Interface for {@link EntryController} whose instances support dynamic summary */  public interface DynamicSummary {      /** @return the dynamic summary text */      String getDynamicSummary(); diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/DynamicTitle.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/DynamicTitle.java index af711ddd59c2..cb157734aa6a 100644 --- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/DynamicTitle.java +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/DynamicTitle.java @@ -16,7 +16,7 @@  package com.android.settingslib.drawer; -/** Interface for {@link SwitchController} whose instances support dynamic title */ +/** Interface for {@link EntryController} whose instances support dynamic title */  public interface DynamicTitle {      /** @return the dynamic title text */      String getDynamicTitle(); diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/EntriesProvider.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/EntriesProvider.java new file mode 100644 index 000000000000..1c14c0a72ce4 --- /dev/null +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/EntriesProvider.java @@ -0,0 +1,222 @@ +/* + * 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.settingslib.drawer; + +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.pm.ProviderInfo; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.Log; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * An abstract class for injecting entries to Settings. + */ +public abstract class EntriesProvider extends ContentProvider { +    private static final String TAG = "EntriesProvider"; + +    public static final String METHOD_GET_ENTRY_DATA = "getEntryData"; +    public static final String METHOD_GET_PROVIDER_ICON = "getProviderIcon"; +    public static final String METHOD_GET_DYNAMIC_TITLE = "getDynamicTitle"; +    public static final String METHOD_GET_DYNAMIC_SUMMARY = "getDynamicSummary"; +    public static final String METHOD_IS_CHECKED = "isChecked"; +    public static final String METHOD_ON_CHECKED_CHANGED = "onCheckedChanged"; + +    /** +     * @deprecated use {@link #METHOD_GET_ENTRY_DATA} instead. +     */ +    @Deprecated +    public static final String METHOD_GET_SWITCH_DATA = "getSwitchData"; + +    public static final String EXTRA_ENTRY_DATA = "entry_data"; +    public static final String EXTRA_SWITCH_CHECKED_STATE = "checked_state"; +    public static final String EXTRA_SWITCH_SET_CHECKED_ERROR = "set_checked_error"; +    public static final String EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE = "set_checked_error_message"; + +    /** +     * @deprecated use {@link #EXTRA_ENTRY_DATA} instead. +     */ +    @Deprecated +    public static final String EXTRA_SWITCH_DATA = "switch_data"; + +    private String mAuthority; +    private final Map<String, EntryController> mControllerMap = new LinkedHashMap<>(); +    private final List<Bundle> mEntryDataList = new ArrayList<>(); + +    /** +     * Get a list of {@link EntryController} for this provider. +     */ +    protected abstract List<? extends EntryController> createEntryControllers(); + +    protected EntryController getController(String key) { +        return mControllerMap.get(key); +    } + +    @Override +    public void attachInfo(Context context, ProviderInfo info) { +        mAuthority = info.authority; +        Log.i(TAG, mAuthority); +        super.attachInfo(context, info); +    } + +    @Override +    public boolean onCreate() { +        final List<? extends EntryController> controllers = createEntryControllers(); +        if (controllers == null || controllers.isEmpty()) { +            throw new IllegalArgumentException(); +        } + +        for (EntryController controller : controllers) { +            final String key = controller.getKey(); +            if (TextUtils.isEmpty(key)) { +                throw new NullPointerException("Entry key cannot be null: " +                        + controller.getClass().getSimpleName()); +            } else if (mControllerMap.containsKey(key)) { +                throw new IllegalArgumentException("Entry key " + key + " is duplicated by: " +                        + controller.getClass().getSimpleName()); +            } + +            controller.setAuthority(mAuthority); +            mControllerMap.put(key, controller); +            if (!(controller instanceof PrimarySwitchController)) { +                mEntryDataList.add(controller.getBundle()); +            } +        } +        return true; +    } + +    @Override +    public Bundle call(String method, String uriString, Bundle extras) { +        final Bundle bundle = new Bundle(); +        final String key = extras != null +                ? extras.getString(META_DATA_PREFERENCE_KEYHINT) +                : null; +        if (TextUtils.isEmpty(key)) { +            switch (method) { +                case METHOD_GET_ENTRY_DATA: +                    bundle.putParcelableList(EXTRA_ENTRY_DATA, mEntryDataList); +                    return bundle; +                case METHOD_GET_SWITCH_DATA: +                    bundle.putParcelableList(EXTRA_SWITCH_DATA, mEntryDataList); +                    return bundle; +                default: +                    return null; +            } +        } + +        final EntryController controller = mControllerMap.get(key); +        if (controller == null) { +            return null; +        } + +        switch (method) { +            case METHOD_GET_ENTRY_DATA: +            case METHOD_GET_SWITCH_DATA: +                if (!(controller instanceof PrimarySwitchController)) { +                    return controller.getBundle(); +                } +                break; +            case METHOD_GET_PROVIDER_ICON: +                if (controller instanceof ProviderIcon) { +                    return ((ProviderIcon) controller).getProviderIcon(); +                } +                break; +            case METHOD_GET_DYNAMIC_TITLE: +                if (controller instanceof DynamicTitle) { +                    bundle.putString(META_DATA_PREFERENCE_TITLE, +                            ((DynamicTitle) controller).getDynamicTitle()); +                    return bundle; +                } +                break; +            case METHOD_GET_DYNAMIC_SUMMARY: +                if (controller instanceof DynamicSummary) { +                    bundle.putString(META_DATA_PREFERENCE_SUMMARY, +                            ((DynamicSummary) controller).getDynamicSummary()); +                    return bundle; +                } +                break; +            case METHOD_IS_CHECKED: +                if (controller instanceof ProviderSwitch) { +                    bundle.putBoolean(EXTRA_SWITCH_CHECKED_STATE, +                            ((ProviderSwitch) controller).isSwitchChecked()); +                    return bundle; +                } +                break; +            case METHOD_ON_CHECKED_CHANGED: +                if (controller instanceof ProviderSwitch) { +                    return onSwitchCheckedChanged(extras.getBoolean(EXTRA_SWITCH_CHECKED_STATE), +                            (ProviderSwitch) controller); +                } +                break; +        } +        return null; +    } + +    private Bundle onSwitchCheckedChanged(boolean checked, ProviderSwitch controller) { +        final boolean success = controller.onSwitchCheckedChanged(checked); +        final Bundle bundle = new Bundle(); +        bundle.putBoolean(EXTRA_SWITCH_SET_CHECKED_ERROR, !success); +        if (success) { +            if (controller instanceof DynamicSummary) { +                ((EntryController) controller).notifySummaryChanged(getContext()); +            } +        } else { +            bundle.putString(EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE, +                    controller.getSwitchErrorMessage(checked)); +        } +        return bundle; +    } + +    @Override +    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, +            String sortOrder) { +        throw new UnsupportedOperationException(); +    } + +    @Override +    public String getType(Uri uri) { +        throw new UnsupportedOperationException(); +    } + +    @Override +    public Uri insert(Uri uri, ContentValues values) { +        throw new UnsupportedOperationException(); +    } + +    @Override +    public int delete(Uri uri, String selection, String[] selectionArgs) { +        throw new UnsupportedOperationException(); +    } + +    @Override +    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { +        throw new UnsupportedOperationException(); +    } +} + diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/EntryController.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/EntryController.java new file mode 100644 index 000000000000..5d6e6a3adeb7 --- /dev/null +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/EntryController.java @@ -0,0 +1,246 @@ +/* + * 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.settingslib.drawer; + +import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_SUMMARY; +import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_TITLE; +import static com.android.settingslib.drawer.TileUtils.EXTRA_CATEGORY_KEY; +import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_ORDER; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_BACKGROUND_ARGB; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_BACKGROUND_HINT; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_PENDING_INTENT; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SWITCH_URI; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI; + +import android.app.PendingIntent; +import android.content.ContentResolver; +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +/** + * A controller that manages events for switch. + */ +public abstract class EntryController { + +    private String mAuthority; + +    /** +     * Returns the key for this switch. +     */ +    public abstract String getKey(); + +    /** +     * Returns the {@link MetaData} for this switch. +     */ +    protected abstract MetaData getMetaData(); + +    /** +     * Notify registered observers that title was updated and attempt to sync changes. +     */ +    public void notifyTitleChanged(Context context) { +        if (this instanceof DynamicTitle) { +            notifyChanged(context, METHOD_GET_DYNAMIC_TITLE); +        } +    } + +    /** +     * Notify registered observers that summary was updated and attempt to sync changes. +     */ +    public void notifySummaryChanged(Context context) { +        if (this instanceof DynamicSummary) { +            notifyChanged(context, METHOD_GET_DYNAMIC_SUMMARY); +        } +    } + +    void setAuthority(String authority) { +        mAuthority = authority; +    } + +    Bundle getBundle() { +        final MetaData metaData = getMetaData(); +        if (metaData == null) { +            throw new NullPointerException("Should not return null in getMetaData()"); +        } + +        final Bundle bundle = metaData.build(); +        final String uriString = new Uri.Builder() +                .scheme(ContentResolver.SCHEME_CONTENT) +                .authority(mAuthority) +                .build() +                .toString(); +        bundle.putString(META_DATA_PREFERENCE_KEYHINT, getKey()); +        if (this instanceof ProviderIcon) { +            bundle.putString(META_DATA_PREFERENCE_ICON_URI, uriString); +        } +        if (this instanceof DynamicTitle) { +            bundle.putString(META_DATA_PREFERENCE_TITLE_URI, uriString); +        } +        if (this instanceof DynamicSummary) { +            bundle.putString(META_DATA_PREFERENCE_SUMMARY_URI, uriString); +        } +        if (this instanceof ProviderSwitch) { +            bundle.putString(META_DATA_PREFERENCE_SWITCH_URI, uriString); +        } +        return bundle; +    } + +    private void notifyChanged(Context context, String method) { +        final Uri uri = TileUtils.buildUri(mAuthority, method, getKey()); +        context.getContentResolver().notifyChange(uri, null); +    } + +    /** +     * Collects all meta data of the item. +     */ +    protected static class MetaData { +        private String mCategory; +        private int mOrder; +        @DrawableRes +        private int mIcon; +        private int mIconBackgroundHint; +        private int mIconBackgroundArgb; +        private Boolean mIconTintable; +        @StringRes +        private int mTitleId; +        private String mTitle; +        @StringRes +        private int mSummaryId; +        private String mSummary; +        private PendingIntent mPendingIntent; + +        /** +         * @param category the category of the switch. This value must be from {@link CategoryKey}. +         */ +        public MetaData(@NonNull String category) { +            mCategory = category; +        } + +        /** +         * Set the order of the item that should be displayed on screen. Bigger value items displays +         * closer on top. +         */ +        public MetaData setOrder(int order) { +            mOrder = order; +            return this; +        } + +        /** Set the icon that should be displayed for the item. */ +        public MetaData setIcon(@DrawableRes int icon) { +            mIcon = icon; +            return this; +        } + +        /** Set the icon background color. The value may or may not be used by Settings app. */ +        public MetaData setIconBackgoundHint(int hint) { +            mIconBackgroundHint = hint; +            return this; +        } + +        /** Set the icon background color as raw ARGB. */ +        public MetaData setIconBackgoundArgb(int argb) { +            mIconBackgroundArgb = argb; +            return this; +        } + +        /** Specify whether the icon is tintable. */ +        public MetaData setIconTintable(boolean tintable) { +            mIconTintable = tintable; +            return this; +        } + +        /** Set the title that should be displayed for the item. */ +        public MetaData setTitle(@StringRes int id) { +            mTitleId = id; +            return this; +        } + +        /** Set the title that should be displayed for the item. */ +        public MetaData setTitle(String title) { +            mTitle = title; +            return this; +        } + +        /** Set the summary text that should be displayed for the item. */ +        public MetaData setSummary(@StringRes int id) { +            mSummaryId = id; +            return this; +        } + +        /** Set the summary text that should be displayed for the item. */ +        public MetaData setSummary(String summary) { +            mSummary = summary; +            return this; +        } + +        public MetaData setPendingIntent(PendingIntent pendingIntent) { +            mPendingIntent = pendingIntent; +            return this; +        } + +        protected Bundle build() { +            final Bundle bundle = new Bundle(); +            bundle.putString(EXTRA_CATEGORY_KEY, mCategory); + +            if (mOrder != 0) { +                bundle.putInt(META_DATA_KEY_ORDER, mOrder); +            } + +            if (mIcon != 0) { +                bundle.putInt(META_DATA_PREFERENCE_ICON, mIcon); +            } +            if (mIconBackgroundHint != 0) { +                bundle.putInt(META_DATA_PREFERENCE_ICON_BACKGROUND_HINT, mIconBackgroundHint); +            } +            if (mIconBackgroundArgb != 0) { +                bundle.putInt(META_DATA_PREFERENCE_ICON_BACKGROUND_ARGB, mIconBackgroundArgb); +            } +            if (mIconTintable != null) { +                bundle.putBoolean(META_DATA_PREFERENCE_ICON_TINTABLE, mIconTintable); +            } + +            if (mTitleId != 0) { +                bundle.putInt(META_DATA_PREFERENCE_TITLE, mTitleId); +            } else if (mTitle != null) { +                bundle.putString(META_DATA_PREFERENCE_TITLE, mTitle); +            } + +            if (mSummaryId != 0) { +                bundle.putInt(META_DATA_PREFERENCE_SUMMARY, mSummaryId); +            } else if (mSummary != null) { +                bundle.putString(META_DATA_PREFERENCE_SUMMARY, mSummary); +            } + +            if (mPendingIntent != null) { +                bundle.putParcelable(META_DATA_PREFERENCE_PENDING_INTENT, mPendingIntent); +            } + +            return bundle; +        } +    } +} diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderIcon.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderIcon.java index 2945d5c1099a..3aa6fcb06016 100644 --- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderIcon.java +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderIcon.java @@ -19,7 +19,7 @@ package com.android.settingslib.drawer;  import android.os.Bundle;  /** - *  Interface for {@link SwitchController} whose instances support icon provided from the content + *  Interface for {@link EntryController} whose instances support icon provided from the content   *  provider   */  public interface ProviderIcon { diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderSwitch.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderSwitch.java new file mode 100644 index 000000000000..47eb31cefa29 --- /dev/null +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderSwitch.java @@ -0,0 +1,41 @@ +/* + * 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.settingslib.drawer; + +/** + *  Interface for {@link EntryController} whose instances support switch widget provided from the + *  content provider + */ +public interface ProviderSwitch { +    /** +     * Returns the checked state of this switch. +     */ +    boolean isSwitchChecked(); + +    /** +     * Called when the checked state of this switch is changed. +     * +     * @return true if the checked state was successfully changed, otherwise false +     */ +    boolean onSwitchCheckedChanged(boolean checked); + +    /** +     * Returns the error message which will be toasted when {@link #onSwitchCheckedChanged} returns +     * false. +     */ +    String getSwitchErrorMessage(boolean attemptedChecked); +} diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderTile.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderTile.java index 54da585aba7a..b775e93bf69c 100644 --- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderTile.java +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/ProviderTile.java @@ -75,7 +75,7 @@ public class ProviderTile extends Tile {              if (infoList != null && !infoList.isEmpty()) {                  final ProviderInfo providerInfo = infoList.get(0).providerInfo;                  mComponentInfo = providerInfo; -                setMetaData(TileUtils.getSwitchDataFromProvider(context, providerInfo.authority, +                setMetaData(TileUtils.getEntryDataFromProvider(context, providerInfo.authority,                          mKey));              } else {                  Log.e(TAG, "Cannot find package info for " diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchController.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchController.java index 23669b2743ce..a1a4e5867299 100644 --- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchController.java +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchController.java @@ -16,38 +16,16 @@  package com.android.settingslib.drawer; -import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_SUMMARY; -import static com.android.settingslib.drawer.SwitchesProvider.METHOD_GET_DYNAMIC_TITLE; -import static com.android.settingslib.drawer.SwitchesProvider.METHOD_IS_CHECKED; -import static com.android.settingslib.drawer.TileUtils.EXTRA_CATEGORY_KEY; -import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_ORDER; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_BACKGROUND_ARGB; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_BACKGROUND_HINT; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_TINTABLE; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SWITCH_URI; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE_URI; - -import android.content.ContentResolver; -import android.content.Context; -import android.net.Uri; -import android.os.Bundle; - -import androidx.annotation.DrawableRes;  import androidx.annotation.NonNull; -import androidx.annotation.StringRes;  /**   * A controller that manages events for switch. + * + * @deprecated Use {@link EntriesProvider} with {@link ProviderSwitch} instead.   */ -public abstract class SwitchController { +@Deprecated +public abstract class SwitchController extends EntryController implements ProviderSwitch { -    private String mAuthority;      /**       * Returns the key for this switch. @@ -55,11 +33,6 @@ public abstract class SwitchController {      public abstract String getSwitchKey();      /** -     * Returns the {@link MetaData} for this switch. -     */ -    protected abstract MetaData getMetaData(); - -    /**       * Returns the checked state of this switch.       */      protected abstract boolean isChecked(); @@ -76,181 +49,41 @@ public abstract class SwitchController {       */      protected abstract String getErrorMessage(boolean attemptedChecked); -    /** -     * Notify registered observers that title was updated and attempt to sync changes. -     */ -    public void notifyTitleChanged(Context context) { -        if (this instanceof DynamicTitle) { -            notifyChanged(context, METHOD_GET_DYNAMIC_TITLE); -        } -    } - -    /** -     * Notify registered observers that summary was updated and attempt to sync changes. -     */ -    public void notifySummaryChanged(Context context) { -        if (this instanceof DynamicSummary) { -            notifyChanged(context, METHOD_GET_DYNAMIC_SUMMARY); -        } -    } - -    /** -     * Notify registered observers that checked state was updated and attempt to sync changes. -     */ -    public void notifyCheckedChanged(Context context) { -        notifyChanged(context, METHOD_IS_CHECKED); +    @Override +    public String getKey() { +        return getSwitchKey();      } -    void setAuthority(String authority) { -        mAuthority = authority; +    @Override +    public boolean isSwitchChecked() { +        return isChecked();      } -    Bundle getBundle() { -        final MetaData metaData = getMetaData(); -        if (metaData == null) { -            throw new NullPointerException("Should not return null in getMetaData()"); -        } - -        final Bundle bundle = metaData.build(); -        final String uriString = new Uri.Builder() -                .scheme(ContentResolver.SCHEME_CONTENT) -                .authority(mAuthority) -                .build() -                .toString(); -        bundle.putString(META_DATA_PREFERENCE_KEYHINT, getSwitchKey()); -        bundle.putString(META_DATA_PREFERENCE_SWITCH_URI, uriString); -        if (this instanceof ProviderIcon) { -            bundle.putString(META_DATA_PREFERENCE_ICON_URI, uriString); -        } -        if (this instanceof DynamicTitle) { -            bundle.putString(META_DATA_PREFERENCE_TITLE_URI, uriString); -        } -        if (this instanceof DynamicSummary) { -            bundle.putString(META_DATA_PREFERENCE_SUMMARY_URI, uriString); -        } -        return bundle; +    @Override +    public boolean onSwitchCheckedChanged(boolean checked) { +        return onCheckedChanged(checked);      } -    private void notifyChanged(Context context, String method) { -        final Uri uri = TileUtils.buildUri(mAuthority, method, getSwitchKey()); -        context.getContentResolver().notifyChange(uri, null); +    @Override +    public String getSwitchErrorMessage(boolean attemptedChecked) { +        return getErrorMessage(attemptedChecked);      }      /** -     * Collects all meta data of the item. +     * Same as {@link EntryController.MetaData}, for backwards compatibility purpose. +     * +     * @deprecated Use {@link EntryController.MetaData} instead.       */ -    protected static class MetaData { -        private String mCategory; -        private int mOrder; -        @DrawableRes -        private int mIcon; -        private int mIconBackgroundHint; -        private int mIconBackgroundArgb; -        private Boolean mIconTintable; -        @StringRes -        private int mTitleId; -        private String mTitle; -        @StringRes -        private int mSummaryId; -        private String mSummary; - +    @Deprecated +    protected static class MetaData extends EntryController.MetaData {          /**           * @param category the category of the switch. This value must be from {@link CategoryKey}. +         * +         * @deprecated Use {@link EntryController.MetaData} instead.           */ +        @Deprecated          public MetaData(@NonNull String category) { -            mCategory = category; -        } - -        /** -         * Set the order of the item that should be displayed on screen. Bigger value items displays -         * closer on top. -         */ -        public MetaData setOrder(int order) { -            mOrder = order; -            return this; -        } - -        /** Set the icon that should be displayed for the item. */ -        public MetaData setIcon(@DrawableRes int icon) { -            mIcon = icon; -            return this; -        } - -        /** Set the icon background color. The value may or may not be used by Settings app. */ -        public MetaData setIconBackgoundHint(int hint) { -            mIconBackgroundHint = hint; -            return this; -        } - -        /** Set the icon background color as raw ARGB. */ -        public MetaData setIconBackgoundArgb(int argb) { -            mIconBackgroundArgb = argb; -            return this; -        } - -        /** Specify whether the icon is tintable. */ -        public MetaData setIconTintable(boolean tintable) { -            mIconTintable = tintable; -            return this; -        } - -        /** Set the title that should be displayed for the item. */ -        public MetaData setTitle(@StringRes int id) { -            mTitleId = id; -            return this; -        } - -        /** Set the title that should be displayed for the item. */ -        public MetaData setTitle(String title) { -            mTitle = title; -            return this; -        } - -        /** Set the summary text that should be displayed for the item. */ -        public MetaData setSummary(@StringRes int id) { -            mSummaryId = id; -            return this; -        } - -        /** Set the summary text that should be displayed for the item. */ -        public MetaData setSummary(String summary) { -            mSummary = summary; -            return this; -        } - -        private Bundle build() { -            final Bundle bundle = new Bundle(); -            bundle.putString(EXTRA_CATEGORY_KEY, mCategory); - -            if (mOrder != 0) { -                bundle.putInt(META_DATA_KEY_ORDER, mOrder); -            } - -            if (mIcon != 0) { -                bundle.putInt(META_DATA_PREFERENCE_ICON, mIcon); -            } -            if (mIconBackgroundHint != 0) { -                bundle.putInt(META_DATA_PREFERENCE_ICON_BACKGROUND_HINT, mIconBackgroundHint); -            } -            if (mIconBackgroundArgb != 0) { -                bundle.putInt(META_DATA_PREFERENCE_ICON_BACKGROUND_ARGB, mIconBackgroundArgb); -            } -            if (mIconTintable != null) { -                bundle.putBoolean(META_DATA_PREFERENCE_ICON_TINTABLE, mIconTintable); -            } - -            if (mTitleId != 0) { -                bundle.putInt(META_DATA_PREFERENCE_TITLE, mTitleId); -            } else if (mTitle != null) { -                bundle.putString(META_DATA_PREFERENCE_TITLE, mTitle); -            } - -            if (mSummaryId != 0) { -                bundle.putInt(META_DATA_PREFERENCE_SUMMARY, mSummaryId); -            } else if (mSummary != null) { -                bundle.putString(META_DATA_PREFERENCE_SUMMARY, mSummary); -            } -            return bundle; +            super(category);          }      }  } diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchesProvider.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchesProvider.java index f2b3e30dc252..ad00ced8a3ac 100644 --- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchesProvider.java +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/SwitchesProvider.java @@ -16,46 +16,15 @@  package com.android.settingslib.drawer; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; -import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE; - -import android.content.ContentProvider; -import android.content.ContentValues; -import android.content.Context; -import android.content.pm.ProviderInfo; -import android.database.Cursor; -import android.net.Uri; -import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; - -import java.util.ArrayList; -import java.util.LinkedHashMap;  import java.util.List; -import java.util.Map;  /**   * An abstract class for injecting switches to Settings. + * + * @deprecated Use {@link EntriesProvider} instead.   */ -public abstract class SwitchesProvider extends ContentProvider { -    private static final String TAG = "SwitchesProvider"; - -    public static final String METHOD_GET_SWITCH_DATA = "getSwitchData"; -    public static final String METHOD_GET_PROVIDER_ICON = "getProviderIcon"; -    public static final String METHOD_GET_DYNAMIC_TITLE = "getDynamicTitle"; -    public static final String METHOD_GET_DYNAMIC_SUMMARY = "getDynamicSummary"; -    public static final String METHOD_IS_CHECKED = "isChecked"; -    public static final String METHOD_ON_CHECKED_CHANGED = "onCheckedChanged"; - -    public static final String EXTRA_SWITCH_DATA = "switch_data"; -    public static final String EXTRA_SWITCH_CHECKED_STATE = "checked_state"; -    public static final String EXTRA_SWITCH_SET_CHECKED_ERROR = "set_checked_error"; -    public static final String EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE = "set_checked_error_message"; - -    private String mAuthority; -    private final Map<String, SwitchController> mControllerMap = new LinkedHashMap<>(); -    private final List<Bundle> mSwitchDataList = new ArrayList<>(); +@Deprecated +public abstract class SwitchesProvider extends EntriesProvider {      /**       * Get a list of {@link SwitchController} for this provider. @@ -63,129 +32,7 @@ public abstract class SwitchesProvider extends ContentProvider {      protected abstract List<SwitchController> createSwitchControllers();      @Override -    public void attachInfo(Context context, ProviderInfo info) { -        mAuthority = info.authority; -        Log.i(TAG, mAuthority); -        super.attachInfo(context, info); -    } - -    @Override -    public boolean onCreate() { -        final List<SwitchController> controllers = createSwitchControllers(); -        if (controllers == null || controllers.isEmpty()) { -            throw new IllegalArgumentException(); -        } - -        controllers.forEach(controller -> { -            final String key = controller.getSwitchKey(); -            if (TextUtils.isEmpty(key)) { -                throw new NullPointerException("Switch key cannot be null: " -                        + controller.getClass().getSimpleName()); -            } else if (mControllerMap.containsKey(key)) { -                throw new IllegalArgumentException("Switch key " + key + " is duplicated by: " -                        + controller.getClass().getSimpleName()); -            } - -            controller.setAuthority(mAuthority); -            mControllerMap.put(key, controller); -            if (!(controller instanceof PrimarySwitchController)) { -                mSwitchDataList.add(controller.getBundle()); -            } -        }); -        return true; -    } - -    @Override -    public Bundle call(String method, String uriString, Bundle extras) { -        final Bundle bundle = new Bundle(); -        final String key = extras != null -                ? extras.getString(META_DATA_PREFERENCE_KEYHINT) -                : null; -        if (TextUtils.isEmpty(key)) { -            if (METHOD_GET_SWITCH_DATA.equals(method)) { -                bundle.putParcelableList(EXTRA_SWITCH_DATA, mSwitchDataList); -                return bundle; -            } -            return null; -        } - -        final SwitchController controller = mControllerMap.get(key); -        if (controller == null) { -            return null; -        } - -        switch (method) { -            case METHOD_GET_SWITCH_DATA: -                if (!(controller instanceof PrimarySwitchController)) { -                    return controller.getBundle(); -                } -                break; -            case METHOD_GET_PROVIDER_ICON: -                if (controller instanceof ProviderIcon) { -                    return ((ProviderIcon) controller).getProviderIcon(); -                } -                break; -            case METHOD_GET_DYNAMIC_TITLE: -                if (controller instanceof DynamicTitle) { -                    bundle.putString(META_DATA_PREFERENCE_TITLE, -                            ((DynamicTitle) controller).getDynamicTitle()); -                    return bundle; -                } -                break; -            case METHOD_GET_DYNAMIC_SUMMARY: -                if (controller instanceof DynamicSummary) { -                    bundle.putString(META_DATA_PREFERENCE_SUMMARY, -                            ((DynamicSummary) controller).getDynamicSummary()); -                    return bundle; -                } -                break; -            case METHOD_IS_CHECKED: -                bundle.putBoolean(EXTRA_SWITCH_CHECKED_STATE, controller.isChecked()); -                return bundle; -            case METHOD_ON_CHECKED_CHANGED: -                return onCheckedChanged(extras.getBoolean(EXTRA_SWITCH_CHECKED_STATE), controller); -        } -        return null; -    } - -    private Bundle onCheckedChanged(boolean checked, SwitchController controller) { -        final boolean success = controller.onCheckedChanged(checked); -        final Bundle bundle = new Bundle(); -        bundle.putBoolean(EXTRA_SWITCH_SET_CHECKED_ERROR, !success); -        if (success) { -            if (controller instanceof DynamicSummary) { -                controller.notifySummaryChanged(getContext()); -            } -        } else { -            bundle.putString(EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE, -                    controller.getErrorMessage(checked)); -        } -        return bundle; -    } - -    @Override -    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, -            String sortOrder) { -        throw new UnsupportedOperationException(); -    } - -    @Override -    public String getType(Uri uri) { -        throw new UnsupportedOperationException(); -    } - -    @Override -    public Uri insert(Uri uri, ContentValues values) { -        throw new UnsupportedOperationException(); -    } - -    @Override -    public int delete(Uri uri, String selection, String[] selectionArgs) { -        throw new UnsupportedOperationException(); -    } - -    @Override -    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { -        throw new UnsupportedOperationException(); +    protected List<? extends EntryController> createEntryControllers() { +        return createSwitchControllers();      }  } diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java index a0c8ac4e0a51..00dd8cc88da2 100644 --- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/Tile.java @@ -19,6 +19,7 @@ package com.android.settingslib.drawer;  import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_ORDER;  import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_PROFILE;  import static com.android.settingslib.drawer.TileUtils.META_DATA_NEW_TASK; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_GROUP_KEY;  import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON;  import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT;  import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; @@ -29,6 +30,7 @@ import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITL  import static com.android.settingslib.drawer.TileUtils.PROFILE_ALL;  import static com.android.settingslib.drawer.TileUtils.PROFILE_PRIMARY; +import android.app.PendingIntent;  import android.content.Context;  import android.content.Intent;  import android.content.pm.ComponentInfo; @@ -47,6 +49,7 @@ import androidx.annotation.VisibleForTesting;  import java.util.ArrayList;  import java.util.Comparator; +import java.util.HashMap;  /**   * Description of a single dashboard tile that the user can select. @@ -60,6 +63,8 @@ public abstract class Tile implements Parcelable {       */      public ArrayList<UserHandle> userHandle = new ArrayList<>(); +    public HashMap<UserHandle, PendingIntent> pendingIntentMap = new HashMap<>(); +      @VisibleForTesting      long mLastUpdateTime;      private final String mComponentPackage; @@ -186,6 +191,13 @@ public abstract class Tile implements Parcelable {      }      /** +     * Check whether tile has a pending intent. +     */ +    public boolean hasPendingIntent() { +        return !pendingIntentMap.isEmpty(); +    } + +    /**       * Title of the tile that is shown to the user.       */      public CharSequence getTitle(Context context) { @@ -395,6 +407,76 @@ public abstract class Tile implements Parcelable {          return TextUtils.equals(profile, PROFILE_PRIMARY);      } +    /** +     * Returns whether the tile belongs to another group / category. +     */ +    public boolean hasGroupKey() { +        return mMetaData != null +                && !TextUtils.isEmpty(mMetaData.getString(META_DATA_PREFERENCE_GROUP_KEY)); +    } + +    /** +     * Returns the group / category key this tile belongs to. +     */ +    public String getGroupKey() { +        return (mMetaData == null) ? null : mMetaData.getString(META_DATA_PREFERENCE_GROUP_KEY); +    } + +    /** +     * The type of the tile. +     */ +    public enum Type { +        /** +         * A preference that can be tapped on to open a new page. +         */ +        ACTION, + +        /** +         * A preference that can be tapped on to open an external app. +         */ +        EXTERNAL_ACTION, + +        /** +         * A preference that shows an on / off switch that can be toggled by the user. +         */ +        SWITCH, + +        /** +         * A preference with both an on / off switch, and a tappable area that can perform an +         * action. +         */ +        SWITCH_WITH_ACTION, + +        /** +         * A preference category with a title that can be used to group multiple preferences +         * together. +         */ +        GROUP; +    } + +    /** +     * Returns the type of the tile. +     * +     * @see Type +     */ +    public Type getType() { +        boolean hasExternalAction = hasPendingIntent(); +        boolean hasAction = hasExternalAction || this instanceof ActivityTile; +        boolean hasSwitch = hasSwitch(); + +        if (hasSwitch && hasAction) { +            return Type.SWITCH_WITH_ACTION; +        } else if (hasSwitch) { +            return Type.SWITCH; +        } else if (hasExternalAction) { +            return Type.EXTERNAL_ACTION; +        } else if (hasAction) { +            return Type.ACTION; +        } else { +            return Type.GROUP; +        } +    } +      public static final Comparator<Tile> TILE_COMPARATOR =              (lhs, rhs) -> rhs.getOrder() - lhs.getOrder();  } diff --git a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java index acc0087f6dcf..e46db75f633e 100644 --- a/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java +++ b/packages/SettingsLib/Tile/src/com/android/settingslib/drawer/TileUtils.java @@ -113,6 +113,12 @@ public class TileUtils {      public static final String META_DATA_PREFERENCE_KEYHINT = "com.android.settings.keyhint";      /** +     * Name of the meta-data item that can be set in the AndroidManifest.xml or in the content +     * provider to specify the key of a group / category where this preference belongs to. +     */ +    public static final String META_DATA_PREFERENCE_GROUP_KEY = "com.android.settings.group_key"; + +    /**       * Order of the item that should be displayed on screen. Bigger value items displays closer on       * top.       */ @@ -202,6 +208,13 @@ public class TileUtils {              "com.android.settings.switch_uri";      /** +     * Name of the meta-data item that can be set from the content provider providing the intent +     * that will be executed when the user taps on the preference. +     */ +    public static final String META_DATA_PREFERENCE_PENDING_INTENT = +            "com.android.settings.pending_intent"; + +    /**       * Value for {@link #META_DATA_KEY_PROFILE}. When the device has a managed profile,       * the app will always be run in the primary profile.       * @@ -331,12 +344,12 @@ public class TileUtils {                  continue;              }              final ProviderInfo providerInfo = resolved.providerInfo; -            final List<Bundle> switchData = getSwitchDataFromProvider(context, +            final List<Bundle> entryData = getEntryDataFromProvider(context,                      providerInfo.authority); -            if (switchData == null || switchData.isEmpty()) { +            if (entryData == null || entryData.isEmpty()) {                  continue;              } -            for (Bundle metaData : switchData) { +            for (Bundle metaData : entryData) {                  loadTile(user, addedCache, defaultCategory, outTiles, intent, metaData,                          providerInfo);              } @@ -386,27 +399,43 @@ public class TileUtils {          if (!tile.userHandle.contains(user)) {              tile.userHandle.add(user);          } +        if (metaData.containsKey(META_DATA_PREFERENCE_PENDING_INTENT)) { +            tile.pendingIntentMap.put( +                    user, metaData.getParcelable(META_DATA_PREFERENCE_PENDING_INTENT)); +        }          if (!outTiles.contains(tile)) {              outTiles.add(tile);          }      } -    /** Returns the switch data of the key specified from the provider */ +    /** Returns the entry data of the key specified from the provider */      // TODO(b/144732809): rearrange methods by access level modifiers -    static Bundle getSwitchDataFromProvider(Context context, String authority, String key) { +    static Bundle getEntryDataFromProvider(Context context, String authority, String key) {          final Map<String, IContentProvider> providerMap = new ArrayMap<>(); -        final Uri uri = buildUri(authority, SwitchesProvider.METHOD_GET_SWITCH_DATA, key); -        return getBundleFromUri(context, uri, providerMap, null /* bundle */); +        final Uri uri = buildUri(authority, EntriesProvider.METHOD_GET_ENTRY_DATA, key); +        Bundle result = getBundleFromUri(context, uri, providerMap, null /* bundle */); +        if (result == null) { +            Uri fallbackUri = buildUri(authority, EntriesProvider.METHOD_GET_SWITCH_DATA, key); +            result = getBundleFromUri(context, fallbackUri, providerMap, null /* bundle */); +        } +        return result;      } -    /** Returns all switch data from the provider */ -    private static List<Bundle> getSwitchDataFromProvider(Context context, String authority) { +    /** Returns all entry data from the provider */ +    private static List<Bundle> getEntryDataFromProvider(Context context, String authority) {          final Map<String, IContentProvider> providerMap = new ArrayMap<>(); -        final Uri uri = buildUri(authority, SwitchesProvider.METHOD_GET_SWITCH_DATA); +        final Uri uri = buildUri(authority, EntriesProvider.METHOD_GET_ENTRY_DATA);          final Bundle result = getBundleFromUri(context, uri, providerMap, null /* bundle */); -        return result != null -                ? result.getParcelableArrayList(SwitchesProvider.EXTRA_SWITCH_DATA) -                : null; +        if (result != null) { +            return result.getParcelableArrayList(EntriesProvider.EXTRA_ENTRY_DATA); +        } else { +            Uri fallbackUri = buildUri(authority, EntriesProvider.METHOD_GET_SWITCH_DATA); +            Bundle fallbackResult = +                    getBundleFromUri(context, fallbackUri, providerMap, null /* bundle */); +            return fallbackResult != null +                    ? fallbackResult.getParcelableArrayList(EntriesProvider.EXTRA_SWITCH_DATA) +                    : null; +        }      }      /** diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index 2e6bb535a8f0..f522fd13c9f8 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -583,7 +583,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>       */      public void setName(String name) {          // Prevent getName() to be set to null if setName(null) is called -        if (name == null || TextUtils.equals(name, getName())) { +        if (TextUtils.isEmpty(name) || TextUtils.equals(name, getName())) {              return;          }          mDevice.setAlias(name); diff --git a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java index 09abc394634a..9ee8a32fdc77 100644 --- a/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java +++ b/packages/SettingsLib/src/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java @@ -209,8 +209,7 @@ public class MetricsFeatureProvider {          }          final ComponentName cn = intent.getComponent();          final String key = cn != null ? cn.flattenToString() : intent.getAction(); -        return logSettingsTileClick(key + (isWorkProfile ? "/work" : "/personal"), -                sourceMetricsCategory); +        return logSettingsTileClickWithProfile(key, sourceMetricsCategory, isWorkProfile);      }      /** @@ -226,4 +225,20 @@ public class MetricsFeatureProvider {          clicked(sourceMetricsCategory, logKey);          return true;      } + +    /** +     * Logs an event when the setting key is clicked with a specific profile from Profile select +     * dialog. +     * +     * @return true if the key is loggable, otherwise false +     */ +    public boolean logSettingsTileClickWithProfile(String logKey, int sourceMetricsCategory, +            boolean isWorkProfile) { +        if (TextUtils.isEmpty(logKey)) { +            // Not loggable +            return false; +        } +        clicked(sourceMetricsCategory, logKey + (isWorkProfile ? "/work" : "/personal")); +        return true; +    }  } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java index 6444f3bd4341..4b61ff1177bd 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceTest.java @@ -1015,6 +1015,13 @@ public class CachedBluetoothDeviceTest {      }      @Test +    public void setName_setDeviceNameIsEmpty() { +        mCachedDevice.setName(""); + +        verify(mDevice, never()).setAlias(any()); +    } + +    @Test      public void getProfileConnectionState_nullProfile_returnDisconnected() {          assertThat(mCachedDevice.getProfileConnectionState(null)).isEqualTo(                  BluetoothProfile.STATE_DISCONNECTED); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/MetricsFeatureProviderTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/MetricsFeatureProviderTest.java index 3352d86b2dcc..dd8d54a62ff4 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/MetricsFeatureProviderTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/core/instrumentation/MetricsFeatureProviderTest.java @@ -203,4 +203,24 @@ public class MetricsFeatureProviderTest {          assertThat(loggable).isFalse();          verifyNoMoreInteractions(mLogWriter);      } + +    @Test +    public void logSettingsTileClickWithProfile_isPersonalProfile_shouldTagPersonal() { +        final String key = "abc"; +        final boolean loggable = mProvider.logSettingsTileClickWithProfile(key, +                MetricsEvent.SETTINGS_GESTURES, false); + +        assertThat(loggable).isTrue(); +        verify(mLogWriter).clicked(MetricsEvent.SETTINGS_GESTURES, "abc/personal"); +    } + +    @Test +    public void logSettingsTileClickWithProfile_isWorkProfile_shouldTagWork() { +        final String key = "abc"; +        final boolean loggable = mProvider.logSettingsTileClickWithProfile(key, +                MetricsEvent.SETTINGS_GESTURES, true); + +        assertThat(loggable).isTrue(); +        verify(mLogWriter).clicked(MetricsEvent.SETTINGS_GESTURES, "abc/work"); +    }  } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ActivityTileTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ActivityTileTest.java index aa6b0bf33b69..4d2b1ae2ade0 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ActivityTileTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ActivityTileTest.java @@ -17,19 +17,23 @@ package com.android.settingslib.drawer;  import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_ORDER;  import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_PROFILE; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_GROUP_KEY;  import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON;  import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SWITCH_URI;  import static com.android.settingslib.drawer.TileUtils.PROFILE_ALL;  import static com.android.settingslib.drawer.TileUtils.PROFILE_PRIMARY;  import static com.google.common.truth.Truth.assertThat; +import android.app.PendingIntent;  import android.content.Context;  import android.content.Intent;  import android.content.pm.ActivityInfo;  import android.content.pm.ApplicationInfo;  import android.content.pm.ResolveInfo;  import android.os.Bundle; +import android.os.UserHandle;  import org.junit.Before;  import org.junit.Test; @@ -191,4 +195,65 @@ public class ActivityTileTest {          assertThat(tile.getTitle(RuntimeEnvironment.application)).isNull();      } + +    @Test +    public void hasPendingIntent_empty_returnsFalse() { +        final Tile tile = new ActivityTile(mActivityInfo, "category"); + +        assertThat(tile.hasPendingIntent()).isFalse(); +    } + +    @Test +    public void hasPendingIntent_notEmpty_returnsTrue() { +        final Tile tile = new ActivityTile(mActivityInfo, "category"); +        tile.pendingIntentMap.put( +                UserHandle.CURRENT, PendingIntent.getActivity(mContext, 0, new Intent(), 0)); + +        assertThat(tile.hasPendingIntent()).isTrue(); +    } + +    @Test +    public void hasGroupKey_empty_returnsFalse() { +        final Tile tile = new ActivityTile(mActivityInfo, "category"); + +        assertThat(tile.hasGroupKey()).isFalse(); +    } + +    @Test +    public void hasGroupKey_notEmpty_returnsTrue() { +        mActivityInfo.metaData.putString(META_DATA_PREFERENCE_GROUP_KEY, "test_key"); +        final Tile tile = new ActivityTile(mActivityInfo, "category"); + +        assertThat(tile.hasGroupKey()).isTrue(); +    } + +    @Test +    public void getGroupKey_empty_returnsNull() { +        final Tile tile = new ActivityTile(mActivityInfo, "category"); + +        assertThat(tile.getGroupKey()).isNull(); +    } + +    @Test +    public void getGroupKey_notEmpty_returnsValue() { +        mActivityInfo.metaData.putString(META_DATA_PREFERENCE_GROUP_KEY, "test_key"); +        final Tile tile = new ActivityTile(mActivityInfo, "category"); + +        assertThat(tile.getGroupKey()).isEqualTo("test_key"); +    } + +    @Test +    public void getType_withoutSwitch_returnsAction() { +        final Tile tile = new ActivityTile(mActivityInfo, "category"); + +        assertThat(tile.getType()).isEqualTo(Tile.Type.ACTION); +    } + +    @Test +    public void getType_withSwitch_returnsSwitchWithAction() { +        mActivityInfo.metaData.putString(META_DATA_PREFERENCE_SWITCH_URI, "test://testabc/"); +        final Tile tile = new ActivityTile(mActivityInfo, "category"); + +        assertThat(tile.getType()).isEqualTo(Tile.Type.SWITCH_WITH_ACTION); +    }  } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/EntriesProviderTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/EntriesProviderTest.java new file mode 100644 index 000000000000..a2483305c94a --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/EntriesProviderTest.java @@ -0,0 +1,472 @@ +/* + * 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.settingslib.drawer; + +import static com.android.settingslib.drawer.EntriesProvider.EXTRA_ENTRY_DATA; +import static com.android.settingslib.drawer.EntriesProvider.EXTRA_SWITCH_CHECKED_STATE; +import static com.android.settingslib.drawer.EntriesProvider.EXTRA_SWITCH_DATA; +import static com.android.settingslib.drawer.EntriesProvider.EXTRA_SWITCH_SET_CHECKED_ERROR; +import static com.android.settingslib.drawer.EntriesProvider.EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE; +import static com.android.settingslib.drawer.EntriesProvider.METHOD_GET_DYNAMIC_SUMMARY; +import static com.android.settingslib.drawer.EntriesProvider.METHOD_GET_DYNAMIC_TITLE; +import static com.android.settingslib.drawer.EntriesProvider.METHOD_GET_ENTRY_DATA; +import static com.android.settingslib.drawer.EntriesProvider.METHOD_GET_PROVIDER_ICON; +import static com.android.settingslib.drawer.EntriesProvider.METHOD_GET_SWITCH_DATA; +import static com.android.settingslib.drawer.EntriesProvider.METHOD_IS_CHECKED; +import static com.android.settingslib.drawer.EntriesProvider.METHOD_ON_CHECKED_CHANGED; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_PENDING_INTENT; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE; + +import static com.google.common.truth.Truth.assertThat; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ProviderInfo; +import android.os.Bundle; + +import com.android.settingslib.drawer.EntryController.MetaData; +import com.android.settingslib.drawer.PrimarySwitchControllerTest.TestPrimarySwitchController; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +public class EntriesProviderTest { + +    @Rule +    public final ExpectedException thrown = ExpectedException.none(); + +    private Context mContext; +    private ProviderInfo mProviderInfo; + +    private TestEntriesProvider mEntriesProvider; + +    @Before +    public void setUp() { +        mContext = RuntimeEnvironment.application; +        mEntriesProvider = new TestEntriesProvider(); +        mProviderInfo = new ProviderInfo(); +        mProviderInfo.authority = "auth"; +    } + +    @Test +    public void attachInfo_noController_shouldThrowIllegalArgumentException() { +        thrown.expect(IllegalArgumentException.class); + +        mEntriesProvider.attachInfo(mContext, mProviderInfo); +    } + +    @Test +    public void attachInfo_NoKeyInController_shouldThrowNullPointerException() { +        thrown.expect(NullPointerException.class); +        final TestEntryController controller = new TestEntryController(); +        mEntriesProvider.addController(controller); + +        mEntriesProvider.attachInfo(mContext, mProviderInfo); +    } + +    @Test +    public void attachInfo_NoMetaDataInController_shouldThrowNullPointerException() { +        thrown.expect(NullPointerException.class); +        final TestEntryController controller = new TestEntryController(); +        controller.setKey("123"); +        mEntriesProvider.addController(controller); + +        mEntriesProvider.attachInfo(mContext, mProviderInfo); +    } + +    @Test +    public void attachInfo_duplicateKey_shouldThrowIllegalArgumentException() { +        thrown.expect(IllegalArgumentException.class); +        final TestEntryController controller1 = new TestEntryController(); +        final TestEntryController controller2 = new TestEntryController(); +        controller1.setKey("123"); +        controller2.setKey("123"); +        controller1.setMetaData(new MetaData("category")); +        controller2.setMetaData(new MetaData("category")); +        mEntriesProvider.addController(controller1); +        mEntriesProvider.addController(controller2); + +        mEntriesProvider.attachInfo(mContext, mProviderInfo); +    } + +    @Test +    public void attachInfo_hasDifferentControllers_shouldNotThrowException() { +        final TestEntryController controller1 = new TestEntryController(); +        final TestEntryController controller2 = new TestEntryController(); +        controller1.setKey("123"); +        controller2.setKey("456"); +        controller1.setMetaData(new MetaData("category")); +        controller2.setMetaData(new MetaData("category")); +        mEntriesProvider.addController(controller1); +        mEntriesProvider.addController(controller2); + +        mEntriesProvider.attachInfo(mContext, mProviderInfo); +    } + +    @Test +    public void getEntryData_shouldNotReturnPrimarySwitchData() { +        final EntryController controller = new TestPrimarySwitchController("123"); +        mEntriesProvider.addController(controller); +        mEntriesProvider.attachInfo(mContext, mProviderInfo); + +        final Bundle switchData = mEntriesProvider.call(METHOD_GET_ENTRY_DATA, "uri", +                null /* extras*/); + +        final ArrayList<Bundle> dataList = switchData.getParcelableArrayList(EXTRA_ENTRY_DATA); +        assertThat(dataList).isEmpty(); +    } + +    @Test +    public void getEntryData_shouldReturnDataList() { +        final TestEntryController controller = new TestEntryController(); +        final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0); +        controller.setKey("123"); +        controller.setMetaData(new MetaData("category").setPendingIntent(pendingIntent)); +        mEntriesProvider.addController(controller); +        mEntriesProvider.attachInfo(mContext, mProviderInfo); + +        final Bundle entryData = mEntriesProvider.call(METHOD_GET_ENTRY_DATA, "uri", +                null /* extras*/); + +        final ArrayList<Bundle> dataList = entryData.getParcelableArrayList(EXTRA_ENTRY_DATA); +        assertThat(dataList).hasSize(1); +        assertThat(dataList.get(0).getString(META_DATA_PREFERENCE_KEYHINT)).isEqualTo("123"); +        assertThat(dataList.get(0).getParcelable(META_DATA_PREFERENCE_PENDING_INTENT, +                PendingIntent.class)) +                .isEqualTo(pendingIntent); +    } + +    @Test +    public void getSwitchData_shouldReturnDataList() { +        final TestEntryController controller = new TestEntryController(); +        final PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0); +        controller.setKey("123"); +        controller.setMetaData(new MetaData("category").setPendingIntent(pendingIntent)); +        mEntriesProvider.addController(controller); +        mEntriesProvider.attachInfo(mContext, mProviderInfo); + +        final Bundle entryData = mEntriesProvider.call(METHOD_GET_SWITCH_DATA, "uri", +                null /* extras*/); + +        final ArrayList<Bundle> dataList = entryData.getParcelableArrayList(EXTRA_SWITCH_DATA); +        assertThat(dataList).hasSize(1); +        assertThat(dataList.get(0).getString(META_DATA_PREFERENCE_KEYHINT)).isEqualTo("123"); +        assertThat(dataList.get(0).getParcelable(META_DATA_PREFERENCE_PENDING_INTENT, +                PendingIntent.class)) +                .isEqualTo(pendingIntent); +    } + +    @Test +    public void getEntryDataByKey_shouldReturnData() { +        final Bundle extras = new Bundle(); +        extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); +        final TestEntryController controller = new TestEntryController(); +        controller.setKey("123"); +        controller.setMetaData(new MetaData("category")); +        mEntriesProvider.addController(controller); +        mEntriesProvider.attachInfo(mContext, mProviderInfo); + +        final Bundle entryData = mEntriesProvider.call(METHOD_GET_ENTRY_DATA, "uri", extras); + +        assertThat(entryData.getString(META_DATA_PREFERENCE_KEYHINT)).isEqualTo("123"); +    } + +    @Test +    public void getSwitchDataByKey_shouldReturnData() { +        final Bundle extras = new Bundle(); +        extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); +        final TestEntryController controller = new TestEntryController(); +        controller.setKey("123"); +        controller.setMetaData(new MetaData("category")); +        mEntriesProvider.addController(controller); +        mEntriesProvider.attachInfo(mContext, mProviderInfo); + +        final Bundle entryData = mEntriesProvider.call(METHOD_GET_SWITCH_DATA, "uri", extras); + +        assertThat(entryData.getString(META_DATA_PREFERENCE_KEYHINT)).isEqualTo("123"); +    } + +    @Test +    public void isSwitchChecked_shouldReturnCheckedState() { +        final Bundle extras = new Bundle(); +        extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); +        final TestSwitchController controller = new TestSwitchController(); +        controller.setKey("123"); +        controller.setMetaData(new MetaData("category")); +        mEntriesProvider.addController(controller); +        mEntriesProvider.attachInfo(mContext, mProviderInfo); + +        controller.setSwitchChecked(true); +        Bundle result = mEntriesProvider.call(METHOD_IS_CHECKED, "uri", extras); + +        assertThat(result.getBoolean(EXTRA_SWITCH_CHECKED_STATE)).isTrue(); + +        controller.setSwitchChecked(false); +        result = mEntriesProvider.call(METHOD_IS_CHECKED, "uri", extras); + +        assertThat(result.getBoolean(EXTRA_SWITCH_CHECKED_STATE)).isFalse(); +    } + +    @Test +    public void getProviderIcon_noImplementInterface_shouldReturnNull() { +        final Bundle extras = new Bundle(); +        extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); +        final TestEntryController controller = new TestEntryController(); +        controller.setKey("123"); +        controller.setMetaData(new MetaData("category")); +        mEntriesProvider.addController(controller); +        mEntriesProvider.attachInfo(mContext, mProviderInfo); + +        final Bundle iconBundle = mEntriesProvider.call(METHOD_GET_PROVIDER_ICON, "uri", extras); + +        assertThat(iconBundle).isNull(); +    } + +    @Test +    public void getProviderIcon_implementInterface_shouldReturnIcon() { +        final Bundle extras = new Bundle(); +        extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); +        final TestEntryController controller = new TestDynamicController(); +        controller.setKey("123"); +        controller.setMetaData(new MetaData("category")); +        mEntriesProvider.addController(controller); +        mEntriesProvider.attachInfo(mContext, mProviderInfo); + +        final Bundle iconBundle = mEntriesProvider.call(METHOD_GET_PROVIDER_ICON, "uri", extras); + +        assertThat(iconBundle).isEqualTo(TestDynamicController.ICON_BUNDLE); +    } + +    @Test +    public void getDynamicTitle_noImplementInterface_shouldReturnNull() { +        final Bundle extras = new Bundle(); +        extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); +        final TestEntryController controller = new TestEntryController(); +        controller.setKey("123"); +        controller.setMetaData(new MetaData("category")); +        mEntriesProvider.addController(controller); +        mEntriesProvider.attachInfo(mContext, mProviderInfo); + +        final Bundle result = mEntriesProvider.call(METHOD_GET_DYNAMIC_TITLE, "uri", extras); + +        assertThat(result).isNull(); +    } + +    @Test +    public void getDynamicTitle_implementInterface_shouldReturnTitle() { +        final Bundle extras = new Bundle(); +        extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); +        final TestEntryController controller = new TestDynamicController(); +        controller.setKey("123"); +        controller.setMetaData(new MetaData("category")); +        mEntriesProvider.addController(controller); +        mEntriesProvider.attachInfo(mContext, mProviderInfo); + +        final Bundle result = mEntriesProvider.call(METHOD_GET_DYNAMIC_TITLE, "uri", extras); + +        assertThat(result.getString(META_DATA_PREFERENCE_TITLE)) +                .isEqualTo(TestDynamicController.TITLE); +    } + +    @Test +    public void getDynamicSummary_noImplementInterface_shouldReturnNull() { +        final Bundle extras = new Bundle(); +        extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); +        final TestEntryController controller = new TestEntryController(); +        controller.setKey("123"); +        controller.setMetaData(new MetaData("category")); +        mEntriesProvider.addController(controller); +        mEntriesProvider.attachInfo(mContext, mProviderInfo); + +        final Bundle result = mEntriesProvider.call(METHOD_GET_DYNAMIC_SUMMARY, "uri", extras); + +        assertThat(result).isNull(); +    } + +    @Test +    public void getDynamicSummary_implementInterface_shouldReturnSummary() { +        final Bundle extras = new Bundle(); +        extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); +        final TestEntryController controller = new TestDynamicController(); +        controller.setKey("123"); +        controller.setMetaData(new MetaData("category")); +        mEntriesProvider.addController(controller); +        mEntriesProvider.attachInfo(mContext, mProviderInfo); + +        final Bundle result = mEntriesProvider.call(METHOD_GET_DYNAMIC_SUMMARY, "uri", extras); + +        assertThat(result.getString(META_DATA_PREFERENCE_SUMMARY)) +                .isEqualTo(TestDynamicController.SUMMARY); +    } + +    @Test +    public void onSwitchCheckedChangedSuccess_shouldReturnNoError() { +        final Bundle extras = new Bundle(); +        extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); +        final TestSwitchController controller = new TestSwitchController(); +        controller.setKey("123"); +        controller.setMetaData(new MetaData("category")); +        mEntriesProvider.addController(controller); +        mEntriesProvider.attachInfo(mContext, mProviderInfo); + +        final Bundle result = mEntriesProvider.call(METHOD_ON_CHECKED_CHANGED, "uri", extras); + +        assertThat(result.getBoolean(EXTRA_SWITCH_SET_CHECKED_ERROR)).isFalse(); +    } + +    @Test +    public void onSwitchCheckedChangedFailed_shouldReturnErrorMessage() { +        final Bundle extras = new Bundle(); +        extras.putString(META_DATA_PREFERENCE_KEYHINT, "123"); +        final TestSwitchController controller = new TestSwitchController(); +        controller.setKey("123"); +        controller.setMetaData(new MetaData("category")); +        controller.setSwitchErrorMessage("error"); +        mEntriesProvider.addController(controller); +        mEntriesProvider.attachInfo(mContext, mProviderInfo); + +        final Bundle result = mEntriesProvider.call(METHOD_ON_CHECKED_CHANGED, "uri", extras); + +        assertThat(result.getBoolean(EXTRA_SWITCH_SET_CHECKED_ERROR)).isTrue(); +        assertThat(result.getString(EXTRA_SWITCH_SET_CHECKED_ERROR_MESSAGE)).isEqualTo("error"); +    } + +    private static class TestEntriesProvider extends EntriesProvider { + +        private List<EntryController> mControllers; + +        @Override +        protected List<EntryController> createEntryControllers() { +            return mControllers; +        } + +        void addController(EntryController controller) { +            if (mControllers == null) { +                mControllers = new ArrayList<>(); +            } +            mControllers.add(controller); +        } +    } + +    private static class TestEntryController extends EntryController { + +        private String mKey; +        private MetaData mMetaData; + +        @Override +        public String getKey() { +            return mKey; +        } + +        @Override +        protected MetaData getMetaData() { +            return mMetaData; +        } + +        void setKey(String key) { +            mKey = key; +        } + +        void setMetaData(MetaData metaData) { +            mMetaData = metaData; +        } +    } + +    private static class TestSwitchController extends EntryController implements ProviderSwitch { + +        private String mKey; +        private MetaData mMetaData; +        private boolean mChecked; +        private String mErrorMsg; + +        @Override +        public String getKey() { +            return mKey; +        } + +        @Override +        protected MetaData getMetaData() { +            return mMetaData; +        } + +        @Override +        public boolean isSwitchChecked() { +            return mChecked; +        } + +        @Override +        public boolean onSwitchCheckedChanged(boolean checked) { +            return mErrorMsg == null ? true : false; +        } + +        @Override +        public String getSwitchErrorMessage(boolean attemptedChecked) { +            return mErrorMsg; +        } + +        void setKey(String key) { +            mKey = key; +        } + +        void setMetaData(MetaData metaData) { +            mMetaData = metaData; +        } + +        void setSwitchChecked(boolean checked) { +            mChecked = checked; +        } + +        void setSwitchErrorMessage(String errorMsg) { +            mErrorMsg = errorMsg; +        } +    } + +    private static class TestDynamicController extends TestEntryController +            implements ProviderIcon, DynamicTitle, DynamicSummary { + +        static final String TITLE = "title"; +        static final String SUMMARY = "summary"; +        static final Bundle ICON_BUNDLE = new Bundle(); + +        @Override +        public Bundle getProviderIcon() { +            return ICON_BUNDLE; +        } + +        @Override +        public String getDynamicTitle() { +            return TITLE; +        } + +        @Override +        public String getDynamicSummary() { +            return SUMMARY; +        } +    } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java index abfb407d749e..80f9efb8b5ac 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/ProviderTileTest.java @@ -17,20 +17,24 @@ package com.android.settingslib.drawer;  import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_ORDER;  import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_PROFILE; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_GROUP_KEY;  import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON;  import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SWITCH_URI;  import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_TITLE;  import static com.android.settingslib.drawer.TileUtils.PROFILE_ALL;  import static com.android.settingslib.drawer.TileUtils.PROFILE_PRIMARY;  import static com.google.common.truth.Truth.assertThat; +import android.app.PendingIntent;  import android.content.Context;  import android.content.Intent;  import android.content.pm.ApplicationInfo;  import android.content.pm.ProviderInfo;  import android.content.pm.ResolveInfo;  import android.os.Bundle; +import android.os.UserHandle;  import org.junit.Before;  import org.junit.Rule; @@ -173,13 +177,93 @@ public class ProviderTileTest {          assertThat(tile.mLastUpdateTime).isNotEqualTo(staleTimeStamp);      } +    @Test +    public void hasPendingIntent_empty_returnsFalse() { +        final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + +        assertThat(tile.hasPendingIntent()).isFalse(); +    } + +    @Test +    public void hasPendingIntent_notEmpty_returnsTrue() { +        final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); +        tile.pendingIntentMap.put( +                UserHandle.CURRENT, PendingIntent.getActivity(mContext, 0, new Intent(), 0)); + +        assertThat(tile.hasPendingIntent()).isTrue(); +    } + +    @Test +    public void hasGroupKey_empty_returnsFalse() { +        final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + +        assertThat(tile.hasGroupKey()).isFalse(); +    } + +    @Test +    public void hasGroupKey_notEmpty_returnsTrue() { +        mMetaData.putString(META_DATA_PREFERENCE_GROUP_KEY, "test_key"); +        final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + +        assertThat(tile.hasGroupKey()).isTrue(); +    } + +    @Test +    public void getGroupKey_empty_returnsNull() { +        final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + +        assertThat(tile.getGroupKey()).isNull(); +    } + +    @Test +    public void getGroupKey_notEmpty_returnsValue() { +        mMetaData.putString(META_DATA_PREFERENCE_GROUP_KEY, "test_key"); +        final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + +        assertThat(tile.getGroupKey()).isEqualTo("test_key"); +    } + +    @Test +    public void getType_withSwitch_returnsSwitch() { +        mMetaData.putString(META_DATA_PREFERENCE_SWITCH_URI, "test://testabc/"); +        final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + +        assertThat(tile.getType()).isEqualTo(Tile.Type.SWITCH); +    } + +    @Test +    public void getType_withSwitchAndPendingIntent_returnsSwitchWithAction() { +        mMetaData.putString(META_DATA_PREFERENCE_SWITCH_URI, "test://testabc/"); +        final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); +        tile.pendingIntentMap.put( +                UserHandle.CURRENT, PendingIntent.getActivity(mContext, 0, new Intent(), 0)); + +        assertThat(tile.getType()).isEqualTo(Tile.Type.SWITCH_WITH_ACTION); +    } + +    @Test +    public void getType_withPendingIntent_returnsExternalAction() { +        final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); +        tile.pendingIntentMap.put( +                UserHandle.CURRENT, PendingIntent.getActivity(mContext, 0, new Intent(), 0)); + +        assertThat(tile.getType()).isEqualTo(Tile.Type.EXTERNAL_ACTION); +    } + +    @Test +    public void getType_withoutSwitchAndPendingIntent_returnsGroup() { +        final Tile tile = new ProviderTile(mProviderInfo, "category", mMetaData); + +        assertThat(tile.getType()).isEqualTo(Tile.Type.GROUP); +    } +      @Implements(TileUtils.class)      private static class ShadowTileUtils {          private static Bundle sMetaData;          @Implementation -        protected static Bundle getSwitchDataFromProvider(Context context, String authority, +        protected static Bundle getEntryDataFromProvider(Context context, String authority,                  String key) {              return sMetaData;          } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/TileUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/TileUtilsTest.java index 906e06e81e2b..20864664e512 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/TileUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/drawer/TileUtilsTest.java @@ -21,6 +21,7 @@ import static com.android.settingslib.drawer.TileUtils.META_DATA_KEY_PROFILE;  import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON;  import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_ICON_URI;  import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_KEYHINT; +import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_PENDING_INTENT;  import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY;  import static com.android.settingslib.drawer.TileUtils.META_DATA_PREFERENCE_SUMMARY_URI;  import static com.android.settingslib.drawer.TileUtils.PROFILE_ALL; @@ -40,6 +41,7 @@ import static org.mockito.Mockito.when;  import static org.robolectric.RuntimeEnvironment.application;  import android.app.ActivityManager; +import android.app.PendingIntent;  import android.content.ContentResolver;  import android.content.Context;  import android.content.Intent; @@ -350,6 +352,53 @@ public class TileUtilsTest {          assertThat(outTiles).isEmpty();      } +    @Test +    public void loadTilesForAction_multipleUserProfiles_updatesUserHandle() { +        Map<Pair<String, String>, Tile> addedCache = new ArrayMap<>(); +        List<Tile> outTiles = new ArrayList<>(); +        List<ResolveInfo> info = new ArrayList<>(); +        ResolveInfo resolveInfo = newInfo(true, null /* category */, null, URI_GET_ICON, +                URI_GET_SUMMARY, null, 123, PROFILE_ALL); +        info.add(resolveInfo); + +        when(mPackageManager.queryIntentActivitiesAsUser(any(Intent.class), anyInt(), anyInt())) +                .thenReturn(info); + +        TileUtils.loadTilesForAction(mContext, UserHandle.CURRENT, IA_SETTINGS_ACTION, +                addedCache, null /* defaultCategory */, outTiles, false /* requiresSettings */); +        TileUtils.loadTilesForAction(mContext, new UserHandle(10), IA_SETTINGS_ACTION, +                addedCache, null /* defaultCategory */, outTiles, false /* requiresSettings */); + +        assertThat(outTiles).hasSize(1); +        assertThat(outTiles.get(0).userHandle) +                .containsExactly(UserHandle.CURRENT, new UserHandle(10)); +    } + +    @Test +    public void loadTilesForAction_withPendingIntent_updatesPendingIntentMap() { +        Map<Pair<String, String>, Tile> addedCache = new ArrayMap<>(); +        List<Tile> outTiles = new ArrayList<>(); +        List<ResolveInfo> info = new ArrayList<>(); +        ResolveInfo resolveInfo = newInfo(true, null /* category */, null, URI_GET_ICON, +                URI_GET_SUMMARY, null, 123, PROFILE_ALL); +        PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0); +        resolveInfo.activityInfo.metaData +                .putParcelable(META_DATA_PREFERENCE_PENDING_INTENT, pendingIntent); +        info.add(resolveInfo); + +        when(mPackageManager.queryIntentActivitiesAsUser(any(Intent.class), anyInt(), anyInt())) +                .thenReturn(info); + +        TileUtils.loadTilesForAction(mContext, UserHandle.CURRENT, IA_SETTINGS_ACTION, +                addedCache, null /* defaultCategory */, outTiles, false /* requiresSettings */); +        TileUtils.loadTilesForAction(mContext, new UserHandle(10), IA_SETTINGS_ACTION, +                addedCache, null /* defaultCategory */, outTiles, false /* requiresSettings */); + +        assertThat(outTiles).hasSize(1); +        assertThat(outTiles.get(0).pendingIntentMap).containsExactly( +                UserHandle.CURRENT, pendingIntent, new UserHandle(10), pendingIntent); +    } +      public static ResolveInfo newInfo(boolean systemApp, String category) {          return newInfo(systemApp, category, null);      } @@ -424,7 +473,7 @@ public class TileUtilsTest {          private static Bundle sMetaData;          @Implementation -        protected static List<Bundle> getSwitchDataFromProvider(Context context, String authority) { +        protected static List<Bundle> getEntryDataFromProvider(Context context, String authority) {              return Arrays.asList(sMetaData);          } diff --git a/packages/SystemUI/res/layout/biometric_prompt_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_layout.xml index 05ff1b1c2e6f..8eb62ecba5fa 100644 --- a/packages/SystemUI/res/layout/biometric_prompt_layout.xml +++ b/packages/SystemUI/res/layout/biometric_prompt_layout.xml @@ -39,6 +39,7 @@          android:singleLine="true"          android:marqueeRepeatLimit="1"          android:ellipsize="marquee" +        android:importantForAccessibility="no"          style="@style/TextAppearance.AuthCredential.Subtitle"/>      <TextView diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java index cd8f04d18500..ed4b91c7c4e4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricView.java @@ -737,7 +737,7 @@ public abstract class AuthBiometricView extends LinearLayout implements AuthBiom          });          mUseCredentialButton.setOnClickListener((view) -> { -            startTransitionToCredentialUI(); +            startTransitionToCredentialUI(false /* isError */);          });          mConfirmButton.setOnClickListener((view) -> { @@ -768,9 +768,12 @@ public abstract class AuthBiometricView extends LinearLayout implements AuthBiom      /**       * Kicks off the animation process and invokes the callback. +     * +     * @param isError if this was triggered due to an error and not a user action (unused, +     *                previously for haptics).       */      @Override -    public void startTransitionToCredentialUI() { +    public void startTransitionToCredentialUI(boolean isError) {          updateSize(AuthDialog.SIZE_LARGE);          mCallback.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL);      } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricViewAdapter.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricViewAdapter.kt index 631511c231e4..68db564606fd 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricViewAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthBiometricViewAdapter.kt @@ -38,7 +38,7 @@ interface AuthBiometricViewAdapter {      fun onHelp(@BiometricAuthenticator.Modality modality: Int, help: String) -    fun startTransitionToCredentialUI() +    fun startTransitionToCredentialUI(isError: Boolean)      fun requestLayout() diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index 7f706859abb3..7a2f2443dbd2 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -374,7 +374,6 @@ public class AuthContainerView extends LinearLayout          if (Utils.isBiometricAllowed(config.mPromptInfo)) {              mPromptSelectorInteractorProvider.get().useBiometricsForAuthentication(                      config.mPromptInfo, -                    config.mRequireConfirmation,                      config.mUserId,                      config.mOperationId,                      new BiometricModalities(fpProps, faceProps)); @@ -802,9 +801,9 @@ public class AuthContainerView extends LinearLayout      }      @Override -    public void animateToCredentialUI() { +    public void animateToCredentialUI(boolean isError) {          if (mBiometricView != null) { -            mBiometricView.startTransitionToCredentialUI(); +            mBiometricView.startTransitionToCredentialUI(isError);          } else {              Log.e(TAG, "animateToCredentialUI(): mBiometricView is null");          } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java index 57f1928fe545..96b144731a1b 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java @@ -85,7 +85,6 @@ import com.android.systemui.flags.FeatureFlags;  import com.android.systemui.keyguard.WakefulnessLifecycle;  import com.android.systemui.keyguard.data.repository.BiometricType;  import com.android.systemui.statusbar.CommandQueue; -import com.android.systemui.statusbar.VibratorHelper;  import com.android.systemui.util.concurrency.DelayableExecutor;  import com.android.systemui.util.concurrency.Execution; @@ -185,18 +184,6 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,      private final @Background DelayableExecutor mBackgroundExecutor;      private final DisplayInfo mCachedDisplayInfo = new DisplayInfo(); -    private final VibratorHelper mVibratorHelper; - -    private void vibrateSuccess(int modality) { -        mVibratorHelper.vibrateAuthSuccess( -                getClass().getSimpleName() + ", modality = " + modality + "BP::success"); -    } - -    private void vibrateError(int modality) { -        mVibratorHelper.vibrateAuthError( -                getClass().getSimpleName() + ", modality = " + modality + "BP::error"); -    } -      @VisibleForTesting      final TaskStackListener mTaskStackListener = new TaskStackListener() {          @Override @@ -776,7 +763,6 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,              @NonNull InteractionJankMonitor jankMonitor,              @Main Handler handler,              @Background DelayableExecutor bgExecutor, -            @NonNull VibratorHelper vibrator,              @NonNull UdfpsUtils udfpsUtils) {          mContext = context;          mFeatureFlags = featureFlags; @@ -798,7 +784,6 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,          mUdfpsEnrolledForUser = new SparseBooleanArray();          mSfpsEnrolledForUser = new SparseBooleanArray();          mFaceEnrolledForUser = new SparseBooleanArray(); -        mVibratorHelper = vibrator;          mUdfpsUtils = udfpsUtils;          mApplicationCoroutineScope = applicationCoroutineScope; @@ -987,8 +972,6 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,      public void onBiometricAuthenticated(@Modality int modality) {          if (DEBUG) Log.d(TAG, "onBiometricAuthenticated: "); -        vibrateSuccess(modality); -          if (mCurrentDialog != null) {              mCurrentDialog.onAuthenticationSucceeded(modality);          } else { @@ -1048,6 +1031,18 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,          return false;      } +    private String getNotRecognizedString(@Modality int modality) { +        final int messageRes; +        final int userId = mCurrentDialogArgs.argi1; +        if (isFaceAuthEnrolled(userId) && isFingerprintEnrolled(userId)) { +            messageRes = modality == TYPE_FACE +                    ? R.string.biometric_face_not_recognized +                    : R.string.fingerprint_error_not_match; +        } else { +            messageRes = R.string.biometric_not_recognized; +        } +        return mContext.getString(messageRes); +    }      private String getErrorString(@Modality int modality, int error, int vendorCode) {          switch (modality) { @@ -1073,8 +1068,6 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,              Log.d(TAG, String.format("onBiometricError(%d, %d, %d)", modality, error, vendorCode));          } -        vibrateError(modality); -          final boolean isLockout = (error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT)                  || (error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT); @@ -1091,10 +1084,11 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,          if (mCurrentDialog != null) {              if (mCurrentDialog.isAllowDeviceCredentials() && isLockout) {                  if (DEBUG) Log.d(TAG, "onBiometricError, lockout"); -                mCurrentDialog.animateToCredentialUI(); +                mCurrentDialog.animateToCredentialUI(true /* isError */);              } else if (isSoftError) { -                final String errorMessage = (error == BiometricConstants.BIOMETRIC_PAUSED_REJECTED) -                        ? mContext.getString(R.string.biometric_not_recognized) +                final String errorMessage = (error == BiometricConstants.BIOMETRIC_PAUSED_REJECTED +                        || error == BiometricConstants.BIOMETRIC_ERROR_TIMEOUT) +                        ? getNotRecognizedString(modality)                          : getErrorString(modality, error, vendorCode);                  if (DEBUG) Log.d(TAG, "onBiometricError, soft error: " + errorMessage);                  // The camera privacy error can return before the prompt initializes its state, @@ -1204,8 +1198,11 @@ public class AuthController implements CoreStartable, CommandQueue.Callbacks,          final PromptInfo promptInfo = (PromptInfo) args.arg1;          final int[] sensorIds = (int[]) args.arg3; + +        // TODO(b/251476085): remove these unused parameters (replaced with SSOT elsewhere)          final boolean credentialAllowed = (boolean) args.arg4;          final boolean requireConfirmation = (boolean) args.arg5; +          final int userId = args.argi1;          final String opPackageName = (String) args.arg6;          final long operationId = args.argl1; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java index b6eabfa76e36..3cfc6f280110 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java @@ -162,7 +162,7 @@ public interface AuthDialog extends Dumpable {      /**       * Animate to credential UI. Typically called after biometric is locked out.       */ -    void animateToCredentialUI(); +    void animateToCredentialUI(boolean isError);      /**       * @return true if device credential is allowed. diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java index 6db266f4f1cb..9d8dcc1efdd8 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java @@ -21,6 +21,8 @@ import static android.app.PendingIntent.FLAG_IMMUTABLE;  import static com.android.systemui.biometrics.BiometricNotificationBroadcastReceiver.ACTION_SHOW_FACE_REENROLL_DIALOG;  import static com.android.systemui.biometrics.BiometricNotificationBroadcastReceiver.ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG; +import android.annotation.NonNull; +import android.annotation.Nullable;  import android.app.Notification;  import android.app.NotificationChannel;  import android.app.NotificationManager; @@ -30,6 +32,9 @@ import android.content.Intent;  import android.content.IntentFilter;  import android.hardware.biometrics.BiometricFaceConstants;  import android.hardware.biometrics.BiometricSourceType; +import android.hardware.biometrics.BiometricStateListener; +import android.hardware.face.FaceManager; +import android.hardware.fingerprint.FingerprintManager;  import android.os.Handler;  import android.os.UserHandle;  import android.provider.Settings; @@ -42,7 +47,6 @@ import com.android.systemui.R;  import com.android.systemui.dagger.SysUISingleton;  import com.android.systemui.statusbar.policy.KeyguardStateController; -  import java.util.Optional;  import javax.inject.Inject; @@ -69,6 +73,8 @@ public class BiometricNotificationService implements CoreStartable {      private final NotificationManager mNotificationManager;      private final BiometricNotificationBroadcastReceiver mBroadcastReceiver;      private final FingerprintReEnrollNotification mFingerprintReEnrollNotification; +    private final FingerprintManager mFingerprintManager; +    private final FaceManager mFaceManager;      private NotificationChannel mNotificationChannel;      private boolean mFaceNotificationQueued;      private boolean mFingerprintNotificationQueued; @@ -119,14 +125,29 @@ public class BiometricNotificationService implements CoreStartable {                  }              }; +    private final BiometricStateListener mFaceStateListener = new BiometricStateListener() { +        @Override +        public void onEnrollmentsChanged(int userId, int sensorId, boolean hasEnrollments) { +            mNotificationManager.cancelAsUser(TAG, FACE_NOTIFICATION_ID, UserHandle.CURRENT); +        } +    }; + +    private final BiometricStateListener mFingerprintStateListener = new BiometricStateListener() { +        @Override +        public void onEnrollmentsChanged(int userId, int sensorId, boolean hasEnrollments) { +            mNotificationManager.cancelAsUser(TAG, FINGERPRINT_NOTIFICATION_ID, UserHandle.CURRENT); +        } +    };      @Inject -    public BiometricNotificationService(Context context, -            KeyguardUpdateMonitor keyguardUpdateMonitor, -            KeyguardStateController keyguardStateController, -            Handler handler, NotificationManager notificationManager, -            BiometricNotificationBroadcastReceiver biometricNotificationBroadcastReceiver, -            Optional<FingerprintReEnrollNotification> fingerprintReEnrollNotification) { +    public BiometricNotificationService(@NonNull Context context, +            @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor, +            @NonNull KeyguardStateController keyguardStateController, +            @NonNull Handler handler, @NonNull NotificationManager notificationManager, +            @NonNull BiometricNotificationBroadcastReceiver biometricNotificationBroadcastReceiver, +            @NonNull Optional<FingerprintReEnrollNotification> fingerprintReEnrollNotification, +            @Nullable FingerprintManager fingerprintManager, +            @Nullable FaceManager faceManager) {          mContext = context;          mKeyguardUpdateMonitor = keyguardUpdateMonitor;          mKeyguardStateController = keyguardStateController; @@ -135,6 +156,8 @@ public class BiometricNotificationService implements CoreStartable {          mBroadcastReceiver = biometricNotificationBroadcastReceiver;          mFingerprintReEnrollNotification = fingerprintReEnrollNotification.orElse(                  new FingerprintReEnrollNotificationImpl()); +        mFingerprintManager = fingerprintManager; +        mFaceManager = faceManager;      }      @Override @@ -148,9 +171,16 @@ public class BiometricNotificationService implements CoreStartable {          intentFilter.addAction(ACTION_SHOW_FACE_REENROLL_DIALOG);          mContext.registerReceiver(mBroadcastReceiver, intentFilter,                  Context.RECEIVER_EXPORTED_UNAUDITED); +        if (mFingerprintManager != null) { +            mFingerprintManager.registerBiometricStateListener(mFingerprintStateListener); +        } +        if (mFaceManager != null) { +            mFaceManager.registerBiometricStateListener(mFaceStateListener); +        }      }      private void queueFaceReenrollNotification() { +        Log.d(TAG, "Face re-enroll notification queued.");          mFaceNotificationQueued = true;          final String title = mContext.getString(R.string.face_re_enroll_notification_title);          final String content = mContext.getString( @@ -163,6 +193,7 @@ public class BiometricNotificationService implements CoreStartable {      }      private void queueFingerprintReenrollNotification() { +        Log.d(TAG, "Fingerprint re-enroll notification queued.");          mFingerprintNotificationQueued = true;          final String title = mContext.getString(R.string.fingerprint_re_enroll_notification_title);          final String content = mContext.getString( diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java index 10e45dadee60..16d12bbfe3b4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java @@ -188,6 +188,8 @@ public class UdfpsController implements DozeReceiver, Dumpable {      @Nullable private VelocityTracker mVelocityTracker;      // The ID of the pointer for which ACTION_DOWN has occurred. -1 means no pointer is active.      private int mActivePointerId = -1; +    // Whether a pointer has been pilfered for current gesture +    private boolean mPointerPilfered = false;      // The timestamp of the most recent touch log.      private long mTouchLogTime;      // The timestamp of the most recent log of a touch InteractionEvent. @@ -354,7 +356,8 @@ public class UdfpsController implements DozeReceiver, Dumpable {                  UdfpsController.this.mAlternateTouchProvider.onUiReady();              } else {                  final long requestId = (mOverlay != null) ? mOverlay.getRequestId() : 0L; -                UdfpsController.this.mFingerprintManager.onUiReady(requestId, sensorId); +                UdfpsController.this.mFingerprintManager.onUdfpsUiEvent( +                        FingerprintManager.UDFPS_UI_READY, requestId, sensorId);              }          }      } @@ -557,6 +560,11 @@ public class UdfpsController implements DozeReceiver, Dumpable {                  || mPrimaryBouncerInteractor.isInTransit()) {              return false;          } +        if (event.getAction() == MotionEvent.ACTION_DOWN +                || event.getAction() == MotionEvent.ACTION_HOVER_ENTER) { +            // Reset on ACTION_DOWN, start of new gesture +            mPointerPilfered = false; +        }          final TouchProcessorResult result = mTouchProcessor.processTouch(event, mActivePointerId,                  mOverlayParams); @@ -636,10 +644,11 @@ public class UdfpsController implements DozeReceiver, Dumpable {              shouldPilfer = true;          } -        // Execute the pilfer -        if (shouldPilfer) { +        // Pilfer only once per gesture +        if (shouldPilfer && !mPointerPilfered) {              mInputManager.pilferPointers(                      mOverlay.getOverlayView().getViewRootImpl().getInputToken()); +            mPointerPilfered = true;          }          return processedTouch.getTouchData().isWithinBounds(mOverlayParams.getNativeSensorBounds()); @@ -958,6 +967,10 @@ public class UdfpsController implements DozeReceiver, Dumpable {              mOnFingerDown = false;              mAttemptedToDismissKeyguard = false;              mOrientationListener.enable(); +            if (mFingerprintManager != null) { +                mFingerprintManager.onUdfpsUiEvent(FingerprintManager.UDFPS_UI_OVERLAY_SHOWN, +                        overlay.getRequestId(), mSensorProps.sensorId); +            }          } else {              Log.v(TAG, "showUdfpsOverlay | the overlay is already showing");          } @@ -1099,7 +1112,8 @@ public class UdfpsController implements DozeReceiver, Dumpable {                  mLatencyTracker.onActionEnd(LatencyTracker.ACTION_UDFPS_ILLUMINATE);              });          } else { -            mFingerprintManager.onUiReady(requestId, mSensorProps.sensorId); +            mFingerprintManager.onUdfpsUiEvent(FingerprintManager.UDFPS_UI_READY, requestId, +                    mSensorProps.sensorId);              mLatencyTracker.onActionEnd(LatencyTracker.ACTION_UDFPS_ILLUMINATE);          }      } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt index ddf1457e385c..a5e846ad61ca 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt @@ -17,6 +17,8 @@  package com.android.systemui.biometrics.dagger  import com.android.settingslib.udfps.UdfpsUtils +import com.android.systemui.biometrics.data.repository.FaceSettingsRepository +import com.android.systemui.biometrics.data.repository.FaceSettingsRepositoryImpl  import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository  import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepositoryImpl  import com.android.systemui.biometrics.data.repository.PromptRepository @@ -47,6 +49,10 @@ interface BiometricsModule {      @Binds      @SysUISingleton +    fun faceSettings(impl: FaceSettingsRepositoryImpl): FaceSettingsRepository + +    @Binds +    @SysUISingleton      fun biometricPromptRepository(impl: PromptRepositoryImpl): PromptRepository      @Binds diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepository.kt new file mode 100644 index 000000000000..3d5ed823f771 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepository.kt @@ -0,0 +1,57 @@ +/* + * 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.systemui.biometrics.data.repository + +import android.os.Handler +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.util.settings.SecureSettings +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject + +/** + * Repository for the global state of users Face Unlock preferences. + * + * Largely a wrapper around [SecureSettings]'s proxy to Settings.Secure. + */ +interface FaceSettingsRepository { + +    /** Get Settings for the given user [id]. */ +    fun forUser(id: Int?): FaceUserSettingsRepository +} + +@SysUISingleton +class FaceSettingsRepositoryImpl +@Inject +constructor( +    @Main private val mainHandler: Handler, +    private val secureSettings: SecureSettings, +) : FaceSettingsRepository { + +    private val userSettings = ConcurrentHashMap<Int, FaceUserSettingsRepository>() + +    override fun forUser(id: Int?): FaceUserSettingsRepository = +        if (id != null) { +            userSettings.computeIfAbsent(id) { _ -> +                FaceUserSettingsRepositoryImpl(id, mainHandler, secureSettings).also { repo -> +                    repo.start() +                } +            } +        } else { +            FaceUserSettingsRepositoryImpl.Empty +        } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FaceUserSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FaceUserSettingsRepository.kt new file mode 100644 index 000000000000..68c4a10fcfad --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FaceUserSettingsRepository.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics.data.repository + +import android.database.ContentObserver +import android.os.Handler +import android.provider.Settings.Secure.FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.util.settings.SecureSettings +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flowOf + +/** Settings for a user. */ +interface FaceUserSettingsRepository { +    /** The user's id. */ +    val userId: Int + +    /** If BiometricPrompt should always require confirmation (overrides app's preference). */ +    val alwaysRequireConfirmationInApps: Flow<Boolean> +} + +class FaceUserSettingsRepositoryImpl( +    override val userId: Int, +    @Main private val mainHandler: Handler, +    private val secureSettings: SecureSettings, +) : FaceUserSettingsRepository { + +    /** Indefinitely subscribe to user preference changes. */ +    fun start() { +        watch( +            FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION, +            _alwaysRequireConfirmationInApps, +        ) +    } + +    private var _alwaysRequireConfirmationInApps = MutableStateFlow(false) +    override val alwaysRequireConfirmationInApps: Flow<Boolean> = +        _alwaysRequireConfirmationInApps.asStateFlow() + +    /** Defaults to use when no user is specified. */ +    object Empty : FaceUserSettingsRepository { +        override val userId = -1 +        override val alwaysRequireConfirmationInApps = flowOf(false) +    } + +    private fun watch( +        key: String, +        toUpdate: MutableStateFlow<Boolean>, +        defaultValue: Boolean = false, +    ) = secureSettings.watch(userId, mainHandler, key, defaultValue) { v -> toUpdate.value = v } +} + +private fun SecureSettings.watch( +    userId: Int, +    handler: Handler, +    key: String, +    defaultValue: Boolean = false, +    onChange: (Boolean) -> Unit, +) { +    fun fetch(): Boolean = getIntForUser(key, if (defaultValue) 1 else 0, userId) > 0 + +    registerContentObserverForUser( +        key, +        false /* notifyForDescendants */, +        object : ContentObserver(handler) { +            override fun onChange(selfChange: Boolean) = onChange(fetch()) +        }, +        userId +    ) + +    onChange(fetch()) +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt index b4dc272b71da..b35fbbc7bb32 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/PromptRepository.kt @@ -1,3 +1,19 @@ +/* + * 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.systemui.biometrics.data.repository  import android.hardware.biometrics.PromptInfo @@ -12,6 +28,10 @@ import kotlinx.coroutines.flow.Flow  import kotlinx.coroutines.flow.MutableStateFlow  import kotlinx.coroutines.flow.StateFlow  import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map  /**   * A repository for the global state of BiometricPrompt. @@ -40,7 +60,7 @@ interface PromptRepository {       *       * Note: overlaps/conflicts with [PromptInfo.isConfirmationRequested], which needs clean up.       */ -    val isConfirmationRequired: StateFlow<Boolean> +    val isConfirmationRequired: Flow<Boolean>      /** Update the prompt configuration, which should be set before [isShowing]. */      fun setPrompt( @@ -48,7 +68,6 @@ interface PromptRepository {          userId: Int,          gatekeeperChallenge: Long?,          kind: PromptKind, -        requireConfirmation: Boolean = false,      )      /** Unset the prompt info. */ @@ -56,8 +75,12 @@ interface PromptRepository {  }  @SysUISingleton -class PromptRepositoryImpl @Inject constructor(private val authController: AuthController) : -    PromptRepository { +class PromptRepositoryImpl +@Inject +constructor( +    private val faceSettings: FaceSettingsRepository, +    private val authController: AuthController, +) : PromptRepository {      override val isShowing: Flow<Boolean> = conflatedCallbackFlow {          val callback = @@ -85,21 +108,30 @@ class PromptRepositoryImpl @Inject constructor(private val authController: AuthC      private val _kind: MutableStateFlow<PromptKind> = MutableStateFlow(PromptKind.Biometric())      override val kind = _kind.asStateFlow() -    private val _isConfirmationRequired: MutableStateFlow<Boolean> = MutableStateFlow(false) -    override val isConfirmationRequired = _isConfirmationRequired.asStateFlow() +    private val _faceSettings = +        _userId.map { id -> faceSettings.forUser(id) }.distinctUntilChanged() +    private val _faceSettingAlwaysRequireConfirmation = +        _faceSettings.flatMapLatest { it.alwaysRequireConfirmationInApps }.distinctUntilChanged() + +    private val _isConfirmationRequired = _promptInfo.map { it?.isConfirmationRequested ?: false } +    override val isConfirmationRequired = +        combine(_isConfirmationRequired, _faceSettingAlwaysRequireConfirmation) { +                appRequiresConfirmation, +                forceRequireConfirmation -> +                forceRequireConfirmation || appRequiresConfirmation +            } +            .distinctUntilChanged()      override fun setPrompt(          promptInfo: PromptInfo,          userId: Int,          gatekeeperChallenge: Long?,          kind: PromptKind, -        requireConfirmation: Boolean,      ) {          _kind.value = kind          _userId.value = userId          _challenge.value = gatekeeperChallenge          _promptInfo.value = promptInfo -        _isConfirmationRequired.value = requireConfirmation      }      override fun unsetPrompt() { @@ -107,7 +139,6 @@ class PromptRepositoryImpl @Inject constructor(private val authController: AuthC          _userId.value = null          _challenge.value = null          _kind.value = PromptKind.Biometric() -        _isConfirmationRequired.value = false      }      companion object { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt index e6e07f9d7794..be99dd92f1bd 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt @@ -59,13 +59,15 @@ interface PromptSelectorInteractor {       */      val credentialKind: Flow<PromptKind> -    /** If the API caller requested explicit confirmation after successful authentication. */ -    val isConfirmationRequested: Flow<Boolean> +    /** +     * If the API caller or the user's personal preferences require explicit confirmation after +     * successful authentication. +     */ +    val isConfirmationRequired: Flow<Boolean>      /** Use biometrics for authentication. */      fun useBiometricsForAuthentication(          promptInfo: PromptInfo, -        requireConfirmation: Boolean,          userId: Int,          challenge: Long,          modalities: BiometricModalities, @@ -114,10 +116,8 @@ constructor(              }          } -    override val isConfirmationRequested: Flow<Boolean> = -        promptRepository.promptInfo -            .map { info -> info?.isConfirmationRequested ?: false } -            .distinctUntilChanged() +    override val isConfirmationRequired: Flow<Boolean> = +        promptRepository.isConfirmationRequired.distinctUntilChanged()      override val isCredentialAllowed: Flow<Boolean> =          promptRepository.promptInfo @@ -142,7 +142,6 @@ constructor(      override fun useBiometricsForAuthentication(          promptInfo: PromptInfo, -        requireConfirmation: Boolean,          userId: Int,          challenge: Long,          modalities: BiometricModalities @@ -152,7 +151,6 @@ constructor(              userId = userId,              gatekeeperChallenge = challenge,              kind = PromptKind.Biometric(modalities), -            requireConfirmation = requireConfirmation,          )      } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricModality.kt b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/BiometricModality.kt index 3197c0935d0b..fb580ca54aff 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricModality.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/shared/model/BiometricModality.kt @@ -14,7 +14,7 @@   * limitations under the License.   */ -package com.android.systemui.biometrics.domain.model +package com.android.systemui.biometrics.shared.model  import android.hardware.biometrics.BiometricAuthenticator diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index 8486c3f96b21..e5a4d1a644f1 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -46,9 +46,9 @@ import com.android.systemui.biometrics.AuthIconController  import com.android.systemui.biometrics.AuthPanelController  import com.android.systemui.biometrics.Utils  import com.android.systemui.biometrics.domain.model.BiometricModalities -import com.android.systemui.biometrics.domain.model.BiometricModality -import com.android.systemui.biometrics.domain.model.asBiometricModality +import com.android.systemui.biometrics.shared.model.BiometricModality  import com.android.systemui.biometrics.shared.model.PromptKind +import com.android.systemui.biometrics.shared.model.asBiometricModality  import com.android.systemui.biometrics.ui.BiometricPromptLayout  import com.android.systemui.biometrics.ui.viewmodel.FingerprintStartMode  import com.android.systemui.biometrics.ui.viewmodel.PromptMessage @@ -158,7 +158,7 @@ object BiometricViewBinder {                  view.updateFingerprintAffordanceSize(iconController)              }              if (iconController is HackyCoexIconController) { -                iconController.faceMode = !viewModel.isConfirmationRequested.first() +                iconController.faceMode = !viewModel.isConfirmationRequired.first()              }              // the icon controller must be created before this happens for the legacy @@ -339,7 +339,13 @@ object BiometricViewBinder {                              launch {                                  delay(authState.delay) -                                legacyCallback.onAction(Callback.ACTION_AUTHENTICATED) +                                legacyCallback.onAction( +                                    if (authState.isAuthenticatedAndExplicitlyConfirmed) { +                                        Callback.ACTION_AUTHENTICATED_AND_CONFIRMED +                                    } else { +                                        Callback.ACTION_AUTHENTICATED +                                    } +                                )                              }                          }                      } @@ -390,7 +396,6 @@ private class Spaghetti(      private var lifecycleScope: CoroutineScope? = null      private var modalities: BiometricModalities = BiometricModalities() -    private var faceFailedAtLeastOnce = false      private var legacyCallback: Callback? = null      override var legacyIconController: AuthIconController? = null @@ -470,19 +475,15 @@ private class Spaghetti(          viewModel.ensureFingerprintHasStarted(isDelayed = true)          applicationScope.launch { -            val suppress = -                modalities.hasFaceAndFingerprint && -                    (failedModality == BiometricModality.Face) && -                    faceFailedAtLeastOnce -            if (failedModality == BiometricModality.Face) { -                faceFailedAtLeastOnce = true -            } -              viewModel.showTemporaryError(                  failureReason,                  messageAfterError = modalities.asDefaultHelpMessage(applicationContext),                  authenticateAfterError = modalities.hasFingerprint, -                suppressIfErrorShowing = suppress, +                suppressIf = { currentMessage -> +                    modalities.hasFaceAndFingerprint && +                        failedModality == BiometricModality.Face && +                        currentMessage.isError +                },                  failedModality = failedModality,              )          } @@ -495,11 +496,10 @@ private class Spaghetti(          }          applicationScope.launch { -            val suppress = -                modalities.hasFaceAndFingerprint && (errorModality == BiometricModality.Face)              viewModel.showTemporaryError(                  error, -                suppressIfErrorShowing = suppress, +                messageAfterError = modalities.asDefaultHelpMessage(applicationContext), +                authenticateAfterError = modalities.hasFingerprint,              )              delay(BiometricPrompt.HIDE_DIALOG_DELAY.toLong())              legacyCallback?.onAction(Callback.ACTION_ERROR) @@ -512,9 +512,12 @@ private class Spaghetti(          }          applicationScope.launch { -            viewModel.showTemporaryHelp( +            // help messages from the HAL should be displayed as temporary (i.e. soft) errors +            viewModel.showTemporaryError(                  help, -                messageAfterHelp = modalities.asDefaultHelpMessage(applicationContext), +                messageAfterError = modalities.asDefaultHelpMessage(applicationContext), +                authenticateAfterError = modalities.hasFingerprint, +                hapticFeedback = false,              )          }      } @@ -527,7 +530,7 @@ private class Spaghetti(              else -> false          } -    override fun startTransitionToCredentialUI() { +    override fun startTransitionToCredentialUI(isError: Boolean) {          applicationScope.launch {              viewModel.onSwitchToCredential()              legacyCallback?.onAction(Callback.ACTION_USE_DEVICE_CREDENTIAL) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt index 1dffa80a084f..1a286cf57982 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt @@ -28,6 +28,7 @@ import android.view.accessibility.AccessibilityManager  import android.widget.TextView  import androidx.core.animation.addListener  import androidx.core.view.doOnLayout +import androidx.core.view.isGone  import androidx.lifecycle.lifecycleScope  import com.android.systemui.R  import com.android.systemui.biometrics.AuthDialog @@ -78,9 +79,11 @@ object BiometricViewSizeBinder {          // cache the original position of the icon view (as done in legacy view)          // this must happen before any size changes can be made -        var iconHolderOriginalY = 0f          view.doOnLayout { -            iconHolderOriginalY = iconHolderView.y +            // TODO(b/251476085): this old way of positioning has proven itself unreliable +            // remove this and associated thing like (UdfpsDialogMeasureAdapter) and +            // pin to the physical sensor +            val iconHolderOriginalY = iconHolderView.y              // bind to prompt              // TODO(b/251476085): migrate the legacy panel controller and simplify this @@ -141,7 +144,11 @@ object BiometricViewSizeBinder {                                          listOf(                                              iconHolderView.asVerticalAnimator(                                                  duration = duration.toLong(), -                                                toY = iconHolderOriginalY, +                                                toY = +                                                    iconHolderOriginalY - +                                                        viewsToHideWhenSmall +                                                            .filter { it.isGone } +                                                            .sumOf { it.height },                                              ),                                              viewsToFadeInOnSizeChange.asFadeInAnimator(                                                  duration = duration.toLong(), diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthState.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthState.kt index 9cb91b3d51a7..2f9557f70a32 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthState.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthState.kt @@ -16,7 +16,7 @@  package com.android.systemui.biometrics.ui.viewmodel -import com.android.systemui.biometrics.domain.model.BiometricModality +import com.android.systemui.biometrics.shared.model.BiometricModality  /**   * The authenticated state with the [authenticatedModality] (when [isAuthenticated]) with an @@ -29,10 +29,16 @@ data class PromptAuthState(      val needsUserConfirmation: Boolean = false,      val delay: Long = 0,  ) { +    private var wasConfirmed = false +      /** If authentication was successful and the user has confirmed (or does not need to). */      val isAuthenticatedAndConfirmed: Boolean          get() = isAuthenticated && !needsUserConfirmation +    /** Same as [isAuthenticatedAndConfirmed] but only true if the user clicked a confirm button. */ +    val isAuthenticatedAndExplicitlyConfirmed: Boolean +        get() = isAuthenticated && wasConfirmed +      /** If a successful authentication has not occurred. */      val isNotAuthenticated: Boolean          get() = !isAuthenticated @@ -45,12 +51,16 @@ data class PromptAuthState(      val isAuthenticatedByFingerprint: Boolean          get() = isAuthenticated && authenticatedModality == BiometricModality.Fingerprint -    /** Copies this state, but toggles [needsUserConfirmation] to false. */ -    fun asConfirmed(): PromptAuthState = +    /** +     * Copies this state, but toggles [needsUserConfirmation] to false and ensures that +     * [isAuthenticatedAndExplicitlyConfirmed] is true. +     */ +    fun asExplicitlyConfirmed(): PromptAuthState =          PromptAuthState( -            isAuthenticated = isAuthenticated, -            authenticatedModality = authenticatedModality, -            needsUserConfirmation = false, -            delay = delay, -        ) +                isAuthenticated = isAuthenticated, +                authenticatedModality = authenticatedModality, +                needsUserConfirmation = false, +                delay = delay, +            ) +            .apply { wasConfirmed = true }  } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptMessage.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptMessage.kt index 219da716f7d9..50f491142949 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptMessage.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptMessage.kt @@ -33,9 +33,9 @@ sealed interface PromptMessage {                  else -> ""              } -    /** If this is an [Error] or [Help] message. */ -    val isErrorOrHelp: Boolean -        get() = this is Error || this is Help +    /** If this is an [Error]. */ +    val isError: Boolean +        get() = this is Error      /** An error message. */      data class Error(val errorMessage: String) : PromptMessage diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index 2f8ed096f4ba..8a2e4059ee73 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -20,8 +20,9 @@ import android.util.Log  import com.android.systemui.biometrics.AuthBiometricView  import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor  import com.android.systemui.biometrics.domain.model.BiometricModalities -import com.android.systemui.biometrics.domain.model.BiometricModality +import com.android.systemui.biometrics.shared.model.BiometricModality  import com.android.systemui.biometrics.shared.model.PromptKind +import com.android.systemui.statusbar.VibratorHelper  import javax.inject.Inject  import kotlinx.coroutines.Job  import kotlinx.coroutines.coroutineScope @@ -41,6 +42,7 @@ class PromptViewModel  @Inject  constructor(      private val interactor: PromptSelectorInteractor, +    private val vibrator: VibratorHelper,  ) {      /** The set of modalities available for this prompt */      val modalities: Flow<BiometricModalities> = @@ -61,8 +63,11 @@ constructor(      /** If the user has successfully authenticated and confirmed (when explicitly required). */      val isAuthenticated: Flow<PromptAuthState> = _isAuthenticated.asStateFlow() -    /** If the API caller requested explicit confirmation after successful authentication. */ -    val isConfirmationRequested: Flow<Boolean> = interactor.isConfirmationRequested +    /** +     * If the API caller or the user's personal preferences require explicit confirmation after +     * successful authentication. +     */ +    val isConfirmationRequired: Flow<Boolean> = interactor.isConfirmationRequired      /** The kind of credential the user has. */      val credentialKind: Flow<PromptKind> = interactor.credentialKind @@ -91,7 +96,7 @@ constructor(                  _forceLargeSize,                  _forceMediumSize,                  modalities, -                interactor.isConfirmationRequested, +                interactor.isConfirmationRequired,                  fingerprintStartMode,              ) { forceLarge, forceMedium, modalities, confirmationRequired, fpStartMode ->                  when { @@ -202,37 +207,43 @@ constructor(      private var messageJob: Job? = null      /** -     * Show a temporary error [message] associated with an optional [failedModality]. +     * Show a temporary error [message] associated with an optional [failedModality] and play +     * [hapticFeedback].       * -     * An optional [messageAfterError] will be shown via [showAuthenticating] when -     * [authenticateAfterError] is set (or via [showHelp] when not set) after the error is -     * dismissed. +     * The [messageAfterError] will be shown via [showAuthenticating] when [authenticateAfterError] +     * is set (or via [showHelp] when not set) after the error is dismissed.       * -     * The error is ignored if the user has already authenticated and it is treated as -     * [onSilentError] if [suppressIfErrorShowing] is set and an error message is already showing. +     * The error is ignored if the user has already authenticated or if [suppressIf] is true given +     * the currently showing [PromptMessage].       */      suspend fun showTemporaryError(          message: String, -        messageAfterError: String = "", -        authenticateAfterError: Boolean = false, -        suppressIfErrorShowing: Boolean = false, +        messageAfterError: String, +        authenticateAfterError: Boolean, +        suppressIf: (PromptMessage) -> Boolean = { false }, +        hapticFeedback: Boolean = true,          failedModality: BiometricModality = BiometricModality.None,      ) = coroutineScope {          if (_isAuthenticated.value.isAuthenticated) {              return@coroutineScope          } -        if (_message.value.isErrorOrHelp && suppressIfErrorShowing) { -            onSilentError(failedModality) + +        _canTryAgainNow.value = supportsRetry(failedModality) + +        if (suppressIf(_message.value)) {              return@coroutineScope          }          _isAuthenticating.value = false          _isAuthenticated.value = PromptAuthState(false)          _forceMediumSize.value = true -        _canTryAgainNow.value = supportsRetry(failedModality)          _message.value = PromptMessage.Error(message)          _legacyState.value = AuthBiometricView.STATE_ERROR +        if (hapticFeedback) { +            vibrator.error(failedModality) +        } +          messageJob?.cancel()          messageJob = launch {              delay(BiometricPrompt.HIDE_DIALOG_DELAY.toLong()) @@ -245,18 +256,6 @@ constructor(      }      /** -     * Call instead of [showTemporaryError] if an error from the HAL should be silently ignored to -     * enable retry (if the [failedModality] supports retrying). -     * -     * Ignored if the user has already authenticated. -     */ -    private fun onSilentError(failedModality: BiometricModality = BiometricModality.None) { -        if (_isAuthenticated.value.isNotAuthenticated) { -            _canTryAgainNow.value = supportsRetry(failedModality) -        } -    } - -    /**       * Call to ensure the fingerprint sensor has started. Either when the dialog is first shown       * (most cases) or when it should be enabled after a first error (coex implicit flow).       */ @@ -373,6 +372,10 @@ constructor(                  AuthBiometricView.STATE_AUTHENTICATED              } +        if (!needsUserConfirmation) { +            vibrator.success(modality) +        } +          messageJob?.cancel()          messageJob = null @@ -383,18 +386,18 @@ constructor(      private suspend fun needsExplicitConfirmation(modality: BiometricModality): Boolean {          val availableModalities = modalities.first() -        val confirmationRequested = interactor.isConfirmationRequested.first() +        val confirmationRequired = isConfirmationRequired.first()          if (availableModalities.hasFaceAndFingerprint) {              // coex only needs confirmation when face is successful, unless it happens on the              // first attempt (i.e. without failure) before fingerprint scanning starts +            val fingerprintStarted = fingerprintStartMode.first() != FingerprintStartMode.Pending              if (modality == BiometricModality.Face) { -                return (fingerprintStartMode.first() != FingerprintStartMode.Pending) || -                    confirmationRequested +                return fingerprintStarted || confirmationRequired              }          }          if (availableModalities.hasFaceOnly) { -            return confirmationRequested +            return confirmationRequired          }          // fingerprint only never requires confirmation          return false @@ -409,15 +412,16 @@ constructor(      fun confirmAuthenticated() {          val authState = _isAuthenticated.value          if (authState.isNotAuthenticated) { -            "Cannot show authenticated after authenticated"              Log.w(TAG, "Cannot confirm authenticated when not authenticated")              return          } -        _isAuthenticated.value = authState.asConfirmed() +        _isAuthenticated.value = authState.asExplicitlyConfirmed()          _message.value = PromptMessage.Empty          _legacyState.value = AuthBiometricView.STATE_AUTHENTICATED +        vibrator.success(authState.authenticatedModality) +          messageJob?.cancel()          messageJob = null      } @@ -431,6 +435,12 @@ constructor(          _forceLargeSize.value = true      } +    private fun VibratorHelper.success(modality: BiometricModality) = +        vibrateAuthSuccess("$TAG, modality = $modality BP::success") + +    private fun VibratorHelper.error(modality: BiometricModality = BiometricModality.None) = +        vibrateAuthError("$TAG, modality = $modality BP::error") +      companion object {          private const val TAG = "PromptViewModel"      } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java index a431a59fcef6..a90980fddfb0 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java @@ -16,6 +16,7 @@  package com.android.systemui.dagger; +import com.android.systemui.globalactions.ShutdownUiModule;  import com.android.systemui.keyguard.CustomizationProvider;  import com.android.systemui.statusbar.NotificationInsetsModule;  import com.android.systemui.statusbar.QsFrameTranslateModule; @@ -31,6 +32,7 @@ import dagger.Subcomponent;          DependencyProvider.class,          NotificationInsetsModule.class,          QsFrameTranslateModule.class, +        ShutdownUiModule.class,          SystemUIBinder.class,          SystemUIModule.class,          SystemUICoreStartableModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 366056af7ab3..5087b8bd18c9 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -221,7 +221,7 @@ object Flags {      // TODO(b/267722622): Tracking Bug      @JvmField      val WALLPAPER_PICKER_UI_FOR_AIWP = -            unreleasedFlag( +            releasedFlag(                      229,                      "wallpaper_picker_ui_for_aiwp"              ) @@ -662,7 +662,7 @@ object Flags {      @JvmField val UDFPS_NEW_TOUCH_DETECTION = releasedFlag(2200, "udfps_new_touch_detection")      @JvmField val UDFPS_ELLIPSE_DETECTION = releasedFlag(2201, "udfps_ellipse_detection")      // TODO(b/278622168): Tracking Bug -    @JvmField val BIOMETRIC_BP_STRONG = unreleasedFlag(2202, "biometric_bp_strong") +    @JvmField val BIOMETRIC_BP_STRONG = releasedFlag(2202, "biometric_bp_strong")      // 2300 - stylus      @JvmField val TRACK_STYLUS_EVER_USED = releasedFlag(2300, "track_stylus_ever_used") diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java index 290bf0d0734c..c5027cc511a4 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsImpl.java @@ -15,27 +15,12 @@  package com.android.systemui.globalactions;  import static android.app.StatusBarManager.DISABLE2_GLOBAL_ACTIONS; -import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; -import android.annotation.Nullable; -import android.annotation.StringRes; -import android.app.Dialog;  import android.content.Context; -import android.os.PowerManager; -import android.view.View; -import android.view.ViewGroup; -import android.view.Window; -import android.view.WindowManager; -import android.widget.ProgressBar; -import android.widget.TextView; -import com.android.internal.R; -import com.android.settingslib.Utils;  import com.android.systemui.plugins.GlobalActions; -import com.android.systemui.scrim.ScrimDrawable;  import com.android.systemui.statusbar.BlurUtils;  import com.android.systemui.statusbar.CommandQueue; -import com.android.systemui.statusbar.phone.ScrimController;  import com.android.systemui.statusbar.policy.DeviceProvisionedController;  import com.android.systemui.statusbar.policy.KeyguardStateController; @@ -50,12 +35,14 @@ public class GlobalActionsImpl implements GlobalActions, CommandQueue.Callbacks      private final CommandQueue mCommandQueue;      private final GlobalActionsDialogLite mGlobalActionsDialog;      private boolean mDisabled; +    private ShutdownUi mShutdownUi;      @Inject      public GlobalActionsImpl(Context context, CommandQueue commandQueue,              GlobalActionsDialogLite globalActionsDialog, BlurUtils blurUtils,              KeyguardStateController keyguardStateController, -            DeviceProvisionedController deviceProvisionedController) { +            DeviceProvisionedController deviceProvisionedController, +            ShutdownUi shutdownUi) {          mContext = context;          mGlobalActionsDialog = globalActionsDialog;          mKeyguardStateController = keyguardStateController; @@ -63,6 +50,7 @@ public class GlobalActionsImpl implements GlobalActions, CommandQueue.Callbacks          mCommandQueue = commandQueue;          mBlurUtils = blurUtils;          mCommandQueue.addCallback(this); +        mShutdownUi = shutdownUi;      }      @Override @@ -80,103 +68,8 @@ public class GlobalActionsImpl implements GlobalActions, CommandQueue.Callbacks      @Override      public void showShutdownUi(boolean isReboot, String reason) { -        ScrimDrawable background = new ScrimDrawable(); - -        final Dialog d = new Dialog(mContext, -                com.android.systemui.R.style.Theme_SystemUI_Dialog_GlobalActions); - -        d.setOnShowListener(dialog -> { -            if (mBlurUtils.supportsBlursOnWindows()) { -                int backgroundAlpha = (int) (ScrimController.BUSY_SCRIM_ALPHA * 255); -                background.setAlpha(backgroundAlpha); -                mBlurUtils.applyBlur(d.getWindow().getDecorView().getViewRootImpl(), -                        (int) mBlurUtils.blurRadiusOfRatio(1), backgroundAlpha == 255); -            } else { -                float backgroundAlpha = mContext.getResources().getFloat( -                        com.android.systemui.R.dimen.shutdown_scrim_behind_alpha); -                background.setAlpha((int) (backgroundAlpha * 255)); -            } -        }); - -        // Window initialization -        Window window = d.getWindow(); -        window.requestFeature(Window.FEATURE_NO_TITLE); -        window.getAttributes().systemUiVisibility |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN -                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE -                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; -        // Inflate the decor view, so the attributes below are not overwritten by the theme. -        window.getDecorView(); -        window.getAttributes().width = ViewGroup.LayoutParams.MATCH_PARENT; -        window.getAttributes().height = ViewGroup.LayoutParams.MATCH_PARENT; -        window.getAttributes().layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; -        window.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY); -        window.getAttributes().setFitInsetsTypes(0 /* types */); -        window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); -        window.addFlags( -                WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN -                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL -                        | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR -                        | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED -                        | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH -                        | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); -        window.setBackgroundDrawable(background); -        window.setWindowAnimations(com.android.systemui.R.style.Animation_ShutdownUi); - -        d.setContentView(R.layout.shutdown_dialog); -        d.setCancelable(false); - -        int color; -        if (mBlurUtils.supportsBlursOnWindows()) { -            color = Utils.getColorAttrDefaultColor(mContext, -                    com.android.systemui.R.attr.wallpaperTextColor); -        } else { -            color = mContext.getResources().getColor( -                    com.android.systemui.R.color.global_actions_shutdown_ui_text); -        } - -        ProgressBar bar = d.findViewById(R.id.progress); -        bar.getIndeterminateDrawable().setTint(color); - -        TextView reasonView = d.findViewById(R.id.text1); -        TextView messageView = d.findViewById(R.id.text2); - -        reasonView.setTextColor(color); -        messageView.setTextColor(color); - -        messageView.setText(getRebootMessage(isReboot, reason)); -        String rebootReasonMessage = getReasonMessage(reason); -        if (rebootReasonMessage != null) { -            reasonView.setVisibility(View.VISIBLE); -            reasonView.setText(rebootReasonMessage); -        } - -        d.show(); -    } - -    @StringRes -    private int getRebootMessage(boolean isReboot, @Nullable String reason) { -        if (reason != null && reason.startsWith(PowerManager.REBOOT_RECOVERY_UPDATE)) { -            return R.string.reboot_to_update_reboot; -        } else if (reason != null && reason.equals(PowerManager.REBOOT_RECOVERY)) { -            return R.string.reboot_to_reset_message; -        } else if (isReboot) { -            return R.string.reboot_to_reset_message; -        } else { -            return R.string.shutdown_progress; -        } +        mShutdownUi.showShutdownUi(isReboot, reason);      } - -    @Nullable -    private String getReasonMessage(@Nullable String reason) { -        if (reason != null && reason.startsWith(PowerManager.REBOOT_RECOVERY_UPDATE)) { -            return mContext.getString(R.string.reboot_to_update_title); -        } else if (reason != null && reason.equals(PowerManager.REBOOT_RECOVERY)) { -            return mContext.getString(R.string.reboot_to_reset_title); -        } else { -            return null; -        } -    } -      @Override      public void disable(int displayId, int state1, int state2, boolean animate) {          final boolean disabled = (state2 & DISABLE2_GLOBAL_ACTIONS) != 0; diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUi.java b/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUi.java new file mode 100644 index 000000000000..68dc1b3dc7d7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUi.java @@ -0,0 +1,164 @@ +/* + * 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.systemui.globalactions; + +import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + +import android.annotation.Nullable; +import android.annotation.StringRes; +import android.app.Dialog; +import android.content.Context; +import android.os.PowerManager; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.annotation.VisibleForTesting; + +import com.android.internal.R; +import com.android.settingslib.Utils; +import com.android.systemui.scrim.ScrimDrawable; +import com.android.systemui.statusbar.BlurUtils; +import com.android.systemui.statusbar.phone.ScrimController; + +/** + * Provides the UI shown during system shutdown. + */ +public class ShutdownUi { + +    private Context mContext; +    private BlurUtils mBlurUtils; +    public ShutdownUi(Context context, BlurUtils blurUtils) { +        mContext = context; +        mBlurUtils = blurUtils; +    } + +    /** +     * Display the shutdown UI. +     * @param isReboot Whether the device will be rebooting after this shutdown. +     * @param reason Cause for the shutdown. +     */ +    public void showShutdownUi(boolean isReboot, String reason) { +        ScrimDrawable background = new ScrimDrawable(); + +        final Dialog d = new Dialog(mContext, +                com.android.systemui.R.style.Theme_SystemUI_Dialog_GlobalActions); + +        d.setOnShowListener(dialog -> { +            if (mBlurUtils.supportsBlursOnWindows()) { +                int backgroundAlpha = (int) (ScrimController.BUSY_SCRIM_ALPHA * 255); +                background.setAlpha(backgroundAlpha); +                mBlurUtils.applyBlur(d.getWindow().getDecorView().getViewRootImpl(), +                        (int) mBlurUtils.blurRadiusOfRatio(1), backgroundAlpha == 255); +            } else { +                float backgroundAlpha = mContext.getResources().getFloat( +                        com.android.systemui.R.dimen.shutdown_scrim_behind_alpha); +                background.setAlpha((int) (backgroundAlpha * 255)); +            } +        }); + +        // Window initialization +        Window window = d.getWindow(); +        window.requestFeature(Window.FEATURE_NO_TITLE); +        window.getAttributes().systemUiVisibility |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN +                | View.SYSTEM_UI_FLAG_LAYOUT_STABLE +                | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; +        // Inflate the decor view, so the attributes below are not overwritten by the theme. +        window.getDecorView(); +        window.getAttributes().width = ViewGroup.LayoutParams.MATCH_PARENT; +        window.getAttributes().height = ViewGroup.LayoutParams.MATCH_PARENT; +        window.getAttributes().layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; +        window.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY); +        window.getAttributes().setFitInsetsTypes(0 /* types */); +        window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); +        window.addFlags( +                WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN +                        | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL +                        | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR +                        | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED +                        | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH +                        | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); +        window.setBackgroundDrawable(background); +        window.setWindowAnimations(com.android.systemui.R.style.Animation_ShutdownUi); + +        d.setContentView(getShutdownDialogContent(isReboot)); +        d.setCancelable(false); + +        int color; +        if (mBlurUtils.supportsBlursOnWindows()) { +            color = Utils.getColorAttrDefaultColor(mContext, +                    com.android.systemui.R.attr.wallpaperTextColor); +        } else { +            color = mContext.getResources().getColor( +                    com.android.systemui.R.color.global_actions_shutdown_ui_text); +        } + +        ProgressBar bar = d.findViewById(R.id.progress); +        bar.getIndeterminateDrawable().setTint(color); + +        TextView reasonView = d.findViewById(R.id.text1); +        TextView messageView = d.findViewById(R.id.text2); + +        reasonView.setTextColor(color); +        messageView.setTextColor(color); + +        messageView.setText(getRebootMessage(isReboot, reason)); +        String rebootReasonMessage = getReasonMessage(reason); +        if (rebootReasonMessage != null) { +            reasonView.setVisibility(View.VISIBLE); +            reasonView.setText(rebootReasonMessage); +        } + +        d.show(); +    } + +    /** +     * Returns the layout resource to use for UI while shutting down. +     * @param isReboot Whether this is a reboot or a shutdown. +     * @return +     */ +    public int getShutdownDialogContent(boolean isReboot) { +        return R.layout.shutdown_dialog; +    } + +    @StringRes +    @VisibleForTesting int getRebootMessage(boolean isReboot, @Nullable String reason) { +        if (reason != null && reason.startsWith(PowerManager.REBOOT_RECOVERY_UPDATE)) { +            return R.string.reboot_to_update_reboot; +        } else if (reason != null && reason.equals(PowerManager.REBOOT_RECOVERY)) { +            return R.string.reboot_to_reset_message; +        } else if (isReboot) { +            return R.string.reboot_to_reset_message; +        } else { +            return R.string.shutdown_progress; +        } +    } + +    @Nullable +    @VisibleForTesting String getReasonMessage(@Nullable String reason) { +        if (reason != null && reason.startsWith(PowerManager.REBOOT_RECOVERY_UPDATE)) { +            return mContext.getString(R.string.reboot_to_update_title); +        } else if (reason != null && reason.equals(PowerManager.REBOOT_RECOVERY)) { +            return mContext.getString(R.string.reboot_to_reset_title); +        } else { +            return null; +        } +    } +} diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUiModule.kt b/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUiModule.kt new file mode 100644 index 000000000000..b7285da49bb7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUiModule.kt @@ -0,0 +1,31 @@ +/* + * 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.systemui.globalactions + +import android.content.Context +import com.android.systemui.statusbar.BlurUtils +import dagger.Module +import dagger.Provides + +/** Provides the UI shown during system shutdown. */ +@Module +class ShutdownUiModule { +    /** Shutdown UI provider. */ +    @Provides +    fun provideShutdownUi(context: Context?, blurUtils: BlurUtils?): ShutdownUi { +        return ShutdownUi(context, blurUtils) +    } +} diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSysUIComponent.java b/packages/SystemUI/src/com/android/systemui/tv/TvSysUIComponent.java index 640adcc9dd94..5e489b0f38ac 100644 --- a/packages/SystemUI/src/com/android/systemui/tv/TvSysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/tv/TvSysUIComponent.java @@ -20,15 +20,14 @@ import com.android.systemui.dagger.DefaultComponentBinder;  import com.android.systemui.dagger.DependencyProvider;  import com.android.systemui.dagger.SysUIComponent;  import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.SystemUIBinder;  import com.android.systemui.dagger.SystemUIModule; +import com.android.systemui.globalactions.ShutdownUiModule; +import com.android.systemui.keyguard.dagger.KeyguardModule; +import com.android.systemui.recents.RecentsModule;  import com.android.systemui.statusbar.dagger.CentralSurfacesDependenciesModule;  import com.android.systemui.statusbar.notification.dagger.NotificationsModule;  import com.android.systemui.statusbar.notification.row.NotificationRowModule; -import com.android.systemui.keyguard.dagger.KeyguardModule; -import com.android.systemui.recents.RecentsModule; -  import dagger.Subcomponent;  /** @@ -43,6 +42,7 @@ import dagger.Subcomponent;          NotificationRowModule.class,          NotificationsModule.class,          RecentsModule.class, +        ShutdownUiModule.class,          SystemUIModule.class,          TvSystemUIBinder.class,          TVSystemUICoreStartableModule.class, diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index d31a86ae2809..e3e61306bcd7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -51,6 +51,7 @@ import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel  import com.android.systemui.flags.FakeFeatureFlags  import com.android.systemui.flags.Flags  import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.statusbar.VibratorHelper  import com.android.systemui.util.concurrency.FakeExecutor  import com.android.systemui.util.time.FakeSystemClock  import com.google.common.truth.Truth.assertThat @@ -99,6 +100,8 @@ open class AuthContainerViewTest : SysuiTestCase() {      lateinit var windowToken: IBinder      @Mock      lateinit var interactionJankMonitor: InteractionJankMonitor +    @Mock +    lateinit var vibrator: VibratorHelper      // TODO(b/278622168): remove with flag      open val useNewBiometricPrompt = false @@ -325,7 +328,7 @@ open class AuthContainerViewTest : SysuiTestCase() {              authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK or                      BiometricManager.Authenticators.DEVICE_CREDENTIAL          ) -        container.animateToCredentialUI() +        container.animateToCredentialUI(false)          waitForIdleSync()          assertThat(container.hasCredentialView()).isTrue() @@ -514,7 +517,7 @@ open class AuthContainerViewTest : SysuiTestCase() {          { authBiometricFingerprintViewModel },          { promptSelectorInteractor },          { bpCredentialInteractor }, -        PromptViewModel(promptSelectorInteractor), +        PromptViewModel(promptSelectorInteractor, vibrator),          { credentialViewModel },          Handler(TestableLooper.get(this).looper),          fakeExecutor diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java index b9f92a064bc8..0a73a9eba9ee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java @@ -191,6 +191,10 @@ public class AuthControllerTest extends SysuiTestCase {      private ArgumentCaptor<IFaceAuthenticatorsRegisteredCallback> mFaceAuthenticatorsRegisteredCaptor;      @Captor      private ArgumentCaptor<BiometricStateListener> mBiometricStateCaptor; +    @Captor +    private ArgumentCaptor<Integer> mModalityCaptor; +    @Captor +    private ArgumentCaptor<String> mMessageCaptor;      @Mock      private Resources mResources; @@ -202,9 +206,6 @@ public class AuthControllerTest extends SysuiTestCase {      private TestableAuthController mAuthController;      private FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); -    @Mock -    private VibratorHelper mVibratorHelper; -      @Before      public void setup() throws RemoteException {          // TODO(b/278622168): remove with flag @@ -267,7 +268,6 @@ public class AuthControllerTest extends SysuiTestCase {                          true /* supportsSelfIllumination */,                          true /* resetLockoutRequireHardwareAuthToken */));          when(mFaceManager.getSensorPropertiesInternal()).thenReturn(faceProps); -        when(mVibratorHelper.hasVibrator()).thenReturn(true);          mAuthController = new TestableAuthController(mContextSpy); @@ -482,16 +482,63 @@ public class AuthControllerTest extends SysuiTestCase {                  BiometricConstants.BIOMETRIC_PAUSED_REJECTED,                  0 /* vendorCode */); -        ArgumentCaptor<Integer> modalityCaptor = ArgumentCaptor.forClass(Integer.class); -        ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class); -        verify(mDialog1).onAuthenticationFailed(modalityCaptor.capture(), messageCaptor.capture()); +        verify(mDialog1).onAuthenticationFailed(mModalityCaptor.capture(), mMessageCaptor.capture()); -        assertEquals(modalityCaptor.getValue().intValue(), modality); -        assertEquals(messageCaptor.getValue(), +        assertEquals(mModalityCaptor.getValue().intValue(), modality); +        assertEquals(mMessageCaptor.getValue(),                  mContext.getString(R.string.biometric_not_recognized));      }      @Test +    public void testOnAuthenticationFailedInvoked_coex_whenFaceAuthRejected_withPaused() { +        testOnAuthenticationFailedInvoked_coex_whenFaceAuthRejected( +                BiometricConstants.BIOMETRIC_PAUSED_REJECTED); +    } + +    @Test +    public void testOnAuthenticationFailedInvoked_coex_whenFaceAuthRejected_withTimeout() { +        testOnAuthenticationFailedInvoked_coex_whenFaceAuthRejected( +                BiometricConstants.BIOMETRIC_ERROR_TIMEOUT); +    } + +    private void testOnAuthenticationFailedInvoked_coex_whenFaceAuthRejected(int error) { +        final int modality = BiometricAuthenticator.TYPE_FACE; +        final int userId = 0; + +        enrollFingerprintAndFace(userId); + +        showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */); + +        mAuthController.onBiometricError(modality, error, 0 /* vendorCode */); + +        verify(mDialog1).onAuthenticationFailed(mModalityCaptor.capture(), mMessageCaptor.capture()); + +        assertThat(mModalityCaptor.getValue().intValue()).isEqualTo(modality); +        assertThat(mMessageCaptor.getValue()).isEqualTo( +                mContext.getString(R.string.biometric_face_not_recognized)); +    } + +    @Test +    public void testOnAuthenticationFailedInvoked_whenFingerprintAuthRejected() { +        final int modality = BiometricAuthenticator.TYPE_FINGERPRINT; +        final int userId = 0; + +        enrollFingerprintAndFace(userId); + +        showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */); + +        mAuthController.onBiometricError(modality, +                BiometricConstants.BIOMETRIC_PAUSED_REJECTED, +                0 /* vendorCode */); + +        verify(mDialog1).onAuthenticationFailed(mModalityCaptor.capture(), mMessageCaptor.capture()); + +        assertThat(mModalityCaptor.getValue().intValue()).isEqualTo(modality); +        assertThat(mMessageCaptor.getValue()).isEqualTo( +                mContext.getString(R.string.fingerprint_error_not_match)); +    } + +    @Test      public void testOnAuthenticationFailedInvoked_whenBiometricTimedOut() {          showDialog(new int[] {1} /* sensorIds */, false /* credentialAllowed */);          final int modality = BiometricAuthenticator.TYPE_FACE; @@ -499,13 +546,11 @@ public class AuthControllerTest extends SysuiTestCase {          final int vendorCode = 0;          mAuthController.onBiometricError(modality, error, vendorCode); -        ArgumentCaptor<Integer> modalityCaptor = ArgumentCaptor.forClass(Integer.class); -        ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class); -        verify(mDialog1).onAuthenticationFailed(modalityCaptor.capture(), messageCaptor.capture()); +        verify(mDialog1).onAuthenticationFailed(mModalityCaptor.capture(), mMessageCaptor.capture()); -        assertEquals(modalityCaptor.getValue().intValue(), modality); -        assertEquals(messageCaptor.getValue(), -                FaceManager.getErrorString(mContext, error, vendorCode)); +        assertThat(mModalityCaptor.getValue().intValue()).isEqualTo(modality); +        assertThat(mMessageCaptor.getValue()).isEqualTo( +                mContext.getString(R.string.biometric_not_recognized));      }      @Test @@ -515,12 +560,10 @@ public class AuthControllerTest extends SysuiTestCase {          final String helpMessage = "help";          mAuthController.onBiometricHelp(modality, helpMessage); -        ArgumentCaptor<Integer> modalityCaptor = ArgumentCaptor.forClass(Integer.class); -        ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class); -        verify(mDialog1).onHelp(modalityCaptor.capture(), messageCaptor.capture()); +        verify(mDialog1).onHelp(mModalityCaptor.capture(), mMessageCaptor.capture()); -        assertEquals(modalityCaptor.getValue().intValue(), modality); -        assertEquals(messageCaptor.getValue(), helpMessage); +        assertThat(mModalityCaptor.getValue().intValue()).isEqualTo(modality); +        assertThat(mMessageCaptor.getValue()).isEqualTo(helpMessage);      }      @Test @@ -531,12 +574,10 @@ public class AuthControllerTest extends SysuiTestCase {          final int vendorCode = 0;          mAuthController.onBiometricError(modality, error, vendorCode); -        ArgumentCaptor<Integer> modalityCaptor = ArgumentCaptor.forClass(Integer.class); -        ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class); -        verify(mDialog1).onError(modalityCaptor.capture(), messageCaptor.capture()); +        verify(mDialog1).onError(mModalityCaptor.capture(), mMessageCaptor.capture()); -        assertEquals(modalityCaptor.getValue().intValue(), modality); -        assertEquals(messageCaptor.getValue(), +        assertThat(mModalityCaptor.getValue().intValue()).isEqualTo(modality); +        assertThat(mMessageCaptor.getValue()).isEqualTo(                  FaceManager.getErrorString(mContext, error, vendorCode));      } @@ -550,7 +591,7 @@ public class AuthControllerTest extends SysuiTestCase {          mAuthController.onBiometricError(BiometricAuthenticator.TYPE_FACE, error, vendorCode);          verify(mDialog1, never()).onError(anyInt(), anyString()); -        verify(mDialog1).animateToCredentialUI(); +        verify(mDialog1).animateToCredentialUI(eq(true));      }      @Test @@ -563,7 +604,7 @@ public class AuthControllerTest extends SysuiTestCase {          mAuthController.onBiometricError(BiometricAuthenticator.TYPE_FACE, error, vendorCode);          verify(mDialog1, never()).onError(anyInt(), anyString()); -        verify(mDialog1).animateToCredentialUI(); +        verify(mDialog1).animateToCredentialUI(eq(true));      }      @Test @@ -578,7 +619,7 @@ public class AuthControllerTest extends SysuiTestCase {          mAuthController.onBiometricError(modality, error, vendorCode);          verify(mDialog1).onError(                  eq(modality), eq(FaceManager.getErrorString(mContext, error, vendorCode))); -        verify(mDialog1, never()).animateToCredentialUI(); +        verify(mDialog1, never()).animateToCredentialUI(eq(true));      }      @Test @@ -593,7 +634,7 @@ public class AuthControllerTest extends SysuiTestCase {          mAuthController.onBiometricError(modality, error, vendorCode);          verify(mDialog1).onError(                  eq(modality), eq(FaceManager.getErrorString(mContext, error, vendorCode))); -        verify(mDialog1, never()).animateToCredentialUI(); +        verify(mDialog1, never()).animateToCredentialUI(eq(true));      }      @Test @@ -998,6 +1039,31 @@ public class AuthControllerTest extends SysuiTestCase {          return HAT;      } +    private void enrollFingerprintAndFace(final int userId) { + +        // Enroll fingerprint +        verify(mFingerprintManager).registerBiometricStateListener( +                mBiometricStateCaptor.capture()); +        assertFalse(mAuthController.isFingerprintEnrolled(userId)); + +        mBiometricStateCaptor.getValue().onEnrollmentsChanged(userId, +                1 /* sensorId */, true /* hasEnrollments */); +        waitForIdleSync(); + +        assertTrue(mAuthController.isFingerprintEnrolled(userId)); + +        // Enroll face +        verify(mFaceManager).registerBiometricStateListener( +                mBiometricStateCaptor.capture()); +        assertFalse(mAuthController.isFaceAuthEnrolled(userId)); + +        mBiometricStateCaptor.getValue().onEnrollmentsChanged(userId, +                2 /* sensorId */, true /* hasEnrollments */); +        waitForIdleSync(); + +        assertTrue(mAuthController.isFaceAuthEnrolled(userId)); +    } +      private final class TestableAuthController extends AuthController {          private int mBuildCount = 0;          private PromptInfo mLastBiometricPromptInfo; @@ -1012,7 +1078,7 @@ public class AuthControllerTest extends SysuiTestCase {                      () -> mBiometricPromptCredentialInteractor, () -> mPromptSelectionInteractor,                      () -> mCredentialViewModel, () -> mPromptViewModel,                      mInteractionJankMonitor, mHandler, -                    mBackgroundExecutor, mVibratorHelper, mUdfpsUtils); +                    mBackgroundExecutor, mUdfpsUtils);          }          @Override diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java index 38c9caf085e2..9cb3b1aa9a55 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationServiceTest.java @@ -30,7 +30,11 @@ import android.app.Notification;  import android.app.NotificationManager;  import android.hardware.biometrics.BiometricFaceConstants;  import android.hardware.biometrics.BiometricSourceType; +import android.hardware.biometrics.BiometricStateListener; +import android.hardware.face.FaceManager; +import android.hardware.fingerprint.FingerprintManager;  import android.os.Handler; +import android.os.UserHandle;  import android.testing.AndroidTestingRunner;  import android.testing.TestableLooper; @@ -69,6 +73,10 @@ public class BiometricNotificationServiceTest extends SysuiTestCase {      Optional<FingerprintReEnrollNotification> mFingerprintReEnrollNotificationOptional;      @Mock      FingerprintReEnrollNotification mFingerprintReEnrollNotification; +    @Mock +    FingerprintManager mFingerprintManager; +    @Mock +    FaceManager mFaceManager;      private static final String TAG = "BiometricNotificationService";      private static final int FACE_NOTIFICATION_ID = 1; @@ -81,6 +89,8 @@ public class BiometricNotificationServiceTest extends SysuiTestCase {      private TestableLooper mLooper;      private KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback;      private KeyguardStateController.Callback mKeyguardStateControllerCallback; +    private BiometricStateListener mFaceStateListener; +    private BiometricStateListener mFingerprintStateListener;      @Before      public void setUp() { @@ -99,25 +109,37 @@ public class BiometricNotificationServiceTest extends SysuiTestCase {                          mKeyguardUpdateMonitor, mKeyguardStateController, handler,                          mNotificationManager,                          broadcastReceiver, -                        mFingerprintReEnrollNotificationOptional); +                        mFingerprintReEnrollNotificationOptional, +                        mFingerprintManager, +                        mFaceManager);          biometricNotificationService.start();          ArgumentCaptor<KeyguardUpdateMonitorCallback> updateMonitorCallbackArgumentCaptor =                  ArgumentCaptor.forClass(KeyguardUpdateMonitorCallback.class);          ArgumentCaptor<KeyguardStateController.Callback> stateControllerCallbackArgumentCaptor =                  ArgumentCaptor.forClass(KeyguardStateController.Callback.class); +        ArgumentCaptor<BiometricStateListener> faceStateListenerArgumentCaptor = +                ArgumentCaptor.forClass(BiometricStateListener.class); +        ArgumentCaptor<BiometricStateListener> fingerprintStateListenerArgumentCaptor = +                ArgumentCaptor.forClass(BiometricStateListener.class);          verify(mKeyguardUpdateMonitor).registerCallback(                  updateMonitorCallbackArgumentCaptor.capture());          verify(mKeyguardStateController).addCallback(                  stateControllerCallbackArgumentCaptor.capture()); +        verify(mFaceManager).registerBiometricStateListener( +                faceStateListenerArgumentCaptor.capture()); +        verify(mFingerprintManager).registerBiometricStateListener( +                fingerprintStateListenerArgumentCaptor.capture()); +        mFaceStateListener = faceStateListenerArgumentCaptor.getValue(); +        mFingerprintStateListener = fingerprintStateListenerArgumentCaptor.getValue();          mKeyguardUpdateMonitorCallback = updateMonitorCallbackArgumentCaptor.getValue();          mKeyguardStateControllerCallback = stateControllerCallbackArgumentCaptor.getValue();      }      @Test -    public void testShowFingerprintReEnrollNotification() { +    public void testShowFingerprintReEnrollNotification_onAcquiredReEnroll() {          when(mKeyguardStateController.isShowing()).thenReturn(false);          mKeyguardUpdateMonitorCallback.onBiometricHelp( @@ -139,7 +161,7 @@ public class BiometricNotificationServiceTest extends SysuiTestCase {                  .isEqualTo(ACTION_SHOW_FINGERPRINT_REENROLL_DIALOG);      }      @Test -    public void testShowFaceReEnrollNotification() { +    public void testShowFaceReEnrollNotification_onErrorReEnroll() {          when(mKeyguardStateController.isShowing()).thenReturn(false);          mKeyguardUpdateMonitorCallback.onBiometricError( @@ -161,4 +183,52 @@ public class BiometricNotificationServiceTest extends SysuiTestCase {                  .isEqualTo(ACTION_SHOW_FACE_REENROLL_DIALOG);      } +    @Test +    public void testCancelReEnrollmentNotification_onFaceEnrollmentStateChange() { +        when(mKeyguardStateController.isShowing()).thenReturn(false); + +        mKeyguardUpdateMonitorCallback.onBiometricError( +                BiometricFaceConstants.BIOMETRIC_ERROR_RE_ENROLL, +                "Testing Face Re-enrollment" /* errString */, +                BiometricSourceType.FACE +        ); +        mKeyguardStateControllerCallback.onKeyguardShowingChanged(); + +        mLooper.moveTimeForward(SHOW_NOTIFICATION_DELAY_MS); +        mLooper.processAllMessages(); + +        verify(mNotificationManager).notifyAsUser(eq(TAG), eq(FACE_NOTIFICATION_ID), +                mNotificationArgumentCaptor.capture(), any()); + +        mFaceStateListener.onEnrollmentsChanged(0 /* userId */, 0 /* sensorId */, +                false /* hasEnrollments */); + +        verify(mNotificationManager).cancelAsUser(eq(TAG), eq(FACE_NOTIFICATION_ID), +                eq(UserHandle.CURRENT)); +    } + +    @Test +    public void testCancelReEnrollmentNotification_onFingerprintEnrollmentStateChange() { +        when(mKeyguardStateController.isShowing()).thenReturn(false); + +        mKeyguardUpdateMonitorCallback.onBiometricHelp( +                FINGERPRINT_ACQUIRED_RE_ENROLL, +                "Testing Fingerprint Re-enrollment" /* errString */, +                BiometricSourceType.FINGERPRINT +        ); +        mKeyguardStateControllerCallback.onKeyguardShowingChanged(); + +        mLooper.moveTimeForward(SHOW_NOTIFICATION_DELAY_MS); +        mLooper.processAllMessages(); + +        verify(mNotificationManager).notifyAsUser(eq(TAG), eq(FINGERPRINT_NOTIFICATION_ID), +                mNotificationArgumentCaptor.capture(), any()); + +        mFingerprintStateListener.onEnrollmentsChanged(0 /* userId */, 0 /* sensorId */, +                false /* hasEnrollments */); + +        verify(mNotificationManager).cancelAsUser(eq(TAG), eq(FINGERPRINT_NOTIFICATION_ID), +                eq(UserHandle.CURRENT)); +    } +  } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java index 78341915edb7..7578cc774c48 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java @@ -442,6 +442,16 @@ public class UdfpsControllerTest extends SysuiTestCase {      }      @Test +    public void showUdfpsOverlay_callsListener() throws RemoteException { +        mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId, +                BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); +        mFgExecutor.runAllReady(); + +        verify(mFingerprintManager).onUdfpsUiEvent(FingerprintManager.UDFPS_UI_OVERLAY_SHOWN, +                TEST_REQUEST_ID, mOpticalProps.sensorId); +    } + +    @Test      public void testSubscribesToOrientationChangesWhenShowingOverlay() throws Exception {          mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,                  BiometricOverlayConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback); @@ -761,17 +771,20 @@ public class UdfpsControllerTest extends SysuiTestCase {                  inOrder.verify(mAlternateTouchProvider).onUiReady();                  inOrder.verify(mLatencyTracker).onActionEnd(                          eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE)); -                verify(mFingerprintManager, never()).onUiReady(anyLong(), anyInt()); +                verify(mFingerprintManager, never()).onUdfpsUiEvent( +                        eq(FingerprintManager.UDFPS_UI_READY), anyLong(), anyInt());              } else {                  InOrder inOrder = inOrder(mFingerprintManager, mLatencyTracker); -                inOrder.verify(mFingerprintManager).onUiReady(eq(TEST_REQUEST_ID), +                inOrder.verify(mFingerprintManager).onUdfpsUiEvent( +                        eq(FingerprintManager.UDFPS_UI_READY), eq(TEST_REQUEST_ID),                          eq(testParams.sensorProps.sensorId));                  inOrder.verify(mLatencyTracker).onActionEnd(                          eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE));                  verify(mAlternateTouchProvider, never()).onUiReady();              }          } else { -            verify(mFingerprintManager, never()).onUiReady(anyLong(), anyInt()); +            verify(mFingerprintManager, never()).onUdfpsUiEvent( +                    eq(FingerprintManager.UDFPS_UI_READY), anyLong(), anyInt());              verify(mAlternateTouchProvider, never()).onUiReady();              verify(mLatencyTracker, never()).onActionEnd(                      eq(LatencyTracker.ACTION_UDFPS_ILLUMINATE)); @@ -1327,12 +1340,22 @@ public class UdfpsControllerTest extends SysuiTestCase {          // WHEN ACTION_DOWN is received          when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn(                  processorResultDown); -        MotionEvent downEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0); -        mTouchListenerCaptor.getValue().onTouch(mUdfpsView, downEvent); +        MotionEvent event = MotionEvent.obtain(0, 0, ACTION_DOWN, 0, 0, 0); +        mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event);          mBiometricExecutor.runAllReady(); -        downEvent.recycle(); -        // THEN the touch is pilfered +        // WHEN ACTION_MOVE is received after +        final TouchProcessorResult processorResultUnchanged = +                new TouchProcessorResult.ProcessedTouch( +                        InteractionEvent.UNCHANGED, 1 /* pointerId */, touchData); +        when(mSinglePointerTouchProcessor.processTouch(any(), anyInt(), any())).thenReturn( +                processorResultUnchanged); +        event.setAction(ACTION_MOVE); +        mTouchListenerCaptor.getValue().onTouch(mUdfpsView, event); +        mBiometricExecutor.runAllReady(); +        event.recycle(); + +        // THEN only pilfer once on the initial down          verify(mInputManager).pilferPointers(any());      } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepositoryImplTest.kt new file mode 100644 index 000000000000..0df4fbf86d98 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepositoryImplTest.kt @@ -0,0 +1,124 @@ +/* + * 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.systemui.biometrics.data.repository + +import android.database.ContentObserver +import android.os.Handler +import android.provider.Settings.Secure.FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.util.mockito.captureMany +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.mockito.withArgCaptor +import com.android.systemui.util.settings.SecureSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnit + +private const val USER_ID = 8 + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class FaceSettingsRepositoryImplTest : SysuiTestCase() { + +    @JvmField @Rule var mockitoRule = MockitoJUnit.rule() +    private val testScope = TestScope() + +    @Mock private lateinit var mainHandler: Handler +    @Mock private lateinit var secureSettings: SecureSettings + +    private lateinit var repository: FaceSettingsRepositoryImpl + +    @Before +    fun setup() { +        repository = FaceSettingsRepositoryImpl(mainHandler, secureSettings) +    } + +    @Test +    fun createsOneRepositoryPerUser() = +        testScope.runTest { +            val userRepo = repository.forUser(USER_ID) + +            assertThat(userRepo.userId).isEqualTo(USER_ID) + +            assertThat(repository.forUser(USER_ID)).isSameInstanceAs(userRepo) +            assertThat(repository.forUser(USER_ID + 1)).isNotSameInstanceAs(userRepo) +        } + +    @Test +    fun startsRepoImmediatelyWithAllSettingKeys() = +        testScope.runTest { +            val userRepo = repository.forUser(USER_ID) + +            val keys = +                captureMany<String> { +                    verify(secureSettings) +                        .registerContentObserverForUser(capture(), anyBoolean(), any(), eq(USER_ID)) +                } + +            assertThat(keys).containsExactly(FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION) +        } + +    @Test +    fun forwardsSettingsValues() = runTest { +        val userRepo = repository.forUser(USER_ID) + +        val intAsBooleanSettings = +            listOf( +                FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION to +                    collectLastValue(userRepo.alwaysRequireConfirmationInApps) +            ) + +        for ((setting, accessor) in intAsBooleanSettings) { +            val observer = +                withArgCaptor<ContentObserver> { +                    verify(secureSettings) +                        .registerContentObserverForUser( +                            eq(setting), +                            anyBoolean(), +                            capture(), +                            eq(USER_ID) +                        ) +                } + +            for (value in listOf(true, false)) { +                secureSettings.mockIntSetting(setting, if (value) 1 else 0) +                observer.onChange(false) +                assertThat(accessor()).isEqualTo(value) +            } +        } +    } + +    private fun SecureSettings.mockIntSetting(key: String, value: Int) { +        whenever(getIntForUser(eq(key), anyInt(), eq(USER_ID))).thenReturn(value) +    } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt index 4836af635aed..ec7ce634fd78 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/PromptRepositoryImplTest.kt @@ -1,3 +1,19 @@ +/* + * 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.systemui.biometrics.data.repository  import android.hardware.biometrics.PromptInfo @@ -5,12 +21,16 @@ import androidx.test.filters.SmallTest  import com.android.systemui.SysuiTestCase  import com.android.systemui.biometrics.AuthController  import com.android.systemui.biometrics.shared.model.PromptKind +import com.android.systemui.coroutines.collectLastValue  import com.android.systemui.util.mockito.whenever  import com.android.systemui.util.mockito.withArgCaptor  import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi  import kotlinx.coroutines.flow.toList  import kotlinx.coroutines.launch -import kotlinx.coroutines.test.runBlockingTest +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest  import org.junit.Before  import org.junit.Rule  import org.junit.Test @@ -21,61 +41,109 @@ import org.mockito.Mock  import org.mockito.Mockito.verify  import org.mockito.junit.MockitoJUnit +private const val USER_ID = 9 +private const val CHALLENGE = 90L + +@OptIn(ExperimentalCoroutinesApi::class)  @SmallTest  @RunWith(JUnit4::class)  class PromptRepositoryImplTest : SysuiTestCase() {      @JvmField @Rule var mockitoRule = MockitoJUnit.rule() +    private val testScope = TestScope() +    private val faceSettings = FakeFaceSettingsRepository() +      @Mock private lateinit var authController: AuthController      private lateinit var repository: PromptRepositoryImpl      @Before      fun setup() { -        repository = PromptRepositoryImpl(authController) +        repository = PromptRepositoryImpl(faceSettings, authController)      }      @Test -    fun isShowing() = runBlockingTest { -        whenever(authController.isShowing).thenReturn(true) +    fun isShowing() = +        testScope.runTest { +            whenever(authController.isShowing).thenReturn(true) + +            val values = mutableListOf<Boolean>() +            val job = launch { repository.isShowing.toList(values) } +            runCurrent() -        val values = mutableListOf<Boolean>() -        val job = launch { repository.isShowing.toList(values) } -        assertThat(values).containsExactly(true) +            assertThat(values).containsExactly(true) -        withArgCaptor<AuthController.Callback> { -            verify(authController).addCallback(capture()) +            withArgCaptor<AuthController.Callback> { +                verify(authController).addCallback(capture()) -            value.onBiometricPromptShown() -            assertThat(values).containsExactly(true, true) +                value.onBiometricPromptShown() +                runCurrent() +                assertThat(values).containsExactly(true, true) -            value.onBiometricPromptDismissed() -            assertThat(values).containsExactly(true, true, false).inOrder() +                value.onBiometricPromptDismissed() +                runCurrent() +                assertThat(values).containsExactly(true, true, false).inOrder() -            job.cancel() -            verify(authController).removeCallback(eq(value)) +                job.cancel() +                runCurrent() +                verify(authController).removeCallback(eq(value)) +            }          } -    }      @Test -    fun setsAndUnsetsPrompt() = runBlockingTest { -        val kind = PromptKind.Pin -        val uid = 8 -        val challenge = 90L -        val promptInfo = PromptInfo() +    fun isConfirmationRequired_whenNotForced() = +        testScope.runTest { +            faceSettings.setUserSettings(USER_ID, alwaysRequireConfirmationInApps = false) +            val isConfirmationRequired by collectLastValue(repository.isConfirmationRequired) + +            for (case in listOf(true, false)) { +                repository.setPrompt( +                    PromptInfo().apply { isConfirmationRequested = case }, +                    USER_ID, +                    CHALLENGE, +                    PromptKind.Biometric() +                ) + +                assertThat(isConfirmationRequired).isEqualTo(case) +            } +        } -        repository.setPrompt(promptInfo, uid, challenge, kind) +    @Test +    fun isConfirmationRequired_whenForced() = +        testScope.runTest { +            faceSettings.setUserSettings(USER_ID, alwaysRequireConfirmationInApps = true) +            val isConfirmationRequired by collectLastValue(repository.isConfirmationRequired) + +            for (case in listOf(true, false)) { +                repository.setPrompt( +                    PromptInfo().apply { isConfirmationRequested = case }, +                    USER_ID, +                    CHALLENGE, +                    PromptKind.Biometric() +                ) + +                assertThat(isConfirmationRequired).isTrue() +            } +        } -        assertThat(repository.kind.value).isEqualTo(kind) -        assertThat(repository.userId.value).isEqualTo(uid) -        assertThat(repository.challenge.value).isEqualTo(challenge) -        assertThat(repository.promptInfo.value).isSameInstanceAs(promptInfo) +    @Test +    fun setsAndUnsetsPrompt() = +        testScope.runTest { +            val kind = PromptKind.Pin +            val promptInfo = PromptInfo() -        repository.unsetPrompt() +            repository.setPrompt(promptInfo, USER_ID, CHALLENGE, kind) -        assertThat(repository.promptInfo.value).isNull() -        assertThat(repository.userId.value).isNull() -        assertThat(repository.challenge.value).isNull() -    } +            assertThat(repository.kind.value).isEqualTo(kind) +            assertThat(repository.userId.value).isEqualTo(USER_ID) +            assertThat(repository.challenge.value).isEqualTo(CHALLENGE) +            assertThat(repository.promptInfo.value).isSameInstanceAs(promptInfo) + +            repository.unsetPrompt() + +            assertThat(repository.promptInfo.value).isNull() +            assertThat(repository.userId.value).isNull() +            assertThat(repository.challenge.value).isNull() +        }  } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt index a62ea3b77fc2..81cbaeab2a32 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt @@ -106,17 +106,11 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() {          val currentPrompt by collectLastValue(interactor.prompt)          val credentialKind by collectLastValue(interactor.credentialKind)          val isCredentialAllowed by collectLastValue(interactor.isCredentialAllowed) -        val isExplicitConfirmationRequired by collectLastValue(interactor.isConfirmationRequested) +        val isExplicitConfirmationRequired by collectLastValue(interactor.isConfirmationRequired)          assertThat(currentPrompt).isNull() -        interactor.useBiometricsForAuthentication( -            info, -            confirmationRequired, -            USER_ID, -            CHALLENGE, -            modalities -        ) +        interactor.useBiometricsForAuthentication(info, USER_ID, CHALLENGE, modalities)          assertThat(currentPrompt).isNotNull()          assertThat(currentPrompt?.title).isEqualTo(TITLE) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthStateTest.kt index 689bb0023675..278a43ea1bf1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthStateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptAuthStateTest.kt @@ -18,7 +18,7 @@ package com.android.systemui.biometrics.ui.viewmodel  import androidx.test.filters.SmallTest  import com.android.systemui.SysuiTestCase -import com.android.systemui.biometrics.domain.model.BiometricModality +import com.android.systemui.biometrics.shared.model.BiometricModality  import com.google.common.truth.Truth.assertThat  import org.junit.Test  import org.junit.runner.RunWith @@ -33,6 +33,7 @@ class PromptAuthStateTest : SysuiTestCase() {          with(PromptAuthState(isAuthenticated = false)) {              assertThat(isNotAuthenticated).isTrue()              assertThat(isAuthenticatedAndConfirmed).isFalse() +            assertThat(isAuthenticatedAndExplicitlyConfirmed).isFalse()              assertThat(isAuthenticatedByFace).isFalse()              assertThat(isAuthenticatedByFingerprint).isFalse()          } @@ -43,6 +44,7 @@ class PromptAuthStateTest : SysuiTestCase() {          with(PromptAuthState(isAuthenticated = true)) {              assertThat(isNotAuthenticated).isFalse()              assertThat(isAuthenticatedAndConfirmed).isTrue() +            assertThat(isAuthenticatedAndExplicitlyConfirmed).isFalse()              assertThat(isAuthenticatedByFace).isFalse()              assertThat(isAuthenticatedByFingerprint).isFalse()          } @@ -50,10 +52,12 @@ class PromptAuthStateTest : SysuiTestCase() {          with(PromptAuthState(isAuthenticated = true, needsUserConfirmation = true)) {              assertThat(isNotAuthenticated).isFalse()              assertThat(isAuthenticatedAndConfirmed).isFalse() +            assertThat(isAuthenticatedAndExplicitlyConfirmed).isFalse()              assertThat(isAuthenticatedByFace).isFalse()              assertThat(isAuthenticatedByFingerprint).isFalse() -            assertThat(asConfirmed().isAuthenticatedAndConfirmed).isTrue() +            assertThat(asExplicitlyConfirmed().isAuthenticatedAndConfirmed).isTrue() +            assertThat(asExplicitlyConfirmed().isAuthenticatedAndExplicitlyConfirmed).isTrue()          }      } @@ -64,6 +68,7 @@ class PromptAuthStateTest : SysuiTestCase() {          ) {              assertThat(isNotAuthenticated).isFalse()              assertThat(isAuthenticatedAndConfirmed).isTrue() +            assertThat(isAuthenticatedAndExplicitlyConfirmed).isFalse()              assertThat(isAuthenticatedByFace).isTrue()              assertThat(isAuthenticatedByFingerprint).isFalse()          } @@ -79,6 +84,7 @@ class PromptAuthStateTest : SysuiTestCase() {          ) {              assertThat(isNotAuthenticated).isFalse()              assertThat(isAuthenticatedAndConfirmed).isTrue() +            assertThat(isAuthenticatedAndExplicitlyConfirmed).isFalse()              assertThat(isAuthenticatedByFace).isFalse()              assertThat(isAuthenticatedByFingerprint).isTrue()          } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index 3ba6004e4532..91140a9b0fc4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -27,11 +27,14 @@ import com.android.systemui.biometrics.data.repository.FakePromptRepository  import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor  import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractorImpl  import com.android.systemui.biometrics.domain.model.BiometricModalities -import com.android.systemui.biometrics.domain.model.BiometricModality  import com.android.systemui.biometrics.extractAuthenticatorTypes  import com.android.systemui.biometrics.faceSensorPropertiesInternal  import com.android.systemui.biometrics.fingerprintSensorPropertiesInternal +import com.android.systemui.biometrics.shared.model.BiometricModality  import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues +import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.util.mockito.any  import com.google.common.truth.Truth.assertThat  import kotlinx.coroutines.ExperimentalCoroutinesApi  import kotlinx.coroutines.flow.first @@ -45,6 +48,9 @@ import org.junit.Test  import org.junit.runner.RunWith  import org.junit.runners.Parameterized  import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify  import org.mockito.junit.MockitoJUnit  private const val USER_ID = 4 @@ -58,6 +64,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa      @JvmField @Rule var mockitoRule = MockitoJUnit.rule()      @Mock private lateinit var lockPatternUtils: LockPatternUtils +    @Mock private lateinit var vibrator: VibratorHelper      private val testScope = TestScope()      private val promptRepository = FakePromptRepository() @@ -70,11 +77,11 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa          selector = PromptSelectorInteractorImpl(promptRepository, lockPatternUtils)          selector.resetPrompt() -        viewModel = PromptViewModel(selector) +        viewModel = PromptViewModel(selector, vibrator)      }      @Test -    fun `start idle and show authenticating`() = +    fun start_idle_and_show_authenticating() =          runGenericTest(doNotStart = true) {              val expectedSize =                  if (testCase.shouldStartAsImplicitFlow) PromptSize.SMALL else PromptSize.MEDIUM @@ -107,7 +114,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa          }      @Test -    fun `shows authenticated - no errors`() = runGenericTest { +    fun shows_authenticated_with_no_errors() = runGenericTest {          // this case can't happen until fingerprint is started          // trigger it now since no error has occurred in this test          val forceError = testCase.isCoex && testCase.authenticatedByFingerprint @@ -124,6 +131,24 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa          )      } +    @Test +    fun play_haptic_on_confirm_when_confirmation_required_otherwise_on_authenticated() = +        runGenericTest { +            val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false) + +            viewModel.showAuthenticated(testCase.authenticatedModality, 1_000L) + +            verify(vibrator, if (expectConfirmation) never() else times(1)) +                .vibrateAuthSuccess(any()) + +            if (expectConfirmation) { +                viewModel.confirmAuthenticated() +            } + +            verify(vibrator).vibrateAuthSuccess(any()) +            verify(vibrator, never()).vibrateAuthError(any()) +        } +      private suspend fun TestScope.showAuthenticated(          authenticatedModality: BiometricModality,          expectConfirmation: Boolean, @@ -172,7 +197,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa      }      @Test -    fun `shows temporary errors`() = runGenericTest { +    fun shows_temporary_errors() = runGenericTest {          val checkAtEnd = suspend { assertButtonsVisible(negative = true) }          showTemporaryErrors(restart = false) { checkAtEnd() } @@ -180,6 +205,32 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa          showTemporaryErrors(restart = true) { checkAtEnd() }      } +    @Test +    fun plays_haptic_on_errors() = runGenericTest { +        viewModel.showTemporaryError( +            "so sad", +            messageAfterError = "", +            authenticateAfterError = false, +            hapticFeedback = true, +        ) + +        verify(vibrator).vibrateAuthError(any()) +        verify(vibrator, never()).vibrateAuthSuccess(any()) +    } + +    @Test +    fun plays_haptic_on_errors_unless_skipped() = runGenericTest { +        viewModel.showTemporaryError( +            "still sad", +            messageAfterError = "", +            authenticateAfterError = false, +            hapticFeedback = false, +        ) + +        verify(vibrator, never()).vibrateAuthError(any()) +        verify(vibrator, never()).vibrateAuthSuccess(any()) +    } +      private suspend fun TestScope.showTemporaryErrors(          restart: Boolean,          helpAfterError: String = "", @@ -233,7 +284,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa      }      @Test -    fun `no errors or temporary help after authenticated`() = runGenericTest { +    fun no_errors_or_temporary_help_after_authenticated() = runGenericTest {          val authenticating by collectLastValue(viewModel.isAuthenticating)          val authenticated by collectLastValue(viewModel.isAuthenticated)          val message by collectLastValue(viewModel.message) @@ -249,7 +300,13 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa              assertThat(canTryAgain).isFalse()          } -        val errorJob = launch { viewModel.showTemporaryError("error") } +        val errorJob = launch { +            viewModel.showTemporaryError( +                "error", +                messageAfterError = "", +                authenticateAfterError = false, +            ) +        }          verifyNoError()          errorJob.join()          verifyNoError() @@ -268,16 +325,70 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa          assertThat(messageIsShowing).isTrue()      } -    //    @Test -    fun `suppress errors`() = runGenericTest { -        val errorMessage = "woot" -        val message by collectLastValue(viewModel.message) +    @Test +    fun suppress_temporary_error() = runGenericTest { +        val messages by collectValues(viewModel.message) + +        for (error in listOf("never", "see", "me")) { +            launch { +                viewModel.showTemporaryError( +                    error, +                    messageAfterError = "or me", +                    authenticateAfterError = false, +                    suppressIf = { _ -> true }, +                ) +            } +        } + +        testScheduler.advanceUntilIdle() +        assertThat(messages).containsExactly(PromptMessage.Empty) +    } + +    @Test +    fun suppress_temporary_error_when_already_showing_when_requested() = +        suppress_temporary_error_when_already_showing(suppress = true) -        val errorJob = launch { viewModel.showTemporaryError(errorMessage) } +    @Test +    fun do_not_suppress_temporary_error_when_already_showing_when_not_requested() = +        suppress_temporary_error_when_already_showing(suppress = false) + +    private fun suppress_temporary_error_when_already_showing(suppress: Boolean) = runGenericTest { +        val errors = listOf("woot", "oh yeah", "nope") +        val afterSuffix = "(after)" +        val expectedErrorMessage = if (suppress) errors.first() else errors.last() +        val messages by collectValues(viewModel.message) + +        for (error in errors) { +            launch { +                viewModel.showTemporaryError( +                    error, +                    messageAfterError = "$error $afterSuffix", +                    authenticateAfterError = false, +                    suppressIf = { currentMessage -> suppress && currentMessage.isError }, +                ) +            } +        } + +        testScheduler.runCurrent() +        assertThat(messages) +            .containsExactly( +                PromptMessage.Empty, +                PromptMessage.Error(expectedErrorMessage), +            ) +            .inOrder() + +        testScheduler.advanceUntilIdle() +        assertThat(messages) +            .containsExactly( +                PromptMessage.Empty, +                PromptMessage.Error(expectedErrorMessage), +                PromptMessage.Help("$expectedErrorMessage $afterSuffix"), +            ) +            .inOrder()      }      @Test -    fun `authenticated at most once`() = runGenericTest { +    fun authenticated_at_most_once() = runGenericTest {          val authenticating by collectLastValue(viewModel.isAuthenticating)          val authenticated by collectLastValue(viewModel.isAuthenticated) @@ -293,7 +404,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa      }      @Test -    fun `authenticating cannot restart after authenticated`() = runGenericTest { +    fun authenticating_cannot_restart_after_authenticated() = runGenericTest {          val authenticating by collectLastValue(viewModel.isAuthenticating)          val authenticated by collectLastValue(viewModel.isAuthenticated) @@ -309,7 +420,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa      }      @Test -    fun `confirm authentication`() = runGenericTest { +    fun confirm_authentication() = runGenericTest {          val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)          viewModel.showAuthenticated(testCase.authenticatedModality, 0) @@ -341,7 +452,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa      }      @Test -    fun `cannot confirm unless authenticated`() = runGenericTest { +    fun cannot_confirm_unless_authenticated() = runGenericTest {          val authenticating by collectLastValue(viewModel.isAuthenticating)          val authenticated by collectLastValue(viewModel.isAuthenticated) @@ -360,7 +471,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa      }      @Test -    fun `shows help - before authenticated`() = runGenericTest { +    fun shows_help_before_authenticated() = runGenericTest {          val helpMessage = "please help yourself to some cookies"          val message by collectLastValue(viewModel.message)          val messageVisible by collectLastValue(viewModel.isIndicatorMessageVisible) @@ -379,7 +490,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa      }      @Test -    fun `shows help - after authenticated`() = runGenericTest { +    fun shows_help_after_authenticated() = runGenericTest {          val expectConfirmation = testCase.expectConfirmation(atLeastOneFailure = false)          val helpMessage = "more cookies please"          val authenticating by collectLastValue(viewModel.isAuthenticating) @@ -409,7 +520,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa      }      @Test -    fun `retries after failure`() = runGenericTest { +    fun retries_after_failure() = runGenericTest {          val errorMessage = "bad"          val helpMessage = "again?"          val expectTryAgainButton = testCase.isFaceOnly @@ -455,7 +566,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa      }      @Test -    fun `switch to credential fallback`() = runGenericTest { +    fun switch_to_credential_fallback() = runGenericTest {          val size by collectLastValue(viewModel.size)          // TODO(b/251476085): remove Spaghetti, migrate logic, and update this test @@ -631,7 +742,6 @@ private fun PromptSelectorInteractor.initializePrompt(          }      useBiometricsForAuthentication(          info, -        requireConfirmation,          USER_ID,          CHALLENGE,          BiometricModalities(fingerprintProperties = fingerprint, faceProperties = face), diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/ShutdownUiTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/ShutdownUiTest.java new file mode 100644 index 000000000000..9d9b263c5df5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/ShutdownUiTest.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.globalactions; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNull; + +import android.os.PowerManager; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.internal.R; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.statusbar.BlurUtils; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; + + +@SmallTest +@RunWith(AndroidTestingRunner.class) +public class ShutdownUiTest extends SysuiTestCase { + +    ShutdownUi mShutdownUi; +    @Mock +    BlurUtils mBlurUtils; + +    @Before +    public void setUp() throws Exception { +        mShutdownUi = new ShutdownUi(getContext(), mBlurUtils); +    } + +    @Test +    public void getRebootMessage_update() { +        int messageId = mShutdownUi.getRebootMessage(true, PowerManager.REBOOT_RECOVERY_UPDATE); +        assertEquals(messageId, R.string.reboot_to_update_reboot); +    } + +    @Test +    public void getRebootMessage_rebootDefault() { +        int messageId = mShutdownUi.getRebootMessage(true, "anything-else"); +        assertEquals(messageId, R.string.reboot_to_reset_message); +    } + +    @Test +    public void getRebootMessage_shutdown() { +        int messageId = mShutdownUi.getRebootMessage(false, "anything-else"); +        assertEquals(messageId, R.string.shutdown_progress); +    } + +    @Test +    public void getReasonMessage_update() { +        String message = mShutdownUi.getReasonMessage(PowerManager.REBOOT_RECOVERY_UPDATE); +        assertEquals(message, mContext.getString(R.string.reboot_to_update_title)); +    } + +    @Test +    public void getReasonMessage_rebootDefault() { +        String message = mShutdownUi.getReasonMessage(PowerManager.REBOOT_RECOVERY); +        assertEquals(message, mContext.getString(R.string.reboot_to_reset_title)); +    } + +    @Test +    public void getRebootMessage_defaultToNone() { +        String message = mShutdownUi.getReasonMessage("anything-else"); +        assertNull(message); +    } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFaceSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFaceSettingsRepository.kt new file mode 100644 index 000000000000..af2706e6b287 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFaceSettingsRepository.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics.data.repository + +import kotlinx.coroutines.flow.flowOf + +/** Fake settings for tests. */ +class FakeFaceSettingsRepository : FaceSettingsRepository { + +    private val userRepositories = mutableMapOf<Int, FaceUserSettingsRepository>() + +    /** Add fixed settings for a user. */ +    fun setUserSettings(userId: Int, alwaysRequireConfirmationInApps: Boolean = false) { +        userRepositories[userId] = +            object : FaceUserSettingsRepository { +                override val userId = userId +                override val alwaysRequireConfirmationInApps = +                    flowOf(alwaysRequireConfirmationInApps) +            } +    } + +    override fun forUser(id: Int?) = userRepositories[id] ?: FaceUserSettingsRepositoryImpl.Empty +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt index d270700aa856..42ec8fed0127 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakePromptRepository.kt @@ -31,13 +31,20 @@ class FakePromptRepository : PromptRepository {          userId: Int,          gatekeeperChallenge: Long?,          kind: PromptKind, -        requireConfirmation: Boolean, +    ) = setPrompt(promptInfo, userId, gatekeeperChallenge, kind, forceConfirmation = false) + +    fun setPrompt( +        promptInfo: PromptInfo, +        userId: Int, +        gatekeeperChallenge: Long?, +        kind: PromptKind, +        forceConfirmation: Boolean = false,      ) {          _promptInfo.value = promptInfo          _userId.value = userId          _challenge.value = gatekeeperChallenge          _kind.value = kind -        _isConfirmationRequired.value = requireConfirmation +        _isConfirmationRequired.value = promptInfo.isConfirmationRequested || forceConfirmation      }      override fun unsetPrompt() { diff --git a/services/core/java/com/android/server/app/GameManagerService.java b/services/core/java/com/android/server/app/GameManagerService.java index e4a5a3e0ed00..ca15dd79adbc 100644 --- a/services/core/java/com/android/server/app/GameManagerService.java +++ b/services/core/java/com/android/server/app/GameManagerService.java @@ -2166,7 +2166,7 @@ public final class GameManagerService extends IGameManagerService.Stub {          @Override          public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) {              synchronized (mUidObserverLock) { -                if (ActivityManager.isProcStateBackground(procState)) { +                if (procState != ActivityManager.PROCESS_STATE_TOP) {                      disableGameMode(uid);                      return;                  } diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java index 8ef2a1bd26c2..cb5e7f1be571 100644 --- a/services/core/java/com/android/server/biometrics/AuthSession.java +++ b/services/core/java/com/android/server/biometrics/AuthSession.java @@ -21,7 +21,10 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRIN  import static android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE;  import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR;  import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_VENDOR_BASE; +import static android.hardware.biometrics.BiometricManager.Authenticators.BIOMETRIC_CONVENIENCE; +import static com.android.server.biometrics.BiometricSensor.STATE_CANCELING; +import static com.android.server.biometrics.BiometricSensor.STATE_UNKNOWN;  import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTHENTICATED_PENDING_SYSUI;  import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_CALLED;  import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_IDLE; @@ -439,6 +442,13 @@ public final class AuthSession implements IBinder.DeathRecipient {              return false;          } +        final boolean errorLockout = error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT +                || error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT; +        if (errorLockout) { +            cancelAllSensors(sensor -> Utils.isAtLeastStrength(sensorIdToStrength(sensorId), +                    sensor.getCurrentStrength())); +        } +          mErrorEscrow = error;          mVendorCodeEscrow = vendorCode; @@ -477,8 +487,6 @@ public final class AuthSession implements IBinder.DeathRecipient {              case STATE_AUTH_STARTED:              case STATE_AUTH_STARTED_UI_SHOWING: { -                final boolean errorLockout = error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT -                        || error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT;                  if (isAllowDeviceCredential() && errorLockout) {                      // SystemUI handles transition from biometric to device credential.                      mState = STATE_SHOWING_DEVICE_CREDENTIAL; @@ -675,7 +683,9 @@ public final class AuthSession implements IBinder.DeathRecipient {      }      private boolean pauseSensorIfSupported(int sensorId) { -        if (sensorIdToModality(sensorId) == TYPE_FACE) { +        boolean isSensorCancelling = sensorIdToState(sensorId) == STATE_CANCELING; +        // If the sensor is locked out, canceling sensors operation is handled in onErrorReceived() +        if (sensorIdToModality(sensorId) == TYPE_FACE && !isSensorCancelling) {              cancelAllSensors(sensor -> sensor.id == sensorId);              return true;          } @@ -948,6 +958,27 @@ public final class AuthSession implements IBinder.DeathRecipient {          return TYPE_NONE;      } +    private @BiometricSensor.SensorState int sensorIdToState(int sensorId) { +        for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) { +            if (sensorId == sensor.id) { +                return sensor.getSensorState(); +            } +        } +        Slog.e(TAG, "Unknown sensor: " + sensorId); +        return STATE_UNKNOWN; +    } + +    @BiometricManager.Authenticators.Types +    private int sensorIdToStrength(int sensorId) { +        for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) { +            if (sensorId == sensor.id) { +                return sensor.getCurrentStrength(); +            } +        } +        Slog.e(TAG, "Unknown sensor: " + sensorId); +        return BIOMETRIC_CONVENIENCE; +    } +      private String getAcquiredMessageForSensor(int sensorId, int acquiredInfo, int vendorCode) {          final @Modality int modality = sensorIdToModality(sensorId);          switch (modality) { diff --git a/services/core/java/com/android/server/biometrics/BiometricSensorPrivacy.java b/services/core/java/com/android/server/biometrics/BiometricSensorPrivacy.java new file mode 100644 index 000000000000..6727fbcdec66 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/BiometricSensorPrivacy.java @@ -0,0 +1,25 @@ +/* + * 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.server.biometrics; + +/** + * Interface for biometric operations to get camera privacy state. + */ +public interface BiometricSensorPrivacy { +    /* Returns true if privacy is enabled and camera access is disabled. */ +    boolean isCameraPrivacyEnabled(); +} diff --git a/services/core/java/com/android/server/biometrics/BiometricSensorPrivacyImpl.java b/services/core/java/com/android/server/biometrics/BiometricSensorPrivacyImpl.java new file mode 100644 index 000000000000..b6701da1d348 --- /dev/null +++ b/services/core/java/com/android/server/biometrics/BiometricSensorPrivacyImpl.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + *      http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.biometrics; + +import static android.hardware.SensorPrivacyManager.Sensors.CAMERA; + +import android.annotation.Nullable; +import android.hardware.SensorPrivacyManager; + +public class BiometricSensorPrivacyImpl implements +        BiometricSensorPrivacy { +    private final SensorPrivacyManager mSensorPrivacyManager; + +    public BiometricSensorPrivacyImpl(@Nullable SensorPrivacyManager sensorPrivacyManager) { +        mSensorPrivacyManager = sensorPrivacyManager; +    } + +    @Override +    public boolean isCameraPrivacyEnabled() { +        return mSensorPrivacyManager != null && mSensorPrivacyManager +                .isSensorPrivacyEnabled(SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE, CAMERA); +    } +} diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index 0942d8527565..1fa97a3cb97f 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -33,6 +33,7 @@ import android.content.Context;  import android.content.pm.PackageManager;  import android.content.pm.UserInfo;  import android.database.ContentObserver; +import android.hardware.SensorPrivacyManager;  import android.hardware.biometrics.BiometricAuthenticator;  import android.hardware.biometrics.BiometricConstants;  import android.hardware.biometrics.BiometricPrompt; @@ -124,6 +125,8 @@ public class BiometricService extends SystemService {      AuthSession mAuthSession;      private final Handler mHandler = new Handler(Looper.getMainLooper()); +    private final BiometricSensorPrivacy mBiometricSensorPrivacy; +      /**       * Tracks authenticatorId invalidation. For more details, see       * {@link com.android.server.biometrics.sensors.InvalidationRequesterClient}. @@ -933,7 +936,7 @@ public class BiometricService extends SystemService {          return PreAuthInfo.create(mTrustManager, mDevicePolicyManager, mSettingObserver, mSensors,                  userId, promptInfo, opPackageName, false /* checkDevicePolicyManager */, -                getContext()); +                getContext(), mBiometricSensorPrivacy);      }      /** @@ -1026,6 +1029,11 @@ public class BiometricService extends SystemService {          public UserManager getUserManager(Context context) {              return context.getSystemService(UserManager.class);          } + +        public BiometricSensorPrivacy getBiometricSensorPrivacy(Context context) { +            return new BiometricSensorPrivacyImpl(context.getSystemService( +                    SensorPrivacyManager.class)); +        }      }      /** @@ -1054,6 +1062,7 @@ public class BiometricService extends SystemService {          mRequestCounter = mInjector.getRequestGenerator();          mBiometricContext = injector.getBiometricContext(context);          mUserManager = injector.getUserManager(context); +        mBiometricSensorPrivacy = injector.getBiometricSensorPrivacy(context);          try {              injector.getActivityManagerService().registerUserSwitchObserver( @@ -1290,7 +1299,7 @@ public class BiometricService extends SystemService {                  final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager,                          mDevicePolicyManager, mSettingObserver, mSensors, userId, promptInfo,                          opPackageName, promptInfo.isDisallowBiometricsIfPolicyExists(), -                        getContext()); +                        getContext(), mBiometricSensorPrivacy);                  final Pair<Integer, Integer> preAuthStatus = preAuthInfo.getPreAuthenticateStatus(); @@ -1300,9 +1309,7 @@ public class BiometricService extends SystemService {                          + promptInfo.isIgnoreEnrollmentState());                  // BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED is added so that BiometricPrompt can                  // be shown for this case. -                if (preAuthStatus.second == BiometricConstants.BIOMETRIC_SUCCESS -                        || preAuthStatus.second -                        == BiometricConstants.BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED) { +                if (preAuthStatus.second == BiometricConstants.BIOMETRIC_SUCCESS) {                      // If BIOMETRIC_WEAK or BIOMETRIC_STRONG are allowed, but not enrolled, but                      // CREDENTIAL is requested and available, set the bundle to only request                      // CREDENTIAL. diff --git a/services/core/java/com/android/server/biometrics/PreAuthInfo.java b/services/core/java/com/android/server/biometrics/PreAuthInfo.java index 3813fd1971a6..e6f25cb88006 100644 --- a/services/core/java/com/android/server/biometrics/PreAuthInfo.java +++ b/services/core/java/com/android/server/biometrics/PreAuthInfo.java @@ -27,7 +27,6 @@ import android.annotation.NonNull;  import android.app.admin.DevicePolicyManager;  import android.app.trust.ITrustManager;  import android.content.Context; -import android.hardware.SensorPrivacyManager;  import android.hardware.biometrics.BiometricAuthenticator;  import android.hardware.biometrics.BiometricManager;  import android.hardware.biometrics.PromptInfo; @@ -73,13 +72,16 @@ class PreAuthInfo {      final Context context;      private final boolean mBiometricRequested;      private final int mBiometricStrengthRequested; +    private final BiometricSensorPrivacy mBiometricSensorPrivacy; +      private PreAuthInfo(boolean biometricRequested, int biometricStrengthRequested,              boolean credentialRequested, List<BiometricSensor> eligibleSensors,              List<Pair<BiometricSensor, Integer>> ineligibleSensors, boolean credentialAvailable,              boolean confirmationRequested, boolean ignoreEnrollmentState, int userId, -            Context context) { +            Context context, BiometricSensorPrivacy biometricSensorPrivacy) {          mBiometricRequested = biometricRequested;          mBiometricStrengthRequested = biometricStrengthRequested; +        mBiometricSensorPrivacy = biometricSensorPrivacy;          this.credentialRequested = credentialRequested;          this.eligibleSensors = eligibleSensors; @@ -96,7 +98,8 @@ class PreAuthInfo {              BiometricService.SettingObserver settingObserver,              List<BiometricSensor> sensors,              int userId, PromptInfo promptInfo, String opPackageName, -            boolean checkDevicePolicyManager, Context context) +            boolean checkDevicePolicyManager, Context context, +            BiometricSensorPrivacy biometricSensorPrivacy)              throws RemoteException {          final boolean confirmationRequested = promptInfo.isConfirmationRequested(); @@ -124,7 +127,7 @@ class PreAuthInfo {                          checkDevicePolicyManager, requestedStrength,                          promptInfo.getAllowedSensorIds(),                          promptInfo.isIgnoreEnrollmentState(), -                        context); +                        biometricSensorPrivacy);                  Slog.d(TAG, "Package: " + opPackageName                          + " Sensor ID: " + sensor.id @@ -138,7 +141,7 @@ class PreAuthInfo {                  //                  // Note: if only a certain sensor is required and the privacy is enabled,                  // canAuthenticate() will return false. -                if (status == AUTHENTICATOR_OK || status == BIOMETRIC_SENSOR_PRIVACY_ENABLED) { +                if (status == AUTHENTICATOR_OK) {                      eligibleSensors.add(sensor);                  } else {                      ineligibleSensors.add(new Pair<>(sensor, status)); @@ -148,7 +151,7 @@ class PreAuthInfo {          return new PreAuthInfo(biometricRequested, requestedStrength, credentialRequested,                  eligibleSensors, ineligibleSensors, credentialAvailable, confirmationRequested, -                promptInfo.isIgnoreEnrollmentState(), userId, context); +                promptInfo.isIgnoreEnrollmentState(), userId, context, biometricSensorPrivacy);      }      /** @@ -165,7 +168,7 @@ class PreAuthInfo {              BiometricSensor sensor, int userId, String opPackageName,              boolean checkDevicePolicyManager, int requestedStrength,              @NonNull List<Integer> requestedSensorIds, -            boolean ignoreEnrollmentState, Context context) { +            boolean ignoreEnrollmentState, BiometricSensorPrivacy biometricSensorPrivacy) {          if (!requestedSensorIds.isEmpty() && !requestedSensorIds.contains(sensor.id)) {              return BIOMETRIC_NO_HARDWARE; @@ -191,12 +194,10 @@ class PreAuthInfo {                      && !ignoreEnrollmentState) {                  return BIOMETRIC_NOT_ENROLLED;              } -            final SensorPrivacyManager sensorPrivacyManager = context -                    .getSystemService(SensorPrivacyManager.class); -            if (sensorPrivacyManager != null && sensor.modality == TYPE_FACE) { -                if (sensorPrivacyManager -                        .isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, userId)) { +            if (biometricSensorPrivacy != null && sensor.modality == TYPE_FACE) { +                if (biometricSensorPrivacy.isCameraPrivacyEnabled()) { +                    //Camera privacy is enabled as the access is disabled                      return BIOMETRIC_SENSOR_PRIVACY_ENABLED;                  }              } @@ -266,17 +267,30 @@ class PreAuthInfo {      }      private Pair<BiometricSensor, Integer> calculateErrorByPriority() { -        // If the caller requested STRONG, and the device contains both STRONG and non-STRONG -        // sensors, prioritize BIOMETRIC_NOT_ENROLLED over the weak sensor's -        // BIOMETRIC_INSUFFICIENT_STRENGTH error. Pretty sure we can always prioritize -        // BIOMETRIC_NOT_ENROLLED over any other error (unless of course its calculation is -        // wrong, in which case we should fix that instead). +        Pair<BiometricSensor, Integer> sensorNotEnrolled = null; +        Pair<BiometricSensor, Integer> sensorLockout = null;          for (Pair<BiometricSensor, Integer> pair : ineligibleSensors) { +            int status = pair.second; +            if (status == BIOMETRIC_LOCKOUT_TIMED || status == BIOMETRIC_LOCKOUT_PERMANENT) { +                sensorLockout = pair; +            }              if (pair.second == BIOMETRIC_NOT_ENROLLED) { -                return pair; +                sensorNotEnrolled = pair;              }          } +        // If there is a sensor locked out, prioritize lockout over other sensor's error. +        // See b/286923477. +        if (sensorLockout != null) { +            return sensorLockout; +        } + +        // If the caller requested STRONG, and the device contains both STRONG and non-STRONG +        // sensors, prioritize BIOMETRIC_NOT_ENROLLED over the weak sensor's +        // BIOMETRIC_INSUFFICIENT_STRENGTH error. +        if (sensorNotEnrolled != null) { +            return sensorNotEnrolled; +        }          return ineligibleSensors.get(0);      } @@ -292,13 +306,9 @@ class PreAuthInfo {          @AuthenticatorStatus final int status;          @BiometricAuthenticator.Modality int modality = TYPE_NONE; -        final SensorPrivacyManager sensorPrivacyManager = context -                .getSystemService(SensorPrivacyManager.class); -          boolean cameraPrivacyEnabled = false; -        if (sensorPrivacyManager != null) { -            cameraPrivacyEnabled = sensorPrivacyManager -                    .isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, userId); +        if (mBiometricSensorPrivacy != null) { +            cameraPrivacyEnabled = mBiometricSensorPrivacy.isCameraPrivacyEnabled();          }          if (mBiometricRequested && credentialRequested) { @@ -315,7 +325,7 @@ class PreAuthInfo {                      // and the face sensor privacy is enabled then return                      // BIOMETRIC_SENSOR_PRIVACY_ENABLED.                      // -                    // Note: This sensor will still be eligible for calls to authenticate. +                    // Note: This sensor will not be eligible for calls to authenticate.                      status = BIOMETRIC_SENSOR_PRIVACY_ENABLED;                  } else {                      status = AUTHENTICATOR_OK; @@ -340,7 +350,7 @@ class PreAuthInfo {                      // If the only modality requested is face and the privacy is enabled                      // then return BIOMETRIC_SENSOR_PRIVACY_ENABLED.                      // -                    // Note: This sensor will still be eligible for calls to authenticate. +                    // Note: This sensor will not be eligible for calls to authenticate.                      status = BIOMETRIC_SENSOR_PRIVACY_ENABLED;                  } else {                      status = AUTHENTICATOR_OK; diff --git a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java index 46c77e8a82f2..aa6a0f1bb55f 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java @@ -210,4 +210,8 @@ public abstract class AcquisitionClient<T> extends HalClientMonitor<T> implement      public boolean isInterruptable() {          return true;      } + +    public boolean isAlreadyCancelled() { +        return mAlreadyCancelled; +    }  } diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java index 05ca6e4554fb..6ac163121d8c 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java @@ -266,8 +266,12 @@ public abstract class AuthenticationClient<T, O extends AuthenticateOptions>              }          } else {              if (isBackgroundAuth) { -                Slog.e(TAG, "cancelling due to background auth"); -                cancel(); +                Slog.e(TAG, "Sending cancel to client(Due to background auth)"); +                if (mTaskStackListener != null) { +                    mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener); +                } +                sendCancelOnly(getListener()); +                mCallback.onClientFinished(this, false);              } else {                  // Allow system-defined limit of number of attempts before giving up                  if (mShouldUseLockoutTracker) { diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java index 7fa4d6ce4d49..78c38089e803 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java @@ -268,6 +268,14 @@ public class BiometricScheduler {              return;          } +        if (mCurrentOperation.isAcquisitionOperation()) { +            AcquisitionClient client = (AcquisitionClient) mCurrentOperation.getClientMonitor(); +            if (client.isAlreadyCancelled()) { +                mCurrentOperation.cancel(mHandler, mInternalCallback); +                return; +            } +        } +          if (mGestureAvailabilityDispatcher != null && mCurrentOperation.isAcquisitionOperation()) {              mGestureAvailabilityDispatcher.markSensorActive(                      mCurrentOperation.getSensorId(), true /* active */); diff --git a/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java b/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java index 2e1a363bcc68..1a682a9ffefa 100644 --- a/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java +++ b/services/core/java/com/android/server/biometrics/sensors/ClientMonitorCallbackConverter.java @@ -170,6 +170,12 @@ public class ClientMonitorCallbackConverter {          }      } +    public void onUdfpsOverlayShown() throws RemoteException { +        if (mFingerprintServiceReceiver != null) { +            mFingerprintServiceReceiver.onUdfpsOverlayShown(); +        } +    } +      // Face-specific callbacks for FaceManager only      /** diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java index 50d375c56f4a..35fc43ae5f74 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java @@ -43,7 +43,6 @@ import com.android.server.biometrics.log.BiometricLogger;  import com.android.server.biometrics.log.OperationContextExt;  import com.android.server.biometrics.sensors.AuthSessionCoordinator;  import com.android.server.biometrics.sensors.AuthenticationClient; -import com.android.server.biometrics.sensors.BiometricNotificationUtils;  import com.android.server.biometrics.sensors.ClientMonitorCallback;  import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter;  import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback; @@ -242,9 +241,6 @@ class FaceAuthenticationClient extends AuthenticationClient<AidlSession, FaceAut                  vendorCode,                  getTargetUserId())); -        if (error == BiometricConstants.BIOMETRIC_ERROR_RE_ENROLL) { -            BiometricNotificationUtils.showReEnrollmentNotification(getContext()); -        }          super.onError(error, vendorCode);      } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java index ece35c522ec7..3d6a156d6022 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java @@ -929,17 +929,19 @@ public class FingerprintService extends SystemService {          @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)          @Override -        public void onUiReady(long requestId, int sensorId) { -            super.onUiReady_enforcePermission(); +        public void onUdfpsUiEvent(@FingerprintManager.UdfpsUiEvent int event, long requestId, +                int sensorId) { +            super.onUdfpsUiEvent_enforcePermission();              final ServiceProvider provider = mRegistry.getProviderForSensor(sensorId);              if (provider == null) { -                Slog.w(TAG, "No matching provider for onUiReady, sensorId: " + sensorId); +                Slog.w(TAG, "No matching provider for onUdfpsUiEvent, sensorId: " + sensorId);                  return;              } -            provider.onUiReady(requestId, sensorId); +            provider.onUdfpsUiEvent(event, requestId, sensorId);          } +          @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)          @Override          public void setUdfpsOverlayController(@NonNull IUdfpsOverlayController controller) { diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java index 004af2c2ad62..26701c110c39 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/ServiceProvider.java @@ -130,7 +130,7 @@ public interface ServiceProvider extends      void onPointerUp(long requestId, int sensorId, PointerContext pc); -    void onUiReady(long requestId, int sensorId); +    void onUdfpsUiEvent(@FingerprintManager.UdfpsUiEvent int event, long requestId, int sensorId);      void setUdfpsOverlayController(@NonNull IUdfpsOverlayController controller); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/Udfps.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/Udfps.java index da7163a2a678..dce0175ca593 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/Udfps.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/Udfps.java @@ -17,6 +17,7 @@  package com.android.server.biometrics.sensors.fingerprint;  import android.hardware.biometrics.fingerprint.PointerContext; +import android.hardware.fingerprint.FingerprintManager;  import com.android.server.biometrics.sensors.BaseClientMonitor; @@ -28,6 +29,6 @@ import com.android.server.biometrics.sensors.BaseClientMonitor;  public interface Udfps {      void onPointerDown(PointerContext pc);      void onPointerUp(PointerContext pc); -    void onUiReady(); +    void onUdfpsUiEvent(@FingerprintManager.UdfpsUiEvent int event);      boolean isPointerDown();  } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java index 135eccf9026a..ec1eeb138505 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java @@ -114,6 +114,11 @@ class BiometricTestSessionImpl extends ITestSession.Stub {          public void onUdfpsPointerUp(int sensorId) {          } + +        @Override +        public void onUdfpsOverlayShown() { + +        }      };      BiometricTestSessionImpl(@NonNull Context context, int sensorId, diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java index 2bfc2391b948..3fc36b6df92d 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java @@ -27,6 +27,7 @@ import android.hardware.biometrics.BiometricManager.Authenticators;  import android.hardware.biometrics.common.ICancellationSignal;  import android.hardware.biometrics.fingerprint.PointerContext;  import android.hardware.fingerprint.FingerprintAuthenticateOptions; +import android.hardware.fingerprint.FingerprintManager;  import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;  import android.hardware.fingerprint.ISidefpsController;  import android.hardware.fingerprint.IUdfpsOverlay; @@ -363,9 +364,11 @@ class FingerprintAuthenticationClient      }      @Override -    public void onUiReady() { +    public void onUdfpsUiEvent(@FingerprintManager.UdfpsUiEvent int event) {          try { -            getFreshDaemon().getSession().onUiReady(); +            if (event == FingerprintManager.UDFPS_UI_READY) { +                getFreshDaemon().getSession().onUiReady(); +            }          } catch (RemoteException e) {              Slog.e(TAG, "Remote exception", e);          } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java index c2ca78e91fe3..d35469c4655c 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java @@ -265,11 +265,20 @@ class FingerprintEnrollClient extends EnrollClient<AidlSession> implements Udfps      }      @Override -    public void onUiReady() { +    public void onUdfpsUiEvent(@FingerprintManager.UdfpsUiEvent int event) {          try { -            getFreshDaemon().getSession().onUiReady(); +            switch (event) { +                case FingerprintManager.UDFPS_UI_OVERLAY_SHOWN: +                    getListener().onUdfpsOverlayShown(); +                    break; +                case FingerprintManager.UDFPS_UI_READY: +                    getFreshDaemon().getSession().onUiReady(); +                    break; +                default: +                    Slog.w(TAG, "No matching event for onUdfpsUiEvent"); +            }          } catch (RemoteException e) { -            Slog.e(TAG, "Unable to send UI ready", e); +            Slog.e(TAG, "Unable to send onUdfpsUiEvent", e);          }      } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java index 58ece898a9fe..3e33ef63c7d7 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintProvider.java @@ -669,14 +669,15 @@ public class FingerprintProvider implements IBinder.DeathRecipient, ServiceProvi      }      @Override -    public void onUiReady(long requestId, int sensorId) { +    public void onUdfpsUiEvent(@FingerprintManager.UdfpsUiEvent int event, long requestId, +            int sensorId) {          mFingerprintSensors.get(sensorId).getScheduler().getCurrentClientIfMatches(                  requestId, (client) -> {                      if (!(client instanceof Udfps)) { -                        Slog.e(getTag(), "onUiReady received during client: " + client); +                        Slog.e(getTag(), "onUdfpsUiEvent received during client: " + client);                          return;                      } -                    ((Udfps) client).onUiReady(); +                    ((Udfps) client).onUdfpsUiEvent(event);                  });      } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/BiometricTestSessionImpl.java index 86a9f7998398..c20a9eb958c4 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/BiometricTestSessionImpl.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/BiometricTestSessionImpl.java @@ -115,6 +115,11 @@ public class BiometricTestSessionImpl extends ITestSession.Stub {          public void onUdfpsPointerUp(int sensorId) {          } + +        @Override +        public void onUdfpsOverlayShown() { + +        }      };      BiometricTestSessionImpl(@NonNull Context context, int sensorId, diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java index 9e6f4e4f13f3..1cbbf89e052a 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21.java @@ -829,13 +829,14 @@ public class Fingerprint21 implements IHwBinder.DeathRecipient, ServiceProvider      }      @Override -    public void onUiReady(long requestId, int sensorId) { +    public void onUdfpsUiEvent(@FingerprintManager.UdfpsUiEvent int event, long requestId, +            int sensorId) {          mScheduler.getCurrentClientIfMatches(requestId, (client) -> {              if (!(client instanceof Udfps)) { -                Slog.w(TAG, "onUiReady received during client: " + client); +                Slog.w(TAG, "onUdfpsUiEvent received during client: " + client);                  return;              } -            ((Udfps) client).onUiReady(); +            ((Udfps) client).onUdfpsUiEvent(event);          });      } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java index d22aef8b3971..2a6233824c2e 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java @@ -27,6 +27,7 @@ import android.hardware.biometrics.BiometricManager.Authenticators;  import android.hardware.biometrics.fingerprint.PointerContext;  import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;  import android.hardware.fingerprint.FingerprintAuthenticateOptions; +import android.hardware.fingerprint.FingerprintManager;  import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;  import android.hardware.fingerprint.ISidefpsController;  import android.hardware.fingerprint.IUdfpsOverlay; @@ -273,7 +274,7 @@ class FingerprintAuthenticationClient      }      @Override -    public void onUiReady() { +    public void onUdfpsUiEvent(@FingerprintManager.UdfpsUiEvent int event) {          // Unsupported in HIDL.      }  } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java index 362c820b9e8d..ed0a2015a461 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java @@ -25,6 +25,7 @@ import android.hardware.biometrics.BiometricOverlayConstants;  import android.hardware.biometrics.fingerprint.PointerContext;  import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;  import android.hardware.fingerprint.FingerprintAuthenticateOptions; +import android.hardware.fingerprint.FingerprintManager;  import android.hardware.fingerprint.IUdfpsOverlay;  import android.hardware.fingerprint.IUdfpsOverlayController;  import android.os.IBinder; @@ -130,7 +131,7 @@ class FingerprintDetectClient extends AcquisitionClient<IBiometricsFingerprint>      }      @Override -    public void onUiReady() { +    public void onUdfpsUiEvent(@FingerprintManager.UdfpsUiEvent int event) {          // Unsupported in HIDL.      } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java index 78039ef99986..c2b7944eac35 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintEnrollClient.java @@ -186,7 +186,7 @@ public class FingerprintEnrollClient extends EnrollClient<IBiometricsFingerprint      }      @Override -    public void onUiReady() { +    public void onUdfpsUiEvent(@FingerprintManager.UdfpsUiEvent int event) {          // Unsupported in HIDL.      }  } diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index e85eee817d29..e3262cfbd30b 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -22,7 +22,6 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED;  import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;  import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;  import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; -import static android.net.NetworkCapabilities.TRANSPORT_VPN;  import static android.net.RouteInfo.RTN_THROW;  import static android.net.RouteInfo.RTN_UNREACHABLE;  import static android.net.VpnManager.NOTIFICATION_CHANNEL_VPN; @@ -280,15 +279,22 @@ public class Vpn {      private static final int VPN_DEFAULT_SCORE = 101;      /** -     * The reset session timer for data stall. If a session has not successfully revalidated after -     * the delay, the session will be torn down and restarted in an attempt to recover. Delay +     * The recovery timer for data stall. If a session has not successfully revalidated after +     * the delay, the session will perform MOBIKE or be restarted in an attempt to recover. Delay       * counter is reset on successful validation only.       * +     * <p>The first {@code MOBIKE_RECOVERY_ATTEMPT} timers are used for performing MOBIKE. +     * System will perform session reset for the remaining timers.       * <p>If retries have exceeded the length of this array, the last entry in the array will be       * used as a repeating interval.       */ -    private static final long[] DATA_STALL_RESET_DELAYS_SEC = {30L, 60L, 120L, 240L, 480L, 960L}; - +    // TODO: use ms instead to speed up the test. +    private static final long[] DATA_STALL_RECOVERY_DELAYS_SEC = +            {1L, 5L, 30L, 60L, 120L, 240L, 480L, 960L}; +    /** +     * Maximum attempts to perform MOBIKE when the network is bad. +     */ +    private static final int MAX_MOBIKE_RECOVERY_ATTEMPT = 2;      /**       * The initial token value of IKE session.       */ @@ -380,6 +386,7 @@ public class Vpn {      private final INetworkManagementService mNms;      private final INetd mNetd;      @VisibleForTesting +    @GuardedBy("this")      protected VpnConfig mConfig;      private final NetworkProvider mNetworkProvider;      @VisibleForTesting @@ -392,7 +399,6 @@ public class Vpn {      private final UserManager mUserManager;      private final VpnProfileStore mVpnProfileStore; -    protected boolean mDataStallSuspected = false;      @VisibleForTesting      VpnProfileStore getVpnProfileStore() { @@ -685,14 +691,14 @@ public class Vpn {          }          /** -         * Get the length of time to wait before resetting the ike session when a data stall is -         * suspected. +         * Get the length of time to wait before perform data stall recovery when the validation +         * result is bad.           */ -        public long getDataStallResetSessionSeconds(int count) { -            if (count >= DATA_STALL_RESET_DELAYS_SEC.length) { -                return DATA_STALL_RESET_DELAYS_SEC[DATA_STALL_RESET_DELAYS_SEC.length - 1]; +        public long getValidationFailRecoverySeconds(int count) { +            if (count >= DATA_STALL_RECOVERY_DELAYS_SEC.length) { +                return DATA_STALL_RECOVERY_DELAYS_SEC[DATA_STALL_RECOVERY_DELAYS_SEC.length - 1];              } else { -                return DATA_STALL_RESET_DELAYS_SEC[count]; +                return DATA_STALL_RECOVERY_DELAYS_SEC[count];              }          } @@ -1598,6 +1604,8 @@ public class Vpn {          return network;      } +    // TODO : this is not synchronized(this) but reads from mConfig, which is dangerous +    // This file makes an effort to avoid partly initializing mConfig, but this is still not great      private LinkProperties makeLinkProperties() {          // The design of disabling IPv6 is only enabled for IKEv2 VPN because it needs additional          // logic to handle IPv6 only VPN, and the IPv6 only VPN may be restarted when its MTU @@ -1679,6 +1687,7 @@ public class Vpn {       * registering a new NetworkAgent. This is not always possible if the new VPN configuration       * has certain changes, in which case this method would just return {@code false}.       */ +    // TODO : this method is not synchronized(this) but reads from mConfig      private boolean updateLinkPropertiesInPlaceIfPossible(NetworkAgent agent, VpnConfig oldConfig) {          // NetworkAgentConfig cannot be updated without registering a new NetworkAgent.          // Strictly speaking, bypassability is affected by lockdown and therefore it's possible @@ -2269,7 +2278,12 @@ public class Vpn {       */      public synchronized VpnConfig getVpnConfig() {          enforceControlPermission(); -        return mConfig; +        // Constructor of VpnConfig cannot take a null parameter. Return null directly if mConfig is +        // null +        if (mConfig == null) return null; +        // mConfig is guarded by "this" and can be modified by another thread as soon as +        // this method returns, so this method must return a copy. +        return new VpnConfig(mConfig);      }      @Deprecated @@ -2315,6 +2329,7 @@ public class Vpn {          }      }; +    @GuardedBy("this")      private void cleanupVpnStateLocked() {          mStatusIntent = null;          resetNetworkCapabilities(); @@ -2837,9 +2852,7 @@ public class Vpn {          }          final boolean isLegacyVpn = mVpnRunner instanceof LegacyVpnRunner; -          mVpnRunner.exit(); -        mVpnRunner = null;          // LegacyVpn uses daemons that must be shut down before new ones are brought up.          // The same limitation does not apply to Platform VPNs. @@ -3044,7 +3057,6 @@ public class Vpn {          @Nullable private IkeSessionWrapper mSession;          @Nullable private IkeSessionConnectionInfo mIkeConnectionInfo; -        @Nullable private VpnConnectivityDiagnosticsCallback mDiagnosticsCallback;          // mMobikeEnabled can only be updated after IKE AUTH is finished.          private boolean mMobikeEnabled = false; @@ -3055,7 +3067,7 @@ public class Vpn {           * <p>This variable controls the retry delay, and is reset when the VPN pass network           * validation.           */ -        private int mDataStallRetryCount = 0; +        private int mValidationFailRetryCount = 0;          /**           * The number of attempts since the last successful connection. @@ -3084,6 +3096,7 @@ public class Vpn {                      }          }; +        // GuardedBy("Vpn.this") (annotation can't be applied to constructor)          IkeV2VpnRunner(                  @NonNull Ikev2VpnProfile profile, @NonNull ScheduledThreadPoolExecutor executor) {              super(TAG); @@ -3136,15 +3149,6 @@ public class Vpn {                  mConnectivityManager.registerSystemDefaultNetworkCallback(mNetworkCallback,                          new Handler(mLooper));              } - -            // DiagnosticsCallback may return more than one alive VPNs, but VPN will filter based on -            // Network object. -            final NetworkRequest diagRequest = new NetworkRequest.Builder() -                    .addTransportType(TRANSPORT_VPN) -                    .removeCapability(NET_CAPABILITY_NOT_VPN).build(); -            mDiagnosticsCallback = new VpnConnectivityDiagnosticsCallback(); -            mConnectivityDiagnosticsManager.registerConnectivityDiagnosticsCallback( -                    diagRequest, mExecutor, mDiagnosticsCallback);          }          private boolean isActiveNetwork(@Nullable Network network) { @@ -3710,11 +3714,14 @@ public class Vpn {          }          public void updateVpnTransportInfoAndNetCap(int keepaliveDelaySec) { -            final VpnTransportInfo info = new VpnTransportInfo( -                    getActiveVpnType(), -                    mConfig.session, -                    mConfig.allowBypass && !mLockdown, -                    areLongLivedTcpConnectionsExpensive(keepaliveDelaySec)); +            final VpnTransportInfo info; +            synchronized (Vpn.this) { +                info = new VpnTransportInfo( +                        getActiveVpnType(), +                        mConfig.session, +                        mConfig.allowBypass && !mLockdown, +                        areLongLivedTcpConnectionsExpensive(keepaliveDelaySec)); +            }              final boolean ncUpdateRequired = !info.equals(mNetworkCapabilities.getTransportInfo());              if (ncUpdateRequired) {                  mNetworkCapabilities = new NetworkCapabilities.Builder(mNetworkCapabilities) @@ -3875,39 +3882,12 @@ public class Vpn {              }          } -        class VpnConnectivityDiagnosticsCallback -                extends ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback { -            // The callback runs in the executor thread. -            @Override -            public void onDataStallSuspected( -                    ConnectivityDiagnosticsManager.DataStallReport report) { -                synchronized (Vpn.this) { -                    // Ignore stale runner. -                    if (mVpnRunner != Vpn.IkeV2VpnRunner.this) return; - -                    // Handle the report only for current VPN network. If data stall is already -                    // reported, ignoring the other reports. It means that the stall is not -                    // recovered by MOBIKE and should be on the way to reset the ike session. -                    if (mNetworkAgent != null -                            && mNetworkAgent.getNetwork().equals(report.getNetwork()) -                            && !mDataStallSuspected) { -                        Log.d(TAG, "Data stall suspected"); - -                        // Trigger MOBIKE. -                        maybeMigrateIkeSessionAndUpdateVpnTransportInfo(mActiveNetwork); -                        mDataStallSuspected = true; -                    } -                } -            } -        } -          public void onValidationStatus(int status) {              mEventChanges.log("[Validation] validation status " + status);              if (status == NetworkAgent.VALIDATION_STATUS_VALID) {                  // No data stall now. Reset it.                  mExecutor.execute(() -> { -                    mDataStallSuspected = false; -                    mDataStallRetryCount = 0; +                    mValidationFailRetryCount = 0;                      if (mScheduledHandleDataStallFuture != null) {                          Log.d(TAG, "Recovered from stall. Cancel pending reset action.");                          mScheduledHandleDataStallFuture.cancel(false /* mayInterruptIfRunning */); @@ -3918,8 +3898,21 @@ public class Vpn {                  // Skip other invalid status if the scheduled recovery exists.                  if (mScheduledHandleDataStallFuture != null) return; +                if (mValidationFailRetryCount < MAX_MOBIKE_RECOVERY_ATTEMPT) { +                    Log.d(TAG, "Validation failed"); + +                    // Trigger MOBIKE to recover first. +                    mExecutor.schedule(() -> { +                        maybeMigrateIkeSessionAndUpdateVpnTransportInfo(mActiveNetwork); +                    }, mDeps.getValidationFailRecoverySeconds(mValidationFailRetryCount++), +                            TimeUnit.SECONDS); +                    return; +                } + +                // Data stall is not recovered by MOBIKE. Try to reset session to recover it.                  mScheduledHandleDataStallFuture = mExecutor.schedule(() -> { -                    if (mDataStallSuspected) { +                    // Only perform the recovery when the network is still bad. +                    if (mValidationFailRetryCount > 0) {                          Log.d(TAG, "Reset session to recover stalled network");                          // This will reset old state if it exists.                          startIkeSession(mActiveNetwork); @@ -3928,7 +3921,9 @@ public class Vpn {                      // Reset mScheduledHandleDataStallFuture since it's already run on executor                      // thread.                      mScheduledHandleDataStallFuture = null; -                }, mDeps.getDataStallResetSessionSeconds(mDataStallRetryCount++), TimeUnit.SECONDS); +                    // TODO: compute the delay based on the last recovery timestamp +                }, mDeps.getValidationFailRecoverySeconds(mValidationFailRetryCount++), +                        TimeUnit.SECONDS);              }          } @@ -4220,7 +4215,7 @@ public class Vpn {           * consistency of the Ikev2VpnRunner fields.           */          private void disconnectVpnRunner() { -            mEventChanges.log("[VPNRunner] Disconnect runner, underlying network" + mActiveNetwork); +            mEventChanges.log("[VPNRunner] Disconnect runner, underlying net " + mActiveNetwork);              mActiveNetwork = null;              mUnderlyingNetworkCapabilities = null;              mUnderlyingLinkProperties = null; @@ -4231,8 +4226,6 @@ public class Vpn {              mCarrierConfigManager.unregisterCarrierConfigChangeListener(                      mCarrierConfigChangeListener);              mConnectivityManager.unregisterNetworkCallback(mNetworkCallback); -            mConnectivityDiagnosticsManager.unregisterConnectivityDiagnosticsCallback( -                    mDiagnosticsCallback);              clearVpnNetworkPreference(mSessionKey);              mExecutor.shutdown(); @@ -4293,6 +4286,7 @@ public class Vpn {              }          }; +        // GuardedBy("Vpn.this") (annotation can't be applied to constructor)          LegacyVpnRunner(VpnConfig config, String[] racoon, String[] mtpd, VpnProfile profile) {              super(TAG);              if (racoon == null && mtpd == null) { @@ -4500,46 +4494,46 @@ public class Vpn {                  }                  // Set the interface and the addresses in the config. -                mConfig.interfaze = parameters[0].trim(); +                synchronized (Vpn.this) { +                    mConfig.interfaze = parameters[0].trim(); -                mConfig.addLegacyAddresses(parameters[1]); -                // Set the routes if they are not set in the config. -                if (mConfig.routes == null || mConfig.routes.isEmpty()) { -                    mConfig.addLegacyRoutes(parameters[2]); -                } +                    mConfig.addLegacyAddresses(parameters[1]); +                    // Set the routes if they are not set in the config. +                    if (mConfig.routes == null || mConfig.routes.isEmpty()) { +                        mConfig.addLegacyRoutes(parameters[2]); +                    } -                // Set the DNS servers if they are not set in the config. -                if (mConfig.dnsServers == null || mConfig.dnsServers.size() == 0) { -                    String dnsServers = parameters[3].trim(); -                    if (!dnsServers.isEmpty()) { -                        mConfig.dnsServers = Arrays.asList(dnsServers.split(" ")); +                    // Set the DNS servers if they are not set in the config. +                    if (mConfig.dnsServers == null || mConfig.dnsServers.size() == 0) { +                        String dnsServers = parameters[3].trim(); +                        if (!dnsServers.isEmpty()) { +                            mConfig.dnsServers = Arrays.asList(dnsServers.split(" ")); +                        }                      } -                } -                // Set the search domains if they are not set in the config. -                if (mConfig.searchDomains == null || mConfig.searchDomains.size() == 0) { -                    String searchDomains = parameters[4].trim(); -                    if (!searchDomains.isEmpty()) { -                        mConfig.searchDomains = Arrays.asList(searchDomains.split(" ")); +                    // Set the search domains if they are not set in the config. +                    if (mConfig.searchDomains == null || mConfig.searchDomains.size() == 0) { +                        String searchDomains = parameters[4].trim(); +                        if (!searchDomains.isEmpty()) { +                            mConfig.searchDomains = Arrays.asList(searchDomains.split(" ")); +                        }                      } -                } -                // Add a throw route for the VPN server endpoint, if one was specified. -                if (endpointAddress instanceof Inet4Address) { -                    mConfig.routes.add(new RouteInfo( -                            new IpPrefix(endpointAddress, 32), null /*gateway*/, -                            null /*iface*/, RTN_THROW)); -                } else if (endpointAddress instanceof Inet6Address) { -                    mConfig.routes.add(new RouteInfo( -                            new IpPrefix(endpointAddress, 128), null /*gateway*/, -                            null /*iface*/, RTN_THROW)); -                } else { -                    Log.e(TAG, "Unknown IP address family for VPN endpoint: " -                            + endpointAddress); -                } +                    // Add a throw route for the VPN server endpoint, if one was specified. +                    if (endpointAddress instanceof Inet4Address) { +                        mConfig.routes.add(new RouteInfo( +                                new IpPrefix(endpointAddress, 32), null /*gateway*/, +                                null /*iface*/, RTN_THROW)); +                    } else if (endpointAddress instanceof Inet6Address) { +                        mConfig.routes.add(new RouteInfo( +                                new IpPrefix(endpointAddress, 128), null /*gateway*/, +                                null /*iface*/, RTN_THROW)); +                    } else { +                        Log.e(TAG, "Unknown IP address family for VPN endpoint: " +                                + endpointAddress); +                    } -                // Here is the last step and it must be done synchronously. -                synchronized (Vpn.this) { +                    // Here is the last step and it must be done synchronously.                      // Set the start time                      mConfig.startTime = SystemClock.elapsedRealtime(); @@ -4773,25 +4767,26 @@ public class Vpn {          try {              // Build basic config -            mConfig = new VpnConfig(); +            final VpnConfig config = new VpnConfig();              if (VpnConfig.LEGACY_VPN.equals(packageName)) { -                mConfig.legacy = true; -                mConfig.session = profile.name; -                mConfig.user = profile.key; +                config.legacy = true; +                config.session = profile.name; +                config.user = profile.key;                  // TODO: Add support for configuring meteredness via Settings. Until then, use a                  // safe default. -                mConfig.isMetered = true; +                config.isMetered = true;              } else { -                mConfig.user = packageName; -                mConfig.isMetered = profile.isMetered; +                config.user = packageName; +                config.isMetered = profile.isMetered;              } -            mConfig.startTime = SystemClock.elapsedRealtime(); -            mConfig.proxyInfo = profile.proxy; -            mConfig.requiresInternetValidation = profile.requiresInternetValidation; -            mConfig.excludeLocalRoutes = profile.excludeLocalRoutes; -            mConfig.allowBypass = profile.isBypassable; -            mConfig.disallowedApplications = getAppExclusionList(mPackage); +            config.startTime = SystemClock.elapsedRealtime(); +            config.proxyInfo = profile.proxy; +            config.requiresInternetValidation = profile.requiresInternetValidation; +            config.excludeLocalRoutes = profile.excludeLocalRoutes; +            config.allowBypass = profile.isBypassable; +            config.disallowedApplications = getAppExclusionList(mPackage); +            mConfig = config;              switch (profile.type) {                  case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS: @@ -4805,6 +4800,7 @@ public class Vpn {                      mVpnRunner.start();                      break;                  default: +                    mConfig = null;                      updateState(DetailedState.FAILED, "Invalid platform VPN type");                      Log.d(TAG, "Unknown VPN profile type: " + profile.type);                      break; @@ -5216,7 +5212,7 @@ public class Vpn {                  pw.println("MOBIKE " + (runner.mMobikeEnabled ? "enabled" : "disabled"));                  pw.println("Profile: " + runner.mProfile);                  pw.println("Token: " + runner.mCurrentToken); -                if (mDataStallSuspected) pw.println("Data stall suspected"); +                pw.println("Validation failed retry count:" + runner.mValidationFailRetryCount);                  if (runner.mScheduledHandleDataStallFuture != null) {                      pw.println("Reset session scheduled");                  } diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java index 75709fbb365a..d647757442e0 100644 --- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java +++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java @@ -223,11 +223,11 @@ public class AutomaticBrightnessController {      private final ShortTermModel mShortTermModel;      private final ShortTermModel mPausedShortTermModel; -    // Controls High Brightness Mode. -    private HighBrightnessModeController mHbmController; +    // Controls Brightness range (including High Brightness Mode). +    private final BrightnessRangeController mBrightnessRangeController;      // Throttles (caps) maximum allowed brightness -    private BrightnessThrottler mBrightnessThrottler; +    private final BrightnessThrottler mBrightnessThrottler;      private boolean mIsBrightnessThrottled;      // Context-sensitive brightness configurations require keeping track of the foreground app's @@ -257,7 +257,8 @@ public class AutomaticBrightnessController {              HysteresisLevels screenBrightnessThresholds,              HysteresisLevels ambientBrightnessThresholdsIdle,              HysteresisLevels screenBrightnessThresholdsIdle, Context context, -            HighBrightnessModeController hbmController, BrightnessThrottler brightnessThrottler, +            BrightnessRangeController brightnessModeController, +            BrightnessThrottler brightnessThrottler,              BrightnessMappingStrategy idleModeBrightnessMapper, int ambientLightHorizonShort,              int ambientLightHorizonLong, float userLux, float userBrightness) {          this(new Injector(), callbacks, looper, sensorManager, lightSensor, @@ -267,7 +268,7 @@ public class AutomaticBrightnessController {                  darkeningLightDebounceConfig, resetAmbientLuxAfterWarmUpConfig,                  ambientBrightnessThresholds, screenBrightnessThresholds,                  ambientBrightnessThresholdsIdle, screenBrightnessThresholdsIdle, context, -                hbmController, brightnessThrottler, idleModeBrightnessMapper, +                brightnessModeController, brightnessThrottler, idleModeBrightnessMapper,                  ambientLightHorizonShort, ambientLightHorizonLong, userLux, userBrightness          );      } @@ -283,7 +284,8 @@ public class AutomaticBrightnessController {              HysteresisLevels screenBrightnessThresholds,              HysteresisLevels ambientBrightnessThresholdsIdle,              HysteresisLevels screenBrightnessThresholdsIdle, Context context, -            HighBrightnessModeController hbmController, BrightnessThrottler brightnessThrottler, +            BrightnessRangeController brightnessModeController, +            BrightnessThrottler brightnessThrottler,              BrightnessMappingStrategy idleModeBrightnessMapper, int ambientLightHorizonShort,              int ambientLightHorizonLong, float userLux, float userBrightness) {          mInjector = injector; @@ -326,7 +328,7 @@ public class AutomaticBrightnessController {          mPendingForegroundAppPackageName = null;          mForegroundAppCategory = ApplicationInfo.CATEGORY_UNDEFINED;          mPendingForegroundAppCategory = ApplicationInfo.CATEGORY_UNDEFINED; -        mHbmController = hbmController; +        mBrightnessRangeController = brightnessModeController;          mBrightnessThrottler = brightnessThrottler;          mInteractiveModeBrightnessMapper = interactiveModeBrightnessMapper;          mIdleModeBrightnessMapper = idleModeBrightnessMapper; @@ -607,10 +609,11 @@ public class AutomaticBrightnessController {          pw.println();          pw.println("  mInteractiveMapper="); -        mInteractiveModeBrightnessMapper.dump(pw, mHbmController.getNormalBrightnessMax()); +        mInteractiveModeBrightnessMapper.dump(pw, +                mBrightnessRangeController.getNormalBrightnessMax());          if (mIdleModeBrightnessMapper != null) {              pw.println("  mIdleMapper="); -            mIdleModeBrightnessMapper.dump(pw, mHbmController.getNormalBrightnessMax()); +            mIdleModeBrightnessMapper.dump(pw, mBrightnessRangeController.getNormalBrightnessMax());          }          pw.println(); @@ -736,7 +739,7 @@ public class AutomaticBrightnessController {              mAmbientDarkeningThreshold =                      mAmbientBrightnessThresholds.getDarkeningThreshold(lux);          } -        mHbmController.onAmbientLuxChange(mAmbientLux); +        mBrightnessRangeController.onAmbientLuxChange(mAmbientLux);          // If the short term model was invalidated and the change is drastic enough, reset it. @@ -976,9 +979,9 @@ public class AutomaticBrightnessController {      // Clamps values with float range [0.0-1.0]      private float clampScreenBrightness(float value) { -        final float minBrightness = Math.min(mHbmController.getCurrentBrightnessMin(), +        final float minBrightness = Math.min(mBrightnessRangeController.getCurrentBrightnessMin(),                  mBrightnessThrottler.getBrightnessCap()); -        final float maxBrightness = Math.min(mHbmController.getCurrentBrightnessMax(), +        final float maxBrightness = Math.min(mBrightnessRangeController.getCurrentBrightnessMax(),                  mBrightnessThrottler.getBrightnessCap());          return MathUtils.constrain(value, minBrightness, maxBrightness);      } diff --git a/services/core/java/com/android/server/display/BrightnessRangeController.java b/services/core/java/com/android/server/display/BrightnessRangeController.java new file mode 100644 index 000000000000..47cde1517450 --- /dev/null +++ b/services/core/java/com/android/server/display/BrightnessRangeController.java @@ -0,0 +1,126 @@ +/* + * 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.server.display; + +import android.hardware.display.BrightnessInfo; +import android.os.IBinder; + +import java.io.PrintWriter; +import java.util.function.BooleanSupplier; + +class BrightnessRangeController { + +    private static final boolean NBM_FEATURE_FLAG = false; + +    private final HighBrightnessModeController mHbmController; +    private final NormalBrightnessModeController mNormalBrightnessModeController = +            new NormalBrightnessModeController(); + +    private final Runnable mModeChangeCallback; + +    BrightnessRangeController(HighBrightnessModeController hbmController, +            Runnable modeChangeCallback) { +        mHbmController = hbmController; +        mModeChangeCallback = modeChangeCallback; +    } + + +    void dump(PrintWriter pw) { +        mHbmController.dump(pw); +    } + +    void onAmbientLuxChange(float ambientLux) { +        applyChanges( +                () -> mNormalBrightnessModeController.onAmbientLuxChange(ambientLux), +                () -> mHbmController.onAmbientLuxChange(ambientLux) +        ); +    } + +    float getNormalBrightnessMax() { +        return mHbmController.getNormalBrightnessMax(); +    } + +    void loadFromConfig(HighBrightnessModeMetadata hbmMetadata, IBinder token, +            DisplayDeviceInfo info, DisplayDeviceConfig displayDeviceConfig) { +        applyChanges( +                () -> mNormalBrightnessModeController.resetNbmData( +                        displayDeviceConfig.getLuxThrottlingData()), +                () -> { +                    mHbmController.setHighBrightnessModeMetadata(hbmMetadata); +                    mHbmController.resetHbmData(info.width, info.height, token, info.uniqueId, +                            displayDeviceConfig.getHighBrightnessModeData(), +                            displayDeviceConfig::getHdrBrightnessFromSdr); +                } +        ); +    } + +    void stop() { +        mHbmController.stop(); +    } + +    void setAutoBrightnessEnabled(int state) { +        applyChanges( +                () -> mNormalBrightnessModeController.setAutoBrightnessState(state), +                () ->  mHbmController.setAutoBrightnessEnabled(state) +        ); +    } + +    void onBrightnessChanged(float brightness, float unthrottledBrightness, +            @BrightnessInfo.BrightnessMaxReason int throttlingReason) { +        mHbmController.onBrightnessChanged(brightness, unthrottledBrightness, throttlingReason); +    } + +    float getCurrentBrightnessMin() { +        return mHbmController.getCurrentBrightnessMin(); +    } + + +    float getCurrentBrightnessMax() { +        if (NBM_FEATURE_FLAG && mHbmController.getHighBrightnessMode() +                == BrightnessInfo.HIGH_BRIGHTNESS_MODE_OFF) { +            return Math.min(mHbmController.getCurrentBrightnessMax(), +                    mNormalBrightnessModeController.getCurrentBrightnessMax()); +        } +        return mHbmController.getCurrentBrightnessMax(); +    } + +    int getHighBrightnessMode() { +        return mHbmController.getHighBrightnessMode(); +    } + +    float getHdrBrightnessValue() { +        return mHbmController.getHdrBrightnessValue(); +    } + +    float getTransitionPoint() { +        return mHbmController.getTransitionPoint(); +    } + +    private void applyChanges(BooleanSupplier nbmChangesFunc, Runnable hbmChangesFunc) { +        if (NBM_FEATURE_FLAG) { +            boolean nbmTransitionChanged = nbmChangesFunc.getAsBoolean(); +            hbmChangesFunc.run(); +            // if nbm transition changed - trigger callback +            // HighBrightnessModeController handles sending changes itself +            if (nbmTransitionChanged) { +                mModeChangeCallback.run(); +            } +        } else { +            hbmChangesFunc.run(); +        } +    } +} diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java index 7a797dd2250c..7ccfb448cf61 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java +++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java @@ -41,6 +41,7 @@ import com.android.internal.annotations.VisibleForTesting;  import com.android.internal.display.BrightnessSynchronizer;  import com.android.server.display.config.AutoBrightness;  import com.android.server.display.config.BlockingZoneConfig; +import com.android.server.display.config.BrightnessLimitMap;  import com.android.server.display.config.BrightnessThresholds;  import com.android.server.display.config.BrightnessThrottlingMap;  import com.android.server.display.config.BrightnessThrottlingPoint; @@ -51,8 +52,11 @@ import com.android.server.display.config.DisplayQuirks;  import com.android.server.display.config.HbmTiming;  import com.android.server.display.config.HighBrightnessMode;  import com.android.server.display.config.IntegerArray; +import com.android.server.display.config.LuxThrottling;  import com.android.server.display.config.NitsMap; +import com.android.server.display.config.NonNegativeFloatToFloatPoint;  import com.android.server.display.config.Point; +import com.android.server.display.config.PredefinedBrightnessLimitNames;  import com.android.server.display.config.RefreshRateConfigs;  import com.android.server.display.config.RefreshRateRange;  import com.android.server.display.config.RefreshRateThrottlingMap; @@ -219,6 +223,22 @@ import javax.xml.datatype.DatatypeConfigurationException;   *        <allowInLowPowerMode>false</allowInLowPowerMode>   *      </highBrightnessMode>   * + *      <luxThrottling> + *        <brightnessLimitMap> + *          <type>default</type> + *          <map> + *            <point> + *                <first>5000</first> + *                <second>0.3</second> + *            </point> + *            <point> + *               <first>5000</first> + *               <second>0.3</second> + *            </point> + *          </map> + *        </brightnessPeakMap> + *      </luxThrottling> + *   *      <quirks>   *       <quirk>canSetBrightnessViaHwc</quirk>   *      </quirks> @@ -693,6 +713,9 @@ public class DisplayDeviceConfig {      private final Map<String, SparseArray<SurfaceControl.RefreshRateRange>>              mRefreshRateThrottlingMap = new HashMap<>(); +    private final Map<BrightnessLimitMapType, Map<Float, Float>> +            mLuxThrottlingData = new HashMap<>(); +      @Nullable      private HostUsiVersion mHostUsiVersion; @@ -1344,6 +1367,11 @@ public class DisplayDeviceConfig {          return hbmData;      } +    @NonNull +    public Map<BrightnessLimitMapType, Map<Float, Float>> getLuxThrottlingData() { +        return mLuxThrottlingData; +    } +      public List<RefreshRateLimitation> getRefreshRateLimitations() {          return mRefreshRateLimitations;      } @@ -1530,6 +1558,7 @@ public class DisplayDeviceConfig {                  + ", mBrightnessDefault=" + mBrightnessDefault                  + ", mQuirks=" + mQuirks                  + ", isHbmEnabled=" + mIsHighBrightnessModeEnabled +                + ", mLuxThrottlingData=" + mLuxThrottlingData                  + ", mHbmData=" + mHbmData                  + ", mSdrToHdrRatioSpline=" + mSdrToHdrRatioSpline                  + ", mThermalBrightnessThrottlingDataMapByThrottlingId=" @@ -1676,6 +1705,7 @@ public class DisplayDeviceConfig {                  loadBrightnessMap(config);                  loadThermalThrottlingConfig(config);                  loadHighBrightnessModeData(config); +                loadLuxThrottling(config);                  loadQuirks(config);                  loadBrightnessRamps(config);                  loadAmbientLightSensorFromDdc(config); @@ -2428,6 +2458,54 @@ public class DisplayDeviceConfig {          }      } +    private void loadLuxThrottling(DisplayConfiguration config) { +        LuxThrottling cfg = config.getLuxThrottling(); +        if (cfg != null) { +            HighBrightnessMode hbm = config.getHighBrightnessMode(); +            float hbmTransitionPoint = hbm != null ? hbm.getTransitionPoint_all().floatValue() +                    : PowerManager.BRIGHTNESS_MAX; +            List<BrightnessLimitMap> limitMaps = cfg.getBrightnessLimitMap(); +            for (BrightnessLimitMap map : limitMaps) { +                PredefinedBrightnessLimitNames type = map.getType(); +                BrightnessLimitMapType mappedType = BrightnessLimitMapType.convert(type); +                if (mappedType == null) { +                    Slog.wtf(TAG, "Invalid NBM config: unsupported map type=" + type); +                    continue; +                } +                if (mLuxThrottlingData.containsKey(mappedType)) { +                    Slog.wtf(TAG, "Invalid NBM config: duplicate map type=" + mappedType); +                    continue; +                } +                Map<Float, Float> luxToTransitionPointMap = new HashMap<>(); + +                List<NonNegativeFloatToFloatPoint> points = map.getMap().getPoint(); +                for (NonNegativeFloatToFloatPoint point : points) { +                    float lux = point.getFirst().floatValue(); +                    float maxBrightness = point.getSecond().floatValue(); +                    if (maxBrightness > hbmTransitionPoint) { +                        Slog.wtf(TAG, +                                "Invalid NBM config: maxBrightness is greater than hbm" +                                        + ".transitionPoint. type=" +                                        + type + "; lux=" + lux + "; maxBrightness=" +                                        + maxBrightness); +                        continue; +                    } +                    if (luxToTransitionPointMap.containsKey(lux)) { +                        Slog.wtf(TAG, +                                "Invalid NBM config: duplicate lux key. type=" + type + "; lux=" +                                        + lux); +                        continue; +                    } +                    luxToTransitionPointMap.put(lux, +                            mBacklightToBrightnessSpline.interpolate(maxBrightness)); +                } +                if (!luxToTransitionPointMap.isEmpty()) { +                    mLuxThrottlingData.put(mappedType, luxToTransitionPointMap); +                } +            } +        } +    } +      private void loadBrightnessRamps(DisplayConfiguration config) {          // Priority 1: Value in the display device config (float)          // Priority 2: Value in the config.xml (int) @@ -3155,4 +3233,19 @@ public class DisplayDeviceConfig {              }          }      } + +    public enum BrightnessLimitMapType { +        DEFAULT, ADAPTIVE; + +        @Nullable +        private static BrightnessLimitMapType convert(PredefinedBrightnessLimitNames type) { +            switch (type) { +                case _default: +                    return DEFAULT; +                case adaptive: +                    return ADAPTIVE; +            } +            return null; +        } +    }  } diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 9d31572c7d76..f6e074f41743 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -445,7 +445,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call      private final ColorDisplayServiceInternal mCdsi;      private float[] mNitsRange; -    private final HighBrightnessModeController mHbmController; +    private final BrightnessRangeController mBrightnessRangeController;      private final HighBrightnessModeMetadata mHighBrightnessModeMetadata;      private final BrightnessThrottler mBrightnessThrottler; @@ -654,8 +654,19 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call          loadBrightnessRampRates();          mSkipScreenOnBrightnessRamp = resources.getBoolean(                  com.android.internal.R.bool.config_skipScreenOnBrightnessRamp); +        Runnable modeChangeCallback = () -> { +            sendUpdatePowerState(); +            postBrightnessChangeRunnable(); +            // TODO(b/192258832): Switch the HBMChangeCallback to a listener pattern. +            if (mAutomaticBrightnessController != null) { +                mAutomaticBrightnessController.update(); +            } +        }; -        mHbmController = createHbmControllerLocked(); +        HighBrightnessModeController hbmController = createHbmControllerLocked(modeChangeCallback); + +        mBrightnessRangeController = new BrightnessRangeController(hbmController, +                modeChangeCallback);          mBrightnessThrottler = createBrightnessThrottlerLocked(); @@ -802,7 +813,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call      @Override      public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux) { -        mHbmController.onAmbientLuxChange(ambientLux); +        mBrightnessRangeController.onAmbientLuxChange(ambientLux);          if (nits < 0) {              mBrightnessToFollow = leadDisplayBrightness;          } else { @@ -1039,17 +1050,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call                      mBrightnessRampIncreaseMaxTimeMillis,                      mBrightnessRampDecreaseMaxTimeMillis);          } -        mHbmController.setHighBrightnessModeMetadata(hbmMetadata); -        mHbmController.resetHbmData(info.width, info.height, token, info.uniqueId, -                mDisplayDeviceConfig.getHighBrightnessModeData(), -                new HighBrightnessModeController.HdrBrightnessDeviceConfig() { -                    @Override -                    public float getHdrBrightnessFromSdr( -                            float sdrBrightness, float maxDesiredHdrSdrRatio) { -                        return mDisplayDeviceConfig.getHdrBrightnessFromSdr( -                                sdrBrightness, maxDesiredHdrSdrRatio); -                    } -                }); +        mBrightnessRangeController.loadFromConfig(hbmMetadata, token, info, mDisplayDeviceConfig);          mBrightnessThrottler.loadThermalBrightnessThrottlingDataFromDisplayDeviceConfig(                  mDisplayDeviceConfig.getThermalBrightnessThrottlingDataMapByThrottlingId(),                  mThermalBrightnessThrottlingDataId, mUniqueDisplayId); @@ -1264,7 +1265,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call                      darkeningLightDebounce, autoBrightnessResetAmbientLuxAfterWarmUp,                      ambientBrightnessThresholds, screenBrightnessThresholds,                      ambientBrightnessThresholdsIdle, screenBrightnessThresholdsIdle, mContext, -                    mHbmController, mBrightnessThrottler, mIdleModeBrightnessMapper, +                    mBrightnessRangeController, mBrightnessThrottler, mIdleModeBrightnessMapper,                      mDisplayDeviceConfig.getAmbientHorizonShort(),                      mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userBrightness); @@ -1364,7 +1365,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call      /** Clean up all resources that are accessed via the {@link #mHandler} thread. */      private void cleanupHandlerThreadAfterStop() {          setProximitySensorEnabled(false); -        mHbmController.stop(); +        mBrightnessRangeController.stop();          mBrightnessThrottler.stop();          mHandler.removeCallbacksAndMessages(null); @@ -1647,7 +1648,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call                      mShouldResetShortTermModel);              mShouldResetShortTermModel = false;          } -        mHbmController.setAutoBrightnessEnabled(mUseAutoBrightness +        mBrightnessRangeController.setAutoBrightnessEnabled(mUseAutoBrightness                  ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED                  : AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED); @@ -1820,7 +1821,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call          // here instead of having HbmController listen to the brightness setting because certain          // brightness sources (such as an app override) are not saved to the setting, but should be          // reflected in HBM calculations. -        mHbmController.onBrightnessChanged(brightnessState, unthrottledBrightnessState, +        mBrightnessRangeController.onBrightnessChanged(brightnessState, unthrottledBrightnessState,                  mBrightnessThrottler.getBrightnessMaxReason());          // Animate the screen brightness when the screen is on or dozing. @@ -1874,13 +1875,14 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call              float sdrAnimateValue = animateValue;              // TODO(b/216365040): The decision to prevent HBM for HDR in low power mode should be              // done in HighBrightnessModeController. -            if (mHbmController.getHighBrightnessMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR +            if (mBrightnessRangeController.getHighBrightnessMode() +                    == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR                      && (mBrightnessReasonTemp.getModifier() & BrightnessReason.MODIFIER_DIMMED) == 0                      && (mBrightnessReasonTemp.getModifier() & BrightnessReason.MODIFIER_LOW_POWER)                      == 0) {                  // We want to scale HDR brightness level with the SDR level, we also need to restore                  // SDR brightness immediately when entering dim or low power mode. -                animateValue = mHbmController.getHdrBrightnessValue(); +                animateValue = mBrightnessRangeController.getHdrBrightnessValue();              }              final float currentBrightness = mPowerState.getScreenBrightness(); @@ -1942,8 +1944,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call          mTempBrightnessEvent.setBrightness(brightnessState);          mTempBrightnessEvent.setPhysicalDisplayId(mUniqueDisplayId);          mTempBrightnessEvent.setReason(mBrightnessReason); -        mTempBrightnessEvent.setHbmMax(mHbmController.getCurrentBrightnessMax()); -        mTempBrightnessEvent.setHbmMode(mHbmController.getHighBrightnessMode()); +        mTempBrightnessEvent.setHbmMax(mBrightnessRangeController.getCurrentBrightnessMax()); +        mTempBrightnessEvent.setHbmMode(mBrightnessRangeController.getHighBrightnessMode());          mTempBrightnessEvent.setFlags(mTempBrightnessEvent.getFlags()                  | (mIsRbcActive ? BrightnessEvent.FLAG_RBC : 0)                  | (mPowerRequest.lowPowerMode ? BrightnessEvent.FLAG_LOW_POWER_MODE : 0)); @@ -2104,9 +2106,11 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call      private boolean saveBrightnessInfo(float brightness, float adjustedBrightness) {          synchronized (mCachedBrightnessInfo) { -            final float minBrightness = Math.min(mHbmController.getCurrentBrightnessMin(), +            final float minBrightness = Math.min( +                    mBrightnessRangeController.getCurrentBrightnessMin(),                      mBrightnessThrottler.getBrightnessCap()); -            final float maxBrightness = Math.min(mHbmController.getCurrentBrightnessMax(), +            final float maxBrightness = Math.min( +                    mBrightnessRangeController.getCurrentBrightnessMax(),                      mBrightnessThrottler.getBrightnessCap());              boolean changed = false; @@ -2124,10 +2128,10 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call                              maxBrightness);              changed |=                      mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.hbmMode, -                            mHbmController.getHighBrightnessMode()); +                            mBrightnessRangeController.getHighBrightnessMode());              changed |=                      mCachedBrightnessInfo.checkAndSetFloat(mCachedBrightnessInfo.hbmTransitionPoint, -                            mHbmController.getTransitionPoint()); +                            mBrightnessRangeController.getTransitionPoint());              changed |=                      mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.brightnessMaxReason,                              mBrightnessThrottler.getBrightnessMaxReason()); @@ -2137,10 +2141,13 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call      }      void postBrightnessChangeRunnable() { -        mHandler.post(mOnBrightnessChangeRunnable); +        if (!mHandler.hasCallbacks(mOnBrightnessChangeRunnable)) { +            mHandler.post(mOnBrightnessChangeRunnable); +        }      } -    private HighBrightnessModeController createHbmControllerLocked() { +    private HighBrightnessModeController createHbmControllerLocked( +            Runnable modeChangeCallback) {          final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked();          final DisplayDeviceConfig ddConfig = device.getDisplayDeviceConfig();          final IBinder displayToken = @@ -2159,15 +2166,7 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call                          return mDisplayDeviceConfig.getHdrBrightnessFromSdr(                                  sdrBrightness, maxDesiredHdrSdrRatio);                      } -                }, -                () -> { -                    sendUpdatePowerState(); -                    postBrightnessChangeRunnable(); -                    // TODO(b/192258832): Switch the HBMChangeCallback to a listener pattern. -                    if (mAutomaticBrightnessController != null) { -                        mAutomaticBrightnessController.update(); -                    } -                }, mHighBrightnessModeMetadata, mContext); +                }, modeChangeCallback, mHighBrightnessModeMetadata, mContext);      }      private BrightnessThrottler createBrightnessThrottlerLocked() { @@ -2328,8 +2327,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call          if (Float.isNaN(value)) {              value = PowerManager.BRIGHTNESS_MIN;          } -        return MathUtils.constrain(value, -                mHbmController.getCurrentBrightnessMin(), mHbmController.getCurrentBrightnessMax()); +        return MathUtils.constrain(value, mBrightnessRangeController.getCurrentBrightnessMin(), +                mBrightnessRangeController.getCurrentBrightnessMax());      }      // Checks whether the brightness is within the valid brightness range, not including off. @@ -3003,8 +3002,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call              mScreenOffBrightnessSensorController.dump(pw);          } -        if (mHbmController != null) { -            mHbmController.dump(pw); +        if (mBrightnessRangeController != null) { +            mBrightnessRangeController.dump(pw);          }          if (mBrightnessThrottler != null) { @@ -3471,7 +3470,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call                  HysteresisLevels screenBrightnessThresholds,                  HysteresisLevels ambientBrightnessThresholdsIdle,                  HysteresisLevels screenBrightnessThresholdsIdle, Context context, -                HighBrightnessModeController hbmController, BrightnessThrottler brightnessThrottler, +                BrightnessRangeController brightnessRangeController, +                BrightnessThrottler brightnessThrottler,                  BrightnessMappingStrategy idleModeBrightnessMapper, int ambientLightHorizonShort,                  int ambientLightHorizonLong, float userLux, float userBrightness) {              return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor, @@ -3480,9 +3480,9 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call                      brighteningLightDebounceConfig, darkeningLightDebounceConfig,                      resetAmbientLuxAfterWarmUpConfig, ambientBrightnessThresholds,                      screenBrightnessThresholds, ambientBrightnessThresholdsIdle, -                    screenBrightnessThresholdsIdle, context, hbmController, brightnessThrottler, -                    idleModeBrightnessMapper, ambientLightHorizonShort, ambientLightHorizonLong, -                    userLux, userBrightness); +                    screenBrightnessThresholdsIdle, context, brightnessRangeController, +                    brightnessThrottler, idleModeBrightnessMapper, ambientLightHorizonShort, +                    ambientLightHorizonLong, userLux, userBrightness);          }          BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources, diff --git a/services/core/java/com/android/server/display/DisplayPowerController2.java b/services/core/java/com/android/server/display/DisplayPowerController2.java index 41e4671df1a7..2e8c3420c317 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController2.java +++ b/services/core/java/com/android/server/display/DisplayPowerController2.java @@ -376,8 +376,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal      private final ColorDisplayServiceInternal mCdsi;      private float[] mNitsRange; -    private final HighBrightnessModeController mHbmController; -    private final HighBrightnessModeMetadata mHighBrightnessModeMetadata; +    private final BrightnessRangeController mBrightnessRangeController;      private final BrightnessThrottler mBrightnessThrottler; @@ -489,7 +488,6 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal          mDisplayPowerProximityStateController = mInjector.getDisplayPowerProximityStateController(                  mWakelockController, mDisplayDeviceConfig, mHandler.getLooper(),                  () -> updatePowerState(), mDisplayId, mSensorManager); -        mHighBrightnessModeMetadata = hbmMetadata;          mDisplayStateController = new DisplayStateController(mDisplayPowerProximityStateController);          mAutomaticBrightnessStrategy = new AutomaticBrightnessStrategy(context, mDisplayId);          mTag = "DisplayPowerController2[" + mDisplayId + "]"; @@ -532,9 +530,22 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal          mSkipScreenOnBrightnessRamp = resources.getBoolean(                  R.bool.config_skipScreenOnBrightnessRamp); -        mHbmController = createHbmControllerLocked(); +        Runnable modeChangeCallback = () -> { +            sendUpdatePowerState(); +            postBrightnessChangeRunnable(); +            // TODO(b/192258832): Switch the HBMChangeCallback to a listener pattern. +            if (mAutomaticBrightnessController != null) { +                mAutomaticBrightnessController.update(); +            } +        }; +        HighBrightnessModeController hbmController = createHbmControllerLocked(hbmMetadata, +                modeChangeCallback);          mBrightnessThrottler = createBrightnessThrottlerLocked(); + +        mBrightnessRangeController = new BrightnessRangeController(hbmController, +                modeChangeCallback); +          mDisplayBrightnessController =                  new DisplayBrightnessController(context, null,                          mDisplayId, mLogicalDisplay.getDisplayInfoLocked().brightnessDefault, @@ -848,17 +859,8 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal                      mBrightnessRampIncreaseMaxTimeMillis,                      mBrightnessRampDecreaseMaxTimeMillis);          } -        mHbmController.setHighBrightnessModeMetadata(hbmMetadata); -        mHbmController.resetHbmData(info.width, info.height, token, info.uniqueId, -                mDisplayDeviceConfig.getHighBrightnessModeData(), -                new HighBrightnessModeController.HdrBrightnessDeviceConfig() { -                    @Override -                    public float getHdrBrightnessFromSdr( -                            float sdrBrightness, float maxDesiredHdrSdrRatio) { -                        return mDisplayDeviceConfig.getHdrBrightnessFromSdr( -                                sdrBrightness, maxDesiredHdrSdrRatio); -                    } -                }); + +        mBrightnessRangeController.loadFromConfig(hbmMetadata, token, info, mDisplayDeviceConfig);          mBrightnessThrottler.loadThermalBrightnessThrottlingDataFromDisplayDeviceConfig(                  mDisplayDeviceConfig.getThermalBrightnessThrottlingDataMapByThrottlingId(),                  mThermalBrightnessThrottlingDataId, mUniqueDisplayId); @@ -1076,7 +1078,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal                      darkeningLightDebounce, autoBrightnessResetAmbientLuxAfterWarmUp,                      ambientBrightnessThresholds, screenBrightnessThresholds,                      ambientBrightnessThresholdsIdle, screenBrightnessThresholdsIdle, mContext, -                    mHbmController, mBrightnessThrottler, mIdleModeBrightnessMapper, +                    mBrightnessRangeController, mBrightnessThrottler, mIdleModeBrightnessMapper,                      mDisplayDeviceConfig.getAmbientHorizonShort(),                      mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userBrightness);              mDisplayBrightnessController.setAutomaticBrightnessController( @@ -1180,7 +1182,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal      /** Clean up all resources that are accessed via the {@link #mHandler} thread. */      private void cleanupHandlerThreadAfterStop() {          mDisplayPowerProximityStateController.cleanup(); -        mHbmController.stop(); +        mBrightnessRangeController.stop();          mBrightnessThrottler.stop();          mHandler.removeCallbacksAndMessages(null); @@ -1295,7 +1297,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal                  && (mAutomaticBrightnessStrategy.getAutoBrightnessAdjustmentChanged()                  || userSetBrightnessChanged); -        mHbmController.setAutoBrightnessEnabled(mAutomaticBrightnessStrategy +        mBrightnessRangeController.setAutoBrightnessEnabled(mAutomaticBrightnessStrategy                  .shouldUseAutoBrightness()                  ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED                  : AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED); @@ -1452,7 +1454,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal          // here instead of having HbmController listen to the brightness setting because certain          // brightness sources (such as an app override) are not saved to the setting, but should be          // reflected in HBM calculations. -        mHbmController.onBrightnessChanged(brightnessState, unthrottledBrightnessState, +        mBrightnessRangeController.onBrightnessChanged(brightnessState, unthrottledBrightnessState,                  mBrightnessThrottler.getBrightnessMaxReason());          // Animate the screen brightness when the screen is on or dozing. @@ -1509,13 +1511,14 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal              float sdrAnimateValue = animateValue;              // TODO(b/216365040): The decision to prevent HBM for HDR in low power mode should be              // done in HighBrightnessModeController. -            if (mHbmController.getHighBrightnessMode() == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR +            if (mBrightnessRangeController.getHighBrightnessMode() +                    == BrightnessInfo.HIGH_BRIGHTNESS_MODE_HDR                      && (mBrightnessReasonTemp.getModifier() & BrightnessReason.MODIFIER_DIMMED) == 0                      && (mBrightnessReasonTemp.getModifier() & BrightnessReason.MODIFIER_LOW_POWER)                      == 0) {                  // We want to scale HDR brightness level with the SDR level, we also need to restore                  // SDR brightness immediately when entering dim or low power mode. -                animateValue = mHbmController.getHdrBrightnessValue(); +                animateValue = mBrightnessRangeController.getHdrBrightnessValue();              }              final float currentBrightness = mPowerState.getScreenBrightness(); @@ -1579,8 +1582,8 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal          mTempBrightnessEvent.setBrightness(brightnessState);          mTempBrightnessEvent.setPhysicalDisplayId(mUniqueDisplayId);          mTempBrightnessEvent.setReason(mBrightnessReason); -        mTempBrightnessEvent.setHbmMax(mHbmController.getCurrentBrightnessMax()); -        mTempBrightnessEvent.setHbmMode(mHbmController.getHighBrightnessMode()); +        mTempBrightnessEvent.setHbmMax(mBrightnessRangeController.getCurrentBrightnessMax()); +        mTempBrightnessEvent.setHbmMode(mBrightnessRangeController.getHighBrightnessMode());          mTempBrightnessEvent.setFlags(mTempBrightnessEvent.getFlags()                  | (mIsRbcActive ? BrightnessEvent.FLAG_RBC : 0)                  | (mPowerRequest.lowPowerMode ? BrightnessEvent.FLAG_LOW_POWER_MODE : 0)); @@ -1750,9 +1753,11 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal      private boolean saveBrightnessInfo(float brightness, float adjustedBrightness) {          synchronized (mCachedBrightnessInfo) { -            final float minBrightness = Math.min(mHbmController.getCurrentBrightnessMin(), +            final float minBrightness = Math.min( +                    mBrightnessRangeController.getCurrentBrightnessMin(),                      mBrightnessThrottler.getBrightnessCap()); -            final float maxBrightness = Math.min(mHbmController.getCurrentBrightnessMax(), +            final float maxBrightness = Math.min( +                    mBrightnessRangeController.getCurrentBrightnessMax(),                      mBrightnessThrottler.getBrightnessCap());              boolean changed = false; @@ -1770,10 +1775,10 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal                              maxBrightness);              changed |=                      mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.hbmMode, -                            mHbmController.getHighBrightnessMode()); +                            mBrightnessRangeController.getHighBrightnessMode());              changed |=                      mCachedBrightnessInfo.checkAndSetFloat(mCachedBrightnessInfo.hbmTransitionPoint, -                            mHbmController.getTransitionPoint()); +                            mBrightnessRangeController.getTransitionPoint());              changed |=                      mCachedBrightnessInfo.checkAndSetInt(mCachedBrightnessInfo.brightnessMaxReason,                              mBrightnessThrottler.getBrightnessMaxReason()); @@ -1783,10 +1788,13 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal      }      void postBrightnessChangeRunnable() { -        mHandler.post(mOnBrightnessChangeRunnable); +        if (!mHandler.hasCallbacks(mOnBrightnessChangeRunnable)) { +            mHandler.post(mOnBrightnessChangeRunnable); +        }      } -    private HighBrightnessModeController createHbmControllerLocked() { +    private HighBrightnessModeController createHbmControllerLocked( +            HighBrightnessModeMetadata hbmMetadata, Runnable modeChangeCallback) {          final DisplayDevice device = mLogicalDisplay.getPrimaryDisplayDeviceLocked();          final DisplayDeviceConfig ddConfig = device.getDisplayDeviceConfig();          final IBinder displayToken = @@ -1798,22 +1806,9 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal          final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();          return new HighBrightnessModeController(mHandler, info.width, info.height, displayToken,                  displayUniqueId, PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX, hbmData, -                new HighBrightnessModeController.HdrBrightnessDeviceConfig() { -                    @Override -                    public float getHdrBrightnessFromSdr( -                            float sdrBrightness, float maxDesiredHdrSdrRatio) { -                        return mDisplayDeviceConfig.getHdrBrightnessFromSdr( -                                sdrBrightness, maxDesiredHdrSdrRatio); -                    } -                }, -                () -> { -                    sendUpdatePowerState(); -                    postBrightnessChangeRunnable(); -                    // TODO(b/192258832): Switch the HBMChangeCallback to a listener pattern. -                    if (mAutomaticBrightnessController != null) { -                        mAutomaticBrightnessController.update(); -                    } -                }, mHighBrightnessModeMetadata, mContext); +                (sdrBrightness, maxDesiredHdrSdrRatio) -> +                        mDisplayDeviceConfig.getHdrBrightnessFromSdr(sdrBrightness, +                                maxDesiredHdrSdrRatio), modeChangeCallback, hbmMetadata, mContext);      }      private BrightnessThrottler createBrightnessThrottlerLocked() { @@ -1960,8 +1955,8 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal          if (Float.isNaN(value)) {              value = PowerManager.BRIGHTNESS_MIN;          } -        return MathUtils.constrain(value, -                mHbmController.getCurrentBrightnessMin(), mHbmController.getCurrentBrightnessMax()); +        return MathUtils.constrain(value, mBrightnessRangeController.getCurrentBrightnessMin(), +                mBrightnessRangeController.getCurrentBrightnessMax());      }      private void animateScreenBrightness(float target, float sdrTarget, float rate) { @@ -2195,7 +2190,7 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal      @Override      public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux) { -        mHbmController.onAmbientLuxChange(ambientLux); +        mBrightnessRangeController.onAmbientLuxChange(ambientLux);          if (nits < 0) {              mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness);          } else { @@ -2374,8 +2369,8 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal          dumpRbcEvents(pw); -        if (mHbmController != null) { -            mHbmController.dump(pw); +        if (mBrightnessRangeController != null) { +            mBrightnessRangeController.dump(pw);          }          if (mBrightnessThrottler != null) { @@ -2840,7 +2835,8 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal                  HysteresisLevels screenBrightnessThresholds,                  HysteresisLevels ambientBrightnessThresholdsIdle,                  HysteresisLevels screenBrightnessThresholdsIdle, Context context, -                HighBrightnessModeController hbmController, BrightnessThrottler brightnessThrottler, +                BrightnessRangeController brightnessModeController, +                BrightnessThrottler brightnessThrottler,                  BrightnessMappingStrategy idleModeBrightnessMapper, int ambientLightHorizonShort,                  int ambientLightHorizonLong, float userLux, float userBrightness) {              return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor, @@ -2849,9 +2845,9 @@ final class DisplayPowerController2 implements AutomaticBrightnessController.Cal                      brighteningLightDebounceConfig, darkeningLightDebounceConfig,                      resetAmbientLuxAfterWarmUpConfig, ambientBrightnessThresholds,                      screenBrightnessThresholds, ambientBrightnessThresholdsIdle, -                    screenBrightnessThresholdsIdle, context, hbmController, brightnessThrottler, -                    idleModeBrightnessMapper, ambientLightHorizonShort, ambientLightHorizonLong, -                    userLux, userBrightness); +                    screenBrightnessThresholdsIdle, context, brightnessModeController, +                    brightnessThrottler, idleModeBrightnessMapper, ambientLightHorizonShort, +                    ambientLightHorizonLong, userLux, userBrightness);          }          BrightnessMappingStrategy getInteractiveModeBrightnessMapper(Resources resources, diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index c7c0fab6140d..7701bc6271ae 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -701,11 +701,15 @@ final class LocalDisplayAdapter extends DisplayAdapter {                          maxDisplayMode == null ? mInfo.width : maxDisplayMode.getPhysicalWidth();                  final int maxHeight =                          maxDisplayMode == null ? mInfo.height : maxDisplayMode.getPhysicalHeight(); -                mInfo.displayCutout = DisplayCutout.fromResourcesRectApproximation(res, -                        mInfo.uniqueId, maxWidth, maxHeight, mInfo.width, mInfo.height); -                mInfo.roundedCorners = RoundedCorners.fromResources( -                        res, mInfo.uniqueId, maxWidth, maxHeight, mInfo.width, mInfo.height); +                // We cannot determine cutouts and rounded corners of external displays. +                if (mStaticDisplayInfo.isInternal) { +                    mInfo.displayCutout = DisplayCutout.fromResourcesRectApproximation(res, +                            mInfo.uniqueId, maxWidth, maxHeight, mInfo.width, mInfo.height); +                    mInfo.roundedCorners = RoundedCorners.fromResources( +                            res, mInfo.uniqueId, maxWidth, maxHeight, mInfo.width, mInfo.height); +                } +                  mInfo.installOrientation = mStaticDisplayInfo.installOrientation;                  mInfo.displayShape = DisplayShape.fromResources( diff --git a/services/core/java/com/android/server/display/NormalBrightnessModeController.java b/services/core/java/com/android/server/display/NormalBrightnessModeController.java new file mode 100644 index 000000000000..dbabc2441224 --- /dev/null +++ b/services/core/java/com/android/server/display/NormalBrightnessModeController.java @@ -0,0 +1,94 @@ +/* + * 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.server.display; + +import android.annotation.NonNull; +import android.os.PowerManager; + +import com.android.server.display.DisplayDeviceConfig.BrightnessLimitMapType; + +import java.util.HashMap; +import java.util.Map; + +/** + * Limits brightness for normal-brightness mode, based on ambient lux + **/ +class NormalBrightnessModeController { +    @NonNull +    private Map<BrightnessLimitMapType, Map<Float, Float>> mMaxBrightnessLimits = new HashMap<>(); +    private float mAmbientLux = Float.MAX_VALUE; +    private boolean mAutoBrightnessEnabled = false; + +    // brightness limit in normal brightness mode, based on ambient lux. +    private float mMaxBrightness = PowerManager.BRIGHTNESS_MAX; + +    boolean onAmbientLuxChange(float ambientLux) { +        mAmbientLux = ambientLux; +        return recalculateMaxBrightness(); +    } + +    boolean setAutoBrightnessState(int state) { +        boolean isEnabled = state == AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED; +        if (isEnabled != mAutoBrightnessEnabled) { +            mAutoBrightnessEnabled = isEnabled; +            return recalculateMaxBrightness(); +        } +        return false; +    } + +    float getCurrentBrightnessMax() { +        return mMaxBrightness; +    } + +    boolean resetNbmData( +            @NonNull Map<BrightnessLimitMapType, Map<Float, Float>> maxBrightnessLimits) { +        mMaxBrightnessLimits = maxBrightnessLimits; +        return recalculateMaxBrightness(); +    } + +    private boolean recalculateMaxBrightness() { +        float foundAmbientBoundary = Float.MAX_VALUE; +        float foundMaxBrightness = PowerManager.BRIGHTNESS_MAX; + +        Map<Float, Float> maxBrightnessPoints = null; + +        if (mAutoBrightnessEnabled) { +            maxBrightnessPoints = mMaxBrightnessLimits.get(BrightnessLimitMapType.ADAPTIVE); +        } + +        if (maxBrightnessPoints == null) { +            maxBrightnessPoints = mMaxBrightnessLimits.get(BrightnessLimitMapType.DEFAULT); +        } + +        if (maxBrightnessPoints != null) { +            for (Map.Entry<Float, Float> brightnessPoint : maxBrightnessPoints.entrySet()) { +                float ambientBoundary = brightnessPoint.getKey(); +                // find ambient lux upper boundary closest to current ambient lux +                if (ambientBoundary > mAmbientLux && ambientBoundary < foundAmbientBoundary) { +                    foundMaxBrightness = brightnessPoint.getValue(); +                    foundAmbientBoundary = ambientBoundary; +                } +            } +        } + +        if (mMaxBrightness != foundMaxBrightness) { +            mMaxBrightness = foundMaxBrightness; +            return true; +        } +        return false; +    } +} diff --git a/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java b/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java index b4613a76c751..efb36227c7bd 100644 --- a/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java +++ b/services/core/java/com/android/server/power/batterysaver/BatterySaverPolicy.java @@ -171,7 +171,7 @@ public class BatterySaverPolicy extends ContentObserver implements              true,  /* disableAod */              true,  /* disableLaunchBoost */              true,  /* disableOptionalSensors */ -            true,  /* disableVibration */ +            false,  /* disableVibration */              false, /* enableAdjustBrightness */              false, /* enableDataSaver */              true,  /* enableFirewall */ diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd index f96ca582c28f..7104a80c668d 100644 --- a/services/core/xsd/display-device-config/display-device-config.xsd +++ b/services/core/xsd/display-device-config/display-device-config.xsd @@ -46,6 +46,8 @@                      <xs:annotation name="nonnull"/>                      <xs:annotation name="final"/>                  </xs:element> +                <xs:element type="luxThrottling" name="luxThrottling" minOccurs="0" +                            maxOccurs="1"/>                  <xs:element type="highBrightnessMode" name="highBrightnessMode" minOccurs="0"                              maxOccurs="1"/>                  <xs:element type="displayQuirks" name="quirks" minOccurs="0" maxOccurs="1"/> @@ -137,6 +139,39 @@          </xs:sequence>      </xs:complexType> +    <xs:complexType name="luxThrottling"> +        <xs:sequence> +            <xs:element name="brightnessLimitMap" type="brightnessLimitMap" +                        maxOccurs="unbounded"> +                <xs:annotation name="nonnull"/> +                <xs:annotation name="final"/> +            </xs:element> +        </xs:sequence> +    </xs:complexType> + +    <xs:complexType name="brightnessLimitMap"> +        <xs:sequence> +            <xs:element name="type" type="PredefinedBrightnessLimitNames"> +                <xs:annotation name="nonnull"/> +                <xs:annotation name="final"/> +            </xs:element> +            <!-- lux level from light sensor to screen brightness recommended max value map. +            Screen brightness recommended max value is to highBrightnessMode.transitionPoint and must be below that --> +            <xs:element name="map" type="nonNegativeFloatToFloatMap"> +                <xs:annotation name="nonnull"/> +                <xs:annotation name="final"/> +            </xs:element> +        </xs:sequence> +    </xs:complexType> + +    <!-- Predefined type names as defined by DisplayDeviceConfig.BrightnessLimitMapType --> +    <xs:simpleType  name="PredefinedBrightnessLimitNames"> +        <xs:restriction base="xs:string"> +            <xs:enumeration value="default"/> +            <xs:enumeration value="adaptive"/> +        </xs:restriction> +    </xs:simpleType> +      <xs:complexType name="highBrightnessMode">          <xs:all>              <xs:element name="transitionPoint" type="nonNegativeDecimal" minOccurs="1" @@ -575,4 +610,27 @@              <xs:annotation name="final"/>          </xs:element>      </xs:complexType> + +    <!-- generic types --> +    <xs:complexType name="nonNegativeFloatToFloatPoint"> +        <xs:sequence> +            <xs:element name="first" type="nonNegativeDecimal"> +                <xs:annotation name="nonnull"/> +                <xs:annotation name="final"/> +            </xs:element> +            <xs:element name="second" type="nonNegativeDecimal"> +                <xs:annotation name="nonnull"/> +                <xs:annotation name="final"/> +            </xs:element> +        </xs:sequence> +    </xs:complexType> + +    <xs:complexType name="nonNegativeFloatToFloatMap"> +        <xs:sequence> +            <xs:element name="point" type="nonNegativeFloatToFloatPoint" maxOccurs="unbounded"> +                <xs:annotation name="nonnull"/> +                <xs:annotation name="final"/> +            </xs:element> +        </xs:sequence> +    </xs:complexType>  </xs:schema> diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt index ad6434e0c545..507c9dccda59 100644 --- a/services/core/xsd/display-device-config/schema/current.txt +++ b/services/core/xsd/display-device-config/schema/current.txt @@ -26,6 +26,14 @@ package com.android.server.display.config {      method public final java.util.List<com.android.server.display.config.DisplayBrightnessPoint> getDisplayBrightnessPoint();    } +  public class BrightnessLimitMap { +    ctor public BrightnessLimitMap(); +    method @NonNull public final com.android.server.display.config.NonNegativeFloatToFloatMap getMap(); +    method @NonNull public final com.android.server.display.config.PredefinedBrightnessLimitNames getType(); +    method public final void setMap(@NonNull com.android.server.display.config.NonNegativeFloatToFloatMap); +    method public final void setType(@NonNull com.android.server.display.config.PredefinedBrightnessLimitNames); +  } +    public class BrightnessThresholds {      ctor public BrightnessThresholds();      method public final com.android.server.display.config.ThresholdPoints getBrightnessThresholdPoints(); @@ -89,6 +97,7 @@ package com.android.server.display.config {      method public final com.android.server.display.config.Thresholds getDisplayBrightnessChangeThresholdsIdle();      method public com.android.server.display.config.HighBrightnessMode getHighBrightnessMode();      method public final com.android.server.display.config.SensorDetails getLightSensor(); +    method public com.android.server.display.config.LuxThrottling getLuxThrottling();      method @Nullable public final String getName();      method public final com.android.server.display.config.SensorDetails getProxSensor();      method public com.android.server.display.config.DisplayQuirks getQuirks(); @@ -115,6 +124,7 @@ package com.android.server.display.config {      method public final void setDisplayBrightnessChangeThresholdsIdle(com.android.server.display.config.Thresholds);      method public void setHighBrightnessMode(com.android.server.display.config.HighBrightnessMode);      method public final void setLightSensor(com.android.server.display.config.SensorDetails); +    method public void setLuxThrottling(com.android.server.display.config.LuxThrottling);      method public final void setName(@Nullable String);      method public final void setProxSensor(com.android.server.display.config.SensorDetails);      method public void setQuirks(com.android.server.display.config.DisplayQuirks); @@ -173,6 +183,11 @@ package com.android.server.display.config {      method public java.util.List<java.math.BigInteger> getItem();    } +  public class LuxThrottling { +    ctor public LuxThrottling(); +    method @NonNull public final java.util.List<com.android.server.display.config.BrightnessLimitMap> getBrightnessLimitMap(); +  } +    public class NitsMap {      ctor public NitsMap();      method public String getInterpolation(); @@ -180,6 +195,19 @@ package com.android.server.display.config {      method public void setInterpolation(String);    } +  public class NonNegativeFloatToFloatMap { +    ctor public NonNegativeFloatToFloatMap(); +    method @NonNull public final java.util.List<com.android.server.display.config.NonNegativeFloatToFloatPoint> getPoint(); +  } + +  public class NonNegativeFloatToFloatPoint { +    ctor public NonNegativeFloatToFloatPoint(); +    method @NonNull public final java.math.BigDecimal getFirst(); +    method @NonNull public final java.math.BigDecimal getSecond(); +    method public final void setFirst(@NonNull java.math.BigDecimal); +    method public final void setSecond(@NonNull java.math.BigDecimal); +  } +    public class Point {      ctor public Point();      method @NonNull public final java.math.BigDecimal getNits(); @@ -188,6 +216,12 @@ package com.android.server.display.config {      method public final void setValue(@NonNull java.math.BigDecimal);    } +  public enum PredefinedBrightnessLimitNames { +    method public String getRawName(); +    enum_constant public static final com.android.server.display.config.PredefinedBrightnessLimitNames _default; +    enum_constant public static final com.android.server.display.config.PredefinedBrightnessLimitNames adaptive; +  } +    public class RefreshRateConfigs {      ctor public RefreshRateConfigs();      method public final java.math.BigInteger getDefaultPeakRefreshRate(); diff --git a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java index 32243f04f6e8..212a243c6a9e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/app/GameManagerServiceTests.java @@ -2221,7 +2221,7 @@ public class GameManagerServiceTests {          String[] packages = {mPackageName};          when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);          gameManagerService.mUidObserver.onUidStateChanged( -                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0); +                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0);          verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true);      } @@ -2238,12 +2238,12 @@ public class GameManagerServiceTests {          doAnswer(inv -> powerState.put(inv.getArgument(0), inv.getArgument(1)))                  .when(mMockPowerManager).setPowerMode(anyInt(), anyBoolean());          gameManagerService.mUidObserver.onUidStateChanged( -                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0); +                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0);          assertTrue(powerState.get(Mode.GAME));          gameManagerService.mUidObserver.onUidStateChanged(                  DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0);          gameManagerService.mUidObserver.onUidStateChanged( -                somePackageId, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0); +                somePackageId, ActivityManager.PROCESS_STATE_TOP, 0, 0);          assertTrue(powerState.get(Mode.GAME));          gameManagerService.mUidObserver.onUidStateChanged(                  somePackageId, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0); @@ -2260,13 +2260,13 @@ public class GameManagerServiceTests {          int somePackageId = DEFAULT_PACKAGE_UID + 1;          when(mMockPackageManager.getPackagesForUid(somePackageId)).thenReturn(packages2);          gameManagerService.mUidObserver.onUidStateChanged( -                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0); +                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0);          gameManagerService.mUidObserver.onUidStateChanged( -                somePackageId, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0); +                somePackageId, ActivityManager.PROCESS_STATE_TOP, 0, 0);          gameManagerService.mUidObserver.onUidStateChanged( -                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0); +                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);          gameManagerService.mUidObserver.onUidStateChanged( -                somePackageId, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0); +                somePackageId, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);          verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, true);          verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, false);      } @@ -2277,9 +2277,9 @@ public class GameManagerServiceTests {          String[] packages = {mPackageName};          when(mMockPackageManager.getPackagesForUid(DEFAULT_PACKAGE_UID)).thenReturn(packages);          gameManagerService.mUidObserver.onUidStateChanged( -                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0); +                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TOP, 0, 0);          gameManagerService.mUidObserver.onUidStateChanged( -                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0); +                DEFAULT_PACKAGE_UID, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0, 0);          verify(mMockPowerManager, times(1)).setPowerMode(Mode.GAME, false);      } diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java index 170076098b7d..4f98dca660df 100644 --- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java +++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerController2Test.java @@ -815,7 +815,7 @@ public final class DisplayPowerController2Test {                  any(HysteresisLevels.class),                  any(HysteresisLevels.class),                  eq(mContext), -                any(HighBrightnessModeController.class), +                any(BrightnessRangeController.class),                  any(BrightnessThrottler.class),                  isNull(),                  anyInt(), @@ -1064,7 +1064,7 @@ public final class DisplayPowerController2Test {                  HysteresisLevels screenBrightnessThresholds,                  HysteresisLevels ambientBrightnessThresholdsIdle,                  HysteresisLevels screenBrightnessThresholdsIdle, Context context, -                HighBrightnessModeController hbmController, +                BrightnessRangeController brightnessRangeController,                  BrightnessThrottler brightnessThrottler,                  BrightnessMappingStrategy idleModeBrightnessMapper,                  int ambientLightHorizonShort, int ambientLightHorizonLong, float userLux, diff --git a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java index 5c0810fdca44..a93640b592cd 100644 --- a/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/display/DisplayPowerControllerTest.java @@ -819,7 +819,7 @@ public final class DisplayPowerControllerTest {                  any(HysteresisLevels.class),                  any(HysteresisLevels.class),                  eq(mContext), -                any(HighBrightnessModeController.class), +                any(BrightnessRangeController.class),                  any(BrightnessThrottler.class),                  isNull(),                  anyInt(), @@ -1038,7 +1038,7 @@ public final class DisplayPowerControllerTest {                  HysteresisLevels screenBrightnessThresholds,                  HysteresisLevels ambientBrightnessThresholdsIdle,                  HysteresisLevels screenBrightnessThresholdsIdle, Context context, -                HighBrightnessModeController hbmController, +                BrightnessRangeController brightnessRangeController,                  BrightnessThrottler brightnessThrottler,                  BrightnessMappingStrategy idleModeBrightnessMapper,                  int ambientLightHorizonShort, int ambientLightHorizonLong, float userLux, diff --git a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java index b7dbaf93b9e2..f89f73c98cfd 100644 --- a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java @@ -37,6 +37,7 @@ import static org.mockito.Mockito.when;  import android.content.Context;  import android.content.res.Resources;  import android.content.res.TypedArray; +import android.graphics.Rect;  import android.os.Binder;  import android.os.Handler;  import android.os.IBinder; @@ -1009,6 +1010,72 @@ public class LocalDisplayAdapterTest {                  0.001f);      } +    @Test +    public void test_getDisplayDeviceInfoLocked_internalDisplay_usesCutoutAndCorners() +            throws Exception { +        setupCutoutAndRoundedCorners(); +        FakeDisplay display = new FakeDisplay(PORT_A); +        display.info.isInternal = true; +        setUpDisplay(display); +        updateAvailableDisplays(); +        mAdapter.registerLocked(); +        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); +        assertThat(mListener.addedDisplays.size()).isEqualTo(1); +        DisplayDevice displayDevice = mListener.addedDisplays.get(0); + +        // Turn on / initialize +        Runnable changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0, +                0); +        changeStateRunnable.run(); +        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); +        mListener.changedDisplays.clear(); + +        DisplayDeviceInfo info = displayDevice.getDisplayDeviceInfoLocked(); + +        assertThat(info.displayCutout).isNotNull(); +        assertThat(info.displayCutout.getBoundingRectTop()).isEqualTo(new Rect(507, 33, 573, 99)); +        assertThat(info.roundedCorners).isNotNull(); +        assertThat(info.roundedCorners.getRoundedCorner(0).getRadius()).isEqualTo(5); +    } + +    @Test public void test_getDisplayDeviceInfoLocked_externalDisplay_doesNotUseCutoutOrCorners() +            throws Exception { +        setupCutoutAndRoundedCorners(); +        FakeDisplay display = new FakeDisplay(PORT_A); +        display.info.isInternal = false; +        setUpDisplay(display); +        updateAvailableDisplays(); +        mAdapter.registerLocked(); +        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); +        assertThat(mListener.addedDisplays.size()).isEqualTo(1); +        DisplayDevice displayDevice = mListener.addedDisplays.get(0); + +        // Turn on / initialize +        Runnable changeStateRunnable = displayDevice.requestDisplayStateLocked(Display.STATE_ON, 0, +                0); +        changeStateRunnable.run(); +        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS); +        mListener.changedDisplays.clear(); + +        DisplayDeviceInfo info = displayDevice.getDisplayDeviceInfoLocked(); + +        assertThat(info.displayCutout).isNull(); +        assertThat(info.roundedCorners).isNull(); +    } + +    private void setupCutoutAndRoundedCorners() { +        String sampleCutout = "M 507,66\n" +                + "a 33,33 0 1 0 66,0 33,33 0 1 0 -66,0\n" +                + "Z\n" +                + "@left\n"; +        // Setup some default cutout +        when(mMockedResources.getString( +                com.android.internal.R.string.config_mainBuiltInDisplayCutout)) +                .thenReturn(sampleCutout); +        when(mMockedResources.getDimensionPixelSize( +                com.android.internal.R.dimen.rounded_corner_radius)).thenReturn(5); +    } +      private void assertDisplayDpi(DisplayDeviceInfo info, int expectedPort,                                    float expectedXdpi,                                    float expectedYDpi, diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java index 662477ddbbe9..8346050c3c89 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java @@ -26,6 +26,7 @@ import static android.hardware.biometrics.BiometricPrompt.DISMISSED_REASON_NEGAT  import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_CALLED;  import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_STARTED;  import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTH_STARTED_UI_SHOWING; +import static com.android.server.biometrics.BiometricServiceStateProto.STATE_ERROR_PENDING_SYSUI;  import static junit.framework.Assert.assertEquals;  import static junit.framework.Assert.assertFalse; @@ -48,6 +49,7 @@ import android.app.admin.DevicePolicyManager;  import android.app.trust.ITrustManager;  import android.content.Context;  import android.content.res.Resources; +import android.hardware.biometrics.BiometricConstants;  import android.hardware.biometrics.BiometricManager.Authenticators;  import android.hardware.biometrics.BiometricsProtoEnums;  import android.hardware.biometrics.ComponentInfoInternal; @@ -104,6 +106,7 @@ public class AuthSessionTest {      @Mock private KeyStore mKeyStore;      @Mock private AuthSession.ClientDeathReceiver mClientDeathReceiver;      @Mock private BiometricFrameworkStatsLogger mBiometricFrameworkStatsLogger; +    @Mock BiometricSensorPrivacy mBiometricSensorPrivacy;      private Random mRandom;      private IBinder mToken; @@ -210,6 +213,40 @@ public class AuthSessionTest {      }      @Test +    public void testOnErrorReceived_lockoutError() throws RemoteException { +        setupFingerprint(0 /* id */, FingerprintSensorProperties.TYPE_REAR); +        setupFace(1 /* id */, false /* confirmationAlwaysRequired */, +                mock(IBiometricAuthenticator.class)); +        final AuthSession session = createAuthSession(mSensors, +                false /* checkDevicePolicyManager */, +                Authenticators.BIOMETRIC_STRONG, +                TEST_REQUEST_ID, +                0 /* operationId */, +                0 /* userId */); +        session.goToInitialState(); +        for (BiometricSensor sensor : session.mPreAuthInfo.eligibleSensors) { +            assertEquals(BiometricSensor.STATE_WAITING_FOR_COOKIE, sensor.getSensorState()); +            session.onCookieReceived( +                    session.mPreAuthInfo.eligibleSensors.get(sensor.id).getCookie()); +        } +        assertTrue(session.allCookiesReceived()); +        assertEquals(STATE_AUTH_STARTED, session.getState()); + +        // Either of strong sensor's lockout should cancel both sensors. +        final int cookie1 = session.mPreAuthInfo.eligibleSensors.get(0).getCookie(); +        session.onErrorReceived(0, cookie1, BiometricConstants.BIOMETRIC_ERROR_LOCKOUT, 0); +        for (BiometricSensor sensor : session.mPreAuthInfo.eligibleSensors) { +            assertEquals(BiometricSensor.STATE_CANCELING, sensor.getSensorState()); +        } +        assertEquals(STATE_ERROR_PENDING_SYSUI, session.getState()); + +        // If the sensor is STATE_CANCELING, delayed onAuthenticationRejected() shouldn't change the +        // session state to STATE_AUTH_PAUSED. +        session.onAuthenticationRejected(1); +        assertEquals(STATE_ERROR_PENDING_SYSUI, session.getState()); +    } + +    @Test      public void testCancelReducesAppetiteForCookies() throws Exception {          setupFace(0 /* id */, false /* confirmationAlwaysRequired */,                  mock(IBiometricAuthenticator.class)); @@ -571,7 +608,8 @@ public class AuthSessionTest {                  promptInfo,                  TEST_PACKAGE,                  checkDevicePolicyManager, -                mContext); +                mContext, +                mBiometricSensorPrivacy);      }      private AuthSession createAuthSession(List<BiometricSensor> sensors, diff --git a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java index 67be37616d5f..41f7dbcb0ff5 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/BiometricServiceTest.java @@ -18,7 +18,6 @@ package com.android.server.biometrics;  import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRINT;  import static android.hardware.biometrics.BiometricManager.Authenticators; -import static android.hardware.biometrics.SensorProperties.STRENGTH_STRONG;  import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;  import static com.android.server.biometrics.BiometricServiceStateProto.STATE_AUTHENTICATED_PENDING_SYSUI; @@ -183,6 +182,10 @@ public class BiometricServiceTest {                  .thenReturn(ERROR_HW_UNAVAILABLE);          when(mResources.getString(R.string.biometric_not_recognized))                  .thenReturn(ERROR_NOT_RECOGNIZED); +        when(mResources.getString(R.string.biometric_face_not_recognized)) +                .thenReturn(ERROR_NOT_RECOGNIZED); +        when(mResources.getString(R.string.fingerprint_error_not_match)) +                .thenReturn(ERROR_NOT_RECOGNIZED);          when(mResources.getString(R.string.biometric_error_user_canceled))                  .thenReturn(ERROR_USER_CANCELED); @@ -903,6 +906,45 @@ public class BiometricServiceTest {      }      @Test +    public void testMultiBiometricAuth_whenLockoutTimed_sendsErrorAndModality() +            throws Exception { +        testMultiBiometricAuth_whenLockout(LockoutTracker.LOCKOUT_TIMED, +                BiometricPrompt.BIOMETRIC_ERROR_LOCKOUT); +    } + +    @Test +    public void testMultiBiometricAuth_whenLockoutPermanent_sendsErrorAndModality() +            throws Exception { +        testMultiBiometricAuth_whenLockout(LockoutTracker.LOCKOUT_PERMANENT, +                BiometricPrompt.BIOMETRIC_ERROR_LOCKOUT_PERMANENT); +    } + +    private void testMultiBiometricAuth_whenLockout(@LockoutTracker.LockoutMode int lockoutMode, +            int biometricPromptError) throws Exception { +        final int[] modalities = new int[] { +                TYPE_FINGERPRINT, +                BiometricAuthenticator.TYPE_FACE, +        }; + +        final int[] strengths = new int[] { +                Authenticators.BIOMETRIC_STRONG, +                Authenticators.BIOMETRIC_STRONG, +        }; +        setupAuthForMultiple(modalities, strengths); + +        when(mFingerprintAuthenticator.getLockoutModeForUser(anyInt())) +                .thenReturn(lockoutMode); +        when(mFaceAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(false); +        invokeAuthenticate(mBiometricService.mImpl, mReceiver1, +                false /* requireConfirmation */, null /* authenticators */); +        waitForIdle(); + +        // The lockout error should be sent, instead of ERROR_NONE_ENROLLED. See b/286923477. +        verify(mReceiver1).onError(eq(TYPE_FINGERPRINT), +                eq(biometricPromptError), eq(0) /* vendorCode */); +    } + +    @Test      public void testBiometricOrCredentialAuth_whenBiometricLockout_showsCredential()              throws Exception {          when(mTrustManager.isDeviceSecure(anyInt(), anyInt())).thenReturn(true); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java new file mode 100644 index 000000000000..0c98c8d88d83 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java @@ -0,0 +1,139 @@ +/* + * 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.server.biometrics; + +import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE; +import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE; + +import static com.android.server.biometrics.sensors.LockoutTracker.LOCKOUT_NONE; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; + +import android.app.admin.DevicePolicyManager; +import android.app.trust.ITrustManager; +import android.content.Context; +import android.hardware.biometrics.BiometricManager; +import android.hardware.biometrics.IBiometricAuthenticator; +import android.hardware.biometrics.PromptInfo; +import android.os.RemoteException; +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.List; + +@Presubmit +@SmallTest +public class PreAuthInfoTest { +    @Rule +    public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + +    private static final int SENSOR_ID_FACE = 1; +    private static final String TEST_PACKAGE_NAME = "PreAuthInfoTestPackage"; + +    @Mock +    IBiometricAuthenticator mFaceAuthenticator; +    @Mock +    Context mContext; +    @Mock +    ITrustManager mTrustManager; +    @Mock +    DevicePolicyManager mDevicePolicyManager; +    @Mock +    BiometricService.SettingObserver mSettingObserver; +    @Mock +    BiometricSensorPrivacy mBiometricSensorPrivacyUtil; + +    @Before +    public void setup() throws RemoteException { +        when(mTrustManager.isDeviceSecure(anyInt(), anyInt())).thenReturn(true); +        when(mDevicePolicyManager.getKeyguardDisabledFeatures(any(), anyInt())) +                .thenReturn(KEYGUARD_DISABLE_FEATURES_NONE); +        when(mSettingObserver.getEnabledForApps(anyInt())).thenReturn(true); +        when(mFaceAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(true); +        when(mFaceAuthenticator.isHardwareDetected(any())).thenReturn(true); +        when(mFaceAuthenticator.getLockoutModeForUser(anyInt())) +                .thenReturn(LOCKOUT_NONE); +    } + +    @Test +    public void testFaceAuthentication_whenCameraPrivacyIsEnabled() throws Exception { +        when(mBiometricSensorPrivacyUtil.isCameraPrivacyEnabled()).thenReturn(true); + +        BiometricSensor sensor = new BiometricSensor(mContext, SENSOR_ID_FACE, TYPE_FACE, +                BiometricManager.Authenticators.BIOMETRIC_STRONG, mFaceAuthenticator) { +            @Override +            boolean confirmationAlwaysRequired(int userId) { +                return false; +            } + +            @Override +            boolean confirmationSupported() { +                return false; +            } +        }; +        PromptInfo promptInfo = new PromptInfo(); +        promptInfo.setConfirmationRequested(false /* requireConfirmation */); +        promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG); +        promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */); +        PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, +                mSettingObserver, List.of(sensor), +                0 /* userId */, promptInfo, TEST_PACKAGE_NAME, +                false /* checkDevicePolicyManager */, mContext, mBiometricSensorPrivacyUtil); + +        assertThat(preAuthInfo.eligibleSensors).isEmpty(); +    } + +    @Test +    public void testFaceAuthentication_whenCameraPrivacyIsDisabled() throws Exception { +        when(mBiometricSensorPrivacyUtil.isCameraPrivacyEnabled()).thenReturn(false); + +        BiometricSensor sensor = new BiometricSensor(mContext, SENSOR_ID_FACE, TYPE_FACE, +                BiometricManager.Authenticators.BIOMETRIC_STRONG, mFaceAuthenticator) { +            @Override +            boolean confirmationAlwaysRequired(int userId) { +                return false; +            } + +            @Override +            boolean confirmationSupported() { +                return false; +            } +        }; +        PromptInfo promptInfo = new PromptInfo(); +        promptInfo.setConfirmationRequested(false /* requireConfirmation */); +        promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG); +        promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */); +        PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, +                mSettingObserver, List.of(sensor), +                0 /* userId */, promptInfo, TEST_PACKAGE_NAME, +                false /* checkDevicePolicyManager */, mContext, mBiometricSensorPrivacyUtil); + +        assertThat(preAuthInfo.eligibleSensors).hasSize(1); +    } +} diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java index e9d82696affc..89299002a227 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerTest.java @@ -19,6 +19,8 @@ package com.android.server.biometrics.sensors;  import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED;  import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_SUCCESS; +import static com.google.common.truth.Truth.assertThat; +  import static junit.framework.Assert.assertTrue;  import static junit.framework.Assert.fail; @@ -405,6 +407,59 @@ public class BiometricSchedulerTest {          testCancelsEnrollWhenRequestId(10L, 20, false /* started */);      } +    @Test +    public void testCancelAuthenticationClientWithoutStarting() { +        final Supplier<Object> lazyDaemon = () -> mock(Object.class); +        final TestHalClientMonitor client1 = new TestHalClientMonitor(mContext, mToken, lazyDaemon); +        final ClientMonitorCallbackConverter callback = mock(ClientMonitorCallbackConverter.class); +        final TestAuthenticationClient client2 = new TestAuthenticationClient(mContext, lazyDaemon, +                mToken, callback, mBiometricContext); + +        //Schedule authentication client to the pending queue +        mScheduler.scheduleClientMonitor(client1); +        mScheduler.scheduleClientMonitor(client2); +        waitForIdle(); + +        assertThat(mScheduler.getCurrentClient()).isEqualTo(client1); + +        client2.cancel(); +        waitForIdle(); + +        assertThat(client2.isAlreadyCancelled()).isTrue(); + +        client1.getCallback().onClientFinished(client1, false); +        waitForIdle(); + +        assertThat(mScheduler.getCurrentClient()).isNull(); +    } + +    @Test +    public void testCancelAuthenticationClientWithoutStarting_whenAppCrashes() { +        final Supplier<Object> lazyDaemon = () -> mock(Object.class); +        final TestHalClientMonitor client1 = new TestHalClientMonitor(mContext, mToken, lazyDaemon); +        final ClientMonitorCallbackConverter callback = mock(ClientMonitorCallbackConverter.class); +        final TestAuthenticationClient client2 = new TestAuthenticationClient(mContext, lazyDaemon, +                mToken, callback, mBiometricContext); + +        //Schedule authentication client to the pending queue +        mScheduler.scheduleClientMonitor(client1); +        mScheduler.scheduleClientMonitor(client2); +        waitForIdle(); + +        assertThat(mScheduler.getCurrentClient()).isEqualTo(client1); + +        //App crashes +        client2.binderDied(); +        waitForIdle(); + +        assertThat(client2.isAlreadyCancelled()).isTrue(); + +        client1.getCallback().onClientFinished(client1, false); +        waitForIdle(); + +        assertThat(mScheduler.getCurrentClient()).isNull(); +    } +      private void testCancelsEnrollWhenRequestId(@Nullable Long requestId, long cancelRequestId,              boolean started) {          final Supplier<Object> lazyDaemon = () -> mock(Object.class); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java index d5d06d3b4eb8..046b01c831b5 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java @@ -16,8 +16,10 @@  package com.android.server.biometrics.sensors.face.aidl; +import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED;  import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT;  import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT; +  import static com.google.common.truth.Truth.assertThat;  import static org.mockito.ArgumentMatchers.any; @@ -205,7 +207,9 @@ public class FaceAuthenticationClientTest {          client.onAuthenticated(new Face("friendly", 1 /* faceId */, 2 /* deviceId */),                  true /* authenticated */, new ArrayList<>()); -        verify(mCancellationSignal).cancel(); +        verify(mCancellationSignal, never()).cancel(); +        verify(mClientMonitorCallbackConverter) +                .onError(anyInt(), anyInt(), eq(BIOMETRIC_ERROR_CANCELED), anyInt());      }      private FaceAuthenticationClient createClient() throws RemoteException { diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java index f8f40fe44457..c383a96d5de3 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java @@ -16,6 +16,8 @@  package com.android.server.biometrics.sensors.fingerprint.aidl; +import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED; +  import static com.google.common.truth.Truth.assertThat;  import static org.mockito.ArgumentMatchers.anyBoolean; @@ -399,7 +401,9 @@ public class FingerprintAuthenticationClientTest {          mLooper.moveTimeForward(10);          mLooper.dispatchAll(); -        verify(mCancellationSignal).cancel(); +        verify(mCancellationSignal, never()).cancel(); +        verify(mClientMonitorCallbackConverter) +                .onError(anyInt(), anyInt(), eq(BIOMETRIC_ERROR_CANCELED), anyInt());      }      private FingerprintAuthenticationClient createClient() throws RemoteException { diff --git a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java index 962e86776ea2..a6acd60f3bd7 100644 --- a/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/display/AutomaticBrightnessControllerTest.java @@ -85,7 +85,7 @@ public class AutomaticBrightnessControllerTest {      @Mock HysteresisLevels mAmbientBrightnessThresholdsIdle;      @Mock HysteresisLevels mScreenBrightnessThresholdsIdle;      @Mock Handler mNoOpHandler; -    @Mock HighBrightnessModeController mHbmController; +    @Mock BrightnessRangeController mBrightnessRangeController;      @Mock BrightnessThrottler mBrightnessThrottler;      @Before @@ -134,12 +134,15 @@ public class AutomaticBrightnessControllerTest {                  DARKENING_LIGHT_DEBOUNCE_CONFIG, RESET_AMBIENT_LUX_AFTER_WARMUP_CONFIG,                  mAmbientBrightnessThresholds, mScreenBrightnessThresholds,                  mAmbientBrightnessThresholdsIdle, mScreenBrightnessThresholdsIdle, -                mContext, mHbmController, mBrightnessThrottler, mIdleBrightnessMappingStrategy, -                AMBIENT_LIGHT_HORIZON_SHORT, AMBIENT_LIGHT_HORIZON_LONG, userLux, userBrightness +                mContext, mBrightnessRangeController, mBrightnessThrottler, +                mIdleBrightnessMappingStrategy, AMBIENT_LIGHT_HORIZON_SHORT, +                AMBIENT_LIGHT_HORIZON_LONG, userLux, userBrightness          ); -        when(mHbmController.getCurrentBrightnessMax()).thenReturn(BRIGHTNESS_MAX_FLOAT); -        when(mHbmController.getCurrentBrightnessMin()).thenReturn(BRIGHTNESS_MIN_FLOAT); +        when(mBrightnessRangeController.getCurrentBrightnessMax()).thenReturn( +                BRIGHTNESS_MAX_FLOAT); +        when(mBrightnessRangeController.getCurrentBrightnessMin()).thenReturn( +                BRIGHTNESS_MIN_FLOAT);          // Disable brightness throttling by default. Individual tests can enable it as needed.          when(mBrightnessThrottler.getBrightnessCap()).thenReturn(BRIGHTNESS_MAX_FLOAT);          when(mBrightnessThrottler.isThrottled()).thenReturn(false); diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java index 5837b21b89fd..708421d2a431 100644 --- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java +++ b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java @@ -52,6 +52,7 @@ import java.nio.file.Path;  import java.util.ArrayList;  import java.util.HashMap;  import java.util.List; +import java.util.Map;  @SmallTest  @RunWith(AndroidJUnit4.class) @@ -376,6 +377,116 @@ public final class DisplayDeviceConfigTest {          assertEquals(90, testMap.get(Temperature.THROTTLING_EMERGENCY).max, SMALL_DELTA);      } +    @Test +    public void testValidLuxThrottling() throws Exception { +        setupDisplayDeviceConfigFromDisplayConfigFile(); + +        Map<DisplayDeviceConfig.BrightnessLimitMapType, Map<Float, Float>> luxThrottlingData = +                mDisplayDeviceConfig.getLuxThrottlingData(); +        assertEquals(2, luxThrottlingData.size()); + +        Map<Float, Float> adaptiveOnBrightnessPoints = luxThrottlingData.get( +                DisplayDeviceConfig.BrightnessLimitMapType.ADAPTIVE); +        assertEquals(2, adaptiveOnBrightnessPoints.size()); +        assertEquals(0.3f, adaptiveOnBrightnessPoints.get(1000f), SMALL_DELTA); +        assertEquals(0.5f, adaptiveOnBrightnessPoints.get(5000f), SMALL_DELTA); + +        Map<Float, Float> adaptiveOffBrightnessPoints = luxThrottlingData.get( +                DisplayDeviceConfig.BrightnessLimitMapType.DEFAULT); +        assertEquals(2, adaptiveOffBrightnessPoints.size()); +        assertEquals(0.35f, adaptiveOffBrightnessPoints.get(1500f), SMALL_DELTA); +        assertEquals(0.55f, adaptiveOffBrightnessPoints.get(5500f), SMALL_DELTA); +    } + +    @Test +    public void testInvalidLuxThrottling() throws Exception { +        setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getInvalidLuxThrottling())); + +        Map<DisplayDeviceConfig.BrightnessLimitMapType, Map<Float, Float>> luxThrottlingData = +                mDisplayDeviceConfig.getLuxThrottlingData(); +        assertEquals(1, luxThrottlingData.size()); + +        Map<Float, Float> adaptiveOnBrightnessPoints = luxThrottlingData.get( +                DisplayDeviceConfig.BrightnessLimitMapType.ADAPTIVE); +        assertEquals(1, adaptiveOnBrightnessPoints.size()); +        assertEquals(0.3f, adaptiveOnBrightnessPoints.get(1000f), SMALL_DELTA); +    } + +    private String getValidLuxThrottling() { +        return "<luxThrottling>\n" +                + "    <brightnessLimitMap>\n" +                + "        <type>adaptive</type>\n" +                + "        <map>\n" +                + "            <point>" +                + "                <first>1000</first>\n" +                + "                <second>0.3</second>\n" +                + "            </point>" +                + "            <point>" +                + "                <first>5000</first>\n" +                + "                <second>0.5</second>\n" +                + "            </point>" +                + "        </map>\n" +                + "    </brightnessLimitMap>\n" +                + "    <brightnessLimitMap>\n" +                + "        <type>default</type>\n" +                + "        <map>\n" +                + "            <point>" +                + "                <first>1500</first>\n" +                + "                <second>0.35</second>\n" +                + "            </point>" +                + "            <point>" +                + "                <first>5500</first>\n" +                + "                <second>0.55</second>\n" +                + "            </point>" +                + "        </map>\n" +                + "    </brightnessLimitMap>\n" +                + "</luxThrottling>"; +    } + +    private String getInvalidLuxThrottling() { +        return "<luxThrottling>\n" +                + "    <brightnessLimitMap>\n" +                + "        <type>adaptive</type>\n" +                + "        <map>\n" +                + "            <point>" +                + "                <first>1000</first>\n" +                + "                <second>0.3</second>\n" +                + "            </point>" +                + "            <point>" // second > hbm.transitionPoint, skipped +                + "                <first>1500</first>\n" +                + "                <second>0.9</second>\n" +                + "            </point>" +                + "            <point>" // same lux value, skipped +                + "                <first>1000</first>\n" +                + "                <second>0.5</second>\n" +                + "            </point>" +                + "        </map>\n" +                + "    </brightnessLimitMap>\n" +                + "    <brightnessLimitMap>\n" // Same type, skipped +                + "        <type>adaptive</type>\n" +                + "        <map>\n" +                + "            <point>" +                + "                <first>2000</first>\n" +                + "                <second>0.35</second>\n" +                + "            </point>" +                + "            <point>" +                + "                <first>6000</first>\n" +                + "                <second>0.55</second>\n" +                + "            </point>" +                + "        </map>\n" +                + "    </brightnessLimitMap>\n" +                + "    <brightnessLimitMap>\n" // Invalid points only, skipped +                + "        <type>default</type>\n" +                + "        <map>\n" +                + "            <point>" +                + "                <first>2500</first>\n" +                + "                <second>0.99</second>\n" +                + "            </point>" +                + "        </map>\n" +                + "    </brightnessLimitMap>\n" +                + "</luxThrottling>"; +    } +      private String getRefreshThermalThrottlingMaps() {          return "<refreshRateThrottlingMap>\n"                 + "    <refreshRateThrottlingPoint>\n" @@ -405,6 +516,10 @@ public final class DisplayDeviceConfigTest {      }      private String getContent() { +        return getContent(getValidLuxThrottling()); +    } + +    private String getContent(String brightnessCapConfig) {          return "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"                  + "<displayConfiguration>\n"                  +   "<name>Example Display</name>" @@ -462,6 +577,7 @@ public final class DisplayDeviceConfigTest {                  +            "</point>\n"                  +       "</sdrHdrRatioMap>\n"                  +   "</highBrightnessMode>\n" +                + brightnessCapConfig                  +   "<screenOffBrightnessSensor>\n"                  +       "<type>sensor_12345</type>\n"                  +       "<name>Sensor 12345</name>\n" @@ -731,8 +847,12 @@ public final class DisplayDeviceConfigTest {      }      private void setupDisplayDeviceConfigFromDisplayConfigFile() throws IOException { +        setupDisplayDeviceConfigFromDisplayConfigFile(getContent()); +    } + +    private void setupDisplayDeviceConfigFromDisplayConfigFile(String content) throws IOException {          Path tempFile = Files.createTempFile("display_config", ".tmp"); -        Files.write(tempFile, getContent().getBytes(StandardCharsets.UTF_8)); +        Files.write(tempFile, content.getBytes(StandardCharsets.UTF_8));          mDisplayDeviceConfig = new DisplayDeviceConfig(mContext);          mDisplayDeviceConfig.initFromFile(tempFile.toFile());      } diff --git a/services/tests/servicestests/src/com/android/server/display/NormalBrightnessModeControllerTest.java b/services/tests/servicestests/src/com/android/server/display/NormalBrightnessModeControllerTest.java new file mode 100644 index 000000000000..c379d6b79ee7 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/display/NormalBrightnessModeControllerTest.java @@ -0,0 +1,116 @@ +/* + * 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.server.display; + +import static org.junit.Assert.assertEquals; + +import android.os.PowerManager; + +import androidx.test.filters.SmallTest; + +import com.android.internal.annotations.Keep; +import com.android.server.display.DisplayDeviceConfig.BrightnessLimitMapType; + +import com.google.common.collect.ImmutableMap; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.HashMap; +import java.util.Map; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + +@SmallTest +@RunWith(JUnitParamsRunner.class) +public class NormalBrightnessModeControllerTest { +    private static final float FLOAT_TOLERANCE = 0.001f; + +    private final NormalBrightnessModeController mController = new NormalBrightnessModeController(); + +    @Keep +    private static Object[][] brightnessData() { +        return new Object[][]{ +                // no brightness config +                {0, AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED, new HashMap<>(), +                        PowerManager.BRIGHTNESS_MAX}, +                {0, AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED, new HashMap<>(), +                        PowerManager.BRIGHTNESS_MAX}, +                {100, AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED, new HashMap<>(), +                        PowerManager.BRIGHTNESS_MAX}, +                // Auto brightness - on, config only for default +                {100, AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED, ImmutableMap.of( +                        BrightnessLimitMapType.DEFAULT, +                        ImmutableMap.of(99f, 0.1f, 101f, 0.2f) +                ), 0.2f}, +                // Auto brightness - off, config only for default +                {100, AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED, ImmutableMap.of( +                        BrightnessLimitMapType.DEFAULT, +                        ImmutableMap.of(99f, 0.1f, 101f, 0.2f) +                ), 0.2f}, +                // Auto brightness - off, config only for adaptive +                {100, AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED, ImmutableMap.of( +                        BrightnessLimitMapType.ADAPTIVE, +                        ImmutableMap.of(99f, 0.1f, 101f, 0.2f) +                ), PowerManager.BRIGHTNESS_MAX}, +                // Auto brightness - on, config only for adaptive +                {100, AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED, ImmutableMap.of( +                        BrightnessLimitMapType.ADAPTIVE, +                        ImmutableMap.of(99f, 0.1f, 101f, 0.2f) +                ), 0.2f}, +                // Auto brightness - on, config for both +                {100, AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED, ImmutableMap.of( +                        BrightnessLimitMapType.DEFAULT, +                        ImmutableMap.of(99f, 0.1f, 101f, 0.2f), +                        BrightnessLimitMapType.ADAPTIVE, +                        ImmutableMap.of(99f, 0.3f, 101f, 0.4f) +                ), 0.4f}, +                // Auto brightness - off, config for both +                {100, AutomaticBrightnessController.AUTO_BRIGHTNESS_DISABLED, ImmutableMap.of( +                        BrightnessLimitMapType.DEFAULT, +                        ImmutableMap.of(99f, 0.1f, 101f, 0.2f), +                        BrightnessLimitMapType.ADAPTIVE, +                        ImmutableMap.of(99f, 0.3f, 101f, 0.4f) +                ), 0.2f}, +                // Auto brightness - on, config for both, ambient high +                {1000, AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED, ImmutableMap.of( +                        BrightnessLimitMapType.DEFAULT, +                        ImmutableMap.of(1000f, 0.1f, 2000f, 0.2f), +                        BrightnessLimitMapType.ADAPTIVE, +                        ImmutableMap.of(99f, 0.3f, 101f, 0.4f) +                ), PowerManager.BRIGHTNESS_MAX}, +        }; +    } + +    @Test +    @Parameters(method = "brightnessData") +    public void testReturnsCorrectMaxBrightness(float ambientLux, int autoBrightnessState, +            Map<BrightnessLimitMapType, Map<Float, Float>> maxBrightnessConfig, +            float expectedBrightness) { +        setupController(ambientLux, autoBrightnessState, maxBrightnessConfig); + +        assertEquals(expectedBrightness, mController.getCurrentBrightnessMax(), FLOAT_TOLERANCE); +    } + +    private void setupController(float ambientLux, int autoBrightnessState, +            Map<BrightnessLimitMapType, Map<Float, Float>> maxBrightnessConfig) { +        mController.onAmbientLuxChange(ambientLux); +        mController.setAutoBrightnessState(autoBrightnessState); +        mController.resetNbmData(maxBrightnessConfig); +    } +} diff --git a/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java b/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java index aa6ee09e0179..0b13f9a35c1f 100644 --- a/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java +++ b/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySaverPolicyTest.java @@ -52,7 +52,7 @@ public class BatterySaverPolicyTest extends AndroidTestCase {      private static final int SOUND_TRIGGER_MODE = 0; // SOUND_TRIGGER_MODE_ALL_ENABLED      private static final int DEFAULT_SOUND_TRIGGER_MODE =              PowerManager.SOUND_TRIGGER_MODE_CRITICAL_ONLY; -    private static final String BATTERY_SAVER_CONSTANTS = "disable_vibration=true," +    private static final String BATTERY_SAVER_CONSTANTS = "disable_vibration=false,"              + "advertise_is_enabled=true,"              + "disable_animation=false,"              + "enable_firewall=true," @@ -117,7 +117,7 @@ public class BatterySaverPolicyTest extends AndroidTestCase {      @SmallTest      public void testGetBatterySaverPolicy_PolicyVibration_DefaultValueCorrect() { -        testServiceDefaultValue_On(ServiceType.VIBRATION); +        testServiceDefaultValue_Off(ServiceType.VIBRATION);      }      @SmallTest @@ -211,7 +211,7 @@ public class BatterySaverPolicyTest extends AndroidTestCase {      private void verifyBatterySaverConstantsUpdated() {          final PowerSaveState vibrationState =                  mBatterySaverPolicy.getBatterySaverPolicy(ServiceType.VIBRATION); -        assertThat(vibrationState.batterySaverEnabled).isTrue(); +        assertThat(vibrationState.batterySaverEnabled).isFalse();          final PowerSaveState animationState =                  mBatterySaverPolicy.getBatterySaverPolicy(ServiceType.ANIMATION); diff --git a/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLoggingLatencyTest.java b/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLoggingLatencyTest.java index 6a1674b7df8e..63b8e1710b50 100644 --- a/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLoggingLatencyTest.java +++ b/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLoggingLatencyTest.java @@ -38,13 +38,16 @@ import android.os.BatteryStatsInternal;  import android.os.Process;  import android.os.RemoteException; +import androidx.test.filters.FlakyTest;  import androidx.test.platform.app.InstrumentationRegistry;  import com.android.internal.util.FakeLatencyTracker;  import org.junit.After;  import org.junit.Before; +import org.junit.Rule;  import org.junit.Test; +import org.junit.rules.Timeout;  import org.junit.runner.RunWith;  import org.junit.runners.JUnit4;  import org.mockito.ArgumentCaptor; @@ -52,8 +55,12 @@ import org.mockito.Mock;  import org.mockito.MockitoAnnotations;  @RunWith(JUnit4.class) +@FlakyTest(bugId = 275746222)  public class SoundTriggerMiddlewareLoggingLatencyTest { +    @Rule +    public Timeout mGlobalTimeout = Timeout.seconds(30); +      private FakeLatencyTracker mLatencyTracker;      @Mock      private BatteryStatsInternal mBatteryStatsInternal; diff --git a/tests/UiBench/Android.bp b/tests/UiBench/Android.bp index 90e61c52da68..0d2f2ef46cab 100644 --- a/tests/UiBench/Android.bp +++ b/tests/UiBench/Android.bp @@ -24,6 +24,5 @@ android_test {          "androidx.recyclerview_recyclerview",          "androidx.leanback_leanback",      ], -    certificate: "platform",      test_suites: ["device-tests"],  } diff --git a/tests/UiBench/AndroidManifest.xml b/tests/UiBench/AndroidManifest.xml index 47211c5fbad1..4fc6ec71f29c 100644 --- a/tests/UiBench/AndroidManifest.xml +++ b/tests/UiBench/AndroidManifest.xml @@ -18,7 +18,6 @@  <manifest xmlns:android="http://schemas.android.com/apk/res/android"       xmlns:tools="http://schemas.android.com/tools"       package="com.android.test.uibench"> -    <uses-permission android:name="android.permission.INJECT_EVENTS" />      <application android:allowBackup="false"           android:theme="@style/Theme.AppCompat.Light.DarkActionBar" diff --git a/tests/UiBench/src/com/android/test/uibench/EditTextTypeActivity.java b/tests/UiBench/src/com/android/test/uibench/EditTextTypeActivity.java index 1b2c3c60ffd4..06b65a7f9bbf 100644 --- a/tests/UiBench/src/com/android/test/uibench/EditTextTypeActivity.java +++ b/tests/UiBench/src/com/android/test/uibench/EditTextTypeActivity.java @@ -15,15 +15,11 @@   */  package com.android.test.uibench; -import android.app.Instrumentation; +import android.content.Intent;  import android.os.Bundle; -import android.os.Looper; -import android.os.MessageQueue; -import androidx.appcompat.app.AppCompatActivity; -import android.view.KeyEvent;  import android.widget.EditText; -import java.util.concurrent.Semaphore; +import androidx.appcompat.app.AppCompatActivity;  /**   * Note: currently incomplete, complexity of input continuously grows, instead of looping @@ -32,7 +28,13 @@ import java.util.concurrent.Semaphore;   * Simulates typing continuously into an EditText.   */  public class EditTextTypeActivity extends AppCompatActivity { -    Thread mThread; + +    /** +     * Broadcast action: Used to notify UiBenchEditTextTypingMicrobenchmark test when the +     * test activity was paused. +     */ +    private static final String ACTION_CANCEL_TYPING_CALLBACK = +            "com.android.uibench.action.CANCEL_TYPING_CALLBACK";      private static String sSeedText = "";      static { @@ -46,9 +48,6 @@ public class EditTextTypeActivity extends AppCompatActivity {          sSeedText = builder.toString();      } -    final Object mLock = new Object(); -    boolean mShouldStop = false; -      @Override      protected void onCreate(Bundle savedInstanceState) {          super.onCreate(savedInstanceState); @@ -56,55 +55,13 @@ public class EditTextTypeActivity extends AppCompatActivity {          EditText editText = new EditText(this);          editText.setText(sSeedText);          setContentView(editText); - -        final Instrumentation instrumentation = new Instrumentation(); -        final Semaphore sem = new Semaphore(0); -        MessageQueue.IdleHandler handler = new MessageQueue.IdleHandler() { -            @Override -            public boolean queueIdle() { -                // TODO: consider other signaling approaches -                sem.release(); -                return true; -            } -        }; -        Looper.myQueue().addIdleHandler(handler); -        synchronized (mLock) { -            mShouldStop = false; -        } -        mThread = new Thread(new Runnable() { -            int codes[] = { KeyEvent.KEYCODE_H, KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_L, -                    KeyEvent.KEYCODE_L, KeyEvent.KEYCODE_O, KeyEvent.KEYCODE_SPACE }; -            int i = 0; -            @Override -            public void run() { -                while (true) { -                    try { -                        sem.acquire(); -                    } catch (InterruptedException e) { -                        // TODO, maybe -                    } -                    int code = codes[i % codes.length]; -                    if (i % 100 == 99) code = KeyEvent.KEYCODE_ENTER; - -                    synchronized (mLock) { -                        if (mShouldStop) break; -                    } - -                    // TODO: bit of a race here, since the event can arrive after pause/stop. -                    // (Can't synchronize on key send, since it's synchronous.) -                    instrumentation.sendKeyDownUpSync(code); -                    i++; -                } -            } -        }); -        mThread.start();      }      @Override      protected void onPause() { -        synchronized (mLock) { -            mShouldStop = true; -        } +        // Cancel the typing when the test activity was paused. +        sendBroadcast(new Intent(ACTION_CANCEL_TYPING_CALLBACK).addFlags( +                Intent.FLAG_RECEIVER_FOREGROUND | Intent.FLAG_RECEIVER_REGISTERED_ONLY));          super.onPause();      }  } diff --git a/wifi/java/src/android/net/wifi/nl80211/InstantWifi.java b/wifi/java/src/android/net/wifi/nl80211/InstantWifi.java new file mode 100644 index 000000000000..433e88c58851 --- /dev/null +++ b/wifi/java/src/android/net/wifi/nl80211/InstantWifi.java @@ -0,0 +1,318 @@ +/* + * 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 android.net.wifi.nl80211; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.AlarmManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Handler; +import android.os.PowerManager; +import android.os.SystemClock; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +/** + * @hide + */ +public class InstantWifi { +    private static final String INSTANT_WIFI_TAG = "InstantWifi"; +    private static final int OVERRIDED_SCAN_CONNECTION_TIMEOUT_MS = 1000; +    private static final int WIFI_NETWORK_EXPIRED_MS = 7 * 24 * 60 * 60 * 1000; // a week +    private static final String NO_CONNECTION_TIMEOUT_ALARM_TAG = +            INSTANT_WIFI_TAG + " No Connection Timeout"; + +    private Context mContext; +    private AlarmManager mAlarmManager; +    private Handler mEventHandler; +    private ConnectivityManager mConnectivityManager; +    private WifiManager mWifiManager; +    private PowerManager mPowerManager; +    private long mLastWifiOnSinceBootMs; +    private long mLastScreenOnSinceBootMs; +    private boolean mIsWifiConnected = false; +    private boolean mScreenOn = false; +    private boolean mWifiEnabled = false; +    private boolean mIsNoConnectionAlarmSet = false; +    private ArrayList<WifiNetwork> mConnectedWifiNetworkList = new ArrayList<>(); +    private AlarmManager.OnAlarmListener mNoConnectionTimeoutCallback = +            new AlarmManager.OnAlarmListener() { +                public void onAlarm() { +                    Log.i(INSTANT_WIFI_TAG, "Timed out waiting for wifi connection"); +                    mIsNoConnectionAlarmSet = false; +                    mWifiManager.startScan(); +                } +            }; + +    public InstantWifi(Context context, AlarmManager alarmManager, Handler eventHandler) { +        mContext = context; +        mAlarmManager = alarmManager; +        mEventHandler = eventHandler; +        mWifiManager = mContext.getSystemService(WifiManager.class); +        mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); +        mConnectivityManager.registerNetworkCallback( +                new NetworkRequest.Builder() +                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) +                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) +                .build(), new WifiNetworkCallback()); +        // System power service was initialized before wifi nl80211 service. +        mPowerManager = mContext.getSystemService(PowerManager.class); +        IntentFilter screenEventfilter = new IntentFilter(); +        screenEventfilter.addAction(Intent.ACTION_SCREEN_ON); +        screenEventfilter.addAction(Intent.ACTION_SCREEN_OFF); +        mContext.registerReceiver( +                new BroadcastReceiver() { +                @Override +                    public void onReceive(Context context, Intent intent) { +                        String action = intent.getAction(); +                        if (action.equals(Intent.ACTION_SCREEN_ON)) { +                            if (!mScreenOn) { +                                mLastScreenOnSinceBootMs = getMockableElapsedRealtime(); +                            } +                            mScreenOn = true; +                        } else if (action.equals(Intent.ACTION_SCREEN_OFF)) { +                            mScreenOn = false; +                        } +                        Log.d(INSTANT_WIFI_TAG, "mScreenOn is changed to " + mScreenOn); +                    } +                }, screenEventfilter, null, mEventHandler); +        mScreenOn = mPowerManager.isInteractive(); +        mContext.registerReceiver( +                new BroadcastReceiver() { +                    @Override +                    public void onReceive(Context context, Intent intent) { +                        int state = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, +                                WifiManager.WIFI_STATE_UNKNOWN); +                        mWifiEnabled = state == WifiManager.WIFI_STATE_ENABLED; +                        if (mWifiEnabled) { +                            mLastWifiOnSinceBootMs = getMockableElapsedRealtime(); +                        } +                        Log.d(INSTANT_WIFI_TAG, "mWifiEnabled is changed to " + mWifiEnabled); +                    } +                }, +                new IntentFilter(WifiManager.WIFI_STATE_CHANGED_ACTION), +                null, mEventHandler); +    } + +    @VisibleForTesting +    protected long getMockableElapsedRealtime() { +        return SystemClock.elapsedRealtime(); +    } + +    private class WifiNetwork { +        private final int mNetId; +        private Set<Integer> mConnectedFrequencies = new HashSet<Integer>(); +        private int[] mLastTwoConnectedFrequencies = new int[2]; +        private long mLastConnectedTimeMillis; +        WifiNetwork(int netId) { +            mNetId = netId; +        } + +        public int getNetId() { +            return mNetId; +        } + +        public boolean addConnectedFrequency(int channelFrequency) { +            mLastConnectedTimeMillis = getMockableElapsedRealtime(); +            if (mLastTwoConnectedFrequencies[0] != channelFrequency +                    && mLastTwoConnectedFrequencies[1] != channelFrequency) { +                mLastTwoConnectedFrequencies[0] = mLastTwoConnectedFrequencies[1]; +                mLastTwoConnectedFrequencies[1] = channelFrequency; +            } +            return mConnectedFrequencies.add(channelFrequency); +        } + +        public Set<Integer> getConnectedFrequencies() { +            return mConnectedFrequencies; +        } + +        public int[] getLastTwoConnectedFrequencies() { +            if ((getMockableElapsedRealtime() - mLastConnectedTimeMillis) +                    > WIFI_NETWORK_EXPIRED_MS) { +                return new int[0]; +            } +            return mLastTwoConnectedFrequencies; +        } + +        public long getLastConnectedTimeMillis() { +            return mLastConnectedTimeMillis; +        } +    } + +    private class WifiNetworkCallback extends NetworkCallback { +        @Override +        public void onAvailable(@NonNull Network network) { +        } + +        @Override +        public void onCapabilitiesChanged(Network network, +                NetworkCapabilities networkCapabilities) { +            if (networkCapabilities != null && network != null) { +                WifiInfo wifiInfo = (WifiInfo) networkCapabilities.getTransportInfo(); +                if (wifiInfo == null || mWifiManager == null) { +                    return; +                } +                WifiConfiguration config = mWifiManager.getPrivilegedConnectedNetwork(); +                if (config == null) { +                    return; +                } +                final int currentNetworkId = config.networkId; +                final int connectecFrequency = wifiInfo.getFrequency(); +                if (connectecFrequency < 0 || currentNetworkId < 0) { +                    return; +                } +                mIsWifiConnected = true; +                if (mIsNoConnectionAlarmSet) { +                    mAlarmManager.cancel(mNoConnectionTimeoutCallback); +                } +                Log.d(INSTANT_WIFI_TAG, "Receive Wifi is connected, freq =  " + connectecFrequency +                        + " and currentNetworkId : " + currentNetworkId +                        + ", wifiinfo = " + wifiInfo); +                boolean isExist = false; +                for (WifiNetwork wifiNetwork : mConnectedWifiNetworkList) { +                    if (wifiNetwork.getNetId() == currentNetworkId) { +                        if (wifiNetwork.addConnectedFrequency(connectecFrequency)) { +                            Log.d(INSTANT_WIFI_TAG, "Update connected frequency: " +                                    + connectecFrequency + " to Network currentNetworkId : " +                                    + currentNetworkId); +                        } +                        isExist = true; +                    } +                } +                if (!isExist) { +                    WifiNetwork currentNetwork = new WifiNetwork(currentNetworkId); +                    currentNetwork.addConnectedFrequency(connectecFrequency); +                    if (mConnectedWifiNetworkList.size() < 5) { +                        mConnectedWifiNetworkList.add(currentNetwork); +                    } else { +                        ArrayList<WifiNetwork> lastConnectedWifiNetworkList = new ArrayList<>(); +                        WifiNetwork legacyNetwork = mConnectedWifiNetworkList.get(0); +                        for (WifiNetwork connectedNetwork : mConnectedWifiNetworkList) { +                            if (connectedNetwork.getNetId() == legacyNetwork.getNetId()) { +                                continue; +                            } +                            // Keep the used recently network in the last connected list +                            if (connectedNetwork.getLastConnectedTimeMillis() +                                    > legacyNetwork.getLastConnectedTimeMillis()) { +                                lastConnectedWifiNetworkList.add(connectedNetwork); +                            } else { +                                lastConnectedWifiNetworkList.add(legacyNetwork); +                                legacyNetwork = connectedNetwork; +                            } +                        } +                        mConnectedWifiNetworkList = lastConnectedWifiNetworkList; +                    } +                } +            } +        } + +        @Override +        public void onLost(@NonNull Network network) { +            mIsWifiConnected = false; +        } +    } + +    /** +     * Returns whether or not the scan freqs should be overrided by using predicted channels. +     */ +    public boolean isUsePredictedScanningChannels() { +        if (mIsWifiConnected || mConnectedWifiNetworkList.size() == 0 +                || !mWifiManager.isWifiEnabled() || !mPowerManager.isInteractive()) { +            return false; +        } +        if (!mWifiEnabled || !mScreenOn) { +            Log.d(INSTANT_WIFI_TAG, "WiFi/Screen State mis-match, run instant Wifi anyway!"); +            return true; +        } +        return (((getMockableElapsedRealtime() - mLastWifiOnSinceBootMs) +                        < OVERRIDED_SCAN_CONNECTION_TIMEOUT_MS) +                || ((getMockableElapsedRealtime() - mLastScreenOnSinceBootMs) +                        < OVERRIDED_SCAN_CONNECTION_TIMEOUT_MS)); +    } + +    /** +     * Overrides the frequenies in SingleScanSetting +     * +     * @param settings the SingleScanSettings will be overrided. +     * @param freqs new frequencies of SingleScanSettings +     */ +    @Nullable +    public void overrideFreqsForSingleScanSettingsIfNecessary( +            @Nullable SingleScanSettings settings, @Nullable Set<Integer> freqs) { +        if (!isUsePredictedScanningChannels() || settings == null || freqs == null +                || freqs.size() == 0) { +            return; +        } +        if (settings.channelSettings == null) { +            settings.channelSettings = new ArrayList<>(); +        } else { +            settings.channelSettings.clear(); +        } +        for (int freq : freqs) { +            if (freq > 0) { +                ChannelSettings channel = new ChannelSettings(); +                channel.frequency = freq; +                settings.channelSettings.add(channel); +            } +        } +        // Monitor connection after last override scan request. +        if (mIsNoConnectionAlarmSet) { +            mAlarmManager.cancel(mNoConnectionTimeoutCallback); +        } +        mAlarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, +                getMockableElapsedRealtime() + OVERRIDED_SCAN_CONNECTION_TIMEOUT_MS, +                NO_CONNECTION_TIMEOUT_ALARM_TAG, mNoConnectionTimeoutCallback, mEventHandler); +        mIsNoConnectionAlarmSet = true; +    } + +    /** +     * Returns the predicted scanning chcnnels set. +     */ +    @NonNull +    public Set<Integer> getPredictedScanningChannels() { +        Set<Integer> predictedScanChannels = new HashSet<>(); +        if (!isUsePredictedScanningChannels()) { +            Log.d(INSTANT_WIFI_TAG, "Drop, size: " + mConnectedWifiNetworkList.size()); +            return predictedScanChannels; +        } +        for (WifiNetwork network : mConnectedWifiNetworkList) { +            for (int connectedFrequency : network.getLastTwoConnectedFrequencies()) { +                if (connectedFrequency > 0) { +                    predictedScanChannels.add(connectedFrequency); +                    Log.d(INSTANT_WIFI_TAG, "Add channel: " + connectedFrequency +                            + " to predicted channel"); +                } +            } +        } +        return predictedScanChannels; +    } +} diff --git a/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java b/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java index 2a199d27a60e..ca12c4cb2020 100644 --- a/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java +++ b/wifi/java/src/android/net/wifi/nl80211/WifiNl80211Manager.java @@ -105,6 +105,8 @@ public class WifiNl80211Manager {      // Cached wificond binder handlers.      private IWificond mWificond; +    private Context mContext; +    private InstantWifi mInstantWifi;      private WificondEventHandler mWificondEventHandler = new WificondEventHandler();      private HashMap<String, IClientInterface> mClientInterfaces = new HashMap<>();      private HashMap<String, IApInterface> mApInterfaces = new HashMap<>(); @@ -174,6 +176,12 @@ public class WifiNl80211Manager {      /** @hide */      @VisibleForTesting +    protected InstantWifi getInstantWifiMockable() { +        return mInstantWifi; +    } + +    /** @hide */ +    @VisibleForTesting      public class WificondEventHandler extends IWificondEventCallback.Stub {          private Map<CountryCodeChangedListener, Executor> mCountryCodeChangedListenerHolder =                  new HashMap<>(); @@ -419,6 +427,7 @@ public class WifiNl80211Manager {      public WifiNl80211Manager(Context context) {          mAlarmManager = context.getSystemService(AlarmManager.class);          mEventHandler = new Handler(context.getMainLooper()); +        mContext = context;      }      /** @@ -434,6 +443,7 @@ public class WifiNl80211Manager {          if (mWificond == null) {              Log.e(TAG, "Failed to get reference to wificond");          } +        mContext = context;      }      /** @hide */ @@ -441,6 +451,7 @@ public class WifiNl80211Manager {      public WifiNl80211Manager(Context context, IWificond wificond) {          this(context);          mWificond = wificond; +        mContext = context;      }      /** @hide */ @@ -744,6 +755,9 @@ public class WifiNl80211Manager {              Log.e(TAG, "Failed to refresh wificond scanner due to remote exception");          } +        if (getInstantWifiMockable() == null) { +            mInstantWifi = new InstantWifi(mContext, mAlarmManager, mEventHandler); +        }          return true;      } @@ -1071,6 +1085,10 @@ public class WifiNl80211Manager {          if (settings == null) {              return false;          } +        if (getInstantWifiMockable() != null) { +            getInstantWifiMockable().overrideFreqsForSingleScanSettingsIfNecessary(settings, +                    getInstantWifiMockable().getPredictedScanningChannels()); +        }          try {              return scannerImpl.scan(settings);          } catch (RemoteException e1) { @@ -1115,6 +1133,10 @@ public class WifiNl80211Manager {          if (settings == null) {              return WifiScanner.REASON_INVALID_ARGS;          } +        if (getInstantWifiMockable() != null) { +            getInstantWifiMockable().overrideFreqsForSingleScanSettingsIfNecessary(settings, +                    getInstantWifiMockable().getPredictedScanningChannels()); +        }          try {              int status = scannerImpl.scanRequest(settings);              return toFrameworkScanStatusCode(status); diff --git a/wifi/tests/src/android/net/wifi/nl80211/InstantWifiTest.java b/wifi/tests/src/android/net/wifi/nl80211/InstantWifiTest.java new file mode 100644 index 000000000000..ebff0e28082e --- /dev/null +++ b/wifi/tests/src/android/net/wifi/nl80211/InstantWifiTest.java @@ -0,0 +1,256 @@ +/* + * 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 android.net.wifi.nl80211; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.AlarmManager; +import android.app.test.TestAlarmManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.net.ConnectivityManager.NetworkCallback; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Handler; +import android.os.IPowerManager; +import android.os.IThermalService; +import android.os.PowerManager; +import android.os.test.TestLooper; + +import androidx.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.HashSet; +import java.util.Set; + +/** + * Unit tests for {@link android.net.wifi.nl80211.InstantWifi}. + */ +@SmallTest +public class InstantWifiTest { +    @Mock private Context mContext; +    @Mock private ConnectivityManager mMockConnectivityManager; +    @Mock private WifiManager mMockWifiManager; +    @Mock private Network mMockWifiNetwork; +    @Mock private WifiInfo mMockWifiInfo; +    @Mock private WifiConfiguration mMockWifiConfiguration; +    @Mock private IPowerManager mPowerManagerService; +    private InstantWifi mInstantWifi; +    private TestLooper mLooper; +    private Handler mHandler; +    private TestAlarmManager mTestAlarmManager; +    private AlarmManager mAlarmManager; +    private PowerManager mMockPowerManager; + +    private final ArgumentCaptor<NetworkCallback> mWifiNetworkCallbackCaptor = +            ArgumentCaptor.forClass(NetworkCallback.class); +    private final ArgumentCaptor<BroadcastReceiver> mScreenBroadcastReceiverCaptor = +            ArgumentCaptor.forClass(BroadcastReceiver.class); +    private final ArgumentCaptor<BroadcastReceiver> mWifiStateBroadcastReceiverCaptor = +            ArgumentCaptor.forClass(BroadcastReceiver.class); + +    private static final int TEST_NETWORK_ID = 1; +    private static final int TEST_24G_FREQUENCY = 2412; +    private static final int TEST_5G_FREQUENCY = 5745; +    private long mTimeOffsetMs = 0; + +    private class InstantWifiSpy extends InstantWifi { +        InstantWifiSpy(Context context, AlarmManager alarmManager, Handler handler) { +            super(context, alarmManager, handler); +        } + +        @Override +        protected long getMockableElapsedRealtime() { +            return mTimeOffsetMs; +        } +    } + +    @Before +    public void setUp() throws Exception { +        MockitoAnnotations.initMocks(this); + +        mLooper = new TestLooper(); +        mHandler = new Handler(mLooper.getLooper()); + +        mTestAlarmManager = new TestAlarmManager(); +        mAlarmManager = mTestAlarmManager.getAlarmManager(); +        when(mContext.getSystemServiceName(AlarmManager.class)).thenReturn(Context.ALARM_SERVICE); +        when(mContext.getSystemService(AlarmManager.class)).thenReturn(mAlarmManager); +        when(mContext.getSystemServiceName(WifiManager.class)).thenReturn(Context.WIFI_SERVICE); +        when(mContext.getSystemService(WifiManager.class)).thenReturn(mMockWifiManager); +        when(mContext.getSystemServiceName(ConnectivityManager.class)) +                .thenReturn(Context.CONNECTIVITY_SERVICE); +        when(mContext.getSystemService(ConnectivityManager.class)) +                .thenReturn(mMockConnectivityManager); +        mMockPowerManager = new PowerManager(mContext, mPowerManagerService, +                mock(IThermalService.class), mHandler); +        when(mContext.getSystemServiceName(PowerManager.class)).thenReturn(Context.POWER_SERVICE); +        when(mContext.getSystemService(PowerManager.class)).thenReturn(mMockPowerManager); +        when(mPowerManagerService.isInteractive()).thenReturn(true); + +        doReturn(mMockWifiInfo).when(mMockWifiInfo).makeCopy(anyLong()); +        mTimeOffsetMs = 0; +        mInstantWifi = new InstantWifiSpy(mContext, mAlarmManager, mHandler); +        verifyInstantWifiInitialization(); +    } + +    private void verifyInstantWifiInitialization() { +        verify(mMockConnectivityManager).registerNetworkCallback(any(), +                mWifiNetworkCallbackCaptor.capture()); +        verify(mContext).registerReceiver(mScreenBroadcastReceiverCaptor.capture(), +                argThat((IntentFilter filter) -> +                        filter.hasAction(Intent.ACTION_SCREEN_ON) +                                && filter.hasAction(Intent.ACTION_SCREEN_OFF)), eq(null), any()); + +        verify(mContext).registerReceiver(mWifiStateBroadcastReceiverCaptor.capture(), +                argThat((IntentFilter filter) -> +                        filter.hasAction(WifiManager.WIFI_STATE_CHANGED_ACTION)), eq(null), any()); +    } + +    private void mockWifiConnectedEvent(int networkId, int connectedFrequency) { +        // Send wifi connected event +        NetworkCapabilities mockWifiNetworkCapabilities = +                new NetworkCapabilities.Builder().setTransportInfo(mMockWifiInfo).build(); +        mMockWifiConfiguration.networkId = networkId; +        when(mMockWifiManager.getPrivilegedConnectedNetwork()).thenReturn(mMockWifiConfiguration); +        when(mMockWifiInfo.getFrequency()).thenReturn(connectedFrequency); +        mWifiNetworkCallbackCaptor.getValue().onCapabilitiesChanged(mMockWifiNetwork, +                mockWifiNetworkCapabilities); +        mLooper.dispatchAll(); +    } + +    private void mockWifiOnScreenOnBroadcast(boolean isWifiOn, boolean isScreenOn) +            throws Exception { +        // Send Wifi On broadcast +        Intent wifiOnIntent = new Intent(WifiManager.WIFI_STATE_CHANGED_ACTION); +        wifiOnIntent.putExtra(WifiManager.EXTRA_WIFI_STATE, +                isWifiOn ? WifiManager.WIFI_STATE_ENABLED : WifiManager.WIFI_STATE_DISABLED); +        mWifiStateBroadcastReceiverCaptor.getValue().onReceive(mContext, wifiOnIntent); +        mLooper.dispatchAll(); +        // Send Screen On broadcast +        Intent screenOnIntent = +                new Intent(isScreenOn ? Intent.ACTION_SCREEN_ON : Intent.ACTION_SCREEN_OFF); +        mScreenBroadcastReceiverCaptor.getValue().onReceive(mContext, screenOnIntent); +        mLooper.dispatchAll(); +        when(mMockWifiManager.isWifiEnabled()).thenReturn(isWifiOn); +        when(mPowerManagerService.isInteractive()).thenReturn(isScreenOn); +    } + +    @Test +    public void testisUsePredictedScanningChannels() throws Exception { +        assertFalse(mInstantWifi.isUsePredictedScanningChannels()); +        mockWifiOnScreenOnBroadcast(true /* isWifiOn */, false /* isScreenOn */); +        assertFalse(mInstantWifi.isUsePredictedScanningChannels()); +        mockWifiOnScreenOnBroadcast(false /* isWifiOn */, true /* isScreenOn */); +        assertFalse(mInstantWifi.isUsePredictedScanningChannels()); +        mockWifiOnScreenOnBroadcast(true /* isWifiOn */, true /* isScreenOn */); +        assertFalse(mInstantWifi.isUsePredictedScanningChannels()); +        // Send wifi connected event +        mockWifiConnectedEvent(TEST_NETWORK_ID, TEST_24G_FREQUENCY); +        assertFalse(mInstantWifi.isUsePredictedScanningChannels()); +        // Send wifi disconnect +        mWifiNetworkCallbackCaptor.getValue().onLost(mMockWifiNetwork); +        assertTrue(mInstantWifi.isUsePredictedScanningChannels()); +        // Shift time to make it expired +        mTimeOffsetMs = 1100; +        assertFalse(mInstantWifi.isUsePredictedScanningChannels()); +    } + +    @Test +    public void testGetPredictedScanningChannels() throws Exception { +        mockWifiOnScreenOnBroadcast(true /* isWifiOn */, true /* isScreenOn */); +        // Send wifi connected event on T0 +        mockWifiConnectedEvent(TEST_NETWORK_ID, TEST_24G_FREQUENCY); +        // Send wifi disconnect +        mWifiNetworkCallbackCaptor.getValue().onLost(mMockWifiNetwork); +        assertTrue(mInstantWifi.isUsePredictedScanningChannels()); +        assertTrue(mInstantWifi.getPredictedScanningChannels().contains(TEST_24G_FREQUENCY)); +        mTimeOffsetMs += 1000; // T1 = 1000 ms +        // Send wifi connected event +        mockWifiConnectedEvent(TEST_NETWORK_ID + 1, TEST_5G_FREQUENCY); +        // Send wifi disconnect +        mWifiNetworkCallbackCaptor.getValue().onLost(mMockWifiNetwork); +        // isUsePredictedScanningChannels is false since wifi on & screen on is expired +        assertFalse(mInstantWifi.isUsePredictedScanningChannels()); +        // Override the Wifi On & Screen on time +        mockWifiOnScreenOnBroadcast(true /* isWifiOn */, true /* isScreenOn */); +        assertTrue(mInstantWifi.getPredictedScanningChannels().contains(TEST_5G_FREQUENCY)); +        mTimeOffsetMs += 7 * 24 * 60 * 60 * 1000; // Make T0 expired +        // Override the Wifi On & Screen on time +        mockWifiOnScreenOnBroadcast(true /* isWifiOn */, true /* isScreenOn */); +        assertFalse(mInstantWifi.getPredictedScanningChannels().contains(TEST_24G_FREQUENCY)); +        assertTrue(mInstantWifi.getPredictedScanningChannels().contains(TEST_5G_FREQUENCY)); +    } + +    @Test +    public void testOverrideFreqsForSingleScanSettings() throws Exception { +        mockWifiOnScreenOnBroadcast(true /* isWifiOn */, true /* isScreenOn */); +        // Send wifi connected event +        mockWifiConnectedEvent(TEST_NETWORK_ID, TEST_24G_FREQUENCY); +        assertFalse(mInstantWifi.isUsePredictedScanningChannels()); +        // Send wifi disconnect +        mWifiNetworkCallbackCaptor.getValue().onLost(mMockWifiNetwork); +        assertTrue(mInstantWifi.isUsePredictedScanningChannels()); + +        final ArgumentCaptor<AlarmManager.OnAlarmListener> alarmListenerCaptor = +                ArgumentCaptor.forClass(AlarmManager.OnAlarmListener.class); +        doNothing().when(mAlarmManager).set(anyInt(), anyLong(), any(), +                alarmListenerCaptor.capture(), any()); +        Set<Integer> testFreqs = Set.of( +                TEST_24G_FREQUENCY, TEST_5G_FREQUENCY); +        SingleScanSettings testSingleScanSettings = new SingleScanSettings(); +        mInstantWifi.overrideFreqsForSingleScanSettingsIfNecessary( +                testSingleScanSettings, new HashSet<Integer>()); +        mInstantWifi.overrideFreqsForSingleScanSettingsIfNecessary( +                testSingleScanSettings, null); +        mInstantWifi.overrideFreqsForSingleScanSettingsIfNecessary(null, null); +        verify(mAlarmManager, never()).set(anyInt(), anyLong(), any(), any(), any()); +        mInstantWifi.overrideFreqsForSingleScanSettingsIfNecessary(testSingleScanSettings, +                testFreqs); +        verify(mAlarmManager).set(anyInt(), anyLong(), any(), any(), any()); +        Set<Integer> overridedFreqs = new HashSet<Integer>(); +        for (ChannelSettings channel : testSingleScanSettings.channelSettings) { +            overridedFreqs.add(channel.frequency); +        } +        assertEquals(testFreqs, overridedFreqs); +        alarmListenerCaptor.getValue().onAlarm(); +        verify(mMockWifiManager).startScan(); +    } +} diff --git a/wifi/tests/src/android/net/wifi/nl80211/WifiNl80211ManagerTest.java b/wifi/tests/src/android/net/wifi/nl80211/WifiNl80211ManagerTest.java index 362eb1425a30..f12818fa64f9 100644 --- a/wifi/tests/src/android/net/wifi/nl80211/WifiNl80211ManagerTest.java +++ b/wifi/tests/src/android/net/wifi/nl80211/WifiNl80211ManagerTest.java @@ -18,6 +18,7 @@ package android.net.wifi.nl80211;  import static org.junit.Assert.assertEquals;  import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals;  import static org.junit.Assert.assertNotNull;  import static org.junit.Assert.assertNull;  import static org.junit.Assert.assertTrue; @@ -26,6 +27,7 @@ import static org.mockito.ArgumentMatchers.eq;  import static org.mockito.Matchers.argThat;  import static org.mockito.Mockito.any;  import static org.mockito.Mockito.anyLong; +import static org.mockito.Mockito.doAnswer;  import static org.mockito.Mockito.doNothing;  import static org.mockito.Mockito.doThrow;  import static org.mockito.Mockito.mock; @@ -37,6 +39,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;  import static org.mockito.Mockito.when;  import android.app.AlarmManager; +import android.app.test.MockAnswerUtil.AnswerWithArguments;  import android.app.test.TestAlarmManager;  import android.content.Context;  import android.net.MacAddress; @@ -104,6 +107,9 @@ public class WifiNl80211ManagerTest {      private WifiNl80211Manager.CountryCodeChangedListener mCountryCodeChangedListener2;      @Mock      private Context mContext; +    @Mock +    private InstantWifi mMockInstantWifi; +      private TestLooper mLooper;      private TestAlarmManager mTestAlarmManager;      private AlarmManager mAlarmManager; @@ -167,6 +173,17 @@ public class WifiNl80211ManagerTest {              0x00, 0x00      }; +    private class WifiNl80211ManagerSpy extends WifiNl80211Manager { +        WifiNl80211ManagerSpy(Context context, IWificond wificond) { +            super(context, wificond); +        } + +        @Override +        protected InstantWifi getInstantWifiMockable() { +            return mMockInstantWifi; +        } +    } +      @Before      public void setUp() throws Exception {          // Setup mocks for successful WificondControl operation. Failure case mocks should be @@ -181,6 +198,8 @@ public class WifiNl80211ManagerTest {          mLooper = new TestLooper();          when(mContext.getMainLooper()).thenReturn(mLooper.getLooper()); +        doNothing().when(mMockInstantWifi).overrideFreqsForSingleScanSettingsIfNecessary( +                any(), any());          when(mWificond.asBinder()).thenReturn(mWifiCondBinder);          when(mClientInterface.getWifiScannerImpl()).thenReturn(mWifiScannerImpl);          when(mWificond.createClientInterface(any())).thenReturn(mClientInterface); @@ -189,7 +208,7 @@ public class WifiNl80211ManagerTest {          when(mWificond.tearDownApInterface(any())).thenReturn(true);          when(mClientInterface.getWifiScannerImpl()).thenReturn(mWifiScannerImpl);          when(mClientInterface.getInterfaceName()).thenReturn(TEST_INTERFACE_NAME); -        mWificondControl = new WifiNl80211Manager(mContext, mWificond); +        mWificondControl = new WifiNl80211ManagerSpy(mContext, mWificond);          mWificondEventHandler = mWificondControl.getWificondEventHandler();          assertEquals(true,                  mWificondControl.setupInterfaceForClientMode(TEST_INTERFACE_NAME, Runnable::run, @@ -1159,6 +1178,40 @@ public class WifiNl80211ManagerTest {          verify(mWificond).notifyCountryCodeChanged();      } +    @Test +    public void testInstantWifi() throws Exception { +        doAnswer(new AnswerWithArguments() { +            public void answer(SingleScanSettings settings, Set<Integer> freqs) { +                if (settings.channelSettings == null) { +                    settings.channelSettings = new ArrayList<>(); +                } else { +                    settings.channelSettings.clear(); +                } +                for (int freq : freqs) { +                    if (freq > 0) { +                        ChannelSettings channel = new ChannelSettings(); +                        channel.frequency = freq; +                        settings.channelSettings.add(channel); +                    } +                } +            } +        }).when(mMockInstantWifi).overrideFreqsForSingleScanSettingsIfNecessary( +                any(), any()); +        Set<Integer> testPredictedChannelsSet = Set.of(2412, 5745); +        assertNotEquals(testPredictedChannelsSet, SCAN_FREQ_SET); +        when(mWifiScannerImpl.scan(any(SingleScanSettings.class))).thenReturn(true); +        when(mMockInstantWifi.getPredictedScanningChannels()).thenReturn(testPredictedChannelsSet); + +        // Trigger scan to check scan settings are changed +        assertTrue(mWificondControl.startScan( +                TEST_INTERFACE_NAME, WifiScanner.SCAN_TYPE_LOW_POWER, +                SCAN_FREQ_SET, SCAN_HIDDEN_NETWORK_SSID_LIST)); +        verify(mMockInstantWifi).getPredictedScanningChannels(); +        verify(mWifiScannerImpl).scan(argThat(new ScanMatcher( +                IWifiScannerImpl.SCAN_TYPE_LOW_POWER, +                testPredictedChannelsSet, SCAN_HIDDEN_NETWORK_SSID_LIST, false, null))); +    } +      // Create a ArgumentMatcher which captures a SingleScanSettings parameter and checks if it      // matches the provided frequency set and ssid set.      private class ScanMatcher implements ArgumentMatcher<SingleScanSettings> {  |