summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/current.txt1
-rw-r--r--core/java/android/content/om/FabricatedOverlay.java31
-rw-r--r--core/java/android/content/res/OWNERS5
-rw-r--r--core/java/android/service/dreams/flags.aconfig2
-rw-r--r--core/java/android/view/SyncRtSurfaceTransactionApplier.java52
-rw-r--r--core/java/android/view/inputmethod/InputMethodManager.java40
-rw-r--r--core/java/android/view/inputmethod/RemoteInputConnectionImpl.java23
-rw-r--r--core/java/android/window/DesktopModeFlags.java2
-rw-r--r--core/java/android/window/flags/windowing_sdk.aconfig11
-rw-r--r--core/java/com/android/internal/widget/NotificationProgressBar.java36
-rw-r--r--core/res/res/drawable/notification_progress_indeterminate_horizontal_material.xml167
-rw-r--r--core/res/res/drawable/vector_notification_progress_indeterminate_horizontal.xml59
-rw-r--r--core/res/res/values-watch/config.xml4
-rw-r--r--core/res/res/values/config.xml4
-rw-r--r--core/res/res/values/strings.xml6
-rw-r--r--core/res/res/values/styles_material.xml1
-rw-r--r--core/res/res/values/symbols.xml6
-rw-r--r--core/tests/coretests/src/android/content/res/XmlResourcesFlaggedTest.kt4
-rw-r--r--core/tests/overlaytests/device/Android.bp2
-rw-r--r--core/tests/overlaytests/device/res/values/config.xml4
-rw-r--r--core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java86
-rw-r--r--libs/WindowManager/Shell/tests/flicker/Android.bp16
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt6
-rw-r--r--media/java/android/media/MediaCodec.java72
-rw-r--r--media/java/android/media/quality/ActiveProcessingPicture.java19
-rw-r--r--media/java/android/media/quality/MediaQualityManager.java41
-rw-r--r--media/java/android/media/quality/PictureProfile.java28
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/IActiveProcessingPictureListener.aidl28
-rw-r--r--media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl8
-rw-r--r--packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v36/settingslib_preference_selector_with_widget.xml (renamed from packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v36/settingslib_expressive_preference_selector_with_widget.xml)26
-rw-r--r--packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java5
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig7
-rw-r--r--packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt32
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt1
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt21
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt13
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt34
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java24
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/WakeGestureMonitorTest.kt101
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt76
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java198
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.kt271
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java1
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.kt763
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt48
-rw-r--r--packages/SystemUI/res/drawable/ic_add_circle_rounded.xml27
-rw-r--r--packages/SystemUI/res/drawable/ic_check_circle_filled.xml27
-rw-r--r--packages/SystemUI/res/drawable/ic_expand_less_rounded.xml25
-rw-r--r--packages/SystemUI/res/drawable/ic_expand_more_rounded.xml25
-rw-r--r--packages/SystemUI/res/drawable/media_output_dialog_background_reduced_radius.xml20
-rw-r--r--packages/SystemUI/res/drawable/media_output_dialog_footer_background.xml22
-rw-r--r--packages/SystemUI/res/drawable/media_output_dialog_item_fixed_volume_background.xml20
-rw-r--r--packages/SystemUI/res/drawable/media_output_dialog_round_button_ripple.xml24
-rw-r--r--packages/SystemUI/res/drawable/media_output_item_expandable_button_background.xml24
-rw-r--r--packages/SystemUI/res/layout/media_output_dialog.xml30
-rw-r--r--packages/SystemUI/res/layout/media_output_list_item_device.xml141
-rw-r--r--packages/SystemUI/res/layout/media_output_list_item_group_divider.xml70
-rw-r--r--packages/SystemUI/res/values-night/colors.xml10
-rw-r--r--packages/SystemUI/res/values/colors.xml10
-rw-r--r--packages/SystemUI/res/values/dimens.xml12
-rw-r--r--packages/SystemUI/res/values/strings.xml11
-rw-r--r--packages/SystemUI/res/values/styles.xml27
-rw-r--r--packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt98
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt61
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt19
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt34
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt39
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt14
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java9
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java23
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/WakeGestureMonitor.kt74
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt61
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt24
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModel.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java148
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.kt116
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java152
-rw-r--r--packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.kt146
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java72
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.kt688
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java12
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java126
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorScheme.kt103
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java7
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java33
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt153
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt36
-rw-r--r--packages/SystemUI/src/com/android/systemui/util/kotlin/AsyncSensorManagerExt.kt57
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt18
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt22
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt26
-rw-r--r--packages/SystemUI/tests/utils/src/android/hardware/display/FakeAmbientDisplayConfiguration.kt3
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/dreams/WakeGestureMonitorKosmos.kt35
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModelKosmos.kt25
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt1
-rwxr-xr-xravenwood/scripts/ravenwood-stats-collector.sh4
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenStats.kt10
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt4
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/dumper/ApiDumper.kt129
-rw-r--r--ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicyWithReason.kt10
-rw-r--r--services/core/Android.bp1
-rw-r--r--services/core/java/com/android/server/am/BroadcastHistory.java44
-rw-r--r--services/core/java/com/android/server/biometrics/AuthenticationStats.java32
-rw-r--r--services/core/java/com/android/server/biometrics/AuthenticationStatsBroadcastReceiver.java3
-rw-r--r--services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java113
-rw-r--r--services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java72
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/BiometricNotification.java5
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/BiometricNotificationImpl.java5
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java66
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/EnrollClient.java11
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java10
-rw-r--r--services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java3
-rw-r--r--services/core/java/com/android/server/clipboard/Android.bp18
-rw-r--r--services/core/java/com/android/server/clipboard/ClipboardService.java87
-rw-r--r--services/core/java/com/android/server/clipboard/flags.aconfig9
-rw-r--r--services/core/java/com/android/server/media/quality/MediaQualityService.java248
-rw-r--r--services/core/java/com/android/server/media/quality/MediaQualityUtils.java81
-rw-r--r--services/core/java/com/android/server/tv/TvInputHal.java19
-rw-r--r--services/core/java/com/android/server/tv/TvInputHardwareManager.java21
-rw-r--r--services/core/java/com/android/server/tv/TvInputManagerService.java19
-rw-r--r--services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt159
-rw-r--r--services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt2
-rw-r--r--services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionStatesTest.kt437
-rw-r--r--services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/BasePermissionPolicyTest.kt9
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java268
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java72
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsTest.java4
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java43
-rw-r--r--services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java45
145 files changed, 6251 insertions, 1230 deletions
diff --git a/core/api/current.txt b/core/api/current.txt
index 27aa3518f958..07224db7dcd3 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -12367,6 +12367,7 @@ package android.content.om {
method @NonNull public void setResourceValue(@NonNull String, @NonNull android.os.ParcelFileDescriptor, @Nullable String);
method @FlaggedApi("android.content.res.asset_file_descriptor_frro") @NonNull public void setResourceValue(@NonNull String, @NonNull android.content.res.AssetFileDescriptor, @Nullable String);
method @FlaggedApi("android.content.res.dimension_frro") public void setResourceValue(@NonNull String, float, int, @Nullable String);
+ method @FlaggedApi("android.content.res.dimension_frro") public void setResourceValue(@NonNull String, float, @Nullable String);
method public void setTargetOverlayable(@Nullable String);
}
diff --git a/core/java/android/content/om/FabricatedOverlay.java b/core/java/android/content/om/FabricatedOverlay.java
index 64e9c339f2d6..2f93adbb1e8c 100644
--- a/core/java/android/content/om/FabricatedOverlay.java
+++ b/core/java/android/content/om/FabricatedOverlay.java
@@ -490,6 +490,17 @@ public class FabricatedOverlay {
return entry;
}
+ @NonNull
+ private static FabricatedOverlayInternalEntry generateFabricatedOverlayInternalEntry(
+ @NonNull String resourceName, float value, @Nullable String configuration) {
+ final FabricatedOverlayInternalEntry entry = new FabricatedOverlayInternalEntry();
+ entry.resourceName = resourceName;
+ entry.dataType = TypedValue.TYPE_FLOAT;
+ entry.data = Float.floatToIntBits(value);
+ entry.configuration = configuration;
+ return entry;
+ }
+
/**
* Sets the resource value in the fabricated overlay for the integer-like types with the
* configuration.
@@ -621,4 +632,24 @@ public class FabricatedOverlay {
mOverlay.entries.add(generateFabricatedOverlayInternalEntry(resourceName, dimensionValue,
dimensionUnit, configuration));
}
+
+ /**
+ * Sets the resource value in the fabricated overlay for the float type with the
+ * configuration.
+ *
+ * @param resourceName name of the target resource to overlay (in the form
+ * [package]:type/entry)
+ * @param value the float representing the new value
+ * @param configuration The string representation of the config this overlay is enabled for
+ * @throws IllegalArgumentException If the resource name is invalid
+ */
+ @FlaggedApi(android.content.res.Flags.FLAG_DIMENSION_FRRO)
+ public void setResourceValue(
+ @NonNull String resourceName,
+ float value,
+ @Nullable String configuration) {
+ ensureValidResourceName(resourceName);
+ mOverlay.entries.add(generateFabricatedOverlayInternalEntry(resourceName, value,
+ configuration));
+ }
}
diff --git a/core/java/android/content/res/OWNERS b/core/java/android/content/res/OWNERS
index 141d58d51353..f3394c3932ba 100644
--- a/core/java/android/content/res/OWNERS
+++ b/core/java/android/content/res/OWNERS
@@ -2,6 +2,7 @@
patb@google.com
zyy@google.com
-branliu@google.com
+jakmcbane@google.com
+markpun@google.com
-per-file FontScaleConverter*=fuego@google.com \ No newline at end of file
+per-file FontScaleConverter*=fuego@google.com
diff --git a/core/java/android/service/dreams/flags.aconfig b/core/java/android/service/dreams/flags.aconfig
index 971942ecfe8b..82b035c8ebfd 100644
--- a/core/java/android/service/dreams/flags.aconfig
+++ b/core/java/android/service/dreams/flags.aconfig
@@ -90,5 +90,5 @@ flag {
namespace: "systemui"
description: "Enables various improvements to the dream experience "
"such as new triggers and various bug fixes"
- bug: "375689917"
+ bug: "403579494"
}
diff --git a/core/java/android/view/SyncRtSurfaceTransactionApplier.java b/core/java/android/view/SyncRtSurfaceTransactionApplier.java
index e9c937cc0f9b..b2f0bd931174 100644
--- a/core/java/android/view/SyncRtSurfaceTransactionApplier.java
+++ b/core/java/android/view/SyncRtSurfaceTransactionApplier.java
@@ -16,6 +16,7 @@
package android.view;
+import android.annotation.SuppressLint;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.view.SurfaceControl.Transaction;
@@ -39,6 +40,9 @@ public class SyncRtSurfaceTransactionApplier {
public static final int FLAG_BACKGROUND_BLUR_RADIUS = 1 << 5;
public static final int FLAG_VISIBILITY = 1 << 6;
public static final int FLAG_TRANSACTION = 1 << 7;
+ public static final int FLAG_EARLY_WAKEUP_START = 1 << 8;
+ public static final int FLAG_EARLY_WAKEUP_END = 1 << 9;
+ public static final int FLAG_OPAQUE = 1 << 10;
private SurfaceControl mTargetSc;
private final ViewRootImpl mTargetViewRootImpl;
@@ -99,6 +103,7 @@ public class SyncRtSurfaceTransactionApplier {
}
}
+ @SuppressLint("MissingPermission")
public static void applyParams(Transaction t, SurfaceParams params, float[] tmpFloat9) {
if ((params.flags & FLAG_TRANSACTION) != 0) {
t.merge(params.mergeTransaction);
@@ -129,6 +134,15 @@ public class SyncRtSurfaceTransactionApplier {
t.hide(params.surface);
}
}
+ if ((params.flags & FLAG_EARLY_WAKEUP_START) != 0) {
+ t.setEarlyWakeupStart();
+ }
+ if ((params.flags & FLAG_EARLY_WAKEUP_END) != 0) {
+ t.setEarlyWakeupEnd();
+ }
+ if ((params.flags & FLAG_OPAQUE) != 0) {
+ t.setOpaque(params.surface, params.opaque);
+ }
}
/**
@@ -172,6 +186,7 @@ public class SyncRtSurfaceTransactionApplier {
Rect windowCrop;
int layer;
boolean visible;
+ boolean opaque;
Transaction mergeTransaction;
/**
@@ -263,17 +278,48 @@ public class SyncRtSurfaceTransactionApplier {
}
/**
+ * Provides a hint to SurfaceFlinger to change its offset so that SurfaceFlinger
+ * wakes up earlier to compose surfaces.
+ * @return this Builder
+ */
+ public Builder withEarlyWakeupStart() {
+ flags |= FLAG_EARLY_WAKEUP_START;
+ return this;
+ }
+
+ /**
+ * Removes the early wake up hint set by earlyWakeupStart.
+ * @return this Builder
+ */
+ public Builder withEarlyWakeupEnd() {
+ flags |= FLAG_EARLY_WAKEUP_END;
+ return this;
+ }
+
+ /**
+ * @param opaque Indicates weather the surface must be considered opaque.
+ * @return this Builder
+ */
+ public Builder withOpaque(boolean opaque) {
+ this.opaque = opaque;
+ flags |= FLAG_OPAQUE;
+ return this;
+ }
+
+ /**
* @return a new SurfaceParams instance
*/
public SurfaceParams build() {
return new SurfaceParams(surface, flags, alpha, matrix, windowCrop, layer,
- cornerRadius, backgroundBlurRadius, visible, mergeTransaction);
+ cornerRadius, backgroundBlurRadius, visible, mergeTransaction,
+ opaque);
}
}
private SurfaceParams(SurfaceControl surface, int params, float alpha, Matrix matrix,
Rect windowCrop, int layer, float cornerRadius,
- int backgroundBlurRadius, boolean visible, Transaction mergeTransaction) {
+ int backgroundBlurRadius, boolean visible,
+ Transaction mergeTransaction, boolean opaque) {
this.flags = params;
this.surface = surface;
this.alpha = alpha;
@@ -284,6 +330,7 @@ public class SyncRtSurfaceTransactionApplier {
this.backgroundBlurRadius = backgroundBlurRadius;
this.visible = visible;
this.mergeTransaction = mergeTransaction;
+ this.opaque = opaque;
}
private final int flags;
@@ -312,5 +359,6 @@ public class SyncRtSurfaceTransactionApplier {
public final boolean visible;
public final Transaction mergeTransaction;
+ public final boolean opaque;
}
}
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index b3bd89c2a87d..7d7570d48970 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -620,6 +620,12 @@ public final class InputMethodManager {
@GuardedBy("mH")
private CompletionInfo[] mCompletions;
+ /**
+ * Tracks last pending {@link #startInputInner(int, IBinder, int, int, int)} sequenceId.
+ */
+ @GuardedBy("mH")
+ private int mLastPendingStartSeqId = INVALID_SEQ_ID;
+
// Cursor position on the screen.
@GuardedBy("mH")
@UnsupportedAppUsage
@@ -652,6 +658,8 @@ public final class InputMethodManager {
private static final String CACHE_KEY_CONNECTIONLESS_STYLUS_HANDWRITING_PROPERTY =
"cache_key.system_server.connectionless_stylus_handwriting";
+ static final int INVALID_SEQ_ID = -1;
+
@GuardedBy("mH")
private int mCursorSelStart;
@GuardedBy("mH")
@@ -1193,12 +1201,18 @@ public final class InputMethodManager {
case MSG_START_INPUT_RESULT: {
final InputBindResult res = (InputBindResult) msg.obj;
final int startInputSeq = msg.arg1;
- if (res == null) {
- // IMMS logs .wtf already.
- return;
- }
- if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res);
synchronized (mH) {
+ if (mLastPendingStartSeqId == startInputSeq) {
+ // last pending startInput has been completed. reset.
+ mLastPendingStartSeqId = INVALID_SEQ_ID;
+ }
+
+ if (res == null) {
+ // IMMS logs .wtf already.
+ return;
+ }
+
+ if (DEBUG) Log.v(TAG, "Starting input: Bind result=" + res);
if (res.id != null) {
updateInputChannelLocked(res.channel);
mCurMethod = res.method; // for @UnsupportedAppUsage
@@ -2220,6 +2234,7 @@ public final class InputMethodManager {
}
mCompletions = null;
mServedConnecting = false;
+ mLastPendingStartSeqId = INVALID_SEQ_ID;
clearConnectionLocked();
}
mReportInputConnectionOpenedRunner = null;
@@ -3274,6 +3289,9 @@ public final class InputMethodManager {
* @param view The view whose text has changed.
*/
public void restartInput(View view) {
+ if (DEBUG) {
+ Log.d(TAG, "restartInput()");
+ }
// Re-dispatch if there is a context mismatch.
final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view);
if (fallbackImm != null) {
@@ -3351,6 +3369,9 @@ public final class InputMethodManager {
*/
public void invalidateInput(@NonNull View view) {
Objects.requireNonNull(view);
+ if (DEBUG) {
+ Log.d(TAG, "IMM#invaldateInput()");
+ }
// Re-dispatch if there is a context mismatch.
final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view);
@@ -3363,7 +3384,8 @@ public final class InputMethodManager {
if (mServedInputConnection == null || getServedViewLocked() != view) {
return;
}
- mServedInputConnection.scheduleInvalidateInput();
+ mServedInputConnection.scheduleInvalidateInput(
+ mLastPendingStartSeqId != INVALID_SEQ_ID);
}
}
@@ -3532,7 +3554,7 @@ public final class InputMethodManager {
? editorInfo.targetInputMethodUser.getIdentifier() : UserHandle.myUserId();
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMM.startInputOrWindowGainedFocus");
- int startInputSeq = -1;
+ int startInputSeq = INVALID_SEQ_ID;
if (Flags.useZeroJankProxy()) {
// async result delivered via MSG_START_INPUT_RESULT.
startInputSeq = IInputMethodManagerGlobalInvoker.startInputOrWindowGainedFocusAsync(
@@ -3557,6 +3579,9 @@ public final class InputMethodManager {
// initialized and ready for use.
if (ic != null) {
final int seqId = startInputSeq;
+ if (Flags.invalidateInputCallsRestart()) {
+ mLastPendingStartSeqId = seqId;
+ }
mReportInputConnectionOpenedRunner =
new ReportInputConnectionOpenedRunner(startInputSeq) {
@Override
@@ -5047,6 +5072,7 @@ public final class InputMethodManager {
}
p.println(" mServedInputConnection=" + mServedInputConnection);
p.println(" mServedInputConnectionHandler=" + mServedInputConnectionHandler);
+ p.println(" mLastPendingStartSeqId=" + mLastPendingStartSeqId);
p.println(" mCompletions=" + Arrays.toString(mCompletions));
p.println(" mCursorRect=" + mCursorRect);
p.println(" mCursorSelStart=" + mCursorSelStart
diff --git a/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java b/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java
index ced27d6d4886..3e8575324352 100644
--- a/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java
+++ b/core/java/android/view/inputmethod/RemoteInputConnectionImpl.java
@@ -16,6 +16,8 @@
package android.view.inputmethod;
+import static android.view.inputmethod.InputMethodManager.INVALID_SEQ_ID;
+
import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetCursorCapsModeProto;
import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetExtractedTextProto;
import static com.android.internal.inputmethod.InputConnectionProtoDumper.buildGetSelectedTextProto;
@@ -276,8 +278,19 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub {
* make sure that application code is not modifying text context in a reentrant manner.</p>
*/
public void scheduleInvalidateInput() {
+ scheduleInvalidateInput(false /* isRestarting */);
+ }
+
+ /**
+ * @see #scheduleInvalidateInput()
+ * @param isRestarting when {@code true}, there is an in-progress restartInput that could race
+ * with {@link InputMethodManager#invalidateInput(View)}. To prevent race,
+ * fallback to calling {@link InputMethodManager#restartInput(View)}.
+ */
+ void scheduleInvalidateInput(boolean isRestarting) {
if (mHasPendingInvalidation.compareAndSet(false, true)) {
- final int nextSessionId = mCurrentSessionId.incrementAndGet();
+ final int nextSessionId =
+ isRestarting ? INVALID_SEQ_ID : mCurrentSessionId.incrementAndGet();
// By calling InputConnection#takeSnapshot() directly from the message loop, we can make
// sure that application code is not modifying text context in a reentrant manner.
// e.g. We may see methods like EditText#setText() in the callstack here.
@@ -330,6 +343,14 @@ final class RemoteInputConnectionImpl extends IRemoteInputConnection.Stub {
}
}
}
+ if (isRestarting) {
+ if (DEBUG) {
+ Log.d(TAG, "scheduleInvalidateInput called with ongoing restartInput."
+ + " Fallback to calling restartInput().");
+ }
+ mParentInputMethodManager.restartInput(view);
+ return;
+ }
if (!alwaysTrueEndBatchEditDetected) {
final TextSnapshot textSnapshot = ic.takeSnapshot();
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index 527fcdf852f3..1ea4ce10c60e 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -117,7 +117,7 @@ public enum DesktopModeFlags {
ENABLE_OPAQUE_BACKGROUND_FOR_TRANSPARENT_WINDOWS(
Flags::enableOpaqueBackgroundForTransparentWindows, true),
ENABLE_QUICKSWITCH_DESKTOP_SPLIT_BUGFIX(Flags::enableQuickswitchDesktopSplitBugfix, true),
- ENABLE_REQUEST_FULLSCREEN_BUGFIX(Flags::enableRequestFullscreenBugfix, false),
+ ENABLE_REQUEST_FULLSCREEN_BUGFIX(Flags::enableRequestFullscreenBugfix, true),
ENABLE_RESIZING_METRICS(Flags::enableResizingMetrics, true),
ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE(
Flags::enableRestoreToPreviousSizeFromDesktopImmersive, true),
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index e2eb193293c9..8808707961a6 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -163,3 +163,14 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ namespace: "windowing_sdk"
+ name: "exclude_task_from_recents"
+ description: "Enables WCT to set whether the task should be excluded from the Recents list"
+ bug: "404726350"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/com/android/internal/widget/NotificationProgressBar.java b/core/java/com/android/internal/widget/NotificationProgressBar.java
index f6e2a4df8cca..c484a8851dfd 100644
--- a/core/java/com/android/internal/widget/NotificationProgressBar.java
+++ b/core/java/com/android/internal/widget/NotificationProgressBar.java
@@ -26,6 +26,8 @@ import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Rect;
+import android.graphics.drawable.Animatable2;
+import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.graphics.drawable.LayerDrawable;
@@ -66,6 +68,8 @@ public final class NotificationProgressBar extends ProgressBar implements
private static final boolean DEBUG = false;
private static final float FADED_OPACITY = 0.5f;
+ private Animatable2.AnimationCallback mIndeterminateAnimationCallback = null;
+
private NotificationProgressDrawable mNotificationProgressDrawable;
private final Rect mProgressDrawableBounds = new Rect();
@@ -150,6 +154,38 @@ public final class NotificationProgressBar extends ProgressBar implements
0);
}
+ @Override
+ public void setIndeterminateDrawable(Drawable d) {
+ final Drawable oldDrawable = getIndeterminateDrawable();
+ if (oldDrawable != d) {
+ if (mIndeterminateAnimationCallback != null) {
+ ((AnimatedVectorDrawable) oldDrawable).unregisterAnimationCallback(
+ mIndeterminateAnimationCallback);
+ mIndeterminateAnimationCallback = null;
+ }
+ if (d instanceof AnimatedVectorDrawable) {
+ mIndeterminateAnimationCallback = new Animatable2.AnimationCallback() {
+ @Override
+ public void onAnimationEnd(Drawable drawable) {
+ super.onAnimationEnd(drawable);
+
+ if (shouldLoopIndeterminateAnimation()) {
+ ((AnimatedVectorDrawable) drawable).start();
+ }
+ }
+ };
+ ((AnimatedVectorDrawable) d).registerAnimationCallback(
+ mIndeterminateAnimationCallback);
+ }
+ }
+
+ super.setIndeterminateDrawable(d);
+ }
+
+ private boolean shouldLoopIndeterminateAnimation() {
+ return isIndeterminate() && isAttachedToWindow() && isAggregatedVisible();
+ }
+
/**
* Setter for the notification progress model.
*
diff --git a/core/res/res/drawable/notification_progress_indeterminate_horizontal_material.xml b/core/res/res/drawable/notification_progress_indeterminate_horizontal_material.xml
new file mode 100644
index 000000000000..9b0405ad264f
--- /dev/null
+++ b/core/res/res/drawable/notification_progress_indeterminate_horizontal_material.xml
@@ -0,0 +1,167 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2025 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.
+-->
+
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:drawable="@drawable/vector_notification_progress_indeterminate_horizontal">
+ <target android:name="track_behind">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <objectAnimator
+ android:duration="800"
+ android:propertyName="trimPathEnd"
+ android:startOffset="400"
+ android:valueFrom="0"
+ android:valueTo="0.51"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="200"
+ android:propertyName="trimPathEnd"
+ android:valueFrom="0.51"
+ android:valueTo="0.98"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0.2,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="100"
+ android:propertyName="trimPathEnd"
+ android:valueFrom="0.98"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.5 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="progress">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <objectAnimator
+ android:duration="200"
+ android:propertyName="trimPathStart"
+ android:startOffset="200"
+ android:valueFrom="0"
+ android:valueTo="0.02"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.963,0.8 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="800"
+ android:propertyName="trimPathStart"
+ android:valueFrom="0.02"
+ android:valueTo="0.53"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="200"
+ android:propertyName="trimPathStart"
+ android:valueFrom="0.53"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0.2,1 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="progress">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <objectAnimator
+ android:duration="200"
+ android:propertyName="trimPathEnd"
+ android:startOffset="200"
+ android:valueFrom="0"
+ android:valueTo="0.024"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.963,0.834 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="800"
+ android:propertyName="trimPathEnd"
+ android:valueFrom="0.024"
+ android:valueTo="0.98"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="30"
+ android:propertyName="trimPathEnd"
+ android:valueFrom="0.98"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+ <target android:name="track_ahead">
+ <aapt:attr name="android:animation">
+ <set android:ordering="sequentially">
+ <objectAnimator
+ android:duration="200"
+ android:propertyName="trimPathStart"
+ android:valueFrom="0"
+ android:valueTo="0.02"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="200"
+ android:propertyName="trimPathStart"
+ android:valueFrom="0.02"
+ android:valueTo="0.044000000000000004"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.963,0.834 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ <objectAnimator
+ android:duration="800"
+ android:propertyName="trimPathStart"
+ android:valueFrom="0.044000000000000004"
+ android:valueTo="1"
+ android:valueType="floatType">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.3,0 0.8,0.15 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </set>
+ </aapt:attr>
+ </target>
+</animated-vector> \ No newline at end of file
diff --git a/core/res/res/drawable/vector_notification_progress_indeterminate_horizontal.xml b/core/res/res/drawable/vector_notification_progress_indeterminate_horizontal.xml
new file mode 100644
index 000000000000..fe81b79e481c
--- /dev/null
+++ b/core/res/res/drawable/vector_notification_progress_indeterminate_horizontal.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2025 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.
+-->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="300dp"
+ android:height="20dp"
+ android:viewportHeight="16"
+ android:viewportWidth="300">
+ <group
+ android:name="root"
+ android:translateX="150"
+ android:translateY="8">
+ <path
+ android:name="track_ahead"
+ android:pathData="M -147,0.0 L 147,0.0"
+ android:strokeAlpha="0.5"
+ android:strokeColor="?attr/colorControlActivated"
+ android:strokeLineCap="round"
+ android:strokeLineJoin="round"
+ android:strokeWidth="2"
+ android:trimPathEnd="1"
+ android:trimPathOffset="0"
+ android:trimPathStart="0" />
+ <path
+ android:name="progress"
+ android:pathData="M -147,0.0 L 147,0.0"
+ android:strokeColor="?attr/colorControlActivated"
+ android:strokeLineCap="round"
+ android:strokeLineJoin="round"
+ android:strokeWidth="6"
+ android:trimPathEnd="0"
+ android:trimPathOffset="0"
+ android:trimPathStart="0" />
+ <path
+ android:name="track_behind"
+ android:pathData="M -147,0.0 L 147,0.0"
+ android:strokeAlpha="0.5"
+ android:strokeColor="?attr/colorControlActivated"
+ android:strokeLineCap="round"
+ android:strokeLineJoin="round"
+ android:strokeWidth="2"
+ android:trimPathEnd="0"
+ android:trimPathOffset="0"
+ android:trimPathStart="0" />
+ </group>
+</vector> \ No newline at end of file
diff --git a/core/res/res/values-watch/config.xml b/core/res/res/values-watch/config.xml
index 57a09ea34ba1..42210c40e7ba 100644
--- a/core/res/res/values-watch/config.xml
+++ b/core/res/res/values-watch/config.xml
@@ -31,7 +31,7 @@
<dimen name="config_viewMinFlingVelocity">500dp</dimen>
<!-- Maximum velocity to initiate a fling, as measured in dips per second. -->
- <dimen name="config_viewMaxFlingVelocity">8000dp</dimen>
+ <dimen name="config_viewMaxFlingVelocity">2750dp</dimen>
<!-- Minimum velocity (absolute value) to initiate a fling from a rotary encoder device, as
measured in dips per second. Setting this to -1dp disables rotary encoder fling. -->
@@ -39,7 +39,7 @@
<!-- Maximum velocity (absolute value) to initiate a fling from a rotary encoder device, as
measured in dips per second. Setting this to -1dp disables rotary encoder fling. -->
- <dimen name="config_viewMaxRotaryEncoderFlingVelocity">8000dp</dimen>
+ <dimen name="config_viewMaxRotaryEncoderFlingVelocity">2750dp</dimen>
<!-- Whether the View-based scroll haptic feedback implementation is enabled for
{@link InputDevice#SOURCE_ROTARY_ENCODER}s. -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index cb4dd46e70fe..32e612f36cce 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -7466,4 +7466,8 @@
<!-- By default ActivityOptions#makeScaleUpAnimation is only used between activities. This
config enables OEMs to support its usage across tasks.-->
<bool name="config_enableCrossTaskScaleUpAnimation">false</bool>
+
+ <!-- Biometrics fingerprint frr notification target activity component name, it shall be customized by OEMs -->
+ <string translatable="false" name="config_fingerprintFrrTargetComponent"></string>
+
</resources>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index da6ebe9e7ac0..43ba327f4069 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -6867,4 +6867,10 @@ ul.</string>
<string name="usb_apm_usb_plugged_in_when_locked_notification_text">USB device is plugged in when Android is locked. To use device, please unlock Android first and then reinsert USB device to use it.</string>
<string name="usb_apm_usb_suspicious_activity_notification_title">Suspicious USB activity</string>
<string name="usb_apm_usb_suspicious_activity_notification_text">USB data signal has been disabled.</string>
+
+ <!-- Fingerprint failed rate too high notification title -->
+ <string name="fingerprint_frr_notification_title">Having trouble with Fingerprint Unlock?</string>
+ <!-- Fingerprint failed rate too high notification msg -->
+ <string name="fingerprint_frr_notification_msg">Tap to review tips to improve your unlocking experience</string>
+
</resources>
diff --git a/core/res/res/values/styles_material.xml b/core/res/res/values/styles_material.xml
index 8f13ee1ccb49..b6142592296c 100644
--- a/core/res/res/values/styles_material.xml
+++ b/core/res/res/values/styles_material.xml
@@ -508,6 +508,7 @@ please see styles_device_defaults.xml.
<item name="segPointGap">@dimen/notification_progress_segPoint_gap</item>
<item name="progressDrawable">@drawable/notification_progress</item>
<item name="trackerHeight">@dimen/notification_progress_tracker_height</item>
+ <item name="indeterminateDrawable">@drawable/notification_progress_indeterminate_horizontal_material</item>
</style>
<style name="Widget.Material.Notification.Text" parent="Widget.Material.Light.TextView">
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index ab219a595b09..e80243a8f752 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -5992,4 +5992,10 @@
<!-- Default height of desktop view header for freeform tasks on launch. -->
<java-symbol type="dimen" name="desktop_view_default_header_height" />
+
+ <!-- Enable OEMs to support different frr notification target component activity -->
+ <java-symbol type="string" name="config_fingerprintFrrTargetComponent" />
+ <java-symbol type="string" name="fingerprint_frr_notification_title" />
+ <java-symbol type="string" name="fingerprint_frr_notification_msg" />
+
</resources>
diff --git a/core/tests/coretests/src/android/content/res/XmlResourcesFlaggedTest.kt b/core/tests/coretests/src/android/content/res/XmlResourcesFlaggedTest.kt
index 8c20ba0d7fbe..820bcfc03724 100644
--- a/core/tests/coretests/src/android/content/res/XmlResourcesFlaggedTest.kt
+++ b/core/tests/coretests/src/android/content/res/XmlResourcesFlaggedTest.kt
@@ -47,6 +47,8 @@ import java.io.IOException
@Presubmit
@SmallTest
@RunWith(AndroidJUnit4::class)
+@android.platform.test.annotations.DisabledOnRavenwood(bug = 396458006,
+ reason = "Resource flags don't fully work on Ravenwood yet")
class XmlResourcesFlaggedTest {
@get:Rule
val mCheckFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
@@ -111,4 +113,4 @@ class XmlResourcesFlaggedTest {
assertEquals(XmlPullParser.END_TAG, parser.next())
assertEquals(XmlPullParser.END_DOCUMENT, parser.next())
}
-} \ No newline at end of file
+}
diff --git a/core/tests/overlaytests/device/Android.bp b/core/tests/overlaytests/device/Android.bp
index 2b22344a4ef2..db1bf9c295b6 100644
--- a/core/tests/overlaytests/device/Android.bp
+++ b/core/tests/overlaytests/device/Android.bp
@@ -28,8 +28,8 @@ android_test {
certificate: "platform",
static_libs: [
"androidx.test.rules",
- "testng",
"compatibility-device-util-axt",
+ "testng",
],
test_suites: ["device-tests"],
data: [
diff --git a/core/tests/overlaytests/device/res/values/config.xml b/core/tests/overlaytests/device/res/values/config.xml
index a30d66f82128..e031b95f5d22 100644
--- a/core/tests/overlaytests/device/res/values/config.xml
+++ b/core/tests/overlaytests/device/res/values/config.xml
@@ -2,7 +2,7 @@
<resources>
<string name="str">none</string>
<string name="str2">none</string>
- <integer name="overlaid">0</integer>
+ <integer name="overlaidInt">0</integer>
<integer name="matrix_100000">100</integer>
<integer name="matrix_100001">100</integer>
<integer name="matrix_100010">100</integer>
@@ -58,6 +58,8 @@
<item>19</item>
</integer-array>
+ <item name="overlaidFloat" format="float" type="dimen">0</item>
+
<attr name="customAttribute" />
<id name="view_1" />
<id name="view_2" />
diff --git a/core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java b/core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java
index 2da9a2ebbdb6..b48e3b7423ff 100644
--- a/core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java
+++ b/core/tests/overlaytests/device/src/com/android/overlaytest/FabricatedOverlaysTest.java
@@ -22,6 +22,7 @@ import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertThrows;
+import android.annotation.NonNull;
import android.content.Context;
import android.content.om.FabricatedOverlay;
import android.content.om.OverlayIdentifier;
@@ -44,14 +45,17 @@ import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.Collections;
+import java.util.Objects;
import java.util.concurrent.TimeoutException;
+import java.util.function.Function;
@RunWith(JUnit4.class)
@MediumTest
public class FabricatedOverlaysTest {
private static final String TAG = "FabricatedOverlaysTest";
- private final String TEST_RESOURCE = "integer/overlaid";
- private final String TEST_OVERLAY_NAME = "Test";
+ private static final String TEST_INT_RESOURCE = "integer/overlaidInt";
+ private static final String TEST_FLOAT_RESOURCE = "dimen/overlaidFloat";
+ private static final String TEST_OVERLAY_NAME = "Test";
private Context mContext;
private Resources mResources;
@@ -84,10 +88,10 @@ public class FabricatedOverlaysTest {
public void testFabricatedOverlay() throws Exception {
final FabricatedOverlay overlay = new FabricatedOverlay.Builder(
mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName())
- .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
+ .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
.build();
- waitForResourceValue(0);
+ waitForIntResourceValue(0);
mOverlayManager.commit(new OverlayManagerTransaction.Builder()
.registerFabricatedOverlay(overlay)
.build());
@@ -104,63 +108,63 @@ public class FabricatedOverlaysTest {
assertNotNull(info);
assertTrue(info.isEnabled());
- waitForResourceValue(1);
+ waitForIntResourceValue(1);
mOverlayManager.commit(new OverlayManagerTransaction.Builder()
.unregisterFabricatedOverlay(overlay.getIdentifier())
.build());
- waitForResourceValue(0);
+ waitForIntResourceValue(0);
}
@Test
public void testRegisterEnableAtomic() throws Exception {
final FabricatedOverlay overlay = new FabricatedOverlay.Builder(
mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName())
- .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
+ .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
.build();
- waitForResourceValue(0);
+ waitForIntResourceValue(0);
mOverlayManager.commit(new OverlayManagerTransaction.Builder()
.registerFabricatedOverlay(overlay)
.setEnabled(overlay.getIdentifier(), true, mUserId)
.build());
- waitForResourceValue(1);
+ waitForIntResourceValue(1);
}
@Test
public void testRegisterTwice() throws Exception {
FabricatedOverlay overlay = new FabricatedOverlay.Builder(
mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName())
- .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
+ .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
.build();
- waitForResourceValue(0);
+ waitForIntResourceValue(0);
mOverlayManager.commit(new OverlayManagerTransaction.Builder()
.registerFabricatedOverlay(overlay)
.setEnabled(overlay.getIdentifier(), true, mUserId)
.build());
- waitForResourceValue(1);
+ waitForIntResourceValue(1);
overlay = new FabricatedOverlay.Builder(
mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName())
- .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 2)
+ .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 2)
.build();
mOverlayManager.commit(new OverlayManagerTransaction.Builder()
.registerFabricatedOverlay(overlay)
.build());
- waitForResourceValue(2);
+ waitForIntResourceValue(2);
}
@Test
public void testInvalidOwningPackageName() throws Exception {
final FabricatedOverlay overlay = new FabricatedOverlay.Builder(
"android", TEST_OVERLAY_NAME, mContext.getPackageName())
- .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
+ .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
.build();
- waitForResourceValue(0);
+ waitForIntResourceValue(0);
assertThrows(SecurityException.class, () ->
mOverlayManager.commit(new OverlayManagerTransaction.Builder()
.registerFabricatedOverlay(overlay)
@@ -174,10 +178,10 @@ public class FabricatedOverlaysTest {
public void testInvalidOverlayName() throws Exception {
final FabricatedOverlay overlay = new FabricatedOverlay.Builder(
mContext.getPackageName(), "invalid@name", mContext.getPackageName())
- .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
+ .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
.build();
- waitForResourceValue(0);
+ waitForIntResourceValue(0);
assertThrows(SecurityException.class, () ->
mOverlayManager.commit(new OverlayManagerTransaction.Builder()
.registerFabricatedOverlay(overlay)
@@ -195,7 +199,7 @@ public class FabricatedOverlaysTest {
{
FabricatedOverlay overlay = new FabricatedOverlay.Builder(mContext.getPackageName(),
longestName, mContext.getPackageName())
- .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
+ .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
.build();
mOverlayManager.commit(new OverlayManagerTransaction.Builder()
@@ -206,7 +210,7 @@ public class FabricatedOverlaysTest {
{
FabricatedOverlay overlay = new FabricatedOverlay.Builder(mContext.getPackageName(),
longestName + "a", mContext.getPackageName())
- .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
+ .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
.build();
assertThrows(SecurityException.class, () ->
@@ -267,11 +271,11 @@ public class FabricatedOverlaysTest {
public void testInvalidResourceValues() throws Exception {
final FabricatedOverlay overlay = new FabricatedOverlay.Builder(
"android", TEST_OVERLAY_NAME, mContext.getPackageName())
- .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
+ .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
.setResourceValue("color/something", TypedValue.TYPE_INT_DEC, 1)
.build();
- waitForResourceValue(0);
+ waitForIntResourceValue(0);
assertThrows(SecurityException.class, () ->
mOverlayManager.commit(new OverlayManagerTransaction.Builder()
.registerFabricatedOverlay(overlay)
@@ -285,10 +289,10 @@ public class FabricatedOverlaysTest {
public void testTransactionFailRollback() throws Exception {
final FabricatedOverlay overlay = new FabricatedOverlay.Builder(
mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName())
- .setResourceValue(TEST_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
+ .setResourceValue(TEST_INT_RESOURCE, TypedValue.TYPE_INT_DEC, 1)
.build();
- waitForResourceValue(0);
+ waitForIntResourceValue(0);
assertThrows(SecurityException.class, () ->
mOverlayManager.commit(new OverlayManagerTransaction.Builder()
.registerFabricatedOverlay(overlay)
@@ -299,16 +303,40 @@ public class FabricatedOverlaysTest {
assertNull(mOverlayManager.getOverlayInfo(overlay.getIdentifier(), mUserHandle));
}
- void waitForResourceValue(final int expectedValue) throws TimeoutException {
+ @Test
+ public void setResourceValue_forFloatType_succeeds() throws Exception {
+ final float overlaidValue = 5.7f;
+ final FabricatedOverlay overlay = new FabricatedOverlay.Builder(
+ mContext.getPackageName(), TEST_OVERLAY_NAME, mContext.getPackageName()).build();
+ overlay.setResourceValue(TEST_FLOAT_RESOURCE, overlaidValue, null /* configuration */);
+
+ waitForFloatResourceValue(0);
+ mOverlayManager.commit(new OverlayManagerTransaction.Builder()
+ .registerFabricatedOverlay(overlay)
+ .setEnabled(overlay.getIdentifier(), true, mUserId)
+ .build());
+
+ waitForFloatResourceValue(overlaidValue);
+ }
+
+ private void waitForIntResourceValue(final int expectedValue) throws TimeoutException {
+ waitForResourceValue(expectedValue, TEST_INT_RESOURCE, id -> mResources.getInteger(id));
+ }
+
+ private void waitForFloatResourceValue(final float expectedValue) throws TimeoutException {
+ waitForResourceValue(expectedValue, TEST_FLOAT_RESOURCE, id -> mResources.getFloat(id));
+ }
+
+ private <T> void waitForResourceValue(final T expectedValue, final String resourceName,
+ @NonNull Function<Integer, T> resourceValueEmitter) throws TimeoutException {
final long timeOutDuration = 10000;
final long endTime = System.currentTimeMillis() + timeOutDuration;
- final String resourceName = TEST_RESOURCE;
final int resourceId = mResources.getIdentifier(resourceName, "",
mContext.getPackageName());
- int resourceValue = 0;
+ T resourceValue = null;
while (System.currentTimeMillis() < endTime) {
- resourceValue = mResources.getInteger(resourceId);
- if (resourceValue == expectedValue) {
+ resourceValue = resourceValueEmitter.apply(resourceId);
+ if (Objects.equals(expectedValue, resourceValue)) {
return;
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp
index 7b6cfe3f9f8a..98b0bd0b589d 100644
--- a/libs/WindowManager/Shell/tests/flicker/Android.bp
+++ b/libs/WindowManager/Shell/tests/flicker/Android.bp
@@ -38,15 +38,15 @@ java_library {
],
static_libs: [
"androidx.test.ext.junit",
- "flickertestapplib",
+ "com_android_wm_shell_flags_lib",
"flickerlib",
"flickerlib-helpers",
+ "flickertestapplib",
+ "launcher-aosp-tapl",
+ "launcher-helper-lib",
"platform-test-annotations",
"wm-flicker-common-app-helpers",
"wm-flicker-common-assertions",
- "launcher-helper-lib",
- "launcher-aosp-tapl",
- "com_android_wm_shell_flags_lib",
],
}
@@ -60,18 +60,18 @@ java_defaults {
test_suites: ["device-tests"],
libs: ["android.test.runner.stubs.system"],
static_libs: [
- "wm-shell-flicker-utils",
"androidx.test.ext.junit",
- "flickertestapplib",
"flickerlib",
"flickerlib-helpers",
"flickerlib-trace_processor_shell",
+ "flickertestapplib",
+ "launcher-aosp-tapl",
+ "launcher-helper-lib",
"platform-test-annotations",
"platform-test-rules",
"wm-flicker-common-app-helpers",
"wm-flicker-common-assertions",
- "launcher-helper-lib",
- "launcher-aosp-tapl",
+ "wm-shell-flicker-utils",
],
data: [
":FlickerTestApp",
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 5eb2caaa5d4a..d81786b5e6a5 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -2381,9 +2381,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER)
@DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() {
+ whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context))
+ .thenReturn(false)
val homeTask = setUpHomeTask()
val task = setUpFreeformTask()
-
assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
.configuration
.windowConfiguration
@@ -2435,9 +2436,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
)
fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_homeBehindFullscreen_multiDesksEnabled() {
+ whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context))
+ .thenReturn(false)
val homeTask = setUpHomeTask()
val task = setUpFreeformTask()
-
assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY))
.configuration
.windowConfiguration
diff --git a/media/java/android/media/MediaCodec.java b/media/java/android/media/MediaCodec.java
index 15c832392a22..d0676e693b95 100644
--- a/media/java/android/media/MediaCodec.java
+++ b/media/java/android/media/MediaCodec.java
@@ -50,6 +50,7 @@ import android.os.Message;
import android.os.PersistableBundle;
import android.os.Trace;
import android.view.Surface;
+import android.util.Log;
import java.io.IOException;
import java.lang.annotation.Retention;
@@ -1656,6 +1657,7 @@ import java.util.function.Supplier;
</table>
*/
final public class MediaCodec {
+ private static final String TAG = "MediaCodec";
/**
* Per buffer metadata includes an offset and size specifying
@@ -2496,6 +2498,49 @@ final public class MediaCodec {
}
keys[i] = "audio-hw-sync";
values[i] = AudioSystem.getAudioHwSyncForSession(sessionId);
+ } else if (applyPictureProfiles() && mediaQualityFw()
+ && entry.getKey().equals(MediaFormat.KEY_PICTURE_PROFILE_INSTANCE)) {
+ PictureProfile pictureProfile = null;
+ try {
+ pictureProfile = (PictureProfile) entry.getValue();
+ } catch (ClassCastException e) {
+ throw new IllegalArgumentException(
+ "Cannot cast the instance parameter to PictureProfile!");
+ } catch (Exception e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ throw new IllegalArgumentException("Unexpected exception when casting the "
+ + "instance parameter to PictureProfile!");
+ }
+ if (pictureProfile == null) {
+ throw new IllegalArgumentException(
+ "Picture profile instance parameter is null!");
+ }
+ PictureProfileHandle handle = pictureProfile.getHandle();
+ if (handle != PictureProfileHandle.NONE) {
+ keys[i] = PARAMETER_KEY_PICTURE_PROFILE_HANDLE;
+ values[i] = Long.valueOf(handle.getId());
+ }
+ } else if (applyPictureProfiles() && mediaQualityFw()
+ && entry.getKey().equals(MediaFormat.KEY_PICTURE_PROFILE_ID)) {
+ String pictureProfileId = null;
+ try {
+ pictureProfileId = (String) entry.getValue();
+ } catch (ClassCastException e) {
+ throw new IllegalArgumentException(
+ "Cannot cast the KEY_PICTURE_PROFILE_ID parameter to String!");
+ } catch (Exception e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ throw new IllegalArgumentException("Unexpected exception when casting the "
+ + "KEY_PICTURE_PROFILE_ID parameter!");
+ }
+ if (pictureProfileId == null) {
+ throw new IllegalArgumentException(
+ "KEY_PICTURE_PROFILE_ID parameter is null!");
+ }
+ if (!pictureProfileId.isEmpty()) {
+ keys[i] = MediaFormat.KEY_PICTURE_PROFILE_ID;
+ values[i] = pictureProfileId;
+ }
} else {
keys[i] = entry.getKey();
values[i] = entry.getValue();
@@ -5424,7 +5469,7 @@ final public class MediaCodec {
throw new IllegalArgumentException(
"Cannot cast the instance parameter to PictureProfile!");
} catch (Exception e) {
- android.util.Log.getStackTraceString(e);
+ Log.e(TAG, Log.getStackTraceString(e));
throw new IllegalArgumentException("Unexpected exception when casting the "
+ "instance parameter to PictureProfile!");
}
@@ -5437,6 +5482,26 @@ final public class MediaCodec {
keys[i] = PARAMETER_KEY_PICTURE_PROFILE_HANDLE;
values[i] = Long.valueOf(handle.getId());
}
+ } else if (applyPictureProfiles() && mediaQualityFw()
+ && key.equals(MediaFormat.KEY_PICTURE_PROFILE_ID)) {
+ String pictureProfileId = null;
+ try {
+ pictureProfileId = (String) params.get(key);
+ } catch (ClassCastException e) {
+ throw new IllegalArgumentException(
+ "Cannot cast the KEY_PICTURE_PROFILE_ID parameter to String!");
+ } catch (Exception e) {
+ Log.e(TAG, Log.getStackTraceString(e));
+ throw new IllegalArgumentException("Unexpected exception when casting the "
+ + "KEY_PICTURE_PROFILE_ID parameter!");
+ }
+ if (pictureProfileId == null) {
+ throw new IllegalArgumentException("KEY_PICTURE_PROFILE_ID parameter is null!");
+ }
+ if (!pictureProfileId.isEmpty()) {
+ keys[i] = MediaFormat.KEY_PICTURE_PROFILE_ID;
+ values[i] = pictureProfileId;
+ }
} else {
keys[i] = key;
Object value = params.get(key);
@@ -5455,10 +5520,9 @@ final public class MediaCodec {
}
private void logAndRun(String message, Runnable r) {
- final String TAG = "MediaCodec";
- android.util.Log.d(TAG, "enter: " + message);
+ Log.d(TAG, "enter: " + message);
r.run();
- android.util.Log.d(TAG, "exit : " + message);
+ Log.d(TAG, "exit : " + message);
}
/**
diff --git a/media/java/android/media/quality/ActiveProcessingPicture.java b/media/java/android/media/quality/ActiveProcessingPicture.java
index e16ad62e23f2..15c2e47fe820 100644
--- a/media/java/android/media/quality/ActiveProcessingPicture.java
+++ b/media/java/android/media/quality/ActiveProcessingPicture.java
@@ -31,16 +31,26 @@ import androidx.annotation.NonNull;
public final class ActiveProcessingPicture implements Parcelable {
private final int mId;
private final String mProfileId;
+ private final boolean mForGlobal;
public ActiveProcessingPicture(int id, @NonNull String profileId) {
mId = id;
mProfileId = profileId;
+ mForGlobal = true;
+ }
+
+ /** @hide */
+ public ActiveProcessingPicture(int id, @NonNull String profileId, boolean forGlobal) {
+ mId = id;
+ mProfileId = profileId;
+ mForGlobal = forGlobal;
}
/** @hide */
ActiveProcessingPicture(Parcel in) {
mId = in.readInt();
mProfileId = in.readString();
+ mForGlobal = in.readBoolean();
}
@NonNull
@@ -73,6 +83,14 @@ public final class ActiveProcessingPicture implements Parcelable {
return mProfileId;
}
+ /**
+ * @hide
+ */
+ @NonNull
+ public boolean isForGlobal() {
+ return mForGlobal;
+ }
+
@Override
public int describeContents() {
return 0;
@@ -82,5 +100,6 @@ public final class ActiveProcessingPicture implements Parcelable {
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeInt(mId);
dest.writeString(mProfileId);
+ dest.writeBoolean(mForGlobal);
}
}
diff --git a/media/java/android/media/quality/MediaQualityManager.java b/media/java/android/media/quality/MediaQualityManager.java
index bfd01380a2ee..3f0ba3100191 100644
--- a/media/java/android/media/quality/MediaQualityManager.java
+++ b/media/java/android/media/quality/MediaQualityManager.java
@@ -214,11 +214,31 @@ public final class MediaQualityManager {
}
};
+ IActiveProcessingPictureListener apListener = new IActiveProcessingPictureListener.Stub() {
+ @Override
+ public void onActiveProcessingPicturesChanged(List<ActiveProcessingPicture> aps) {
+ List<ActiveProcessingPicture> nonGlobal = new ArrayList<>();
+ for (ActiveProcessingPicture ap : aps) {
+ if (!ap.isForGlobal()) {
+ nonGlobal.add(ap);
+ }
+ }
+ for (ActiveProcessingPictureListenerRecord record : mApListenerRecords) {
+ if (record.mIsGlobal) {
+ record.postActiveProcessingPicturesChanged(aps);
+ } else {
+ record.postActiveProcessingPicturesChanged(nonGlobal);
+ }
+ }
+ }
+ };
+
try {
if (mService != null) {
mService.registerPictureProfileCallback(ppCallback);
mService.registerSoundProfileCallback(spCallback);
mService.registerAmbientBacklightCallback(abCallback);
+ mService.registerActiveProcessingPictureListener(apListener);
}
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -378,6 +398,18 @@ public final class MediaQualityManager {
}
/**
+ * Gets picture profile handle for TV input.
+ * @hide
+ */
+ public long getPictureProfileForTvInput(String inputId) {
+ try {
+ return mService.getPictureProfileForTvInput(inputId, mUserHandle.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Gets sound profile handle by profile ID.
* @hide
*/
@@ -1213,6 +1245,15 @@ public final class MediaQualityManager {
public Consumer<List<ActiveProcessingPicture>> getListener() {
return mListener;
}
+
+ public void postActiveProcessingPicturesChanged(List<ActiveProcessingPicture> aps) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mListener.accept(aps);
+ }
+ });
+ }
}
/**
diff --git a/media/java/android/media/quality/PictureProfile.java b/media/java/android/media/quality/PictureProfile.java
index 3bccd89c91c3..0121193a7c86 100644
--- a/media/java/android/media/quality/PictureProfile.java
+++ b/media/java/android/media/quality/PictureProfile.java
@@ -18,6 +18,7 @@ package android.media.quality;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
+import android.annotation.StringDef;
import android.annotation.SystemApi;
import android.media.tv.TvInputInfo;
import android.media.tv.flags.Flags;
@@ -72,6 +73,19 @@ public final class PictureProfile implements Parcelable {
*/
public static final int TYPE_APPLICATION = 2;
+ /**
+ * Default profile name
+ * @hide
+ */
+ public static final String NAME_DEFAULT = "default";
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef(prefix = "NAME_", value = {
+ NAME_DEFAULT
+ })
+ public @interface ProfileName {}
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(flag = false, prefix = "ERROR_", value = {
@@ -126,6 +140,20 @@ public final class PictureProfile implements Parcelable {
*/
public static final String STATUS_HDR = "HDR";
+ /** @hide */
+ public static final String NAME_STANDARD = "standard";
+ /** @hide */
+ public static final String NAME_VIVID = "vivid";
+ /** @hide */
+ public static final String NAME_SPORTS = "sports";
+ /** @hide */
+ public static final String NAME_GAME = "game";
+ /** @hide */
+ public static final String NAME_MOVIE = "movie";
+ /** @hide */
+ public static final String NAME_ENERGY_SAVING = "energy_saving";
+ /** @hide */
+ public static final String NAME_USER = "user";
private PictureProfile(@NonNull Parcel in) {
mId = in.readString();
diff --git a/media/java/android/media/quality/aidl/android/media/quality/IActiveProcessingPictureListener.aidl b/media/java/android/media/quality/aidl/android/media/quality/IActiveProcessingPictureListener.aidl
new file mode 100644
index 000000000000..f7d19baac7a1
--- /dev/null
+++ b/media/java/android/media/quality/aidl/android/media/quality/IActiveProcessingPictureListener.aidl
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package android.media.quality;
+
+import android.media.quality.ActiveProcessingPicture;
+
+/**
+ * Interface to receive event from media quality service.
+ * @hide
+ */
+oneway interface IActiveProcessingPictureListener {
+ void onActiveProcessingPicturesChanged(in List<ActiveProcessingPicture> ap);
+}
diff --git a/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl b/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl
index 6ac1656b77aa..ff1bf0228474 100644
--- a/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl
+++ b/media/java/android/media/quality/aidl/android/media/quality/IMediaQualityManager.aidl
@@ -17,6 +17,7 @@
package android.media.quality;
import android.media.quality.AmbientBacklightSettings;
+import android.media.quality.IActiveProcessingPictureListener;
import android.media.quality.IAmbientBacklightCallback;
import android.media.quality.IPictureProfileCallback;
import android.media.quality.ISoundProfileCallback;
@@ -47,6 +48,12 @@ interface IMediaQualityManager {
void setPictureProfileAllowList(in List<String> packages, int userId);
List<PictureProfileHandle> getPictureProfileHandle(in String[] id, int userId);
+ long getPictureProfileHandleValue(in String id, int userId);
+ long getDefaultPictureProfileHandleValue(int userId);
+ void notifyPictureProfileHandleSelection(in long handle, int userId);
+
+ long getPictureProfileForTvInput(in String inputId, int userId);
+
void createSoundProfile(in SoundProfile pp, int userId);
void updateSoundProfile(in String id, in SoundProfile pp, int userId);
void removeSoundProfile(in String id, int userId);
@@ -64,6 +71,7 @@ interface IMediaQualityManager {
void registerPictureProfileCallback(in IPictureProfileCallback cb);
void registerSoundProfileCallback(in ISoundProfileCallback cb);
void registerAmbientBacklightCallback(in IAmbientBacklightCallback cb);
+ void registerActiveProcessingPictureListener(in IActiveProcessingPictureListener l);
List<ParameterCapability> getParameterCapabilities(in List<String> names, int userId);
diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v36/settingslib_expressive_preference_selector_with_widget.xml b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v36/settingslib_preference_selector_with_widget.xml
index a79d69dbff8c..adaec8524241 100644
--- a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v36/settingslib_expressive_preference_selector_with_widget.xml
+++ b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v36/settingslib_preference_selector_with_widget.xml
@@ -23,34 +23,20 @@
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
android:paddingEnd="?android:attr/listPreferredItemPaddingEnd">
<LinearLayout
android:id="@android:id/widget_frame"
android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:paddingEnd="@dimen/settingslib_expressive_space_extrasmall6"
+ android:layout_height="wrap_content"
android:gravity="center"
- android:minWidth="32dp"
+ android:minWidth="@dimen/settingslib_expressive_space_medium3"
+ android:minHeight="@dimen/settingslib_expressive_space_medium3"
+ android:layout_marginEnd="@dimen/settingslib_expressive_space_extrasmall6"
android:orientation="vertical"/>
- <LinearLayout
- android:id="@+id/icon_frame"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:gravity="center_vertical"
- android:minWidth="32dp"
- android:orientation="horizontal"
- android:layout_marginEnd="@dimen/settingslib_expressive_space_small1"
- android:paddingTop="@dimen/settingslib_expressive_space_extrasmall2"
- android:paddingBottom="@dimen/settingslib_expressive_space_extrasmall2">
- <androidx.preference.internal.PreferenceImageView
- android:id="@android:id/icon"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- settings:maxWidth="@dimen/secondary_app_icon_size"
- settings:maxHeight="@dimen/secondary_app_icon_size"/>
- </LinearLayout>
+ <include layout="@layout/settingslib_expressive_preference_icon_frame"/>
<LinearLayout
android:layout_width="0dp"
diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java b/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java
index 465b6ccf4d9c..cde8b332f2e7 100644
--- a/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java
+++ b/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java
@@ -238,10 +238,7 @@ public class SelectorWithWidgetPreference extends CheckBoxPreference {
} else {
setWidgetLayoutResource(R.layout.settingslib_preference_widget_radiobutton);
}
- int resID = SettingsThemeHelper.isExpressiveTheme(context)
- ? R.layout.settingslib_expressive_preference_selector_with_widget
- : R.layout.preference_selector_with_widget;
- setLayoutResource(resID);
+ setLayoutResource(R.layout.preference_selector_with_widget);
setIconSpaceReserved(false);
final TypedArray a =
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index f65ca3b60818..08efe34d568c 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1393,13 +1393,6 @@ flag {
}
flag {
- name: "media_controls_posts_optimization"
- namespace: "systemui"
- description: "Ignore duplicate media notifications posted"
- bug: "358645640"
-}
-
-flag {
name: "media_controls_umo_inflation_in_background"
namespace: "systemui"
description: "Inflate UMO in background thread"
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
index 05c9818f0c57..655b79c5b635 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt
@@ -209,8 +209,7 @@ open class SimpleDigitalClockTextView(
lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation)
typeface = lockScreenPaint.typeface
- textBounds = lockScreenPaint.getTextBounds(text)
- targetTextBounds = textBounds
+ updateTextBounds()
textAnimator.setTextStyle(
TextAnimator.Style(fVar = lsFontVariation),
@@ -253,9 +252,9 @@ open class SimpleDigitalClockTextView(
object : TextAnimatorListener {
override fun onInvalidate() = invalidate()
- override fun onRebased() = updateTextBounds()
+ override fun onRebased() = updateAnimationTextBounds()
- override fun onPaintModified() = updateTextBounds()
+ override fun onPaintModified() = updateAnimationTextBounds()
},
)
setInterpolatorPaint()
@@ -414,10 +413,7 @@ open class SimpleDigitalClockTextView(
}
fun refreshText() {
- textBounds = lockScreenPaint.getTextBounds(text)
- targetTextBounds =
- if (!this::textAnimator.isInitialized) textBounds
- else textAnimator.textInterpolator.targetPaint.getTextBounds(text)
+ updateTextBounds()
if (layout == null) {
requestLayout()
@@ -579,8 +575,7 @@ open class SimpleDigitalClockTextView(
if (fontSizePx > 0) {
setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx)
lockScreenPaint.textSize = textSize
- textBounds = lockScreenPaint.getTextBounds(text)
- targetTextBounds = textBounds
+ updateTextBounds()
}
if (!constrainedByHeight) {
val lastUnconstrainedHeight = textBounds.height + lockScreenPaint.strokeWidth * 2
@@ -624,15 +619,26 @@ open class SimpleDigitalClockTextView(
}
}
+ /** Updates both the lockscreen text bounds and animation text bounds */
+ private fun updateTextBounds() {
+ textBounds = lockScreenPaint.getTextBounds(text)
+ updateAnimationTextBounds()
+ }
+
/**
* Called after textAnimator.setTextStyle textAnimator.setTextStyle will update targetPaint, and
* rebase if previous animator is canceled so basePaint will store the state we transition from
* and targetPaint will store the state we transition to
*/
- private fun updateTextBounds() {
+ private fun updateAnimationTextBounds() {
drawnProgress = null
- prevTextBounds = textAnimator.textInterpolator.basePaint.getTextBounds(text)
- targetTextBounds = textAnimator.textInterpolator.targetPaint.getTextBounds(text)
+ if (this::textAnimator.isInitialized) {
+ prevTextBounds = textAnimator.textInterpolator.basePaint.getTextBounds(text)
+ targetTextBounds = textAnimator.textInterpolator.targetPaint.getTextBounds(text)
+ } else {
+ prevTextBounds = textBounds
+ targetTextBounds = textBounds
+ }
}
/**
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
index e8c30bafbba0..c963157318ed 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt
@@ -191,7 +191,6 @@ class SideFpsOverlayViewBinderTest : SysuiTestCase() {
val clickListenerCaptor = ArgumentCaptor.forClass(View.OnClickListener::class.java)
verify(sideFpsView).setOnClickListener(clickListenerCaptor.capture())
clickListenerCaptor.value.onClick(sideFpsView)
- verify(lottieAnimationView).toggleAnimation()
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt
index 293d32471713..51ad6a146d0e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSceneRepositoryImplTest.kt
@@ -21,23 +21,18 @@ import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.shared.model.CommunalScenes
-import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.backgroundScope
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
-import com.android.systemui.scene.shared.model.SceneDataSource
import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.flowOf
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
-import org.mockito.kotlin.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -49,10 +44,8 @@ class CommunalSceneRepositoryImplTest : SysuiTestCase() {
private val Kosmos.underTest by
Kosmos.Fixture {
CommunalSceneRepositoryImpl(
- applicationScope = applicationCoroutineScope,
backgroundScope = backgroundScope,
sceneDataSource = delegator,
- delegator = delegator,
)
}
@@ -90,18 +83,4 @@ class CommunalSceneRepositoryImplTest : SysuiTestCase() {
assertThat(transitionState)
.isEqualTo(ObservableTransitionState.Idle(CommunalScenes.Default))
}
-
- @Test
- fun showHubFromPowerButton() =
- kosmos.runTest {
- fakeKeyguardRepository.setKeyguardShowing(false)
-
- underTest.showHubFromPowerButton()
-
- argumentCaptor<SceneDataSource>().apply {
- verify(delegator).setDelegate(capture())
-
- assertThat(firstValue.currentScene.value).isEqualTo(CommunalScenes.Communal)
- }
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
index b65ecf46dcca..215d36fcb2a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt
@@ -36,6 +36,7 @@ import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
import com.android.systemui.Flags.FLAG_COMMUNAL_RESPONSIVE_GRID
import com.android.systemui.Flags.FLAG_COMMUNAL_WIDGET_RESIZING
import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
+import com.android.systemui.Flags.glanceableHubV2
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.communal.data.model.CommunalSmartspaceTimer
@@ -90,6 +91,7 @@ import platform.test.runner.parameterized.Parameters
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
+@EnableFlags(FLAG_COMMUNAL_HUB)
class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
private val mainUser =
UserInfo(/* id= */ 0, /* name= */ "primary user", /* flags= */ UserInfo.FLAG_MAIN)
@@ -110,7 +112,9 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
kosmos.fakeUserRepository.setUserInfos(listOf(mainUser, secondaryUser))
kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
- mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
+ if (glanceableHubV2()) {
+ kosmos.setCommunalV2ConfigEnabled(true)
+ }
}
@Test
@@ -120,7 +124,9 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
assertThat(underTest.isCommunalEnabled.value).isTrue()
}
+ /** Test not applicable when [FLAG_GLANCEABLE_HUB_V2] enabled */
@Test
+ @DisableFlags(FLAG_GLANCEABLE_HUB_V2)
fun isCommunalAvailable_whenKeyguardShowing_true() =
kosmos.runTest {
communalSettingsInteractor.setSuppressionReasons(emptyList())
@@ -1212,7 +1218,10 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() {
@JvmStatic
@Parameters(name = "{0}")
fun getParams(): List<FlagsParameterization> {
- return FlagsParameterization.allCombinationsOf(FLAG_COMMUNAL_RESPONSIVE_GRID)
+ return FlagsParameterization.allCombinationsOf(
+ FLAG_COMMUNAL_RESPONSIVE_GRID,
+ FLAG_GLANCEABLE_HUB_V2,
+ )
}
private val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
index b08e6761d92f..6b2207e0d754 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt
@@ -18,15 +18,18 @@ package com.android.systemui.communal.widgets
import android.appwidget.AppWidgetProviderInfo
import android.content.pm.UserInfo
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
import com.android.systemui.Flags.FLAG_COMMUNAL_HUB
+import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
import com.android.systemui.SysuiTestCase
import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.communalInteractor
import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.communal.domain.interactor.setCommunalEnabled
+import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled
import com.android.systemui.communal.shared.model.FakeGlanceableHubMultiUserHelper
import com.android.systemui.communal.shared.model.fakeGlanceableHubMultiUserHelper
import com.android.systemui.coroutines.collectLastValue
@@ -54,10 +57,13 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
@SmallTest
-@RunWith(AndroidJUnit4::class)
-class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
+@RunWith(ParameterizedAndroidJunit4::class)
+@EnableFlags(FLAG_COMMUNAL_HUB)
+class CommunalAppWidgetHostStartableTest(flags: FlagsParameterization) : SysuiTestCase() {
private val kosmos = testKosmos()
@Mock private lateinit var appWidgetHost: CommunalAppWidgetHost
@@ -71,12 +77,27 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
private lateinit var communalInteractorSpy: CommunalInteractor
private lateinit var underTest: CommunalAppWidgetHostStartable
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
+ companion object {
+ private val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
+ private val USER_INFO_WORK = UserInfo(10, "work", UserInfo.FLAG_PROFILE)
+
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf(FLAG_GLANCEABLE_HUB_V2)
+ }
+ }
+
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO, USER_INFO_WORK))
kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
- mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB)
+ kosmos.setCommunalV2ConfigEnabled(true)
widgetManager = kosmos.mockGlanceableHubWidgetManager
helper = kosmos.fakeGlanceableHubMultiUserHelper
@@ -327,9 +348,4 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() {
fakeKeyguardRepository.setKeyguardShowing(true)
}
}
-
- private companion object {
- val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
- val USER_INFO_WORK = UserInfo(10, "work", UserInfo.FLAG_PROFILE)
- }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
index 3895595aaea6..33c4c44111aa 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
@@ -16,6 +16,8 @@
package com.android.systemui.dreams;
+import static com.android.systemui.Flags.FLAG_BOUNCER_UI_REVAMP;
+
import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
@@ -32,6 +34,7 @@ import android.app.DreamManager;
import android.content.res.Resources;
import android.graphics.Region;
import android.os.Handler;
+import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.testing.TestableLooper.RunWithLooper;
import android.view.AttachedSurfaceControl;
@@ -231,6 +234,7 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase {
}
@Test
+ @DisableFlags(FLAG_BOUNCER_UI_REVAMP)
public void testBouncerAnimation_updateBlur() {
final ArgumentCaptor<PrimaryBouncerExpansionCallback> bouncerExpansionCaptor =
ArgumentCaptor.forClass(PrimaryBouncerExpansionCallback.class);
@@ -253,6 +257,26 @@ public class DreamOverlayContainerViewControllerTest extends SysuiTestCase {
}
@Test
+ @EnableFlags(FLAG_BOUNCER_UI_REVAMP)
+ public void testBouncerAnimation_doesNotBlur_whenBouncerRevampEnabled() {
+ final ArgumentCaptor<PrimaryBouncerExpansionCallback> bouncerExpansionCaptor =
+ ArgumentCaptor.forClass(PrimaryBouncerExpansionCallback.class);
+ mController.onViewAttached();
+ verify(mPrimaryBouncerCallbackInteractor).addBouncerExpansionCallback(
+ bouncerExpansionCaptor.capture());
+
+ final float blurRadius = 1337f;
+ when(mBlurUtils.blurRadiusOfRatio(anyFloat())).thenReturn(blurRadius);
+
+ bouncerExpansionCaptor.getValue().onStartingToShow();
+ final float bouncerHideAmount = 0.05f;
+
+ bouncerExpansionCaptor.getValue().onExpansionChanged(bouncerHideAmount);
+ verify(mBlurUtils, never()).blurRadiusOfRatio(anyFloat());
+ verify(mBlurUtils, never()).applyBlur(eq(mViewRoot), anyInt(), anyBoolean());
+ }
+
+ @Test
public void testStartDreamEntryAnimationsOnAttachedNonLowLight() {
when(mStateController.isLowLightActive()).thenReturn(false);
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
index fd99313a17b7..b74d53987503 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
@@ -74,6 +74,7 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.navigationbar.gestural.domain.GestureInteractor
import com.android.systemui.navigationbar.gestural.domain.TaskInfo
import com.android.systemui.navigationbar.gestural.domain.TaskMatcher
+import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.scene.data.repository.sceneContainerRepository
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Overlays
@@ -265,6 +266,8 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() {
mDreamOverlayCallbackController,
kosmos.keyguardInteractor,
gestureInteractor,
+ kosmos.wakeGestureMonitor,
+ kosmos.powerInteractor,
WINDOW_NAME,
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/WakeGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/WakeGestureMonitorTest.kt
new file mode 100644
index 000000000000..b5f8f7884d7f
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/WakeGestureMonitorTest.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2025 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.dreams
+
+import android.hardware.Sensor
+import android.hardware.TriggerEventListener
+import android.hardware.display.ambientDisplayConfiguration
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectValues
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.testKosmos
+import com.android.systemui.util.sensors.asyncSensorManager
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class WakeGestureMonitorTest : SysuiTestCase() {
+ private val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+ private val Kosmos.underTest by Kosmos.Fixture { wakeGestureMonitor }
+
+ @Test
+ fun testPickupGestureNotEnabled_doesNotSubscribeToSensor() =
+ kosmos.runTest {
+ ambientDisplayConfiguration.fakePickupGestureEnabled = false
+ val triggerSensor = stubSensorManager()
+
+ val wakeUpDetected by collectValues(underTest.wakeUpDetected)
+ triggerSensor()
+ assertThat(wakeUpDetected).isEmpty()
+ }
+
+ @Test
+ fun testPickupGestureEnabled_subscribesToSensor() =
+ kosmos.runTest {
+ ambientDisplayConfiguration.fakePickupGestureEnabled = true
+ val triggerSensor = stubSensorManager()
+
+ val wakeUpDetected by collectValues(underTest.wakeUpDetected)
+ triggerSensor()
+ assertThat(wakeUpDetected).hasSize(1)
+ triggerSensor()
+ assertThat(wakeUpDetected).hasSize(2)
+ }
+
+ private fun Kosmos.stubSensorManager(): () -> Unit {
+ val callbacks = mutableListOf<TriggerEventListener>()
+ val pickupSensor = mock<Sensor>()
+
+ asyncSensorManager.stub {
+ on { getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE) } doReturn pickupSensor
+ on { requestTriggerSensor(any(), eq(pickupSensor)) } doAnswer
+ {
+ val callback = it.arguments[0] as TriggerEventListener
+ callbacks.add(callback)
+ true
+ }
+ on { cancelTriggerSensor(any(), any()) } doAnswer
+ {
+ val callback = it.arguments[0] as TriggerEventListener
+ callbacks.remove(callback)
+ true
+ }
+ }
+
+ return {
+ val list = callbacks.toList()
+ // Simulate a trigger sensor which unregisters callbacks after triggering.
+ while (callbacks.isNotEmpty()) {
+ callbacks.removeLast()
+ }
+ list.forEach { it.onTrigger(mock()) }
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt
index 57b12990fb97..f88b8529866b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorTest.kt
@@ -21,8 +21,12 @@ import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
-import com.android.systemui.Flags
+import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2
+import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR
import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.data.repository.communalSceneRepository
+import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled
+import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepositorySpy
@@ -32,10 +36,12 @@ import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.testKosmos
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
+import com.google.common.truth.Truth
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
@@ -46,12 +52,10 @@ import org.mockito.Mockito.reset
@RunWith(AndroidJUnit4::class)
class FromGoneTransitionInteractorTest : SysuiTestCase() {
private val kosmos =
- testKosmos().apply {
+ testKosmos().useUnconfinedTestDispatcher().apply {
this.keyguardTransitionRepository = fakeKeyguardTransitionRepositorySpy
}
- private val testScope = kosmos.testScope
private val underTest = kosmos.fromGoneTransitionInteractor
- private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepositorySpy
@Before
fun setUp() {
@@ -61,8 +65,8 @@ class FromGoneTransitionInteractorTest : SysuiTestCase() {
@Test
@Ignore("Fails due to fix for b/324432820 - will re-enable once permanent fix is submitted.")
fun testDoesNotTransitionToLockscreen_ifStartedButNotFinishedInGone() =
- testScope.runTest {
- keyguardTransitionRepository.sendTransitionSteps(
+ kosmos.runTest {
+ fakeKeyguardTransitionRepositorySpy.sendTransitionSteps(
listOf(
TransitionStep(
from = KeyguardState.LOCKSCREEN,
@@ -77,54 +81,74 @@ class FromGoneTransitionInteractorTest : SysuiTestCase() {
),
testScope,
)
- reset(keyguardTransitionRepository)
- kosmos.fakeKeyguardRepository.setKeyguardShowing(true)
- runCurrent()
+ reset(fakeKeyguardTransitionRepositorySpy)
+ fakeKeyguardRepository.setKeyguardShowing(true)
// We're in the middle of a LOCKSCREEN -> GONE transition.
- assertThat(keyguardTransitionRepository).noTransitionsStarted()
+ assertThat(fakeKeyguardTransitionRepositorySpy).noTransitionsStarted()
}
@Test
- @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+ @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
fun testTransitionsToLockscreen_ifFinishedInGone() =
- testScope.runTest {
- keyguardTransitionRepository.sendTransitionSteps(
+ kosmos.runTest {
+ fakeKeyguardTransitionRepositorySpy.sendTransitionSteps(
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.GONE,
testScope,
)
- reset(keyguardTransitionRepository)
- kosmos.fakeKeyguardRepository.setKeyguardShowing(true)
- runCurrent()
+ reset(fakeKeyguardTransitionRepositorySpy)
+ fakeKeyguardRepository.setKeyguardShowing(true)
// We're in the middle of a GONE -> LOCKSCREEN transition.
- assertThat(keyguardTransitionRepository)
+ assertThat(fakeKeyguardTransitionRepositorySpy)
.startedTransition(to = KeyguardState.LOCKSCREEN)
}
@Test
- @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+ @EnableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
fun testTransitionsToLockscreen_ifFinishedInGone_wmRefactor() =
- testScope.runTest {
- keyguardTransitionRepository.sendTransitionSteps(
+ kosmos.runTest {
+ fakeKeyguardTransitionRepositorySpy.sendTransitionSteps(
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.GONE,
testScope,
)
- reset(keyguardTransitionRepository)
+ reset(fakeKeyguardTransitionRepositorySpy)
// Trigger lockdown.
- kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags(
+ fakeBiometricSettingsRepository.setAuthenticationFlags(
AuthenticationFlags(
0,
LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN,
)
)
- runCurrent()
// We're in the middle of a GONE -> LOCKSCREEN transition.
- assertThat(keyguardTransitionRepository)
+ assertThat(fakeKeyguardTransitionRepositorySpy)
.startedTransition(to = KeyguardState.LOCKSCREEN)
}
+
+ @Test
+ @EnableFlags(FLAG_GLANCEABLE_HUB_V2)
+ @DisableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR)
+ fun testTransitionToGlanceableHub() =
+ kosmos.runTest {
+ val currentScene by collectLastValue(communalSceneRepository.currentScene)
+
+ fakeKeyguardTransitionRepositorySpy.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.GONE,
+ testScope,
+ )
+ reset(fakeKeyguardTransitionRepositorySpy)
+ // Communal is enabled
+ setCommunalV2Enabled(true)
+ Truth.assertThat(currentScene).isEqualTo(CommunalScenes.Blank)
+
+ fakeKeyguardRepository.setKeyguardShowing(true)
+
+ Truth.assertThat(currentScene).isEqualTo(CommunalScenes.Communal)
+ assertThat(fakeKeyguardTransitionRepositorySpy).noTransitionsStarted()
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java
deleted file mode 100644
index b177e07d09b6..000000000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.java
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * Copyright (C) 2025 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.lowlightclock;
-
-import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT;
-import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR;
-import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.ComponentName;
-import android.content.pm.PackageManager;
-import android.testing.TestableLooper;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.dream.lowlight.LowLightDreamManager;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.keyguard.ScreenLifecycle;
-import com.android.systemui.shared.condition.Condition;
-import com.android.systemui.shared.condition.Monitor;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.time.FakeSystemClock;
-
-import dagger.Lazy;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.Set;
-
-@SmallTest
-@RunWith(AndroidJUnit4.class)
-@TestableLooper.RunWithLooper()
-public class LowLightMonitorTest extends SysuiTestCase {
-
- @Mock
- private Lazy<LowLightDreamManager> mLowLightDreamManagerLazy;
- @Mock
- private LowLightDreamManager mLowLightDreamManager;
- @Mock
- private Monitor mMonitor;
- @Mock
- private ScreenLifecycle mScreenLifecycle;
- @Mock
- private LowLightLogger mLogger;
-
- private LowLightMonitor mLowLightMonitor;
-
- @Mock
- Lazy<Set<Condition>> mLazyConditions;
-
- @Mock
- private PackageManager mPackageManager;
-
- @Mock
- private ComponentName mDreamComponent;
-
- FakeExecutor mBackgroundExecutor = new FakeExecutor(new FakeSystemClock());
-
- Condition mCondition = mock(Condition.class);
- Set<Condition> mConditionSet = Set.of(mCondition);
-
- @Captor
- ArgumentCaptor<Monitor.Subscription> mPreconditionsSubscriptionCaptor;
-
- @Before
- public void setUp() {
- MockitoAnnotations.initMocks(this);
- when(mLowLightDreamManagerLazy.get()).thenReturn(mLowLightDreamManager);
- when(mLazyConditions.get()).thenReturn(mConditionSet);
- mLowLightMonitor = new LowLightMonitor(mLowLightDreamManagerLazy,
- mMonitor, mLazyConditions, mScreenLifecycle, mLogger, mDreamComponent,
- mPackageManager, mBackgroundExecutor);
- }
-
- @Test
- public void testSetAmbientLowLightWhenInLowLight() {
- mLowLightMonitor.onConditionsChanged(true);
- mBackgroundExecutor.runAllReady();
- // Verify setting low light when condition is true
- verify(mLowLightDreamManager).setAmbientLightMode(AMBIENT_LIGHT_MODE_LOW_LIGHT);
- }
-
- @Test
- public void testExitAmbientLowLightWhenNotInLowLight() {
- mLowLightMonitor.onConditionsChanged(true);
- mLowLightMonitor.onConditionsChanged(false);
- mBackgroundExecutor.runAllReady();
- // Verify ambient light toggles back to light mode regular
- verify(mLowLightDreamManager).setAmbientLightMode(AMBIENT_LIGHT_MODE_REGULAR);
- }
-
- @Test
- public void testStartMonitorLowLightConditionsWhenScreenTurnsOn() {
- mLowLightMonitor.onScreenTurnedOn();
- mBackgroundExecutor.runAllReady();
-
- // Verify subscribing to low light conditions monitor when screen turns on.
- verify(mMonitor).addSubscription(any());
- }
-
- @Test
- public void testStopMonitorLowLightConditionsWhenScreenTurnsOff() {
- final Monitor.Subscription.Token token = mock(Monitor.Subscription.Token.class);
- when(mMonitor.addSubscription(any())).thenReturn(token);
- mLowLightMonitor.onScreenTurnedOn();
-
- // Verify removing subscription when screen turns off.
- mLowLightMonitor.onScreenTurnedOff();
- mBackgroundExecutor.runAllReady();
- verify(mMonitor).removeSubscription(token);
- }
-
- @Test
- public void testSubscribeToLowLightConditionsOnlyOnceWhenScreenTurnsOn() {
- final Monitor.Subscription.Token token = mock(Monitor.Subscription.Token.class);
- when(mMonitor.addSubscription(any())).thenReturn(token);
-
- mLowLightMonitor.onScreenTurnedOn();
- mLowLightMonitor.onScreenTurnedOn();
- mBackgroundExecutor.runAllReady();
- // Verify subscription is only added once.
- verify(mMonitor, times(1)).addSubscription(any());
- }
-
- @Test
- public void testSubscribedToExpectedConditions() {
- final Monitor.Subscription.Token token = mock(Monitor.Subscription.Token.class);
- when(mMonitor.addSubscription(any())).thenReturn(token);
-
- mLowLightMonitor.onScreenTurnedOn();
- mLowLightMonitor.onScreenTurnedOn();
- mBackgroundExecutor.runAllReady();
- Set<Condition> conditions = captureConditions();
- // Verify Monitor is subscribed to the expected conditions
- assertThat(conditions).isEqualTo(mConditionSet);
- }
-
- @Test
- public void testNotUnsubscribeIfNotSubscribedWhenScreenTurnsOff() {
- mLowLightMonitor.onScreenTurnedOff();
- mBackgroundExecutor.runAllReady();
- // Verify doesn't remove subscription since there is none.
- verify(mMonitor, never()).removeSubscription(any());
- }
-
- @Test
- public void testSubscribeIfScreenIsOnWhenStarting() {
- when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_ON);
- mLowLightMonitor.start();
- mBackgroundExecutor.runAllReady();
- // Verify to add subscription on start if the screen state is on
- verify(mMonitor, times(1)).addSubscription(any());
- }
-
- @Test
- public void testNoSubscribeIfDreamNotPresent() {
- LowLightMonitor lowLightMonitor = new LowLightMonitor(mLowLightDreamManagerLazy,
- mMonitor, mLazyConditions, mScreenLifecycle, mLogger, null, mPackageManager,
- mBackgroundExecutor);
- when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_ON);
- lowLightMonitor.start();
- mBackgroundExecutor.runAllReady();
- verify(mScreenLifecycle, never()).addObserver(any());
- }
-
- private Set<Condition> captureConditions() {
- verify(mMonitor).addSubscription(mPreconditionsSubscriptionCaptor.capture());
- return mPreconditionsSubscriptionCaptor.getValue().getConditions();
- }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.kt
new file mode 100644
index 000000000000..11f0f4394a85
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lowlightclock/LowLightMonitorTest.kt
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2025 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.lowlightclock
+
+import android.content.ComponentName
+import android.content.pm.PackageManager
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.dream.lowlight.LowLightDreamManager
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.biometrics.domain.interactor.displayStateInteractor
+import com.android.systemui.display.data.repository.displayRepository
+import com.android.systemui.kosmos.runCurrent
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.shared.condition.Condition
+import com.android.systemui.shared.condition.Monitor
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth
+import dagger.Lazy
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.clearInvocations
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@RunWithLooper
+class LowLightMonitorTest : SysuiTestCase() {
+ val kosmos = testKosmos().useUnconfinedTestDispatcher()
+
+ @Mock private lateinit var lowLightDreamManagerLazy: Lazy<LowLightDreamManager>
+
+ @Mock private lateinit var lowLightDreamManager: LowLightDreamManager
+
+ private val monitor: Monitor = prepareMonitor()
+
+ @Mock private lateinit var logger: LowLightLogger
+
+ private lateinit var lowLightMonitor: LowLightMonitor
+
+ @Mock private lateinit var lazyConditions: Lazy<Set<Condition>>
+
+ @Mock private lateinit var packageManager: PackageManager
+
+ @Mock private lateinit var dreamComponent: ComponentName
+
+ private val condition = mock<Condition>()
+
+ private val conditionSet = setOf(condition)
+
+ @Captor
+ private lateinit var preconditionsSubscriptionCaptor: ArgumentCaptor<Monitor.Subscription>
+
+ private fun prepareMonitor(): Monitor {
+ val monitor = mock<Monitor>()
+ whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(mock())
+
+ return monitor
+ }
+
+ private fun setDisplayOn(screenOn: Boolean) {
+ kosmos.displayRepository.setDefaultDisplayOff(!screenOn)
+ }
+
+ @Before
+ fun setUp() {
+ MockitoAnnotations.initMocks(this)
+ whenever(lowLightDreamManagerLazy.get()).thenReturn(lowLightDreamManager)
+ whenever(lazyConditions.get()).thenReturn(conditionSet)
+ lowLightMonitor =
+ LowLightMonitor(
+ lowLightDreamManagerLazy,
+ monitor,
+ lazyConditions,
+ kosmos.displayStateInteractor,
+ logger,
+ dreamComponent,
+ packageManager,
+ kosmos.testScope.backgroundScope,
+ )
+ whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(mock())
+ val subscriptionCaptor = argumentCaptor<Monitor.Subscription>()
+
+ setDisplayOn(false)
+
+ lowLightMonitor.start()
+ verify(monitor).addSubscription(subscriptionCaptor.capture())
+ clearInvocations(monitor)
+
+ subscriptionCaptor.firstValue.callback.onConditionsChanged(true)
+ }
+
+ private fun getConditionCallback(monitor: Monitor): Monitor.Callback {
+ val subscriptionCaptor = argumentCaptor<Monitor.Subscription>()
+ verify(monitor).addSubscription(subscriptionCaptor.capture())
+ return subscriptionCaptor.firstValue.callback
+ }
+
+ @Test
+ fun testSetAmbientLowLightWhenInLowLight() =
+ kosmos.runTest {
+ // Turn on screen
+ setDisplayOn(true)
+
+ // Set conditions to true
+ val callback = getConditionCallback(monitor)
+ callback.onConditionsChanged(true)
+
+ // Verify setting low light when condition is true
+ Mockito.verify(lowLightDreamManager)
+ .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
+ }
+
+ @Test
+ fun testExitAmbientLowLightWhenNotInLowLight() =
+ kosmos.runTest {
+ // Turn on screen
+ setDisplayOn(true)
+
+ // Set conditions to true then false
+ val callback = getConditionCallback(monitor)
+ callback.onConditionsChanged(true)
+ clearInvocations(lowLightDreamManager)
+ callback.onConditionsChanged(false)
+
+ // Verify ambient light toggles back to light mode regular
+ Mockito.verify(lowLightDreamManager)
+ .setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR)
+ }
+
+ @Test
+ fun testStopMonitorLowLightConditionsWhenScreenTurnsOff() =
+ kosmos.runTest {
+ val token = mock<Monitor.Subscription.Token>()
+ whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(token)
+
+ setDisplayOn(true)
+
+ // Verify removing subscription when screen turns off.
+ setDisplayOn(false)
+ Mockito.verify(monitor).removeSubscription(token)
+ }
+
+ @Test
+ fun testSubscribeToLowLightConditionsOnlyOnceWhenScreenTurnsOn() =
+ kosmos.runTest {
+ val token = mock<Monitor.Subscription.Token>()
+ whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(token)
+
+ setDisplayOn(true)
+ setDisplayOn(true)
+ // Verify subscription is only added once.
+ Mockito.verify(monitor, Mockito.times(1)).addSubscription(ArgumentMatchers.any())
+ }
+
+ @Test
+ fun testSubscribedToExpectedConditions() =
+ kosmos.runTest {
+ val token = mock<Monitor.Subscription.Token>()
+ whenever(monitor.addSubscription(ArgumentMatchers.any())).thenReturn(token)
+
+ setDisplayOn(true)
+
+ val conditions = captureConditions()
+ // Verify Monitor is subscribed to the expected conditions
+ Truth.assertThat(conditions).isEqualTo(conditionSet)
+ }
+
+ @Test
+ fun testNotUnsubscribeIfNotSubscribedWhenScreenTurnsOff() =
+ kosmos.runTest {
+ setDisplayOn(true)
+ clearInvocations(monitor)
+ setDisplayOn(false)
+ runCurrent()
+ // Verify doesn't remove subscription since there is none.
+ Mockito.verify(monitor).removeSubscription(ArgumentMatchers.any())
+ }
+
+ @Test
+ fun testSubscribeIfScreenIsOnWhenStarting() =
+ kosmos.runTest {
+ val monitor = prepareMonitor()
+
+ setDisplayOn(true)
+
+ val targetMonitor =
+ LowLightMonitor(
+ lowLightDreamManagerLazy,
+ monitor,
+ lazyConditions,
+ displayStateInteractor,
+ logger,
+ dreamComponent,
+ packageManager,
+ testScope.backgroundScope,
+ )
+
+ // start
+ targetMonitor.start()
+
+ val callback = getConditionCallback(monitor)
+ clearInvocations(monitor)
+ callback.onConditionsChanged(true)
+
+ // Verify to add subscription on start and when the screen state is on
+ Mockito.verify(monitor).addSubscription(ArgumentMatchers.any())
+ }
+
+ @Test
+ fun testNoSubscribeIfDreamNotPresent() =
+ kosmos.runTest {
+ val monitor = prepareMonitor()
+
+ setDisplayOn(true)
+
+ val lowLightMonitor =
+ LowLightMonitor(
+ lowLightDreamManagerLazy,
+ monitor,
+ lazyConditions,
+ displayStateInteractor,
+ logger,
+ null,
+ packageManager,
+ testScope,
+ )
+
+ // start
+ lowLightMonitor.start()
+
+ val callback = getConditionCallback(monitor)
+ clearInvocations(monitor)
+ callback.onConditionsChanged(true)
+
+ // Verify to add subscription on start and when the screen state is on
+ Mockito.verify(monitor, never()).addSubscription(ArgumentMatchers.any())
+ }
+
+ private fun captureConditions(): Set<Condition?> {
+ Mockito.verify(monitor).addSubscription(preconditionsSubscriptionCaptor.capture())
+ return preconditionsSubscriptionCaptor.value.conditions
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java
index 9c4d93c17d00..f7298dd0bf36 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterLegacyTest.java
@@ -67,6 +67,7 @@ import java.util.concurrent.Executor;
import java.util.stream.Collectors;
@SmallTest
+@DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_REDESIGN)
@RunWith(AndroidJUnit4.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class MediaOutputAdapterLegacyTest extends SysuiTestCase {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.kt
new file mode 100644
index 000000000000..70adfd324e94
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.kt
@@ -0,0 +1,763 @@
+/*
+ * Copyright (C) 2025 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.media.dialog
+
+import android.content.Context
+import android.graphics.drawable.Icon
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.testing.TestableLooper.RunWithLooper
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.widget.LinearLayout
+import androidx.appcompat.view.ContextThemeWrapper
+import androidx.core.graphics.drawable.IconCompat
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.RecyclerView
+import com.android.media.flags.Flags
+import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTED
+import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING
+import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_CONNECTING_FAILED
+import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_DISCONNECTED
+import com.android.settingslib.media.LocalMediaManager.MediaDeviceState.STATE_GROUPING
+import com.android.settingslib.media.MediaDevice
+import com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_GO_TO_APP
+import com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_NONE
+import com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.media.dialog.MediaItem.MediaItemType
+import com.android.systemui.media.dialog.MediaItem.createDeviceMediaItem
+import com.android.systemui.media.dialog.MediaOutputAdapter.MediaDeviceViewHolder
+import com.android.systemui.media.dialog.MediaOutputAdapter.MediaGroupDividerViewHolder
+import com.android.systemui.res.R
+import com.google.android.material.slider.Slider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doNothing
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.stub
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@EnableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_REDESIGN)
+@RunWith(AndroidJUnit4::class)
+@RunWithLooper(setAsMainLooper = true)
+class MediaOutputAdapterTest : SysuiTestCase() {
+ private val mMediaSwitchingController = mock<MediaSwitchingController>()
+ private val mMediaDevice1: MediaDevice = mock<MediaDevice>()
+ private val mMediaDevice2: MediaDevice = mock<MediaDevice>()
+ private val mIcon: Icon = mock<Icon>()
+ private val mIconCompat: IconCompat = mock<IconCompat>()
+ private lateinit var mMediaOutputAdapter: MediaOutputAdapter
+ private val mMediaItems: MutableList<MediaItem> = ArrayList()
+
+ @Before
+ fun setUp() {
+ mMediaSwitchingController.stub {
+ on { getMediaItemList(false) } doReturn mMediaItems
+ on { hasAdjustVolumeUserRestriction() } doReturn false
+ on { isAnyDeviceTransferring } doReturn false
+ on { currentConnectedMediaDevice } doReturn mMediaDevice1
+ on { connectedSpeakersExpandableGroupDivider }
+ .doReturn(
+ MediaItem.createExpandableGroupDividerMediaItem(
+ mContext.getString(R.string.media_output_group_title_connected_speakers)
+ )
+ )
+ on { sessionVolumeMax } doReturn TEST_MAX_VOLUME
+ on { sessionVolume } doReturn TEST_CURRENT_VOLUME
+ on { sessionName } doReturn TEST_SESSION_NAME
+ on { colorSchemeLegacy } doReturn mock<MediaOutputColorSchemeLegacy>()
+ on { colorScheme } doReturn mock<MediaOutputColorScheme>()
+ }
+
+ mIconCompat.stub { on { toIcon(mContext) } doReturn mIcon }
+
+ mMediaDevice1
+ .stub {
+ on { id } doReturn TEST_DEVICE_ID_1
+ on { name } doReturn TEST_DEVICE_NAME_1
+ }
+ .also {
+ whenever(mMediaSwitchingController.getDeviceIconCompat(it)) doReturn mIconCompat
+ }
+
+ mMediaDevice2
+ .stub {
+ on { id } doReturn TEST_DEVICE_ID_2
+ on { name } doReturn TEST_DEVICE_NAME_2
+ }
+ .also {
+ whenever(mMediaSwitchingController.getDeviceIconCompat(it)) doReturn mIconCompat
+ }
+
+ mMediaOutputAdapter = MediaOutputAdapter(mMediaSwitchingController)
+ }
+
+ @Test
+ fun getItemCount_returnsMediaItemSize() {
+ updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2))
+
+ assertThat(mMediaOutputAdapter.itemCount).isEqualTo(mMediaItems.size)
+ }
+
+ @Test
+ fun getItemId_forDifferentItemsTypes_returnCorrespondingHashCode() {
+ updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2))
+
+ assertThat(mMediaOutputAdapter.getItemId(0))
+ .isEqualTo(mMediaItems[0].mediaDevice.get().id.hashCode())
+ }
+
+ @Test
+ fun getItemId_invalidPosition_returnPosition() {
+ updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2))
+ val invalidPosition = mMediaItems.size + 1
+
+ assertThat(mMediaOutputAdapter.getItemId(invalidPosition)).isEqualTo(RecyclerView.NO_ID)
+ }
+
+ @Test
+ fun onBindViewHolder_bindDisconnectedDevice_verifyView() {
+ mMediaDevice2.stub { on { state } doReturn STATE_DISCONNECTED }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleIcon.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2)
+ assertThat(mSlider.visibility).isEqualTo(GONE)
+ }
+ }
+
+ @Test
+ fun onBindViewHolder_bindConnectedDevice_verifyView() {
+ mMediaDevice1.stub { on { state } doReturn STATE_CONNECTED }
+ updateAdapterWithDevices(listOf(mMediaDevice1))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleIcon.visibility).isEqualTo(GONE)
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1)
+ assertThat(mSlider.visibility).isEqualTo(VISIBLE)
+ }
+ }
+
+ @Test
+ fun onBindViewHolder_isMutingExpectedDevice_verifyView() {
+ mMediaDevice1.stub {
+ on { isMutingExpectedDevice } doReturn true
+ on { state } doReturn STATE_DISCONNECTED
+ }
+ mMediaSwitchingController.stub { on { isCurrentConnectedDeviceRemote } doReturn false }
+ updateAdapterWithDevices(listOf(mMediaDevice1))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1)
+ assertThat(mLoadingIndicator.visibility).isEqualTo(GONE)
+ assertThat(mSlider.visibility).isEqualTo(GONE)
+ assertThat(mGroupButton.visibility).isEqualTo(GONE)
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ }
+ }
+
+ @Test
+ fun onBindViewHolder_bindConnectedDeviceWithMutingExpectedDeviceExist_verifyView() {
+ mMediaDevice1.stub {
+ on { isMutingExpectedDevice } doReturn true
+ on { state } doReturn STATE_CONNECTED
+ }
+ mMediaSwitchingController.stub {
+ on { hasMutingExpectedDevice() } doReturn true
+ on { isCurrentConnectedDeviceRemote } doReturn false
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice1))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mLoadingIndicator.visibility).isEqualTo(GONE)
+ assertThat(mSlider.visibility).isEqualTo(GONE)
+ assertThat(mGroupButton.visibility).isEqualTo(GONE)
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1)
+ }
+ }
+
+ @Test
+ fun onBindViewHolder_initSeekbar_setsVolume() {
+ mMediaDevice1.stub {
+ on { state } doReturn STATE_CONNECTED
+ on { maxVolume } doReturn TEST_MAX_VOLUME
+ on { currentVolume } doReturn TEST_CURRENT_VOLUME
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice1))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mSlider.visibility).isEqualTo(VISIBLE)
+ assertThat(mSlider.value).isEqualTo(TEST_CURRENT_VOLUME)
+ assertThat(mSlider.valueFrom).isEqualTo(0)
+ assertThat(mSlider.valueTo).isEqualTo(TEST_MAX_VOLUME)
+ }
+ }
+
+ @Test
+ fun onBindViewHolder_dragSeekbar_adjustsVolume() {
+ mMediaDevice1.stub {
+ on { maxVolume } doReturn TEST_MAX_VOLUME
+ on { currentVolume } doReturn TEST_CURRENT_VOLUME
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice1))
+
+ val viewHolder =
+ mMediaOutputAdapter.onCreateViewHolder(
+ LinearLayout(mContext),
+ MediaItemType.TYPE_DEVICE,
+ ) as MediaDeviceViewHolder
+
+ var sliderChangeListener: Slider.OnChangeListener? = null
+ viewHolder.mSlider =
+ object : Slider(contextWithTheme(mContext)) {
+ override fun addOnChangeListener(listener: OnChangeListener) {
+ sliderChangeListener = listener
+ }
+ }
+ mMediaOutputAdapter.onBindViewHolder(viewHolder, 0)
+ sliderChangeListener?.onValueChange(viewHolder.mSlider, 5f, true)
+
+ verify(mMediaSwitchingController).adjustVolume(mMediaDevice1, 5)
+ }
+
+ @Test
+ fun onBindViewHolder_dragSeekbar_logsInteraction() {
+ mMediaDevice1
+ .stub {
+ on { maxVolume } doReturn TEST_MAX_VOLUME
+ on { currentVolume } doReturn TEST_CURRENT_VOLUME
+ }
+ .also { mMediaItems.add(createDeviceMediaItem(it)) }
+ updateAdapterWithDevices(listOf(mMediaDevice1))
+
+ val viewHolder =
+ mMediaOutputAdapter.onCreateViewHolder(
+ LinearLayout(mContext),
+ MediaItemType.TYPE_DEVICE,
+ ) as MediaDeviceViewHolder
+
+ var sliderTouchListener: Slider.OnSliderTouchListener? = null
+ viewHolder.mSlider =
+ object : Slider(contextWithTheme(mContext)) {
+ override fun addOnSliderTouchListener(listener: OnSliderTouchListener) {
+ sliderTouchListener = listener
+ }
+ }
+ mMediaOutputAdapter.onBindViewHolder(viewHolder, 0)
+ sliderTouchListener?.onStopTrackingTouch(viewHolder.mSlider)
+
+ verify(mMediaSwitchingController).logInteractionAdjustVolume(mMediaDevice1)
+ }
+
+ @Test
+ fun onBindViewHolder_bindSelectableDevice_verifyView() {
+ mMediaSwitchingController.stub {
+ on { selectableMediaDevice } doReturn listOf(mMediaDevice2)
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 1).apply {
+ assertThat(mLoadingIndicator.visibility).isEqualTo(GONE)
+ assertThat(mDivider.visibility).isEqualTo(VISIBLE)
+ assertThat(mGroupButton.visibility).isEqualTo(VISIBLE)
+ assertThat(mGroupButton.contentDescription)
+ .isEqualTo(mContext.getString(R.string.accessibility_add_device_to_group))
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2)
+
+ mGroupButton.performClick()
+ }
+ verify(mMediaSwitchingController).addDeviceToPlayMedia(mMediaDevice2)
+ }
+
+ @Test
+ fun onBindViewHolder_bindDeselectableDevice_verifyView() {
+ mMediaSwitchingController.stub {
+ on { selectedMediaDevice } doReturn listOf(mMediaDevice1, mMediaDevice2)
+ on { deselectableMediaDevice } doReturn listOf(mMediaDevice1, mMediaDevice2)
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 1).apply {
+ assertThat(mGroupButton.visibility).isEqualTo(VISIBLE)
+ assertThat(mGroupButton.contentDescription)
+ .isEqualTo(mContext.getString(R.string.accessibility_remove_device_from_group))
+ mGroupButton.performClick()
+ }
+
+ verify(mMediaSwitchingController).removeDeviceFromPlayMedia(mMediaDevice2)
+ }
+
+ @Test
+ fun onBindViewHolder_bindNonDeselectableDevice_verifyView() {
+ mMediaSwitchingController.stub {
+ on { selectedMediaDevice } doReturn listOf(mMediaDevice1)
+ on { deselectableMediaDevice } doReturn ArrayList()
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1)
+ assertThat(mGroupButton.visibility).isEqualTo(GONE)
+ }
+ }
+
+ @Test
+ fun onBindViewHolder_bindFailedStateDevice_verifyView() {
+ mMediaDevice2.stub { on { state } doReturn STATE_CONNECTING_FAILED }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mStatusIcon.visibility).isEqualTo(VISIBLE)
+ assertThat(mSubTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mSubTitleText.text.toString())
+ .isEqualTo(mContext.getText(R.string.media_output_dialog_connect_failed).toString())
+ }
+ }
+
+ @Test
+ fun onBindViewHolder_deviceHasSubtext_displaySubtitle() {
+ mMediaDevice2.stub {
+ on { state } doReturn STATE_DISCONNECTED
+ on { hasSubtext() } doReturn true
+ on { subtextString } doReturn TEST_CUSTOM_SUBTEXT
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2)
+ assertThat(mSubTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mSubTitleText.text.toString()).isEqualTo(TEST_CUSTOM_SUBTEXT)
+ }
+ }
+
+ @Test
+ fun onBindViewHolder_deviceWithOngoingSession_displaysGoToAppButton() {
+ mMediaDevice2.stub {
+ on { state } doReturn STATE_DISCONNECTED
+ on { hasOngoingSession() } doReturn true
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ val viewHolder =
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2)
+ assertThat(mOngoingSessionButton.visibility).isEqualTo(VISIBLE)
+ assertThat(mOngoingSessionButton.contentDescription)
+ .isEqualTo(mContext.getString(R.string.accessibility_open_application))
+ mOngoingSessionButton.performClick()
+ }
+
+ verify(mMediaSwitchingController)
+ .tryToLaunchInAppRoutingIntent(TEST_DEVICE_ID_2, viewHolder.mOngoingSessionButton)
+ }
+
+ @Test
+ fun onItemClick_selectionBehaviorTransfer_connectsDevice() {
+ mMediaDevice2.stub {
+ on { state } doReturn STATE_DISCONNECTED
+ on { selectionBehavior } doReturn SELECTION_BEHAVIOR_TRANSFER
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply { mMainContent.performClick() }
+
+ verify(mMediaSwitchingController).connectDevice(mMediaDevice2)
+ }
+
+ @Test
+ fun onItemClick_selectionBehaviorTransferAndSessionHost_showsEndSessionDialog() {
+ mMediaSwitchingController.stub {
+ on { isCurrentOutputDeviceHasSessionOngoing() } doReturn true
+ }
+ mMediaDevice2.stub {
+ on { state } doReturn STATE_DISCONNECTED
+ on { selectionBehavior } doReturn SELECTION_BEHAVIOR_TRANSFER
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ val viewHolder =
+ mMediaOutputAdapter.onCreateViewHolder(
+ LinearLayout(mContext),
+ MediaItemType.TYPE_DEVICE,
+ ) as MediaDeviceViewHolder
+ val spyMediaDeviceViewHolder = spy(viewHolder)
+ doNothing().whenever(spyMediaDeviceViewHolder).showCustomEndSessionDialog(mMediaDevice2)
+
+ mMediaOutputAdapter.onBindViewHolder(spyMediaDeviceViewHolder, 0)
+ spyMediaDeviceViewHolder.mMainContent.performClick()
+
+ verify(mMediaSwitchingController, never()).connectDevice(ArgumentMatchers.any())
+ verify(spyMediaDeviceViewHolder).showCustomEndSessionDialog(mMediaDevice2)
+ }
+
+ @Test
+ fun onItemClick_selectionBehaviorGoToApp_sendsLaunchIntent() {
+ mMediaDevice2.stub {
+ on { state } doReturn STATE_DISCONNECTED
+ on { selectionBehavior } doReturn SELECTION_BEHAVIOR_GO_TO_APP
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ val viewHolder =
+ createAndBindDeviceViewHolder(position = 0).apply { mMainContent.performClick() }
+ verify(mMediaSwitchingController)
+ .tryToLaunchInAppRoutingIntent(TEST_DEVICE_ID_2, viewHolder.mMainContent)
+ }
+
+ @Test
+ fun onItemClick_selectionBehaviorNone_doesNothing() {
+ mMediaDevice2.stub {
+ on { state } doReturn STATE_DISCONNECTED
+ on { selectionBehavior } doReturn SELECTION_BEHAVIOR_NONE
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+ createAndBindDeviceViewHolder(position = 0).apply { mMainContent.performClick() }
+
+ verify(mMediaSwitchingController, never()).tryToLaunchInAppRoutingIntent(any(), any())
+ verify(mMediaSwitchingController, never()).connectDevice(any())
+ }
+
+ @DisableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT)
+ @Test
+ fun clickFullItemOfSelectableDevice_flagOff_verifyConnectDevice() {
+ mMediaSwitchingController.stub {
+ on { selectableMediaDevice } doReturn listOf(mMediaDevice2)
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2)
+ mMainContent.performClick()
+ }
+ verify(mMediaSwitchingController).connectDevice(mMediaDevice2)
+ }
+
+ @EnableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT)
+ @Test
+ fun clickFullItemOfSelectableDevice_flagOn_hasListingPreference_verifyConnectDevice() {
+ mMediaDevice2.stub { on { hasRouteListingPreferenceItem() } doReturn true }
+ mMediaSwitchingController.stub {
+ on { selectableMediaDevice } doReturn listOf(mMediaDevice2)
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2)
+ mMainContent.performClick()
+ }
+ verify(mMediaSwitchingController).connectDevice(mMediaDevice2)
+ }
+
+ @EnableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT)
+ @Test
+ fun clickFullItemOfSelectableDevice_flagOn_isTransferable_verifyConnectDevice() {
+ mMediaSwitchingController.stub {
+ on { selectableMediaDevice } doReturn listOf(mMediaDevice2)
+ on { transferableMediaDevices } doReturn listOf(mMediaDevice2)
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2)
+ mMainContent.performClick()
+ }
+ verify(mMediaSwitchingController).connectDevice(mMediaDevice2)
+ }
+
+ @EnableFlags(Flags.FLAG_DISABLE_TRANSFER_WHEN_APPS_DO_NOT_SUPPORT)
+ @Test
+ fun clickFullItemOfSelectableDevice_flagOn_notTransferable_verifyNotConnectDevice() {
+ mMediaDevice2.stub { on { hasRouteListingPreferenceItem() } doReturn false }
+ mMediaSwitchingController.stub {
+ on { selectableMediaDevice } doReturn listOf(mMediaDevice2)
+ on { transferableMediaDevices } doReturn listOf()
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2)
+ mMainContent.performClick()
+ }
+ verify(mMediaSwitchingController, never()).connectDevice(any())
+ }
+
+ @Test
+ fun onBindViewHolder_inTransferring_bindTransferringDevice_verifyView() {
+ mMediaSwitchingController.stub { on { isAnyDeviceTransferring() } doReturn true }
+ mMediaDevice2.stub { on { state } doReturn STATE_CONNECTING }
+ updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2))
+
+ // Connected device, looks like disconnected during transfer
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1)
+ assertThat(mSlider.visibility).isEqualTo(GONE)
+ assertThat(mLoadingIndicator.visibility).isEqualTo(GONE)
+ }
+
+ // Connecting device
+ createAndBindDeviceViewHolder(position = 1).apply {
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2)
+ assertThat(mSlider.visibility).isEqualTo(GONE)
+ assertThat(mLoadingIndicator.visibility).isEqualTo(VISIBLE)
+ }
+ }
+
+ @Test
+ fun onBindViewHolder_bindGroupingDevice_verifyView() {
+ mMediaDevice1.stub { on { state } doReturn STATE_GROUPING }
+ updateAdapterWithDevices(listOf(mMediaDevice1))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1)
+ assertThat(mSlider.visibility).isEqualTo(GONE)
+ assertThat(mSubTitleText.visibility).isEqualTo(GONE)
+ assertThat(mGroupButton.visibility).isEqualTo(GONE)
+ assertThat(mLoadingIndicator.visibility).isEqualTo(VISIBLE)
+ }
+ }
+
+ @Test
+ fun onItemClick_clicksWithMutingExpectedDeviceExist_cancelsMuteAwaitConnection() {
+ mMediaSwitchingController.stub {
+ on { hasMutingExpectedDevice() } doReturn true
+ on { isCurrentConnectedDeviceRemote() } doReturn false
+ }
+ mMediaDevice1.stub { on { isMutingExpectedDevice } doReturn false }
+ updateAdapterWithDevices(listOf(mMediaDevice1))
+
+ createAndBindDeviceViewHolder(position = 0).apply { mMainContent.performClick() }
+ verify(mMediaSwitchingController).cancelMuteAwaitConnection()
+ }
+
+ @Test
+ fun onGroupActionTriggered_clicksSelectableDevice_triggerGrouping() {
+ mMediaSwitchingController.stub {
+ on { selectableMediaDevice } doReturn listOf(mMediaDevice2)
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply { mGroupButton.performClick() }
+ verify(mMediaSwitchingController).addDeviceToPlayMedia(mMediaDevice2)
+ }
+
+ @Test
+ fun onGroupActionTriggered_clickSelectedRemoteDevice_triggerUngrouping() {
+ mMediaSwitchingController.stub {
+ on { selectableMediaDevice } doReturn listOf(mMediaDevice2)
+ on { selectedMediaDevice } doReturn listOf(mMediaDevice1)
+ on { deselectableMediaDevice } doReturn listOf(mMediaDevice1)
+ on { isCurrentConnectedDeviceRemote } doReturn true
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2))
+
+ createAndBindDeviceViewHolder(position = 0).apply { mGroupButton.performClick() }
+ verify(mMediaSwitchingController).removeDeviceFromPlayMedia(mMediaDevice1)
+ }
+
+ @Test
+ fun onBindViewHolder_hasVolumeAdjustmentRestriction_verifySeekbarDisabled() {
+ mMediaSwitchingController.stub {
+ on { isCurrentConnectedDeviceRemote } doReturn true
+ on { hasAdjustVolumeUserRestriction() } doReturn true
+ }
+ mMediaDevice1.stub { on { state } doReturn STATE_CONNECTED }
+ updateAdapterWithDevices(listOf(mMediaDevice1))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mSlider.visibility).isEqualTo(GONE)
+ }
+ }
+
+ @Test
+ fun onBindViewHolder_volumeControlChangeToEnabled_enableSeekbarAgain() {
+ mMediaSwitchingController.stub {
+ on { isVolumeControlEnabled(mMediaDevice1) } doReturn false
+ }
+ mMediaDevice1.stub {
+ on { state } doReturn STATE_CONNECTED
+ on { currentVolume } doReturn TEST_CURRENT_VOLUME
+ on { maxVolume } doReturn TEST_MAX_VOLUME
+ }
+ updateAdapterWithDevices(listOf(mMediaDevice1))
+
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mSlider.visibility).isEqualTo(VISIBLE)
+ assertThat(mSlider.isEnabled).isFalse()
+ }
+
+ mMediaSwitchingController.stub {
+ on { isVolumeControlEnabled(mMediaDevice1) } doReturn true
+ }
+ createAndBindDeviceViewHolder(position = 0).apply {
+ assertThat(mSlider.visibility).isEqualTo(VISIBLE)
+ assertThat(mSlider.isEnabled).isTrue()
+ }
+ }
+
+ @Test
+ fun updateItems_controllerItemsUpdated_notUpdatesInAdapterUntilUpdateItems() {
+ mMediaOutputAdapter.updateItems()
+ val updatedList: MutableList<MediaItem> = ArrayList()
+ updatedList.add(MediaItem.createDeviceGroupMediaItem())
+ whenever(mMediaSwitchingController.getMediaItemList(false)).doReturn(updatedList)
+ assertThat(mMediaOutputAdapter.itemCount).isEqualTo(mMediaItems.size)
+
+ mMediaOutputAdapter.updateItems()
+ assertThat(mMediaOutputAdapter.itemCount).isEqualTo(updatedList.size)
+ }
+
+ @Test
+ fun multipleSelectedDevices_listCollapsed_verifyItemTypes() {
+ mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn true }
+ initializeSession()
+
+ with(mMediaOutputAdapter) {
+ assertThat(itemCount).isEqualTo(2)
+ assertThat(getItemViewType(0)).isEqualTo(MediaItemType.TYPE_GROUP_DIVIDER)
+ assertThat(getItemViewType(1)).isEqualTo(MediaItemType.TYPE_DEVICE_GROUP)
+ }
+ }
+
+ @Test
+ fun multipleSelectedDevices_listCollapsed_verifySessionControl() {
+ mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn true }
+ initializeSession()
+
+ createAndBindDeviceViewHolder(position = 1).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_SESSION_NAME)
+ assertThat(mSlider.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mSlider.value).isEqualTo(TEST_CURRENT_VOLUME)
+ }
+
+ val viewHolder =
+ mMediaOutputAdapter.onCreateViewHolder(
+ LinearLayout(mContext),
+ MediaItemType.TYPE_DEVICE_GROUP,
+ ) as MediaDeviceViewHolder
+
+ var sliderChangeListener: Slider.OnChangeListener? = null
+ viewHolder.mSlider =
+ object : Slider(contextWithTheme(mContext)) {
+ override fun addOnChangeListener(listener: OnChangeListener) {
+ sliderChangeListener = listener
+ }
+ }
+ mMediaOutputAdapter.onBindViewHolder(viewHolder, 1)
+ sliderChangeListener?.onValueChange(viewHolder.mSlider, 7f, true)
+
+ verify(mMediaSwitchingController).adjustSessionVolume(7)
+ }
+
+ @Test
+ fun multipleSelectedDevices_expandIconClicked_verifyIndividualDevices() {
+ mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn true }
+ initializeSession()
+
+ val groupDividerViewHolder =
+ mMediaOutputAdapter.onCreateViewHolder(
+ LinearLayout(mContext),
+ MediaItemType.TYPE_GROUP_DIVIDER,
+ ) as MediaGroupDividerViewHolder
+ mMediaOutputAdapter.onBindViewHolder(groupDividerViewHolder, 0)
+
+ mMediaSwitchingController.stub { on { isGroupListCollapsed } doReturn false }
+ groupDividerViewHolder.mExpandButton.performClick()
+
+ createAndBindDeviceViewHolder(position = 1).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_1)
+ assertThat(mSlider.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mGroupButton.visibility).isEqualTo(VISIBLE)
+ }
+
+ createAndBindDeviceViewHolder(position = 2).apply {
+ assertThat(mTitleText.text.toString()).isEqualTo(TEST_DEVICE_NAME_2)
+ assertThat(mSlider.visibility).isEqualTo(VISIBLE)
+ assertThat(mTitleText.visibility).isEqualTo(VISIBLE)
+ assertThat(mGroupButton.visibility).isEqualTo(VISIBLE)
+ }
+ }
+
+ private fun contextWithTheme(context: Context) =
+ ContextThemeWrapper(
+ context,
+ com.google.android.material.R.style.Theme_Material3_DynamicColors_DayNight,
+ )
+
+ private fun updateAdapterWithDevices(deviceList: List<MediaDevice>) {
+ for (device in deviceList) {
+ mMediaItems.add(createDeviceMediaItem(device))
+ }
+ mMediaOutputAdapter.updateItems()
+ }
+
+ private fun createAndBindDeviceViewHolder(position: Int): MediaDeviceViewHolder {
+ val viewHolder =
+ mMediaOutputAdapter.onCreateViewHolder(
+ LinearLayout(mContext),
+ mMediaOutputAdapter.getItemViewType(position),
+ )
+ if (viewHolder is MediaDeviceViewHolder) {
+ mMediaOutputAdapter.onBindViewHolder(viewHolder, position)
+ return viewHolder
+ } else {
+ throw RuntimeException("ViewHolder for position $position is not MediaDeviceViewHolder")
+ }
+ }
+
+ private fun initializeSession() {
+ val selectedDevices = listOf(mMediaDevice1, mMediaDevice2)
+ mMediaSwitchingController.stub {
+ on { selectableMediaDevice } doReturn selectedDevices
+ on { selectedMediaDevice } doReturn selectedDevices
+ on { deselectableMediaDevice } doReturn selectedDevices
+ }
+ mMediaOutputAdapter = MediaOutputAdapter(mMediaSwitchingController)
+ updateAdapterWithDevices(listOf(mMediaDevice1, mMediaDevice2))
+ }
+
+ companion object {
+ private const val TEST_DEVICE_NAME_1 = "test_device_name_1"
+ private const val TEST_DEVICE_NAME_2 = "test_device_name_2"
+ private const val TEST_DEVICE_ID_1 = "test_device_id_1"
+ private const val TEST_DEVICE_ID_2 = "test_device_id_2"
+ private const val TEST_SESSION_NAME = "test_session_name"
+ private const val TEST_CUSTOM_SUBTEXT = "custom subtext"
+
+ private const val TEST_MAX_VOLUME = 20
+ private const val TEST_CURRENT_VOLUME = 10
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt
index 402b53c12bda..7664e2ad3072 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/BlurUtilsTest.kt
@@ -19,22 +19,23 @@ package com.android.systemui.statusbar
import android.content.res.Resources
import android.view.CrossWindowBlurListeners
import android.view.SurfaceControl
+import android.view.SyncRtSurfaceTransactionApplier
import android.view.ViewRootImpl
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.dump.DumpManager
import com.android.systemui.keyguard.ui.transitions.BlurConfig
+import com.google.common.truth.Truth.assertThat
+import junit.framework.TestCase.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
import org.mockito.Mock
-import org.mockito.Mockito.any
-import org.mockito.Mockito.anyInt
import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.eq
import org.mockito.Mockito.mock
-import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
@@ -45,10 +46,13 @@ class BlurUtilsTest : SysuiTestCase() {
val blurConfig: BlurConfig = BlurConfig(minBlurRadiusPx = 1.0f, maxBlurRadiusPx = 100.0f)
@Mock lateinit var dumpManager: DumpManager
- @Mock lateinit var transaction: SurfaceControl.Transaction
@Mock lateinit var crossWindowBlurListeners: CrossWindowBlurListeners
@Mock lateinit var resources: Resources
- lateinit var blurUtils: TestableBlurUtils
+ @Mock lateinit var syncRTTransactionApplier: SyncRtSurfaceTransactionApplier
+ @Mock lateinit var transaction: SurfaceControl.Transaction
+ @Captor
+ private lateinit var captor: ArgumentCaptor<SyncRtSurfaceTransactionApplier.SurfaceParams>
+ private lateinit var blurUtils: TestableBlurUtils
@Before
fun setup() {
@@ -77,9 +81,10 @@ class BlurUtilsTest : SysuiTestCase() {
`when`(viewRootImpl.surfaceControl).thenReturn(surfaceControl)
`when`(surfaceControl.isValid).thenReturn(true)
blurUtils.applyBlur(viewRootImpl, radius, true /* opaque */)
- verify(transaction).setBackgroundBlurRadius(eq(surfaceControl), eq(radius))
- verify(transaction).setOpaque(eq(surfaceControl), eq(true))
- verify(transaction).apply()
+
+ verify(syncRTTransactionApplier).scheduleApply(captor.capture())
+ assertThat(captor.value.opaque).isTrue()
+ assertEquals(radius, captor.value.backgroundBlurRadius)
}
@Test
@@ -92,9 +97,10 @@ class BlurUtilsTest : SysuiTestCase() {
blurUtils.blursEnabled = false
blurUtils.applyBlur(viewRootImpl, radius, true /* opaque */)
- verify(transaction).setOpaque(eq(surfaceControl), eq(true))
- verify(transaction, never()).setBackgroundBlurRadius(any(), anyInt())
- verify(transaction).apply()
+
+ verify(syncRTTransactionApplier).scheduleApply(captor.capture())
+ assertThat(captor.value.opaque).isTrue()
+ assertEquals(0 /* unset value */, captor.value.backgroundBlurRadius)
}
@Test
@@ -102,24 +108,32 @@ class BlurUtilsTest : SysuiTestCase() {
val radius = 10
val surfaceControl = mock(SurfaceControl::class.java)
val viewRootImpl = mock(ViewRootImpl::class.java)
+ val tmpFloatArray = FloatArray(0)
`when`(viewRootImpl.surfaceControl).thenReturn(surfaceControl)
`when`(surfaceControl.isValid).thenReturn(true)
blurUtils.applyBlur(viewRootImpl, radius, true /* opaque */)
+
+ verify(syncRTTransactionApplier).scheduleApply(captor.capture())
+ assertThat(captor.value.opaque).isTrue()
+ SyncRtSurfaceTransactionApplier.applyParams(transaction, captor.value, tmpFloatArray)
verify(transaction).setEarlyWakeupStart()
+
+ clearInvocations(syncRTTransactionApplier)
clearInvocations(transaction)
blurUtils.applyBlur(viewRootImpl, 0, true /* opaque */)
+ verify(syncRTTransactionApplier).scheduleApply(captor.capture())
+ SyncRtSurfaceTransactionApplier.applyParams(transaction, captor.value, tmpFloatArray)
verify(transaction).setEarlyWakeupEnd()
}
- inner class TestableBlurUtils : BlurUtils(resources, blurConfig, crossWindowBlurListeners, dumpManager) {
+ inner class TestableBlurUtils :
+ BlurUtils(resources, blurConfig, crossWindowBlurListeners, dumpManager) {
var blursEnabled = true
+ override val transactionApplier: SyncRtSurfaceTransactionApplier
+ get() = syncRTTransactionApplier
override fun supportsBlursOnWindows(): Boolean {
return blursEnabled
}
-
- override fun createTransaction(): SurfaceControl.Transaction {
- return transaction
- }
}
}
diff --git a/packages/SystemUI/res/drawable/ic_add_circle_rounded.xml b/packages/SystemUI/res/drawable/ic_add_circle_rounded.xml
new file mode 100644
index 000000000000..467a3813e461
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_add_circle_rounded.xml
@@ -0,0 +1,27 @@
+<!--
+ ~ Copyright (C) 2025 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <group>
+ <path
+ android:pathData="M11,13V16C11,16.283 11.092,16.525 11.275,16.725C11.475,16.908 11.717,17 12,17C12.283,17 12.517,16.908 12.7,16.725C12.9,16.525 13,16.283 13,16V13H16C16.283,13 16.517,12.908 16.7,12.725C16.9,12.525 17,12.283 17,12C17,11.717 16.9,11.483 16.7,11.3C16.517,11.1 16.283,11 16,11H13V8C13,7.717 12.9,7.483 12.7,7.3C12.517,7.1 12.283,7 12,7C11.717,7 11.475,7.1 11.275,7.3C11.092,7.483 11,7.717 11,8V11H8C7.717,11 7.475,11.1 7.275,11.3C7.092,11.483 7,11.717 7,12C7,12.283 7.092,12.525 7.275,12.725C7.475,12.908 7.717,13 8,13H11ZM12,22C10.617,22 9.317,21.742 8.1,21.225C6.883,20.692 5.825,19.975 4.925,19.075C4.025,18.175 3.308,17.117 2.775,15.9C2.258,14.683 2,13.383 2,12C2,10.617 2.258,9.317 2.775,8.1C3.308,6.883 4.025,5.825 4.925,4.925C5.825,4.025 6.883,3.317 8.1,2.8C9.317,2.267 10.617,2 12,2C13.383,2 14.683,2.267 15.9,2.8C17.117,3.317 18.175,4.025 19.075,4.925C19.975,5.825 20.683,6.883 21.2,8.1C21.733,9.317 22,10.617 22,12C22,13.383 21.733,14.683 21.2,15.9C20.683,17.117 19.975,18.175 19.075,19.075C18.175,19.975 17.117,20.692 15.9,21.225C14.683,21.742 13.383,22 12,22ZM12,20C14.233,20 16.125,19.225 17.675,17.675C19.225,16.125 20,14.233 20,12C20,9.767 19.225,7.875 17.675,6.325C16.125,4.775 14.233,4 12,4C9.767,4 7.875,4.775 6.325,6.325C4.775,7.875 4,9.767 4,12C4,14.233 4.775,16.125 6.325,17.675C7.875,19.225 9.767,20 12,20Z"
+ android:fillColor="#ffffff"/>
+ </group>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_check_circle_filled.xml b/packages/SystemUI/res/drawable/ic_check_circle_filled.xml
new file mode 100644
index 000000000000..935733c3333d
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_check_circle_filled.xml
@@ -0,0 +1,27 @@
+<!--
+ ~ Copyright (C) 2025 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <group>
+ <path
+ android:pathData="M10.6,13.8L8.45,11.65C8.267,11.467 8.033,11.375 7.75,11.375C7.467,11.375 7.233,11.467 7.05,11.65C6.867,11.833 6.775,12.067 6.775,12.35C6.775,12.633 6.867,12.867 7.05,13.05L9.9,15.9C10.1,16.1 10.333,16.2 10.6,16.2C10.867,16.2 11.1,16.1 11.3,15.9L16.95,10.25C17.133,10.067 17.225,9.833 17.225,9.55C17.225,9.267 17.133,9.033 16.95,8.85C16.767,8.667 16.533,8.575 16.25,8.575C15.967,8.575 15.733,8.667 15.55,8.85L10.6,13.8ZM12,22C10.617,22 9.317,21.742 8.1,21.225C6.883,20.692 5.825,19.975 4.925,19.075C4.025,18.175 3.308,17.117 2.775,15.9C2.258,14.683 2,13.383 2,12C2,10.617 2.258,9.317 2.775,8.1C3.308,6.883 4.025,5.825 4.925,4.925C5.825,4.025 6.883,3.317 8.1,2.8C9.317,2.267 10.617,2 12,2C13.383,2 14.683,2.267 15.9,2.8C17.117,3.317 18.175,4.025 19.075,4.925C19.975,5.825 20.683,6.883 21.2,8.1C21.733,9.317 22,10.617 22,12C22,13.383 21.733,14.683 21.2,15.9C20.683,17.117 19.975,18.175 19.075,19.075C18.175,19.975 17.117,20.692 15.9,21.225C14.683,21.742 13.383,22 12,22Z"
+ android:fillColor="#ffffff"/>
+ </group>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_expand_less_rounded.xml b/packages/SystemUI/res/drawable/ic_expand_less_rounded.xml
new file mode 100644
index 000000000000..5570fcfdab28
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_expand_less_rounded.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2025 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="16dp"
+ android:height="16dp"
+ android:viewportWidth="16"
+ android:viewportHeight="16">
+ <path
+ android:pathData="M8,5.25C8.097,5.25 8.194,5.264 8.292,5.292C8.375,5.333 8.451,5.389 8.521,5.458L12.479,9.417C12.632,9.569 12.708,9.743 12.708,9.938C12.694,10.146 12.611,10.326 12.458,10.479C12.306,10.632 12.132,10.708 11.938,10.708C11.729,10.708 11.549,10.632 11.396,10.479L8,7.063L4.583,10.479C4.431,10.632 4.257,10.701 4.063,10.688C3.854,10.688 3.674,10.611 3.521,10.458C3.368,10.306 3.292,10.125 3.292,9.917C3.292,9.722 3.368,9.549 3.521,9.396L7.479,5.458C7.549,5.389 7.632,5.333 7.729,5.292C7.813,5.264 7.903,5.25 8,5.25Z"
+ android:fillColor="#ffffffff"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_expand_more_rounded.xml b/packages/SystemUI/res/drawable/ic_expand_more_rounded.xml
new file mode 100644
index 000000000000..dec620e54995
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_expand_more_rounded.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2025 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.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="16dp"
+ android:height="16dp"
+ android:viewportHeight="16"
+ android:viewportWidth="16">
+ <path
+ android:pathData="M8,10.75C7.903,10.75 7.806,10.736 7.708,10.708C7.625,10.667 7.549,10.611 7.479,10.542L3.521,6.583C3.368,6.431 3.292,6.257 3.292,6.063C3.306,5.854 3.389,5.674 3.542,5.521C3.694,5.368 3.868,5.292 4.063,5.292C4.271,5.292 4.451,5.368 4.604,5.521L8,8.938L11.417,5.521C11.569,5.368 11.743,5.299 11.938,5.313C12.146,5.313 12.326,5.389 12.479,5.542C12.632,5.694 12.708,5.875 12.708,6.083C12.708,6.278 12.632,6.451 12.479,6.604L8.521,10.542C8.451,10.611 8.368,10.667 8.271,10.708C8.188,10.736 8.097,10.75 8,10.75Z"
+ android:fillColor="#ffffffff"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/media_output_dialog_background_reduced_radius.xml b/packages/SystemUI/res/drawable/media_output_dialog_background_reduced_radius.xml
new file mode 100644
index 000000000000..f78212b44828
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_dialog_background_reduced_radius.xml
@@ -0,0 +1,20 @@
+<!--
+ ~ Copyright (C) 2025 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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/media_output_dialog_corner_radius" />
+ <solid android:color="@color/media_dialog_surface_container" />
+</shape> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/media_output_dialog_footer_background.xml b/packages/SystemUI/res/drawable/media_output_dialog_footer_background.xml
new file mode 100644
index 000000000000..2d27ac1612a9
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_dialog_footer_background.xml
@@ -0,0 +1,22 @@
+<!--
+ ~ Copyright (C) 2025 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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners
+ android:bottomLeftRadius="@dimen/media_output_dialog_corner_radius"
+ android:bottomRightRadius="@dimen/media_output_dialog_corner_radius" />
+ <solid android:color="@color/media_dialog_surface_container" />
+</shape> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/media_output_dialog_item_fixed_volume_background.xml b/packages/SystemUI/res/drawable/media_output_dialog_item_fixed_volume_background.xml
new file mode 100644
index 000000000000..38db2da37f7c
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_dialog_item_fixed_volume_background.xml
@@ -0,0 +1,20 @@
+<!--
+ ~ Copyright (C) 2025 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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners android:radius="20dp" />
+ <solid android:color="@color/media_dialog_primary" />
+</shape> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/media_output_dialog_round_button_ripple.xml b/packages/SystemUI/res/drawable/media_output_dialog_round_button_ripple.xml
new file mode 100644
index 000000000000..d23c1837c501
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_dialog_round_button_ripple.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2025 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.
+ -->
+<ripple android:color="?android:colorControlHighlight"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <solid android:color="@android:color/white" />
+ <corners android:radius="20dp" />
+ </shape>
+ </item>
+</ripple> \ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/media_output_item_expandable_button_background.xml b/packages/SystemUI/res/drawable/media_output_item_expandable_button_background.xml
new file mode 100644
index 000000000000..8fc8744a3827
--- /dev/null
+++ b/packages/SystemUI/res/drawable/media_output_item_expandable_button_background.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2025 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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <corners
+ android:radius="@dimen/media_output_item_expand_icon_height"/>
+ <size
+ android:width="@dimen/media_output_item_expand_icon_width"
+ android:height="@dimen/media_output_item_expand_icon_height" />
+ <solid android:color="@color/media_dialog_on_surface" />
+</shape> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/media_output_dialog.xml b/packages/SystemUI/res/layout/media_output_dialog.xml
index 9b629ace76af..15657284030d 100644
--- a/packages/SystemUI/res/layout/media_output_dialog.xml
+++ b/packages/SystemUI/res/layout/media_output_dialog.xml
@@ -97,6 +97,23 @@
</LinearLayout>
</LinearLayout>
+ <LinearLayout
+ android:id="@+id/quick_access_shelf"
+ android:paddingHorizontal="@dimen/media_output_dialog_margin_horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone">
+
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/connect_device"
+ app:icon="@drawable/ic_add"
+ style="@style/MediaOutput.Dialog.QuickAccessButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/media_output_dialog_button_connect_device"
+ android:layout_marginBottom="8dp"/>
+ </LinearLayout>
+
<ViewStub
android:id="@+id/broadcast_qrcode"
android:layout="@layout/media_output_broadcast_area"
@@ -123,13 +140,15 @@
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
+ android:id="@+id/dialog_footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginTop="4dp"
- android:layout_marginStart="@dimen/dialog_side_padding"
- android:layout_marginEnd="@dimen/dialog_side_padding"
- android:layout_marginBottom="@dimen/dialog_bottom_padding"
- android:orientation="horizontal">
+ android:paddingTop="4dp"
+ android:paddingStart="@dimen/dialog_side_padding"
+ android:paddingEnd="@dimen/dialog_side_padding"
+ android:paddingBottom="@dimen/dialog_bottom_padding"
+ android:orientation="horizontal"
+ android:gravity="end">
<Button
android:id="@+id/stop"
@@ -140,6 +159,7 @@
android:visibility="gone"/>
<Space
+ android:id="@+id/footer_spacer"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent"/>
diff --git a/packages/SystemUI/res/layout/media_output_list_item_device.xml b/packages/SystemUI/res/layout/media_output_list_item_device.xml
new file mode 100644
index 000000000000..29d5bfcc1743
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_output_list_item_device.xml
@@ -0,0 +1,141 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2025 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.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/item_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:baselineAligned="false"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/main_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="start|center_vertical"
+ android:background="?android:attr/selectableItemBackground"
+ android:focusable="true"
+ android:orientation="horizontal"
+ android:layout_marginHorizontal="@dimen/media_output_dialog_margin_horizontal"
+ android:paddingVertical="@dimen/media_output_item_content_vertical_margin">
+
+ <ImageView
+ android:id="@+id/title_icon"
+ style="@style/MediaOutput.Item.Icon"
+ android:layout_marginEnd="@dimen/media_output_item_horizontal_gap"
+ android:importantForAccessibility="no"
+ tools:src="@drawable/ic_smartphone"
+ tools:visibility="visible"/>
+
+ <LinearLayout
+ android:id="@+id/text_container"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/media_output_item_icon_size"
+ android:layout_gravity="start"
+ android:gravity="center_vertical|start"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fontFamily="variable-title-small"
+ android:ellipsize="end"
+ android:maxLines="1" />
+
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fontFamily="variable-title-small"
+ android:alpha="@dimen/media_output_item_subtitle_alpha"
+ android:maxLines="1"
+ android:singleLine="true" />
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/status_icon"
+ style="@style/MediaOutput.Item.Icon"
+ android:layout_marginStart="@dimen/media_output_item_horizontal_gap"
+ android:importantForAccessibility="no"
+ android:visibility="gone"
+ app:tint="@color/media_dialog_on_surface_variant"
+ tools:src="@drawable/media_output_status_failed"
+ tools:visibility="visible" />
+
+ <ProgressBar
+ android:id="@+id/loading_indicator"
+ style="?android:attr/progressBarStyleSmallTitle"
+ android:layout_width="@dimen/media_output_item_icon_size"
+ android:layout_height="@dimen/media_output_item_icon_size"
+ android:padding="@dimen/media_output_item_icon_padding"
+ android:scaleType="fitCenter"
+ android:layout_marginStart="@dimen/media_output_item_horizontal_gap"
+ android:indeterminate="true"
+ android:indeterminateOnly="true"
+ android:visibility="gone"
+ tools:indeterminateTint="@color/media_dialog_on_surface_variant"
+ tools:visibility="visible" />
+
+ <View
+ android:id="@+id/divider"
+ android:layout_width="1dp"
+ android:layout_height="@dimen/media_output_item_icon_size"
+ android:layout_marginStart="@dimen/media_output_item_horizontal_gap"
+ android:background="@color/media_dialog_outline"
+ android:visibility="visible"
+ />
+
+ <ImageButton
+ android:id="@+id/ongoing_session_button"
+ style="@style/MediaOutput.Item.Icon"
+ android:src="@drawable/ic_sound_bars_anim"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:focusable="true"
+ android:contentDescription="@string/accessibility_open_application"
+ android:layout_marginStart="@dimen/media_output_item_horizontal_gap"
+ android:visibility="gone"
+ tools:visibility="visible"/>
+
+ <ImageButton
+ android:id="@+id/group_button"
+ style="@style/MediaOutput.Item.Icon"
+ android:layout_marginStart="@dimen/media_output_item_horizontal_gap"
+ android:src="@drawable/ic_add_circle_rounded"
+ android:background="@drawable/media_output_dialog_round_button_ripple"
+ android:focusable="true"
+ android:contentDescription="@null"
+ android:visibility="gone"
+ tools:visibility="visible"/>
+ </LinearLayout>
+
+ <com.google.android.material.slider.Slider
+ android:id="@+id/volume_seekbar"
+ android:layout_width="match_parent"
+ android:layout_height="44dp"
+ android:layout_marginVertical="3dp"
+ android:theme="@style/Theme.Material3.DynamicColors.DayNight"
+ app:labelBehavior="gone"
+ app:tickVisible="false"
+ app:trackCornerSize="12dp"
+ app:trackHeight="32dp"
+ app:trackIconSize="20dp"
+ app:trackStopIndicatorSize="0dp" />
+</LinearLayout> \ No newline at end of file
diff --git a/packages/SystemUI/res/layout/media_output_list_item_group_divider.xml b/packages/SystemUI/res/layout/media_output_list_item_group_divider.xml
new file mode 100644
index 000000000000..f8c6c1f9f616
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_output_list_item_group_divider.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2025 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.
+ -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/media_output_dialog_margin_horizontal"
+ android:orientation="vertical">
+
+ <View
+ android:id="@+id/top_separator"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_marginVertical="8dp"
+ android:background="@color/media_dialog_outline_variant"
+ android:visibility="gone" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="40dp"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:accessibilityHeading="true"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:fontFamily="variable-label-large-emphasized"
+ android:gravity="center_vertical|start" />
+
+ <FrameLayout
+ android:id="@+id/expand_button"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:layout_marginStart="@dimen/media_output_item_horizontal_gap"
+ android:contentDescription="@string/accessibility_open_application"
+ android:focusable="true"
+ android:visibility="gone">
+
+ <ImageView
+ android:id="@+id/expand_button_icon"
+ android:layout_width="@dimen/media_output_item_expand_icon_width"
+ android:layout_height="@dimen/media_output_item_expand_icon_height"
+ android:layout_gravity="center"
+ android:background="@drawable/media_output_item_expandable_button_background"
+ android:contentDescription="@null"
+ android:focusable="false"
+ android:scaleType="centerInside" />
+ </FrameLayout>
+ </LinearLayout>
+</LinearLayout>
+
diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml
index 85182a02faaf..3dd01996afb8 100644
--- a/packages/SystemUI/res/values-night/colors.xml
+++ b/packages/SystemUI/res/values-night/colors.xml
@@ -76,6 +76,16 @@
<color name="media_dialog_seekbar_progress">@color/material_dynamic_secondary40</color>
<color name="media_dialog_button_background">@color/material_dynamic_primary70</color>
<color name="media_dialog_solid_button_text">@color/material_dynamic_secondary20</color>
+ <color name="media_dialog_primary">@android:color/system_primary_dark</color>
+ <color name="media_dialog_on_primary">@android:color/system_on_primary_dark</color>
+ <color name="media_dialog_secondary">@android:color/system_secondary_dark</color>
+ <color name="media_dialog_secondary_container">@android:color/system_secondary_container_dark</color>
+ <color name="media_dialog_surface_container">@android:color/system_surface_container_dark</color>
+ <color name="media_dialog_surface_container_high">@android:color/system_surface_container_high_dark</color>
+ <color name="media_dialog_on_surface">@android:color/system_on_surface_dark</color>
+ <color name="media_dialog_on_surface_variant">@android:color/system_on_surface_variant_dark</color>
+ <color name="media_dialog_outline">@android:color/system_outline_dark</color>
+ <color name="media_dialog_outline_variant">@android:color/system_outline_variant_dark</color>
<!-- Biometric dialog colors -->
<color name="biometric_dialog_gray">#ffcccccc</color>
diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml
index fe65f32c6eb0..cb656ca0a108 100644
--- a/packages/SystemUI/res/values/colors.xml
+++ b/packages/SystemUI/res/values/colors.xml
@@ -210,6 +210,16 @@
<color name="media_dialog_seekbar_progress">@android:color/system_accent1_200</color>
<color name="media_dialog_button_background">@color/material_dynamic_primary40</color>
<color name="media_dialog_solid_button_text">@color/material_dynamic_neutral95</color>
+ <color name="media_dialog_primary">@android:color/system_primary_light</color>
+ <color name="media_dialog_on_primary">@android:color/system_on_primary_light</color>
+ <color name="media_dialog_secondary">@android:color/system_secondary_light</color>
+ <color name="media_dialog_secondary_container">@android:color/system_secondary_container_light</color>
+ <color name="media_dialog_surface_container">@android:color/system_surface_container_light</color>
+ <color name="media_dialog_surface_container_high">@android:color/system_surface_container_high_light</color>
+ <color name="media_dialog_on_surface">@android:color/system_on_surface_light</color>
+ <color name="media_dialog_on_surface_variant">@android:color/system_on_surface_variant_light</color>
+ <color name="media_dialog_outline">@android:color/system_outline_light</color>
+ <color name="media_dialog_outline_variant">@android:color/system_outline_variant_light</color>
<!-- controls -->
<color name="control_primary_text">#E6FFFFFF</color>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index ca984881713b..f062bd1d4990 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1567,8 +1567,20 @@
<dimen name="media_output_dialog_item_height">64dp</dimen>
<dimen name="media_output_dialog_margin_horizontal">16dp</dimen>
<dimen name="media_output_dialog_list_padding_top">8dp</dimen>
+ <dimen name="media_output_dialog_app_icon_size">16dp</dimen>
+ <dimen name="media_output_dialog_app_icon_bottom_margin">11dp</dimen>
<dimen name="media_output_dialog_icon_left_radius">@dimen/media_output_dialog_active_background_radius</dimen>
<dimen name="media_output_dialog_icon_right_radius">0dp</dimen>
+ <dimen name="media_output_dialog_corner_radius">20dp</dimen>
+ <dimen name="media_output_dialog_button_gap">8dp</dimen>
+ <dimen name="media_output_item_content_vertical_margin">8dp</dimen>
+ <dimen name="media_output_item_content_vertical_margin_active">4dp</dimen>
+ <dimen name="media_output_item_horizontal_gap">12dp</dimen>
+ <dimen name="media_output_item_icon_size">40dp</dimen>
+ <dimen name="media_output_item_icon_padding">8dp</dimen>
+ <dimen name="media_output_item_expand_icon_width">28dp</dimen>
+ <dimen name="media_output_item_expand_icon_height">20dp</dimen>
+ <item name="media_output_item_subtitle_alpha" format="float" type="dimen">0.8</item>
<!-- Distance that the full shade transition takes in order to complete by tapping on a button
like "expand". -->
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 50242f69a755..bbf56936d560 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -592,6 +592,9 @@
<!-- Content description of the button to expand the group of devices. [CHAR LIMIT=NONE] -->
<string name="accessibility_expand_group">Expand group.</string>
+ <!-- Content description of the button to collapse the group of devices. [CHAR LIMIT=NONE] -->
+ <string name="accessibility_collapse_group">Collapse group.</string>
+
<!-- Content description of the button to add a device to a group. [CHAR LIMIT=NONE] -->
<string name="accessibility_add_device_to_group">Add device to group.</string>
@@ -3293,6 +3296,8 @@
<string name="media_output_dialog_connect_failed">Can\'t switch. Tap to try again.</string>
<!-- Title for connecting item [CHAR LIMIT=60] -->
<string name="media_output_dialog_pairing_new">Connect a device</string>
+ <!-- Button text for connecting a new device [CHAR LIMIT=60] -->
+ <string name="media_output_dialog_button_connect_device">Connect Device</string>
<!-- App name when can't get app name [CHAR LIMIT=60] -->
<string name="media_output_dialog_unknown_launch_app_name">Unknown app</string>
<!-- Button text for stopping casting [CHAR LIMIT=60] -->
@@ -3303,6 +3308,8 @@
<string name="media_output_dialog_accessibility_seekbar">Volume</string>
<!-- Summary for media output volume of a device in percentage [CHAR LIMIT=NONE] -->
<string name="media_output_dialog_volume_percentage"><xliff:g id="percentage" example="10">%1$d</xliff:g>%%</string>
+ <!-- Title for Connected speakers expandable group. [CHAR LIMIT=NONE] -->
+ <string name="media_output_group_title_connected_speakers">Connected speakers</string>
<!-- Title for Speakers and Displays group. [CHAR LIMIT=NONE] -->
<string name="media_output_group_title_speakers_and_displays">Speakers &amp; Displays</string>
<!-- Title for Suggested Devices group. [CHAR LIMIT=NONE] -->
@@ -3452,6 +3459,10 @@
<string name="keyguard_try_fingerprint">Use fingerprint to open</string>
<!-- Accessibility announcement to inform user to unlock using the fingerprint sensor [CHAR LIMIT=NONE] -->
<string name="accessibility_fingerprint_bouncer">Authentication required. Touch the fingerprint sensor to authenticate.</string>
+ <!-- Accessibility action label for resuming animation -->
+ <string name="resume_animation">Resume animation</string>
+ <!-- Accessibility action label for pausing animation -->
+ <string name="pause_animation">Pause animation</string>
<!-- Content description for a chip in the status bar showing that the user is currently on a call. [CHAR LIMIT=NONE] -->
<string name="ongoing_call_content_description">Ongoing call</string>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index bde750145ff7..fb72123a0a3b 100644
--- a/packages/SystemUI/res/values/styles.xml
+++ b/packages/SystemUI/res/values/styles.xml
@@ -708,6 +708,33 @@
<item name="android:colorBackground">@color/media_dialog_background</item>
</style>
+ <style name="MediaOutput" />
+ <style name="MediaOutput.Dialog" />
+ <style name="MediaOutput.Dialog.QuickAccessButton" parent="@style/Widget.Material3.Button.OutlinedButton.Icon">
+ <item name="theme">@style/Theme.Material3.DynamicColors.DayNight</item>
+ <item name="android:paddingTop">6dp</item>
+ <item name="android:minHeight">32dp</item>
+ <item name="android:paddingBottom">6dp</item>
+ <item name="android:paddingStart">8dp</item>
+ <item name="android:paddingEnd">12dp</item>
+ <item name="android:insetTop">0dp</item>
+ <item name="android:insetBottom">0dp</item>
+ <item name="android:textColor">@color/media_dialog_on_surface_variant</item>
+ <item name="iconSize">18dp</item>
+ <item name="iconTint">@color/media_dialog_primary</item>
+ <item name="shapeAppearance">@style/ShapeAppearance.Material3.Corner.Small</item>
+ <item name="strokeColor">@color/media_dialog_outline_variant</item>
+ </style>
+
+ <style name="MediaOutput.Item" />
+ <style name="MediaOutput.Item.Icon">
+ <item name="android:layout_width">@dimen/media_output_item_icon_size</item>
+ <item name="android:layout_height">@dimen/media_output_item_icon_size</item>
+ <item name="android:padding">@dimen/media_output_item_icon_padding</item>
+ <item name="android:scaleType">fitCenter</item>
+ <item name="tint">@color/media_dialog_on_surface</item>
+ </style>
+
<style name="MediaOutputItemInactiveTitle">
<item name="android:textSize">16sp</item>
<item name="android:textColor">@color/media_dialog_item_main_content</item>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
index a518c57bdd16..96307c7c301f 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/model/Task.java
@@ -309,6 +309,13 @@ public class Task {
taskInfo.topActivity);
}
+ /**
+ * Creates a task object from the given [taskInfo].
+ */
+ public static Task from(TaskInfo taskInfo) {
+ return from(new TaskKey(taskInfo), taskInfo, /* isLocked= */ false);
+ }
+
public Task(TaskKey key) {
this.key = key;
this.taskDescription = new TaskDescription();
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
index ceb2b10ab517..1d7562ea64b9 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt
@@ -25,6 +25,9 @@ import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import android.view.accessibility.AccessibilityEvent
+import androidx.core.view.AccessibilityDelegateCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.airbnb.lottie.LottieAnimationView
@@ -67,6 +70,59 @@ constructor(
private val sfpsSensorInteractor: Lazy<SideFpsSensorInteractor>,
private val windowManager: Lazy<WindowManager>,
) : CoreStartable {
+ private val pauseDelegate: AccessibilityDelegateCompat =
+ object : AccessibilityDelegateCompat() {
+ override fun onInitializeAccessibilityNodeInfo(
+ host: View,
+ info: AccessibilityNodeInfoCompat,
+ ) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ info.addAction(
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ AccessibilityNodeInfoCompat.ACTION_CLICK,
+ host.context.getString(R.string.pause_animation),
+ )
+ )
+ }
+
+ override fun dispatchPopulateAccessibilityEvent(
+ host: View,
+ event: AccessibilityEvent,
+ ): Boolean {
+ return if (event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
+ true
+ } else {
+ super.dispatchPopulateAccessibilityEvent(host, event)
+ }
+ }
+ }
+
+ private val resumeDelegate: AccessibilityDelegateCompat =
+ object : AccessibilityDelegateCompat() {
+ override fun onInitializeAccessibilityNodeInfo(
+ host: View,
+ info: AccessibilityNodeInfoCompat,
+ ) {
+ super.onInitializeAccessibilityNodeInfo(host, info)
+ info.addAction(
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat(
+ AccessibilityNodeInfoCompat.ACTION_CLICK,
+ host.context.getString(R.string.resume_animation),
+ )
+ )
+ }
+
+ override fun dispatchPopulateAccessibilityEvent(
+ host: View,
+ event: AccessibilityEvent,
+ ): Boolean {
+ return if (event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
+ true
+ } else {
+ super.dispatchPopulateAccessibilityEvent(host, event)
+ }
+ }
+ }
override fun start() {
applicationScope.launch {
@@ -135,6 +191,7 @@ constructor(
overlayView!!.setOnClickListener { v ->
v.requireViewById<LottieAnimationView>(R.id.sidefps_animation).toggleAnimation()
}
+ ViewCompat.setAccessibilityDelegate(overlayView!!, pauseDelegate)
Log.d(TAG, "show(): adding overlayView $overlayView")
windowManager.get().addView(overlayView, overlayViewModel.defaultOverlayViewParams)
}
@@ -177,29 +234,6 @@ constructor(
overlayShowAnimator.start()
- /**
- * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback
- * from speaking @string/accessibility_fingerprint_label twice when sensor location
- * indicator is in focus
- */
- it.setAccessibilityDelegate(
- object : View.AccessibilityDelegate() {
- override fun dispatchPopulateAccessibilityEvent(
- host: View,
- event: AccessibilityEvent,
- ): Boolean {
- return if (
- event.getEventType() ===
- AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
- ) {
- true
- } else {
- super.dispatchPopulateAccessibilityEvent(host, event)
- }
- }
- }
- )
-
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.lottieCallbacks.collect { callbacks ->
@@ -224,6 +258,16 @@ constructor(
}
}
}
+
+ private fun LottieAnimationView.toggleAnimation() {
+ if (isAnimating) {
+ pauseAnimation()
+ ViewCompat.setAccessibilityDelegate(this, resumeDelegate)
+ } else {
+ resumeAnimation()
+ ViewCompat.setAccessibilityDelegate(this, pauseDelegate)
+ }
+ }
}
private fun LottieAnimationView.addOverlayDynamicColor(colorCallbacks: List<LottieCallback>) {
@@ -236,11 +280,3 @@ private fun LottieAnimationView.addOverlayDynamicColor(colorCallbacks: List<Lott
resumeAnimation()
}
}
-
-fun LottieAnimationView.toggleAnimation() {
- if (isAnimating) {
- pauseAnimation()
- } else {
- resumeAnimation()
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
index 2b8cf008c0c7..100e21d34c42 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSceneRepository.kt
@@ -19,16 +19,13 @@ package com.android.systemui.communal.data.repository
import android.content.res.Configuration
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.compose.animation.scene.ObservableTransitionState
-import com.android.compose.animation.scene.OverlayKey
import com.android.compose.animation.scene.SceneKey
import com.android.compose.animation.scene.TransitionKey
import com.android.systemui.communal.dagger.Communal
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.scene.shared.model.SceneDataSource
-import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
@@ -59,9 +56,6 @@ interface CommunalSceneRepository {
/** Immediately snaps to the desired scene. */
fun snapToScene(toScene: SceneKey)
- /** Shows the hub from a power button press. */
- suspend fun showHubFromPowerButton()
-
/**
* Updates the transition state of the hub [SceneTransitionLayout].
*
@@ -77,10 +71,8 @@ interface CommunalSceneRepository {
class CommunalSceneRepositoryImpl
@Inject
constructor(
- @Application private val applicationScope: CoroutineScope,
@Background backgroundScope: CoroutineScope,
@Communal private val sceneDataSource: SceneDataSource,
- @Communal private val delegator: SceneDataSourceDelegator,
) : CommunalSceneRepository {
override val currentScene: StateFlow<SceneKey> = sceneDataSource.currentScene
@@ -102,37 +94,17 @@ constructor(
_communalContainerOrientation.asStateFlow()
override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) {
- applicationScope.launch {
- // SceneTransitionLayout state updates must be triggered on the thread the STL was
- // created on.
- sceneDataSource.changeScene(toScene, transitionKey)
- }
+ sceneDataSource.changeScene(toScene, transitionKey)
}
override fun snapToScene(toScene: SceneKey) {
- applicationScope.launch {
- // SceneTransitionLayout state updates must be triggered on the thread the STL was
- // created on.
- sceneDataSource.snapToScene(toScene)
- }
+ sceneDataSource.snapToScene(toScene)
}
override fun setCommunalContainerOrientation(orientation: Int) {
_communalContainerOrientation.value = orientation
}
- override suspend fun showHubFromPowerButton() {
- // If keyguard is not showing yet, the hub view is not ready and the
- // [SceneDataSourceDelegator] will still be using the default [NoOpSceneDataSource]
- // and initial key, which is Blank. This means that when the hub container loads, it
- // will default to not showing the hub. Attempting to set the scene in this state
- // is simply ignored by the [NoOpSceneDataSource]. Instead, we temporarily override
- // it with a new one that defaults to Communal. This delegate will be overwritten
- // once the [CommunalContainer] loads.
- // TODO(b/392969914): show the hub first instead of forcing the scene.
- delegator.setDelegate(NoOpSceneDataSource(CommunalScenes.Communal))
- }
-
/**
* Updates the transition state of the hub [SceneTransitionLayout].
*
@@ -141,33 +113,4 @@ constructor(
override fun setTransitionState(transitionState: Flow<ObservableTransitionState>?) {
_transitionState.value = transitionState
}
-
- /** Noop implementation of a scene data source that always returns the initial [SceneKey]. */
- private class NoOpSceneDataSource(initialSceneKey: SceneKey) : SceneDataSource {
- override val currentScene: StateFlow<SceneKey> =
- MutableStateFlow(initialSceneKey).asStateFlow()
-
- override val currentOverlays: StateFlow<Set<OverlayKey>> =
- MutableStateFlow(emptySet<OverlayKey>()).asStateFlow()
-
- override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) = Unit
-
- override fun snapToScene(toScene: SceneKey) = Unit
-
- override fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) = Unit
-
- override fun hideOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) = Unit
-
- override fun replaceOverlay(
- from: OverlayKey,
- to: OverlayKey,
- transitionKey: TransitionKey?,
- ) = Unit
-
- override fun instantlyShowOverlay(overlay: OverlayKey) = Unit
-
- override fun instantlyHideOverlay(overlay: OverlayKey) = Unit
-
- override fun freezeAndAnimateToCurrentState() = Unit
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
index 684c52ad45f3..272439e68f71 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt
@@ -149,9 +149,18 @@ constructor(
val isCommunalEnabled: StateFlow<Boolean> = communalSettingsInteractor.isCommunalEnabled
/** Whether communal features are enabled and available. */
- val isCommunalAvailable: Flow<Boolean> =
- allOf(communalSettingsInteractor.isCommunalEnabled, keyguardInteractor.isKeyguardShowing)
- .distinctUntilChanged()
+ @Deprecated("Use isCommunalEnabled instead", replaceWith = ReplaceWith("isCommunalEnabled"))
+ val isCommunalAvailable: Flow<Boolean> by lazy {
+ val availableFlow =
+ if (communalSettingsInteractor.isV2FlagEnabled()) {
+ communalSettingsInteractor.isCommunalEnabled
+ } else {
+ allOf(
+ communalSettingsInteractor.isCommunalEnabled,
+ keyguardInteractor.isKeyguardShowing,
+ )
+ }
+ availableFlow
.onEach { available ->
logger.i({ "Communal is ${if (bool1) "" else "un"}available" }) {
bool1 = available
@@ -167,6 +176,7 @@ constructor(
started = SharingStarted.WhileSubscribed(),
replay = 1,
)
+ }
private val _isDisclaimerDismissed = MutableStateFlow(false)
val isDisclaimerDismissed: Flow<Boolean> = _isDisclaimerDismissed.asStateFlow()
@@ -467,6 +477,7 @@ constructor(
size = CommunalContentSize.toSize(widget.spanY),
)
}
+
is CommunalWidgetContentModel.Pending -> {
WidgetContent.PendingWidget(
appWidgetId = widget.appWidgetId,
@@ -493,6 +504,7 @@ constructor(
when (model) {
is CommunalWidgetContentModel.Available ->
model.providerInfo.profile.identifier
+
is CommunalWidgetContentModel.Pending -> model.user.identifier
}
uid != disallowedByDevicePolicyUser.id
@@ -576,6 +588,7 @@ constructor(
when (widget) {
is CommunalWidgetContentModel.Available ->
currentUserIds.contains(widget.providerInfo.profile?.identifier)
+
is CommunalWidgetContentModel.Pending -> true
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
index a112dd25e006..80222299177b 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractor.kt
@@ -29,6 +29,7 @@ import com.android.systemui.communal.shared.model.CommunalScenes.toSceneContaine
import com.android.systemui.communal.shared.model.EditModeState
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -37,6 +38,7 @@ import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
import com.android.systemui.util.kotlin.pairwiseBy
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
@@ -57,6 +59,7 @@ class CommunalSceneInteractor
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
+ @Main private val mainImmediateDispatcher: CoroutineDispatcher,
private val repository: CommunalSceneRepository,
private val logger: CommunalSceneLogger,
private val sceneInteractor: SceneInteractor,
@@ -113,6 +116,12 @@ constructor(
onSceneAboutToChangeListener.add(processor)
}
+ /** Unregisters a previously registered listener. */
+ fun unregisterSceneStateProcessor(processor: OnSceneAboutToChangeListener) {
+ SceneContainerFlag.assertInLegacyMode()
+ onSceneAboutToChangeListener.remove(processor)
+ }
+
/**
* Asks for an asynchronous scene witch to [newScene], which will use the corresponding
* installed transition or the one specified by [transitionKey], if provided.
@@ -123,7 +132,7 @@ constructor(
transitionKey: TransitionKey? = null,
keyguardState: KeyguardState? = null,
) {
- applicationScope.launch("$TAG#changeScene") {
+ applicationScope.launch("$TAG#changeScene", mainImmediateDispatcher) {
if (SceneContainerFlag.isEnabled) {
sceneInteractor.changeScene(
toScene = newScene.toSceneContainerSceneKey(),
@@ -175,29 +184,6 @@ constructor(
}
}
- fun showHubFromPowerButton() {
- val loggingReason = "showing hub from power button"
- applicationScope.launch("$TAG#showHubFromPowerButton") {
- if (SceneContainerFlag.isEnabled) {
- sceneInteractor.changeScene(
- toScene = CommunalScenes.Communal.toSceneContainerSceneKey(),
- loggingReason = loggingReason,
- )
- return@launch
- }
-
- if (currentScene.value == CommunalScenes.Communal) return@launch
- logger.logSceneChangeRequested(
- from = currentScene.value,
- to = CommunalScenes.Communal,
- reason = loggingReason,
- isInstant = true,
- )
- notifyListeners(CommunalScenes.Communal, null)
- repository.showHubFromPowerButton()
- }
- }
-
private fun notifyListeners(newScene: SceneKey, keyguardState: KeyguardState?) {
onSceneAboutToChangeListener.forEach { it.onSceneAboutToChange(newScene, keyguardState) }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt
index 477b87119563..89d738ef3bcc 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt
@@ -25,6 +25,7 @@ import com.android.systemui.communal.data.repository.CommunalSceneTransitionRepo
import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.domain.interactor.InternalKeyguardTransitionInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -37,6 +38,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.util.kotlin.pairwise
import java.util.UUID
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
@@ -64,6 +66,7 @@ constructor(
val internalTransitionInteractor: InternalKeyguardTransitionInteractor,
private val settingsInteractor: CommunalSettingsInteractor,
@Application private val applicationScope: CoroutineScope,
+ @Main private val mainImmediateDispatcher: CoroutineDispatcher,
private val sceneInteractor: CommunalSceneInteractor,
private val repository: CommunalSceneTransitionRepository,
private val powerInteractor: PowerInteractor,
@@ -143,7 +146,7 @@ constructor(
/** Monitors [SceneTransitionLayout] state and updates KTF state accordingly. */
private fun listenForSceneTransitionProgress() {
- applicationScope.launch {
+ applicationScope.launch("$TAG#listenForSceneTransitionProgress", mainImmediateDispatcher) {
sceneInteractor.transitionState
.pairwise(ObservableTransitionState.Idle(CommunalScenes.Blank))
.collect { (prevTransition, transition) ->
@@ -256,7 +259,10 @@ constructor(
private fun collectProgress(transition: ObservableTransitionState.Transition) {
progressJob?.cancel()
- progressJob = applicationScope.launch { transition.progress.collect { updateProgress(it) } }
+ progressJob =
+ applicationScope.launch("$TAG#collectProgress", mainImmediateDispatcher) {
+ transition.progress.collect { updateProgress(it) }
+ }
}
private suspend fun startTransitionFromGlanceableHub() {
@@ -300,4 +306,8 @@ constructor(
TransitionState.RUNNING,
)
}
+
+ private companion object {
+ const val TAG = "CommunalSceneTransitionInteractor"
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt
index 25c7f477c815..b9a420c45262 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/posturing/domain/interactor/PosturingInteractor.kt
@@ -18,8 +18,6 @@ package com.android.systemui.communal.posturing.domain.interactor
import android.annotation.SuppressLint
import android.hardware.Sensor
-import android.hardware.TriggerEvent
-import android.hardware.TriggerEventListener
import com.android.systemui.communal.posturing.data.model.PositionState
import com.android.systemui.communal.posturing.data.repository.PosturingRepository
import com.android.systemui.communal.posturing.shared.model.PosturedState
@@ -30,20 +28,19 @@ import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.CommunalLog
import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf
+import com.android.systemui.util.kotlin.observeTriggerSensor
import com.android.systemui.util.kotlin.slidingWindow
import com.android.systemui.util.sensors.AsyncSensorManager
import com.android.systemui.util.time.SystemClock
-import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
-import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@@ -178,35 +175,9 @@ constructor(
* Helper for observing a trigger sensor, which automatically unregisters itself after it
* executes once.
*/
- private fun observeTriggerSensor(type: Int): Flow<Unit> = conflatedCallbackFlow {
- val sensor = asyncSensorManager.getDefaultSensor(type)
- val isRegistered = AtomicBoolean(false)
-
- fun registerCallbackInternal(callback: TriggerEventListener) {
- if (isRegistered.compareAndSet(false, true)) {
- asyncSensorManager.requestTriggerSensor(callback, sensor)
- }
- }
-
- val callback =
- object : TriggerEventListener() {
- override fun onTrigger(event: TriggerEvent) {
- trySend(Unit)
- if (isRegistered.getAndSet(false)) {
- registerCallbackInternal(this)
- }
- }
- }
-
- if (sensor != null) {
- registerCallbackInternal(callback)
- }
-
- awaitClose {
- if (isRegistered.getAndSet(false)) {
- asyncSensorManager.cancelTriggerSensor(callback, sensor)
- }
- }
+ private fun observeTriggerSensor(type: Int): Flow<Unit> {
+ val sensor = asyncSensorManager.getDefaultSensor(type) ?: return emptyFlow()
+ return asyncSensorManager.observeTriggerSensor(sensor)
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
index 440c3001a2f9..701aa5c8d2c5 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt
@@ -92,7 +92,17 @@ constructor(
!glanceableHubMultiUserHelper.glanceableHubHsumFlagEnabled ||
!glanceableHubMultiUserHelper.isHeadlessSystemUserMode()
) {
- anyOf(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen)
+ val isAvailable =
+ if (communalSettingsInteractor.isV2FlagEnabled()) {
+ allOf(
+ communalInteractor.isCommunalEnabled,
+ keyguardInteractor.isKeyguardShowing,
+ )
+ } else {
+ communalInteractor.isCommunalAvailable
+ }
+
+ anyOf(isAvailable, communalInteractor.editModeOpen)
// Only trigger updates on state changes, ignoring the initial false value.
.pairwise(false)
.filter { (previous, new) -> previous != new }
@@ -153,6 +163,7 @@ constructor(
is CommunalWidgetContentModel.Available ->
widget.providerInfo.widgetCategory and
AppWidgetProviderInfo.WIDGET_CATEGORY_NOT_KEYGUARD != 0
+
else -> false
}
}
@@ -171,6 +182,7 @@ constructor(
when (widget) {
is CommunalWidgetContentModel.Available ->
widget.providerInfo.profile?.identifier
+
is CommunalWidgetContentModel.Pending -> widget.user.identifier
}
!currentUserIds.contains(uid)
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
index 599c945db064..c78231f16437 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
@@ -21,6 +21,7 @@ import static android.service.dreams.Flags.dreamHandlesBeingObscured;
import static com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress;
import static com.android.keyguard.BouncerPanelExpansionCalculator.getDreamAlphaScaledExpansion;
import static com.android.keyguard.BouncerPanelExpansionCalculator.getDreamYPositionScaledExpansion;
+import static com.android.systemui.Flags.bouncerUiRevamp;
import static com.android.systemui.complication.ComplicationLayoutParams.POSITION_BOTTOM;
import static com.android.systemui.complication.ComplicationLayoutParams.POSITION_TOP;
import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
@@ -362,9 +363,11 @@ public class DreamOverlayContainerViewController extends
});
}
- mBlurUtils.applyBlur(mView.getViewRootImpl(),
- (int) mBlurUtils.blurRadiusOfRatio(
- 1 - aboutToShowBouncerProgress(bouncerHideAmount)), false);
+ if (!bouncerUiRevamp()) {
+ mBlurUtils.applyBlur(mView.getViewRootImpl(),
+ (int) mBlurUtils.blurRadiusOfRatio(
+ 1 - aboutToShowBouncerProgress(bouncerHideAmount)), false);
+ }
}
private static float getAlpha(int position, float expansion) {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index fd716eea799a..501883e257ab 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -17,6 +17,7 @@
package com.android.systemui.dreams;
import static android.service.dreams.Flags.dreamWakeRedirect;
+import static android.service.dreams.Flags.dreamsV2;
import static com.android.systemui.Flags.glanceableHubAllowKeyguardWhenDreaming;
import static com.android.systemui.dreams.dagger.DreamModule.DREAM_OVERLAY_WINDOW_TITLE;
@@ -29,6 +30,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
+import android.os.PowerManager;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
@@ -69,6 +71,7 @@ import com.android.systemui.dreams.dagger.DreamOverlayComponent;
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor;
import com.android.systemui.navigationbar.gestural.domain.GestureInteractor;
import com.android.systemui.navigationbar.gestural.domain.TaskMatcher;
+import com.android.systemui.power.domain.interactor.PowerInteractor;
import com.android.systemui.scene.domain.interactor.SceneInteractor;
import com.android.systemui.scene.shared.flag.SceneContainerFlag;
import com.android.systemui.scene.shared.model.Overlays;
@@ -77,6 +80,8 @@ import com.android.systemui.shade.ShadeExpansionChangeEvent;
import com.android.systemui.touch.TouchInsetManager;
import com.android.systemui.util.concurrency.DelayableExecutor;
+import kotlin.Unit;
+
import kotlinx.coroutines.Job;
import java.util.ArrayList;
@@ -105,6 +110,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
private final Context mContext;
// The Executor ensures actions and ui updates happen on the same thread.
private final DelayableExecutor mExecutor;
+ private final PowerInteractor mPowerInteractor;
// A controller for the dream overlay container view (which contains both the status bar and the
// content area).
private DreamOverlayContainerViewController mDreamOverlayContainerViewController;
@@ -230,6 +236,15 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
}
};
+ private final Consumer<Unit> mPickupConsumer = new Consumer<>() {
+ @Override
+ public void accept(Unit unit) {
+ mExecutor.execute(() ->
+ mPowerInteractor.wakeUpIfDreaming("pickupGesture",
+ PowerManager.WAKE_REASON_LIFT));
+ }
+ };
+
/**
* {@link ResetHandler} protects resetting {@link DreamOverlayService} by making sure reset
* requests are processed before subsequent actions proceed. Requests themselves are also
@@ -398,6 +413,8 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
DreamOverlayCallbackController dreamOverlayCallbackController,
KeyguardInteractor keyguardInteractor,
GestureInteractor gestureInteractor,
+ WakeGestureMonitor wakeGestureMonitor,
+ PowerInteractor powerInteractor,
@Named(DREAM_OVERLAY_WINDOW_TITLE) String windowTitle) {
super(executor);
mContext = context;
@@ -424,6 +441,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
mTouchInsetManager = touchInsetManager;
mLifecycleOwner = lifecycleOwner;
mLifecycleRegistry = lifecycleOwner.getRegistry();
+ mPowerInteractor = powerInteractor;
mExecutor.execute(() -> setLifecycleStateLocked(Lifecycle.State.CREATED));
@@ -438,6 +456,11 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ
mFlows.add(collectFlow(getLifecycle(), keyguardInteractor.primaryBouncerShowing,
mBouncerShowingConsumer));
}
+
+ if (dreamsV2()) {
+ mFlows.add(collectFlow(getLifecycle(), wakeGestureMonitor.getWakeUpDetected(),
+ mPickupConsumer));
+ }
}
@NonNull
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/WakeGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/dreams/WakeGestureMonitor.kt
new file mode 100644
index 000000000000..1ba170bf656a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/WakeGestureMonitor.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2025 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.dreams
+
+import android.hardware.Sensor
+import android.hardware.display.AmbientDisplayConfiguration
+import android.provider.Settings
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.util.kotlin.emitOnStart
+import com.android.systemui.util.kotlin.observeTriggerSensor
+import com.android.systemui.util.sensors.AsyncSensorManager
+import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
+import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+@SysUISingleton
+class WakeGestureMonitor
+@Inject
+constructor(
+ private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
+ private val asyncSensorManager: AsyncSensorManager,
+ @Background bgContext: CoroutineContext,
+ private val secureSettings: SecureSettings,
+ selectedUserInteractor: SelectedUserInteractor,
+) {
+
+ private val pickupSensor by lazy {
+ asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)
+ }
+
+ private val pickupGestureEnabled: Flow<Boolean> =
+ selectedUserInteractor.selectedUser.flatMapLatestConflated { userId ->
+ isPickupEnabledForUser(userId)
+ }
+
+ private fun isPickupEnabledForUser(userId: Int): Flow<Boolean> =
+ secureSettings
+ .observerFlow(userId, Settings.Secure.DOZE_PICK_UP_GESTURE)
+ .emitOnStart()
+ .map { ambientDisplayConfiguration.pickupGestureEnabled(userId) }
+
+ val wakeUpDetected: Flow<Unit> =
+ pickupGestureEnabled
+ .flatMapLatestConflated { enabled ->
+ if (enabled && pickupSensor != null) {
+ asyncSensorManager.observeTriggerSensor(pickupSensor!!)
+ } else {
+ emptyFlow()
+ }
+ }
+ .flowOn(bgContext)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
index 74cf7e4f7359..6caff6432cb2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt
@@ -147,10 +147,8 @@ constructor(
configuration,
occludingAppDeviceEntryMessageViewModel,
chipbarCoordinator,
- screenOffAnimationController,
shadeInteractor,
- clockInteractor,
- keyguardClockViewModel,
+ smartspaceViewModel,
deviceEntryHapticsInteractor,
vibratorHelper,
falsingManager,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 099a7f067482..170966b45618 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -2433,11 +2433,7 @@ public class KeyguardViewMediator implements CoreStartable,
private void doKeyguardLocked(Bundle options) {
// If the power button behavior requests to open the glanceable hub.
if (options != null && options.getBoolean(EXTRA_TRIGGER_HUB)) {
- if (mCommunalSettingsInteractor.get().getAutoOpenEnabled().getValue()) {
- // Set the hub to show immediately when the SysUI window shows, then continue to
- // lock the device.
- mCommunalSceneInteractor.get().showHubFromPowerButton();
- } else {
+ if (!mKeyguardInteractor.showGlanceableHub()) {
// If the hub is not available, go to sleep instead of locking. This can happen
// because the power button behavior does not check all possible reasons the hub
// might be disabled.
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
index f53421d539fe..4fca453a184c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt
@@ -93,8 +93,8 @@ constructor(
// transition.
scope.launch("$TAG#listenForAodToAwake") {
powerInteractor.detailedWakefulness
- .filterRelevantKeyguardStateAnd { wakefulness -> wakefulness.isAwake() }
.debounce(50L)
+ .filterRelevantKeyguardStateAnd { wakefulness -> wakefulness.isAwake() }
.sample(
transitionInteractor.startedKeyguardTransitionStep,
wakeToGoneInteractor.canWakeDirectlyToGone,
@@ -140,7 +140,8 @@ constructor(
val shouldTransitionToCommunal =
communalSettingsInteractor.isV2FlagEnabled() &&
autoOpenCommunal &&
- !detailedWakefulness.isAwakeFromMotionOrLift()
+ !detailedWakefulness.isAwakeFromMotionOrLift() &&
+ !isKeyguardOccludedLegacy
if (shouldTransitionToGone) {
// TODO(b/360368320): Adapt for scene framework
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index 4aaa1fab4c65..d673f22386b7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -125,7 +125,9 @@ constructor(
wakefulness: WakefulnessModel,
) =
if (communalSettingsInteractor.isV2FlagEnabled()) {
- shouldShowCommunal && !wakefulness.isAwakeFromMotionOrLift()
+ shouldShowCommunal &&
+ !wakefulness.isAwakeFromMotionOrLift() &&
+ !keyguardInteractor.isKeyguardOccluded.value
} else {
isCommunalAvailable && dreamManager.canStartDreaming(false)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
index ca6a7907a8eb..cc5ec79a1060 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
@@ -20,6 +20,8 @@ import android.animation.ValueAnimator
import com.android.app.animation.Interpolators
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
+import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
@@ -48,6 +50,7 @@ constructor(
keyguardInteractor: KeyguardInteractor,
powerInteractor: PowerInteractor,
private val communalSceneInteractor: CommunalSceneInteractor,
+ private val communalSettingsInteractor: CommunalSettingsInteractor,
keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
private val keyguardShowWhileAwakeInteractor: KeyguardShowWhileAwakeInteractor,
) :
@@ -77,6 +80,26 @@ constructor(
}
/**
+ * Attempt to show the glanceable hub from the gone state (eg due to power button press).
+ *
+ * This will return whether the hub was successfully shown or not.
+ */
+ fun showGlanceableHub(): Boolean {
+ val isRelevantKeyguardState =
+ transitionInteractor.startedKeyguardTransitionStep.value.to == KeyguardState.GONE
+ val showGlanceableHub =
+ isRelevantKeyguardState &&
+ communalSettingsInteractor.isV2FlagEnabled() &&
+ communalSettingsInteractor.autoOpenEnabled.value &&
+ !keyguardInteractor.isKeyguardOccluded.value
+ if (showGlanceableHub) {
+ communalSceneInteractor.snapToScene(CommunalScenes.Communal, "showGlanceableHub()")
+ return true
+ }
+ return false
+ }
+
+ /**
* A special case supported on foldables, where folding the device may put the device on an
* unlocked lockscreen, but if an occluding app is already showing (like a active phone call),
* then go directly to OCCLUDED.
@@ -100,28 +123,36 @@ constructor(
scope.launch {
keyguardShowWhileAwakeInteractor.showWhileAwakeEvents
.filterRelevantKeyguardState()
- .sample(communalSceneInteractor.isIdleOnCommunalNotEditMode, ::Pair)
- .collect { (lockReason, idleOnCommunal) ->
- val to =
- if (idleOnCommunal) {
- KeyguardState.GLANCEABLE_HUB
- } else {
- KeyguardState.LOCKSCREEN
- }
- startTransitionTo(to, ownerReason = "lockWhileAwake: $lockReason")
+ .sample(communalSettingsInteractor.autoOpenEnabled, ::Pair)
+ .collect { (lockReason, autoOpenHub) ->
+ if (autoOpenHub) {
+ communalSceneInteractor.changeScene(
+ CommunalScenes.Communal,
+ "lockWhileAwake: $lockReason",
+ )
+ } else {
+ startTransitionTo(
+ KeyguardState.LOCKSCREEN,
+ ownerReason = "lockWhileAwake: $lockReason",
+ )
+ }
}
}
} else {
- scope.launch("$TAG#listenForGoneToLockscreenOrHubOrOccluded") {
+ scope.launch("$TAG#listenForGoneToLockscreenOrHubOrOccluded", mainDispatcher) {
keyguardInteractor.isKeyguardShowing
.filterRelevantKeyguardStateAnd { isKeyguardShowing -> isKeyguardShowing }
- .sample(communalSceneInteractor.isIdleOnCommunalNotEditMode, ::Pair)
- .collect { (_, isIdleOnCommunal) ->
+ .sample(communalSettingsInteractor.autoOpenEnabled, ::Pair)
+ .collect { (_, autoOpenHub) ->
val to =
- if (isIdleOnCommunal) {
- KeyguardState.GLANCEABLE_HUB
- } else if (keyguardInteractor.isKeyguardOccluded.value) {
+ if (keyguardInteractor.isKeyguardOccluded.value) {
KeyguardState.OCCLUDED
+ } else if (autoOpenHub) {
+ communalSceneInteractor.changeScene(
+ CommunalScenes.Communal,
+ "keyguard interactor says keyguard is showing",
+ )
+ return@collect
} else {
KeyguardState.LOCKSCREEN
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 2d5ff61a5015..e625fd72e159 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -507,6 +507,11 @@ constructor(
}
/** Temporary shim, until [KeyguardWmStateRefactor] is enabled */
+ fun showGlanceableHub(): Boolean {
+ return fromGoneTransitionInteractor.get().showGlanceableHub()
+ }
+
+ /** Temporary shim, until [KeyguardWmStateRefactor] is enabled */
fun dismissKeyguard() {
when (keyguardTransitionInteractor.transitionState.value.to) {
LOCKSCREEN -> fromLockscreenTransitionInteractor.get().dismissKeyguard()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
index 2fdca6bc68d9..d90292517b01 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt
@@ -51,13 +51,12 @@ import com.android.systemui.common.ui.view.onLayoutChanged
import com.android.systemui.common.ui.view.onTouchListener
import com.android.systemui.customization.R as customR
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor
-import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.ui.view.layout.sections.AodPromotedNotificationSection
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
import com.android.systemui.keyguard.ui.viewmodel.TransitionData
import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
@@ -72,7 +71,6 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shared.R as sharedR
import com.android.systemui.statusbar.CrossFadeHelper
import com.android.systemui.statusbar.VibratorHelper
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.temporarydisplay.ViewPriority
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
@@ -103,10 +101,8 @@ object KeyguardRootViewBinder {
configuration: ConfigurationState,
occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel?,
chipbarCoordinator: ChipbarCoordinator?,
- screenOffAnimationController: ScreenOffAnimationController,
shadeInteractor: ShadeInteractor,
- clockInteractor: KeyguardClockInteractor,
- clockViewModel: KeyguardClockViewModel,
+ smartspaceViewModel: KeyguardSmartspaceViewModel,
deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor?,
vibratorHelper: VibratorHelper?,
falsingManager: FalsingManager?,
@@ -326,7 +322,7 @@ object KeyguardRootViewBinder {
if (isFullyAnyExpanded) {
INVISIBLE
} else {
- View.VISIBLE
+ VISIBLE
}
}
}
@@ -394,7 +390,7 @@ object KeyguardRootViewBinder {
OnLayoutChange(
viewModel,
blueprintViewModel,
- clockViewModel,
+ smartspaceViewModel,
childViews,
burnInParams,
Logger(blueprintLog, TAG),
@@ -452,7 +448,7 @@ object KeyguardRootViewBinder {
private class OnLayoutChange(
private val viewModel: KeyguardRootViewModel,
private val blueprintViewModel: KeyguardBlueprintViewModel,
- private val clockViewModel: KeyguardClockViewModel,
+ private val smartspaceViewModel: KeyguardSmartspaceViewModel,
private val childViews: Map<Int, View>,
private val burnInParams: MutableStateFlow<BurnInParameters>,
private val logger: Logger,
@@ -470,12 +466,16 @@ object KeyguardRootViewBinder {
oldRight: Int,
oldBottom: Int,
) {
+ val prevSmartspaceVisibility = smartspaceViewModel.bcSmartspaceVisibility.value
+ val smartspaceVisibility = childViews[bcSmartspaceId]?.visibility ?: GONE
+ val smartspaceVisibilityChanged = prevSmartspaceVisibility != smartspaceVisibility
+
// After layout, ensure the notifications are positioned correctly
childViews[nsslPlaceholderId]?.let { notificationListPlaceholder ->
// Do not update a second time while a blueprint transition is running
val transition = blueprintViewModel.currentTransition.value
val shouldAnimate = transition != null && transition.config.type.animateNotifChanges
- if (prevTransition == transition && shouldAnimate) {
+ if (prevTransition == transition && shouldAnimate && !smartspaceVisibilityChanged) {
logger.w("Skipping onNotificationContainerBoundsChanged during transition")
return
}
@@ -484,7 +484,7 @@ object KeyguardRootViewBinder {
viewModel.onNotificationContainerBoundsChanged(
notificationListPlaceholder.top.toFloat(),
notificationListPlaceholder.bottom.toFloat(),
- animate = shouldAnimate,
+ animate = (shouldAnimate || smartspaceVisibilityChanged),
)
}
@@ -583,6 +583,8 @@ object KeyguardRootViewBinder {
private val aodNotificationIconContainerId = R.id.aod_notification_icon_container
private val largeClockId = customR.id.lockscreen_clock_view_large
private val largeClockDateId = sharedR.id.date_smartspace_view_large
+ private val largeClockWeatherId = sharedR.id.weather_smartspace_view_large
+ private val bcSmartspaceId = sharedR.id.bc_smartspace_view
private val smallClockId = customR.id.lockscreen_clock_view
private val indicationArea = R.id.keyguard_indication_area
private val startButton = R.id.start_button
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModel.kt
index 6d95ade59211..4bc722bc6695 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModel.kt
@@ -30,15 +30,15 @@ import kotlinx.coroutines.flow.Flow
@SysUISingleton
class GoneToGlanceableHubTransitionViewModel
@Inject
-constructor(
- animationFlow: KeyguardTransitionAnimationFlow,
-) : DeviceEntryIconTransition {
+constructor(animationFlow: KeyguardTransitionAnimationFlow) : DeviceEntryIconTransition {
private val transitionAnimation =
animationFlow
.setup(duration = TO_GLANCEABLE_HUB_DURATION, edge = Edge.INVALID)
.setupWithoutSceneContainer(edge = Edge.create(GONE, GLANCEABLE_HUB))
+ val keyguardAlpha = transitionAnimation.immediatelyTransitionTo(0f)
+
override val deviceEntryParentViewAlpha: Flow<Float> =
transitionAnimation.sharedFlow(
duration = 167.milliseconds,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 830afeac7b96..a5051657c2f0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -116,6 +116,7 @@ constructor(
private val goneToDozingTransitionViewModel: GoneToDozingTransitionViewModel,
private val goneToDreamingTransitionViewModel: GoneToDreamingTransitionViewModel,
private val goneToLockscreenTransitionViewModel: GoneToLockscreenTransitionViewModel,
+ private val goneToGlanceableHubTransitionViewModel: GoneToGlanceableHubTransitionViewModel,
private val lockscreenToAodTransitionViewModel: LockscreenToAodTransitionViewModel,
private val lockscreenToDozingTransitionViewModel: LockscreenToDozingTransitionViewModel,
private val lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel,
@@ -277,6 +278,7 @@ constructor(
primaryBouncerToAodTransitionViewModel.lockscreenAlpha,
primaryBouncerToGoneTransitionViewModel.lockscreenAlpha,
primaryBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
+ goneToGlanceableHubTransitionViewModel.keyguardAlpha,
)
.onStart { emit(0f) },
) { hideKeyguard, alpha ->
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt
index c1658e1f1694..5e9e930812a6 100644
--- a/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/AmbientLightModeMonitor.kt
@@ -23,7 +23,7 @@ import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.util.Log
import com.android.systemui.Dumpable
-import com.android.systemui.lowlightclock.dagger.LowLightModule.LIGHT_SENSOR
+import com.android.systemui.lowlightclock.dagger.LowLightModule.Companion.LIGHT_SENSOR
import com.android.systemui.util.sensors.AsyncSensorManager
import java.io.PrintWriter
import java.util.Optional
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java
deleted file mode 100644
index e5eec64ac615..000000000000
--- a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.lowlightclock;
-
-import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT;
-import static com.android.dream.lowlight.LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR;
-import static com.android.systemui.dreams.dagger.DreamModule.LOW_LIGHT_DREAM_SERVICE;
-import static com.android.systemui.keyguard.ScreenLifecycle.SCREEN_ON;
-import static com.android.systemui.lowlightclock.dagger.LowLightModule.LOW_LIGHT_PRECONDITIONS;
-
-import android.content.ComponentName;
-import android.content.pm.PackageManager;
-
-import androidx.annotation.Nullable;
-
-import com.android.dream.lowlight.LowLightDreamManager;
-import com.android.systemui.dagger.qualifiers.Background;
-import com.android.systemui.dagger.qualifiers.SystemUser;
-import com.android.systemui.keyguard.ScreenLifecycle;
-import com.android.systemui.shared.condition.Condition;
-import com.android.systemui.shared.condition.Monitor;
-import com.android.systemui.util.condition.ConditionalCoreStartable;
-
-import dagger.Lazy;
-
-import java.util.Set;
-import java.util.concurrent.Executor;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-
-/**
- * Tracks environment (low-light or not) in order to correctly show or hide a low-light clock while
- * dreaming.
- */
-public class LowLightMonitor extends ConditionalCoreStartable implements Monitor.Callback,
- ScreenLifecycle.Observer {
- private static final String TAG = "LowLightMonitor";
-
- private final Lazy<LowLightDreamManager> mLowLightDreamManager;
- private final Monitor mConditionsMonitor;
- private final Lazy<Set<Condition>> mLowLightConditions;
- private Monitor.Subscription.Token mSubscriptionToken;
- private ScreenLifecycle mScreenLifecycle;
- private final LowLightLogger mLogger;
-
- private final ComponentName mLowLightDreamService;
-
- private final PackageManager mPackageManager;
-
- private final Executor mExecutor;
-
- @Inject
- public LowLightMonitor(Lazy<LowLightDreamManager> lowLightDreamManager,
- @SystemUser Monitor conditionsMonitor,
- @Named(LOW_LIGHT_PRECONDITIONS) Lazy<Set<Condition>> lowLightConditions,
- ScreenLifecycle screenLifecycle,
- LowLightLogger lowLightLogger,
- @Nullable @Named(LOW_LIGHT_DREAM_SERVICE) ComponentName lowLightDreamService,
- PackageManager packageManager,
- @Background Executor backgroundExecutor) {
- super(conditionsMonitor);
- mLowLightDreamManager = lowLightDreamManager;
- mConditionsMonitor = conditionsMonitor;
- mLowLightConditions = lowLightConditions;
- mScreenLifecycle = screenLifecycle;
- mLogger = lowLightLogger;
- mLowLightDreamService = lowLightDreamService;
- mPackageManager = packageManager;
- mExecutor = backgroundExecutor;
- }
-
- @Override
- public void onConditionsChanged(boolean allConditionsMet) {
- mExecutor.execute(() -> {
- mLogger.d(TAG, "Low light enabled: " + allConditionsMet);
-
- mLowLightDreamManager.get().setAmbientLightMode(allConditionsMet
- ? AMBIENT_LIGHT_MODE_LOW_LIGHT : AMBIENT_LIGHT_MODE_REGULAR);
- });
- }
-
- @Override
- public void onScreenTurnedOn() {
- mExecutor.execute(() -> {
- if (mSubscriptionToken == null) {
- mLogger.d(TAG, "Screen turned on. Subscribing to low light conditions.");
-
- mSubscriptionToken = mConditionsMonitor.addSubscription(
- new Monitor.Subscription.Builder(this)
- .addConditions(mLowLightConditions.get())
- .build());
- }
- });
- }
-
-
- @Override
- public void onScreenTurnedOff() {
- mExecutor.execute(() -> {
- if (mSubscriptionToken != null) {
- mLogger.d(TAG, "Screen turned off. Removing subscription to low light conditions.");
-
- mConditionsMonitor.removeSubscription(mSubscriptionToken);
- mSubscriptionToken = null;
- }
- });
- }
-
- @Override
- protected void onStart() {
- mExecutor.execute(() -> {
- if (mLowLightDreamService != null) {
- // Note that the dream service is disabled by default. This prevents the dream from
- // appearing in settings on devices that don't have it explicitly excluded (done in
- // the settings overlay). Therefore, the component is enabled if it is to be used
- // here.
- mPackageManager.setComponentEnabledSetting(
- mLowLightDreamService,
- PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
- PackageManager.DONT_KILL_APP
- );
- } else {
- // If there is no low light dream service, do not observe conditions.
- return;
- }
-
- mScreenLifecycle.addObserver(this);
- if (mScreenLifecycle.getScreenState() == SCREEN_ON) {
- onScreenTurnedOn();
- }
- });
-
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.kt
new file mode 100644
index 000000000000..137226332e38
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/LowLightMonitor.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2025 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.lowlightclock
+
+import android.content.ComponentName
+import android.content.pm.PackageManager
+import com.android.dream.lowlight.LowLightDreamManager
+import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.SystemUser
+import com.android.systemui.dreams.dagger.DreamModule
+import com.android.systemui.lowlightclock.dagger.LowLightModule
+import com.android.systemui.shared.condition.Condition
+import com.android.systemui.shared.condition.Monitor
+import com.android.systemui.util.condition.ConditionalCoreStartable
+import com.android.systemui.util.kotlin.BooleanFlowOperators.not
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import dagger.Lazy
+import javax.inject.Inject
+import javax.inject.Named
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.launch
+
+/**
+ * Tracks environment (low-light or not) in order to correctly show or hide a low-light clock while
+ * dreaming.
+ */
+class LowLightMonitor
+@Inject
+constructor(
+ private val lowLightDreamManager: Lazy<LowLightDreamManager>,
+ @param:SystemUser private val conditionsMonitor: Monitor,
+ @param:Named(LowLightModule.LOW_LIGHT_PRECONDITIONS)
+ private val lowLightConditions: Lazy<Set<Condition>>,
+ displayStateInteractor: DisplayStateInteractor,
+ private val logger: LowLightLogger,
+ @param:Named(DreamModule.LOW_LIGHT_DREAM_SERVICE)
+ private val lowLightDreamService: ComponentName?,
+ private val packageManager: PackageManager,
+ @Background private val scope: CoroutineScope,
+) : ConditionalCoreStartable(conditionsMonitor) {
+ private val isScreenOn = not(displayStateInteractor.isDefaultDisplayOff).distinctUntilChanged()
+
+ private val isLowLight = conflatedCallbackFlow {
+ val token =
+ conditionsMonitor.addSubscription(
+ Monitor.Subscription.Builder { trySend(it) }
+ .addConditions(lowLightConditions.get())
+ .build()
+ )
+
+ awaitClose { conditionsMonitor.removeSubscription(token) }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ override fun onStart() {
+ scope.launch {
+ if (lowLightDreamService != null) {
+ // Note that the dream service is disabled by default. This prevents the dream from
+ // appearing in settings on devices that don't have it explicitly excluded (done in
+ // the settings overlay). Therefore, the component is enabled if it is to be used
+ // here.
+ packageManager.setComponentEnabledSetting(
+ lowLightDreamService,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ PackageManager.DONT_KILL_APP,
+ )
+ } else {
+ // If there is no low light dream service, do not observe conditions.
+ return@launch
+ }
+
+ isScreenOn
+ .flatMapLatest {
+ if (it) {
+ isLowLight
+ } else {
+ flowOf(false)
+ }
+ }
+ .distinctUntilChanged()
+ .collect {
+ logger.d(TAG, "Low light enabled: $it")
+ lowLightDreamManager
+ .get()
+ .setAmbientLightMode(
+ if (it) LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT
+ else LowLightDreamManager.AMBIENT_LIGHT_MODE_REGULAR
+ )
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "LowLightMonitor"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java
deleted file mode 100644
index f8072f2f79b4..000000000000
--- a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.java
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.lowlightclock.dagger;
-
-import android.annotation.Nullable;
-import android.content.res.Resources;
-import android.hardware.Sensor;
-
-import com.android.dream.lowlight.dagger.LowLightDreamModule;
-import com.android.systemui.CoreStartable;
-import com.android.systemui.communal.DeviceInactiveCondition;
-import com.android.systemui.dagger.SysUISingleton;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.log.LogBuffer;
-import com.android.systemui.log.LogBufferFactory;
-import com.android.systemui.lowlightclock.AmbientLightModeMonitor;
-import com.android.systemui.lowlightclock.DirectBootCondition;
-import com.android.systemui.lowlightclock.ForceLowLightCondition;
-import com.android.systemui.lowlightclock.LowLightCondition;
-import com.android.systemui.lowlightclock.LowLightDisplayController;
-import com.android.systemui.lowlightclock.LowLightMonitor;
-import com.android.systemui.lowlightclock.ScreenSaverEnabledCondition;
-import com.android.systemui.res.R;
-import com.android.systemui.shared.condition.Condition;
-
-import dagger.Binds;
-import dagger.BindsOptionalOf;
-import dagger.Module;
-import dagger.Provides;
-import dagger.multibindings.ClassKey;
-import dagger.multibindings.IntoMap;
-import dagger.multibindings.IntoSet;
-
-import javax.inject.Named;
-
-@Module(includes = LowLightDreamModule.class)
-public abstract class LowLightModule {
- public static final String Y_TRANSLATION_ANIMATION_OFFSET =
- "y_translation_animation_offset";
- public static final String Y_TRANSLATION_ANIMATION_DURATION_MILLIS =
- "y_translation_animation_duration_millis";
- public static final String ALPHA_ANIMATION_IN_START_DELAY_MILLIS =
- "alpha_animation_in_start_delay_millis";
- public static final String ALPHA_ANIMATION_DURATION_MILLIS =
- "alpha_animation_duration_millis";
- public static final String LOW_LIGHT_PRECONDITIONS = "low_light_preconditions";
- public static final String LIGHT_SENSOR = "low_light_monitor_light_sensor";
-
-
- /**
- * Provides a {@link LogBuffer} for logs related to low-light features.
- */
- @Provides
- @SysUISingleton
- @LowLightLog
- public static LogBuffer provideLowLightLogBuffer(LogBufferFactory factory) {
- return factory.create("LowLightLog", 250);
- }
-
- @Binds
- @IntoSet
- @Named(LOW_LIGHT_PRECONDITIONS)
- abstract Condition bindScreenSaverEnabledCondition(ScreenSaverEnabledCondition condition);
-
- @Provides
- @IntoSet
- @Named(LOW_LIGHT_PRECONDITIONS)
- static Condition provideLowLightCondition(LowLightCondition lowLightCondition,
- DirectBootCondition directBootCondition) {
- // Start lowlight if we are either in lowlight or in direct boot. The ordering of the
- // conditions matters here since we don't want to start the lowlight condition if
- // we are in direct boot mode.
- return directBootCondition.or(lowLightCondition);
- }
-
- @Binds
- @IntoSet
- @Named(LOW_LIGHT_PRECONDITIONS)
- abstract Condition bindForceLowLightCondition(ForceLowLightCondition condition);
-
- @Binds
- @IntoSet
- @Named(LOW_LIGHT_PRECONDITIONS)
- abstract Condition bindDeviceInactiveCondition(DeviceInactiveCondition condition);
-
- @BindsOptionalOf
- abstract LowLightDisplayController bindsLowLightDisplayController();
-
- @BindsOptionalOf
- @Nullable
- @Named(LIGHT_SENSOR)
- abstract Sensor bindsLightSensor();
-
- @BindsOptionalOf
- abstract AmbientLightModeMonitor.DebounceAlgorithm bindsDebounceAlgorithm();
-
- /**
- *
- */
- @Provides
- @Named(Y_TRANSLATION_ANIMATION_OFFSET)
- static int providesAnimationInOffset(@Main Resources resources) {
- return resources.getDimensionPixelOffset(
- R.dimen.low_light_clock_translate_animation_offset);
- }
-
- /**
- *
- */
- @Provides
- @Named(Y_TRANSLATION_ANIMATION_DURATION_MILLIS)
- static long providesAnimationDurationMillis(@Main Resources resources) {
- return resources.getInteger(R.integer.low_light_clock_translate_animation_duration_ms);
- }
-
- /**
- *
- */
- @Provides
- @Named(ALPHA_ANIMATION_IN_START_DELAY_MILLIS)
- static long providesAlphaAnimationInStartDelayMillis(@Main Resources resources) {
- return resources.getInteger(R.integer.low_light_clock_alpha_animation_in_start_delay_ms);
- }
-
- /**
- *
- */
- @Provides
- @Named(ALPHA_ANIMATION_DURATION_MILLIS)
- static long providesAlphaAnimationDurationMillis(@Main Resources resources) {
- return resources.getInteger(R.integer.low_light_clock_alpha_animation_duration_ms);
- }
- /** Inject into LowLightMonitor. */
- @Binds
- @IntoMap
- @ClassKey(LowLightMonitor.class)
- abstract CoreStartable bindLowLightMonitor(LowLightMonitor lowLightMonitor);
-}
diff --git a/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.kt b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.kt
new file mode 100644
index 000000000000..6b3254e928ec
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/lowlightclock/dagger/LowLightModule.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2025 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.lowlightclock.dagger
+
+import android.content.res.Resources
+import android.hardware.Sensor
+import com.android.dream.lowlight.dagger.LowLightDreamModule
+import com.android.systemui.CoreStartable
+import com.android.systemui.communal.DeviceInactiveCondition
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogBufferFactory
+import com.android.systemui.lowlightclock.AmbientLightModeMonitor.DebounceAlgorithm
+import com.android.systemui.lowlightclock.DirectBootCondition
+import com.android.systemui.lowlightclock.ForceLowLightCondition
+import com.android.systemui.lowlightclock.LowLightCondition
+import com.android.systemui.lowlightclock.LowLightDisplayController
+import com.android.systemui.lowlightclock.LowLightMonitor
+import com.android.systemui.lowlightclock.ScreenSaverEnabledCondition
+import com.android.systemui.res.R
+import com.android.systemui.shared.condition.Condition
+import dagger.Binds
+import dagger.BindsOptionalOf
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+import dagger.multibindings.IntoSet
+import javax.inject.Named
+
+@Module(includes = [LowLightDreamModule::class])
+abstract class LowLightModule {
+ @Binds
+ @IntoSet
+ @Named(LOW_LIGHT_PRECONDITIONS)
+ abstract fun bindScreenSaverEnabledCondition(condition: ScreenSaverEnabledCondition): Condition
+
+ @Binds
+ @IntoSet
+ @Named(LOW_LIGHT_PRECONDITIONS)
+ abstract fun bindForceLowLightCondition(condition: ForceLowLightCondition): Condition
+
+ @Binds
+ @IntoSet
+ @Named(LOW_LIGHT_PRECONDITIONS)
+ abstract fun bindDeviceInactiveCondition(condition: DeviceInactiveCondition): Condition
+
+ @BindsOptionalOf abstract fun bindsLowLightDisplayController(): LowLightDisplayController
+
+ @BindsOptionalOf @Named(LIGHT_SENSOR) abstract fun bindsLightSensor(): Sensor
+
+ @BindsOptionalOf abstract fun bindsDebounceAlgorithm(): DebounceAlgorithm
+
+ /** Inject into LowLightMonitor. */
+ @Binds
+ @IntoMap
+ @ClassKey(LowLightMonitor::class)
+ abstract fun bindLowLightMonitor(lowLightMonitor: LowLightMonitor): CoreStartable
+
+ companion object {
+ const val Y_TRANSLATION_ANIMATION_OFFSET: String = "y_translation_animation_offset"
+ const val Y_TRANSLATION_ANIMATION_DURATION_MILLIS: String =
+ "y_translation_animation_duration_millis"
+ const val ALPHA_ANIMATION_IN_START_DELAY_MILLIS: String =
+ "alpha_animation_in_start_delay_millis"
+ const val ALPHA_ANIMATION_DURATION_MILLIS: String = "alpha_animation_duration_millis"
+ const val LOW_LIGHT_PRECONDITIONS: String = "low_light_preconditions"
+ const val LIGHT_SENSOR: String = "low_light_monitor_light_sensor"
+
+ /** Provides a [LogBuffer] for logs related to low-light features. */
+ @JvmStatic
+ @Provides
+ @SysUISingleton
+ @LowLightLog
+ fun provideLowLightLogBuffer(factory: LogBufferFactory): LogBuffer {
+ return factory.create("LowLightLog", 250)
+ }
+
+ @Provides
+ @IntoSet
+ @Named(LOW_LIGHT_PRECONDITIONS)
+ fun provideLowLightCondition(
+ lowLightCondition: LowLightCondition,
+ directBootCondition: DirectBootCondition,
+ ): Condition {
+ // Start lowlight if we are either in lowlight or in direct boot. The ordering of the
+ // conditions matters here since we don't want to start the lowlight condition if
+ // we are in direct boot mode.
+ return directBootCondition.or(lowLightCondition)
+ }
+
+ /** */
+ @JvmStatic
+ @Provides
+ @Named(Y_TRANSLATION_ANIMATION_OFFSET)
+ fun providesAnimationInOffset(@Main resources: Resources): Int {
+ return resources.getDimensionPixelOffset(
+ R.dimen.low_light_clock_translate_animation_offset
+ )
+ }
+
+ /** */
+ @JvmStatic
+ @Provides
+ @Named(Y_TRANSLATION_ANIMATION_DURATION_MILLIS)
+ fun providesAnimationDurationMillis(@Main resources: Resources): Long {
+ return resources
+ .getInteger(R.integer.low_light_clock_translate_animation_duration_ms)
+ .toLong()
+ }
+
+ /** */
+ @JvmStatic
+ @Provides
+ @Named(ALPHA_ANIMATION_IN_START_DELAY_MILLIS)
+ fun providesAlphaAnimationInStartDelayMillis(@Main resources: Resources): Long {
+ return resources
+ .getInteger(R.integer.low_light_clock_alpha_animation_in_start_delay_ms)
+ .toLong()
+ }
+
+ /** */
+ @JvmStatic
+ @Provides
+ @Named(ALPHA_ANIMATION_DURATION_MILLIS)
+ fun providesAlphaAnimationDurationMillis(@Main resources: Resources): Long {
+ return resources
+ .getInteger(R.integer.low_light_clock_alpha_animation_duration_ms)
+ .toLong()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt
index df0e1adee968..2e0e3a78c5b7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaProcessingHelper.kt
@@ -24,7 +24,6 @@ import android.media.session.MediaController
import android.media.session.PlaybackState
import android.os.BadParcelableException
import android.util.Log
-import com.android.systemui.Flags.mediaControlsPostsOptimization
import com.android.systemui.biometrics.Utils.toBitmap
import com.android.systemui.media.controls.shared.model.MediaData
@@ -45,7 +44,7 @@ fun isSameMediaData(
new: MediaData,
old: MediaData?,
): Boolean {
- if (old == null || !mediaControlsPostsOptimization()) return false
+ if (old == null) return false
return new.userId == old.userId &&
new.app == old.app &&
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt
index 78a8cf8e9432..6175b6e47b52 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/viewmodel/SeekBarViewModel.kt
@@ -36,7 +36,6 @@ import androidx.annotation.WorkerThread
import androidx.core.view.GestureDetectorCompat
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
-import com.android.systemui.Flags
import com.android.systemui.classifier.Classifier.MEDIA_SEEKBAR
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.media.NotificationMediaManager
@@ -151,7 +150,6 @@ constructor(
}
override fun onMetadataChanged(metadata: MediaMetadata?) {
- if (!Flags.mediaControlsPostsOptimization()) return
val (enabled, duration) = getEnabledStateAndDuration(metadata)
if (_data.duration != duration) {
_data = _data.copy(enabled = enabled, duration = duration)
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java
index 7b1c62e2a0e5..78e66235112a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaItem.java
@@ -37,16 +37,21 @@ public class MediaItem {
@MediaItemType
private final int mMediaItemType;
private final boolean mIsFirstDeviceInGroup;
+ private final boolean mIsExpandableDivider;
+ private final boolean mHasTopSeparator;
@Retention(RetentionPolicy.SOURCE)
@IntDef({
MediaItemType.TYPE_DEVICE,
MediaItemType.TYPE_GROUP_DIVIDER,
- MediaItemType.TYPE_PAIR_NEW_DEVICE})
+ MediaItemType.TYPE_PAIR_NEW_DEVICE,
+ MediaItemType.TYPE_DEVICE_GROUP
+ })
public @interface MediaItemType {
int TYPE_DEVICE = 0;
int TYPE_GROUP_DIVIDER = 1;
int TYPE_PAIR_NEW_DEVICE = 2;
+ int TYPE_DEVICE_GROUP = 3;
}
/**
@@ -70,6 +75,18 @@ public class MediaItem {
}
/**
+ * Returns a new {@link MediaItemType#TYPE_DEVICE_GROUP} {@link MediaItem}. This items controls
+ * the volume of the group session.
+ */
+ public static MediaItem createDeviceGroupMediaItem() {
+ return new MediaItem(
+ /* device */ null,
+ /* title */ null,
+ /* type */ MediaItemType.TYPE_DEVICE_GROUP,
+ /* misFirstDeviceInGroup */ false);
+ }
+
+ /**
* Returns a new {@link MediaItemType#TYPE_PAIR_NEW_DEVICE} {@link MediaItem} with both {@link
* #getMediaDevice() media device} and title set to {@code null}.
*/
@@ -93,15 +110,58 @@ public class MediaItem {
/* misFirstDeviceInGroup */ false);
}
+ /**
+ * Returns a new {@link MediaItemType#TYPE_GROUP_DIVIDER} {@link MediaItem} with the specified
+ * title and a {@code null} {@link #getMediaDevice() media device}. This item needs to be
+ * rendered with a separator above it.
+ */
+ public static MediaItem createGroupDividerWithSeparatorMediaItem(@Nullable String title) {
+ return new MediaItem(
+ /* device */ null,
+ title,
+ MediaItemType.TYPE_GROUP_DIVIDER,
+ /* isFirstDeviceInGroup */ false,
+ /* isExpandableDivider */ false,
+ /* hasTopSeparator */ true);
+ }
+
+ /**
+ * Returns a new {@link MediaItemType#TYPE_GROUP_DIVIDER} {@link MediaItem} with the specified
+ * title and a {@code null} {@link #getMediaDevice() media device}. The item serves as a toggle
+ * for expanding/collapsing the group of devices.
+ */
+ public static MediaItem createExpandableGroupDividerMediaItem(@Nullable String title) {
+ return new MediaItem(
+ /* device */ null,
+ title,
+ MediaItemType.TYPE_GROUP_DIVIDER,
+ /* isFirstDeviceInGroup */ false,
+ /* isExpandableDivider */ true,
+ /* hasTopSeparator */ false);
+ }
+
private MediaItem(
@Nullable MediaDevice device,
@Nullable String title,
@MediaItemType int type,
boolean isFirstDeviceInGroup) {
+ this(device, title, type, isFirstDeviceInGroup, /* isExpandableDivider */
+ false, /* hasTopSeparator */ false);
+ }
+
+ private MediaItem(
+ @Nullable MediaDevice device,
+ @Nullable String title,
+ @MediaItemType int type,
+ boolean isFirstDeviceInGroup,
+ boolean isExpandableDivider,
+ boolean hasTopSeparator) {
this.mMediaDeviceOptional = Optional.ofNullable(device);
this.mTitle = title;
this.mMediaItemType = type;
this.mIsFirstDeviceInGroup = isFirstDeviceInGroup;
+ this.mIsExpandableDivider = isExpandableDivider;
+ this.mHasTopSeparator = hasTopSeparator;
}
public Optional<MediaDevice> getMediaDevice() {
@@ -133,4 +193,14 @@ public class MediaItem {
public boolean isFirstDeviceInGroup() {
return mIsFirstDeviceInGroup;
}
+
+ /** Returns whether a group divider has a button that expands group device list */
+ public boolean isExpandableDivider() {
+ return mIsExpandableDivider;
+ }
+
+ /** Returns whether a group divider has a border at the top */
+ public boolean hasTopSeparator() {
+ return mHasTopSeparator;
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.kt
new file mode 100644
index 000000000000..4c34250c9653
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.kt
@@ -0,0 +1,688 @@
+/*
+ * Copyright (C) 2025 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.media.dialog
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.graphics.Typeface
+import android.graphics.drawable.AnimatedVectorDrawable
+import android.graphics.drawable.Drawable
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.View.GONE
+import android.view.View.VISIBLE
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.ProgressBar
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+import androidx.appcompat.content.res.AppCompatResources
+import androidx.recyclerview.widget.RecyclerView
+import com.android.settingslib.media.InputMediaDevice
+import com.android.settingslib.media.MediaDevice
+import com.android.systemui.FontStyles.GSF_TITLE_MEDIUM_EMPHASIZED
+import com.android.systemui.FontStyles.GSF_TITLE_SMALL
+import com.android.systemui.media.dialog.MediaItem.MediaItemType.TYPE_DEVICE
+import com.android.systemui.media.dialog.MediaItem.MediaItemType.TYPE_DEVICE_GROUP
+import com.android.systemui.media.dialog.MediaItem.MediaItemType.TYPE_GROUP_DIVIDER
+import com.android.systemui.media.dialog.MediaOutputAdapterBase.ConnectionState.CONNECTED
+import com.android.systemui.media.dialog.MediaOutputAdapterBase.ConnectionState.CONNECTING
+import com.android.systemui.media.dialog.MediaOutputAdapterBase.ConnectionState.DISCONNECTED
+import com.android.systemui.res.R
+import com.android.systemui.util.kotlin.getOrNull
+import com.google.android.material.slider.Slider
+
+/** A RecyclerView adapter for the legacy UI media output dialog device list. */
+class MediaOutputAdapter(controller: MediaSwitchingController) :
+ MediaOutputAdapterBase(controller) {
+ private val mGroupSelectedItems = mController.selectedMediaDevice.size > 1
+
+ /** Refreshes the RecyclerView dataset and forces re-render. */
+ override fun updateItems() {
+ val newList =
+ mController.getMediaItemList(false /* addConnectNewDeviceButton */).toMutableList()
+
+ addSeparatorForTheFirstGroupDivider(newList)
+ coalesceSelectedDevices(newList)
+
+ mMediaItemList.clear()
+ mMediaItemList.addAll(newList)
+
+ notifyDataSetChanged()
+ }
+
+ private fun addSeparatorForTheFirstGroupDivider(newList: MutableList<MediaItem>) {
+ for ((i, item) in newList.withIndex()) {
+ if (item.mediaItemType == TYPE_GROUP_DIVIDER) {
+ newList[i] = MediaItem.createGroupDividerWithSeparatorMediaItem(item.title)
+ break
+ }
+ }
+ }
+
+ /**
+ * If there are 2+ selected devices, adds an "Connected speakers" expandable group divider and
+ * displays a single session control instead of individual device controls.
+ */
+ private fun coalesceSelectedDevices(newList: MutableList<MediaItem>) {
+ val selectedDevices = newList.filter { this.isSelectedDevice(it) }
+
+ if (mGroupSelectedItems && selectedDevices.size > 1) {
+ newList.removeAll(selectedDevices.toSet())
+ if (mController.isGroupListCollapsed) {
+ newList.add(0, MediaItem.createDeviceGroupMediaItem())
+ } else {
+ newList.addAll(0, selectedDevices)
+ }
+ newList.add(0, mController.connectedSpeakersExpandableGroupDivider)
+ }
+ }
+
+ private fun isSelectedDevice(mediaItem: MediaItem): Boolean {
+ return mediaItem.mediaDevice.getOrNull()?.let { device ->
+ isDeviceIncluded(mController.selectedMediaDevice, device)
+ } ?: false
+ }
+
+ override fun getItemId(position: Int): Long {
+ if (position >= mMediaItemList.size) {
+ Log.e(TAG, "Item position exceeds list size: $position")
+ return RecyclerView.NO_ID
+ }
+ val currentMediaItem = mMediaItemList[position]
+ return when (currentMediaItem.mediaItemType) {
+ TYPE_DEVICE ->
+ currentMediaItem.mediaDevice.getOrNull()?.id?.hashCode()?.toLong()
+ ?: RecyclerView.NO_ID
+ TYPE_GROUP_DIVIDER -> currentMediaItem.title.hashCode().toLong()
+ TYPE_DEVICE_GROUP -> currentMediaItem.hashCode().toLong()
+ else -> RecyclerView.NO_ID
+ }
+ }
+
+ override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ val context = viewGroup.context
+ return when (viewType) {
+ TYPE_GROUP_DIVIDER -> {
+ val holderView =
+ LayoutInflater.from(context)
+ .inflate(R.layout.media_output_list_item_group_divider, viewGroup, false)
+ MediaGroupDividerViewHolder(holderView, context)
+ }
+
+ TYPE_DEVICE,
+ TYPE_DEVICE_GROUP -> {
+ val holderView =
+ LayoutInflater.from(context)
+ .inflate(R.layout.media_output_list_item_device, viewGroup, false)
+ MediaDeviceViewHolder(holderView, context)
+ }
+
+ else -> throw IllegalArgumentException("Invalid view type: $viewType")
+ }
+ }
+
+ override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
+ require(position < itemCount) { "Invalid position: $position, list size: $itemCount" }
+ val currentMediaItem = mMediaItemList[position]
+ when (currentMediaItem.mediaItemType) {
+ TYPE_GROUP_DIVIDER ->
+ (viewHolder as MediaGroupDividerViewHolder).onBind(
+ groupDividerTitle = currentMediaItem.title,
+ isExpandableDivider = currentMediaItem.isExpandableDivider,
+ hasTopSeparator = currentMediaItem.hasTopSeparator(),
+ )
+
+ TYPE_DEVICE ->
+ (viewHolder as MediaDeviceViewHolder).onBindDevice(
+ mediaItem = currentMediaItem,
+ position = position,
+ )
+
+ TYPE_DEVICE_GROUP -> (viewHolder as MediaDeviceViewHolder).onBindDeviceGroup()
+ else ->
+ throw IllegalArgumentException(
+ "Invalid item type ${currentMediaItem.mediaItemType} for position: $position"
+ )
+ }
+ }
+
+ val controller: MediaSwitchingController
+ get() = mController
+
+ /** ViewHolder for binding device view. */
+ inner class MediaDeviceViewHolder(view: View, context: Context?) :
+ MediaDeviceViewHolderBase(view, context) {
+ @VisibleForTesting val mMainContent: LinearLayout = view.requireViewById(R.id.main_content)
+
+ @VisibleForTesting val mItemLayout: LinearLayout = view.requireViewById(R.id.item_layout)
+
+ @VisibleForTesting val mTitleText: TextView = view.requireViewById(R.id.title)
+
+ @VisibleForTesting val mSubTitleText: TextView = view.requireViewById(R.id.subtitle)
+
+ @VisibleForTesting val mTitleIcon: ImageView = view.requireViewById(R.id.title_icon)
+
+ @VisibleForTesting
+ val mLoadingIndicator: ProgressBar = view.requireViewById(R.id.loading_indicator)
+
+ @VisibleForTesting val mStatusIcon: ImageView = view.requireViewById(R.id.status_icon)
+
+ @VisibleForTesting val mGroupButton: ImageButton = view.requireViewById(R.id.group_button)
+
+ @VisibleForTesting val mDivider: View = view.requireViewById(R.id.divider)
+
+ @VisibleForTesting
+ val mOngoingSessionButton: ImageButton = view.requireViewById(R.id.ongoing_session_button)
+
+ @VisibleForTesting var mSlider: Slider = view.requireViewById(R.id.volume_seekbar)
+ private var mLatestUpdateVolume = NO_VOLUME_SET
+
+ private val mInactivePadding =
+ mContext.resources.getDimension(R.dimen.media_output_item_content_vertical_margin)
+ private val mActivePadding =
+ mContext.resources.getDimension(
+ R.dimen.media_output_item_content_vertical_margin_active
+ )
+ private val mSubtitleAlpha =
+ mContext.resources.getFloat(R.dimen.media_output_item_subtitle_alpha)
+
+ fun onBindDevice(mediaItem: MediaItem, position: Int) {
+ resetViewState()
+ renderItem(mediaItem, position)
+ }
+
+ fun onBindDeviceGroup() {
+ resetViewState()
+ renderDeviceGroupItem()
+ }
+
+ private fun resetViewState() {
+ mItemLayout.visibility = VISIBLE
+ mGroupButton.visibility = GONE
+ mOngoingSessionButton.visibility = GONE
+ mStatusIcon.visibility = GONE
+ mLoadingIndicator.visibility = GONE
+ mDivider.visibility = GONE
+ mSubTitleText.visibility = GONE
+ mMainContent.setOnClickListener(null)
+ }
+
+ override fun renderDeviceItem(
+ hideGroupItem: Boolean,
+ device: MediaDevice,
+ connectionState: ConnectionState,
+ restrictVolumeAdjustment: Boolean,
+ groupStatus: GroupStatus?,
+ ongoingSessionStatus: OngoingSessionStatus?,
+ clickListener: View.OnClickListener?,
+ deviceDisabled: Boolean,
+ subtitle: String?,
+ deviceStatusIcon: Drawable?,
+ ) {
+ val fixedVolumeConnected = connectionState == CONNECTED && restrictVolumeAdjustment
+ val colorTheme = ColorTheme(fixedVolumeConnected, deviceDisabled)
+
+ updateTitle(device.name, connectionState, colorTheme)
+ updateTitleIcon(device, connectionState, restrictVolumeAdjustment, colorTheme)
+ updateSubtitle(subtitle, colorTheme)
+ updateSeekBar(device, connectionState, restrictVolumeAdjustment, colorTheme)
+ updateEndArea(device, connectionState, groupStatus, ongoingSessionStatus, colorTheme)
+ updateLoadingIndicator(connectionState, colorTheme)
+ updateDeviceStatusIcon(deviceStatusIcon, colorTheme)
+ updateContentBackground(fixedVolumeConnected, colorTheme)
+ updateContentClickListener(clickListener)
+ }
+
+ override fun renderDeviceGroupItem() {
+ mTitleIcon.visibility = GONE
+ val colorTheme = ColorTheme()
+ updateTitle(
+ title = mController.sessionName ?: "",
+ connectionState = CONNECTED,
+ colorTheme = colorTheme,
+ )
+ updateGroupSeekBar(colorTheme)
+ }
+
+ private fun updateTitle(
+ title: CharSequence,
+ connectionState: ConnectionState,
+ colorTheme: ColorTheme,
+ ) {
+ mTitleText.text = title
+ val fontFamilyName: String =
+ if (connectionState == CONNECTED) GSF_TITLE_MEDIUM_EMPHASIZED else GSF_TITLE_SMALL
+ mTitleText.typeface = Typeface.create(fontFamilyName, Typeface.NORMAL)
+ mTitleText.setTextColor(colorTheme.titleColor)
+ mTitleText.alpha = colorTheme.contentAlpha
+ }
+
+ private fun updateContentBackground(fixedVolumeConnected: Boolean, colorTheme: ColorTheme) {
+ if (fixedVolumeConnected) {
+ mMainContent.backgroundTintList =
+ ColorStateList.valueOf(colorTheme.containerRestrictedVolumeBackground)
+ mMainContent.background =
+ AppCompatResources.getDrawable(
+ mContext,
+ R.drawable.media_output_dialog_item_fixed_volume_background,
+ )
+ } else {
+ mMainContent.background = null
+ mMainContent.setBackgroundColor(Color.TRANSPARENT)
+ }
+ }
+
+ private fun updateContentPadding(verticalPadding: Float) {
+ mMainContent.setPadding(0, verticalPadding.toInt(), 0, verticalPadding.toInt())
+ }
+
+ private fun updateLayoutForSlider(showSlider: Boolean) {
+ updateContentPadding(if (showSlider) mActivePadding else mInactivePadding)
+ mSlider.visibility = if (showSlider) VISIBLE else GONE
+ mSlider.alpha = if (showSlider) 1f else 0f
+ }
+
+ private fun updateSeekBar(
+ device: MediaDevice,
+ connectionState: ConnectionState,
+ restrictVolumeAdjustment: Boolean,
+ colorTheme: ColorTheme,
+ ) {
+ val showSlider = connectionState == CONNECTED && !restrictVolumeAdjustment
+ if (showSlider) {
+ updateLayoutForSlider(showSlider = true)
+ initSeekbar(
+ volumeChangeCallback = { volume: Int ->
+ mController.adjustVolume(device, volume)
+ },
+ settleCallback = { mController.logInteractionAdjustVolume(device) },
+ deviceDrawable = mController.getDeviceIconDrawable(device),
+ isInputDevice = device is InputMediaDevice,
+ isVolumeControlAllowed = mController.isVolumeControlEnabled(device),
+ currentVolume = device.currentVolume,
+ maxVolume = device.maxVolume,
+ colorTheme = colorTheme,
+ )
+ } else {
+ updateLayoutForSlider(showSlider = false)
+ }
+ }
+
+ private fun updateGroupSeekBar(colorTheme: ColorTheme) {
+ mSlider.visibility = VISIBLE
+ updateContentPadding(mActivePadding)
+ val groupDrawable =
+ AppCompatResources.getDrawable(
+ mContext,
+ com.android.settingslib.R.drawable.ic_media_group_device,
+ )
+ initSeekbar(
+ volumeChangeCallback = { volume: Int -> mController.adjustSessionVolume(volume) },
+ deviceDrawable = groupDrawable,
+ isVolumeControlAllowed = mController.isVolumeControlEnabledForSession,
+ currentVolume = mController.sessionVolume,
+ maxVolume = mController.sessionVolumeMax,
+ colorTheme = colorTheme,
+ )
+ }
+
+ private fun updateSubtitle(subtitle: String?, colorTheme: ColorTheme) {
+ if (subtitle.isNullOrEmpty()) {
+ mSubTitleText.visibility = GONE
+ } else {
+ mSubTitleText.text = subtitle
+ mSubTitleText.setTextColor(colorTheme.subtitleColor)
+ mSubTitleText.alpha = mSubtitleAlpha * colorTheme.contentAlpha
+ mSubTitleText.visibility = VISIBLE
+ }
+ }
+
+ private fun updateLoadingIndicator(
+ connectionState: ConnectionState,
+ colorTheme: ColorTheme,
+ ) {
+ if (connectionState == CONNECTING) {
+ mLoadingIndicator.visibility = VISIBLE
+ mLoadingIndicator.indeterminateDrawable.setTintList(
+ ColorStateList.valueOf(colorTheme.statusIconColor)
+ )
+ } else {
+ mLoadingIndicator.visibility = GONE
+ }
+ }
+
+ private fun initializeSeekbarVolume(currentVolume: Int) {
+ tryResolveVolumeUserRequest(currentVolume)
+ if (!isDragging && hasNoPendingVolumeRequests()) {
+ mSlider.value = currentVolume.toFloat()
+ }
+ }
+
+ private fun tryResolveVolumeUserRequest(currentVolume: Int) {
+ if (currentVolume == mLatestUpdateVolume) {
+ mLatestUpdateVolume = NO_VOLUME_SET
+ }
+ }
+
+ private fun hasNoPendingVolumeRequests(): Boolean {
+ return mLatestUpdateVolume == NO_VOLUME_SET
+ }
+
+ private fun setLatestVolumeRequest(volume: Int) {
+ mLatestUpdateVolume = volume
+ }
+
+ private fun initSeekbar(
+ volumeChangeCallback: (Int) -> Unit,
+ settleCallback: () -> Unit = {},
+ deviceDrawable: Drawable?,
+ isInputDevice: Boolean = false,
+ isVolumeControlAllowed: Boolean,
+ currentVolume: Int,
+ maxVolume: Int,
+ colorTheme: ColorTheme,
+ ) {
+ if (maxVolume == 0) {
+ Log.e(TAG, "Invalid maxVolume value")
+ // Slider doesn't allow valueFrom == valueTo, return to prevent crash.
+ return
+ }
+
+ mSlider.isEnabled = isVolumeControlAllowed
+ mSlider.valueFrom = 0f
+ mSlider.valueTo = maxVolume.toFloat()
+ mSlider.stepSize = 1f
+ mSlider.thumbTintList = ColorStateList.valueOf(colorTheme.sliderActiveColor)
+ mSlider.trackActiveTintList = ColorStateList.valueOf(colorTheme.sliderActiveColor)
+ mSlider.trackInactiveTintList = ColorStateList.valueOf(colorTheme.sliderInactiveColor)
+ mSlider.trackIconActiveColor = ColorStateList.valueOf(colorTheme.sliderActiveIconColor)
+ mSlider.trackIconInactiveColor =
+ ColorStateList.valueOf(colorTheme.sliderInactiveIconColor)
+ val muteDrawable = getMuteDrawable(isInputDevice)
+ updateSliderIconsVisibility(
+ deviceDrawable = deviceDrawable,
+ muteDrawable = muteDrawable,
+ isMuted = currentVolume == 0,
+ )
+ initializeSeekbarVolume(currentVolume)
+
+ mSlider.clearOnChangeListeners() // Prevent adding multiple listeners
+ mSlider.addOnChangeListener { _: Slider, value: Float, fromUser: Boolean ->
+ if (fromUser) {
+ val seekBarVolume = value.toInt()
+ updateSliderIconsVisibility(
+ deviceDrawable = deviceDrawable,
+ muteDrawable = muteDrawable,
+ isMuted = seekBarVolume == 0,
+ )
+ if (seekBarVolume != currentVolume) {
+ setLatestVolumeRequest(seekBarVolume)
+ volumeChangeCallback(seekBarVolume)
+ }
+ }
+ }
+
+ mSlider.clearOnSliderTouchListeners() // Prevent adding multiple listeners
+ mSlider.addOnSliderTouchListener(
+ object : Slider.OnSliderTouchListener {
+ override fun onStartTrackingTouch(slider: Slider) {
+ setIsDragging(true)
+ }
+
+ override fun onStopTrackingTouch(slider: Slider) {
+ setIsDragging(false)
+ settleCallback()
+ }
+ }
+ )
+ }
+
+ private fun getMuteDrawable(isInputDevice: Boolean): Drawable? {
+ return AppCompatResources.getDrawable(
+ mContext,
+ if (isInputDevice) R.drawable.ic_mic_off
+ else R.drawable.media_output_icon_volume_off,
+ )
+ }
+
+ private fun updateSliderIconsVisibility(
+ deviceDrawable: Drawable?,
+ muteDrawable: Drawable?,
+ isMuted: Boolean,
+ ) {
+ mSlider.trackIconInactiveStart = if (isMuted) muteDrawable else null
+ // A workaround for the slider glitch that sometimes shows the active icon in inactive
+ // state.
+ mSlider.trackIconActiveStart = if (isMuted) null else deviceDrawable
+ }
+
+ private fun updateTitleIcon(
+ device: MediaDevice,
+ connectionState: ConnectionState,
+ restrictVolumeAdjustment: Boolean,
+ colorTheme: ColorTheme,
+ ) {
+ if (connectionState == CONNECTED && !restrictVolumeAdjustment) {
+ mTitleIcon.visibility = GONE
+ } else {
+ mTitleIcon.imageTintList = ColorStateList.valueOf(colorTheme.iconColor)
+ val drawable = mController.getDeviceIconDrawable(device)
+ mTitleIcon.setImageDrawable(drawable)
+ mTitleIcon.visibility = VISIBLE
+ mTitleIcon.alpha = colorTheme.contentAlpha
+ }
+ }
+
+ private fun updateDeviceStatusIcon(deviceStatusIcon: Drawable?, colorTheme: ColorTheme) {
+ if (deviceStatusIcon == null) {
+ mStatusIcon.visibility = GONE
+ } else {
+ mStatusIcon.setImageDrawable(deviceStatusIcon)
+ mStatusIcon.alpha = colorTheme.contentAlpha
+ mStatusIcon.imageTintList = ColorStateList.valueOf(colorTheme.statusIconColor)
+ mStatusIcon.visibility = VISIBLE
+ }
+ }
+
+ private fun updateEndArea(
+ device: MediaDevice,
+ connectionState: ConnectionState,
+ groupStatus: GroupStatus?,
+ ongoingSessionStatus: OngoingSessionStatus?,
+ colorTheme: ColorTheme,
+ ) {
+ var showDivider = false
+
+ if (ongoingSessionStatus != null) {
+ showDivider = true
+ mOngoingSessionButton.visibility = VISIBLE
+ updateOngoingSessionButton(device, ongoingSessionStatus.host, colorTheme)
+ }
+
+ if (groupStatus != null && shouldShowGroupCheckbox(groupStatus)) {
+ showDivider = true
+ mGroupButton.visibility = VISIBLE
+ updateGroupButton(device, groupStatus, colorTheme)
+ }
+
+ mDivider.visibility =
+ if (showDivider && connectionState == DISCONNECTED) VISIBLE else GONE
+ mDivider.setBackgroundColor(mController.colorScheme.getOutline())
+ }
+
+ private fun shouldShowGroupCheckbox(groupStatus: GroupStatus): Boolean {
+ val disabled = groupStatus.selected && !groupStatus.deselectable
+ return !disabled
+ }
+
+ private fun updateOngoingSessionButton(
+ device: MediaDevice,
+ isHost: Boolean,
+ colorTheme: ColorTheme,
+ ) {
+ val iconDrawableId =
+ if (isHost) R.drawable.media_output_status_edit_session
+ else R.drawable.ic_sound_bars_anim
+ mOngoingSessionButton.setOnClickListener { v: View? ->
+ mController.tryToLaunchInAppRoutingIntent(device.id, v)
+ }
+ val drawable = AppCompatResources.getDrawable(mContext, iconDrawableId)
+ mOngoingSessionButton.setImageDrawable(drawable)
+ mOngoingSessionButton.imageTintList = ColorStateList.valueOf(colorTheme.iconColor)
+ if (drawable is AnimatedVectorDrawable) {
+ drawable.start()
+ }
+ }
+
+ private fun updateGroupButton(
+ device: MediaDevice,
+ groupStatus: GroupStatus,
+ colorTheme: ColorTheme,
+ ) {
+ mGroupButton.contentDescription =
+ mContext.getString(
+ if (groupStatus.selected) R.string.accessibility_remove_device_from_group
+ else R.string.accessibility_add_device_to_group
+ )
+ mGroupButton.setImageResource(
+ if (groupStatus.selected) R.drawable.ic_check_circle_filled
+ else R.drawable.ic_add_circle_rounded
+ )
+ mGroupButton.setOnClickListener {
+ onGroupActionTriggered(!groupStatus.selected, device)
+ }
+ mGroupButton.imageTintList = ColorStateList.valueOf(colorTheme.iconColor)
+ }
+
+ private fun updateContentClickListener(listener: View.OnClickListener?) {
+ mMainContent.setOnClickListener(listener)
+ if (listener == null) {
+ mMainContent.isClickable = false // clickable is not removed automatically.
+ }
+ }
+
+ override fun disableSeekBar() {
+ mSlider.isEnabled = false
+ }
+ }
+
+ inner class MediaGroupDividerViewHolder(itemView: View, val mContext: Context) :
+ RecyclerView.ViewHolder(itemView) {
+ private val mTopSeparator: View = itemView.requireViewById(R.id.top_separator)
+ private val mTitleText: TextView = itemView.requireViewById(R.id.title)
+ @VisibleForTesting
+ val mExpandButton: ViewGroup = itemView.requireViewById(R.id.expand_button)
+ private val mExpandButtonIcon: ImageView = itemView.requireViewById(R.id.expand_button_icon)
+
+ fun onBind(
+ groupDividerTitle: String?,
+ isExpandableDivider: Boolean,
+ hasTopSeparator: Boolean,
+ ) {
+ mTitleText.text = groupDividerTitle
+ mTitleText.setTextColor(mController.colorScheme.getPrimary())
+ if (hasTopSeparator) {
+ mTopSeparator.visibility = VISIBLE
+ mTopSeparator.setBackgroundColor(mController.colorScheme.getOutlineVariant())
+ } else {
+ mTopSeparator.visibility = GONE
+ }
+ updateExpandButton(isExpandableDivider)
+ }
+
+ private fun updateExpandButton(isExpandableDivider: Boolean) {
+ if (!isExpandableDivider) {
+ mExpandButton.visibility = GONE
+ return
+ }
+ val isCollapsed = mController.isGroupListCollapsed
+ mExpandButtonIcon.setImageDrawable(
+ AppCompatResources.getDrawable(
+ mContext,
+ if (isCollapsed) R.drawable.ic_expand_more_rounded
+ else R.drawable.ic_expand_less_rounded,
+ )
+ )
+ mExpandButtonIcon.contentDescription =
+ mContext.getString(
+ if (isCollapsed) R.string.accessibility_expand_group
+ else R.string.accessibility_collapse_group
+ )
+ mExpandButton.visibility = VISIBLE
+ mExpandButton.setOnClickListener { toggleGroupList() }
+ mExpandButtonIcon.backgroundTintList =
+ ColorStateList.valueOf(mController.colorScheme.getOnSurface())
+ .withAlpha((255 * 0.1).toInt())
+ mExpandButtonIcon.imageTintList =
+ ColorStateList.valueOf(mController.colorScheme.getOnSurface())
+ }
+
+ private fun toggleGroupList() {
+ mController.isGroupListCollapsed = !mController.isGroupListCollapsed
+ updateItems()
+ }
+ }
+
+ private inner class ColorTheme(
+ isConnectedWithFixedVolume: Boolean = false,
+ deviceDisabled: Boolean = false,
+ ) {
+ private val colorScheme: MediaOutputColorScheme = mController.colorScheme
+
+ val titleColor =
+ if (isConnectedWithFixedVolume) {
+ colorScheme.getOnPrimary()
+ } else {
+ colorScheme.getOnSurface()
+ }
+ val subtitleColor =
+ if (isConnectedWithFixedVolume) {
+ colorScheme.getOnPrimary()
+ } else {
+ colorScheme.getOnSurfaceVariant()
+ }
+ val iconColor =
+ if (isConnectedWithFixedVolume) {
+ colorScheme.getOnPrimary()
+ } else {
+ colorScheme.getOnSurface()
+ }
+ val statusIconColor =
+ if (isConnectedWithFixedVolume) {
+ colorScheme.getOnPrimary()
+ } else {
+ colorScheme.getOnSurfaceVariant()
+ }
+ val sliderActiveColor = colorScheme.getPrimary()
+ val sliderActiveIconColor = colorScheme.getOnPrimary()
+ val sliderInactiveColor = colorScheme.getSecondaryContainer()
+ val sliderInactiveIconColor = colorScheme.getOnSurface()
+ val containerRestrictedVolumeBackground = colorScheme.getPrimary()
+ val contentAlpha = if (deviceDisabled) DEVICE_DISABLED_ALPHA else DEVICE_ACTIVE_ALPHA
+ }
+
+ companion object {
+ private const val TAG = "MediaOutputAdapter"
+ private const val DEVICE_DISABLED_ALPHA = 0.5f
+ private const val DEVICE_ACTIVE_ALPHA = 1f
+ private const val NO_VOLUME_SET = -1
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java
index e3990d25f94e..d46cca2736da 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapterBase.java
@@ -19,6 +19,7 @@ package com.android.systemui.media.dialog;
import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_GO_TO_APP;
import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_NONE;
import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER;
+import static com.android.media.flags.Flags.enableOutputSwitcherRedesign;
import android.content.Context;
import android.graphics.drawable.Drawable;
@@ -46,11 +47,11 @@ import java.util.concurrent.CopyOnWriteArrayList;
* manipulate the layout directly.
*/
public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
- record OngoingSessionStatus(boolean host) {}
+ public record OngoingSessionStatus(boolean host) {}
- record GroupStatus(Boolean selected, Boolean deselectable) {}
+ public record GroupStatus(Boolean selected, Boolean deselectable) {}
- enum ConnectionState {
+ public enum ConnectionState {
CONNECTED,
CONNECTING,
DISCONNECTED,
@@ -138,7 +139,7 @@ public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<Recycl
return mMediaItemList.size();
}
- abstract class MediaDeviceViewHolderBase extends RecyclerView.ViewHolder {
+ public abstract class MediaDeviceViewHolderBase extends RecyclerView.ViewHolder {
Context mContext;
@@ -211,7 +212,8 @@ public abstract class MediaOutputAdapterBase extends RecyclerView.Adapter<Recycl
clickListener = v -> cancelMuteAwaitConnection();
} else if (device.getState() == MediaDeviceState.STATE_GROUPING) {
connectionState = ConnectionState.CONNECTING;
- } else if (mShouldGroupSelectedMediaItems && hasMultipleSelectedDevices()
+ } else if (!enableOutputSwitcherRedesign() && mShouldGroupSelectedMediaItems
+ && hasMultipleSelectedDevices()
&& isSelected) {
if (mediaItem.isFirstDeviceInGroup()) {
isDeviceGroup = true;
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index 49d09cf64c8e..7f9370ca671d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -19,16 +19,23 @@ package com.android.systemui.media.dialog;
import static android.view.WindowInsets.Type.navigationBars;
import static android.view.WindowInsets.Type.statusBars;
+import static com.android.media.flags.Flags.enableOutputSwitcherRedesign;
+import static com.android.systemui.FontStyles.GSF_LABEL_LARGE;
+import static com.android.systemui.FontStyles.GSF_TITLE_MEDIUM_EMPHASIZED;
+import static com.android.systemui.FontStyles.GSF_TITLE_SMALL;
+
import android.annotation.NonNull;
import android.app.WallpaperColors;
import android.bluetooth.BluetoothLeBroadcast;
import android.bluetooth.BluetoothLeBroadcastMetadata;
import android.content.Context;
import android.content.SharedPreferences;
+import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.graphics.ColorFilter;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
+import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Bundle;
@@ -49,6 +56,7 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.graphics.drawable.IconCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -57,6 +65,8 @@ import com.android.systemui.broadcast.BroadcastSender;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.phone.SystemUIDialog;
+import com.google.android.material.button.MaterialButton;
+
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -71,7 +81,7 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
private static final int HANDLE_BROADCAST_FAILED_DELAY = 3000;
protected final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
- private final RecyclerView.LayoutManager mLayoutManager;
+ private final LinearLayoutManager mLayoutManager;
final Context mContext;
final MediaSwitchingController mMediaSwitchingController;
@@ -93,8 +103,12 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
private ImageView mBroadcastIcon;
private RecyclerView mDevicesRecyclerView;
private ViewGroup mDeviceListLayout;
+ private ViewGroup mQuickAccessShelf;
+ private MaterialButton mConnectDeviceButton;
private LinearLayout mMediaMetadataSectionLayout;
private Button mDoneButton;
+ private ViewGroup mDialogFooter;
+ private View mFooterSpacer;
private Button mStopButton;
private WallpaperColors mWallpaperColors;
private boolean mShouldLaunchLeBroadcastDialog;
@@ -229,7 +243,11 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
mHeaderTitle = mDialogView.requireViewById(R.id.header_title);
mHeaderSubtitle = mDialogView.requireViewById(R.id.header_subtitle);
mHeaderIcon = mDialogView.requireViewById(R.id.header_icon);
+ mQuickAccessShelf = mDialogView.requireViewById(R.id.quick_access_shelf);
+ mConnectDeviceButton = mDialogView.requireViewById(R.id.connect_device);
mDevicesRecyclerView = mDialogView.requireViewById(R.id.list_result);
+ mDialogFooter = mDialogView.requireViewById(R.id.dialog_footer);
+ mFooterSpacer = mDialogView.requireViewById(R.id.footer_spacer);
mMediaMetadataSectionLayout = mDialogView.requireViewById(R.id.media_metadata_section);
mDeviceListLayout = mDialogView.requireViewById(R.id.device_list);
mDoneButton = mDialogView.requireViewById(R.id.done);
@@ -252,6 +270,49 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
}
mDismissing = false;
+
+ if (enableOutputSwitcherRedesign()) {
+ // Reduce radius of dialog background.
+ mDialogView.setBackground(AppCompatResources.getDrawable(mContext,
+ R.drawable.media_output_dialog_background_reduced_radius));
+ // Set non-transparent footer background to change it color on scroll.
+ mDialogFooter.setBackground(AppCompatResources.getDrawable(mContext,
+ R.drawable.media_output_dialog_footer_background));
+ // Right-align the footer buttons.
+ LinearLayout.LayoutParams layoutParams =
+ (LinearLayout.LayoutParams) mFooterSpacer.getLayoutParams();
+ layoutParams.width = (int) mContext.getResources().getDimension(
+ R.dimen.media_output_dialog_button_gap);
+ mFooterSpacer.setLayoutParams(layoutParams);
+ layoutParams.weight = 0;
+ // Update font family to Google Sans Flex.
+ Typeface buttonTypeface = Typeface.create(GSF_LABEL_LARGE, Typeface.NORMAL);
+ mDoneButton.setTypeface(buttonTypeface);
+ mStopButton.setTypeface(buttonTypeface);
+ mHeaderTitle
+ .setTypeface(Typeface.create(GSF_TITLE_MEDIUM_EMPHASIZED, Typeface.NORMAL));
+ mHeaderSubtitle
+ .setTypeface(Typeface.create(GSF_TITLE_SMALL, Typeface.NORMAL));
+ // Reduce the size of the app icon.
+ float appIconSize = mContext.getResources().getDimension(
+ R.dimen.media_output_dialog_app_icon_size);
+ float appIconBottomMargin = mContext.getResources().getDimension(
+ R.dimen.media_output_dialog_app_icon_bottom_margin);
+ ViewGroup.MarginLayoutParams params =
+ (ViewGroup.MarginLayoutParams) mAppResourceIcon.getLayoutParams();
+ params.bottomMargin = (int) appIconBottomMargin;
+ params.width = (int) appIconSize;
+ params.height = (int) appIconSize;
+ mAppResourceIcon.setLayoutParams(params);
+ // Change footer background color on scroll.
+ mDevicesRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+ super.onScrolled(recyclerView, dx, dy);
+ changeFooterColorForScroll();
+ }
+ });
+ }
}
@Override
@@ -366,6 +427,18 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
}
}
+ if (enableOutputSwitcherRedesign()) {
+ if (mMediaSwitchingController.getConnectNewDeviceItem() != null) {
+ mQuickAccessShelf.setVisibility(View.VISIBLE);
+ mConnectDeviceButton.setVisibility(View.VISIBLE);
+ mConnectDeviceButton.setOnClickListener(
+ mMediaSwitchingController::launchBluetoothPairing);
+ } else {
+ mQuickAccessShelf.setVisibility(View.GONE);
+ mConnectDeviceButton.setVisibility(View.GONE);
+ }
+ }
+
// Show when remote media session is available or
// when the device supports BT LE audio + media is playing
mStopButton.setVisibility(getStopButtonVisibility());
@@ -390,21 +463,48 @@ public abstract class MediaOutputBaseDialog extends SystemUIDialog
}
private void updateButtonBackgroundColorFilter() {
- ColorFilter buttonColorFilter =
- new PorterDuffColorFilter(
- mMediaSwitchingController.getColorSchemeLegacy().getColorButtonBackground(),
- PorterDuff.Mode.SRC_IN);
- mDoneButton.getBackground().setColorFilter(buttonColorFilter);
- mStopButton.getBackground().setColorFilter(buttonColorFilter);
- mDoneButton.setTextColor(
- mMediaSwitchingController.getColorSchemeLegacy().getColorPositiveButtonText());
+ if (enableOutputSwitcherRedesign()) {
+ mDoneButton.getBackground().setTint(
+ mMediaSwitchingController.getColorScheme().getPrimary());
+ mDoneButton.setTextColor(mMediaSwitchingController.getColorScheme().getOnPrimary());
+ mStopButton.getBackground().setTint(
+ mMediaSwitchingController.getColorScheme().getOutlineVariant());
+ mStopButton.setTextColor(mMediaSwitchingController.getColorScheme().getPrimary());
+ mConnectDeviceButton.setTextColor(
+ mMediaSwitchingController.getColorScheme().getOnSurfaceVariant());
+ mConnectDeviceButton.setStrokeColor(ColorStateList.valueOf(
+ mMediaSwitchingController.getColorScheme().getOutlineVariant()));
+ mConnectDeviceButton.setIconTint(ColorStateList.valueOf(
+ mMediaSwitchingController.getColorScheme().getPrimary()));
+ } else {
+ ColorFilter buttonColorFilter = new PorterDuffColorFilter(
+ mMediaSwitchingController.getColorSchemeLegacy().getColorButtonBackground(),
+ PorterDuff.Mode.SRC_IN);
+ mDoneButton.getBackground().setColorFilter(buttonColorFilter);
+ mStopButton.getBackground().setColorFilter(buttonColorFilter);
+ mDoneButton.setTextColor(
+ mMediaSwitchingController.getColorSchemeLegacy().getColorPositiveButtonText());
+ }
}
private void updateDialogBackgroundColor() {
- getDialogView().getBackground().setTint(
- mMediaSwitchingController.getColorSchemeLegacy().getColorDialogBackground());
- mDeviceListLayout.setBackgroundColor(
- mMediaSwitchingController.getColorSchemeLegacy().getColorDialogBackground());
+ int backgroundColor = enableOutputSwitcherRedesign()
+ ? mMediaSwitchingController.getColorScheme().getSurfaceContainer()
+ : mMediaSwitchingController.getColorSchemeLegacy().getColorDialogBackground();
+ getDialogView().getBackground().setTint(backgroundColor);
+ mDeviceListLayout.setBackgroundColor(backgroundColor);
+ }
+
+ private void changeFooterColorForScroll() {
+ int totalItemCount = mLayoutManager.getItemCount();
+ int lastVisibleItemPosition =
+ mLayoutManager.findLastCompletelyVisibleItemPosition();
+ boolean hasBottomScroll =
+ totalItemCount > 0 && lastVisibleItemPosition != totalItemCount - 1;
+ mDialogFooter.getBackground().setTint(
+ hasBottomScroll
+ ? mMediaSwitchingController.getColorScheme().getSurfaceContainerHigh()
+ : mMediaSwitchingController.getColorScheme().getSurfaceContainer());
}
public void handleLeBroadcastStarted() {
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorScheme.kt b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorScheme.kt
new file mode 100644
index 000000000000..21b92cc11406
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputColorScheme.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2025 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.media.dialog
+
+import android.content.Context
+import com.android.systemui.monet.ColorScheme
+import com.android.systemui.res.R
+
+abstract class MediaOutputColorScheme {
+ companion object Factory {
+ @JvmStatic
+ fun fromDynamicColors(dynamicScheme: ColorScheme): MediaOutputColorScheme {
+ return MediaOutputColorSchemeDynamic(dynamicScheme)
+ }
+
+ @JvmStatic
+ fun fromSystemColors(context: Context): MediaOutputColorScheme {
+ return MediaOutputColorSchemeSystem(context)
+ }
+ }
+
+ abstract fun getPrimary(): Int
+
+ abstract fun getOnPrimary(): Int
+
+ abstract fun getSecondary(): Int
+
+ abstract fun getSecondaryContainer(): Int
+
+ abstract fun getSurfaceContainer(): Int
+
+ abstract fun getSurfaceContainerHigh(): Int
+
+ abstract fun getOnSurface(): Int
+
+ abstract fun getOnSurfaceVariant(): Int
+
+ abstract fun getOutline(): Int
+
+ abstract fun getOutlineVariant(): Int
+}
+
+class MediaOutputColorSchemeDynamic(dynamicScheme: ColorScheme) : MediaOutputColorScheme() {
+ private val mMaterialScheme = dynamicScheme.materialScheme
+
+ override fun getPrimary() = mMaterialScheme.primary
+
+ override fun getOnPrimary() = mMaterialScheme.onPrimary
+
+ override fun getSecondary() = mMaterialScheme.secondary
+
+ override fun getSecondaryContainer() = mMaterialScheme.secondaryContainer
+
+ override fun getSurfaceContainer() = mMaterialScheme.surfaceContainer
+
+ override fun getSurfaceContainerHigh() = mMaterialScheme.surfaceContainerHigh
+
+ override fun getOnSurface() = mMaterialScheme.onSurface
+
+ override fun getOnSurfaceVariant() = mMaterialScheme.onSurfaceVariant
+
+ override fun getOutline() = mMaterialScheme.outline
+
+ override fun getOutlineVariant() = mMaterialScheme.outlineVariant
+}
+
+class MediaOutputColorSchemeSystem(private val mContext: Context) : MediaOutputColorScheme() {
+ override fun getPrimary() = mContext.getColor(R.color.media_dialog_primary)
+
+ override fun getOnPrimary() = mContext.getColor(R.color.media_dialog_on_primary)
+
+ override fun getSecondary() = mContext.getColor(R.color.media_dialog_secondary)
+
+ override fun getSecondaryContainer() =
+ mContext.getColor(R.color.media_dialog_secondary_container)
+
+ override fun getSurfaceContainer() = mContext.getColor(R.color.media_dialog_surface_container)
+
+ override fun getSurfaceContainerHigh() =
+ mContext.getColor(R.color.media_dialog_surface_container_high)
+
+ override fun getOnSurface() = mContext.getColor(R.color.media_dialog_on_surface)
+
+ override fun getOnSurfaceVariant() = mContext.getColor(R.color.media_dialog_on_surface_variant)
+
+ override fun getOutline() = mContext.getColor(R.color.media_dialog_outline)
+
+ override fun getOutlineVariant() = mContext.getColor(R.color.media_dialog_outline_variant)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
index 163ff248b9df..225ad724ce71 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputDialog.java
@@ -17,6 +17,7 @@
package com.android.systemui.media.dialog;
import static com.android.settingslib.flags.Flags.legacyLeAudioSharing;
+import static com.android.media.flags.Flags.enableOutputSwitcherRedesign;
import android.content.Context;
import android.os.Bundle;
@@ -57,8 +58,10 @@ public class MediaOutputDialog extends MediaOutputBaseDialog {
super(context, broadcastSender, mediaSwitchingController, includePlaybackAndAppMetadata);
mDialogTransitionAnimator = dialogTransitionAnimator;
mUiEventLogger = uiEventLogger;
- mAdapter = new MediaOutputAdapterLegacy(mMediaSwitchingController, mainExecutor,
- backgroundExecutor);
+ mAdapter = enableOutputSwitcherRedesign()
+ ? new MediaOutputAdapter(mMediaSwitchingController)
+ : new MediaOutputAdapterLegacy(mMediaSwitchingController, mainExecutor,
+ backgroundExecutor);
if (!aboveStatusbar) {
getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
index 4f86257e3870..0b4a9321618d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaSwitchingController.java
@@ -171,7 +171,9 @@ public class MediaSwitchingController
private FeatureFlags mFeatureFlags;
private UserTracker mUserTracker;
private VolumePanelGlobalStateInteractor mVolumePanelGlobalStateInteractor;
+ @NonNull private MediaOutputColorScheme mMediaOutputColorScheme;
@NonNull private MediaOutputColorSchemeLegacy mMediaOutputColorSchemeLegacy;
+ private boolean mIsGroupListCollapsed = true;
public enum BroadcastNotifyDialog {
ACTION_FIRST_LAUNCH,
@@ -229,6 +231,7 @@ public class MediaSwitchingController
mOutputMediaItemListProxy = new OutputMediaItemListProxy(context);
mDialogTransitionAnimator = dialogTransitionAnimator;
mNearbyMediaDevicesManager = nearbyMediaDevicesManager;
+ mMediaOutputColorScheme = MediaOutputColorScheme.fromSystemColors(mContext);
mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromSystemColors(mContext);
if (enableInputRouting()) {
@@ -499,7 +502,7 @@ public class MediaSwitchingController
return getNotificationIcon();
}
- IconCompat getDeviceIconCompat(MediaDevice device) {
+ Drawable getDeviceIconDrawable(MediaDevice device) {
Drawable drawable = device.getIcon();
if (drawable == null) {
if (DEBUG) {
@@ -509,7 +512,19 @@ public class MediaSwitchingController
// Use default Bluetooth device icon to handle getIcon() is null case.
drawable = mContext.getDrawable(com.android.internal.R.drawable.ic_bt_headphones_a2dp);
}
- return BluetoothUtils.createIconWithDrawable(drawable);
+ return drawable;
+ }
+
+ IconCompat getDeviceIconCompat(MediaDevice device) {
+ return BluetoothUtils.createIconWithDrawable(getDeviceIconDrawable(device));
+ }
+
+ public void setGroupListCollapsed(boolean isCollapsed) {
+ mIsGroupListCollapsed = isCollapsed;
+ }
+
+ public boolean isGroupListCollapsed() {
+ return mIsGroupListCollapsed;
}
boolean isActiveItem(MediaDevice device) {
@@ -560,10 +575,16 @@ public class MediaSwitchingController
void updateCurrentColorScheme(WallpaperColors wallpaperColors, boolean isDarkTheme) {
ColorScheme currentColorScheme = new ColorScheme(wallpaperColors,
isDarkTheme);
+ mMediaOutputColorScheme = MediaOutputColorScheme.fromDynamicColors(
+ currentColorScheme);
mMediaOutputColorSchemeLegacy = MediaOutputColorSchemeLegacy.fromDynamicColors(
currentColorScheme, isDarkTheme);
}
+ MediaOutputColorScheme getColorScheme() {
+ return mMediaOutputColorScheme;
+ }
+
MediaOutputColorSchemeLegacy getColorSchemeLegacy() {
return mMediaOutputColorSchemeLegacy;
}
@@ -786,8 +807,14 @@ public class MediaSwitchingController
}
}
+ @NonNull
+ MediaItem getConnectedSpeakersExpandableGroupDivider() {
+ return MediaItem.createExpandableGroupDividerMediaItem(
+ mContext.getString(R.string.media_output_group_title_connected_speakers));
+ }
+
@Nullable
- private MediaItem getConnectNewDeviceItem() {
+ MediaItem getConnectNewDeviceItem() {
boolean isSelectedDeviceNotAGroup = getSelectedMediaDevice().size() == 1;
if (enableInputRouting()) {
// When input routing is enabled, there are expected to be at least 2 total selected
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt
index 9940ae523b60..6d04c27b9e5e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt
@@ -28,7 +28,7 @@ import android.util.Log
import android.util.MathUtils
import android.view.CrossWindowBlurListeners
import android.view.CrossWindowBlurListeners.CROSS_WINDOW_BLUR_SUPPORTED
-import android.view.SurfaceControl
+import android.view.SyncRtSurfaceTransactionApplier
import android.view.ViewRootImpl
import androidx.annotation.VisibleForTesting
import com.android.systemui.Dumpable
@@ -36,26 +36,35 @@ import com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
-import java.io.PrintWriter
-import javax.inject.Inject
import com.android.systemui.keyguard.ui.transitions.BlurConfig
import com.android.systemui.res.R
+import java.io.PrintWriter
+import javax.inject.Inject
@SysUISingleton
-open class BlurUtils @Inject constructor(
+open class BlurUtils
+@Inject
+constructor(
@Main resources: Resources,
blurConfig: BlurConfig,
private val crossWindowBlurListeners: CrossWindowBlurListeners,
- dumpManager: DumpManager
+ dumpManager: DumpManager,
) : Dumpable {
val minBlurRadius = resources.getDimensionPixelSize(R.dimen.min_window_blur_radius).toFloat()
- val maxBlurRadius = if (Flags.notificationShadeBlur()) {
- blurConfig.maxBlurRadiusPx
- } else {
- resources.getDimensionPixelSize(R.dimen.max_window_blur_radius).toFloat()
- }
+ val maxBlurRadius =
+ if (Flags.notificationShadeBlur()) {
+ blurConfig.maxBlurRadiusPx
+ } else {
+ resources.getDimensionPixelSize(R.dimen.max_window_blur_radius).toFloat()
+ }
private var lastAppliedBlur = 0
+ private var lastTargetViewRootImpl: ViewRootImpl? = null
+ private var _transactionApplier = SyncRtSurfaceTransactionApplier(null)
+ @VisibleForTesting
+ open val transactionApplier: SyncRtSurfaceTransactionApplier
+ get() = _transactionApplier
+
private var earlyWakeupEnabled = false
/** When this is true, early wakeup flag is not reset on surface flinger when blur drops to 0 */
@@ -65,9 +74,7 @@ open class BlurUtils @Inject constructor(
dumpManager.registerDumpable(this)
}
- /**
- * Translates a ratio from 0 to 1 to a blur radius in pixels.
- */
+ /** Translates a ratio from 0 to 1 to a blur radius in pixels. */
fun blurRadiusOfRatio(ratio: Float): Float {
if (ratio == 0f) {
return 0f
@@ -75,15 +82,18 @@ open class BlurUtils @Inject constructor(
return MathUtils.lerp(minBlurRadius, maxBlurRadius, ratio)
}
- /**
- * Translates a blur radius in pixels to a ratio between 0 to 1.
- */
+ /** Translates a blur radius in pixels to a ratio between 0 to 1. */
fun ratioOfBlurRadius(blur: Float): Float {
if (blur == 0f) {
return 0f
}
- return MathUtils.map(minBlurRadius, maxBlurRadius,
- 0f /* maxStart */, 1f /* maxStop */, blur)
+ return MathUtils.map(
+ minBlurRadius,
+ maxBlurRadius,
+ 0f /* maxStart */,
+ 1f /* maxStop */,
+ blur,
+ )
}
/**
@@ -91,16 +101,20 @@ open class BlurUtils @Inject constructor(
* early-wakeup flag in SurfaceFlinger.
*/
fun prepareBlur(viewRootImpl: ViewRootImpl?, radius: Int) {
- if (viewRootImpl == null || !viewRootImpl.surfaceControl.isValid ||
- !shouldBlur(radius) || earlyWakeupEnabled
+ if (
+ viewRootImpl == null ||
+ !viewRootImpl.surfaceControl.isValid ||
+ !shouldBlur(radius) ||
+ earlyWakeupEnabled
) {
return
}
+ updateTransactionApplier(viewRootImpl)
+ val builder =
+ SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(viewRootImpl.surfaceControl)
if (lastAppliedBlur == 0 && radius != 0) {
- createTransaction().use {
- earlyWakeupStart(it, "eEarlyWakeup (prepareBlur)")
- it.apply()
- }
+ earlyWakeupStart(builder, "eEarlyWakeup (prepareBlur)")
+ transactionApplier.scheduleApply(builder.build())
}
}
@@ -115,25 +129,32 @@ open class BlurUtils @Inject constructor(
if (viewRootImpl == null || !viewRootImpl.surfaceControl.isValid) {
return
}
- createTransaction().use {
- if (shouldBlur(radius)) {
- it.setBackgroundBlurRadius(viewRootImpl.surfaceControl, radius)
- if (!earlyWakeupEnabled && lastAppliedBlur == 0 && radius != 0) {
- earlyWakeupStart(it, "eEarlyWakeup (applyBlur)")
- }
- if (
- earlyWakeupEnabled &&
- lastAppliedBlur != 0 &&
- radius == 0 &&
- !persistentEarlyWakeupRequired
- ) {
- earlyWakeupEnd(it, "applyBlur")
- }
- lastAppliedBlur = radius
+ updateTransactionApplier(viewRootImpl)
+ val builder =
+ SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(viewRootImpl.surfaceControl)
+ if (shouldBlur(radius)) {
+ builder.withBackgroundBlur(radius)
+ if (!earlyWakeupEnabled && lastAppliedBlur == 0 && radius != 0) {
+ earlyWakeupStart(builder, "eEarlyWakeup (applyBlur)")
+ }
+ if (
+ earlyWakeupEnabled &&
+ lastAppliedBlur != 0 &&
+ radius == 0 &&
+ !persistentEarlyWakeupRequired
+ ) {
+ earlyWakeupEnd(builder, "applyBlur")
}
- it.setOpaque(viewRootImpl.surfaceControl, opaque)
- it.apply()
+ lastAppliedBlur = radius
}
+ builder.withOpaque(opaque)
+ transactionApplier.scheduleApply(builder.build())
+ }
+
+ private fun updateTransactionApplier(viewRootImpl: ViewRootImpl) {
+ if (lastTargetViewRootImpl == viewRootImpl) return
+ _transactionApplier = SyncRtSurfaceTransactionApplier(viewRootImpl.view)
+ lastTargetViewRootImpl = viewRootImpl
}
private fun v(verboseLog: String) {
@@ -141,47 +162,49 @@ open class BlurUtils @Inject constructor(
}
@SuppressLint("MissingPermission")
- private fun earlyWakeupStart(transaction: SurfaceControl.Transaction, traceMethodName: String) {
+ private fun earlyWakeupStart(
+ builder: SyncRtSurfaceTransactionApplier.SurfaceParams.Builder,
+ traceMethodName: String,
+ ) {
v("earlyWakeupStart from $traceMethodName")
Trace.asyncTraceForTrackBegin(TRACE_TAG_APP, TRACK_NAME, traceMethodName, 0)
- transaction.setEarlyWakeupStart()
+ builder.withEarlyWakeupStart()
earlyWakeupEnabled = true
}
@SuppressLint("MissingPermission")
- private fun earlyWakeupEnd(transaction: SurfaceControl.Transaction, loggingContext: String) {
+ private fun earlyWakeupEnd(
+ builder: SyncRtSurfaceTransactionApplier.SurfaceParams.Builder,
+ loggingContext: String,
+ ) {
v("earlyWakeupEnd from $loggingContext")
- transaction.setEarlyWakeupEnd()
+ builder.withEarlyWakeupEnd()
Trace.asyncTraceForTrackEnd(TRACE_TAG_APP, TRACK_NAME, 0)
earlyWakeupEnabled = false
}
- @VisibleForTesting
- open fun createTransaction(): SurfaceControl.Transaction {
- return SurfaceControl.Transaction()
- }
-
private fun shouldBlur(radius: Int): Boolean {
return supportsBlursOnWindows() ||
- ((Flags.notificationShadeBlur() || Flags.bouncerUiRevamp()) &&
- supportsBlursOnWindowsBase() &&
- lastAppliedBlur > 0 &&
- radius == 0)
+ ((Flags.notificationShadeBlur() || Flags.bouncerUiRevamp()) &&
+ supportsBlursOnWindowsBase() &&
+ lastAppliedBlur > 0 &&
+ radius == 0)
}
/**
* If this device can render blurs.
*
- * @see android.view.SurfaceControl.Transaction#setBackgroundBlurRadius(SurfaceControl, int)
* @return {@code true} when supported.
+ * @see android.view.SurfaceControl.Transaction#setBackgroundBlurRadius(SurfaceControl, int)
*/
open fun supportsBlursOnWindows(): Boolean {
return supportsBlursOnWindowsBase() && crossWindowBlurListeners.isCrossWindowBlurEnabled
}
private fun supportsBlursOnWindowsBase(): Boolean {
- return CROSS_WINDOW_BLUR_SUPPORTED && ActivityManager.isHighEndGfx() &&
- !SystemProperties.getBoolean("persist.sysui.disableBlur", false)
+ return CROSS_WINDOW_BLUR_SUPPORTED &&
+ ActivityManager.isHighEndGfx() &&
+ !SystemProperties.getBoolean("persist.sysui.disableBlur", false)
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
@@ -203,12 +226,14 @@ open class BlurUtils @Inject constructor(
fun setPersistentEarlyWakeup(persistentWakeup: Boolean, viewRootImpl: ViewRootImpl?) {
persistentEarlyWakeupRequired = persistentWakeup
if (viewRootImpl == null || !supportsBlursOnWindows()) return
+
+ updateTransactionApplier(viewRootImpl)
+ val builder =
+ SyncRtSurfaceTransactionApplier.SurfaceParams.Builder(viewRootImpl.surfaceControl)
if (persistentEarlyWakeupRequired) {
if (earlyWakeupEnabled) return
- createTransaction().use {
- earlyWakeupStart(it, "setEarlyWakeup")
- it.apply()
- }
+ earlyWakeupStart(builder, "setEarlyWakeup")
+ transactionApplier.scheduleApply(builder.build())
} else {
if (!earlyWakeupEnabled) return
if (lastAppliedBlur > 0) {
@@ -219,10 +244,8 @@ open class BlurUtils @Inject constructor(
" was still active",
)
}
- createTransaction().use {
- earlyWakeupEnd(it, "resetEarlyWakeup")
- it.apply()
- }
+ earlyWakeupEnd(builder, "resetEarlyWakeup")
+ transactionApplier.scheduleApply(builder.build())
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
index 0ea9509f0c13..e3c2fb5b6e59 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt
@@ -25,9 +25,11 @@ import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.common.shared.model.NotificationContainerBounds
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
+import com.android.systemui.communal.shared.model.CommunalScenes
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dump.DumpManager
+import com.android.systemui.kairos.awaitClose
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.Edge
@@ -95,6 +97,7 @@ import com.android.systemui.util.kotlin.BooleanFlowOperators.not
import com.android.systemui.util.kotlin.FlowDumperImpl
import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine
import com.android.systemui.util.kotlin.sample
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -357,14 +360,31 @@ constructor(
)
.dumpValue("isOnLockscreenWithoutShade")
+ private val aboutToTransitionToHub: Flow<Unit> =
+ if (SceneContainerFlag.isEnabled) {
+ emptyFlow()
+ } else {
+ conflatedCallbackFlow {
+ val callback =
+ CommunalSceneInteractor.OnSceneAboutToChangeListener { toScene, _ ->
+ if (toScene == CommunalScenes.Communal) {
+ trySend(Unit)
+ }
+ }
+ communalSceneInteractor.registerSceneStateProcessor(callback)
+ awaitClose { communalSceneInteractor.unregisterSceneStateProcessor(callback) }
+ }
+ }
+
/** If the user is visually on the glanceable hub or transitioning to/from it */
private val isOnGlanceableHub: Flow<Boolean> =
- combine(
- keyguardTransitionInteractor.isFinishedIn(
- content = Scenes.Communal,
- stateWithoutSceneContainer = GLANCEABLE_HUB,
- ),
+ merge(
+ aboutToTransitionToHub.map { true },
anyOf(
+ keyguardTransitionInteractor.isFinishedIn(
+ content = Scenes.Communal,
+ stateWithoutSceneContainer = GLANCEABLE_HUB,
+ ),
keyguardTransitionInteractor.isInTransition(
edge = Edge.create(to = Scenes.Communal),
edgeWithoutSceneContainer = Edge.create(to = GLANCEABLE_HUB),
@@ -374,9 +394,7 @@ constructor(
edgeWithoutSceneContainer = Edge.create(from = GLANCEABLE_HUB),
),
),
- ) { isOnGlanceableHub, transitioningToOrFromHub ->
- isOnGlanceableHub || transitioningToOrFromHub
- }
+ )
.distinctUntilChanged()
.dumpWhileCollecting("isOnGlanceableHub")
@@ -532,6 +550,7 @@ constructor(
emit(1f - qsExpansion)
}
}
+
Split ->
combineTransform(isAnyExpanded, bouncerInteractor.bouncerExpansion) {
isAnyExpanded,
@@ -544,6 +563,7 @@ constructor(
emit(1f)
}
}
+
Dual ->
combineTransform(
shadeModeInteractor.isShadeLayoutWide,
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/AsyncSensorManagerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/AsyncSensorManagerExt.kt
new file mode 100644
index 000000000000..e66ad2c209ae
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/AsyncSensorManagerExt.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2025 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.util.kotlin
+
+import android.hardware.Sensor
+import android.hardware.TriggerEvent
+import android.hardware.TriggerEventListener
+import com.android.systemui.util.sensors.AsyncSensorManager
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Helper for continuously observing a trigger sensor, which automatically unregisters itself after
+ * it executes once. We therefore re-register ourselves after each emission.
+ */
+fun AsyncSensorManager.observeTriggerSensor(sensor: Sensor): Flow<Unit> = conflatedCallbackFlow {
+ val isRegistered = AtomicBoolean(false)
+ fun registerCallbackInternal(callback: TriggerEventListener) {
+ if (isRegistered.compareAndSet(false, true)) {
+ requestTriggerSensor(callback, sensor)
+ }
+ }
+
+ val callback =
+ object : TriggerEventListener() {
+ override fun onTrigger(event: TriggerEvent) {
+ trySend(Unit)
+ if (isRegistered.getAndSet(false)) {
+ registerCallbackInternal(this)
+ }
+ }
+ }
+
+ registerCallbackInternal(callback)
+
+ awaitClose {
+ if (isRegistered.getAndSet(false)) {
+ cancelTriggerSensor(callback, sensor)
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt
index d6b778fe2bc2..0a8572a8a270 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTestKt.kt
@@ -52,8 +52,10 @@ import com.android.systemui.dump.dumpManager
import com.android.systemui.flags.featureFlagsClassic
import com.android.systemui.flags.systemPropertiesHelper
import com.android.systemui.jank.interactionJankMonitor
+import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionBootInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.backgroundScope
import com.android.systemui.kosmos.runTest
@@ -174,6 +176,12 @@ class KeyguardViewMediatorTestKt : SysuiTestCase() {
@Test
fun doKeyguardTimeout_changesCommunalScene() =
kosmos.runTest {
+ // Transition fully to gone
+ fakeKeyguardTransitionRepository.transitionTo(
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.GONE,
+ )
+
// Hub is enabled and hub condition is active.
setCommunalV2Enabled(true)
enableHubOnCharging()
@@ -192,6 +200,11 @@ class KeyguardViewMediatorTestKt : SysuiTestCase() {
@Test
fun doKeyguardTimeout_communalNotAvailable_sleeps() =
kosmos.runTest {
+ fakeKeyguardTransitionRepository.transitionTo(
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.GONE,
+ )
+
// Hub disabled.
setCommunalV2Enabled(false)
@@ -212,6 +225,11 @@ class KeyguardViewMediatorTestKt : SysuiTestCase() {
@Test
fun doKeyguardTimeout_hubConditionNotActive_sleeps() =
kosmos.runTest {
+ fakeKeyguardTransitionRepository.transitionTo(
+ KeyguardState.LOCKSCREEN,
+ KeyguardState.GONE,
+ )
+
// Communal enabled, but hub condition set to never.
setCommunalV2Enabled(true)
disableHubShowingAutomatically()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
index 122af0639030..5713ddc8ae9d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt
@@ -39,7 +39,6 @@ import android.media.session.PlaybackState
import android.net.Uri
import android.os.Bundle
import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import android.service.notification.StatusBarNotification
import android.testing.TestableLooper.RunWithLooper
@@ -2117,7 +2116,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
}
@Test
- @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION)
fun postDuplicateNotification_doesNotCallListeners() {
addNotificationAndLoad()
reset(listener)
@@ -2137,26 +2135,6 @@ class LegacyMediaDataManagerImplTest(flags: FlagsParameterization) : SysuiTestCa
}
@Test
- @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION)
- fun postDuplicateNotification_callsListeners() {
- addNotificationAndLoad()
- reset(listener)
- mediaDataManager.onNotificationAdded(KEY, mediaNotification)
- testScope.assertRunAllReady(foreground = 1, background = 1)
- verify(listener)
- .onMediaDataLoaded(
- eq(KEY),
- eq(KEY),
- capture(mediaDataCaptor),
- eq(true),
- eq(0),
- eq(false),
- )
- verify(kosmos.mediaLogger, never()).logDuplicateMediaNotification(eq(KEY))
- }
-
- @Test
- @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION)
fun postDifferentIntentNotifications_CallsListeners() {
addNotificationAndLoad()
reset(listener)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
index 93c27f01b5ff..52e5e1520ec3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt
@@ -2201,7 +2201,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION)
fun postDuplicateNotification_doesNotCallListeners() {
whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
@@ -2226,31 +2225,6 @@ class MediaDataProcessorTest(flags: FlagsParameterization) : SysuiTestCase() {
}
@Test
- @DisableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION)
- fun postDuplicateNotification_callsListeners() {
- whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
- whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
-
- mediaDataProcessor.addInternalListener(mediaDataFilter)
- mediaDataFilter.mediaDataProcessor = mediaDataProcessor
- addNotificationAndLoad()
- reset(listener)
- mediaDataProcessor.onNotificationAdded(KEY, mediaNotification)
- testScope.assertRunAllReady(foreground = 1, background = 1)
- verify(listener)
- .onMediaDataLoaded(
- eq(KEY),
- eq(KEY),
- capture(mediaDataCaptor),
- eq(true),
- eq(0),
- eq(false),
- )
- verify(kosmos.mediaLogger, never()).logDuplicateMediaNotification(eq(KEY))
- }
-
- @Test
- @EnableFlags(Flags.FLAG_MEDIA_CONTROLS_POSTS_OPTIMIZATION)
fun postDifferentIntentNotifications_CallsListeners() {
whenever(notificationLockscreenUserManager.isCurrentProfile(USER_ID)).thenReturn(true)
whenever(notificationLockscreenUserManager.isProfileAvailable(USER_ID)).thenReturn(true)
diff --git a/packages/SystemUI/tests/utils/src/android/hardware/display/FakeAmbientDisplayConfiguration.kt b/packages/SystemUI/tests/utils/src/android/hardware/display/FakeAmbientDisplayConfiguration.kt
index cdd0ff7c38f7..9d3d7e6ee292 100644
--- a/packages/SystemUI/tests/utils/src/android/hardware/display/FakeAmbientDisplayConfiguration.kt
+++ b/packages/SystemUI/tests/utils/src/android/hardware/display/FakeAmbientDisplayConfiguration.kt
@@ -4,12 +4,13 @@ import android.content.Context
class FakeAmbientDisplayConfiguration(context: Context) : AmbientDisplayConfiguration(context) {
var fakePulseOnNotificationEnabled = true
+ var fakePickupGestureEnabled = true
override fun pulseOnNotificationEnabled(user: Int) = fakePulseOnNotificationEnabled
override fun pulseOnNotificationAvailable() = TODO("Not yet implemented")
- override fun pickupGestureEnabled(user: Int) = TODO("Not yet implemented")
+ override fun pickupGestureEnabled(user: Int) = fakePickupGestureEnabled
override fun dozePickupSensorAvailable() = TODO("Not yet implemented")
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt
index 38e6c8a0cdea..9d3b983b3c86 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalSceneRepository.kt
@@ -32,10 +32,6 @@ class FakeCommunalSceneRepository(
}
}
- override suspend fun showHubFromPowerButton() {
- snapToScene(CommunalScenes.Communal)
- }
-
private val defaultTransitionState = ObservableTransitionState.Idle(CommunalScenes.Default)
private val _transitionState = MutableStateFlow<Flow<ObservableTransitionState>?>(null)
override val transitionState: StateFlow<ObservableTransitionState> =
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt
index 8834af581e73..6b629e4c0472 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneInteractorKosmos.kt
@@ -20,6 +20,7 @@ import com.android.systemui.communal.data.repository.communalSceneRepository
import com.android.systemui.communal.shared.log.communalSceneLogger
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.statusbar.policy.keyguardStateController
@@ -27,6 +28,7 @@ val Kosmos.communalSceneInteractor: CommunalSceneInteractor by
Kosmos.Fixture {
CommunalSceneInteractor(
applicationScope = applicationCoroutineScope,
+ mainImmediateDispatcher = testDispatcher,
repository = communalSceneRepository,
logger = communalSceneLogger,
sceneInteractor = sceneInteractor,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt
index 75c4b6f5366b..e90758715ad8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt
@@ -22,6 +22,7 @@ import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.power.domain.interactor.powerInteractor
val Kosmos.communalSceneTransitionInteractor: CommunalSceneTransitionInteractor by
@@ -35,5 +36,6 @@ val Kosmos.communalSceneTransitionInteractor: CommunalSceneTransitionInteractor
repository = communalSceneTransitionRepository,
keyguardInteractor = keyguardInteractor,
powerInteractor = powerInteractor,
+ mainImmediateDispatcher = testDispatcher,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/WakeGestureMonitorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/WakeGestureMonitorKosmos.kt
new file mode 100644
index 000000000000..7a433281cc7b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/WakeGestureMonitorKosmos.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2025 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.dreams
+
+import android.hardware.display.ambientDisplayConfiguration
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.backgroundCoroutineContext
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+import com.android.systemui.util.sensors.asyncSensorManager
+import com.android.systemui.util.settings.fakeSettings
+
+val Kosmos.wakeGestureMonitor by
+ Kosmos.Fixture {
+ WakeGestureMonitor(
+ ambientDisplayConfiguration = ambientDisplayConfiguration,
+ asyncSensorManager = asyncSensorManager,
+ bgContext = backgroundCoroutineContext,
+ secureSettings = fakeSettings,
+ selectedUserInteractor = selectedUserInteractor,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt
index 1698a5078038..3d684b893e35 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractorKosmos.kt
@@ -17,6 +17,7 @@
package com.android.systemui.keyguard.domain.interactor
import com.android.systemui.communal.domain.interactor.communalSceneInteractor
+import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
@@ -38,5 +39,6 @@ val Kosmos.fromGoneTransitionInteractor by
communalSceneInteractor = communalSceneInteractor,
keyguardOcclusionInteractor = keyguardOcclusionInteractor,
keyguardShowWhileAwakeInteractor = keyguardShowWhileAwakeInteractor,
+ communalSettingsInteractor = communalSettingsInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModelKosmos.kt
new file mode 100644
index 000000000000..0198214de0a9
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToGlanceableHubTransitionViewModelKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2025 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.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+
+var Kosmos.goneToGlanceableHubTransitionViewModel by Fixture {
+ GoneToGlanceableHubTransitionViewModel(animationFlow = keyguardTransitionAnimationFlow)
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index a9aa8cd5a7f9..d0f6ae39cc43 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -71,6 +71,7 @@ val Kosmos.keyguardRootViewModel by Fixture {
goneToDozingTransitionViewModel = goneToDozingTransitionViewModel,
goneToDreamingTransitionViewModel = goneToDreamingTransitionViewModel,
goneToLockscreenTransitionViewModel = goneToLockscreenTransitionViewModel,
+ goneToGlanceableHubTransitionViewModel = goneToGlanceableHubTransitionViewModel,
lockscreenToAodTransitionViewModel = lockscreenToAodTransitionViewModel,
lockscreenToDozingTransitionViewModel = lockscreenToDozingTransitionViewModel,
lockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel,
diff --git a/ravenwood/scripts/ravenwood-stats-collector.sh b/ravenwood/scripts/ravenwood-stats-collector.sh
index c2bf8d82e272..3b323411fd91 100755
--- a/ravenwood/scripts/ravenwood-stats-collector.sh
+++ b/ravenwood/scripts/ravenwood-stats-collector.sh
@@ -114,7 +114,7 @@ collect_apis() {
collect_stats $stats " (import it as 'ravenwood_stats')"
-collect_apis $apis " (import it as 'ravenwood_supported_apis')"
+collect_apis $apis " (import it as 'ravenwood_supported_apis2')"
cp *keep_all.txt $keep_all_dir
echo "Keep all files created at:"
@@ -122,4 +122,4 @@ find $keep_all_dir -type f
cp *dump.txt $dump_dir
echo "Dump files created at:"
-find $dump_dir -type f \ No newline at end of file
+find $dump_dir -type f
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenStats.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenStats.kt
index ea8c25b6833c..4cfc205d5912 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenStats.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/HostStubGenStats.kt
@@ -24,13 +24,9 @@ import org.objectweb.asm.Opcodes
import java.io.PrintWriter
/**
- * TODO This is for the legacy API coverage stats CSV that shows how many APIs are "supported"
- * in each class with some heuristics. We created [ApiDumper] later, which dumpps all methods
- * with the "supported" status. We should update the coverage dashboard to use the [ApiDumper]
- * output and remove this class, once we port all the heuristics to [ApiDumper] as well.
- * (For example, this class ignores non-public and/or abstract methods, but [ApiDumper] shows
- * all of them in the same way. We should probably mark them as "Boring" or maybe "Ignore"
- * for [ApiDumper])
+ * This class is no longer used. It was used for the old ravenwood dashboard. (b/402797626)
+ *
+ * TODO: Delete the class.
*/
open class HostStubGenStats(val classes: ClassNodes) {
data class Stats(
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt
index 112ef01e20cb..741abe3df638 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/asm/AsmUtils.kt
@@ -377,6 +377,10 @@ fun MethodNode.isPublic(): Boolean {
return (this.access and Opcodes.ACC_PUBLIC) != 0
}
+fun MethodNode.isAbstract(): Boolean {
+ return (this.access and Opcodes.ACC_ABSTRACT) != 0
+}
+
fun MethodNode.isNative(): Boolean {
return (this.access and Opcodes.ACC_NATIVE) != 0
}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/dumper/ApiDumper.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/dumper/ApiDumper.kt
index bb8cdccafaa6..6ece17ffa6c2 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/dumper/ApiDumper.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/dumper/ApiDumper.kt
@@ -20,6 +20,8 @@ import com.android.hoststubgen.asm.CTOR_NAME
import com.android.hoststubgen.asm.ClassNodes
import com.android.hoststubgen.asm.getClassNameFromFullClassName
import com.android.hoststubgen.asm.getPackageNameFromFullClassName
+import com.android.hoststubgen.asm.isAbstract
+import com.android.hoststubgen.asm.isPublic
import com.android.hoststubgen.asm.toHumanReadableClassName
import com.android.hoststubgen.csvEscape
import com.android.hoststubgen.filters.FilterPolicy
@@ -27,8 +29,8 @@ import com.android.hoststubgen.filters.FilterPolicyWithReason
import com.android.hoststubgen.filters.OutputFilter
import com.android.hoststubgen.filters.StatsLabel
import com.android.hoststubgen.log
-import org.objectweb.asm.Type
import org.objectweb.asm.tree.ClassNode
+import org.objectweb.asm.tree.MethodNode
import java.io.PrintWriter
/**
@@ -45,19 +47,14 @@ class ApiDumper(
val descriptor: String,
)
- private val javaStandardApiPolicy = FilterPolicy.Keep.withReason(
- "Java standard API",
- StatsLabel.Supported,
- )
-
private val shownMethods = mutableSetOf<MethodKey>()
/**
* Do the dump.
*/
fun dump() {
- pw.printf("PackageName,ClassName,FromSubclass,DeclareClass,MethodName,MethodDesc" +
- ",Supported,Policy,Reason,SupportedLabel\n")
+ pw.printf("PackageName,ClassName,Inherited,DeclareClass,MethodName,MethodDesc" +
+ ",Supported,Policy,Reason,Boring\n")
classes.forEach { classNode ->
shownMethods.clear()
@@ -72,32 +69,21 @@ class ApiDumper(
methodClassName: String,
methodName: String,
methodDesc: String,
- classPolicy: FilterPolicyWithReason,
+ computedMethodLabel: StatsLabel,
methodPolicy: FilterPolicyWithReason,
) {
- if (methodPolicy.statsLabel == StatsLabel.Ignored) {
- return
- }
- // Label hack -- if the method is supported, but the class is boring, then the
- // method is boring too.
- var methodLabel = methodPolicy.statsLabel
- if (methodLabel == StatsLabel.SupportedButBoring
- && classPolicy.statsLabel == StatsLabel.SupportedButBoring) {
- methodLabel = classPolicy.statsLabel
- }
-
pw.printf(
- "%s,%s,%d,%s,%s,%s,%d,%s,%s,%s\n",
+ "%s,%s,%d,%s,%s,%s,%d,%s,%s,%d\n",
csvEscape(classPackage),
csvEscape(className),
if (isSuperClass) { 1 } else { 0 },
csvEscape(methodClassName),
csvEscape(methodName),
- csvEscape(methodDesc),
- methodLabel.statValue,
+ csvEscape(methodName + methodDesc),
+ if (computedMethodLabel.isSupported) { 1 } else { 0 },
methodPolicy.policy,
csvEscape(methodPolicy.reason),
- methodLabel,
+ if (computedMethodLabel == StatsLabel.SupportedButBoring) { 1 } else { 0 },
)
}
@@ -111,6 +97,42 @@ class ApiDumper(
return false
}
+ private fun getClassLabel(cn: ClassNode, classPolicy: FilterPolicyWithReason): StatsLabel {
+ if (!classPolicy.statsLabel.isSupported) {
+ return classPolicy.statsLabel
+ }
+ if (cn.name.endsWith("Proto")
+ || cn.name.endsWith("ProtoEnums")
+ || cn.name.endsWith("LogTags")
+ || cn.name.endsWith("StatsLog")) {
+ return StatsLabel.SupportedButBoring
+ }
+
+ return classPolicy.statsLabel
+ }
+
+ private fun resolveMethodLabel(
+ mn: MethodNode,
+ methodPolicy: FilterPolicyWithReason,
+ classLabel: StatsLabel,
+ ): StatsLabel {
+ // Class label will override the method label
+ if (!classLabel.isSupported) {
+ return classLabel
+ }
+ // If method isn't supported, just use it as-is.
+ if (!methodPolicy.statsLabel.isSupported) {
+ return methodPolicy.statsLabel
+ }
+
+ // Use heuristics to override the label.
+ if (!mn.isPublic() || mn.isAbstract()) {
+ return StatsLabel.SupportedButBoring
+ }
+
+ return methodPolicy.statsLabel
+ }
+
private fun dump(
dumpClass: ClassNode,
methodClass: ClassNode,
@@ -120,9 +142,11 @@ class ApiDumper(
return
}
log.d("Class ${dumpClass.name} -- policy $classPolicy")
+ val classLabel = getClassLabel(dumpClass, classPolicy)
- val pkg = getPackageNameFromFullClassName(dumpClass.name).toHumanReadableClassName()
- val cls = getClassNameFromFullClassName(dumpClass.name).toHumanReadableClassName()
+ val humanReadableClassName = dumpClass.name.toHumanReadableClassName()
+ val pkg = getPackageNameFromFullClassName(humanReadableClassName)
+ val cls = getClassNameFromFullClassName(humanReadableClassName)
val isSuperClass = dumpClass != methodClass
@@ -150,8 +174,12 @@ class ApiDumper(
val renameTo = filter.getRenameTo(methodClass.name, method.name, method.desc)
- dumpMethod(pkg, cls, isSuperClass, methodClass.name.toHumanReadableClassName(),
- renameTo ?: method.name, method.desc, classPolicy, methodPolicy)
+ val methodLabel = resolveMethodLabel(method, methodPolicy, classLabel)
+
+ if (methodLabel != StatsLabel.Ignored) {
+ dumpMethod(pkg, cls, isSuperClass, methodClass.name.toHumanReadableClassName(),
+ renameTo ?: method.name, method.desc, methodLabel, methodPolicy)
+ }
}
// Dump super class methods.
@@ -178,51 +206,6 @@ class ApiDumper(
dump(dumpClass, methodClass)
return
}
-
- // Dump overriding methods from Java standard classes, except for the Object methods,
- // which are obvious.
- if (methodClassName.startsWith("java/") || methodClassName.startsWith("javax/")) {
- if (methodClassName != "java/lang/Object") {
- dumpStandardClass(dumpClass, methodClassName)
- }
- return
- }
log.w("Super class or interface $methodClassName (used by ${dumpClass.name}) not found.")
}
-
- /**
- * Dump methods from Java standard classes.
- */
- private fun dumpStandardClass(
- dumpClass: ClassNode,
- methodClassName: String,
- ) {
- val pkg = getPackageNameFromFullClassName(dumpClass.name).toHumanReadableClassName()
- val cls = getClassNameFromFullClassName(dumpClass.name).toHumanReadableClassName()
-
- val methodClassName = methodClassName.toHumanReadableClassName()
-
- try {
- val clazz = Class.forName(methodClassName)
-
- // Method.getMethods() returns only public methods, but with inherited ones.
- // Method.getDeclaredMethods() returns private methods too, but no inherited methods.
- //
- // Since we're only interested in public ones, just use getMethods().
- clazz.methods.forEach { method ->
- val methodName = method.name
- val methodDesc = Type.getMethodDescriptor(method)
-
- // If we already printed the method from a subclass, don't print it.
- if (shownAlready(methodName, methodDesc)) {
- return@forEach
- }
-
- dumpMethod(pkg, cls, true, methodClassName,
- methodName, methodDesc, javaStandardApiPolicy, javaStandardApiPolicy)
- }
- } catch (e: ClassNotFoundException) {
- log.w("JVM type $methodClassName (used by ${dumpClass.name}) not found.")
- }
- }
}
diff --git a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicyWithReason.kt b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
index e082bbb0a119..f135c60947b3 100644
--- a/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
+++ b/ravenwood/tools/hoststubgen/lib/com/android/hoststubgen/filters/FilterPolicyWithReason.kt
@@ -32,7 +32,15 @@ enum class StatsLabel(val statValue: Int, val label: String) {
SupportedButBoring(1, "Boring"),
/** Entry should be shown as "supported" */
- Supported(2, "Supported"),
+ Supported(2, "Supported");
+
+ val isSupported: Boolean
+ get() {
+ return when (this) {
+ SupportedButBoring, Supported -> true
+ else -> false
+ }
+ }
}
/**
diff --git a/services/core/Android.bp b/services/core/Android.bp
index cf85dd957b3f..8a983f9e071d 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -263,6 +263,7 @@ java_library_static {
"profiling_flags_lib",
"android.adpf.sessionmanager_aidl-java",
"uprobestats_flags_java_lib",
+ "clipboard_flags_lib",
],
javac_shard_size: 50,
javacflags: [
diff --git a/services/core/java/com/android/server/am/BroadcastHistory.java b/services/core/java/com/android/server/am/BroadcastHistory.java
index 99fdf9c8d229..9f7e5cdb7900 100644
--- a/services/core/java/com/android/server/am/BroadcastHistory.java
+++ b/services/core/java/com/android/server/am/BroadcastHistory.java
@@ -21,6 +21,8 @@ import android.annotation.Nullable;
import android.content.Intent;
import android.os.Bundle;
import android.os.Trace;
+import android.util.ArrayMap;
+import android.util.SparseArray;
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
@@ -84,28 +86,54 @@ public class BroadcastHistory {
final long[] mSummaryHistoryDispatchTime;
final long[] mSummaryHistoryFinishTime;
+ /**
+ * Map of uids to number of pending broadcasts it sent.
+ */
+ private final SparseArray<ArrayMap<String, Integer>> mPendingBroadcastCountsPerUid =
+ new SparseArray<>();
+
void onBroadcastFrozenLocked(@NonNull BroadcastRecord r) {
mFrozenBroadcasts.add(r);
- updateTraceCounters();
}
void onBroadcastEnqueuedLocked(@NonNull BroadcastRecord r) {
mFrozenBroadcasts.remove(r);
- mPendingBroadcasts.add(r);
- updateTraceCounters();
+ if (mPendingBroadcasts.add(r)) {
+ updatePendingBroadcastCounterAndLogToTrace(r, /* delta= */ 1);
+ }
}
void onBroadcastFinishedLocked(@NonNull BroadcastRecord r) {
- mPendingBroadcasts.remove(r);
+ if (mPendingBroadcasts.remove(r)) {
+ updatePendingBroadcastCounterAndLogToTrace(r, /* delta= */ -1);
+ }
addBroadcastToHistoryLocked(r);
- updateTraceCounters();
}
- private void updateTraceCounters() {
+ private void updatePendingBroadcastCounterAndLogToTrace(@NonNull BroadcastRecord r,
+ int delta) {
+ ArrayMap<String, Integer> pendingBroadcastCounts =
+ mPendingBroadcastCountsPerUid.get(r.callingUid);
+ if (pendingBroadcastCounts == null) {
+ pendingBroadcastCounts = new ArrayMap<>();
+ mPendingBroadcastCountsPerUid.put(r.callingUid, pendingBroadcastCounts);
+ }
+ final String callerPackage = r.callerPackage == null ? "null" : r.callerPackage;
+ final Integer currentCount = pendingBroadcastCounts.get(callerPackage);
+ final int newCount = (currentCount == null ? 0 : currentCount) + delta;
+ if (newCount == 0) {
+ pendingBroadcastCounts.remove(callerPackage);
+ if (pendingBroadcastCounts.isEmpty()) {
+ mPendingBroadcastCountsPerUid.remove(r.callingUid);
+ }
+ } else {
+ pendingBroadcastCounts.put(callerPackage, newCount);
+ }
+
+ Trace.instantForTrack(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Broadcasts pending per uid",
+ callerPackage + "/" + r.callingUid + ":" + newCount);
Trace.traceCounter(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Broadcasts pending",
mPendingBroadcasts.size());
- Trace.traceCounter(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Broadcasts frozen",
- mFrozenBroadcasts.size());
}
public void addBroadcastToHistoryLocked(@NonNull BroadcastRecord original) {
diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStats.java b/services/core/java/com/android/server/biometrics/AuthenticationStats.java
index 3c1cc006ffda..461ddf86ff71 100644
--- a/services/core/java/com/android/server/biometrics/AuthenticationStats.java
+++ b/services/core/java/com/android/server/biometrics/AuthenticationStats.java
@@ -32,14 +32,20 @@ public class AuthenticationStats {
private int mTotalAttempts;
private int mRejectedAttempts;
private int mEnrollmentNotifications;
+
+ private long mLastEnrollmentTime;
+ private long mLastFrrNotificationTime;
private final int mModality;
public AuthenticationStats(final int userId, int totalAttempts, int rejectedAttempts,
- int enrollmentNotifications, final int modality) {
+ int enrollmentNotifications, long lastEnrollmentTime, long lastFrrNotificationTime,
+ final int modality) {
mUserId = userId;
mTotalAttempts = totalAttempts;
mRejectedAttempts = rejectedAttempts;
mEnrollmentNotifications = enrollmentNotifications;
+ mLastEnrollmentTime = lastEnrollmentTime;
+ mLastFrrNotificationTime = lastFrrNotificationTime;
mModality = modality;
}
@@ -48,6 +54,8 @@ public class AuthenticationStats {
mTotalAttempts = 0;
mRejectedAttempts = 0;
mEnrollmentNotifications = 0;
+ mLastEnrollmentTime = 0;
+ mLastFrrNotificationTime = 0;
mModality = modality;
}
@@ -71,6 +79,14 @@ public class AuthenticationStats {
return mModality;
}
+ public long getLastEnrollmentTime() {
+ return mLastEnrollmentTime;
+ }
+
+ public long getLastFrrNotificationTime() {
+ return mLastFrrNotificationTime;
+ }
+
/** Calculate FRR. */
public float getFrr() {
if (mTotalAttempts > 0) {
@@ -100,6 +116,16 @@ public class AuthenticationStats {
mEnrollmentNotifications++;
}
+ /** Updates last enrollment time */
+ public void updateLastEnrollmentTime(long lastEnrollmentTime) {
+ mLastEnrollmentTime = lastEnrollmentTime;
+ }
+
+ /** Updates frr notification time */
+ public void updateLastFrrNotificationTime(long lastFrrNotificationTime) {
+ mLastFrrNotificationTime = lastFrrNotificationTime;
+ }
+
@Override
public boolean equals(Object obj) {
if (this == obj) {
@@ -118,6 +144,10 @@ public class AuthenticationStats {
== target.getRejectedAttempts()
&& this.getEnrollmentNotifications()
== target.getEnrollmentNotifications()
+ && this.getLastEnrollmentTime()
+ == target.getLastEnrollmentTime()
+ && this.getLastFrrNotificationTime()
+ == target.getLastFrrNotificationTime()
&& this.getModality() == target.getModality();
}
diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStatsBroadcastReceiver.java b/services/core/java/com/android/server/biometrics/AuthenticationStatsBroadcastReceiver.java
index 832d73fd5d2d..54348403914a 100644
--- a/services/core/java/com/android/server/biometrics/AuthenticationStatsBroadcastReceiver.java
+++ b/services/core/java/com/android/server/biometrics/AuthenticationStatsBroadcastReceiver.java
@@ -27,6 +27,7 @@ import android.util.Slog;
import com.android.server.biometrics.sensors.BiometricNotificationImpl;
+import java.time.Clock;
import java.util.function.Consumer;
/**
@@ -62,7 +63,7 @@ public class AuthenticationStatsBroadcastReceiver extends BroadcastReceiver {
mCollectorConsumer.accept(
new AuthenticationStatsCollector(context, mModality,
- new BiometricNotificationImpl()));
+ new BiometricNotificationImpl(), Clock.systemUTC()));
context.unregisterReceiver(this);
}
diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java b/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java
index 526264d67318..b79bab99f681 100644
--- a/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java
+++ b/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java
@@ -23,6 +23,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
+import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.face.FaceManager;
import android.hardware.fingerprint.FingerprintManager;
import android.os.UserHandle;
@@ -32,6 +33,9 @@ import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.biometrics.sensors.BiometricNotification;
+import java.time.Clock;
+import java.time.Duration;
+import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
@@ -50,7 +54,12 @@ public class AuthenticationStatsCollector {
private static final int AUTHENTICATION_UPLOAD_INTERVAL = 50;
// The maximum number of eligible biometric enrollment notification can be sent.
@VisibleForTesting
- static final int MAXIMUM_ENROLLMENT_NOTIFICATIONS = 1;
+ static final int MAXIMUM_ENROLLMENT_NOTIFICATIONS = Flags.frrDialogImprovement() ? 3 : 1;
+ @VisibleForTesting
+ static final Duration FRR_MINIMAL_DURATION = Duration.ofDays(7);
+
+ public static final String ACTION_LAST_ENROLL_TIME_CHANGED = "last_enroll_time_changed";
+ public static final String EXTRA_MODALITY = "modality";
@NonNull private final Context mContext;
@NonNull private final PackageManager mPackageManager;
@@ -64,6 +73,7 @@ public class AuthenticationStatsCollector {
@NonNull private final Map<Integer, AuthenticationStats> mUserAuthenticationStatsMap;
@NonNull private AuthenticationStatsPersister mAuthenticationStatsPersister;
@NonNull private BiometricNotification mBiometricNotification;
+ @NonNull private final Clock mClock;
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
@@ -78,8 +88,24 @@ public class AuthenticationStatsCollector {
}
};
+ private final BroadcastReceiver mEnrollTimeUpdatedReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(@NonNull Context context, @NonNull Intent intent) {
+ int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
+ int modality = intent.getIntExtra(EXTRA_MODALITY,
+ BiometricsProtoEnums.MODALITY_UNKNOWN);
+ if (modality == mModality) {
+ updateAuthenticationStatsMapIfNeeded(userId);
+ AuthenticationStats authenticationStats =
+ mUserAuthenticationStatsMap.get(userId);
+ Slog.d(TAG, "Update enroll time for user: " + userId);
+ authenticationStats.updateLastEnrollmentTime(mClock.millis());
+ }
+ }
+ };
+
public AuthenticationStatsCollector(@NonNull Context context, int modality,
- @NonNull BiometricNotification biometricNotification) {
+ @NonNull BiometricNotification biometricNotification, @NonNull Clock clock) {
mContext = context;
mEnabled = context.getResources().getBoolean(R.bool.config_biometricFrrNotificationEnabled);
mThreshold = context.getResources()
@@ -87,6 +113,7 @@ public class AuthenticationStatsCollector {
mUserAuthenticationStatsMap = new HashMap<>();
mModality = modality;
mBiometricNotification = biometricNotification;
+ mClock = clock;
mPackageManager = context.getPackageManager();
mFaceManager = mContext.getSystemService(FaceManager.class);
@@ -100,6 +127,11 @@ public class AuthenticationStatsCollector {
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_USER_REMOVED);
context.registerReceiver(mBroadcastReceiver, intentFilter);
+
+ IntentFilter enrollTimeChangedFilter = new IntentFilter();
+ enrollTimeChangedFilter.addAction(ACTION_LAST_ENROLL_TIME_CHANGED);
+ context.registerReceiver(mEnrollTimeUpdatedReceiver, enrollTimeChangedFilter,
+ Context.RECEIVER_NOT_EXPORTED);
}
private void initializeUserAuthenticationStatsMap() {
@@ -123,19 +155,9 @@ public class AuthenticationStatsCollector {
return;
}
- // SharedPreference is not ready when starting system server, initialize
- // mUserAuthenticationStatsMap in authentication to ensure SharedPreference
- // is ready for application use.
- if (mUserAuthenticationStatsMap.isEmpty()) {
- initializeUserAuthenticationStatsMap();
- }
- // Check if this is a new user.
- if (!mUserAuthenticationStatsMap.containsKey(userId)) {
- mUserAuthenticationStatsMap.put(userId, new AuthenticationStats(userId, mModality));
- }
+ updateAuthenticationStatsMapIfNeeded(userId);
AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId);
-
if (authenticationStats.getEnrollmentNotifications() >= MAXIMUM_ENROLLMENT_NOTIFICATIONS) {
return;
}
@@ -147,34 +169,91 @@ public class AuthenticationStatsCollector {
persistDataIfNeeded(userId);
}
+ private void updateAuthenticationStatsMapIfNeeded(int userId) {
+ // SharedPreference is not ready when starting system server, initialize
+ // mUserAuthenticationStatsMap in authentication to ensure SharedPreference
+ // is ready for application use.
+ if (mUserAuthenticationStatsMap.isEmpty()) {
+ initializeUserAuthenticationStatsMap();
+ }
+ // Check if this is a new user.
+ if (!mUserAuthenticationStatsMap.containsKey(userId)) {
+ mUserAuthenticationStatsMap.put(userId, new AuthenticationStats(userId, mModality));
+ }
+ }
+
/** Check if a notification should be sent after a calculation cycle. */
private void sendNotificationIfNeeded(int userId) {
AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId);
if (authenticationStats.getTotalAttempts() < MINIMUM_ATTEMPTS) {
return;
}
+
+ boolean showFrr;
+ if (Flags.frrDialogImprovement()) {
+ long lastFrrOrEnrollTime = Math.max(authenticationStats.getLastEnrollmentTime(),
+ authenticationStats.getLastFrrNotificationTime());
+ showFrr = authenticationStats.getEnrollmentNotifications()
+ < MAXIMUM_ENROLLMENT_NOTIFICATIONS
+ && authenticationStats.getFrr() >= mThreshold
+ && isFrrMinimalDurationPassed(lastFrrOrEnrollTime);
+ } else {
+ showFrr = authenticationStats.getEnrollmentNotifications()
+ < MAXIMUM_ENROLLMENT_NOTIFICATIONS
+ && authenticationStats.getFrr() >= mThreshold;
+ }
+
// Don't send notification if FRR below the threshold.
- if (authenticationStats.getEnrollmentNotifications() >= MAXIMUM_ENROLLMENT_NOTIFICATIONS
- || authenticationStats.getFrr() < mThreshold) {
+ if (!showFrr) {
authenticationStats.resetData();
return;
}
-
authenticationStats.resetData();
+ if (Flags.frrDialogImprovement()
+ && mModality == BiometricsProtoEnums.MODALITY_FINGERPRINT) {
+ boolean sent = mBiometricNotification.sendCustomizeFpFrrNotification(mContext);
+ if (sent) {
+ authenticationStats.updateLastFrrNotificationTime(mClock.millis());
+ authenticationStats.updateNotificationCounter();
+ return;
+ }
+ }
+
final boolean hasEnrolledFace = hasEnrolledFace(userId);
final boolean hasEnrolledFingerprint = hasEnrolledFingerprint(userId);
if (hasEnrolledFace && !hasEnrolledFingerprint) {
mBiometricNotification.sendFpEnrollNotification(mContext);
+ authenticationStats.updateLastFrrNotificationTime(mClock.millis());
authenticationStats.updateNotificationCounter();
} else if (!hasEnrolledFace && hasEnrolledFingerprint) {
mBiometricNotification.sendFaceEnrollNotification(mContext);
+ authenticationStats.updateLastFrrNotificationTime(mClock.millis());
authenticationStats.updateNotificationCounter();
}
}
+ private boolean isFrrMinimalDurationPassed(long previousMillis) {
+ Instant previous = Instant.ofEpochMilli(previousMillis);
+ long nowMillis = mClock.millis();
+ Instant now = Instant.ofEpochMilli(nowMillis);
+
+ if (now.isAfter(previous)) {
+ Duration between = Duration.between(/* startInclusive= */ previous,
+ /* endExclusive= */ now);
+ if (between.compareTo(FRR_MINIMAL_DURATION) > 0) {
+ return true;
+ } else {
+ Slog.d(TAG, "isFrrMinimalDurationPassed, duration too short");
+ }
+ } else {
+ Slog.d(TAG, "isFrrMinimalDurationPassed, date not match");
+ }
+ return false;
+ }
+
private void persistDataIfNeeded(int userId) {
AuthenticationStats authenticationStats = mUserAuthenticationStatsMap.get(userId);
if (authenticationStats.getTotalAttempts() % AUTHENTICATION_UPLOAD_INTERVAL == 0) {
@@ -182,6 +261,8 @@ public class AuthenticationStatsCollector {
authenticationStats.getTotalAttempts(),
authenticationStats.getRejectedAttempts(),
authenticationStats.getEnrollmentNotifications(),
+ authenticationStats.getLastEnrollmentTime(),
+ authenticationStats.getLastFrrNotificationTime(),
authenticationStats.getModality());
}
}
diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java b/services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java
index 5625bfd21e76..746d00909900 100644
--- a/services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java
+++ b/services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java
@@ -48,8 +48,13 @@ public class AuthenticationStatsPersister {
private static final String USER_ID = "user_id";
private static final String FACE_ATTEMPTS = "face_attempts";
private static final String FACE_REJECTIONS = "face_rejections";
+ private static final String FACE_LAST_ENROLL_TIME = "face_last_enroll_time";
+ private static final String FACE_LAST_FRR_NOTIFICATION_TIME = "face_last_notification_time";
private static final String FINGERPRINT_ATTEMPTS = "fingerprint_attempts";
private static final String FINGERPRINT_REJECTIONS = "fingerprint_rejections";
+ private static final String FINGERPRINT_LAST_ENROLL_TIME = "fingerprint_last_enroll_time";
+ private static final String FINGERPRINT_LAST_FRR_NOTIFICATION_TIME =
+ "fingerprint_last_notification_time";
private static final String ENROLLMENT_NOTIFICATIONS = "enrollment_notifications";
private static final String KEY = "frr_stats";
private static final String THRESHOLD_KEY = "frr_threshold";
@@ -73,21 +78,10 @@ public class AuthenticationStatsPersister {
try {
JSONObject frrStatsJson = new JSONObject(frrStats);
if (modality == BiometricsProtoEnums.MODALITY_FACE) {
- authenticationStatsList.add(new AuthenticationStats(
- getIntValue(frrStatsJson, USER_ID,
- UserHandle.USER_NULL /* defaultValue */),
- getIntValue(frrStatsJson, FACE_ATTEMPTS),
- getIntValue(frrStatsJson, FACE_REJECTIONS),
- getIntValue(frrStatsJson, ENROLLMENT_NOTIFICATIONS),
- modality));
+ authenticationStatsList.add(getFaceAuthenticationStatsFromJson(frrStatsJson));
} else if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT) {
- authenticationStatsList.add(new AuthenticationStats(
- getIntValue(frrStatsJson, USER_ID,
- UserHandle.USER_NULL /* defaultValue */),
- getIntValue(frrStatsJson, FINGERPRINT_ATTEMPTS),
- getIntValue(frrStatsJson, FINGERPRINT_REJECTIONS),
- getIntValue(frrStatsJson, ENROLLMENT_NOTIFICATIONS),
- modality));
+ authenticationStatsList.add(
+ getFingerprintAuthenticationStatsFromJson(frrStatsJson));
}
} catch (JSONException e) {
Slog.w(TAG, String.format("Unable to resolve authentication stats JSON: %s",
@@ -97,6 +91,33 @@ public class AuthenticationStatsPersister {
return authenticationStatsList;
}
+ @NonNull
+ AuthenticationStats getFaceAuthenticationStatsFromJson(JSONObject json) throws JSONException {
+ return new AuthenticationStats(
+ /* userId */ getIntValue(json, USER_ID, UserHandle.USER_NULL),
+ /* totalAttempts */ getIntValue(json, FACE_ATTEMPTS),
+ /* rejectedAttempts */ getIntValue(json, FACE_REJECTIONS),
+ /* enrollmentNotifications */ getIntValue(json, ENROLLMENT_NOTIFICATIONS),
+ /* lastEnrollmentTime */ getLongValue(json, FACE_LAST_ENROLL_TIME),
+ /* lastFrrNotificationTime */getLongValue(json, FACE_LAST_FRR_NOTIFICATION_TIME),
+ /* modality */ BiometricsProtoEnums.MODALITY_FACE);
+ }
+
+ @NonNull
+ AuthenticationStats getFingerprintAuthenticationStatsFromJson(JSONObject json)
+ throws JSONException {
+ return new AuthenticationStats(
+ /* userId */ getIntValue(json, USER_ID, UserHandle.USER_NULL),
+ /* totalAttempts */ getIntValue(json, FINGERPRINT_ATTEMPTS),
+ /* rejectedAttempts */ getIntValue(json, FINGERPRINT_REJECTIONS),
+ /* enrollmentNotifications */ getIntValue(json, ENROLLMENT_NOTIFICATIONS),
+ /* lastEnrollmentTime */ getLongValue(json,
+ FINGERPRINT_LAST_ENROLL_TIME),
+ /* lastFrrNotificationTime */ getLongValue(json,
+ FINGERPRINT_LAST_FRR_NOTIFICATION_TIME),
+ /* modality */ BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ }
+
/**
* Remove frr data for a specific user.
*/
@@ -124,7 +145,8 @@ public class AuthenticationStatsPersister {
* Persist frr data for a specific user.
*/
public void persistFrrStats(int userId, int totalAttempts, int rejectedAttempts,
- int enrollmentNotifications, int modality) {
+ int enrollmentNotifications, long lastEnrollmentTime, long lastFrrNotificationTime,
+ int modality) {
try {
// Copy into a new HashSet to allow modification.
Set<String> frrStatsSet = new HashSet<>(readFrrStats());
@@ -147,7 +169,8 @@ public class AuthenticationStatsPersister {
frrStatJson = new JSONObject().put(USER_ID, userId);
}
frrStatsSet.add(buildFrrStats(frrStatJson, totalAttempts, rejectedAttempts,
- enrollmentNotifications, modality));
+ enrollmentNotifications, lastEnrollmentTime, lastFrrNotificationTime,
+ modality));
Slog.d(TAG, "frrStatsSet to persist: " + frrStatsSet);
@@ -171,18 +194,24 @@ public class AuthenticationStatsPersister {
// Update frr stats for existing frrStats JSONObject and build the new string.
private String buildFrrStats(JSONObject frrStats, int totalAttempts, int rejectedAttempts,
- int enrollmentNotifications, int modality) throws JSONException {
+ int enrollmentNotifications, long lastEnrollmentTime, long lastFrrNotificationTime,
+ int modality)
+ throws JSONException {
if (modality == BiometricsProtoEnums.MODALITY_FACE) {
return frrStats
.put(FACE_ATTEMPTS, totalAttempts)
.put(FACE_REJECTIONS, rejectedAttempts)
.put(ENROLLMENT_NOTIFICATIONS, enrollmentNotifications)
+ .put(FACE_LAST_ENROLL_TIME, lastEnrollmentTime)
+ .put(FACE_LAST_FRR_NOTIFICATION_TIME, lastFrrNotificationTime)
.toString();
} else if (modality == BiometricsProtoEnums.MODALITY_FINGERPRINT) {
return frrStats
.put(FINGERPRINT_ATTEMPTS, totalAttempts)
.put(FINGERPRINT_REJECTIONS, rejectedAttempts)
.put(ENROLLMENT_NOTIFICATIONS, enrollmentNotifications)
+ .put(FINGERPRINT_LAST_ENROLL_TIME, lastEnrollmentTime)
+ .put(FINGERPRINT_LAST_FRR_NOTIFICATION_TIME, lastFrrNotificationTime)
.toString();
} else {
return frrStats.toString();
@@ -201,4 +230,13 @@ public class AuthenticationStatsPersister {
throws JSONException {
return jsonObject.has(key) ? jsonObject.getInt(key) : defaultValue;
}
+
+ private long getLongValue(JSONObject jsonObject, String key) throws JSONException {
+ return getLongValue(jsonObject, key, 0 /* defaultValue */);
+ }
+
+ private long getLongValue(JSONObject jsonObject, String key, long defaultValue)
+ throws JSONException {
+ return jsonObject.has(key) ? jsonObject.getLong(key) : defaultValue;
+ }
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricNotification.java b/services/core/java/com/android/server/biometrics/sensors/BiometricNotification.java
index 90e18604d945..9fb25f429020 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricNotification.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricNotification.java
@@ -33,4 +33,9 @@ public interface BiometricNotification {
* Sends a fingerprint enrollment notification.
*/
void sendFpEnrollNotification(@NonNull Context context);
+
+ /**
+ * Sends a customized fingerprint frr notification.
+ */
+ boolean sendCustomizeFpFrrNotification(@NonNull Context context);
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationImpl.java b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationImpl.java
index 7b420468f628..3ab157082c0b 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationImpl.java
@@ -35,4 +35,9 @@ public class BiometricNotificationImpl implements BiometricNotification {
public void sendFpEnrollNotification(@NonNull Context context) {
BiometricNotificationUtils.showFingerprintEnrollNotification(context);
}
+
+ @Override
+ public boolean sendCustomizeFpFrrNotification(@NonNull Context context) {
+ return BiometricNotificationUtils.showCustomizeFpFrrNotification(context);
+ }
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
index 27f9cc88e28f..3bad3c2a3f8f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
@@ -17,12 +17,16 @@
package com.android.server.biometrics.sensors;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.face.FaceEnrollOptions;
@@ -30,6 +34,7 @@ import android.hardware.fingerprint.FingerprintEnrollOptions;
import android.os.SystemClock;
import android.os.UserHandle;
import android.text.BidiFormatter;
+import android.text.TextUtils;
import android.util.Slog;
import com.android.internal.R;
@@ -56,6 +61,7 @@ public class BiometricNotificationUtils {
private static final String FACE_ENROLL_CHANNEL = "FaceEnrollNotificationChannel";
private static final String FACE_RE_ENROLL_CHANNEL = "FaceReEnrollNotificationChannel";
private static final String FINGERPRINT_ENROLL_CHANNEL = "FingerprintEnrollNotificationChannel";
+ private static final String FINGERPRINT_FRR_CHANNEL = "FingerprintFrrNotificationChannel";
private static final String FINGERPRINT_RE_ENROLL_CHANNEL =
"FingerprintReEnrollNotificationChannel";
private static final String FINGERPRINT_BAD_CALIBRATION_CHANNEL =
@@ -69,6 +75,7 @@ public class BiometricNotificationUtils {
public static final int NOTIFICATION_ID = 1;
public static final String FACE_ENROLL_NOTIFICATION_TAG = "FaceEnroll";
public static final String FINGERPRINT_ENROLL_NOTIFICATION_TAG = "FingerprintEnroll";
+ public static final String FINGERPRINT_FRR_NOTIFICATION_TAG = "FingerprintFrr";
/**
* Shows a face re-enrollment notification.
*/
@@ -151,6 +158,65 @@ public class BiometricNotificationUtils {
}
/**
+ * Shows a customized fingerprint frr notification.
+ *
+ * @return true if notification shows
+ */
+ public static boolean showCustomizeFpFrrNotification(@NonNull Context context) {
+ final String name =
+ context.getString(R.string.device_unlock_notification_name);
+ final String title =
+ context.getString(R.string.fingerprint_frr_notification_title);
+ final String content =
+ context.getString(R.string.fingerprint_frr_notification_msg);
+
+ Intent intent = getIntentFromFpFrrComponentNameStringRes(context);
+ Slog.d(TAG, "Showing Customize Fingerprint Frr Notification result:" + (intent != null));
+ if (intent == null) {
+ return false;
+ }
+
+ final PendingIntent pendingIntent = PendingIntent.getActivityAsUser(context,
+ 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE /* flags */,
+ null /* options */, UserHandle.CURRENT);
+
+ showNotificationHelper(context, name, title, content, pendingIntent,
+ Notification.CATEGORY_RECOMMENDATION, FINGERPRINT_FRR_CHANNEL,
+ FINGERPRINT_FRR_NOTIFICATION_TAG, Notification.VISIBILITY_PUBLIC, true);
+
+ return true;
+ }
+
+ @Nullable
+ private static Intent getIntentFromFpFrrComponentNameStringRes(@NonNull Context context) {
+ String componentNameString = context.getResources().getString(
+ R.string.config_fingerprintFrrTargetComponent);
+ if (TextUtils.isEmpty(componentNameString)) {
+ return null;
+ }
+
+ ComponentName componentName = ComponentName.unflattenFromString(componentNameString);
+ if (componentName == null) {
+ return null;
+ }
+
+ PackageManager packageManager = context.getPackageManager();
+ Intent intent = new Intent();
+ intent.setComponent(componentName);
+ intent.setAction(Intent.ACTION_MAIN);
+ intent.addCategory(Intent.CATEGORY_LAUNCHER);
+
+ ResolveInfo resolveInfo = packageManager.resolveActivity(intent,
+ PackageManager.MATCH_DEFAULT_ONLY);
+ if (resolveInfo != null) {
+ return intent;
+ } else {
+ Slog.d(TAG, "Component for " + componentNameString + " not found");
+ return null;
+ }
+ }
+
+ /**
* Shows a fingerprint notification for loss of enrollment
*/
public static void showFingerprintLoeNotification(@NonNull Context context) {
diff --git a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
index 38bf99932838..1632e0d7ca6f 100644
--- a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java
@@ -18,6 +18,7 @@ package com.android.server.biometrics.sensors;
import android.annotation.NonNull;
import android.content.Context;
+import android.content.Intent;
import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.BiometricRequestConstants;
import android.hardware.face.FaceEnrollOptions;
@@ -26,6 +27,7 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.util.Slog;
+import com.android.server.biometrics.AuthenticationStatsCollector;
import com.android.server.biometrics.BiometricsProto;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
@@ -159,4 +161,13 @@ public abstract class EnrollClient<T> extends AcquisitionClient<T> implements En
default -> BiometricRequestConstants.REASON_UNKNOWN;
};
}
+
+ protected void notifyLastEnrollmentTime(int modality) {
+ // Notify the last enrollment time to re-count authentication stats for frr.
+ final Intent intent = new Intent(
+ AuthenticationStatsCollector.ACTION_LAST_ENROLL_TIME_CHANGED);
+ intent.putExtra(Intent.EXTRA_USER_HANDLE, getTargetUserId());
+ intent.putExtra(AuthenticationStatsCollector.EXTRA_MODALITY, modality);
+ getContext().sendBroadcast(intent);
+ }
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
index d4ec573e1667..e7b2d41024a4 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java
@@ -24,9 +24,11 @@ import static android.hardware.face.FaceManager.getErrorString;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
+import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricFaceConstants;
import android.hardware.biometrics.BiometricSourceType;
+import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.biometrics.common.ICancellationSignal;
import android.hardware.biometrics.events.AuthenticationErrorInfo;
import android.hardware.biometrics.events.AuthenticationHelpInfo;
@@ -171,6 +173,14 @@ public class FaceEnrollClient extends EnrollClient<AidlSession> {
onAcquiredInternal(acquireInfo, vendorCode, shouldSend);
}
+ @Override
+ public void onEnrollResult(BiometricAuthenticator.Identifier identifier, int remaining) {
+ super.onEnrollResult(identifier, remaining);
+ if (remaining == 0) {
+ notifyLastEnrollmentTime(BiometricsProtoEnums.MODALITY_FACE);
+ }
+ }
+
/**
* Called each time a new frame is received during face enrollment.
*
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 993a68fd6ff8..776435d5abc8 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
@@ -30,6 +30,7 @@ import android.hardware.biometrics.BiometricFingerprintConstants;
import android.hardware.biometrics.BiometricFingerprintConstants.FingerprintAcquired;
import android.hardware.biometrics.BiometricSourceType;
import android.hardware.biometrics.BiometricStateListener;
+import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.biometrics.common.ICancellationSignal;
import android.hardware.biometrics.events.AuthenticationAcquiredInfo;
import android.hardware.biometrics.events.AuthenticationErrorInfo;
@@ -156,8 +157,8 @@ public class FingerprintEnrollClient extends EnrollClient<AidlSession> implement
BiometricSourceType.FINGERPRINT,
getRequestReasonFromFingerprintEnrollReason(mEnrollReason)).build()
);
+ notifyLastEnrollmentTime(BiometricsProtoEnums.MODALITY_FINGERPRINT);
}
-
}
@Override
diff --git a/services/core/java/com/android/server/clipboard/Android.bp b/services/core/java/com/android/server/clipboard/Android.bp
new file mode 100644
index 000000000000..6905fc157a9a
--- /dev/null
+++ b/services/core/java/com/android/server/clipboard/Android.bp
@@ -0,0 +1,18 @@
+aconfig_declarations {
+ name: "clipboard_flags",
+ package: "com.android.server.clipboard",
+ container: "system",
+ srcs: ["*.aconfig"],
+}
+
+java_aconfig_library {
+ name: "clipboard_flags_lib",
+ aconfig_declarations: "clipboard_flags",
+}
+
+java_aconfig_library {
+ name: "clipboard_flags_host_lib",
+ host_supported: true,
+ libs: ["fake_device_config"],
+ aconfig_declarations: "clipboard_flags",
+}
diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java
index 6122fdaafe77..40136c3e03ec 100644
--- a/services/core/java/com/android/server/clipboard/ClipboardService.java
+++ b/services/core/java/com/android/server/clipboard/ClipboardService.java
@@ -19,9 +19,27 @@ package com.android.server.clipboard;
import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY;
import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD;
+import static android.content.ClipDescription.MIMETYPE_UNKNOWN;
+import static android.content.ClipDescription.MIMETYPE_TEXT_PLAIN;
+import static android.content.ClipDescription.MIMETYPE_TEXT_HTML;
+import static android.content.ClipDescription.MIMETYPE_TEXT_URILIST;
+import static android.content.ClipDescription.MIMETYPE_TEXT_INTENT;
+import static android.content.ClipDescription.MIMETYPE_APPLICATION_ACTIVITY;
+import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT;
+import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK;
import static android.content.Context.DEVICE_ID_DEFAULT;
import static android.content.Context.DEVICE_ID_INVALID;
+import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_UNKNOWN;
+import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_PLAIN;
+import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_HTML;
+import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_URILIST;
+import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_INTENT;
+import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_APPLICATION_ACTIVITY;
+import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_APPLICATION_SHORTCUT;
+import static com.android.internal.util.FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_APPLICATION_TASK;
+import static com.android.server.clipboard.Flags.clipboardGetEventLogging;
+
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -70,6 +88,7 @@ import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
+import android.util.IntArray;
import android.util.Pair;
import android.util.SafetyProtectionUtils;
import android.util.Slog;
@@ -98,6 +117,7 @@ import com.android.server.wm.WindowManagerInternal;
import java.util.HashSet;
import java.util.List;
+import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
@@ -132,6 +152,10 @@ public class ClipboardService extends SystemService {
private static final String PROPERTY_MAX_CLASSIFICATION_LENGTH = "max_classification_length";
private static final int DEFAULT_MAX_CLASSIFICATION_LENGTH = 400;
+ private static final int[] CLIP_DATA_TYPES_UNKNOWN = {
+ CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_UNKNOWN
+ };
+
private final ActivityManagerInternal mAmInternal;
private final IUriGrantsManager mUgm;
private final UriGrantsManagerInternal mUgmInternal;
@@ -657,6 +681,8 @@ public class ClipboardService extends SystemService {
pkg, intendingUid, intendingUserId, clipboard, deviceId);
notifyTextClassifierLocked(clipboard, pkg, intendingUid);
if (clipboard.primaryClip != null) {
+ scheduleWriteClipDataStats(clipboard.primaryClip,
+ clipboard.primaryClipUid, intendingUid);
scheduleAutoClear(userId, intendingUid, intendingDeviceId);
}
return clipboard.primaryClip;
@@ -1600,4 +1626,65 @@ public class ClipboardService extends SystemService {
Context context = getContext().createContextAsUser(UserHandle.of(userId), /* flags= */ 0);
return context.getSystemService(TextClassificationManager.class);
}
+
+ private static int mimeTypeToClipDataType(@NonNull String mimeType) {
+ switch (mimeType) {
+ case MIMETYPE_TEXT_PLAIN:
+ return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_PLAIN;
+ case MIMETYPE_TEXT_HTML:
+ return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_HTML;
+ case MIMETYPE_TEXT_URILIST:
+ return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_URILIST;
+ case MIMETYPE_TEXT_INTENT:
+ return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_TEXT_INTENT;
+ case MIMETYPE_APPLICATION_ACTIVITY:
+ return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_APPLICATION_ACTIVITY;
+ case MIMETYPE_APPLICATION_SHORTCUT:
+ return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_APPLICATION_SHORTCUT;
+ case MIMETYPE_APPLICATION_TASK:
+ return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_APPLICATION_TASK;
+ case MIMETYPE_UNKNOWN:
+ // fallthrough
+ default:
+ return CLIPBOARD_GET_EVENT_REPORTED__CLIP_DATA_TYPE__MIMETYPE_UNKNOWN;
+ }
+ }
+
+ private void scheduleWriteClipDataStats(@NonNull ClipData clipData,
+ int sourceUid, int intendingUid) {
+ if (!clipboardGetEventLogging()) {
+ return;
+ }
+ final ClipDescription description = clipData.getDescription();
+ if (description != null) {
+ final IntArray mimeTypes = new IntArray();
+ final int secondsSinceSet = (int) TimeUnit.MILLISECONDS.toSeconds(
+ System.currentTimeMillis() - description.getTimestamp());
+ for (int i = description.getMimeTypeCount() - 1; i >= 0; i--) {
+ final String mimeType = description.getMimeType(i);
+ if (mimeType != null) {
+ final int clipDataType = mimeTypeToClipDataType(mimeType);
+ if (!mimeTypes.contains(clipDataType)) {
+ mimeTypes.add(clipDataType);
+ }
+ }
+ }
+ // The getUidProcessState() will hit AMS lock which might be slow, while getting the
+ // clip data might be on the critical UI path. So post to the work thread.
+ // There could be race conditions where the UID state might have been changed
+ // between now and the work thread execution time, but this should be acceptable.
+ mWorkerHandler.post(() -> FrameworkStatsLog.write(
+ FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED,
+ sourceUid, intendingUid,
+ mAmInternal.getUidProcessState(intendingUid),
+ mimeTypes.toArray(),
+ secondsSinceSet));
+ } else {
+ mWorkerHandler.post(() -> FrameworkStatsLog.write(
+ FrameworkStatsLog.CLIPBOARD_GET_EVENT_REPORTED,
+ sourceUid, intendingUid,
+ mAmInternal.getUidProcessState(intendingUid),
+ CLIP_DATA_TYPES_UNKNOWN, 0));
+ }
+ }
}
diff --git a/services/core/java/com/android/server/clipboard/flags.aconfig b/services/core/java/com/android/server/clipboard/flags.aconfig
new file mode 100644
index 000000000000..964242d794a4
--- /dev/null
+++ b/services/core/java/com/android/server/clipboard/flags.aconfig
@@ -0,0 +1,9 @@
+package: "com.android.server.clipboard"
+container: "system"
+
+flag {
+ name: "clipboard_get_event_logging"
+ namespace: "backstage_power"
+ description: "Log the clipboard retrieval event in statsd"
+ bug: "402542624"
+}
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java
index 2152f76395a9..a3d9c66c2668 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityService.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java
@@ -42,9 +42,11 @@ import android.hardware.tv.mediaquality.SoundParameter;
import android.hardware.tv.mediaquality.SoundParameters;
import android.hardware.tv.mediaquality.StreamStatus;
import android.hardware.tv.mediaquality.VendorParamCapability;
+import android.media.quality.ActiveProcessingPicture;
import android.media.quality.AmbientBacklightEvent;
import android.media.quality.AmbientBacklightMetadata;
import android.media.quality.AmbientBacklightSettings;
+import android.media.quality.IActiveProcessingPictureListener;
import android.media.quality.IAmbientBacklightCallback;
import android.media.quality.IMediaQualityManager;
import android.media.quality.IPictureProfileCallback;
@@ -72,6 +74,8 @@ import android.util.Log;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
+import android.view.SurfaceControlActivePicture;
+import android.view.SurfaceControlActivePictureListener;
import com.android.internal.annotations.GuardedBy;
import com.android.server.SystemService;
@@ -105,6 +109,7 @@ public class MediaQualityService extends SystemService {
private final MediaQualityDbHelper mMediaQualityDbHelper;
private final BiMap<Long, String> mPictureProfileTempIdMap;
private final BiMap<Long, String> mSoundProfileTempIdMap;
+ private final Map<String, Long> mPackageDefaultPictureProfileHandleMap = new HashMap<>();
private IMediaQuality mMediaQuality;
private PictureProfileAdjustmentListenerImpl mPictureProfileAdjListener;
private SoundProfileAdjustmentListenerImpl mSoundProfileAdjListener;
@@ -120,6 +125,7 @@ public class MediaQualityService extends SystemService {
private MqManagerNotifier mMqManagerNotifier;
private MqDatabaseUtils mMqDatabaseUtils;
private Handler mHandler;
+ private SurfaceControlActivePictureListener mSurfaceControlActivePictureListener;
// A global lock for picture profile objects.
private final Object mPictureProfileLock = new Object();
@@ -164,6 +170,7 @@ public class MediaQualityService extends SystemService {
soundProfilePrefs, Context.MODE_PRIVATE);
}
+ @GuardedBy("mPictureProfileLock")
@Override
public void onStart() {
IBinder binder = ServiceManager.getService(IMediaQuality.DESCRIPTOR + "/default");
@@ -173,6 +180,14 @@ public class MediaQualityService extends SystemService {
}
Slogf.d(TAG, "Binder is not null");
+ mSurfaceControlActivePictureListener = new SurfaceControlActivePictureListener() {
+ @Override
+ public void onActivePicturesChanged(SurfaceControlActivePicture[] activePictures) {
+ handleOnActivePicturesChanged(activePictures);
+ }
+ };
+ mSurfaceControlActivePictureListener.startListening(); // TODO: stop listening
+
mMediaQuality = IMediaQuality.Stub.asInterface(binder);
if (mMediaQuality != null) {
try {
@@ -184,6 +199,22 @@ public class MediaQualityService extends SystemService {
mMediaQuality.setPictureProfileAdjustmentListener(mPictureProfileAdjListener);
mMediaQuality.setSoundProfileAdjustmentListener(mSoundProfileAdjListener);
+ synchronized (mPictureProfileLock) {
+ String selection = BaseParameters.PARAMETER_TYPE + " = ? AND "
+ + BaseParameters.PARAMETER_NAME + " = ?";
+ String[] selectionArguments = {
+ Integer.toString(PictureProfile.TYPE_SYSTEM),
+ PictureProfile.NAME_DEFAULT
+ };
+ List<PictureProfile> packageDefaultPictureProfiles =
+ mMqDatabaseUtils.getPictureProfilesBasedOnConditions(MediaQualityUtils
+ .getMediaProfileColumns(false), selection, selectionArguments);
+ mPackageDefaultPictureProfileHandleMap.clear();
+ for (PictureProfile profile : packageDefaultPictureProfiles) {
+ mPackageDefaultPictureProfileHandleMap.put(
+ profile.getPackageName(), profile.getHandle().getId());
+ }
+ }
} catch (RemoteException e) {
Slog.e(TAG, "Failed to set ambient backlight detector callback", e);
}
@@ -192,6 +223,54 @@ public class MediaQualityService extends SystemService {
publishBinderService(Context.MEDIA_QUALITY_SERVICE, new BinderService());
}
+ private void handleOnActivePicturesChanged(SurfaceControlActivePicture[] scActivePictures) {
+ if (DEBUG) {
+ Slog.d(TAG, "handleOnActivePicturesChanged");
+ }
+ synchronized (mPictureProfileLock) {
+ // TODO handle other users
+ UserState userState = getOrCreateUserState(UserHandle.USER_SYSTEM);
+ int n = userState.mActiveProcessingPictureCallbackList.beginBroadcast();
+ for (int i = 0; i < n; ++i) {
+ try {
+ IActiveProcessingPictureListener l = userState
+ .mActiveProcessingPictureCallbackList
+ .getBroadcastItem(i);
+ ActiveProcessingPictureListenerInfo info =
+ userState.mActiveProcessingPictureListenerMap.get(l);
+ if (info == null) {
+ continue;
+ }
+ int uid = info.mUid;
+ boolean hasGlobalPermission = mContext.checkPermission(
+ android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE,
+ info.mPid, uid)
+ == PackageManager.PERMISSION_GRANTED;
+ List<ActiveProcessingPicture> aps = new ArrayList<>();
+ for (SurfaceControlActivePicture scap : scActivePictures) {
+ if (!hasGlobalPermission && scap.getOwnerUid() != uid) {
+ // should not receive the event
+ continue;
+ }
+ String profileId = mPictureProfileTempIdMap.getValue(
+ scap.getPictureProfileHandle().getId());
+ if (profileId == null) {
+ continue;
+ }
+ aps.add(new ActiveProcessingPicture(
+ scap.getLayerId(), profileId, scap.getOwnerUid() != uid));
+
+ }
+
+ l.onActiveProcessingPicturesChanged(aps);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "failed to report added AD service to callback", e);
+ }
+ }
+ userState.mActiveProcessingPictureCallbackList.finishBroadcast();
+ }
+ }
+
private final class BinderService extends IMediaQualityManager.Stub {
@GuardedBy("mPictureProfileLock")
@@ -226,6 +305,10 @@ public class MediaQualityService extends SystemService {
pp.setProfileId(value);
mMqManagerNotifier.notifyOnPictureProfileAdded(value, pp,
Binder.getCallingUid(), Binder.getCallingPid());
+ if (isPackageDefaultPictureProfile(pp)) {
+ mPackageDefaultPictureProfileHandleMap.put(
+ pp.getPackageName(), pp.getHandle().getId());
+ }
}
}
);
@@ -250,6 +333,10 @@ public class MediaQualityService extends SystemService {
pp.getParameters());
updateDatabaseOnPictureProfileAndNotifyManagerAndHal(values,
pp.getParameters());
+ if (isPackageDefaultPictureProfile(pp)) {
+ mPackageDefaultPictureProfileHandleMap.put(
+ pp.getPackageName(), pp.getHandle().getId());
+ }
}
});
}
@@ -294,6 +381,11 @@ public class MediaQualityService extends SystemService {
mHalNotifier.notifyHalOnPictureProfileChange(dbId, null);
}
}
+
+ if (isPackageDefaultPictureProfile(toDelete)) {
+ mPackageDefaultPictureProfileHandleMap.remove(
+ toDelete.getPackageName());
+ }
}
});
}
@@ -388,6 +480,7 @@ public class MediaQualityService extends SystemService {
if (mMediaQuality != null) {
PictureParameters pp = new PictureParameters();
// put ID in params for profile update in HAL
+ // TODO: update HAL API for this case
params.putLong(BaseParameters.PARAMETER_ID, longId);
PictureParameter[] pictureParameters = MediaQualityUtils
.convertPersistableBundleToPictureParameterList(params);
@@ -443,6 +536,71 @@ public class MediaQualityService extends SystemService {
return toReturn;
}
+ @GuardedBy("mPictureProfileLock")
+ @Override
+ public long getPictureProfileHandleValue(String id, int userId) {
+ synchronized (mPictureProfileLock) {
+ Long value = mPictureProfileTempIdMap.getKey(id);
+ return value != null ? value : -1;
+ }
+ }
+
+ @GuardedBy("mPictureProfileLock")
+ @Override
+ public long getDefaultPictureProfileHandleValue(int userId) {
+ synchronized (mPictureProfileLock) {
+ String packageName = getPackageOfCallingUid();
+ Long value = null;
+ if (packageName != null) {
+ value = mPackageDefaultPictureProfileHandleMap.get(packageName);
+ }
+ return value != null ? value : -1;
+ }
+ }
+
+ @GuardedBy("mPictureProfileLock")
+ @Override
+ public void notifyPictureProfileHandleSelection(long handle, int userId) {
+ PictureProfile profile = mMqDatabaseUtils.getPictureProfile(handle);
+ if (profile != null) {
+ mHalNotifier.notifyHalOnPictureProfileChange(handle, profile.getParameters());
+ }
+ }
+
+ public long getPictureProfileForTvInput(String inputId, int userId) {
+ // TODO: cache profiles
+ if (!hasGlobalPictureQualityServicePermission()) {
+ mMqManagerNotifier.notifyOnPictureProfileError(null,
+ PictureProfile.ERROR_NO_PERMISSION,
+ Binder.getCallingUid(), Binder.getCallingPid());
+ }
+ String[] columns = {BaseParameters.PARAMETER_ID};
+ String selection = BaseParameters.PARAMETER_TYPE + " = ? AND "
+ + BaseParameters.PARAMETER_NAME + " = ? AND "
+ + BaseParameters.PARAMETER_INPUT_ID + " = ?";
+ String[] selectionArguments = {
+ Integer.toString(PictureProfile.TYPE_SYSTEM),
+ PictureProfile.NAME_DEFAULT,
+ inputId
+ };
+ synchronized (mPictureProfileLock) {
+ try (Cursor cursor = mMqDatabaseUtils.getCursorAfterQuerying(
+ mMediaQualityDbHelper.PICTURE_QUALITY_TABLE_NAME,
+ columns, selection, selectionArguments)) {
+ int count = cursor.getCount();
+ if (count == 0) {
+ return -1;
+ }
+ long handle = -1;
+ cursor.moveToFirst();
+ int colIndex = cursor.getColumnIndex(BaseParameters.PARAMETER_ID);
+ if (colIndex != -1) {
+ handle = cursor.getLong(colIndex);
+ }
+ return handle;
+ }
+ }
+ }
@GuardedBy("mSoundProfileLock")
@Override
@@ -650,12 +808,15 @@ public class MediaQualityService extends SystemService {
try {
if (mMediaQuality != null) {
+ SoundParameters sp = new SoundParameters();
// put ID in params for profile update in HAL
+ // TODO: update HAL API for this case
params.putLong(BaseParameters.PARAMETER_ID, longId);
SoundParameter[] soundParameters =
MediaQualityUtils.convertPersistableBundleToSoundParameterList(params);
- SoundParameters sp = new SoundParameters();
+ Parcel parcel = Parcel.obtain();
+ setVendorSoundParameters(sp, parcel, params);
sp.soundParameters = soundParameters;
mMediaQuality.sendDefaultSoundParameters(sp);
@@ -701,21 +862,21 @@ public class MediaQualityService extends SystemService {
}
private boolean hasGlobalPictureQualityServicePermission() {
- return mPackageManager.checkPermission(android.Manifest.permission
- .MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE,
- mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED;
+ return mContext.checkCallingPermission(
+ android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE)
+ == PackageManager.PERMISSION_GRANTED;
}
private boolean hasGlobalSoundQualityServicePermission() {
- return mPackageManager.checkPermission(android.Manifest.permission
- .MANAGE_GLOBAL_SOUND_QUALITY_SERVICE,
- mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED;
+ return mContext.checkCallingPermission(
+ android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE)
+ == PackageManager.PERMISSION_GRANTED;
}
private boolean hasReadColorZonesPermission() {
- return mPackageManager.checkPermission(android.Manifest.permission
- .READ_COLOR_ZONES,
- mContext.getPackageName()) == mPackageManager.PERMISSION_GRANTED;
+ return mContext.checkCallingPermission(
+ android.Manifest.permission.READ_COLOR_ZONES)
+ == PackageManager.PERMISSION_GRANTED;
}
@Override
@@ -739,6 +900,18 @@ public class MediaQualityService extends SystemService {
}
@Override
+ public void registerActiveProcessingPictureListener(
+ final IActiveProcessingPictureListener l) {
+ int callingPid = Binder.getCallingPid();
+ int callingUid = Binder.getCallingUid();
+
+ UserState userState = getOrCreateUserState(Binder.getCallingUid());
+ String packageName = getPackageOfCallingUid();
+ userState.mActiveProcessingPictureListenerMap.put(l,
+ new ActiveProcessingPictureListenerInfo(callingUid, callingPid, packageName));
+ }
+
+ @Override
public void registerAmbientBacklightCallback(IAmbientBacklightCallback callback) {
if (DEBUG) {
Slogf.d(TAG, "registerAmbientBacklightCallback");
@@ -1145,6 +1318,20 @@ public class MediaQualityService extends SystemService {
}
}
+ private class ActiveProcessingPictureCallbackList extends
+ RemoteCallbackList<IActiveProcessingPictureListener> {
+ @Override
+ public void onCallbackDied(IActiveProcessingPictureListener l) {
+ synchronized (mPictureProfileLock) {
+ for (int i = 0; i < mUserStates.size(); i++) {
+ int userId = mUserStates.keyAt(i);
+ UserState userState = getOrCreateUserState(userId);
+ userState.mActiveProcessingPictureListenerMap.remove(l);
+ }
+ }
+ }
+ }
+
private final class UserState {
// A list of callbacks.
private final MediaQualityManagerPictureProfileCallbackList mPictureProfileCallbacks =
@@ -1153,18 +1340,35 @@ public class MediaQualityService extends SystemService {
private final MediaQualityManagerSoundProfileCallbackList mSoundProfileCallbacks =
new MediaQualityManagerSoundProfileCallbackList();
+ private final ActiveProcessingPictureCallbackList mActiveProcessingPictureCallbackList =
+ new ActiveProcessingPictureCallbackList();
+
private final Map<IPictureProfileCallback, Pair<Integer, Integer>>
mPictureProfileCallbackPidUidMap = new HashMap<>();
private final Map<ISoundProfileCallback, Pair<Integer, Integer>>
mSoundProfileCallbackPidUidMap = new HashMap<>();
+ private final Map<IActiveProcessingPictureListener, ActiveProcessingPictureListenerInfo>
+ mActiveProcessingPictureListenerMap = new HashMap<>();
+
private UserState(Context context, int userId) {
}
}
- @GuardedBy("mUserStateLock")
+ private final class ActiveProcessingPictureListenerInfo {
+ private int mUid;
+ private int mPid;
+ private String mPackageName;
+
+ ActiveProcessingPictureListenerInfo(int uid, int pid, String packageName) {
+ mUid = uid;
+ mPid = pid;
+ mPackageName = packageName;
+ }
+ }
+
private UserState getOrCreateUserState(int userId) {
UserState userState = getUserState(userId);
if (userState == null) {
@@ -1530,6 +1734,11 @@ public class MediaQualityService extends SystemService {
soundParameters.soundParameters =
MediaQualityUtils.convertPersistableBundleToSoundParameterList(params);
+ Parcel parcel = Parcel.obtain();
+ if (params != null) {
+ setVendorSoundParameters(soundParameters, parcel, params);
+ }
+
android.hardware.tv.mediaquality.SoundProfile toReturn =
new android.hardware.tv.mediaquality.SoundProfile();
toReturn.soundProfileId = id;
@@ -1933,4 +2142,21 @@ public class MediaQualityService extends SystemService {
vendorBundleToByteArray, vendorBundleToByteArray.length);
pictureParameters.vendorPictureParameters.setParcelable(defaultExtension);
}
+
+ private void setVendorSoundParameters(
+ SoundParameters soundParameters,
+ Parcel parcel,
+ PersistableBundle vendorSoundParameters) {
+ vendorSoundParameters.writeToParcel(parcel, 0);
+ byte[] vendorBundleToByteArray = parcel.marshall();
+ DefaultExtension defaultExtension = new DefaultExtension();
+ defaultExtension.bytes = Arrays.copyOf(
+ vendorBundleToByteArray, vendorBundleToByteArray.length);
+ soundParameters.vendorSoundParameters.setParcelable(defaultExtension);
+ }
+
+ private boolean isPackageDefaultPictureProfile(PictureProfile pp) {
+ return pp != null && pp.getProfileType() == PictureProfile.TYPE_SYSTEM &&
+ pp.getName().equals(PictureProfile.NAME_DEFAULT);
+ }
}
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java
index 08a0b595033c..f58bc982373b 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityUtils.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityUtils.java
@@ -1073,80 +1073,127 @@ public final class MediaQualityUtils {
if (params.containsKey(SoundQuality.PARAMETER_BALANCE)) {
soundParams.add(SoundParameter.balance(params.getInt(
SoundQuality.PARAMETER_BALANCE)));
+ params.remove(SoundQuality.PARAMETER_BALANCE);
}
if (params.containsKey(SoundQuality.PARAMETER_BASS)) {
soundParams.add(SoundParameter.bass(params.getInt(SoundQuality.PARAMETER_BASS)));
+ params.remove(SoundQuality.PARAMETER_BASS);
}
if (params.containsKey(SoundQuality.PARAMETER_TREBLE)) {
soundParams.add(SoundParameter.treble(params.getInt(
SoundQuality.PARAMETER_TREBLE)));
+ params.remove(SoundQuality.PARAMETER_TREBLE);
}
if (params.containsKey(SoundQuality.PARAMETER_SURROUND_SOUND)) {
soundParams.add(SoundParameter.surroundSoundEnabled(params.getBoolean(
SoundQuality.PARAMETER_SURROUND_SOUND)));
+ params.remove(SoundQuality.PARAMETER_SURROUND_SOUND);
}
if (params.containsKey(SoundQuality.PARAMETER_SPEAKERS)) {
soundParams.add(SoundParameter.speakersEnabled(params.getBoolean(
SoundQuality.PARAMETER_SPEAKERS)));
+ params.remove(SoundQuality.PARAMETER_SPEAKERS);
}
if (params.containsKey(SoundQuality.PARAMETER_SPEAKERS_DELAY_MILLIS)) {
soundParams.add(SoundParameter.speakersDelayMs(params.getInt(
SoundQuality.PARAMETER_SPEAKERS_DELAY_MILLIS)));
+ params.remove(SoundQuality.PARAMETER_SPEAKERS_DELAY_MILLIS);
}
if (params.containsKey(SoundQuality.PARAMETER_AUTO_VOLUME_CONTROL)) {
soundParams.add(SoundParameter.autoVolumeControl(params.getBoolean(
SoundQuality.PARAMETER_AUTO_VOLUME_CONTROL)));
+ params.remove(SoundQuality.PARAMETER_AUTO_VOLUME_CONTROL);
}
if (params.containsKey(SoundQuality.PARAMETER_DTS_DRC)) {
soundParams.add(SoundParameter.dtsDrc(params.getBoolean(
SoundQuality.PARAMETER_DTS_DRC)));
+ params.remove(SoundQuality.PARAMETER_DTS_DRC);
}
if (params.containsKey(SoundQuality.PARAMETER_DIGITAL_OUTPUT_DELAY_MILLIS)) {
soundParams.add(SoundParameter.surroundSoundEnabled(params.getBoolean(
SoundQuality.PARAMETER_DIGITAL_OUTPUT_DELAY_MILLIS)));
+ params.remove(SoundQuality.PARAMETER_DIGITAL_OUTPUT_DELAY_MILLIS);
}
if (params.containsKey(SoundQuality.PARAMETER_EARC)) {
soundParams.add(SoundParameter.enhancedAudioReturnChannelEnabled(params.getBoolean(
SoundQuality.PARAMETER_EARC)));
+ params.remove(SoundQuality.PARAMETER_EARC);
}
if (params.containsKey(SoundQuality.PARAMETER_DOWN_MIX_MODE)) {
soundParams.add(SoundParameter.downmixMode((byte) params.getInt(
SoundQuality.PARAMETER_DOWN_MIX_MODE)));
+ params.remove(SoundQuality.PARAMETER_DOWN_MIX_MODE);
}
if (params.containsKey(SoundQuality.PARAMETER_SOUND_STYLE)) {
soundParams.add(SoundParameter.soundStyle((byte) params.getInt(
SoundQuality.PARAMETER_SOUND_STYLE)));
+ params.remove(SoundQuality.PARAMETER_SOUND_STYLE);
}
if (params.containsKey(SoundQuality.PARAMETER_DIGITAL_OUTPUT_MODE)) {
soundParams.add(SoundParameter.digitalOutput((byte) params.getInt(
SoundQuality.PARAMETER_DIGITAL_OUTPUT_MODE)));
+ params.remove(SoundQuality.PARAMETER_DIGITAL_OUTPUT_MODE);
}
if (params.containsKey(SoundQuality.PARAMETER_DIALOGUE_ENHANCER)) {
soundParams.add(SoundParameter.dolbyDialogueEnhancer((byte) params.getInt(
SoundQuality.PARAMETER_DIALOGUE_ENHANCER)));
+ params.remove(SoundQuality.PARAMETER_DIALOGUE_ENHANCER);
}
DolbyAudioProcessing dab = new DolbyAudioProcessing();
- dab.soundMode =
- (byte) params.getInt(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SOUND_MODE);
- dab.volumeLeveler =
- params.getBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_VOLUME_LEVELER);
- dab.surroundVirtualizer = params.getBoolean(
- SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SURROUND_VIRTUALIZER);
- dab.dolbyAtmos =
- params.getBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_DOLBY_ATMOS);
+ if (params.containsKey(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SOUND_MODE)) {
+ dab.soundMode =
+ (byte) params.getInt(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SOUND_MODE);
+ params.remove(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SOUND_MODE);
+ }
+ if (params.containsKey(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_VOLUME_LEVELER)) {
+ dab.volumeLeveler =
+ params.getBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_VOLUME_LEVELER);
+ params.remove(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_VOLUME_LEVELER);
+ }
+ if (params.containsKey(
+ SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SURROUND_VIRTUALIZER)) {
+ dab.surroundVirtualizer = params.getBoolean(
+ SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SURROUND_VIRTUALIZER);
+ params.remove(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_SURROUND_VIRTUALIZER);
+ }
+ if (params.containsKey(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_DOLBY_ATMOS)) {
+ dab.dolbyAtmos =
+ params.getBoolean(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_DOLBY_ATMOS);
+ params.remove(SoundQuality.PARAMETER_DOLBY_AUDIO_PROCESSING_DOLBY_ATMOS);
+ }
soundParams.add(SoundParameter.dolbyAudioProcessing(dab));
DtsVirtualX dts = new DtsVirtualX();
- dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TBHDX);
- dts.limiter = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_LIMITER);
- dts.truSurroundX = params.getBoolean(
- SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_SURROUND_X);
- dts.truVolumeHd = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_VOLUME_HD);
- dts.dialogClarity = params.getBoolean(
- SoundQuality.PARAMETER_DTS_VIRTUAL_X_DIALOG_CLARITY);
- dts.definition = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DEFINITION);
- dts.height = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_HEIGHT);
+ if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TBHDX)) {
+ dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TBHDX);
+ params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TBHDX);
+ }
+
+ if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_LIMITER)) {
+ dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_LIMITER);
+ params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_LIMITER);
+ }
+ if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_SURROUND_X)) {
+ dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_SURROUND_X);
+ params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_SURROUND_X);
+ }
+ if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_VOLUME_HD)) {
+ dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_VOLUME_HD);
+ params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_TRU_VOLUME_HD);
+ }
+ if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DIALOG_CLARITY)) {
+ dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DIALOG_CLARITY);
+ params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DIALOG_CLARITY);
+ }
+ if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DEFINITION)) {
+ dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DEFINITION);
+ params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_DEFINITION);
+ }
+ if (params.containsKey(SoundQuality.PARAMETER_DTS_VIRTUAL_X_HEIGHT)) {
+ dts.tbHdx = params.getBoolean(SoundQuality.PARAMETER_DTS_VIRTUAL_X_HEIGHT);
+ params.remove(SoundQuality.PARAMETER_DTS_VIRTUAL_X_HEIGHT);
+ }
soundParams.add(SoundParameter.dtsVirtualX(dts));
return soundParams.toArray(new SoundParameter[0]);
diff --git a/services/core/java/com/android/server/tv/TvInputHal.java b/services/core/java/com/android/server/tv/TvInputHal.java
index 2a744e6a64ae..8d02f2fc4f8b 100644
--- a/services/core/java/com/android/server/tv/TvInputHal.java
+++ b/services/core/java/com/android/server/tv/TvInputHal.java
@@ -67,6 +67,7 @@ final class TvInputHal implements Handler.Callback {
private static native void nativeClose(long ptr);
private static native int nativeSetTvMessageEnabled(long ptr, int deviceId, int streamId,
int type, boolean enabled);
+
private static native int nativeSetPictureProfile(
long ptr, int deviceId, int streamId, long profileHandle);
@@ -124,6 +125,24 @@ final class TvInputHal implements Handler.Callback {
}
}
+ public int setPictureProfile(int deviceId, TvStreamConfig streamConfig, long profileHandle) {
+ synchronized (mLock) {
+ if (mPtr == 0) {
+ return ERROR_NO_INIT;
+ }
+ int generation = mStreamConfigGenerations.get(deviceId, 0);
+ if (generation != streamConfig.getGeneration()) {
+ return ERROR_STALE_CONFIG;
+ }
+ if (nativeSetPictureProfile(mPtr, deviceId, streamConfig.getStreamId(), profileHandle)
+ == 0) {
+ return SUCCESS;
+ } else {
+ return ERROR_UNKNOWN;
+ }
+ }
+ }
+
public int removeStream(int deviceId, TvStreamConfig streamConfig) {
synchronized (mLock) {
if (mPtr == 0) {
diff --git a/services/core/java/com/android/server/tv/TvInputHardwareManager.java b/services/core/java/com/android/server/tv/TvInputHardwareManager.java
index 92b57645b9a3..d3e3257fe384 100644
--- a/services/core/java/com/android/server/tv/TvInputHardwareManager.java
+++ b/services/core/java/com/android/server/tv/TvInputHardwareManager.java
@@ -596,6 +596,26 @@ class TvInputHardwareManager implements TvInputHal.Callback {
}
}
+ public boolean setPictureProfile(String inputId, long profileHandle) {
+ synchronized (mLock) {
+ int deviceId = findDeviceIdForInputIdLocked(inputId);
+ if (deviceId < 0) {
+ Slog.e(TAG, "Invalid inputId : " + inputId);
+ return false;
+ }
+
+ Connection connection = mConnections.get(deviceId);
+ boolean success = true;
+ for (TvStreamConfig config : connection.getConfigsLocked()) {
+ success = success
+ && mHal.setPictureProfile(deviceId, config, profileHandle)
+ == TvInputHal.SUCCESS;
+ }
+
+ return success;
+ }
+ }
+
/**
* Take a snapshot of the given TV input into the provided Surface.
*/
@@ -844,7 +864,6 @@ class TvInputHardwareManager implements TvInputHal.Callback {
return mCallback;
}
- @GuardedBy("mLock")
public TvStreamConfig[] getConfigsLocked() {
return mConfigs;
}
diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java
index 47d6879129ee..3c3bfeb75c85 100644
--- a/services/core/java/com/android/server/tv/TvInputManagerService.java
+++ b/services/core/java/com/android/server/tv/TvInputManagerService.java
@@ -51,6 +51,7 @@ import android.hardware.hdmi.HdmiDeviceInfo;
import android.hardware.hdmi.HdmiTvClient;
import android.media.AudioPresentation;
import android.media.PlaybackParams;
+import android.media.quality.MediaQualityManager;
import android.media.tv.AdBuffer;
import android.media.tv.AdRequest;
import android.media.tv.AdResponse;
@@ -193,6 +194,8 @@ public final class TvInputManagerService extends SystemService {
private HdmiControlManager mHdmiControlManager = null;
private HdmiTvClient mHdmiTvClient = null;
+ private MediaQualityManager mMediaQualityManager = null;
+
public TvInputManagerService(Context context) {
super(context);
@@ -919,7 +922,7 @@ public final class TvInputManagerService extends SystemService {
sendSessionTokenToClientLocked(sessionState.client,
sessionState.inputId, null, null, sessionState.seq);
}
- if (!serviceState.isHardware) {
+ if (!serviceState.isHardware || serviceState.reconnecting) {
updateServiceConnectionLocked(serviceState.component, userId);
} else {
updateHardwareServiceConnectionDelayed(userId);
@@ -3702,10 +3705,24 @@ public final class TvInputManagerService extends SystemService {
TvInputInfo inputInfo, ComponentName component, int userId) {
ServiceState serviceState = getServiceStateLocked(component, userId);
serviceState.hardwareInputMap.put(inputInfo.getId(), inputInfo);
+ setPictureProfileLocked(inputInfo.getId());
buildTvInputListLocked(userId, null);
}
@GuardedBy("mLock")
+ private void setPictureProfileLocked(String inputId) {
+ if (mMediaQualityManager == null) {
+ mMediaQualityManager = (MediaQualityManager) getContext()
+ .getSystemService(Context.MEDIA_QUALITY_SERVICE);
+ if (mMediaQualityManager == null) {
+ return;
+ }
+ }
+ long profileHandle = mMediaQualityManager.getPictureProfileForTvInput(inputId);
+ mTvInputHardwareManager.setPictureProfile(inputId, profileHandle);
+ }
+
+ @GuardedBy("mLock")
private void removeHardwareInputLocked(String inputId, int userId) {
if (!mTvInputHardwareManager.getInputMap().containsKey(inputId)) {
return;
diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
index cfaf31743a78..f4d7a8ec5484 100644
--- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionPolicy.kt
@@ -21,6 +21,7 @@ import android.content.pm.PackageManager
import android.content.pm.PermissionGroupInfo
import android.content.pm.PermissionInfo
import android.content.pm.SigningDetails
+import android.health.connect.HealthPermissions
import android.os.Build
import android.permission.flags.Flags
import android.util.Slog
@@ -112,7 +113,6 @@ class AppIdPermissionPolicy : SchemePolicy() {
addPermissions(packageState, changedPermissionNames)
trimPermissions(packageState.packageName, changedPermissionNames)
trimPermissionStates(packageState.appId)
- revokePermissionsOnPackageUpdate(packageState.appId)
}
changedPermissionNames.forEachIndexed { _, permissionName ->
evaluatePermissionStateForAllPackages(permissionName, null)
@@ -130,6 +130,7 @@ class AppIdPermissionPolicy : SchemePolicy() {
newState.externalState.userIds.forEachIndexed { _, userId ->
inheritImplicitPermissionStates(packageState.appId, userId)
}
+ revokePermissionsOnPackageUpdate(packageState.appId)
}
}
@@ -140,7 +141,6 @@ class AppIdPermissionPolicy : SchemePolicy() {
addPermissions(packageState, changedPermissionNames)
trimPermissions(packageState.packageName, changedPermissionNames)
trimPermissionStates(packageState.appId)
- revokePermissionsOnPackageUpdate(packageState.appId)
changedPermissionNames.forEachIndexed { _, permissionName ->
evaluatePermissionStateForAllPackages(permissionName, null)
}
@@ -148,6 +148,7 @@ class AppIdPermissionPolicy : SchemePolicy() {
newState.externalState.userIds.forEachIndexed { _, userId ->
inheritImplicitPermissionStates(packageState.appId, userId)
}
+ revokePermissionsOnPackageUpdate(packageState.appId)
}
override fun MutateStateScope.onPackageRemoved(packageName: String, appId: Int) {
@@ -700,6 +701,11 @@ class AppIdPermissionPolicy : SchemePolicy() {
}
private fun MutateStateScope.revokePermissionsOnPackageUpdate(appId: Int) {
+ revokeStorageAndMediaPermissionsOnPackageUpdate(appId)
+ revokeHeartRatePermissionsOnPackageUpdate(appId)
+ }
+
+ private fun MutateStateScope.revokeStorageAndMediaPermissionsOnPackageUpdate(appId: Int) {
val hasOldPackage =
appId in oldState.externalState.appIdPackageNames &&
anyPackageInAppId(appId, oldState) { true }
@@ -747,23 +753,154 @@ class AppIdPermissionPolicy : SchemePolicy() {
// SYSTEM_FIXED. Otherwise the user cannot grant back the permission.
if (
permissionName in STORAGE_AND_MEDIA_PERMISSIONS &&
- oldFlags.hasBits(PermissionFlags.RUNTIME_GRANTED) &&
- !oldFlags.hasAnyBit(SYSTEM_OR_POLICY_FIXED_MASK)
+ oldFlags.hasBits(PermissionFlags.RUNTIME_GRANTED)
) {
- Slog.v(
- LOG_TAG,
- "Revoking storage permission: $permissionName for appId: " +
- " $appId and user: $userId",
+ revokeRuntimePermission(appId, userId, permissionName)
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * If the app is updated, the legacy BODY_SENSOR and READ_HEART_RATE permissions may go out of
+ * sync (for example, when the app eventually requests the implicit new permission). If this
+ * occurs, revoke both permissions to force a re-prompt.
+ */
+ private fun MutateStateScope.revokeHeartRatePermissionsOnPackageUpdate(appId: Int) {
+ val targetSdkVersion = getAppIdTargetSdkVersion(appId, null)
+ // Apps targeting BAKLAVA and above shouldn't be using BODY_SENSORS.
+ if (targetSdkVersion >= Build.VERSION_CODES.BAKLAVA) {
+ return
+ }
+
+ val isBodySensorsRequested =
+ anyPackageInAppId(appId, newState) {
+ Manifest.permission.BODY_SENSORS in it.androidPackage!!.requestedPermissions
+ }
+ val isReadHeartRateRequested =
+ anyPackageInAppId(appId, newState) {
+ HealthPermissions.READ_HEART_RATE in it.androidPackage!!.requestedPermissions
+ }
+ val isBodySensorsBackgroundRequested =
+ anyPackageInAppId(appId, newState) {
+ Manifest.permission.BODY_SENSORS_BACKGROUND in
+ it.androidPackage!!.requestedPermissions
+ }
+ val isReadHealthDataInBackgroundRequested =
+ anyPackageInAppId(appId, newState) {
+ HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND in
+ it.androidPackage!!.requestedPermissions
+ }
+
+ // Walk the list of user IDs and revoke states as needed.
+ newState.userStates.forEachIndexed { _, userId, _ ->
+ // First sync BODY_SENSORS and READ_HEART_RATE, if required.
+ var isBodySensorsGranted =
+ isRuntimePermissionGranted(appId, userId, Manifest.permission.BODY_SENSORS)
+ if (isBodySensorsRequested && isReadHeartRateRequested) {
+ val isReadHeartRateGranted =
+ isRuntimePermissionGranted(appId, userId, HealthPermissions.READ_HEART_RATE)
+ if (isBodySensorsGranted != isReadHeartRateGranted) {
+ if (isBodySensorsGranted) {
+ if (
+ revokeRuntimePermission(appId, userId, Manifest.permission.BODY_SENSORS)
+ ) {
+ isBodySensorsGranted = false
+ }
+ }
+ if (isReadHeartRateGranted) {
+ revokeRuntimePermission(appId, userId, HealthPermissions.READ_HEART_RATE)
+ }
+ }
+ }
+
+ // Then check to ensure we haven't put the background/foreground permissions out of
+ // sync.
+ var isBodySensorsBackgroundGranted =
+ isRuntimePermissionGranted(
+ appId,
+ userId,
+ Manifest.permission.BODY_SENSORS_BACKGROUND,
+ )
+ if (isBodySensorsBackgroundGranted && !isBodySensorsGranted) {
+ if (
+ revokeRuntimePermission(
+ appId,
+ userId,
+ Manifest.permission.BODY_SENSORS_BACKGROUND,
+ )
+ ) {
+ isBodySensorsBackgroundGranted = false
+ }
+ }
+
+ // Finally sync BODY_SENSORS_BACKGROUND and READ_HEALTH_DATA_IN_BACKGROUND, if required.
+ if (isBodySensorsBackgroundRequested && isReadHealthDataInBackgroundRequested) {
+ val isReadHealthDataInBackgroundGranted =
+ isRuntimePermissionGranted(
+ appId,
+ userId,
+ HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND,
+ )
+ if (isBodySensorsBackgroundGranted != isReadHealthDataInBackgroundGranted) {
+ if (isBodySensorsBackgroundGranted) {
+ revokeRuntimePermission(
+ appId,
+ userId,
+ Manifest.permission.BODY_SENSORS_BACKGROUND,
+ )
+ }
+ if (isReadHealthDataInBackgroundGranted) {
+ revokeRuntimePermission(
+ appId,
+ userId,
+ HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND,
)
- val newFlags =
- oldFlags andInv (PermissionFlags.RUNTIME_GRANTED or USER_SETTABLE_MASK)
- setPermissionFlags(appId, userId, permissionName, newFlags)
}
}
}
}
}
+ private fun GetStateScope.isRuntimePermissionGranted(
+ appId: Int,
+ userId: Int,
+ permissionName: String,
+ ): Boolean {
+ val flags = getPermissionFlags(appId, userId, permissionName)
+ return PermissionFlags.isAppOpGranted(flags)
+ }
+
+ fun MutateStateScope.revokeRuntimePermission(
+ appId: Int,
+ userId: Int,
+ permissionName: String,
+ ): Boolean {
+ Slog.v(
+ LOG_TAG,
+ "Revoking runtime permission for appId: $appId, " +
+ "permission: $permissionName, userId: $userId",
+ )
+ var flags = getPermissionFlags(appId, userId, permissionName)
+ if (flags.hasAnyBit(SYSTEM_OR_POLICY_FIXED_MASK)) {
+ Slog.v(
+ LOG_TAG,
+ "Not allowed to revoke $permissionName for appId: $appId, userId: $userId",
+ )
+ return false
+ }
+
+ flags =
+ flags andInv
+ (PermissionFlags.RUNTIME_GRANTED or
+ USER_SETTABLE_MASK or
+ PermissionFlags.PREGRANT or
+ PermissionFlags.ROLE)
+ setPermissionFlags(appId, userId, permissionName, flags)
+ return true
+ }
+
private fun MutateStateScope.evaluatePermissionStateForAllPackages(
permissionName: String,
installedPackageState: PackageState?,
diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt
index bd63918e751b..e3e965de4559 100644
--- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt
+++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt
@@ -441,7 +441,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) {
return false
}
- val newFlags =
+ flags =
flags andInv
(PermissionFlags.RUNTIME_GRANTED or
MASK_USER_SETTABLE or
diff --git a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionStatesTest.kt b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionStatesTest.kt
index bf9033981442..c0f0369d4774 100644
--- a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionStatesTest.kt
+++ b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionStatesTest.kt
@@ -29,6 +29,7 @@ import com.android.server.pm.pkg.PackageState
import com.android.server.testutils.mock
import com.android.server.testutils.whenever
import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -537,6 +538,442 @@ class AppIdPermissionPolicyPermissionStatesTest : BasePermissionPolicyTest() {
.isEqualTo(expectedNewFlags)
}
+ /** Setup: BODY_SENSORS: granted, READ_HEART_RATE: not granted Result: BODY_SENSORS: revoked */
+ @Test
+ fun testEvaluatePermissionState_bodySensorReadHrOutOfSync_revokesGrantedBodySensor() {
+ assumeTrue(action != Action.ON_USER_ADDED)
+ val oldFlags = PermissionFlags.RUNTIME_GRANTED
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_BODY_SENSORS,
+ requestedPermissions = setOf(PERMISSION_BODY_SENSORS, PERMISSION_READ_HEART_RATE),
+ ) {}
+
+ val actualFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime body sensors" +
+ " permission that was granted while read hr was not, the actual permission" +
+ " flags $actualFlags should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * Setup: BODY_SENSORS: not granted, READ_HEART_RATE: granted Result: READ_HEART_RATE: revoked
+ */
+ @Test
+ fun testEvaluatePermissionState_bodySensorReadHrOutOfSync_revokesGrantedReadHr() {
+ assumeTrue(action != Action.ON_USER_ADDED)
+ val oldFlags = 0
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_BODY_SENSORS,
+ requestedPermissions = setOf(PERMISSION_BODY_SENSORS, PERMISSION_READ_HEART_RATE),
+ ) {
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_READ_HEART_RATE,
+ PermissionFlags.RUNTIME_GRANTED,
+ )
+ }
+
+ val actualFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_READ_HEART_RATE)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime body sensors" +
+ " permission that was not granted while read hr was, the actual permission" +
+ "flags $actualFlags should match the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * Setup: BODY_SENSORS: granted, READ_HEART_RATE: not granted Result: nothing revoked since the
+ * targetSdk is Baklava
+ */
+ @Test
+ fun testEvaluatePermissionState_bodySensorReadHrOutOfSync_baklavaTargetSdk_nothingRevoked() {
+ assumeTrue(action != Action.ON_USER_ADDED)
+ val oldFlags = PermissionFlags.RUNTIME_GRANTED
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_BODY_SENSORS,
+ installedPackageTargetSdkVersion = Build.VERSION_CODES.BAKLAVA,
+ requestedPermissions = setOf(PERMISSION_BODY_SENSORS, PERMISSION_READ_HEART_RATE),
+ ) {}
+
+ val actualFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS)
+ val expectedNewFlags = oldFlags
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime body sensors" +
+ " permission that was granted while read hr was not targeting Baklava," +
+ " the actual permission flags $actualFlags should match the expected" +
+ " flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * Setup: BODY_SENSORS_BACKGROUND: granted, BODY_SENSORS: not granted Result:
+ * BODY_SENSORS_BACKGROUND: revoked
+ */
+ @Test
+ fun testEvaluatePermissionState_bodySensorBackgroundGrantMismatch_revokesBackground() {
+ assumeTrue(action != Action.ON_USER_ADDED)
+ val oldFlags = 0
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_BODY_SENSORS,
+ requestedPermissions =
+ setOf(PERMISSION_BODY_SENSORS, PERMISSION_BODY_SENSORS_BACKGROUND),
+ ) {
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_BODY_SENSORS_BACKGROUND,
+ PermissionFlags.RUNTIME_GRANTED,
+ )
+ }
+
+ val actualFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS_BACKGROUND)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime body sensors" +
+ " permission that was not granted while body sensors background was," +
+ " the actual permission flags $actualFlags should match the expected" +
+ " flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * Setup: BODY_SENSORS_BACKGROUND: granted, BODY_SENSORS: not requested Result:
+ * BODY_SENSORS_BACKGROUND: revoked
+ */
+ @Test
+ fun testEvaluatePermissionState_bodySensorBackgroundMissingForeground_baklavaTargetSdk_revokesBackground() {
+ assumeTrue(action != Action.ON_USER_ADDED)
+ val oldFlags = PermissionFlags.RUNTIME_GRANTED
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_BODY_SENSORS_BACKGROUND,
+ requestedPermissions = setOf(PERMISSION_BODY_SENSORS_BACKGROUND),
+ ) {}
+
+ val actualFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS_BACKGROUND)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that has runtime body sensors background" +
+ " permission granted but is not requesting the body sensors foreground" +
+ " permission, the actual permission flags $actualFlags should match the" +
+ " expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * Setup: BODY_SENSORS_BACKGROUND: granted, BODY_SENSORS: granted, READ_HEART_RATE: not granted
+ * Result: BODY_SENSORS_BACKGROUND: revoked
+ */
+ @Test
+ fun testEvaluatePermissionState_bodySensorHeartRateOutOfSync_revokesGrantedBodySensorBackground() {
+ assumeTrue(action != Action.ON_USER_ADDED)
+ val oldFlags = PermissionFlags.RUNTIME_GRANTED
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_BODY_SENSORS,
+ installedPackageTargetSdkVersion = Build.VERSION_CODES.BAKLAVA,
+ requestedPermissions =
+ setOf(PERMISSION_BODY_SENSORS, PERMISSION_READ_HEART_RATE, PERMISSION_BODY_SENSORS),
+ ) {
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_BODY_SENSORS_BACKGROUND,
+ PermissionFlags.RUNTIME_GRANTED,
+ )
+ }
+
+ val actualFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS_BACKGROUND)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime body sensors" +
+ " permission that was granted while read hr was not targeting Baklava," +
+ " the actual permission flags $actualFlags should match the expected" +
+ " flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * Setup: BODY_SENSORS_BACKGROUND: granted, READ_HEALTH_DATA_IN_BACKGROUND: not granted Result:
+ * BODY_SENSORS_BACKGROUND: revoked
+ */
+ @Test
+ fun testEvaluatePermissionState_bodySensorReadHealthBackgroundOutOfSync_revokesGrantedBodySensorBackground() {
+ assumeTrue(action != Action.ON_USER_ADDED)
+ val oldFlags = 0
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_BODY_SENSORS_BACKGROUND,
+ requestedPermissions =
+ setOf(PERMISSION_BODY_SENSORS_BACKGROUND, PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND),
+ ) {
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND,
+ PermissionFlags.RUNTIME_GRANTED,
+ )
+ }
+
+ val actualFlags =
+ getPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND,
+ )
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime body sensors" +
+ " background permission that was not granted while read health data in" +
+ " background was, the actual permission flags $actualFlags should match" +
+ " the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * Setup: BODY_SENSORS_BACKGROUND: not granted, READ_HEALTH_DATA_IN_BACKGROUND: granted Result:
+ * READ_HEALTH_DATA_IN_BACKGROUND: revoked
+ */
+ @Test
+ fun testEvaluatePermissionState_bodySensorReadHealthBackgroundOutOfSync_revokesGrantedReadHealthBackground() {
+ assumeTrue(action != Action.ON_USER_ADDED)
+ val oldFlags = PermissionFlags.RUNTIME_GRANTED
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_BODY_SENSORS_BACKGROUND,
+ requestedPermissions =
+ setOf(PERMISSION_BODY_SENSORS_BACKGROUND, PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND),
+ ) {}
+
+ val actualFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS_BACKGROUND)
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that requests a runtime body sensors" +
+ " background permission that was granted while read health data in" +
+ " background was not, the actual permission flags $actualFlags should match" +
+ " the expected flags $expectedNewFlags"
+ )
+ .that(actualFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * The sequence of events here is:
+ *
+ * Starting:
+ * - READ_HR=not granted
+ * - BODY_SENSORS=granted
+ * - BODY_SENSORS_BACKGROUND=granted,
+ * - READ_HEALTH_DATA_IN_BACKGROUND=granted
+ *
+ * Actions:
+ * - BODY_SENSORS->revoked (due to READ_HR mismatch)
+ * - BODY_SENSORS_BACKGROUND->revoked (due to new BODY_SENSORS mismatch)
+ * - READ_HEALTH_DATA_IN_BACKGROUND->revoked (due to new BODY_SENSORS_BACKGROUND mismatch)
+ *
+ * End result: All permissions revoked.
+ */
+ @Test
+ fun testEvaluatePermissionState_healthPermissionsSync_revocationChain() {
+ assumeTrue(action != Action.ON_USER_ADDED)
+ val oldFlags = 0
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_READ_HEART_RATE,
+ requestedPermissions =
+ setOf(
+ PERMISSION_READ_HEART_RATE,
+ PERMISSION_BODY_SENSORS,
+ PERMISSION_BODY_SENSORS_BACKGROUND,
+ PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND,
+ ),
+ ) {
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_BODY_SENSORS,
+ PermissionFlags.RUNTIME_GRANTED,
+ )
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_BODY_SENSORS_BACKGROUND,
+ PermissionFlags.RUNTIME_GRANTED,
+ )
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND,
+ PermissionFlags.RUNTIME_GRANTED,
+ )
+ }
+
+ val bodySensorsFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS)
+ val bodySensorsBackgroundFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS_BACKGROUND)
+ val readHealthDataInBackgroundFlags =
+ getPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND,
+ )
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that has mismatching health permissions," +
+ " the actual permission flags for body sensors $bodySensorsFlags should" +
+ " match the expected flags $expectedNewFlags"
+ )
+ .that(bodySensorsFlags)
+ .isEqualTo(expectedNewFlags)
+ assertWithMessage(
+ "After $action is called for a package that has mismatching health permissions," +
+ " the actual permission flags for body sensors background" +
+ " $bodySensorsBackgroundFlags should match the expected flags" +
+ " $expectedNewFlags"
+ )
+ .that(bodySensorsBackgroundFlags)
+ .isEqualTo(expectedNewFlags)
+ assertWithMessage(
+ "After $action is called for a package that has mismatching health permissions," +
+ " the actual permission flags for read health data in background" +
+ " $readHealthDataInBackgroundFlags" +
+ " should match the expected flags $expectedNewFlags"
+ )
+ .that(readHealthDataInBackgroundFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
+ /**
+ * Similar to test case above but this time the READ_HR permission is going implicitly to
+ * explicitly granted (which causes it's grant to be revoked).
+ *
+ * Starting:
+ * - READ_HR=imlpicitly granted
+ * - BODY_SENSORS=granted
+ * - BODY_SENSORS_BACKGROUND=granted,
+ * - READ_HEALTH_DATA_IN_BACKGROUND=granted
+ *
+ * Actions:
+ * - READ_HR->revoked (due to implicit permission being explicitly requested)
+ * - BODY_SENSORS->revoked (due to READ_HR mismatch)
+ * - BODY_SENSORS_BACKGROUND->revoked (due to new BODY_SENSORS mismatch)
+ * - READ_HEALTH_DATA_IN_BACKGROUND->revoked (due to new BODY_SENSORS_BACKGROUND mismatch)
+ *
+ * End result: All permissions revoked.
+ */
+ @Test
+ fun testEvaluatePermissionState_implicitHealthPermissionRequested_causesRevocationChain() {
+ assumeTrue(action != Action.ON_USER_ADDED)
+ val oldFlags =
+ PermissionFlags.IMPLICIT or
+ PermissionFlags.RUNTIME_GRANTED or
+ PermissionFlags.USER_SET or
+ PermissionFlags.USER_FIXED
+ testEvaluatePermissionState(
+ oldFlags,
+ PermissionInfo.PROTECTION_DANGEROUS,
+ permissionName = PERMISSION_READ_HEART_RATE,
+ requestedPermissions =
+ setOf(
+ PERMISSION_READ_HEART_RATE,
+ PERMISSION_BODY_SENSORS,
+ PERMISSION_BODY_SENSORS_BACKGROUND,
+ PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND,
+ ),
+ ) {
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_BODY_SENSORS,
+ PermissionFlags.RUNTIME_GRANTED,
+ )
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_BODY_SENSORS_BACKGROUND,
+ PermissionFlags.RUNTIME_GRANTED,
+ )
+ setPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND,
+ PermissionFlags.RUNTIME_GRANTED,
+ )
+ }
+
+ val bodySensorsFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS)
+ val bodySensorsBackgroundFlags =
+ getPermissionFlags(APP_ID_1, getUserIdEvaluated(), PERMISSION_BODY_SENSORS_BACKGROUND)
+ val readHealthDataInBackgroundFlags =
+ getPermissionFlags(
+ APP_ID_1,
+ getUserIdEvaluated(),
+ PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND,
+ )
+ val expectedNewFlags = 0
+ assertWithMessage(
+ "After $action is called for a package that has mismatching health permissions," +
+ "the actual permission flags for body sensors $bodySensorsFlags should match the" +
+ "expected flags $expectedNewFlags"
+ )
+ .that(bodySensorsFlags)
+ .isEqualTo(expectedNewFlags)
+ assertWithMessage(
+ "After $action is called for a package that has mismatching health permissions," +
+ "the actual permission flags for body sensors background $bodySensorsBackgroundFlags should" +
+ "match the expected flags $expectedNewFlags"
+ )
+ .that(bodySensorsBackgroundFlags)
+ .isEqualTo(expectedNewFlags)
+ assertWithMessage(
+ "After $action is called for a package that has mismatching health permissions," +
+ "the actual permission flags for read health data in background $readHealthDataInBackgroundFlags" +
+ "should match the expected flags $expectedNewFlags"
+ )
+ .that(readHealthDataInBackgroundFlags)
+ .isEqualTo(expectedNewFlags)
+ }
+
@Test
fun testEvaluatePermissionState_noLongerImplicitSystemOrPolicyFixedWasGranted_runtimeGranted() {
val oldFlags =
diff --git a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/BasePermissionPolicyTest.kt b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/BasePermissionPolicyTest.kt
index 207820cc3135..6dfd2611e0af 100644
--- a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/BasePermissionPolicyTest.kt
+++ b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/BasePermissionPolicyTest.kt
@@ -21,6 +21,7 @@ import android.content.pm.PackageManager
import android.content.pm.PermissionGroupInfo
import android.content.pm.PermissionInfo
import android.content.pm.SigningDetails
+import android.health.connect.HealthPermissions
import android.os.Build
import android.os.Bundle
import android.util.ArrayMap
@@ -390,6 +391,14 @@ abstract class BasePermissionPolicyTest {
Manifest.permission.ACCESS_BACKGROUND_LOCATION
@JvmStatic
protected val PERMISSION_ACCESS_MEDIA_LOCATION = Manifest.permission.ACCESS_MEDIA_LOCATION
+ @JvmStatic protected val PERMISSION_BODY_SENSORS = Manifest.permission.BODY_SENSORS
+ @JvmStatic
+ protected val PERMISSION_BODY_SENSORS_BACKGROUND =
+ Manifest.permission.BODY_SENSORS_BACKGROUND
+ @JvmStatic protected val PERMISSION_READ_HEART_RATE = HealthPermissions.READ_HEART_RATE
+ @JvmStatic
+ protected val PERMISSION_READ_HEALTH_DATA_IN_BACKGROUND =
+ HealthPermissions.READ_HEALTH_DATA_IN_BACKGROUND
@JvmStatic protected val USER_ID_0 = 0
@JvmStatic protected val USER_ID_NEW = 1
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java
index 9eeb4f3f218f..579114bc6577 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java
@@ -16,6 +16,7 @@
package com.android.server.biometrics;
+import static com.android.server.biometrics.AuthenticationStatsCollector.FRR_MINIMAL_DURATION;
import static com.android.server.biometrics.AuthenticationStatsCollector.MAXIMUM_ENROLLMENT_NOTIFICATIONS;
import static com.google.common.truth.Truth.assertThat;
@@ -37,9 +38,13 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Resources;
+import android.hardware.biometrics.BiometricsProtoEnums;
import android.hardware.face.FaceManager;
import android.hardware.fingerprint.FingerprintManager;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.SetFlagsRule;
import androidx.test.filters.SmallTest;
@@ -54,6 +59,7 @@ import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.io.File;
+import java.time.Clock;
@Presubmit
@SmallTest
@@ -61,6 +67,8 @@ public class AuthenticationStatsCollectorTest {
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
private AuthenticationStatsCollector mAuthenticationStatsCollector;
private static final float FRR_THRESHOLD = 0.2f;
@@ -82,6 +90,8 @@ public class AuthenticationStatsCollectorTest {
private SharedPreferences.Editor mEditor;
@Mock
private BiometricNotification mBiometricNotification;
+ @Mock
+ private Clock mClock;
@Before
public void setUp() {
@@ -107,9 +117,12 @@ public class AuthenticationStatsCollectorTest {
when(mSharedPreferences.edit()).thenReturn(mEditor);
when(mEditor.putFloat(anyString(), anyFloat())).thenReturn(mEditor);
when(mEditor.putStringSet(anyString(), anySet())).thenReturn(mEditor);
+ when(mBiometricNotification.sendCustomizeFpFrrNotification(eq(mContext)))
+ .thenReturn(true);
+ when(mClock.millis()).thenReturn(Clock.systemUTC().millis());
mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext,
- 0 /* modality */, mBiometricNotification);
+ 0 /* modality */, mBiometricNotification, mClock);
}
@Test
@@ -130,6 +143,8 @@ public class AuthenticationStatsCollectorTest {
assertThat(authenticationStats.getTotalAttempts()).isEqualTo(1);
assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0);
assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(0L);
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(0L);
}
@Test
@@ -151,6 +166,8 @@ public class AuthenticationStatsCollectorTest {
assertThat(authenticationStats.getTotalAttempts()).isEqualTo(1);
assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(1);
assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(0L);
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(0L);
}
/**
@@ -165,6 +182,7 @@ public class AuthenticationStatsCollectorTest {
new AuthenticationStats(USER_ID_1, 400 /* totalAttempts */,
40 /* rejectedAttempts */,
MAXIMUM_ENROLLMENT_NOTIFICATIONS /* enrollmentNotifications */,
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
0 /* modality */));
mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
@@ -178,14 +196,18 @@ public class AuthenticationStatsCollectorTest {
assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(40);
assertThat(authenticationStats.getEnrollmentNotifications())
.isEqualTo(MAXIMUM_ENROLLMENT_NOTIFICATIONS);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(200L);
}
+ // TODO WIP
@Test
public void authenticate_frrNotExceeded_notificationNotExceeded_shouldNotSendNotification() {
mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
40 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
0 /* modality */));
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
@@ -196,6 +218,7 @@ public class AuthenticationStatsCollectorTest {
mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
// Assert that no notification should be sent.
+ verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any());
verify(mBiometricNotification, never()).sendFaceEnrollNotification(any());
verify(mBiometricNotification, never()).sendFpEnrollNotification(any());
// Assert that data has been reset.
@@ -205,6 +228,9 @@ public class AuthenticationStatsCollectorTest {
assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0);
assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0);
assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f);
+ // lastEnrollmentTime and lastFrrNotificationTime shall be kept
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(200L);
}
@Test
@@ -214,11 +240,13 @@ public class AuthenticationStatsCollectorTest {
new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
400 /* rejectedAttempts */,
MAXIMUM_ENROLLMENT_NOTIFICATIONS /* enrollmentNotifications */,
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
0 /* modality */));
mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
// Assert that no notification should be sent.
+ verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any());
verify(mBiometricNotification, never()).sendFaceEnrollNotification(any());
verify(mBiometricNotification, never()).sendFpEnrollNotification(any());
// Assert that data hasn't been reset.
@@ -228,15 +256,88 @@ public class AuthenticationStatsCollectorTest {
assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(400);
assertThat(authenticationStats.getEnrollmentNotifications())
.isEqualTo(MAXIMUM_ENROLLMENT_NOTIFICATIONS);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(200L);
assertThat(authenticationStats.getFrr()).isWithin(0f).of(0.8f);
}
@Test
+ @DisableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT)
public void authenticate_frrExceeded_bothBiometricsEnrolled_shouldNotSendNotification() {
mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
400 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
+ 0 /* modality */));
+
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
+ .thenReturn(true);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
+ when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+
+ mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
+
+ // Assert that no notification should be sent.
+ verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any());
+ verify(mBiometricNotification, never()).sendFaceEnrollNotification(any());
+ verify(mBiometricNotification, never()).sendFpEnrollNotification(any());
+ // Assert that data hasn't been reset.
+ AuthenticationStats authenticationStats = mAuthenticationStatsCollector
+ .getAuthenticationStatsForUser(USER_ID_1);
+ assertThat(authenticationStats.getTotalAttempts()).isEqualTo(500);
+ assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(400);
+ assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0);
+ assertThat(authenticationStats.getFrr()).isWithin(0f).of(0.8f);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(200L);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT)
+ public void authenticate_enrollTimeNotPass_bothBiometricsEnrolled_shouldNotSendNotification() {
+
+ long lastEnrollmentTime = 60L * 60L * 1000L;
+ mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
+ new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
+ 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+ lastEnrollmentTime, 0L /* lastFrrNotificationTime */,
+ 0 /* modality */));
+
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
+ .thenReturn(true);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
+ when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ when(mClock.millis()).thenReturn(lastEnrollmentTime + FRR_MINIMAL_DURATION.toMillis());
+
+ mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
+
+ // Assert that no notification should be sent.
+ verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any());
+ verify(mBiometricNotification, never()).sendFaceEnrollNotification(any());
+ verify(mBiometricNotification, never()).sendFpEnrollNotification(any());
+ // Assert that data hasn't been reset.
+ AuthenticationStats authenticationStats = mAuthenticationStatsCollector
+ .getAuthenticationStatsForUser(USER_ID_1);
+ assertThat(authenticationStats.getTotalAttempts()).isEqualTo(500);
+ assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(400);
+ assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0);
+ assertThat(authenticationStats.getFrr()).isWithin(0f).of(0.8f);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(lastEnrollmentTime);
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(0L);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT)
+ public void authenticate_lastFrrTimeNotPass_bothBiometricsEnrolled_shouldNotSendNotification() {
+
+ long lastFrrNotificationTime = 200L;
+ mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
+ new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
+ 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+ 100L /* lastEnrollmentTime */, lastFrrNotificationTime,
0 /* modality */));
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
@@ -244,10 +345,12 @@ public class AuthenticationStatsCollectorTest {
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ when(mClock.millis()).thenReturn(lastFrrNotificationTime + FRR_MINIMAL_DURATION.toMillis());
mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
// Assert that no notification should be sent.
+ verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any());
verify(mBiometricNotification, never()).sendFaceEnrollNotification(any());
verify(mBiometricNotification, never()).sendFpEnrollNotification(any());
// Assert that data hasn't been reset.
@@ -257,6 +360,9 @@ public class AuthenticationStatsCollectorTest {
assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(400);
assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0);
assertThat(authenticationStats.getFrr()).isWithin(0f).of(0.8f);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(
+ lastFrrNotificationTime);
}
@Test
@@ -265,6 +371,7 @@ public class AuthenticationStatsCollectorTest {
mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
400 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
0 /* modality */));
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
@@ -276,6 +383,7 @@ public class AuthenticationStatsCollectorTest {
mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
// Assert that no notification should be sent.
+ verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any());
verify(mBiometricNotification, never()).sendFaceEnrollNotification(any());
verify(mBiometricNotification, never()).sendFpEnrollNotification(any());
// Assert that data hasn't been reset.
@@ -285,26 +393,75 @@ public class AuthenticationStatsCollectorTest {
assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(400);
assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(0);
assertThat(authenticationStats.getFrr()).isWithin(0f).of(0.8f);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(200L);
}
@Test
+ @DisableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT)
public void authenticate_frrExceeded_faceEnrolled_shouldSendFpNotification() {
+ // Use correct modality
+ mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext,
+ BiometricsProtoEnums.MODALITY_FACE, mBiometricNotification, mClock);
+
mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
400 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
- 0 /* modality */));
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
+ BiometricsProtoEnums.MODALITY_FACE /* modality */));
+
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
+ .thenReturn(true);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
+ when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(false);
+ when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ when(mClock.millis()).thenReturn(3344L);
+
+ mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
+
+ // Assert that fingerprint enrollment notification should be sent.
+ verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any());
+ verify(mBiometricNotification, times(1)).sendFpEnrollNotification(mContext);
+ verify(mBiometricNotification, never()).sendFaceEnrollNotification(any());
+ // Assert that data has been reset.
+ AuthenticationStats authenticationStats = mAuthenticationStatsCollector
+ .getAuthenticationStatsForUser(USER_ID_1);
+ assertThat(authenticationStats.getTotalAttempts()).isEqualTo(0);
+ assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0);
+ assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f);
+ // Assert that notification count has been updated.
+ assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(1);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ // Assert that lastFrrNotificationTime has been updated.
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(3344L);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT)
+ public void authenticate_frrExceeded_faceEnrolled_shouldSendFpNotification_withFrrFlag() {
+ // Use correct modality
+ mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext,
+ BiometricsProtoEnums.MODALITY_FACE, mBiometricNotification, mClock);
+
+ mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
+ new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
+ 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
+ BiometricsProtoEnums.MODALITY_FACE /* modality */));
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
.thenReturn(true);
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(false);
when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ long newLastFrrNotificationTime = 200L + FRR_MINIMAL_DURATION.toMillis() + 1;
+ when(mClock.millis()).thenReturn(newLastFrrNotificationTime);
mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
// Assert that fingerprint enrollment notification should be sent.
- verify(mBiometricNotification, times(1))
- .sendFpEnrollNotification(mContext);
+ verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any());
+ verify(mBiometricNotification, times(1)).sendFpEnrollNotification(mContext);
verify(mBiometricNotification, never()).sendFaceEnrollNotification(any());
// Assert that data has been reset.
AuthenticationStats authenticationStats = mAuthenticationStatsCollector
@@ -314,26 +471,116 @@ public class AuthenticationStatsCollectorTest {
assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f);
// Assert that notification count has been updated.
assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(1);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ // Assert that lastFrrNotificationTime has been updated.
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(
+ newLastFrrNotificationTime);
}
@Test
+ @DisableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT)
public void authenticate_frrExceeded_fpEnrolled_shouldSendFaceNotification() {
mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
400 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
- 0 /* modality */));
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
+ BiometricsProtoEnums.MODALITY_FINGERPRINT /* modality */));
+
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
+ .thenReturn(true);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
+ when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(false);
+ when(mClock.millis()).thenReturn(3344L);
+
+ mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
+
+ // Assert that fingerprint enrollment notification should be sent.
+ verify(mBiometricNotification, never()).sendCustomizeFpFrrNotification(any());
+ verify(mBiometricNotification, times(1)).sendFaceEnrollNotification(mContext);
+ verify(mBiometricNotification, never()).sendFpEnrollNotification(any());
+ // Assert that data has been reset.
+ AuthenticationStats authenticationStats = mAuthenticationStatsCollector
+ .getAuthenticationStatsForUser(USER_ID_1);
+ assertThat(authenticationStats.getTotalAttempts()).isEqualTo(0);
+ assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0);
+ assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f);
+ // Assert that notification count has been updated.
+ assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(1);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ // Assert that lastFrrNotificationTime has been updated.
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(3344L);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT)
+ public void authenticate_frrExceeded_fpEnrolled_shouldSendCustNotification_withFrrFlag() {
+ // Use correct modality
+ mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext,
+ BiometricsProtoEnums.MODALITY_FINGERPRINT, mBiometricNotification, mClock);
+
+ mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
+ new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
+ 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
+ BiometricsProtoEnums.MODALITY_FINGERPRINT /* modality */));
+
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
+ .thenReturn(true);
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
+ when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+ when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(false);
+ long newFrrNotificationTime = 200L + FRR_MINIMAL_DURATION.toMillis() + 1;
+ when(mClock.millis()).thenReturn(newFrrNotificationTime);
+
+ mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
+
+ // Assert that fingerprint enrollment notification should be sent.
+ verify(mBiometricNotification, times(1)).sendCustomizeFpFrrNotification(mContext);
+ verify(mBiometricNotification, never()).sendFaceEnrollNotification(any());
+ verify(mBiometricNotification, never()).sendFpEnrollNotification(any());
+ // Assert that data has been reset.
+ AuthenticationStats authenticationStats = mAuthenticationStatsCollector
+ .getAuthenticationStatsForUser(USER_ID_1);
+ assertThat(authenticationStats.getTotalAttempts()).isEqualTo(0);
+ assertThat(authenticationStats.getRejectedAttempts()).isEqualTo(0);
+ assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f);
+ // Assert that notification count has been updated.
+ assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(1);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ // Assert that lastFrrNotificationTime has been updated.
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(
+ newFrrNotificationTime);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_FRR_DIALOG_IMPROVEMENT)
+ public void authenticate_frrExceeded_fpEnrolled_shouldSendFaceNotification_withFrrFlag() {
+ // Use correct modality
+ mAuthenticationStatsCollector = new AuthenticationStatsCollector(mContext,
+ BiometricsProtoEnums.MODALITY_FINGERPRINT, mBiometricNotification, mClock);
+
+ mAuthenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
+ new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
+ 400 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
+ BiometricsProtoEnums.MODALITY_FINGERPRINT /* modality */));
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT))
.thenReturn(true);
when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true);
when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
when(mFaceManager.hasEnrolledTemplates(anyInt())).thenReturn(false);
+ long newFrrNotificationTime = 200L + FRR_MINIMAL_DURATION.toMillis() + 1;
+ when(mClock.millis()).thenReturn(newFrrNotificationTime);
+ when(mBiometricNotification.sendCustomizeFpFrrNotification(eq(mContext)))
+ .thenReturn(false);
mAuthenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
// Assert that fingerprint enrollment notification should be sent.
- verify(mBiometricNotification, times(1))
- .sendFaceEnrollNotification(mContext);
+ verify(mBiometricNotification, times(1)).sendCustomizeFpFrrNotification(mContext);
+ verify(mBiometricNotification, times(1)).sendFaceEnrollNotification(mContext);
verify(mBiometricNotification, never()).sendFpEnrollNotification(any());
// Assert that data has been reset.
AuthenticationStats authenticationStats = mAuthenticationStatsCollector
@@ -343,6 +590,10 @@ public class AuthenticationStatsCollectorTest {
assertThat(authenticationStats.getFrr()).isWithin(0f).of(-1.0f);
// Assert that notification count has been updated.
assertThat(authenticationStats.getEnrollmentNotifications()).isEqualTo(1);
+ assertThat(authenticationStats.getLastEnrollmentTime()).isEqualTo(100L);
+ // Assert that lastFrrNotificationTime has been updated.
+ assertThat(authenticationStats.getLastFrrNotificationTime()).isEqualTo(
+ newFrrNotificationTime);
}
@Test
@@ -352,11 +603,12 @@ public class AuthenticationStatsCollectorTest {
.thenReturn(false);
AuthenticationStatsCollector authenticationStatsCollector =
new AuthenticationStatsCollector(mContext, 0 /* modality */,
- mBiometricNotification);
+ mBiometricNotification, Clock.systemUTC());
authenticationStatsCollector.setAuthenticationStatsForUser(USER_ID_1,
new AuthenticationStats(USER_ID_1, 500 /* totalAttempts */,
400 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+ 0L /* lastEnrollmentTime */, 0L /* lastFrrNotificationTime */,
0 /* modality */));
authenticationStatsCollector.authenticate(USER_ID_1, false /* authenticated */);
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java
index 32c55ebcb674..67da3ed144fc 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java
@@ -60,8 +60,13 @@ public class AuthenticationStatsPersisterTest {
private static final String USER_ID = "user_id";
private static final String FACE_ATTEMPTS = "face_attempts";
private static final String FACE_REJECTIONS = "face_rejections";
+ private static final String FACE_LAST_ENROLL_TIME = "face_last_enroll_time";
+ private static final String FACE_LAST_FRR_NOTIFICATION_TIME = "face_last_notification_time";
private static final String FINGERPRINT_ATTEMPTS = "fingerprint_attempts";
private static final String FINGERPRINT_REJECTIONS = "fingerprint_rejections";
+ private static final String FINGERPRINT_LAST_ENROLL_TIME = "fingerprint_last_enroll_time";
+ private static final String FINGERPRINT_LAST_FRR_NOTIFICATION_TIME =
+ "fingerprint_last_notification_time";
private static final String ENROLLMENT_NOTIFICATIONS = "enrollment_notifications";
private static final String KEY = "frr_stats";
private static final String THRESHOLD_KEY = "frr_threshold";
@@ -95,10 +100,12 @@ public class AuthenticationStatsPersisterTest {
public void getAllFrrStats_face_shouldListAllFrrStats() throws JSONException {
AuthenticationStats stats1 = new AuthenticationStats(USER_ID_1,
300 /* totalAttempts */, 10 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
+ 0 /* enrollmentNotifications */, 100L /* lastEnrollmentTime */,
+ 200L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE);
AuthenticationStats stats2 = new AuthenticationStats(USER_ID_2,
200 /* totalAttempts */, 20 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ 0 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */,
+ 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn(
Set.of(buildFrrStats(stats1), buildFrrStats(stats2)));
@@ -108,7 +115,8 @@ public class AuthenticationStatsPersisterTest {
assertThat(authenticationStatsList.size()).isEqualTo(2);
AuthenticationStats expectedStats2 = new AuthenticationStats(USER_ID_2,
0 /* totalAttempts */, 0 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
+ 0 /* enrollmentNotifications */, 0 /* lastEnrollmentTime */,
+ 0 /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE);
assertThat(authenticationStatsList).contains(stats1);
assertThat(authenticationStatsList).contains(expectedStats2);
}
@@ -118,11 +126,13 @@ public class AuthenticationStatsPersisterTest {
// User 1 with fingerprint authentication stats.
AuthenticationStats stats1 = new AuthenticationStats(USER_ID_1,
200 /* totalAttempts */, 20 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ 0 /* enrollmentNotifications */, 100L /* lastEnrollmentTime */,
+ 200L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
// User 2 without fingerprint authentication stats.
AuthenticationStats stats2 = new AuthenticationStats(USER_ID_2,
300 /* totalAttempts */, 10 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
+ 0 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */,
+ 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE);
when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn(
Set.of(buildFrrStats(stats1), buildFrrStats(stats2)));
@@ -133,7 +143,8 @@ public class AuthenticationStatsPersisterTest {
assertThat(authenticationStatsList.size()).isEqualTo(2);
AuthenticationStats expectedStats2 = new AuthenticationStats(USER_ID_2,
0 /* totalAttempts */, 0 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ 0 /* enrollmentNotifications */, 0 /* lastEnrollmentTime */,
+ 0 /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
assertThat(authenticationStatsList).contains(stats1);
assertThat(authenticationStatsList).contains(expectedStats2);
}
@@ -142,12 +153,15 @@ public class AuthenticationStatsPersisterTest {
public void persistFrrStats_newUser_face_shouldSuccess() throws JSONException {
AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1,
300 /* totalAttempts */, 10 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
+ 0 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */,
+ 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE);
mAuthenticationStatsPersister.persistFrrStats(authenticationStats.getUserId(),
authenticationStats.getTotalAttempts(),
authenticationStats.getRejectedAttempts(),
authenticationStats.getEnrollmentNotifications(),
+ authenticationStats.getLastEnrollmentTime(),
+ authenticationStats.getLastFrrNotificationTime(),
authenticationStats.getModality());
verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture());
@@ -159,12 +173,15 @@ public class AuthenticationStatsPersisterTest {
public void persistFrrStats_newUser_fingerprint_shouldSuccess() throws JSONException {
AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1,
300 /* totalAttempts */, 10 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ 0 /* enrollmentNotifications */, 100L /* lastEnrollmentTime */,
+ 200L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
mAuthenticationStatsPersister.persistFrrStats(authenticationStats.getUserId(),
authenticationStats.getTotalAttempts(),
authenticationStats.getRejectedAttempts(),
authenticationStats.getEnrollmentNotifications(),
+ authenticationStats.getLastEnrollmentTime(),
+ authenticationStats.getLastFrrNotificationTime(),
authenticationStats.getModality());
verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture());
@@ -176,10 +193,12 @@ public class AuthenticationStatsPersisterTest {
public void persistFrrStats_existingUser_shouldUpdateRecord() throws JSONException {
AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1,
300 /* totalAttempts */, 10 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
+ 0 /* enrollmentNotifications */, 100L /* lastEnrollmentTime */,
+ 200L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE);
AuthenticationStats newAuthenticationStats = new AuthenticationStats(USER_ID_1,
500 /* totalAttempts */, 30 /* rejectedAttempts */,
- 1 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
+ 1 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */,
+ 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE);
when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn(
Set.of(buildFrrStats(authenticationStats)));
@@ -187,6 +206,8 @@ public class AuthenticationStatsPersisterTest {
newAuthenticationStats.getTotalAttempts(),
newAuthenticationStats.getRejectedAttempts(),
newAuthenticationStats.getEnrollmentNotifications(),
+ newAuthenticationStats.getLastEnrollmentTime(),
+ newAuthenticationStats.getLastFrrNotificationTime(),
newAuthenticationStats.getModality());
verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture());
@@ -200,11 +221,13 @@ public class AuthenticationStatsPersisterTest {
// User with fingerprint authentication stats.
AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1,
200 /* totalAttempts */, 20 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ 0 /* enrollmentNotifications */, 100L /* lastEnrollmentTime */,
+ 200L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
// The same user with face authentication stats.
AuthenticationStats newAuthenticationStats = new AuthenticationStats(USER_ID_1,
500 /* totalAttempts */, 30 /* rejectedAttempts */,
- 1 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
+ 1 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */,
+ 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE);
when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn(
Set.of(buildFrrStats(authenticationStats)));
@@ -212,12 +235,18 @@ public class AuthenticationStatsPersisterTest {
newAuthenticationStats.getTotalAttempts(),
newAuthenticationStats.getRejectedAttempts(),
newAuthenticationStats.getEnrollmentNotifications(),
+ newAuthenticationStats.getLastEnrollmentTime(),
+ newAuthenticationStats.getLastFrrNotificationTime(),
newAuthenticationStats.getModality());
String expectedFrrStats = new JSONObject(buildFrrStats(authenticationStats))
.put(ENROLLMENT_NOTIFICATIONS, newAuthenticationStats.getEnrollmentNotifications())
.put(FACE_ATTEMPTS, newAuthenticationStats.getTotalAttempts())
- .put(FACE_REJECTIONS, newAuthenticationStats.getRejectedAttempts()).toString();
+ .put(FACE_REJECTIONS, newAuthenticationStats.getRejectedAttempts())
+ .put(FACE_LAST_ENROLL_TIME, newAuthenticationStats.getLastEnrollmentTime())
+ .put(FACE_LAST_FRR_NOTIFICATION_TIME,
+ newAuthenticationStats.getLastFrrNotificationTime())
+ .toString();
verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture());
assertThat(mStringSetArgumentCaptor.getValue()).contains(expectedFrrStats);
}
@@ -226,10 +255,12 @@ public class AuthenticationStatsPersisterTest {
public void persistFrrStats_multiUser_newUser_shouldUpdateRecord() throws JSONException {
AuthenticationStats authenticationStats1 = new AuthenticationStats(USER_ID_1,
300 /* totalAttempts */, 10 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
+ 0 /* enrollmentNotifications */, 100L /* lastEnrollmentTime */,
+ 200L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE);
AuthenticationStats authenticationStats2 = new AuthenticationStats(USER_ID_2,
100 /* totalAttempts */, 5 /* rejectedAttempts */,
- 1 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ 1 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */,
+ 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FINGERPRINT);
// Sets up the shared preference with user 1 only.
when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn(
@@ -240,6 +271,8 @@ public class AuthenticationStatsPersisterTest {
authenticationStats2.getTotalAttempts(),
authenticationStats2.getRejectedAttempts(),
authenticationStats2.getEnrollmentNotifications(),
+ authenticationStats2.getLastEnrollmentTime(),
+ authenticationStats2.getLastFrrNotificationTime(),
authenticationStats2.getModality());
verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture());
@@ -251,7 +284,8 @@ public class AuthenticationStatsPersisterTest {
public void removeFrrStats_existingUser_shouldUpdateRecord() throws JSONException {
AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1,
300 /* totalAttempts */, 10 /* rejectedAttempts */,
- 0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
+ 0 /* enrollmentNotifications */, 200L /* lastEnrollmentTime */,
+ 300L /* lastFrrNotificationTime */, BiometricsProtoEnums.MODALITY_FACE);
when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn(
Set.of(buildFrrStats(authenticationStats)));
@@ -277,6 +311,9 @@ public class AuthenticationStatsPersisterTest {
.put(FACE_ATTEMPTS, authenticationStats.getTotalAttempts())
.put(FACE_REJECTIONS, authenticationStats.getRejectedAttempts())
.put(ENROLLMENT_NOTIFICATIONS, authenticationStats.getEnrollmentNotifications())
+ .put(FACE_LAST_ENROLL_TIME, authenticationStats.getLastEnrollmentTime())
+ .put(FACE_LAST_FRR_NOTIFICATION_TIME,
+ authenticationStats.getLastFrrNotificationTime())
.toString();
} else if (authenticationStats.getModality() == BiometricsProtoEnums.MODALITY_FINGERPRINT) {
return new JSONObject()
@@ -284,6 +321,9 @@ public class AuthenticationStatsPersisterTest {
.put(FINGERPRINT_ATTEMPTS, authenticationStats.getTotalAttempts())
.put(FINGERPRINT_REJECTIONS, authenticationStats.getRejectedAttempts())
.put(ENROLLMENT_NOTIFICATIONS, authenticationStats.getEnrollmentNotifications())
+ .put(FINGERPRINT_LAST_ENROLL_TIME, authenticationStats.getLastEnrollmentTime())
+ .put(FINGERPRINT_LAST_FRR_NOTIFICATION_TIME,
+ authenticationStats.getLastFrrNotificationTime())
.toString();
}
return "";
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsTest.java
index e8e72cb81838..ca7b83caf20f 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsTest.java
@@ -27,6 +27,7 @@ public class AuthenticationStatsTest {
AuthenticationStats authenticationStats =
new AuthenticationStats(1 /* userId */ , 0 /* totalAttempts */,
0 /* rejectedAttempts */, 0 /* enrollmentNotifications */,
+ 100L /* lastEnrollmentTime */, 200L /* lastFrrNotificationTime */,
0 /* modality */);
authenticationStats.authenticate(true /* authenticated */);
@@ -38,5 +39,8 @@ public class AuthenticationStatsTest {
assertEquals(authenticationStats.getTotalAttempts(), 2);
assertEquals(authenticationStats.getRejectedAttempts(), 1);
+
+ assertEquals(authenticationStats.getLastEnrollmentTime(), 100L);
+ assertEquals(authenticationStats.getLastFrrNotificationTime(), 200L);
}
}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java
index 276da39615af..c000f2f4c7a4 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClientTest.java
@@ -19,6 +19,8 @@ package com.android.server.biometrics.sensors.face.aidl;
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_START;
import static android.hardware.biometrics.BiometricFaceConstants.FACE_ACQUIRED_TOO_DARK;
+import static com.android.server.biometrics.AuthenticationStatsCollector.ACTION_LAST_ENROLL_TIME_CHANGED;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -33,6 +35,10 @@ import static org.mockito.Mockito.same;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.hardware.biometrics.BiometricFaceConstants;
import android.hardware.biometrics.BiometricRequestConstants;
import android.hardware.biometrics.BiometricSourceType;
@@ -56,6 +62,7 @@ import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.internal.R;
+import com.android.server.biometrics.AuthenticationStatsCollector;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.log.OperationContextExt;
@@ -74,6 +81,8 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@Presubmit
@@ -200,6 +209,26 @@ public class FaceEnrollClientTest {
eq(BiometricsProtoEnums.ENROLLMENT_SOURCE_SUW), eq(1));
}
+ @Test
+ public void testEnrollWithBroadcastEnrollTime() throws RemoteException, InterruptedException {
+ final FaceEnrollClient client = createClient(4);
+ final CountDownLatch countDownLatch = new CountDownLatch(1);
+ final EnrollmentTimeReceiver receiver = new EnrollmentTimeReceiver(countDownLatch);
+ mContext.registerReceiver(receiver, new IntentFilter(ACTION_LAST_ENROLL_TIME_CHANGED),
+ Context.RECEIVER_NOT_EXPORTED);
+
+ client.start(mCallback);
+ client.onEnrollResult(new Face("face", 1 /* faceId */, 20 /* deviceId */), 0);
+
+ assertThat(countDownLatch.await(2, TimeUnit.SECONDS)).isTrue();
+ final Intent intent = receiver.mIntent;
+ assertThat(intent).isNotNull();
+ assertThat(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)).isEqualTo(USER_ID);
+ assertThat(intent.getIntExtra(AuthenticationStatsCollector.EXTRA_MODALITY,
+ BiometricsProtoEnums.MODALITY_UNKNOWN))
+ .isEqualTo(BiometricsProtoEnums.MODALITY_FACE);
+ }
+
private FaceEnrollClient createClient() throws RemoteException {
return createClient(200 /* version */);
}
@@ -295,4 +324,18 @@ public class FaceEnrollClientTest {
);
}
+ static final class EnrollmentTimeReceiver extends BroadcastReceiver {
+ final CountDownLatch mLatch;
+ Intent mIntent;
+
+ EnrollmentTimeReceiver(CountDownLatch latch) {
+ mLatch = latch;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mIntent = intent;
+ mLatch.countDown();
+ }
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
index ea96d193c762..4d6fd7ef02c4 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClientTest.java
@@ -20,6 +20,8 @@ import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPR
import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_TOO_FAST;
import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_TIMEOUT;
+import static com.android.server.biometrics.AuthenticationStatsCollector.ACTION_LAST_ENROLL_TIME_CHANGED;
+
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -34,6 +36,10 @@ import static org.mockito.Mockito.same;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.hardware.biometrics.BiometricRequestConstants;
import android.hardware.biometrics.BiometricSourceType;
import android.hardware.biometrics.BiometricsProtoEnums;
@@ -62,6 +68,7 @@ import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.internal.R;
+import com.android.server.biometrics.AuthenticationStatsCollector;
import com.android.server.biometrics.log.BiometricContext;
import com.android.server.biometrics.log.BiometricLogger;
import com.android.server.biometrics.log.CallbackWithProbe;
@@ -83,6 +90,8 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@Presubmit
@@ -312,6 +321,27 @@ public class FingerprintEnrollClientTest {
eq(BiometricsProtoEnums.ENROLLMENT_SOURCE_SUW), eq(1));
}
+ @Test
+ public void testEnrollWithBroadcastEnrollTime() throws RemoteException, InterruptedException {
+ final FingerprintEnrollClient client = createClient(4);
+ final CountDownLatch countDownLatch = new CountDownLatch(1);
+ final EnrollmentTimeReceiver receiver = new EnrollmentTimeReceiver(countDownLatch);
+ mContext.registerReceiver(receiver, new IntentFilter(ACTION_LAST_ENROLL_TIME_CHANGED),
+ Context.RECEIVER_NOT_EXPORTED);
+
+ client.start(mCallback);
+ client.onEnrollResult(new Fingerprint("fingerprint", 1 /* fingerId */, 20 /* deviceId */),
+ 0);
+
+ assertThat(countDownLatch.await(2, TimeUnit.SECONDS)).isTrue();
+ final Intent intent = receiver.mIntent;
+ assertThat(intent).isNotNull();
+ assertThat(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)).isEqualTo(0);
+ assertThat(intent.getIntExtra(AuthenticationStatsCollector.EXTRA_MODALITY,
+ BiometricsProtoEnums.MODALITY_UNKNOWN))
+ .isEqualTo(BiometricsProtoEnums.MODALITY_FINGERPRINT);
+ }
+
private void showHideOverlay(
Consumer<FingerprintEnrollClient> block) throws RemoteException {
final FingerprintEnrollClient client = createClient();
@@ -409,4 +439,19 @@ public class FingerprintEnrollClientTest {
.setEnrollReason(ENROLL_SOURCE).build()
);
}
+
+ static final class EnrollmentTimeReceiver extends BroadcastReceiver {
+ final CountDownLatch mLatch;
+ Intent mIntent;
+
+ EnrollmentTimeReceiver(CountDownLatch latch) {
+ mLatch = latch;
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mIntent = intent;
+ mLatch.countDown();
+ }
+ }
}