diff options
597 files changed, 17954 insertions, 6739 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 2f843f9d6164..8547ec164084 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -1809,12 +1809,25 @@ aconfig_declarations { name: "aconfig_settingslib_flags", package: "com.android.settingslib.flags", container: "system", + exportable: true, srcs: [ "packages/SettingsLib/aconfig/settingslib.aconfig", ], } java_aconfig_library { + name: "aconfig_settingslib_exported_flags_java_lib", + aconfig_declarations: "aconfig_settingslib_flags", + defaults: ["framework-minus-apex-aconfig-java-defaults"], + mode: "exported", + min_sdk_version: "30", + apex_available: [ + "//apex_available:platform", + "com.android.permission", + ], +} + +java_aconfig_library { name: "aconfig_settingslib_flags_java_lib", aconfig_declarations: "aconfig_settingslib_flags", defaults: ["framework-minus-apex-aconfig-java-defaults"], diff --git a/MEMORY_OWNERS b/MEMORY_OWNERS index 89ce5140d8ea..12aa2951bbc9 100644 --- a/MEMORY_OWNERS +++ b/MEMORY_OWNERS @@ -2,5 +2,4 @@ surenb@google.com tjmercier@google.com kaleshsingh@google.com jyescas@google.com -carlosgalo@google.com jji@google.com diff --git a/apct-tests/perftests/core/src/android/libcore/ZipFilePerfTest.java b/apct-tests/perftests/core/src/android/libcore/ZipFilePerfTest.java index c77528021201..ed669beae1ce 100644 --- a/apct-tests/perftests/core/src/android/libcore/ZipFilePerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/ZipFilePerfTest.java @@ -63,12 +63,14 @@ public class ZipFilePerfTest { @Test @Parameters(method = "getData") - public void timeZipFileOpenClose(int numEntries) throws Exception { + public void timeZipFileOpen(int numEntries) throws Exception { setUp(numEntries); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); while (state.keepRunning()) { ZipFile zf = new ZipFile(mFile); + state.pauseTiming(); zf.close(); + state.resumeTiming(); } } diff --git a/api/api.go b/api/api.go index e4d783eba4c3..cbdb7e81ab86 100644 --- a/api/api.go +++ b/api/api.go @@ -105,7 +105,7 @@ func (a *CombinedApis) DepsMutator(ctx android.BottomUpMutatorContext) { func (a *CombinedApis) GenerateAndroidBuildActions(ctx android.ModuleContext) { ctx.WalkDeps(func(child, parent android.Module) bool { - if _, ok := child.(java.AndroidLibraryDependency); ok && child.Name() != "framework-res" { + if _, ok := android.OtherModuleProvider(ctx, child, java.AndroidLibraryInfoProvider); ok && child.Name() != "framework-res" { // Stubs of BCP and SSCP libraries should not have any dependencies on apps // This check ensures that we do not run into circular dependencies when UNBUNDLED_BUILD_TARGET_SDK_WITH_API_FINGERPRINT=true ctx.ModuleErrorf( diff --git a/core/api/current.txt b/core/api/current.txt index 32507dfef9b6..7c56a5811abb 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -9190,37 +9190,37 @@ package android.app.blob { package android.app.jank { @FlaggedApi("android.app.jank.detailed_app_jank_metrics_api") public final class AppJankStats { - ctor public AppJankStats(int, @NonNull String, @Nullable String, @Nullable String, long, long, @NonNull android.app.jank.FrameOverrunHistogram); - method @NonNull public android.app.jank.FrameOverrunHistogram getFrameOverrunHistogram(); + ctor public AppJankStats(int, @NonNull String, @Nullable String, @Nullable String, long, long, @NonNull android.app.jank.RelativeFrameTimeHistogram); method public long getJankyFrameCount(); + method @NonNull public android.app.jank.RelativeFrameTimeHistogram getRelativeFrameTimeHistogram(); method public long getTotalFrameCount(); method public int getUid(); method @NonNull public String getWidgetCategory(); method @NonNull public String getWidgetId(); method @NonNull public String getWidgetState(); - field public static final String ANIMATING = "animating"; - field public static final String ANIMATION = "animation"; - field public static final String DRAGGING = "dragging"; - field public static final String FLINGING = "flinging"; - field public static final String KEYBOARD = "keyboard"; - field public static final String MEDIA = "media"; - field public static final String NAVIGATION = "navigation"; - field public static final String NONE = "none"; - field public static final String OTHER = "other"; - field public static final String PLAYBACK = "playback"; - field public static final String PREDICTIVE_BACK = "predictive_back"; - field public static final String SCROLL = "scroll"; - field public static final String SCROLLING = "scrolling"; - field public static final String SWIPING = "swiping"; - field public static final String TAPPING = "tapping"; - field public static final String WIDGET_CATEGORY_UNSPECIFIED = "widget_category_unspecified"; - field public static final String WIDGET_STATE_UNSPECIFIED = "widget_state_unspecified"; - field public static final String ZOOMING = "zooming"; - } - - @FlaggedApi("android.app.jank.detailed_app_jank_metrics_api") public class FrameOverrunHistogram { - ctor public FrameOverrunHistogram(); - method public void addFrameOverrunMillis(int); + field public static final String WIDGET_CATEGORY_ANIMATION = "animation"; + field public static final String WIDGET_CATEGORY_KEYBOARD = "keyboard"; + field public static final String WIDGET_CATEGORY_MEDIA = "media"; + field public static final String WIDGET_CATEGORY_NAVIGATION = "navigation"; + field public static final String WIDGET_CATEGORY_OTHER = "other"; + field public static final String WIDGET_CATEGORY_SCROLL = "scroll"; + field public static final String WIDGET_CATEGORY_UNSPECIFIED = "unspecified"; + field public static final String WIDGET_STATE_ANIMATING = "animating"; + field public static final String WIDGET_STATE_DRAGGING = "dragging"; + field public static final String WIDGET_STATE_FLINGING = "flinging"; + field public static final String WIDGET_STATE_NONE = "none"; + field public static final String WIDGET_STATE_PLAYBACK = "playback"; + field public static final String WIDGET_STATE_PREDICTIVE_BACK = "predictive_back"; + field public static final String WIDGET_STATE_SCROLLING = "scrolling"; + field public static final String WIDGET_STATE_SWIPING = "swiping"; + field public static final String WIDGET_STATE_TAPPING = "tapping"; + field public static final String WIDGET_STATE_UNSPECIFIED = "unspecified"; + field public static final String WIDGET_STATE_ZOOMING = "zooming"; + } + + @FlaggedApi("android.app.jank.detailed_app_jank_metrics_api") public class RelativeFrameTimeHistogram { + ctor public RelativeFrameTimeHistogram(); + method public void addRelativeFrameTimeMillis(int); method @NonNull public int[] getBucketCounters(); method @NonNull public int[] getBucketEndpointsMillis(); } @@ -42521,7 +42521,7 @@ package android.service.settings.preferences { field @NonNull public static final android.os.Parcelable.Creator<android.service.settings.preferences.GetValueRequest> CREATOR; } - public static final class GetValueRequest.Builder { + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public static final class GetValueRequest.Builder { ctor public GetValueRequest.Builder(@NonNull String, @NonNull String); method @NonNull public android.service.settings.preferences.GetValueRequest build(); } @@ -42542,7 +42542,7 @@ package android.service.settings.preferences { field public static final int RESULT_UNSUPPORTED = 1; // 0x1 } - public static final class GetValueResult.Builder { + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public static final class GetValueResult.Builder { ctor public GetValueResult.Builder(int); method @NonNull public android.service.settings.preferences.GetValueResult build(); method @NonNull public android.service.settings.preferences.GetValueResult.Builder setMetadata(@Nullable android.service.settings.preferences.SettingsPreferenceMetadata); @@ -42555,7 +42555,7 @@ package android.service.settings.preferences { field @NonNull public static final android.os.Parcelable.Creator<android.service.settings.preferences.MetadataRequest> CREATOR; } - public static final class MetadataRequest.Builder { + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public static final class MetadataRequest.Builder { ctor public MetadataRequest.Builder(); method @NonNull public android.service.settings.preferences.MetadataRequest build(); } @@ -42571,7 +42571,7 @@ package android.service.settings.preferences { field public static final int RESULT_UNSUPPORTED = 1; // 0x1 } - public static final class MetadataResult.Builder { + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public static final class MetadataResult.Builder { ctor public MetadataResult.Builder(int); method @NonNull public android.service.settings.preferences.MetadataResult build(); method @NonNull public android.service.settings.preferences.MetadataResult.Builder setMetadataList(@NonNull java.util.List<android.service.settings.preferences.SettingsPreferenceMetadata>); @@ -42586,7 +42586,7 @@ package android.service.settings.preferences { field @NonNull public static final android.os.Parcelable.Creator<android.service.settings.preferences.SetValueRequest> CREATOR; } - public static final class SetValueRequest.Builder { + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public static final class SetValueRequest.Builder { ctor public SetValueRequest.Builder(@NonNull String, @NonNull String, @NonNull android.service.settings.preferences.SettingsPreferenceValue); method @NonNull public android.service.settings.preferences.SetValueRequest build(); } @@ -42608,14 +42608,13 @@ package android.service.settings.preferences { field public static final int RESULT_UNSUPPORTED = 1; // 0x1 } - public static final class SetValueResult.Builder { + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public static final class SetValueResult.Builder { ctor public SetValueResult.Builder(int); method @NonNull public android.service.settings.preferences.SetValueResult build(); } @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public final class SettingsPreferenceMetadata implements android.os.Parcelable { method public int describeContents(); - method @NonNull public java.util.List<java.lang.String> getBreadcrumbs(); method @NonNull public android.os.Bundle getExtras(); method @NonNull public String getKey(); method @Nullable public android.content.Intent getLaunchIntent(); @@ -42631,17 +42630,16 @@ package android.service.settings.preferences { method public boolean isWritable(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.service.settings.preferences.SettingsPreferenceMetadata> CREATOR; + field public static final int DEEPLINK_ONLY = 2; // 0x2 field public static final int EXPECT_POST_CONFIRMATION = 1; // 0x1 - field public static final int EXPECT_PRE_CONFIRMATION = 2; // 0x2 field public static final int NO_DIRECT_ACCESS = 3; // 0x3 field public static final int NO_SENSITIVITY = 0; // 0x0 } - public static final class SettingsPreferenceMetadata.Builder { + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public static final class SettingsPreferenceMetadata.Builder { ctor public SettingsPreferenceMetadata.Builder(@NonNull String, @NonNull String); method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata build(); method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setAvailable(boolean); - method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setBreadcrumbs(@NonNull java.util.List<java.lang.String>); method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setEnabled(boolean); method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setExtras(@NonNull android.os.Bundle); method @NonNull public android.service.settings.preferences.SettingsPreferenceMetadata.Builder setLaunchIntent(@Nullable android.content.Intent); @@ -42688,7 +42686,7 @@ package android.service.settings.preferences { field public static final int TYPE_STRING = 3; // 0x3 } - public static final class SettingsPreferenceValue.Builder { + @FlaggedApi("com.android.settingslib.flags.settings_catalyst") public static final class SettingsPreferenceValue.Builder { ctor public SettingsPreferenceValue.Builder(int); method @NonNull public android.service.settings.preferences.SettingsPreferenceValue build(); method @NonNull public android.service.settings.preferences.SettingsPreferenceValue.Builder setBooleanValue(boolean); @@ -53484,9 +53482,9 @@ package android.view { field public static final int CHANGE_FRAME_RATE_ALWAYS = 1; // 0x1 field public static final int CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS = 0; // 0x0 field @NonNull public static final android.os.Parcelable.Creator<android.view.Surface> CREATOR; + field @FlaggedApi("com.android.graphics.surfaceflinger.flags.arr_setframerate_gte_enum") public static final int FRAME_RATE_COMPATIBILITY_AT_LEAST = 2; // 0x2 field public static final int FRAME_RATE_COMPATIBILITY_DEFAULT = 0; // 0x0 field public static final int FRAME_RATE_COMPATIBILITY_FIXED_SOURCE = 1; // 0x1 - field @FlaggedApi("com.android.graphics.surfaceflinger.flags.arr_setframerate_gte_enum") public static final int FRAME_RATE_COMPATIBILITY_GTE = 2; // 0x2 field public static final int ROTATION_0 = 0; // 0x0 field public static final int ROTATION_180 = 2; // 0x2 field public static final int ROTATION_270 = 3; // 0x3 @@ -57120,6 +57118,7 @@ package android.view.contentcapture { method public void close(); method @NonNull public final android.view.contentcapture.ContentCaptureSession createContentCaptureSession(@NonNull android.view.contentcapture.ContentCaptureContext); method public final void destroy(); + method @FlaggedApi("android.view.contentcapture.flags.ccapi_baklava_enabled") public void flush(); method @Nullable public final android.view.contentcapture.ContentCaptureContext getContentCaptureContext(); method @NonNull public final android.view.contentcapture.ContentCaptureSessionId getContentCaptureSessionId(); method @NonNull public android.view.autofill.AutofillId newAutofillId(@NonNull android.view.autofill.AutofillId, long); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 6d24e4684e77..eca8eddc918e 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -360,6 +360,7 @@ package android { field @Deprecated public static final String REQUEST_NETWORK_SCORES = "android.permission.REQUEST_NETWORK_SCORES"; field public static final String REQUEST_NOTIFICATION_ASSISTANT_SERVICE = "android.permission.REQUEST_NOTIFICATION_ASSISTANT_SERVICE"; field public static final String RESET_PASSWORD = "android.permission.RESET_PASSWORD"; + field @FlaggedApi("android.content.pm.uid_based_provider_lookup") public static final String RESOLVE_COMPONENT_FOR_UID = "android.permission.RESOLVE_COMPONENT_FOR_UID"; field public static final String RESTART_WIFI_SUBSYSTEM = "android.permission.RESTART_WIFI_SUBSYSTEM"; field @FlaggedApi("android.permission.flags.health_connect_backup_restore_permission_enabled") public static final String RESTORE_HEALTH_CONNECT_DATA_AND_SETTINGS = "android.permission.RESTORE_HEALTH_CONNECT_DATA_AND_SETTINGS"; field public static final String RESTORE_RUNTIME_PERMISSIONS = "android.permission.RESTORE_RUNTIME_PERMISSIONS"; @@ -4241,6 +4242,7 @@ package android.content.pm { method public abstract void registerDexModule(@NonNull String, @Nullable android.content.pm.PackageManager.DexModuleRegisterCallback); method @RequiresPermission("android.permission.OBSERVE_GRANT_REVOKE_PERMISSIONS") public abstract void removeOnPermissionsChangeListener(@NonNull android.content.pm.PackageManager.OnPermissionsChangedListener); method public void replacePreferredActivity(@NonNull android.content.IntentFilter, int, @NonNull java.util.List<android.content.ComponentName>, @NonNull android.content.ComponentName); + method @FlaggedApi("android.content.pm.uid_based_provider_lookup") @Nullable @RequiresPermission(android.Manifest.permission.RESOLVE_COMPONENT_FOR_UID) public android.content.pm.ProviderInfo resolveContentProviderForUid(@NonNull String, @NonNull android.content.pm.PackageManager.ComponentInfoFlags, int); method @RequiresPermission(android.Manifest.permission.REVOKE_RUNTIME_PERMISSIONS) public abstract void revokeRuntimePermission(@NonNull String, @NonNull String, @NonNull android.os.UserHandle); method @RequiresPermission(android.Manifest.permission.REVOKE_RUNTIME_PERMISSIONS) public void revokeRuntimePermission(@NonNull String, @NonNull String, @NonNull android.os.UserHandle, @NonNull String); method public void sendDeviceCustomizationReadyBroadcast(); @@ -5079,8 +5081,8 @@ package android.hardware.contexthub { } @FlaggedApi("android.chre.flags.offload_api") public class HubEndpoint { - method @Nullable public android.hardware.contexthub.IHubEndpointLifecycleCallback getLifecycleCallback(); - method @Nullable public android.hardware.contexthub.IHubEndpointMessageCallback getMessageCallback(); + method @Nullable public android.hardware.contexthub.HubEndpointLifecycleCallback getLifecycleCallback(); + method @Nullable public android.hardware.contexthub.HubEndpointMessageCallback getMessageCallback(); method @NonNull public java.util.Collection<android.hardware.contexthub.HubServiceInfo> getServiceInfoCollection(); method @Nullable public String getTag(); method public int getVersion(); @@ -5095,14 +5097,19 @@ package android.hardware.contexthub { public static final class HubEndpoint.Builder { ctor public HubEndpoint.Builder(@NonNull android.content.Context); method @NonNull public android.hardware.contexthub.HubEndpoint build(); - method @NonNull public android.hardware.contexthub.HubEndpoint.Builder setLifecycleCallback(@NonNull android.hardware.contexthub.IHubEndpointLifecycleCallback); - method @NonNull public android.hardware.contexthub.HubEndpoint.Builder setLifecycleCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.contexthub.IHubEndpointLifecycleCallback); - method @NonNull public android.hardware.contexthub.HubEndpoint.Builder setMessageCallback(@NonNull android.hardware.contexthub.IHubEndpointMessageCallback); - method @NonNull public android.hardware.contexthub.HubEndpoint.Builder setMessageCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.contexthub.IHubEndpointMessageCallback); + method @NonNull public android.hardware.contexthub.HubEndpoint.Builder setLifecycleCallback(@NonNull android.hardware.contexthub.HubEndpointLifecycleCallback); + method @NonNull public android.hardware.contexthub.HubEndpoint.Builder setLifecycleCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.contexthub.HubEndpointLifecycleCallback); + method @NonNull public android.hardware.contexthub.HubEndpoint.Builder setMessageCallback(@NonNull android.hardware.contexthub.HubEndpointMessageCallback); + method @NonNull public android.hardware.contexthub.HubEndpoint.Builder setMessageCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.contexthub.HubEndpointMessageCallback); method @NonNull public android.hardware.contexthub.HubEndpoint.Builder setServiceInfoCollection(@NonNull java.util.Collection<android.hardware.contexthub.HubServiceInfo>); method @NonNull public android.hardware.contexthub.HubEndpoint.Builder setTag(@NonNull String); } + @FlaggedApi("android.chre.flags.offload_api") public interface HubEndpointDiscoveryCallback { + method public void onEndpointsStarted(@NonNull java.util.List<android.hardware.contexthub.HubDiscoveryInfo>); + method public void onEndpointsStopped(@NonNull java.util.List<android.hardware.contexthub.HubDiscoveryInfo>, int); + } + @FlaggedApi("android.chre.flags.offload_api") public final class HubEndpointInfo implements android.os.Parcelable { method public int describeContents(); method @NonNull public android.hardware.contexthub.HubEndpointInfo.HubEndpointIdentifier getIdentifier(); @@ -5126,6 +5133,16 @@ package android.hardware.contexthub { method public long getHub(); } + @FlaggedApi("android.chre.flags.offload_api") public interface HubEndpointLifecycleCallback { + method public void onSessionClosed(@NonNull android.hardware.contexthub.HubEndpointSession, int); + method @NonNull public android.hardware.contexthub.HubEndpointSessionResult onSessionOpenRequest(@NonNull android.hardware.contexthub.HubEndpointInfo, @Nullable String); + method public void onSessionOpened(@NonNull android.hardware.contexthub.HubEndpointSession); + } + + @FlaggedApi("android.chre.flags.offload_api") public interface HubEndpointMessageCallback { + method public void onMessageReceived(@NonNull android.hardware.contexthub.HubEndpointSession, @NonNull android.hardware.contexthub.HubMessage); + } + @FlaggedApi("android.chre.flags.offload_api") public class HubEndpointSession implements java.lang.AutoCloseable { method @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void close(); method @Nullable public String getServiceDescriptor(); @@ -5174,21 +5191,6 @@ package android.hardware.contexthub { method @NonNull public android.hardware.contexthub.HubServiceInfo build(); } - @FlaggedApi("android.chre.flags.offload_api") public interface IHubEndpointDiscoveryCallback { - method public void onEndpointsStarted(@NonNull java.util.List<android.hardware.contexthub.HubDiscoveryInfo>); - method public void onEndpointsStopped(@NonNull java.util.List<android.hardware.contexthub.HubDiscoveryInfo>, int); - } - - @FlaggedApi("android.chre.flags.offload_api") public interface IHubEndpointLifecycleCallback { - method public void onSessionClosed(@NonNull android.hardware.contexthub.HubEndpointSession, int); - method @NonNull public android.hardware.contexthub.HubEndpointSessionResult onSessionOpenRequest(@NonNull android.hardware.contexthub.HubEndpointInfo, @Nullable String); - method public void onSessionOpened(@NonNull android.hardware.contexthub.HubEndpointSession); - } - - @FlaggedApi("android.chre.flags.offload_api") public interface IHubEndpointMessageCallback { - method public void onMessageReceived(@NonNull android.hardware.contexthub.HubEndpointSession, @NonNull android.hardware.contexthub.HubMessage); - } - } package android.hardware.devicestate { @@ -6192,16 +6194,16 @@ package android.hardware.location { method @Deprecated public int registerCallback(@NonNull android.hardware.location.ContextHubManager.Callback); method @Deprecated public int registerCallback(android.hardware.location.ContextHubManager.Callback, android.os.Handler); method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void registerEndpoint(@NonNull android.hardware.contexthub.HubEndpoint); - method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void registerEndpointDiscoveryCallback(long, @NonNull android.hardware.contexthub.IHubEndpointDiscoveryCallback); - method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void registerEndpointDiscoveryCallback(long, @NonNull android.hardware.contexthub.IHubEndpointDiscoveryCallback, @NonNull java.util.concurrent.Executor); - method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void registerEndpointDiscoveryCallback(@NonNull String, @NonNull android.hardware.contexthub.IHubEndpointDiscoveryCallback); - method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void registerEndpointDiscoveryCallback(@NonNull String, @NonNull android.hardware.contexthub.IHubEndpointDiscoveryCallback, @NonNull java.util.concurrent.Executor); + method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void registerEndpointDiscoveryCallback(@NonNull android.hardware.contexthub.HubEndpointDiscoveryCallback, long); + method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void registerEndpointDiscoveryCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.contexthub.HubEndpointDiscoveryCallback, long); + method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void registerEndpointDiscoveryCallback(@NonNull android.hardware.contexthub.HubEndpointDiscoveryCallback, @NonNull String); + method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void registerEndpointDiscoveryCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.contexthub.HubEndpointDiscoveryCallback, @NonNull String); method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public int sendMessage(int, int, @NonNull android.hardware.location.ContextHubMessage); method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public int unloadNanoApp(int); method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public android.hardware.location.ContextHubTransaction<java.lang.Void> unloadNanoApp(@NonNull android.hardware.location.ContextHubInfo, long); method @Deprecated public int unregisterCallback(@NonNull android.hardware.location.ContextHubManager.Callback); method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void unregisterEndpoint(@NonNull android.hardware.contexthub.HubEndpoint); - method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void unregisterEndpointDiscoveryCallback(@NonNull android.hardware.contexthub.IHubEndpointDiscoveryCallback); + method @FlaggedApi("android.chre.flags.offload_api") @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) public void unregisterEndpointDiscoveryCallback(@NonNull android.hardware.contexthub.HubEndpointDiscoveryCallback); field public static final int AUTHORIZATION_DENIED = 0; // 0x0 field public static final int AUTHORIZATION_DENIED_GRACE_PERIOD = 1; // 0x1 field public static final int AUTHORIZATION_GRANTED = 2; // 0x2 @@ -19091,6 +19093,7 @@ package android.view.contentcapture { method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.view.contentcapture.ContentCaptureEvent> CREATOR; field public static final int TYPE_CONTEXT_UPDATED = 6; // 0x6 + field @FlaggedApi("android.view.contentcapture.flags.ccapi_baklava_enabled") public static final int TYPE_SESSION_FLUSH = 11; // 0xb field public static final int TYPE_SESSION_PAUSED = 8; // 0x8 field public static final int TYPE_SESSION_RESUMED = 7; // 0x7 field public static final int TYPE_VIEW_APPEARED = 1; // 0x1 diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index fee8cdb1ce51..c3ef104075f2 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -5834,7 +5834,11 @@ public class Activity extends ContextThemeWrapper final int size = permissions.length; int[] results = new int[size]; for (int i = 0; i < size; i++) { - results[i] = deviceContext.getPermissionRequestState(permissions[i]); + if (permissions[i] == null) { + results[i] = Context.PERMISSION_REQUEST_STATE_UNREQUESTABLE; + } else { + results[i] = deviceContext.getPermissionRequestState(permissions[i]); + } } return results; } diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index abdfb53537f8..999db18a1229 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -485,6 +485,11 @@ public abstract class ActivityManagerInternal { */ public static final int OOM_ADJ_REASON_FOLLOW_UP = 23; + /** + * Oom Adj Reason: Update after oom adjuster configuration has changed. + */ + public static final int OOM_ADJ_REASON_RECONFIGURATION = 24; + @IntDef(prefix = {"OOM_ADJ_REASON_"}, value = { OOM_ADJ_REASON_NONE, OOM_ADJ_REASON_ACTIVITY, @@ -510,6 +515,7 @@ public abstract class ActivityManagerInternal { OOM_ADJ_REASON_RESTRICTION_CHANGE, OOM_ADJ_REASON_COMPONENT_DISABLED, OOM_ADJ_REASON_FOLLOW_UP, + OOM_ADJ_REASON_RECONFIGURATION, }) @Retention(RetentionPolicy.SOURCE) public @interface OomAdjReason {} diff --git a/core/java/android/app/AppCompatTaskInfo.java b/core/java/android/app/AppCompatTaskInfo.java index 61b56877589b..599f1a8be233 100644 --- a/core/java/android/app/AppCompatTaskInfo.java +++ b/core/java/android/app/AppCompatTaskInfo.java @@ -27,6 +27,7 @@ import android.os.Parcelable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Objects; /** * Stores App Compat information about a particular Task. @@ -58,16 +59,11 @@ public class AppCompatTaskInfo implements Parcelable { public int topActivityLetterboxHeight = PROPERTY_VALUE_UNSET; /** - * Contains the current app height of the letterboxed activity if available or - * {@link TaskInfo#PROPERTY_VALUE_UNSET} otherwise. + * Contains the app bounds of the top activity or size compat mode + * bounds when in size compat mode. If null, contains bounds. */ - public int topActivityLetterboxAppHeight = PROPERTY_VALUE_UNSET; - - /** - * Contains the current app width of the letterboxed activity if available or - * {@link TaskInfo#PROPERTY_VALUE_UNSET} otherwise. - */ - public int topActivityLetterboxAppWidth = PROPERTY_VALUE_UNSET; + @NonNull + public final Rect topActivityAppBounds = new Rect(); /** * Contains the top activity bounds when the activity is letterboxed. @@ -350,8 +346,7 @@ public class AppCompatTaskInfo implements Parcelable { && topActivityLetterboxVerticalPosition == that.topActivityLetterboxVerticalPosition && topActivityLetterboxWidth == that.topActivityLetterboxWidth && topActivityLetterboxHeight == that.topActivityLetterboxHeight - && topActivityLetterboxAppWidth == that.topActivityLetterboxAppWidth - && topActivityLetterboxAppHeight == that.topActivityLetterboxAppHeight + && topActivityAppBounds.equals(that.topActivityAppBounds) && topActivityLetterboxHorizontalPosition == that.topActivityLetterboxHorizontalPosition && cameraCompatTaskInfo.equalsForTaskOrganizer(that.cameraCompatTaskInfo); @@ -371,8 +366,7 @@ public class AppCompatTaskInfo implements Parcelable { == that.topActivityLetterboxHorizontalPosition && topActivityLetterboxWidth == that.topActivityLetterboxWidth && topActivityLetterboxHeight == that.topActivityLetterboxHeight - && topActivityLetterboxAppWidth == that.topActivityLetterboxAppWidth - && topActivityLetterboxAppHeight == that.topActivityLetterboxAppHeight + && topActivityAppBounds.equals(that.topActivityAppBounds) && cameraCompatTaskInfo.equalsForCompatUi(that.cameraCompatTaskInfo); } @@ -385,8 +379,7 @@ public class AppCompatTaskInfo implements Parcelable { topActivityLetterboxHorizontalPosition = source.readInt(); topActivityLetterboxWidth = source.readInt(); topActivityLetterboxHeight = source.readInt(); - topActivityLetterboxAppWidth = source.readInt(); - topActivityLetterboxAppHeight = source.readInt(); + topActivityAppBounds.set(Objects.requireNonNull(source.readTypedObject(Rect.CREATOR))); topActivityLetterboxBounds = source.readTypedObject(Rect.CREATOR); cameraCompatTaskInfo = source.readTypedObject(CameraCompatTaskInfo.CREATOR); } @@ -401,8 +394,7 @@ public class AppCompatTaskInfo implements Parcelable { dest.writeInt(topActivityLetterboxHorizontalPosition); dest.writeInt(topActivityLetterboxWidth); dest.writeInt(topActivityLetterboxHeight); - dest.writeInt(topActivityLetterboxAppWidth); - dest.writeInt(topActivityLetterboxAppHeight); + dest.writeTypedObject(topActivityAppBounds, flags); dest.writeTypedObject(topActivityLetterboxBounds, flags); dest.writeTypedObject(cameraCompatTaskInfo, flags); } @@ -421,8 +413,7 @@ public class AppCompatTaskInfo implements Parcelable { + topActivityLetterboxHorizontalPosition + " topActivityLetterboxWidth=" + topActivityLetterboxWidth + " topActivityLetterboxHeight=" + topActivityLetterboxHeight - + " topActivityLetterboxAppWidth=" + topActivityLetterboxAppWidth - + " topActivityLetterboxAppHeight=" + topActivityLetterboxAppHeight + + " topActivityAppBounds=" + topActivityAppBounds + " isUserFullscreenOverrideEnabled=" + isUserFullscreenOverrideEnabled() + " isSystemFullscreenOverrideEnabled=" + isSystemFullscreenOverrideEnabled() + " hasMinAspectRatioOverride=" + hasMinAspectRatioOverride() diff --git a/core/java/android/app/AppOpsManager.aidl b/core/java/android/app/AppOpsManager.aidl index b4dee2e937cb..56ed290baf2e 100644 --- a/core/java/android/app/AppOpsManager.aidl +++ b/core/java/android/app/AppOpsManager.aidl @@ -19,6 +19,7 @@ package android.app; parcelable AppOpsManager.PackageOps; parcelable AppOpsManager.NoteOpEventProxyInfo; parcelable AppOpsManager.NoteOpEvent; +parcelable AppOpsManager.NotedOp; parcelable AppOpsManager.OpFeatureEntry; parcelable AppOpsManager.OpEntry; diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 19138126698c..53b4b54e9f93 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -262,6 +262,23 @@ public class AppOpsManager { private static final Object sLock = new Object(); + // A map that records noted times for each op. + private static ArrayMap<NotedOp, Integer> sPendingNotedOps = new ArrayMap<>(); + private static HandlerThread sHandlerThread; + private static final int NOTE_OP_BATCHING_DELAY_MILLIS = 1000; + + private boolean isNoteOpBatchingSupported() { + // If noteOp is called from system server no IPC is made, hence we don't need batching. + if (Process.myUid() == Process.SYSTEM_UID) { + return false; + } + return Flags.noteOpBatchingEnabled(); + } + + private static final Object sBatchedNoteOpLock = new Object(); + @GuardedBy("sBatchedNoteOpLock") + private static boolean sIsBatchedNoteOpCallScheduled = false; + /** Current {@link OnOpNotedCallback}. Change via {@link #setOnOpNotedCallback} */ @GuardedBy("sLock") private static @Nullable OnOpNotedCallback sOnOpNotedCallback; @@ -7466,6 +7483,141 @@ public class AppOpsManager { } /** + * A NotedOp is an app op grouped in noteOp API and sent to the system server in a batch + * + * @hide + */ + public static final class NotedOp implements Parcelable { + private final @IntRange(from = 0, to = _NUM_OP - 1) int mOp; + private final @IntRange(from = 0) int mUid; + private final @Nullable String mPackageName; + private final @Nullable String mAttributionTag; + private final int mVirtualDeviceId; + private final @Nullable String mMessage; + private final boolean mShouldCollectAsyncNotedOp; + private final boolean mShouldCollectMessage; + + public NotedOp(int op, int uid, @Nullable String packageName, + @Nullable String attributionTag, int virtualDeviceId, @Nullable String message, + boolean shouldCollectAsyncNotedOp, boolean shouldCollectMessage) { + mOp = op; + mUid = uid; + mPackageName = packageName; + mAttributionTag = attributionTag; + mVirtualDeviceId = virtualDeviceId; + mMessage = message; + mShouldCollectAsyncNotedOp = shouldCollectAsyncNotedOp; + mShouldCollectMessage = shouldCollectMessage; + } + + NotedOp(Parcel source) { + mOp = source.readInt(); + mUid = source.readInt(); + mPackageName = source.readString(); + mAttributionTag = source.readString(); + mVirtualDeviceId = source.readInt(); + mMessage = source.readString(); + mShouldCollectAsyncNotedOp = source.readBoolean(); + mShouldCollectMessage = source.readBoolean(); + } + + public int getOp() { + return mOp; + } + + public int getUid() { + return mUid; + } + + public @Nullable String getPackageName() { + return mPackageName; + } + + public @Nullable String getAttributionTag() { + return mAttributionTag; + } + + public int getVirtualDeviceId() { + return mVirtualDeviceId; + } + + public @Nullable String getMessage() { + return mMessage; + } + + public boolean getShouldCollectAsyncNotedOp() { + return mShouldCollectAsyncNotedOp; + } + + public boolean getShouldCollectMessage() { + return mShouldCollectMessage; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mOp); + dest.writeInt(mUid); + dest.writeString(mPackageName); + dest.writeString(mAttributionTag); + dest.writeInt(mVirtualDeviceId); + dest.writeString(mMessage); + dest.writeBoolean(mShouldCollectAsyncNotedOp); + dest.writeBoolean(mShouldCollectMessage); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NotedOp that = (NotedOp) o; + return mOp == that.mOp + && mUid == that.mUid + && Objects.equals(mPackageName, that.mPackageName) + && Objects.equals(mAttributionTag, that.mAttributionTag) + && mVirtualDeviceId == that.mVirtualDeviceId + && Objects.equals(mMessage, that.mMessage) + && Objects.equals(mShouldCollectAsyncNotedOp, that.mShouldCollectAsyncNotedOp) + && Objects.equals(mShouldCollectMessage, that.mShouldCollectMessage); + } + + @Override + public int hashCode() { + return Objects.hash(mOp, mUid, mPackageName, mAttributionTag, mVirtualDeviceId, + mMessage, mShouldCollectAsyncNotedOp, mShouldCollectMessage); + } + + @Override + public String toString() { + return "NotedOp{" + + "mOp=" + mOp + + ", mUid=" + mUid + + ", mPackageName=" + mPackageName + + ", mAttributionTag=" + mAttributionTag + + ", mVirtualDeviceId=" + mVirtualDeviceId + + ", mMessage=" + mMessage + + ", mShouldCollectAsyncNotedOp=" + mShouldCollectAsyncNotedOp + + ", mShouldCollectMessage=" + mShouldCollectMessage + + "}"; + } + + public static final @NonNull Creator<NotedOp> CREATOR = + new Creator<>() { + @Override public NotedOp createFromParcel(Parcel source) { + return new NotedOp(source); + } + + @Override public NotedOp[] newArray(int size) { + return new NotedOp[size]; + } + }; + } + + /** * Computes the sum of the counts for the given flags in between the begin and * end UID states. * @@ -9301,6 +9453,65 @@ public class AppOpsManager { message); } + /** + * Create a new NotedOp object to represent the note operation. If the note operation is + * a duplicate in the buffer, put it in a batch for an async binder call to the system server. + * + * @return whether this note operation is a duplicate in the buffer. If it's the + * first, the noteOp is not batched, the caller should manually call noteOperation. + */ + private boolean batchDuplicateNoteOps(int op, int uid, @Nullable String packageName, + @Nullable String attributionTag, int virtualDeviceId, @Nullable String message, + boolean collectAsync, boolean shouldCollectMessage) { + synchronized (sBatchedNoteOpLock) { + NotedOp notedOp = new NotedOp(op, uid, packageName, attributionTag, + virtualDeviceId, message, collectAsync, shouldCollectMessage); + + // Batch same noteOp calls and send them with their counters to the system + // service asynchronously. The time window for batching is specified in + // NOTE_OP_BATCHING_DELAY_MILLIS. Always allow the first noteOp call to go + // through binder API. Accumulate subsequent same noteOp calls during the + // time window in sPendingNotedOps. + boolean isDuplicated = sPendingNotedOps.containsKey(notedOp); + if (!isDuplicated) { + sPendingNotedOps.put(notedOp, 0); + } else { + sPendingNotedOps.merge(notedOp, 1, Integer::sum); + } + + if (!sIsBatchedNoteOpCallScheduled) { + if (sHandlerThread == null) { + sHandlerThread = new HandlerThread("AppOpsManagerNoteOpBatching"); + sHandlerThread.start(); + } + + sHandlerThread.getThreadHandler().postDelayed(() -> { + ArrayMap<NotedOp, Integer> pendingNotedOpsCopy; + synchronized(sBatchedNoteOpLock) { + sIsBatchedNoteOpCallScheduled = false; + pendingNotedOpsCopy = sPendingNotedOps; + sPendingNotedOps = new ArrayMap<>(); + } + for (int i = pendingNotedOpsCopy.size() - 1; i >= 0; i--) { + if (pendingNotedOpsCopy.valueAt(i) == 0) { + pendingNotedOpsCopy.removeAt(i); + } + } + if (!pendingNotedOpsCopy.isEmpty()) { + try { + mService.noteOperationsInBatch(pendingNotedOpsCopy); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + }, NOTE_OP_BATCHING_DELAY_MILLIS); + + sIsBatchedNoteOpCallScheduled = true; + } + return isDuplicated; + } + } + private int noteOpNoThrow(int op, int uid, @Nullable String packageName, @Nullable String attributionTag, int virtualDeviceId, @Nullable String message) { try { @@ -9315,15 +9526,34 @@ public class AppOpsManager { } } - SyncNotedAppOp syncOp; - if (virtualDeviceId == Context.DEVICE_ID_DEFAULT) { - syncOp = mService.noteOperation(op, uid, packageName, attributionTag, - collectionMode == COLLECT_ASYNC, message, shouldCollectMessage); - } else { - syncOp = mService.noteOperationForDevice(op, uid, packageName, attributionTag, - virtualDeviceId, collectionMode == COLLECT_ASYNC, message, - shouldCollectMessage); + SyncNotedAppOp syncOp = null; + boolean isNoteOpDuplicated = false; + if (isNoteOpBatchingSupported()) { + int mode = sAppOpModeCache.query( + new AppOpModeQuery(op, uid, packageName, virtualDeviceId, attributionTag, + "noteOpNoThrow")); + // For FOREGROUND mode, we still need to make a binder call to the system service + // to translate it to ALLOWED or IGNORED. So no batching is needed. + if (mode != MODE_FOREGROUND) { + isNoteOpDuplicated = batchDuplicateNoteOps(op, uid, packageName, attributionTag, + virtualDeviceId, message, + collectionMode == COLLECT_ASYNC, shouldCollectMessage); + + syncOp = new SyncNotedAppOp(mode, op, attributionTag, packageName); + } + } + + if (!isNoteOpDuplicated) { + if (virtualDeviceId == Context.DEVICE_ID_DEFAULT) { + syncOp = mService.noteOperation(op, uid, packageName, attributionTag, + collectionMode == COLLECT_ASYNC, message, shouldCollectMessage); + } else { + syncOp = mService.noteOperationForDevice(op, uid, packageName, attributionTag, + virtualDeviceId, collectionMode == COLLECT_ASYNC, message, + shouldCollectMessage); + } } + if (syncOp.getOpMode() == MODE_ALLOWED) { if (collectionMode == COLLECT_SELF) { collectNotedOpForSelf(syncOp); diff --git a/core/java/android/app/AppOpsManagerInternal.java b/core/java/android/app/AppOpsManagerInternal.java index b21defbcc0e3..8b7ea0f8b46a 100644 --- a/core/java/android/app/AppOpsManagerInternal.java +++ b/core/java/android/app/AppOpsManagerInternal.java @@ -29,7 +29,7 @@ import com.android.internal.app.IAppOpsCallback; import com.android.internal.util.function.DodecFunction; import com.android.internal.util.function.HexConsumer; import com.android.internal.util.function.HexFunction; -import com.android.internal.util.function.OctFunction; +import com.android.internal.util.function.NonaFunction; import com.android.internal.util.function.QuadFunction; import com.android.internal.util.function.UndecFunction; @@ -86,9 +86,9 @@ public abstract class AppOpsManagerInternal { */ SyncNotedAppOp noteOperation(int code, int uid, @Nullable String packageName, @Nullable String featureId, int virtualDeviceId, boolean shouldCollectAsyncNotedOp, - @Nullable String message, boolean shouldCollectMessage, - @NonNull OctFunction<Integer, Integer, String, String, Integer, Boolean, String, - Boolean, SyncNotedAppOp> superImpl); + @Nullable String message, boolean shouldCollectMessage, int notedCount, + @NonNull NonaFunction<Integer, Integer, String, String, Integer, Boolean, String, + Boolean, Integer, SyncNotedAppOp> superImpl); /** * Allows overriding note proxy operation behavior. diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index da338474a448..2dead565fa85 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -1751,6 +1751,19 @@ public class ApplicationPackageManager extends PackageManager { } } + /** @hide **/ + @Override + public ProviderInfo resolveContentProviderForUid(@NonNull String authority, + ComponentInfoFlags flags, int callingUid) { + try { + return mPM.resolveContentProviderForUid(authority, + updateFlagsForComponent(flags.getValue(), getUserId(), null), getUserId(), + callingUid); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + @Override public List<ProviderInfo> queryContentProviders(String processName, int uid, int flags) { return queryContentProviders(processName, uid, ComponentInfoFlags.of(flags)); diff --git a/core/java/android/app/BackgroundStartPrivileges.java b/core/java/android/app/BackgroundStartPrivileges.java index 20278eaee3b2..adea0a8a0702 100644 --- a/core/java/android/app/BackgroundStartPrivileges.java +++ b/core/java/android/app/BackgroundStartPrivileges.java @@ -23,12 +23,13 @@ import android.os.IBinder; import com.android.internal.util.Preconditions; import java.util.List; +import java.util.Objects; /** * Privileges granted to a Process that allows it to execute starts from the background. * @hide */ -public class BackgroundStartPrivileges { +public final class BackgroundStartPrivileges { /** No privileges. */ public static final BackgroundStartPrivileges NONE = new BackgroundStartPrivileges( false, false, null); @@ -190,4 +191,22 @@ public class BackgroundStartPrivileges { + ", originatingToken=" + mOriginatingToken + ']'; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BackgroundStartPrivileges that = (BackgroundStartPrivileges) o; + return mAllowsBackgroundActivityStarts == that.mAllowsBackgroundActivityStarts + && mAllowsBackgroundForegroundServiceStarts + == that.mAllowsBackgroundForegroundServiceStarts + && Objects.equals(mOriginatingToken, that.mOriginatingToken); + } + + @Override + public int hashCode() { + return Objects.hash(mAllowsBackgroundActivityStarts, + mAllowsBackgroundForegroundServiceStarts, + mOriginatingToken); + } } diff --git a/core/java/android/app/IUserSwitchObserver.aidl b/core/java/android/app/IUserSwitchObserver.aidl index 1ff7a17e578f..d71ee7c712e7 100644 --- a/core/java/android/app/IUserSwitchObserver.aidl +++ b/core/java/android/app/IUserSwitchObserver.aidl @@ -19,10 +19,10 @@ package android.app; import android.os.IRemoteCallback; /** {@hide} */ -interface IUserSwitchObserver { - void onBeforeUserSwitching(int newUserId); - oneway void onUserSwitching(int newUserId, IRemoteCallback reply); - oneway void onUserSwitchComplete(int newUserId); - oneway void onForegroundProfileSwitch(int newProfileId); - oneway void onLockedBootComplete(int newUserId); +oneway interface IUserSwitchObserver { + void onBeforeUserSwitching(int newUserId, IRemoteCallback reply); + void onUserSwitching(int newUserId, IRemoteCallback reply); + void onUserSwitchComplete(int newUserId); + void onForegroundProfileSwitch(int newProfileId); + void onLockedBootComplete(int newUserId); } diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 17638ee76dba..eeb1ebb69b03 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -11208,8 +11208,8 @@ public class Notification implements Parcelable private static final String KEY_SEGMENT_LENGTH = "length"; private static final String KEY_POINT_POSITION = "position"; - private static final int MAX_PROGRESS_SEGMENT_LIMIT = 15; - private static final int MAX_PROGRESS_STOP_LIMIT = 5; + private static final int MAX_PROGRESS_SEGMENT_LIMIT = 10; + private static final int MAX_PROGRESS_POINT_LIMIT = 4; private static final int DEFAULT_PROGRESS_MAX = 100; private List<Segment> mProgressSegments = new ArrayList<>(); @@ -11286,7 +11286,9 @@ public class Notification implements Parcelable mProgressSegments = new ArrayList<>(); } mProgressSegments.clear(); - mProgressSegments.addAll(progressSegments); + for (Segment segment : progressSegments) { + addProgressSegment(segment); + } return this; } @@ -11302,7 +11304,11 @@ public class Notification implements Parcelable if (mProgressSegments == null) { mProgressSegments = new ArrayList<>(); } - mProgressSegments.add(segment); + if (segment.getLength() > 0) { + mProgressSegments.add(segment); + } else { + Log.w(TAG, "Dropped the segment. The length is not a positive integer."); + } return this; } @@ -11327,7 +11333,14 @@ public class Notification implements Parcelable * @see Point */ public @NonNull ProgressStyle setProgressPoints(@NonNull List<Point> points) { - mProgressPoints = new ArrayList<>(points); + if (mProgressPoints == null) { + mProgressPoints = new ArrayList<>(); + } + mProgressPoints.clear(); + + for (Point point: points) { + addProgressPoint(point); + } return this; } @@ -11348,7 +11361,17 @@ public class Notification implements Parcelable if (mProgressPoints == null) { mProgressPoints = new ArrayList<>(); } - mProgressPoints.add(point); + if (point.getPosition() >= 0) { + mProgressPoints.add(point); + + if (mProgressPoints.size() > MAX_PROGRESS_POINT_LIMIT) { + Log.w(TAG, "Progress points limit is reached. First" + + MAX_PROGRESS_POINT_LIMIT + " points will be rendered."); + } + + } else { + Log.w(TAG, "Dropped the point. The position is a negative integer."); + } return this; } @@ -11384,8 +11407,7 @@ public class Notification implements Parcelable } else { int progressMax = 0; int validSegmentCount = 0; - for (int i = 0; i < progressSegment.size() - && validSegmentCount < MAX_PROGRESS_SEGMENT_LIMIT; i++) { + for (int i = 0; i < progressSegment.size(); i++) { int segmentLength = progressSegment.get(i).getLength(); if (segmentLength > 0) { try { @@ -11832,6 +11854,30 @@ public class Notification implements Parcelable totalLength = DEFAULT_PROGRESS_MAX; segments.add(sanitizeSegment(new Segment(totalLength), backgroundColor, defaultProgressColor)); + } else if (segments.size() > MAX_PROGRESS_SEGMENT_LIMIT) { + // If segment limit is exceeded. All segments will be replaced + // with a single segment + boolean allSameColor = true; + int firstSegmentColor = segments.get(0).getColor(); + + for (int i = 1; i < segments.size(); i++) { + if (segments.get(i).getColor() != firstSegmentColor) { + allSameColor = false; + break; + } + } + + // This single segment length has same max as total. + final Segment singleSegment = new Segment(totalLength); + // Single segment color: if all segments have the same color, + // use that color. Otherwise, use 0 / default. + singleSegment.setColor(allSameColor ? firstSegmentColor + : Notification.COLOR_DEFAULT); + + segments.clear(); + segments.add(sanitizeSegment(singleSegment, + backgroundColor, + defaultProgressColor)); } // Ensure point color contrasts. @@ -11840,6 +11886,9 @@ public class Notification implements Parcelable final int position = point.getPosition(); if (position < 0 || position > totalLength) continue; points.add(sanitizePoint(point, backgroundColor, defaultProgressColor)); + if (points.size() == MAX_PROGRESS_POINT_LIMIT) { + break; + } } model = new NotificationProgressModel(segments, points, @@ -11868,8 +11917,10 @@ public class Notification implements Parcelable * has the same hue as the original color, but is lightened or darkened depending on * whether the background is dark or light. * + * @hide */ - private int sanitizeProgressColor(@ColorInt int color, + @VisibleForTesting + public static int sanitizeProgressColor(@ColorInt int color, @ColorInt int bg, @ColorInt int defaultColor) { return Builder.ensureColorContrast( diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 8ed66eb7e6c0..e9b889a2f1aa 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -55,6 +55,7 @@ import android.os.Parcelable; import android.os.RemoteException; import android.os.ServiceManager; import android.os.StrictMode; +import android.os.SystemClock; import android.os.UserHandle; import android.provider.Settings; import android.provider.Settings.Global; @@ -677,9 +678,14 @@ public class NotificationManager { } /** {@hide} */ - @UnsupportedAppUsage - public NotificationManager(Context context, InstantSource clock) + public NotificationManager(Context context) { + this(context, SystemClock.elapsedRealtimeClock()); + } + + /** {@hide} */ + @UnsupportedAppUsage + public NotificationManager(Context context, InstantSource clock) { mContext = context; mClock = clock; } diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 920b19cd8f78..0bbe9434293a 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -17,7 +17,7 @@ package android.app; import static android.app.appfunctions.flags.Flags.enableAppFunctionManager; -import static android.provider.flags.Flags.stageFlagsForBuild; +import static android.provider.flags.Flags.newStoragePublicApi; import static android.server.Flags.removeGameManagerServiceFromWear; import android.accounts.AccountManager; @@ -289,7 +289,6 @@ import com.android.internal.os.IDropBoxManagerService; import com.android.internal.policy.PhoneLayoutInflater; import com.android.internal.util.Preconditions; -import java.time.InstantSource; import java.util.Map; import java.util.Objects; @@ -625,8 +624,8 @@ public final class SystemServiceRegistry { com.android.internal.R.style.Theme_Dialog, com.android.internal.R.style.Theme_Holo_Dialog, com.android.internal.R.style.Theme_DeviceDefault_Dialog, - com.android.internal.R.style.Theme_DeviceDefault_Light_Dialog)), - InstantSource.system()); + com.android.internal.R.style.Theme_DeviceDefault_Light_Dialog)) + ); }}); registerService(Context.PEOPLE_SERVICE, PeopleManager.class, @@ -1841,7 +1840,7 @@ public final class SystemServiceRegistry { VirtualizationFrameworkInitializer.registerServiceWrappers(); ConnectivityFrameworkInitializerBaklava.registerServiceWrappers(); - if (stageFlagsForBuild()) { + if (newStoragePublicApi()) { ConfigInfrastructureFrameworkInitializer.registerServiceWrappers(); } diff --git a/core/java/android/app/UserSwitchObserver.java b/core/java/android/app/UserSwitchObserver.java index 727799a1f948..1664cfb6f7a8 100644 --- a/core/java/android/app/UserSwitchObserver.java +++ b/core/java/android/app/UserSwitchObserver.java @@ -30,7 +30,11 @@ public class UserSwitchObserver extends IUserSwitchObserver.Stub { } @Override - public void onBeforeUserSwitching(int newUserId) throws RemoteException {} + public void onBeforeUserSwitching(int newUserId, IRemoteCallback reply) throws RemoteException { + if (reply != null) { + reply.sendResult(null); + } + } @Override public void onUserSwitching(int newUserId, IRemoteCallback reply) throws RemoteException { diff --git a/core/java/android/app/contextualsearch/ContextualSearchManager.java b/core/java/android/app/contextualsearch/ContextualSearchManager.java index 3438cc861661..ad43f271a910 100644 --- a/core/java/android/app/contextualsearch/ContextualSearchManager.java +++ b/core/java/android/app/contextualsearch/ContextualSearchManager.java @@ -48,7 +48,9 @@ public final class ContextualSearchManager { /** * Key to get the entrypoint from the extras of the activity launched by contextual search. - * Only supposed to be used with ACTON_LAUNCH_CONTEXTUAL_SEARCH. + * Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH. + * + * @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH */ public static final String EXTRA_ENTRYPOINT = "android.app.contextualsearch.extra.ENTRYPOINT"; @@ -56,14 +58,18 @@ public final class ContextualSearchManager { /** * Key to get the flag_secure value from the extras of the activity launched by contextual * search. The value will be true if flag_secure is found in any of the visible activities. - * Only supposed to be used with ACTON_LAUNCH_CONTEXTUAL_SEARCH. + * Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH. + * + * @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH */ public static final String EXTRA_FLAG_SECURE_FOUND = "android.app.contextualsearch.extra.FLAG_SECURE_FOUND"; /** * Key to get the screenshot from the extras of the activity launched by contextual search. - * Only supposed to be used with ACTON_LAUNCH_CONTEXTUAL_SEARCH. + * Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH. + * + * @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH */ public static final String EXTRA_SCREENSHOT = "android.app.contextualsearch.extra.SCREENSHOT"; @@ -71,7 +77,9 @@ public final class ContextualSearchManager { /** * Key to check whether managed profile is visible from the extras of the activity launched by * contextual search. The value will be true if any one of the visible apps is managed. - * Only supposed to be used with ACTON_LAUNCH_CONTEXTUAL_SEARCH. + * Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH. + * + * @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH */ public static final String EXTRA_IS_MANAGED_PROFILE_VISIBLE = "android.app.contextualsearch.extra.IS_MANAGED_PROFILE_VISIBLE"; @@ -79,7 +87,9 @@ public final class ContextualSearchManager { /** * Key to get the list of visible packages from the extras of the activity launched by * contextual search. - * Only supposed to be used with ACTON_LAUNCH_CONTEXTUAL_SEARCH. + * Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH. + * + * @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH */ public static final String EXTRA_VISIBLE_PACKAGE_NAMES = "android.app.contextualsearch.extra.VISIBLE_PACKAGE_NAMES"; @@ -87,7 +97,9 @@ public final class ContextualSearchManager { /** * Key to get the time the user made the invocation request, based on * {@link SystemClock#uptimeMillis()}. - * Only supposed to be used with ACTON_LAUNCH_CONTEXTUAL_SEARCH. + * Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH. + * + * @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH * * TODO: un-hide in W * @@ -99,11 +111,24 @@ public final class ContextualSearchManager { /** * Key to get the binder token from the extras of the activity launched by contextual search. * This token is needed to invoke {@link CallbackToken#getContextualSearchState} method. - * Only supposed to be used with ACTON_LAUNCH_CONTEXTUAL_SEARCH. + * Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH. + * + * @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH */ public static final String EXTRA_TOKEN = "android.app.contextualsearch.extra.TOKEN"; /** + * Key to check whether audio is playing when contextual search is invoked. + * Only supposed to be used with ACTION_LAUNCH_CONTEXTUAL_SEARCH. + * + * @see #ACTION_LAUNCH_CONTEXTUAL_SEARCH + * + * @hide + */ + public static final String EXTRA_IS_AUDIO_PLAYING = + "android.app.contextualsearch.extra.IS_AUDIO_PLAYING"; + + /** * Intent action for contextual search invocation. The app providing the contextual search * experience must add this intent filter action to the activity it wants to be launched. * <br> diff --git a/core/java/android/app/contextualsearch/flags.aconfig b/core/java/android/app/contextualsearch/flags.aconfig index e8cfd79c9cc7..c19921dcdc61 100644 --- a/core/java/android/app/contextualsearch/flags.aconfig +++ b/core/java/android/app/contextualsearch/flags.aconfig @@ -8,6 +8,7 @@ flag { bug: "309689654" is_exported: true } + flag { name: "enable_token_refresh" namespace: "machine_learning" @@ -27,4 +28,11 @@ flag { namespace: "sysui_integrations" description: "Identify live contextual search UI to exclude from contextual search screenshot." bug: "372510690" +} + +flag { + name: "include_audio_playing_status" + namespace: "sysui_integrations" + description: "Add audio playing status to the contextual search invocation intent." + bug: "372935419" }
\ No newline at end of file diff --git a/core/java/android/app/jank/AppJankStats.java b/core/java/android/app/jank/AppJankStats.java index eea1d2ba5b9e..6ef6a44ddfbb 100644 --- a/core/java/android/app/jank/AppJankStats.java +++ b/core/java/android/app/jank/AppJankStats.java @@ -41,7 +41,8 @@ public final class AppJankStats { // The id that has been set for the widget. private String mWidgetId; - // A general category that the widget applies to. + // A general category the widget falls into based on the functions it performs or helps + // facilitate. private String mWidgetCategory; // The states that the UI elements can report @@ -53,78 +54,78 @@ public final class AppJankStats { // Total number of frames determined to be janky during the reported state. private long mJankyFrames; - // Histogram of frame duration overruns encoded in predetermined buckets. - private FrameOverrunHistogram mFrameOverrunHistogram; + // Histogram of relative frame times encoded in predetermined buckets. + private RelativeFrameTimeHistogram mRelativeFrameTimeHistogram; /** Used to indicate no widget category has been set. */ - public static final String WIDGET_CATEGORY_UNSPECIFIED = - "widget_category_unspecified"; + public static final String WIDGET_CATEGORY_UNSPECIFIED = "unspecified"; /** UI elements that facilitate scrolling. */ - public static final String SCROLL = "scroll"; + public static final String WIDGET_CATEGORY_SCROLL = "scroll"; /** UI elements that facilitate playing animations. */ - public static final String ANIMATION = "animation"; + public static final String WIDGET_CATEGORY_ANIMATION = "animation"; /** UI elements that facilitate media playback. */ - public static final String MEDIA = "media"; + public static final String WIDGET_CATEGORY_MEDIA = "media"; /** UI elements that facilitate in-app navigation. */ - public static final String NAVIGATION = "navigation"; + public static final String WIDGET_CATEGORY_NAVIGATION = "navigation"; /** UI elements that facilitate displaying, hiding or interacting with keyboard. */ - public static final String KEYBOARD = "keyboard"; - - /** UI elements that facilitate predictive back gesture navigation. */ - public static final String PREDICTIVE_BACK = "predictive_back"; + public static final String WIDGET_CATEGORY_KEYBOARD = "keyboard"; /** UI elements that don't fall in one or any of the other categories. */ - public static final String OTHER = "other"; + public static final String WIDGET_CATEGORY_OTHER = "other"; /** Used to indicate no widget state has been set. */ - public static final String WIDGET_STATE_UNSPECIFIED = "widget_state_unspecified"; + public static final String WIDGET_STATE_UNSPECIFIED = "unspecified"; /** Used to indicate the UI element currently has no state and is idle. */ - public static final String NONE = "none"; + public static final String WIDGET_STATE_NONE = "none"; /** Used to indicate the UI element is currently scrolling. */ - public static final String SCROLLING = "scrolling"; + public static final String WIDGET_STATE_SCROLLING = "scrolling"; /** Used to indicate the UI element is currently being flung. */ - public static final String FLINGING = "flinging"; + public static final String WIDGET_STATE_FLINGING = "flinging"; /** Used to indicate the UI element is currently being swiped. */ - public static final String SWIPING = "swiping"; + public static final String WIDGET_STATE_SWIPING = "swiping"; /** Used to indicate the UI element is currently being dragged. */ - public static final String DRAGGING = "dragging"; + public static final String WIDGET_STATE_DRAGGING = "dragging"; /** Used to indicate the UI element is currently zooming. */ - public static final String ZOOMING = "zooming"; + public static final String WIDGET_STATE_ZOOMING = "zooming"; /** Used to indicate the UI element is currently animating. */ - public static final String ANIMATING = "animating"; + public static final String WIDGET_STATE_ANIMATING = "animating"; /** Used to indicate the UI element is currently playing media. */ - public static final String PLAYBACK = "playback"; + public static final String WIDGET_STATE_PLAYBACK = "playback"; /** Used to indicate the UI element is currently being tapped on, for example on a keyboard. */ - public static final String TAPPING = "tapping"; + public static final String WIDGET_STATE_TAPPING = "tapping"; + + /** Used to indicate predictive back navigation is currently being used */ + public static final String WIDGET_STATE_PREDICTIVE_BACK = "predictive_back"; /** + * Provide an organized way to group widgets that have similar purposes or perform related + * functions. * @hide */ - @StringDef(value = { + @StringDef(prefix = {"WIDGET_CATEGORY_"}, value = { WIDGET_CATEGORY_UNSPECIFIED, - SCROLL, - ANIMATION, - MEDIA, - NAVIGATION, - KEYBOARD, - PREDICTIVE_BACK, - OTHER + WIDGET_CATEGORY_SCROLL, + WIDGET_CATEGORY_ANIMATION, + WIDGET_CATEGORY_MEDIA, + WIDGET_CATEGORY_NAVIGATION, + WIDGET_CATEGORY_KEYBOARD, + WIDGET_CATEGORY_OTHER }) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @Retention(RetentionPolicy.SOURCE) @@ -133,17 +134,18 @@ public final class AppJankStats { /** * @hide */ - @StringDef(value = { + @StringDef(prefix = {"WIDGET_STATE_"}, value = { WIDGET_STATE_UNSPECIFIED, - NONE, - SCROLLING, - FLINGING, - SWIPING, - DRAGGING, - ZOOMING, - ANIMATING, - PLAYBACK, - TAPPING, + WIDGET_STATE_NONE, + WIDGET_STATE_SCROLLING, + WIDGET_STATE_FLINGING, + WIDGET_STATE_SWIPING, + WIDGET_STATE_DRAGGING, + WIDGET_STATE_ZOOMING, + WIDGET_STATE_ANIMATING, + WIDGET_STATE_PLAYBACK, + WIDGET_STATE_TAPPING, + WIDGET_STATE_PREDICTIVE_BACK }) @Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @Retention(RetentionPolicy.SOURCE) @@ -156,31 +158,33 @@ public final class AppJankStats { * * @param appUid the Uid of the App that is collecting jank stats. * @param widgetId the widget id that frames will be associated to. - * @param widgetCategory a general functionality category that the widget falls into. Must be - * one of the following: SCROLL, ANIMATION, MEDIA, NAVIGATION, KEYBOARD, - * PREDICTIVE_BACK, OTHER or will be set to WIDGET_CATEGORY_UNSPECIFIED - * if no value is passed. - * @param widgetState the state the widget was in while frames were counted. Must be one of - * the following: NONE, SCROLLING, FLINGING, SWIPING, DRAGGING, ZOOMING, - * ANIMATING, PLAYBACK, TAPPING or will be set to WIDGET_STATE_UNSPECIFIED - * if no value is passed. + * @param widgetCategory a category used to organize widgets in a structured way that indicates + * they serve a similar purpose or perform related functions. Must be + * prefixed with WIDGET_CATEGORY_ and have a suffix of one of the + * following:SCROLL, ANIMATION, MEDIA, NAVIGATION, KEYBOARD, OTHER or + * will be set to UNSPECIFIED if no value is passed. + * @param widgetState the state the widget was in while frames were counted. Must be prefixed + * with WIDGET_STATE_ and have a suffix of one of the following: + * NONE, SCROLLING, FLINGING, SWIPING, DRAGGING, ZOOMING, ANIMATING, + * PLAYBACK, TAPPING, PREDICTIVE_BACK or will be set to + * WIDGET_STATE_UNSPECIFIED if no value is passed. * @param totalFrames the total number of frames that were counted for this stat. * @param jankyFrames the total number of janky frames that were counted for this stat. - * @param frameOverrunHistogram the histogram with predefined buckets. See - * {@link #getFrameOverrunHistogram()} for details. + * @param relativeFrameTimeHistogram the histogram with predefined buckets. See + * {@link #getRelativeFrameTimeHistogram()} for details. * */ public AppJankStats(int appUid, @NonNull String widgetId, @Nullable @WidgetCategory String widgetCategory, @Nullable @WidgetState String widgetState, long totalFrames, long jankyFrames, - @NonNull FrameOverrunHistogram frameOverrunHistogram) { + @NonNull RelativeFrameTimeHistogram relativeFrameTimeHistogram) { mUid = appUid; mWidgetId = widgetId; mWidgetCategory = widgetCategory != null ? widgetCategory : WIDGET_CATEGORY_UNSPECIFIED; mWidgetState = widgetState != null ? widgetState : WIDGET_STATE_UNSPECIFIED; mTotalFrames = totalFrames; mJankyFrames = jankyFrames; - mFrameOverrunHistogram = frameOverrunHistogram; + mRelativeFrameTimeHistogram = relativeFrameTimeHistogram; } /** @@ -203,7 +207,7 @@ public final class AppJankStats { /** * Returns the category that the widget's functionality generally falls into, or - * widget_category_unspecified {@link #WIDGET_CATEGORY_UNSPECIFIED} if no value was passed in. + * {@link #WIDGET_CATEGORY_UNSPECIFIED} if no value was passed in. * * @return the category that the widget's functionality generally falls into, this value cannot * be null. @@ -213,7 +217,7 @@ public final class AppJankStats { } /** - * Returns the widget's state that was reported for this stat, or widget_state_unspecified + * Returns the widget's state that was reported for this stat, or * {@link #WIDGET_STATE_UNSPECIFIED} if no value was passed in. * * @return the widget's state that was reported for this stat. This value cannot be null. @@ -241,13 +245,13 @@ public final class AppJankStats { } /** - * Returns a Histogram containing frame overrun times in millis grouped into predefined buckets. - * See {@link FrameOverrunHistogram} for more information. + * Returns a Histogram containing relative frame times in millis grouped into predefined + * buckets. See {@link RelativeFrameTimeHistogram} for more information. * - * @return Histogram containing frame overrun times in predefined buckets. This value cannot + * @return Histogram containing relative frame times in predefined buckets. This value cannot * be null. */ - public @NonNull FrameOverrunHistogram getFrameOverrunHistogram() { - return mFrameOverrunHistogram; + public @NonNull RelativeFrameTimeHistogram getRelativeFrameTimeHistogram() { + return mRelativeFrameTimeHistogram; } } diff --git a/core/java/android/app/jank/FrameOverrunHistogram.java b/core/java/android/app/jank/FrameOverrunHistogram.java deleted file mode 100644 index 3ad6531a46bf..000000000000 --- a/core/java/android/app/jank/FrameOverrunHistogram.java +++ /dev/null @@ -1,107 +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 android.app.jank; - -import android.annotation.FlaggedApi; -import android.annotation.NonNull; - -import java.util.Arrays; - -/** - * This class is intended to be used when reporting {@link AppJankStats} back to the system. It's - * intended to be used by library widgets to help facilitate the reporting of frame overrun times - * by adding those times into predefined buckets. - */ -@FlaggedApi(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) -public class FrameOverrunHistogram { - private static int[] sBucketEndpoints = new int[]{ - Integer.MIN_VALUE, -200, -150, -100, -90, -80, -70, -60, -50, -40, -30, -25, -20, -18, - -16, -14, -12, -10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 25, 30, 40, - 50, 60, 70, 80, 90, 100, 150, 200, 300, 400, 500, 600, 700, 800, 900, 1000 - }; - private int[] mBucketCounts; - - /** - * Create a new instance of FrameOverrunHistogram. - */ - public FrameOverrunHistogram() { - mBucketCounts = new int[sBucketEndpoints.length]; - } - - /** - * Increases the count by one for the bucket representing the frame overrun duration. - * - * @param frameOverrunMillis frame overrun duration in millis, frame overrun is the difference - * between a frames deadline and when it was rendered. - */ - public void addFrameOverrunMillis(int frameOverrunMillis) { - int countsIndex = getIndexForCountsFromOverrunTime(frameOverrunMillis); - mBucketCounts[countsIndex]++; - } - - /** - * Returns the counts for the all the frame overrun buckets. - * - * @return an array of integers representing the counts of frame overrun times. This value - * cannot be null. - */ - public @NonNull int[] getBucketCounters() { - return Arrays.copyOf(mBucketCounts, mBucketCounts.length); - } - - /** - * Returns the predefined endpoints for the histogram. - * - * @return array of integers representing the endpoints for the predefined histogram count - * buckets. This value cannot be null. - */ - public @NonNull int[] getBucketEndpointsMillis() { - return Arrays.copyOf(sBucketEndpoints, sBucketEndpoints.length); - } - - // This takes the overrun time and returns what bucket it belongs to in the counters array. - private int getIndexForCountsFromOverrunTime(int overrunTime) { - if (overrunTime < 20) { - if (overrunTime >= -20) { - return (overrunTime + 20) / 2 + 12; - } - if (overrunTime >= -30) { - return (overrunTime + 30) / 5 + 10; - } - if (overrunTime >= -100) { - return (overrunTime + 100) / 10 + 3; - } - if (overrunTime >= -200) { - return (overrunTime + 200) / 50 + 1; - } - return 0; - } - if (overrunTime < 30) { - return (overrunTime - 20) / 5 + 32; - } - if (overrunTime < 100) { - return (overrunTime - 30) / 10 + 34; - } - if (overrunTime < 200) { - return (overrunTime - 50) / 100 + 41; - } - if (overrunTime < 1000) { - return (overrunTime - 200) / 100 + 43; - } - return sBucketEndpoints.length - 1; - } -} diff --git a/core/java/android/app/jank/JankDataProcessor.java b/core/java/android/app/jank/JankDataProcessor.java index c9472598b352..b4c293eeb695 100644 --- a/core/java/android/app/jank/JankDataProcessor.java +++ b/core/java/android/app/jank/JankDataProcessor.java @@ -111,7 +111,7 @@ public class JankDataProcessor { pendingStat.mTotalFrames += jankStat.getTotalFrameCount(); mergeOverrunHistograms(pendingStat.mFrameOverrunBuckets, - jankStat.getFrameOverrunHistogram().getBucketCounters()); + jankStat.getRelativeFrameTimeHistogram().getBucketCounters()); } private void mergeNewStat(String stateKey, String activityName, AppJankStats jankStats) { @@ -136,7 +136,7 @@ public class JankDataProcessor { pendingStat.mJankyFrames = jankStats.getJankyFrameCount(); mergeOverrunHistograms(pendingStat.mFrameOverrunBuckets, - jankStats.getFrameOverrunHistogram().getBucketCounters()); + jankStats.getRelativeFrameTimeHistogram().getBucketCounters()); mPendingJankStats.put(stateKey, pendingStat); } @@ -271,7 +271,8 @@ public class JankDataProcessor { private static final int[] sFrameOverrunHistogramBounds = { Integer.MIN_VALUE, -200, -150, -100, -90, -80, -70, -60, -50, -40, -30, -25, -20, -18, -16, -14, -12, -10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 25, - 30, 40, 50, 60, 70, 80, 90, 100, 150, 200, 300, 400, 500, 600, 700, 800, 900, 1000 + 30, 40, 50, 60, 70, 80, 90, 100, 150, 200, 300, 400, 500, 600, 700, 800, 900, 1000, + Integer.MAX_VALUE }; private final int[] mFrameOverrunBuckets = new int[sFrameOverrunHistogramBounds.length]; @@ -414,7 +415,7 @@ public class JankDataProcessor { if (overrunTime < 200) { return (overrunTime - 50) / 100 + 41; } - if (overrunTime < 1000) { + if (overrunTime <= 1000) { return (overrunTime - 200) / 100 + 43; } return sFrameOverrunHistogramBounds.length - 1; diff --git a/core/java/android/app/jank/RelativeFrameTimeHistogram.java b/core/java/android/app/jank/RelativeFrameTimeHistogram.java new file mode 100644 index 000000000000..666f90f89f45 --- /dev/null +++ b/core/java/android/app/jank/RelativeFrameTimeHistogram.java @@ -0,0 +1,129 @@ +/* + * 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.app.jank; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; + +import java.util.Arrays; + +/** + * A histogram of frame times relative to their deadline. + * + * This class aids in reporting {@link AppJankStats} to the system and is designed for use by + * library widgets. It facilitates the recording of frame times in relation to the frame deadline. + * The class records the distribution of time remaining until a frame is considered janky or how + * janky the frame was. + * <p> + * A frame's relative frame time value indicates whether it was delivered early, on time, or late. + * A negative relative frame time value indicates the frame was delivered early, a value of zero + * indicates the frame was delivered on time and a positive value indicates the frame was delivered + * late. The values of the endpoints indicate how early or late a frame was delivered. + * <p> + * The relative frame times are recorded as a histogram: values are + * {@link #addRelativeFrameTimeMillis added} to a bucket by increasing the bucket's counter. The + * count of frames with a relative frame time between + * {@link #getBucketEndpointsMillis bucket endpoints} {@code i} and {@code i+1} can be obtained + * through index {@code i} of {@link #getBucketCounters}. + * + */ +@FlaggedApi(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) +public class RelativeFrameTimeHistogram { + private static int[] sBucketEndpoints = new int[]{ + Integer.MIN_VALUE, -200, -150, -100, -90, -80, -70, -60, -50, -40, -30, -25, -20, -18, + -16, -14, -12, -10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 25, 30, 40, + 50, 60, 70, 80, 90, 100, 150, 200, 300, 400, 500, 600, 700, 800, 900, 1000, + Integer.MAX_VALUE + }; + // + private int[] mBucketCounts; + + /** + * Create a new instance of RelativeFrameTimeHistogram. + */ + public RelativeFrameTimeHistogram() { + mBucketCounts = new int[sBucketEndpoints.length - 1]; + } + + /** + * Increases the count by one for the bucket representing the relative frame time. + * + * @param frameTimeMillis relative frame time in millis, relative frame time is the difference + * between a frames deadline and when it was rendered. + */ + public void addRelativeFrameTimeMillis(int frameTimeMillis) { + int countsIndex = getRelativeFrameTimeBucketIndex(frameTimeMillis); + mBucketCounts[countsIndex]++; + } + + /** + * Returns the counts for the all the relative frame time buckets. + * + * @return an array of integers representing the counts of relative frame times. This value + * cannot be null. + */ + public @NonNull int[] getBucketCounters() { + return Arrays.copyOf(mBucketCounts, mBucketCounts.length); + } + + /** + * Returns the relative frame time endpoints for the histogram. + * <p> + * Index {@code i} of {@link #getBucketCounters} contains the count of frames that had a + * relative frame time between {@code endpoints[i]} (inclusive) and {@code endpoints[i+1]} + * (exclusive). + * + * @return array of integers representing the endpoints for the predefined histogram count + * buckets. This value cannot be null. + */ + public @NonNull int[] getBucketEndpointsMillis() { + return Arrays.copyOf(sBucketEndpoints, sBucketEndpoints.length); + } + + // This takes the relative frame time and returns what bucket it belongs to in the counters + // array. + private int getRelativeFrameTimeBucketIndex(int relativeFrameTime) { + if (relativeFrameTime < 20) { + if (relativeFrameTime >= -20) { + return (relativeFrameTime + 20) / 2 + 12; + } + if (relativeFrameTime >= -30) { + return (relativeFrameTime + 30) / 5 + 10; + } + if (relativeFrameTime >= -100) { + return (relativeFrameTime + 100) / 10 + 3; + } + if (relativeFrameTime >= -200) { + return (relativeFrameTime + 200) / 50 + 1; + } + return 0; + } + if (relativeFrameTime < 30) { + return (relativeFrameTime - 20) / 5 + 32; + } + if (relativeFrameTime < 100) { + return (relativeFrameTime - 30) / 10 + 34; + } + if (relativeFrameTime < 200) { + return (relativeFrameTime - 50) / 100 + 41; + } + if (relativeFrameTime < 1000) { + return (relativeFrameTime - 200) / 100 + 43; + } + return mBucketCounts.length - 1; + } +} diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 9f898b823a76..e6ddbf466cae 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -196,6 +196,21 @@ interface IPackageManager { ProviderInfo resolveContentProvider(String name, long flags, int userId); /** + * Resolve content providers with a given authority, for a specific + * callingUid. + * + * @param authority Authority of the content provider + * @param flags Additional option flags to modify the data returned. + * @param userId Current user ID + * @param callingUid UID of the caller who's access to the content provider + is to be checked + * + * @return ProviderInfo of the resolved content provider. May return null + */ + ProviderInfo resolveContentProviderForUid(String authority, long flags, + int userId, int callingUid); + + /** * Retrieve sync information for all content providers. * * @param outNames Filled in with a list of the root names of the content diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 438a21b7942f..c16582f19c9b 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -8349,6 +8349,25 @@ public abstract class PackageManager { } /** + * Resolve content providers with a given authority, for a specific callingUid. + * @param authority Authority of the content provider + * @param flags Additional option flags to modify the data returned. + * @param callingUid UID of the caller who's access to the content provider is to be checked + + * @return ProviderInfo of the resolved content provider. + * @hide + */ + @Nullable + @FlaggedApi(android.content.pm.Flags.FLAG_UID_BASED_PROVIDER_LOOKUP) + @RequiresPermission(Manifest.permission.RESOLVE_COMPONENT_FOR_UID) + @SystemApi + public ProviderInfo resolveContentProviderForUid(@NonNull String authority, + @NonNull ComponentInfoFlags flags, int callingUid) { + throw new UnsupportedOperationException( + "resolveContentProviderForUid not implemented in subclass"); + } + + /** * Retrieve content provider information. * <p> * <em>Note: unlike most other methods, an empty result set is indicated diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index 0d219a901b9d..7bba06c87813 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -152,7 +152,7 @@ flag { name: "cache_sdk_system_features" namespace: "system_performance" description: "Feature flag to enable optimized cache for SDK-defined system feature lookups." - bug: "375000483" + bug: "326623529" } flag { @@ -375,3 +375,11 @@ flag { description: "Feature flag to remove the consumption of the hidden module status (ModuleInfo#IsHidden) in the Android source tree." bug: "363952383" } + +flag { + name: "uid_based_provider_lookup" + is_exported: true + namespace: "package_manager_service" + bug: "334024639" + description: "Feature flag to check whether a given UID can access a content provider" +} diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java index 852f04793f15..9c6b71b72ec8 100644 --- a/core/java/android/hardware/camera2/CameraDevice.java +++ b/core/java/android/hardware/camera2/CameraDevice.java @@ -417,6 +417,7 @@ public abstract class CameraDevice implements AutoCloseable { * or if any of the output configurations sets a stream use * case different from {@link * android.hardware.camera2.CameraCharacteristics#SCALER_AVAILABLE_STREAM_USE_CASES_DEFAULT}. + * @throws UnsupportedOperationException if the camera has been opened in shared mode * @see CameraExtensionCharacteristics#getSupportedExtensions * @see CameraExtensionCharacteristics#getExtensionSupportedSizes */ @@ -1258,7 +1259,8 @@ public abstract class CameraDevice implements AutoCloseable { * configurations are empty; or the session configuration * executor is invalid; * or the output dynamic range combination is - * invalid/unsupported. + * invalid/unsupported; or the session type is not shared when + * camera has been opened in shared mode. * @throws CameraAccessException In case the camera device is no longer connected or has * encountered a fatal error. * @see #createCaptureSession(List, CameraCaptureSession.StateCallback, Handler) @@ -1292,6 +1294,8 @@ public abstract class CameraDevice implements AutoCloseable { * @throws CameraAccessException if the camera device is no longer connected or has * encountered a fatal error * @throws IllegalStateException if the camera device has been closed + * @throws UnsupportedOperationException if this is not a primary client of a camera opened in + * shared mode */ @NonNull public abstract CaptureRequest.Builder createCaptureRequest(@RequestTemplate int templateType) @@ -1328,6 +1332,8 @@ public abstract class CameraDevice implements AutoCloseable { * @throws CameraAccessException if the camera device is no longer connected or has * encountered a fatal error * @throws IllegalStateException if the camera device has been closed + * @throws UnsupportedOperationException if this is not a primary client of a camera opened in + * shared mode * * @see #TEMPLATE_PREVIEW * @see #TEMPLATE_RECORD @@ -1369,6 +1375,7 @@ public abstract class CameraDevice implements AutoCloseable { * @throws CameraAccessException if the camera device is no longer connected or has * encountered a fatal error * @throws IllegalStateException if the camera device has been closed + * @throws UnsupportedOperationException if the camera has been opened in shared mode * * @see CaptureRequest.Builder * @see TotalCaptureResult diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index aba2345f28d8..bfaff941939c 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -1375,6 +1375,9 @@ public final class CameraManager { * @throws SecurityException if the application does not have permission to * access the camera * + * @throws UnsupportedOperationException if {@link #isCameraDeviceSharingSupported} returns + * false for the given {@code cameraId}. + * * @see #getCameraIdList * @see android.app.admin.DevicePolicyManager#setCameraDisabled * @@ -1393,6 +1396,10 @@ public final class CameraManager { if (executor == null) { throw new IllegalArgumentException("executor was null"); } + if (!isCameraDeviceSharingSupported(cameraId)) { + throw new UnsupportedOperationException( + "CameraDevice sharing is not supported for Camera ID: " + cameraId); + } openCameraImpl(cameraId, callback, executor, /*oomScoreOffset*/0, getRotationOverride(mContext), /*sharedMode*/true); } diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java index 496d316eb028..1c65b0882e0f 100644 --- a/core/java/android/hardware/camera2/CaptureRequest.java +++ b/core/java/android/hardware/camera2/CaptureRequest.java @@ -299,6 +299,24 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> return mRequestType; } + /** + * Get the stream ids corresponding to the target surfaces. + * + * @hide + */ + public int[] getStreamIds() { + return mStreamIdxArray; + }; + + /** + * Get the surface ids corresponding to the target surfaces. + * + * @hide + */ + public int[] getSurfaceIds() { + return mSurfaceIdxArray; + }; + // If this request is part of constrained high speed request list that was created by // {@link android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession#createHighSpeedRequestList} private boolean mIsPartOfCHSRequestList = false; diff --git a/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java index ce8661e90978..7e0456b22be8 100644 --- a/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java @@ -340,6 +340,30 @@ public class CameraCaptureSessionImpl extends CameraCaptureSession } } + /** + * Shared Camera capture session API which can be used by the clients + * to start streaming. + * + * @hide + */ + public int startStreaming(List<Surface> surfaces, Executor executor, + CaptureCallback callback) throws CameraAccessException { + + synchronized (mDeviceImpl.mInterfaceLock) { + checkNotClosed(); + + executor = CameraDeviceImpl.checkExecutor(executor, callback); + + if (DEBUG) { + Log.v(TAG, mIdString + "startStreaming callback " + callback + " executor" + + " " + executor); + } + + return addPendingSequence(mDeviceImpl.startStreaming(surfaces, + createCaptureCallbackProxyWithExecutor(executor, callback), mDeviceExecutor)); + } + } + private void checkRepeatingRequest(CaptureRequest request) { if (request == null) { throw new IllegalArgumentException("request must not be null"); diff --git a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index 34c0f7b19da9..89a6b02b56c4 100644 --- a/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -451,6 +451,16 @@ public class CameraDeviceImpl extends CameraDevice } } + /** + * When camera device is opened in shared mode, call to check if this is a primary client. + * + */ + public boolean isPrimaryClient() { + synchronized (mInterfaceLock) { + return mIsPrimaryClient; + } + } + private Map<String, CameraCharacteristics> getPhysicalIdToChars() { if (mPhysicalIdsToChars == null) { try { @@ -482,8 +492,19 @@ public class CameraDeviceImpl extends CameraDevice mRemoteDevice = new ICameraDeviceUserWrapper(remoteDevice); Parcel resultParcel = Parcel.obtain(); - mRemoteDevice.getCaptureResultMetadataQueue().writeToParcel(resultParcel, 0); + + // Passing in PARCELABLE_WRITE_RETURN_VALUE closes the ParcelFileDescriptors + // owned by MQDescriptor returned by getCaptureResultMetadataQueue() + // Though these will be closed when GC runs, that may not happen for a while. + // Also, apps running with StrictMode would get warnings / crash in the case they're not + // explicitly closed. + mRemoteDevice.getCaptureResultMetadataQueue().writeToParcel(resultParcel, + Parcelable.PARCELABLE_WRITE_RETURN_VALUE); mFMQReader = nativeCreateFMQReader(resultParcel); + // Recycle since resultParcel would dup fds from MQDescriptor as well. We don't + // need them after the native FMQ reader has been created. That is since the native + // creates calls MQDescriptor.readFromParcel() which again dups the fds. + resultParcel.recycle(); IBinder remoteDeviceBinder = remoteDevice.asBinder(); // For legacy camera device, remoteDevice is in the same process, and @@ -847,24 +868,19 @@ public class CameraDeviceImpl extends CameraDevice List<SharedOutputConfiguration> sharedConfigs = sharedSessionConfiguration.getOutputStreamsInformation(); for (SharedOutputConfiguration sharedConfig : sharedConfigs) { - if (outConfig.getConfiguredSize().equals(sharedConfig.getSize()) - && (outConfig.getConfiguredFormat() == sharedConfig.getFormat()) - && (outConfig.getSurfaceGroupId() == OutputConfiguration.SURFACE_GROUP_ID_NONE) - && (outConfig.getSurfaceType() == sharedConfig.getSurfaceType()) + if ((outConfig.getSurfaceGroupId() == OutputConfiguration.SURFACE_GROUP_ID_NONE) && (outConfig.getMirrorMode() == sharedConfig.getMirrorMode()) - && (outConfig.getUsage() == sharedConfig.getUsage()) && (outConfig.isReadoutTimestampEnabled() == sharedConfig.isReadoutTimestampEnabled()) && (outConfig.getTimestampBase() == sharedConfig.getTimestampBase()) && (outConfig.getStreamUseCase() == sharedConfig.getStreamUseCase()) - && (outConfig.getColorSpace().equals( - sharedSessionConfiguration.getColorSpace())) && (outConfig.getDynamicRangeProfile() == DynamicRangeProfiles.STANDARD) - && (outConfig.getConfiguredDataspace() == sharedConfig.getDataspace()) && (Objects.equals(outConfig.getPhysicalCameraId(), sharedConfig.getPhysicalCameraId())) && (outConfig.getSensorPixelModes().isEmpty()) + && (!outConfig.isMultiResolution()) + && (!outConfig.isDeferredConfiguration()) && (!outConfig.isShared())) { //Found valid config, return true return true; @@ -896,14 +912,6 @@ public class CameraDeviceImpl extends CameraDevice if (config.getExecutor() == null) { throw new IllegalArgumentException("Invalid executor"); } - if (mSharedMode) { - if (config.getSessionType() != SessionConfiguration.SESSION_SHARED) { - throw new IllegalArgumentException("Invalid session type"); - } - if (!checkSharedSessionConfiguration(outputConfigs)) { - throw new IllegalArgumentException("Invalid output configurations"); - } - } createCaptureSessionInternal(config.getInputConfiguration(), outputConfigs, config.getStateCallback(), config.getExecutor(), config.getSessionType(), config.getSessionParameters()); @@ -921,17 +929,26 @@ public class CameraDeviceImpl extends CameraDevice checkIfCameraClosedOrInError(); + boolean isSharedSession = (operatingMode == ICameraDeviceUser.SHARED_MODE); + if (Flags.cameraMultiClient() && mSharedMode) { + if (!isSharedSession) { + throw new IllegalArgumentException("Invalid session type"); + } + if (!checkSharedSessionConfiguration(outputConfigurations)) { + throw new IllegalArgumentException("Invalid output configurations"); + } + if (inputConfig != null) { + throw new IllegalArgumentException("Shared capture session doesn't support" + + " input configuration yet."); + } + } + boolean isConstrainedHighSpeed = (operatingMode == ICameraDeviceUser.CONSTRAINED_HIGH_SPEED_MODE); if (isConstrainedHighSpeed && inputConfig != null) { throw new IllegalArgumentException("Constrained high speed session doesn't support" + " input configuration yet."); } - boolean isSharedSession = (operatingMode == ICameraDeviceUser.SHARED_MODE); - if (isSharedSession && inputConfig != null) { - throw new IllegalArgumentException("Shared capture session doesn't support" - + " input configuration yet."); - } if (mCurrentExtensionSession != null) { mCurrentExtensionSession.commitStats(); @@ -993,8 +1010,7 @@ public class CameraDeviceImpl extends CameraDevice mCharacteristics); } else if (isSharedSession) { newSession = new CameraSharedCaptureSessionImpl(mNextSessionId++, - callback, executor, this, mDeviceExecutor, configureSuccess, - mIsPrimaryClient); + callback, executor, this, mDeviceExecutor, configureSuccess); } else { newSession = new CameraCaptureSessionImpl(mNextSessionId++, input, callback, executor, this, mDeviceExecutor, configureSuccess); @@ -1063,6 +1079,11 @@ public class CameraDeviceImpl extends CameraDevice synchronized(mInterfaceLock) { checkIfCameraClosedOrInError(); + if (Flags.cameraMultiClient() && mSharedMode && !mIsPrimaryClient) { + throw new UnsupportedOperationException("In shared session mode," + + "only primary clients can create capture request."); + } + for (String physicalId : physicalCameraIdSet) { if (Objects.equals(physicalId, getId())) { throw new IllegalStateException("Physical id matches the logical id!"); @@ -1089,6 +1110,11 @@ public class CameraDeviceImpl extends CameraDevice synchronized(mInterfaceLock) { checkIfCameraClosedOrInError(); + if (Flags.cameraMultiClient() && mSharedMode && !mIsPrimaryClient) { + throw new UnsupportedOperationException("In shared session mode," + + "only primary clients can create capture request."); + } + CameraMetadataNative templatedRequest = null; templatedRequest = mRemoteDevice.createDefaultRequest(templateType); @@ -1108,6 +1134,10 @@ public class CameraDeviceImpl extends CameraDevice throws CameraAccessException { synchronized(mInterfaceLock) { checkIfCameraClosedOrInError(); + if (Flags.cameraMultiClient() && mSharedMode) { + throw new UnsupportedOperationException("In shared session mode," + + "reprocess capture requests are not supported."); + } CameraMetadataNative resultMetadata = new CameraMetadataNative(inputResult.getNativeCopy()); @@ -1561,6 +1591,74 @@ public class CameraDeviceImpl extends CameraDevice } } + public int startStreaming(List<Surface> surfaces, CaptureCallback callback, + Executor executor) throws CameraAccessException { + // Need a valid executor, or current thread needs to have a looper, if + // callback is valid + executor = checkExecutor(executor, callback); + synchronized (mInterfaceLock) { + checkIfCameraClosedOrInError(); + for (Surface surface : surfaces) { + if (surface == null) { + throw new IllegalArgumentException("Null Surface targets are not allowed"); + } + } + // In shared session mode, if there are other active clients streaming then + // stoprepeating does not actually send request to HAL to cancel the request. + // Cameraservice will use this call to remove this client surfaces provided in its + // previous streaming request. If this is the only client for the shared camera device + // then camerservice will ask HAL to cancel the previous repeating request + stopRepeating(); + + // StartStreaming API does not allow capture parameters to be provided through a capture + // request. If the primary client has an existing repeating request, the camera service + // will either attach the provided surfaces to that request or create a default capture + // request if no repeating request is active. A default capture request is created here + // for initial use. The capture callback will provide capture results that include the + // actual capture parameters used for the streaming. + CaptureRequest.Builder builder = createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); + for (Surface surface : surfaces) { + builder.addTarget(surface); + } + CaptureRequest request = builder.build(); + request.convertSurfaceToStreamId(mConfiguredOutputs); + + SubmitInfo requestInfo; + requestInfo = mRemoteDevice.startStreaming(request.getStreamIds(), + request.getSurfaceIds()); + request.recoverStreamIdToSurface(); + List<CaptureRequest> requestList = new ArrayList<CaptureRequest>(); + requestList.add(request); + + if (callback != null) { + mCaptureCallbackMap.put(requestInfo.getRequestId(), + new CaptureCallbackHolder( + callback, requestList, executor, true, mNextSessionId - 1)); + } else { + if (DEBUG) { + Log.d(TAG, "Listen for request " + requestInfo.getRequestId() + " is null"); + } + } + + if (mRepeatingRequestId != REQUEST_ID_NONE) { + checkEarlyTriggerSequenceCompleteLocked(mRepeatingRequestId, + requestInfo.getLastFrameNumber(), mRepeatingRequestTypes); + } + + CaptureRequest[] requestArray = requestList.toArray( + new CaptureRequest[requestList.size()]); + mRepeatingRequestId = requestInfo.getRequestId(); + mRepeatingRequestTypes = getRequestTypes(requestArray); + + if (mIdle) { + mDeviceExecutor.execute(mCallOnActive); + } + mIdle = false; + + return requestInfo.getRequestId(); + } + } + public int setRepeatingRequest(CaptureRequest request, CaptureCallback callback, Executor executor) throws CameraAccessException { List<CaptureRequest> requestList = new ArrayList<CaptureRequest>(); @@ -2883,6 +2981,11 @@ public class CameraDeviceImpl extends CameraDevice @Override public void createExtensionSession(ExtensionSessionConfiguration extensionConfiguration) throws CameraAccessException { + if (Flags.cameraMultiClient() && mSharedMode) { + throw new UnsupportedOperationException("In shared session mode," + + "extension sessions are not supported."); + } + HashMap<String, CameraCharacteristics> characteristicsMap = new HashMap<>( getPhysicalIdToChars()); characteristicsMap.put(mCameraId, mCharacteristics); @@ -2918,4 +3021,4 @@ public class CameraDeviceImpl extends CameraDevice } } } -}
\ No newline at end of file +} diff --git a/core/java/android/hardware/camera2/impl/CameraSharedCaptureSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraSharedCaptureSessionImpl.java index a1f31c0ced5e..8c0dcfb2a28c 100644 --- a/core/java/android/hardware/camera2/impl/CameraSharedCaptureSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraSharedCaptureSessionImpl.java @@ -19,6 +19,8 @@ import android.annotation.FlaggedApi; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraOfflineSession; +import android.hardware.camera2.CameraOfflineSession.CameraOfflineSessionCallback; import android.hardware.camera2.CameraSharedCaptureSession; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.params.OutputConfiguration; @@ -28,6 +30,7 @@ import android.view.Surface; import com.android.internal.camera.flags.Flags; +import java.util.Collection; import java.util.List; import java.util.concurrent.Executor; @@ -46,7 +49,8 @@ public class CameraSharedCaptureSessionImpl private static final String TAG = "CameraSharedCaptureSessionImpl"; private final CameraCaptureSessionImpl mSessionImpl; private final ConditionVariable mInitialized = new ConditionVariable(); - private boolean mIsPrimary; + private final android.hardware.camera2.impl.CameraDeviceImpl mCameraDevice; + private final Executor mDeviceExecutor; /** * Create a new CameraCaptureSession. @@ -54,24 +58,32 @@ public class CameraSharedCaptureSessionImpl CameraSharedCaptureSessionImpl(int id, CameraCaptureSession.StateCallback callback, Executor stateExecutor, android.hardware.camera2.impl.CameraDeviceImpl deviceImpl, - Executor deviceStateExecutor, boolean configureSuccess, boolean isPrimary) { + Executor deviceStateExecutor, boolean configureSuccess) { CameraCaptureSession.StateCallback wrapperCallback = new WrapperCallback(callback); mSessionImpl = new CameraCaptureSessionImpl(id, /*input*/null, wrapperCallback, stateExecutor, deviceImpl, deviceStateExecutor, configureSuccess); - mIsPrimary = isPrimary; + mCameraDevice = deviceImpl; + mDeviceExecutor = deviceStateExecutor; mInitialized.open(); } @Override - public int startStreaming(List<Surface> surfaces, Executor executor, CaptureCallback listener) + public int startStreaming(List<Surface> surfaces, Executor executor, CaptureCallback callback) throws CameraAccessException { - // Todo: Need to add implementation. - return 0; + if (surfaces.isEmpty()) { + throw new IllegalArgumentException("No surfaces provided for streaming"); + } else if (executor == null) { + throw new IllegalArgumentException("executor must not be null"); + } else if (callback == null) { + throw new IllegalArgumentException("callback must not be null"); + } + + return mSessionImpl.startStreaming(surfaces, executor, callback); } @Override public void stopStreaming() throws CameraAccessException { - // Todo: Need to add implementation. + mSessionImpl.stopRepeating(); } @Override @@ -90,16 +102,24 @@ public class CameraSharedCaptureSessionImpl } @Override + public boolean supportsOfflineProcessing(Surface surface) { + return false; + } + + @Override public void abortCaptures() throws CameraAccessException { - if (mIsPrimary) { + if (mCameraDevice.isPrimaryClient()) { mSessionImpl.abortCaptures(); + return; } + throw new UnsupportedOperationException("Shared capture session only supports this method" + + " for primary clients"); } @Override public int setRepeatingRequest(CaptureRequest request, CaptureCallback listener, Handler handler) throws CameraAccessException { - if (mIsPrimary) { + if (mCameraDevice.isPrimaryClient()) { return mSessionImpl.setRepeatingRequest(request, listener, handler); } throw new UnsupportedOperationException("Shared capture session only supports this method" @@ -107,16 +127,30 @@ public class CameraSharedCaptureSessionImpl } @Override + public int setSingleRepeatingRequest(CaptureRequest request, Executor executor, + CaptureCallback listener) + throws CameraAccessException { + if (mCameraDevice.isPrimaryClient()) { + return mSessionImpl.setSingleRepeatingRequest(request, executor, listener); + } + throw new UnsupportedOperationException("Shared capture session only supports this method" + + " for primary clients"); + } + + @Override public void stopRepeating() throws CameraAccessException { - if (mIsPrimary) { + if (mCameraDevice.isPrimaryClient()) { mSessionImpl.stopRepeating(); + return; } + throw new UnsupportedOperationException("Shared capture session only supports this method" + + " for primary clients"); } @Override public int capture(CaptureRequest request, CaptureCallback listener, Handler handler) throws CameraAccessException { - if (mIsPrimary) { + if (mCameraDevice.isPrimaryClient()) { return mSessionImpl.capture(request, listener, handler); } throw new UnsupportedOperationException("Shared capture session only supports this method" @@ -124,6 +158,17 @@ public class CameraSharedCaptureSessionImpl } @Override + public int captureSingleRequest(CaptureRequest request, Executor executor, + CaptureCallback listener) + throws CameraAccessException { + if (mCameraDevice.isPrimaryClient()) { + return mSessionImpl.captureSingleRequest(request, executor, listener); + } + throw new UnsupportedOperationException("Shared capture session only supports this method" + + " for primary clients"); + } + + @Override public void tearDown(Surface surface) throws CameraAccessException { mSessionImpl.tearDown(surface); } @@ -149,48 +194,72 @@ public class CameraSharedCaptureSessionImpl } @Override + public CameraOfflineSession switchToOffline(Collection<Surface> offlineSurfaces, + Executor executor, CameraOfflineSessionCallback listener) + throws CameraAccessException { + throw new UnsupportedOperationException("Shared capture session do not support this method" + ); + } + + @Override public int setRepeatingBurst(List<CaptureRequest> requests, CaptureCallback listener, Handler handler) throws CameraAccessException { - throw new UnsupportedOperationException("Shared Capture session doesn't support" + throw new UnsupportedOperationException("Shared Capture session do not support" + + " this method"); + } + + @Override + public int setRepeatingBurstRequests(List<CaptureRequest> requests, + Executor executor, CaptureCallback listener) + throws CameraAccessException { + throw new UnsupportedOperationException("Shared Capture session do not support" + " this method"); } @Override public int captureBurst(List<CaptureRequest> requests, CaptureCallback listener, Handler handler) throws CameraAccessException { - throw new UnsupportedOperationException("Shared Capture session doesn't support" + throw new UnsupportedOperationException("Shared Capture session do not support" + + " this method"); + } + + @Override + public int captureBurstRequests(List<CaptureRequest> requests, + Executor executor, CaptureCallback listener) + throws CameraAccessException { + throw new UnsupportedOperationException("Shared Capture session do not support" + " this method"); } @Override public void updateOutputConfiguration(OutputConfiguration config) throws CameraAccessException { - throw new UnsupportedOperationException("Shared capture session doesn't support" + throw new UnsupportedOperationException("Shared capture session do not support" + " this method"); } @Override public void finalizeOutputConfigurations(List<OutputConfiguration> deferredOutputConfigs) throws CameraAccessException { - throw new UnsupportedOperationException("Shared capture session doesn't support" + throw new UnsupportedOperationException("Shared capture session do not support" + " this method"); } @Override public void prepare(Surface surface) throws CameraAccessException { - throw new UnsupportedOperationException("Shared capture session doesn't support" + throw new UnsupportedOperationException("Shared capture session do not support" + " this method"); } @Override public void prepare(int maxCount, Surface surface) throws CameraAccessException { - throw new UnsupportedOperationException("Shared capture session doesn't support" + throw new UnsupportedOperationException("Shared capture session do not support" + " this method"); } @Override public void closeWithoutDraining() { - throw new UnsupportedOperationException("Shared capture session doesn't support" + throw new UnsupportedOperationException("Shared capture session do not support" + " this method"); } diff --git a/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java b/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java index a79e084b7f41..0b8e9c2687c3 100644 --- a/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java +++ b/core/java/android/hardware/camera2/impl/ICameraDeviceUserWrapper.java @@ -65,6 +65,17 @@ public class ICameraDeviceUserWrapper { } } + public SubmitInfo startStreaming(int[] streamIdxArray, int[] surfaceIdxArray) + throws CameraAccessException { + try { + return mRemoteDevice.startStreaming(streamIdxArray, surfaceIdxArray); + } catch (ServiceSpecificException e) { + throw ExceptionUtils.throwAsPublicException(e); + } catch (RemoteException e) { + throw ExceptionUtils.throwAsPublicException(e); + } + } + public SubmitInfo submitRequest(CaptureRequest request, boolean streaming) throws CameraAccessException { try { @@ -325,4 +336,4 @@ public class ICameraDeviceUserWrapper { } } -}
\ No newline at end of file +} diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java index e12c46322d8c..d394154a2c0e 100644 --- a/core/java/android/hardware/camera2/params/OutputConfiguration.java +++ b/core/java/android/hardware/camera2/params/OutputConfiguration.java @@ -1803,6 +1803,19 @@ public final class OutputConfiguration implements Parcelable { } /** + * Get the flag indicating if this {@link OutputConfiguration} is for a multi-resolution output + * with a MultiResolutionImageReader. + * + * @return true if this {@link OutputConfiguration} is for a multi-resolution output with a + * MultiResolutionImageReader. + * + * @hide + */ + public boolean isMultiResolution() { + return mIsMultiResolution; + } + + /** * Get the physical camera ID associated with this {@link OutputConfiguration}. * * <p>If this OutputConfiguration isn't targeting a physical camera of a logical diff --git a/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java b/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java index cdcc92ce4404..365f870ba22d 100644 --- a/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java +++ b/core/java/android/hardware/camera2/params/SharedSessionConfiguration.java @@ -212,7 +212,7 @@ public final class SharedSessionConfiguration { } public @Nullable String getPhysicalCameraId() { - return mPhysicalCameraId; + return mPhysicalCameraId.isEmpty() ? null : mPhysicalCameraId; } } diff --git a/core/java/android/hardware/contexthub/HubEndpoint.java b/core/java/android/hardware/contexthub/HubEndpoint.java index 99f331f43450..de88895ba55c 100644 --- a/core/java/android/hardware/contexthub/HubEndpoint.java +++ b/core/java/android/hardware/contexthub/HubEndpoint.java @@ -99,8 +99,8 @@ public class HubEndpoint { private final Object mLock = new Object(); private final HubEndpointInfo mPendingHubEndpointInfo; - @Nullable private final IHubEndpointLifecycleCallback mLifecycleCallback; - @Nullable private final IHubEndpointMessageCallback mMessageCallback; + @Nullable private final HubEndpointLifecycleCallback mLifecycleCallback; + @Nullable private final HubEndpointMessageCallback mMessageCallback; @NonNull private final Executor mLifecycleCallbackExecutor; @NonNull private final Executor mMessageCallbackExecutor; @@ -335,9 +335,9 @@ public class HubEndpoint { private HubEndpoint( @NonNull HubEndpointInfo pendingEndpointInfo, - @Nullable IHubEndpointLifecycleCallback endpointLifecycleCallback, + @Nullable HubEndpointLifecycleCallback endpointLifecycleCallback, @NonNull Executor lifecycleCallbackExecutor, - @Nullable IHubEndpointMessageCallback endpointMessageCallback, + @Nullable HubEndpointMessageCallback endpointMessageCallback, @NonNull Executor messageCallbackExecutor) { mPendingHubEndpointInfo = pendingEndpointInfo; mLifecycleCallback = endpointLifecycleCallback; @@ -485,12 +485,12 @@ public class HubEndpoint { } @Nullable - public IHubEndpointLifecycleCallback getLifecycleCallback() { + public HubEndpointLifecycleCallback getLifecycleCallback() { return mLifecycleCallback; } @Nullable - public IHubEndpointMessageCallback getMessageCallback() { + public HubEndpointMessageCallback getMessageCallback() { return mMessageCallback; } @@ -498,11 +498,11 @@ public class HubEndpoint { public static final class Builder { private final String mPackageName; - @Nullable private IHubEndpointLifecycleCallback mLifecycleCallback; + @Nullable private HubEndpointLifecycleCallback mLifecycleCallback; @NonNull private Executor mLifecycleCallbackExecutor; - @Nullable private IHubEndpointMessageCallback mMessageCallback; + @Nullable private HubEndpointMessageCallback mMessageCallback; @NonNull private Executor mMessageCallbackExecutor; private int mVersion; @@ -539,10 +539,13 @@ public class HubEndpoint { return this; } - /** Attach a callback interface for lifecycle events for this Endpoint */ + /** + * Attach a callback interface for lifecycle events for this Endpoint. Callback will be + * posted to the main thread. + */ @NonNull public Builder setLifecycleCallback( - @NonNull IHubEndpointLifecycleCallback lifecycleCallback) { + @NonNull HubEndpointLifecycleCallback lifecycleCallback) { mLifecycleCallback = lifecycleCallback; return this; } @@ -554,15 +557,18 @@ public class HubEndpoint { @NonNull public Builder setLifecycleCallback( @NonNull @CallbackExecutor Executor executor, - @NonNull IHubEndpointLifecycleCallback lifecycleCallback) { + @NonNull HubEndpointLifecycleCallback lifecycleCallback) { mLifecycleCallbackExecutor = executor; mLifecycleCallback = lifecycleCallback; return this; } - /** Attach a callback interface for message events for this Endpoint */ + /** + * Attach a callback interface for message events for this Endpoint. Callback will be posted + * to the main thread. + */ @NonNull - public Builder setMessageCallback(@NonNull IHubEndpointMessageCallback messageCallback) { + public Builder setMessageCallback(@NonNull HubEndpointMessageCallback messageCallback) { mMessageCallback = messageCallback; return this; } @@ -574,7 +580,7 @@ public class HubEndpoint { @NonNull public Builder setMessageCallback( @NonNull @CallbackExecutor Executor executor, - @NonNull IHubEndpointMessageCallback messageCallback) { + @NonNull HubEndpointMessageCallback messageCallback) { mMessageCallbackExecutor = executor; mMessageCallback = messageCallback; return this; diff --git a/core/java/android/hardware/contexthub/IHubEndpointDiscoveryCallback.java b/core/java/android/hardware/contexthub/HubEndpointDiscoveryCallback.java index a61a7ebd0de9..4672bbb74170 100644 --- a/core/java/android/hardware/contexthub/IHubEndpointDiscoveryCallback.java +++ b/core/java/android/hardware/contexthub/HubEndpointDiscoveryCallback.java @@ -30,7 +30,7 @@ import java.util.List; */ @SystemApi @FlaggedApi(Flags.FLAG_OFFLOAD_API) -public interface IHubEndpointDiscoveryCallback { +public interface HubEndpointDiscoveryCallback { /** * Called when a list of hub endpoints have started. * diff --git a/core/java/android/hardware/contexthub/IHubEndpointLifecycleCallback.java b/core/java/android/hardware/contexthub/HubEndpointLifecycleCallback.java index 698ed0adfd80..6d75010711cc 100644 --- a/core/java/android/hardware/contexthub/IHubEndpointLifecycleCallback.java +++ b/core/java/android/hardware/contexthub/HubEndpointLifecycleCallback.java @@ -29,7 +29,7 @@ import android.chre.flags.Flags; */ @SystemApi @FlaggedApi(Flags.FLAG_OFFLOAD_API) -public interface IHubEndpointLifecycleCallback { +public interface HubEndpointLifecycleCallback { /** * Called when an endpoint is requesting a session be opened with another endpoint. * diff --git a/core/java/android/hardware/contexthub/IHubEndpointMessageCallback.java b/core/java/android/hardware/contexthub/HubEndpointMessageCallback.java index fde7017b5e76..5db54efc8893 100644 --- a/core/java/android/hardware/contexthub/IHubEndpointMessageCallback.java +++ b/core/java/android/hardware/contexthub/HubEndpointMessageCallback.java @@ -26,18 +26,18 @@ import android.chre.flags.Flags; * <p>This interface can be attached to an endpoint through {@link * HubEndpoint.Builder#setMessageCallback} method. Methods in this interface will only be called * when the endpoint is currently registered and has an open session. The endpoint will receive - * session lifecycle callbacks through {@link IHubEndpointLifecycleCallback}. + * session lifecycle callbacks through {@link HubEndpointLifecycleCallback}. * * @hide */ @SystemApi @FlaggedApi(Flags.FLAG_OFFLOAD_API) -public interface IHubEndpointMessageCallback { +public interface HubEndpointMessageCallback { /** * Callback interface for receiving messages for a particular endpoint session. * * @param session The session this message is sent through. Previously specified in a {@link - * IHubEndpointLifecycleCallback#onSessionOpened(HubEndpointSession)} call. + * HubEndpointLifecycleCallback#onSessionOpened(HubEndpointSession)} call. * @param message The {@link HubMessage} object representing a message received by the endpoint * that registered this callback interface. This message is constructed by the */ diff --git a/core/java/android/hardware/contexthub/HubEndpointSession.java b/core/java/android/hardware/contexthub/HubEndpointSession.java index 77f937ebeabc..f7f5636264e4 100644 --- a/core/java/android/hardware/contexthub/HubEndpointSession.java +++ b/core/java/android/hardware/contexthub/HubEndpointSession.java @@ -137,7 +137,7 @@ public class HubEndpointSession implements AutoCloseable { * no service associated to this session. * * <p>For hub initiated sessions, the object was previously used in as an argument for open - * request in {@link IHubEndpointLifecycleCallback#onSessionOpenRequest}. + * request in {@link HubEndpointLifecycleCallback#onSessionOpenRequest}. * * <p>For app initiated sessions, the object was previously used in an open request in {@link * android.hardware.location.ContextHubManager#openSession} diff --git a/core/java/android/hardware/contexthub/HubEndpointSessionResult.java b/core/java/android/hardware/contexthub/HubEndpointSessionResult.java index 1f2bdb985008..b193d00467aa 100644 --- a/core/java/android/hardware/contexthub/HubEndpointSessionResult.java +++ b/core/java/android/hardware/contexthub/HubEndpointSessionResult.java @@ -23,7 +23,7 @@ import android.annotation.SystemApi; import android.chre.flags.Flags; /** - * Return type of {@link IHubEndpointLifecycleCallback#onSessionOpenRequest}. The value determines + * Return type of {@link HubEndpointLifecycleCallback#onSessionOpenRequest}. The value determines * whether a open session request from the remote is accepted or not. * * @hide diff --git a/core/java/android/hardware/contexthub/HubServiceInfo.java b/core/java/android/hardware/contexthub/HubServiceInfo.java index 2f33e8f8872b..651be3b17c33 100644 --- a/core/java/android/hardware/contexthub/HubServiceInfo.java +++ b/core/java/android/hardware/contexthub/HubServiceInfo.java @@ -112,6 +112,7 @@ public final class HubServiceInfo implements Parcelable { * <p>The value can be one of {@link HubServiceInfo#FORMAT_CUSTOM}, {@link * HubServiceInfo#FORMAT_AIDL} or {@link HubServiceInfo#FORMAT_PW_RPC_PROTOBUF}. */ + @ServiceFormat public int getFormat() { return mFormat; } @@ -178,7 +179,8 @@ public final class HubServiceInfo implements Parcelable { * <li>Pigweed RPC with Protobuf: com.example.proto.ExampleService * </ol> * - * @param serviceDescriptor The service descriptor. + * @param serviceDescriptor The service descriptor for the interface, provided by the + * vendor. * @param format One of {@link HubServiceInfo#FORMAT_CUSTOM}, {@link * HubServiceInfo#FORMAT_AIDL} or {@link HubServiceInfo#FORMAT_PW_RPC_PROTOBUF}. * @param majorVersion Breaking changes should be a major version bump. diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index ffa546067eff..9030810a1c1a 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -106,7 +106,7 @@ public final class DisplayManagerGlobal { @IntDef(prefix = {"EVENT_DISPLAY_"}, flag = true, value = { EVENT_DISPLAY_ADDED, - EVENT_DISPLAY_CHANGED, + EVENT_DISPLAY_BASIC_CHANGED, EVENT_DISPLAY_REMOVED, EVENT_DISPLAY_BRIGHTNESS_CHANGED, EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED, @@ -119,7 +119,8 @@ public final class DisplayManagerGlobal { public @interface DisplayEvent {} public static final int EVENT_DISPLAY_ADDED = 1; - public static final int EVENT_DISPLAY_CHANGED = 2; + public static final int EVENT_DISPLAY_BASIC_CHANGED = 2; + public static final int EVENT_DISPLAY_REMOVED = 3; public static final int EVENT_DISPLAY_BRIGHTNESS_CHANGED = 4; public static final int EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED = 5; @@ -130,7 +131,7 @@ public final class DisplayManagerGlobal { @LongDef(prefix = {"INTERNAL_EVENT_FLAG_"}, flag = true, value = { INTERNAL_EVENT_FLAG_DISPLAY_ADDED, - INTERNAL_EVENT_FLAG_DISPLAY_CHANGED, + INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED, INTERNAL_EVENT_FLAG_DISPLAY_REMOVED, INTERNAL_EVENT_FLAG_DISPLAY_BRIGHTNESS_CHANGED, INTERNAL_EVENT_FLAG_DISPLAY_HDR_SDR_RATIO_CHANGED, @@ -143,7 +144,7 @@ public final class DisplayManagerGlobal { public @interface InternalEventFlag {} public static final long INTERNAL_EVENT_FLAG_DISPLAY_ADDED = 1L << 0; - public static final long INTERNAL_EVENT_FLAG_DISPLAY_CHANGED = 1L << 1; + public static final long INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED = 1L << 1; public static final long INTERNAL_EVENT_FLAG_DISPLAY_REMOVED = 1L << 2; public static final long INTERNAL_EVENT_FLAG_DISPLAY_BRIGHTNESS_CHANGED = 1L << 3; public static final long INTERNAL_EVENT_FLAG_DISPLAY_HDR_SDR_RATIO_CHANGED = 1L << 4; @@ -485,7 +486,7 @@ public final class DisplayManagerGlobal { // There can be racing condition between DMS and WMS callbacks, so force triggering the // listener to make sure the client can get the onDisplayChanged callback even if // DisplayInfo is not changed (Display read from both DisplayInfo and WindowConfiguration). - handleDisplayEvent(displayId, EVENT_DISPLAY_CHANGED, true /* forceUpdate */); + handleDisplayEvent(displayId, EVENT_DISPLAY_BASIC_CHANGED, true /* forceUpdate */); } private static Looper getLooperForHandler(@Nullable Handler handler) { @@ -518,7 +519,8 @@ public final class DisplayManagerGlobal { } if (mDispatchNativeCallbacks) { mask |= INTERNAL_EVENT_FLAG_DISPLAY_ADDED - | INTERNAL_EVENT_FLAG_DISPLAY_CHANGED + | INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED + | INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE | INTERNAL_EVENT_FLAG_DISPLAY_REMOVED; } if (!mTopologyListeners.isEmpty()) { @@ -571,7 +573,8 @@ public final class DisplayManagerGlobal { } info = getDisplayInfoLocked(displayId); - if (event == EVENT_DISPLAY_CHANGED && mDispatchNativeCallbacks) { + if ((event == EVENT_DISPLAY_BASIC_CHANGED + || event == EVENT_DISPLAY_REFRESH_RATE_CHANGED) && mDispatchNativeCallbacks) { // Choreographer only supports a single display, so only dispatch refresh rate // changes for the default display. if (displayId == Display.DEFAULT_DISPLAY) { @@ -1492,9 +1495,9 @@ public final class DisplayManagerGlobal { mListener.onDisplayAdded(displayId); } break; - case EVENT_DISPLAY_CHANGED: - if ((mInternalEventFlagsMask & INTERNAL_EVENT_FLAG_DISPLAY_CHANGED) - != 0) { + case EVENT_DISPLAY_BASIC_CHANGED: + if ((mInternalEventFlagsMask + & INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED) != 0) { if (info != null && (forceUpdate || !info.equals(mDisplayInfo))) { if (extraLogging()) { Slog.i(TAG, "Sending onDisplayChanged: Display Changed. Info: " @@ -1691,8 +1694,8 @@ public final class DisplayManagerGlobal { switch (event) { case EVENT_DISPLAY_ADDED: return "ADDED"; - case EVENT_DISPLAY_CHANGED: - return "CHANGED"; + case EVENT_DISPLAY_BASIC_CHANGED: + return "BASIC_CHANGED"; case EVENT_DISPLAY_REMOVED: return "REMOVED"; case EVENT_DISPLAY_BRIGHTNESS_CHANGED: @@ -1763,7 +1766,11 @@ public final class DisplayManagerGlobal { } if ((eventFlags & DisplayManager.EVENT_FLAG_DISPLAY_CHANGED) != 0) { - baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_CHANGED; + // For backward compatibility, a client subscribing to + // DisplayManager.EVENT_FLAG_DISPLAY_CHANGED will be enrolled to both Basic and + // RR changes + baseEventMask |= INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED + | INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE; } if ((eventFlags diff --git a/core/java/android/hardware/display/DisplayTopology.java b/core/java/android/hardware/display/DisplayTopology.java index 211aefffa34c..ba5dfc094afb 100644 --- a/core/java/android/hardware/display/DisplayTopology.java +++ b/core/java/android/hardware/display/DisplayTopology.java @@ -129,14 +129,38 @@ public final class DisplayTopology implements Parcelable { } /** + * Update the size of a display and normalize the topology. + * @param displayId The logical display ID + * @param width The new width + * @param height The new height + * @return True if the topology has changed. + */ + public boolean updateDisplay(int displayId, float width, float height) { + TreeNode display = findDisplay(displayId, mRoot); + if (display == null) { + return false; + } + if (floatEquals(display.mWidth, width) && floatEquals(display.mHeight, height)) { + return false; + } + display.mWidth = width; + display.mHeight = height; + normalize(); + Slog.i(TAG, "Display with ID " + displayId + " updated, new width: " + width + + ", new height: " + height); + return true; + } + + /** * Remove a display from the topology. * The default topology is created from the remaining displays, as if they were reconnected * one by one. * @param displayId The logical display ID + * @return True if the display was present in the topology and removed. */ - public void removeDisplay(int displayId) { + public boolean removeDisplay(int displayId) { if (findDisplay(displayId, mRoot) == null) { - return; + return false; } Queue<TreeNode> queue = new ArrayDeque<>(); queue.add(mRoot); @@ -159,6 +183,7 @@ public final class DisplayTopology implements Parcelable { } else { Slog.i(TAG, "Display with ID " + displayId + " removed"); } + return true; } /** @@ -685,12 +710,12 @@ public final class DisplayTopology implements Parcelable { /** * The width of the display in density-independent pixels (dp). */ - private final float mWidth; + private float mWidth; /** * The height of the display in density-independent pixels (dp). */ - private final float mHeight; + private float mHeight; /** * The position of this display relative to its parent. diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index 313bad50e88e..c4d11cd8aff7 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -204,3 +204,10 @@ flag { description: "Allows the user to disable input scrolling acceleration for mouse." bug: "383555305" } + +flag { + name: "remove_fallback_modifiers" + namespace: "input" + description: "Removes modifiers from the original key event that activated the fallback, ensuring that only the intended fallback event is sent." + bug: "382545048" +} diff --git a/core/java/android/hardware/location/ContextHubManager.java b/core/java/android/hardware/location/ContextHubManager.java index 1e0cc94612dd..0cd320981c93 100644 --- a/core/java/android/hardware/location/ContextHubManager.java +++ b/core/java/android/hardware/location/ContextHubManager.java @@ -36,11 +36,11 @@ import android.content.pm.PackageManager; import android.hardware.contexthub.ErrorCode; import android.hardware.contexthub.HubDiscoveryInfo; import android.hardware.contexthub.HubEndpoint; +import android.hardware.contexthub.HubEndpointDiscoveryCallback; import android.hardware.contexthub.HubEndpointInfo; +import android.hardware.contexthub.HubEndpointLifecycleCallback; import android.hardware.contexthub.HubServiceInfo; import android.hardware.contexthub.IContextHubEndpointDiscoveryCallback; -import android.hardware.contexthub.IHubEndpointDiscoveryCallback; -import android.hardware.contexthub.IHubEndpointLifecycleCallback; import android.os.Handler; import android.os.HandlerExecutor; import android.os.Looper; @@ -207,7 +207,7 @@ public final class ContextHubManager { private Handler mCallbackHandler; /** A map of endpoint discovery callbacks currently registered */ - private Map<IHubEndpointDiscoveryCallback, IContextHubEndpointDiscoveryCallback> + private Map<HubEndpointDiscoveryCallback, IContextHubEndpointDiscoveryCallback> mDiscoveryCallbacks = new ConcurrentHashMap<>(); /** @@ -718,7 +718,19 @@ public final class ContextHubManager { /** * Find a list of endpoints that provides a specific service. * - * @param serviceDescriptor Statically generated ID for an endpoint. + * <p>Service descriptor should uniquely identify the interface (scoped to type). Convention of + * the descriptor depend on interface type. + * + * <p>Examples: + * + * <ol> + * <li>AOSP-defined AIDL: android.hardware.something.IFoo/default + * <li>Vendor-defined AIDL: com.example.something.IBar/default + * <li>Pigweed RPC with Protobuf: com.example.proto.ExampleService + * </ol> + * + * @param serviceDescriptor The service descriptor for a service provided by the hub. The value + * cannot be null or empty. * @return A list of {@link HubDiscoveryInfo} objects that represents the result of discovery. * @throws IllegalArgumentException if the serviceDescriptor is empty/null. */ @@ -750,14 +762,15 @@ public final class ContextHubManager { /** * Creates an interface to invoke endpoint discovery callbacks to send down to the service. * - * @param callback the callback to invoke at the client process * @param executor the executor to invoke callbacks for this client + * @param callback the callback to invoke at the client process + * @param serviceDescriptor an optional descriptor to match discovery list with * @return the callback interface */ @FlaggedApi(Flags.FLAG_OFFLOAD_API) private IContextHubEndpointDiscoveryCallback createDiscoveryCallback( - IHubEndpointDiscoveryCallback callback, Executor executor, + HubEndpointDiscoveryCallback callback, @Nullable String serviceDescriptor) { return new IContextHubEndpointDiscoveryCallback.Stub() { @Override @@ -829,36 +842,36 @@ public final class ContextHubManager { } /** - * Equivalent to {@link #registerEndpointDiscoveryCallback(long, IHubEndpointDiscoveryCallback, - * Executor)} with the default executor in the main thread. + * Equivalent to {@link #registerEndpointDiscoveryCallback(Executor, + * HubEndpointDiscoveryCallback, long)} with the default executor in the main thread. */ @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) @FlaggedApi(Flags.FLAG_OFFLOAD_API) public void registerEndpointDiscoveryCallback( - long endpointId, @NonNull IHubEndpointDiscoveryCallback callback) { + @NonNull HubEndpointDiscoveryCallback callback, long endpointId) { registerEndpointDiscoveryCallback( - endpointId, callback, new HandlerExecutor(Handler.getMain())); + new HandlerExecutor(Handler.getMain()), callback, endpointId); } /** * Registers a callback to be notified when the hub endpoint with the corresponding endpoint ID * has started or stopped. * - * @param endpointId The identifier of the hub endpoint. - * @param callback The callback to be invoked. * @param executor The executor to invoke the callback on. + * @param callback The callback to be invoked. + * @param endpointId The identifier of the hub endpoint. * @throws UnsupportedOperationException If the operation is not supported. */ @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) @FlaggedApi(Flags.FLAG_OFFLOAD_API) public void registerEndpointDiscoveryCallback( - long endpointId, - @NonNull IHubEndpointDiscoveryCallback callback, - @NonNull Executor executor) { - Objects.requireNonNull(callback, "callback cannot be null"); + @NonNull Executor executor, + @NonNull HubEndpointDiscoveryCallback callback, + long endpointId) { Objects.requireNonNull(executor, "executor cannot be null"); + Objects.requireNonNull(callback, "callback cannot be null"); IContextHubEndpointDiscoveryCallback iCallback = - createDiscoveryCallback(callback, executor, null); + createDiscoveryCallback(executor, callback, null); try { mService.registerEndpointDiscoveryCallbackId(endpointId, iCallback); } catch (RemoteException e) { @@ -869,42 +882,42 @@ public final class ContextHubManager { } /** - * Equivalent to {@link #registerEndpointDiscoveryCallback(String, - * IHubEndpointDiscoveryCallback, Executor)} with the default executor in the main thread. + * Equivalent to {@link #registerEndpointDiscoveryCallback(Executor, + * HubEndpointDiscoveryCallback, String)} with the default executor in the main thread. */ @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) @FlaggedApi(Flags.FLAG_OFFLOAD_API) public void registerEndpointDiscoveryCallback( - @NonNull String serviceDescriptor, @NonNull IHubEndpointDiscoveryCallback callback) { + @NonNull HubEndpointDiscoveryCallback callback, @NonNull String serviceDescriptor) { registerEndpointDiscoveryCallback( - serviceDescriptor, callback, new HandlerExecutor(Handler.getMain())); + new HandlerExecutor(Handler.getMain()), callback, serviceDescriptor); } /** * Registers a callback to be notified when the hub endpoint with the corresponding service * descriptor has started or stopped. * + * @param executor The executor to invoke the callback on. * @param serviceDescriptor The service descriptor of the hub endpoint. * @param callback The callback to be invoked. - * @param executor The executor to invoke the callback on. * @throws IllegalArgumentException if the serviceDescriptor is empty. * @throws UnsupportedOperationException If the operation is not supported. */ @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) @FlaggedApi(Flags.FLAG_OFFLOAD_API) public void registerEndpointDiscoveryCallback( - @NonNull String serviceDescriptor, - @NonNull IHubEndpointDiscoveryCallback callback, - @NonNull Executor executor) { - Objects.requireNonNull(serviceDescriptor, "serviceDescriptor cannot be null"); - Objects.requireNonNull(callback, "callback cannot be null"); + @NonNull Executor executor, + @NonNull HubEndpointDiscoveryCallback callback, + @NonNull String serviceDescriptor) { Objects.requireNonNull(executor, "executor cannot be null"); + Objects.requireNonNull(callback, "callback cannot be null"); + Objects.requireNonNull(serviceDescriptor, "serviceDescriptor cannot be null"); if (serviceDescriptor.isBlank()) { throw new IllegalArgumentException("Invalid service descriptor: " + serviceDescriptor); } IContextHubEndpointDiscoveryCallback iCallback = - createDiscoveryCallback(callback, executor, serviceDescriptor); + createDiscoveryCallback(executor, callback, serviceDescriptor); try { mService.registerEndpointDiscoveryCallbackDescriptor(serviceDescriptor, iCallback); } catch (RemoteException e) { @@ -924,7 +937,7 @@ public final class ContextHubManager { @RequiresPermission(android.Manifest.permission.ACCESS_CONTEXT_HUB) @FlaggedApi(Flags.FLAG_OFFLOAD_API) public void unregisterEndpointDiscoveryCallback( - @NonNull IHubEndpointDiscoveryCallback callback) { + @NonNull HubEndpointDiscoveryCallback callback) { Objects.requireNonNull(callback, "callback cannot be null"); IContextHubEndpointDiscoveryCallback iCallback = mDiscoveryCallbacks.remove(callback); if (iCallback == null) { @@ -1291,7 +1304,7 @@ public final class ContextHubManager { * service. * * <p>Context Hub Service will create the endpoint session and notify the registered endpoint. - * The registered endpoint will receive callbacks on its {@link IHubEndpointLifecycleCallback} + * The registered endpoint will receive callbacks on its {@link HubEndpointLifecycleCallback} * object regarding the lifecycle events of the session. * * @param hubEndpoint {@link HubEndpoint} object previously registered via {@link @@ -1311,7 +1324,7 @@ public final class ContextHubManager { * described by a {@link HubServiceInfo} object. * * <p>Context Hub Service will create the endpoint session and notify the registered endpoint. - * The registered endpoint will receive callbacks on its {@link IHubEndpointLifecycleCallback} + * The registered endpoint will receive callbacks on its {@link HubEndpointLifecycleCallback} * object regarding the lifecycle events of the session. * * @param hubEndpoint {@link HubEndpoint} object previously registered via {@link diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java index 8a0adfaede8a..ecd90e46e432 100644 --- a/core/java/android/os/BaseBundle.java +++ b/core/java/android/os/BaseBundle.java @@ -480,10 +480,10 @@ public class BaseBundle implements Parcel.ClassLoaderProvider { map.erase(); map.ensureCapacity(count); } - int numLazyValues = 0; + int[] numLazyValues = new int[]{0}; try { - numLazyValues = parcelledData.readArrayMap(map, count, !parcelledByNative, - /* lazy */ ownsParcel, this); + parcelledData.readArrayMap(map, count, !parcelledByNative, + /* lazy */ ownsParcel, this, numLazyValues); } catch (BadParcelableException e) { if (sShouldDefuse) { Log.w(TAG, "Failed to parse Bundle, but defusing quietly", e); @@ -494,14 +494,14 @@ public class BaseBundle implements Parcel.ClassLoaderProvider { } finally { mWeakParcelledData = null; if (ownsParcel) { - if (numLazyValues == 0) { + if (numLazyValues[0] == 0) { recycleParcel(parcelledData); } else { mWeakParcelledData = new WeakReference<>(parcelledData); } } - mLazyValues = numLazyValues; + mLazyValues = numLazyValues[0]; mParcelledByNative = false; mMap = map; // Set field last as it is volatile diff --git a/core/java/android/os/BinderProxy.java b/core/java/android/os/BinderProxy.java index 01222cdd38b3..18d2afb8b1b5 100644 --- a/core/java/android/os/BinderProxy.java +++ b/core/java/android/os/BinderProxy.java @@ -687,12 +687,18 @@ public final class BinderProxy implements IBinder { return removeFrozenStateChangeCallbackNative(wrappedCallback); } + public static boolean isFrozenStateChangeCallbackSupported() { + return isFrozenStateChangeCallbackSupportedNative(); + } + private native void addFrozenStateChangeCallbackNative(FrozenStateChangeCallback callback) throws RemoteException; private native boolean removeFrozenStateChangeCallbackNative( FrozenStateChangeCallback callback); + private static native boolean isFrozenStateChangeCallbackSupportedNative(); + /** * Perform a dump on the remote object * diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index 4bbc61c9f115..5ba6553a58c9 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -5565,7 +5565,7 @@ public final class Parcel { private void readArrayMapInternal(@NonNull ArrayMap<? super String, Object> outVal, int size, @Nullable ClassLoaderProvider loaderProvider) { - readArrayMap(outVal, size, /* sorted */ true, /* lazy */ false, loaderProvider); + readArrayMap(outVal, size, /* sorted */ true, /* lazy */ false, loaderProvider, null); } /** @@ -5575,18 +5575,17 @@ public final class Parcel { * @param lazy Whether to populate the map with lazy {@link Function} objects for * length-prefixed values. See {@link Parcel#readLazyValue(ClassLoader)} for more * details. - * @return a count of the lazy values in the map + * @param lazyValueCount number of lazy values added here * @hide */ - int readArrayMap(ArrayMap<? super String, Object> map, int size, boolean sorted, - boolean lazy, @Nullable ClassLoaderProvider loaderProvider) { - int lazyValues = 0; + void readArrayMap(ArrayMap<? super String, Object> map, int size, boolean sorted, + boolean lazy, @Nullable ClassLoaderProvider loaderProvider, int[] lazyValueCount) { while (size > 0) { String key = readString(); Object value = (lazy) ? readLazyValue(loaderProvider) : readValue( getClassLoader(loaderProvider)); if (value instanceof LazyValue) { - lazyValues++; + lazyValueCount[0]++; } if (sorted) { map.append(key, value); @@ -5598,7 +5597,6 @@ public final class Parcel { if (sorted) { map.validate(); } - return lazyValues; } /** diff --git a/core/java/android/os/TestLooperManager.java b/core/java/android/os/TestLooperManager.java index e2169925fdd3..289b98c9b1d4 100644 --- a/core/java/android/os/TestLooperManager.java +++ b/core/java/android/os/TestLooperManager.java @@ -41,6 +41,7 @@ public class TestLooperManager { private boolean mReleased; private boolean mLooperBlocked; + private final boolean mLooperIsMyLooper; /** * @hide @@ -54,8 +55,11 @@ public class TestLooperManager { } mLooper = looper; mQueue = mLooper.getQueue(); - // Post a message that will keep the looper blocked as long as we are dispatching. - new Handler(looper).post(new LooperHolder()); + mLooperIsMyLooper = Looper.myLooper() == looper; + if (!mLooperIsMyLooper) { + // Post a message that will keep the looper blocked as long as we are dispatching. + new Handler(looper).post(new LooperHolder()); + } } /** @@ -82,7 +86,7 @@ public class TestLooperManager { public Message next() { // Wait for the looper block to come up, to make sure we don't accidentally get // the message for the block. - while (!mLooperBlocked) { + while (!mLooperIsMyLooper && !mLooperBlocked) { synchronized (this) { try { wait(); @@ -114,9 +118,6 @@ public class TestLooperManager { * should be executed by this queue. * If the queue is empty or no messages are deliverable, returns null. * This method never blocks. - * - * <p>Callers should always call {@link #recycle(Message)} on the message when all interactions - * with it have completed. */ @FlaggedApi(Flags.FLAG_MESSAGE_QUEUE_TESTABILITY) @SuppressWarnings("AutoBoxing") // box the primitive long, or return null to indicate no value @@ -165,6 +166,9 @@ public class TestLooperManager { // This is being called from the thread it should be executed on, we can just dispatch. message.target.dispatchMessage(message); } else { + if (mLooperIsMyLooper) { + throw new RuntimeException("Cannot call execute from non Looper thread"); + } MessageExecution execution = new MessageExecution(); execution.m = message; synchronized (execution) { diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 132805da7c94..507bcb8c2717 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -940,10 +940,10 @@ public class UserManager { /** * Specifies if a user is disallowed from resetting network settings - * from Settings. This can only be set by device owners and profile owners on the primary user. + * from Settings. This can only be set by device owners and profile owners on the main user. * The default value is <code>false</code>. - * <p>This restriction has no effect on secondary users and managed profiles since only the - * primary user can reset the network settings of the device. + * <p>This restriction has no effect on non-Admin users since they cannot reset the network + * settings of the device. * * <p>Holders of the permission * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_MOBILE_NETWORK} @@ -1077,11 +1077,11 @@ public class UserManager { /** * Specifies if a user is disallowed from configuring cell broadcasts. * - * <p>This restriction can only be set by a device owner, a profile owner on the primary + * <p>This restriction can only be set by a device owner, a profile owner on the main * user or a profile owner of an organization-owned managed profile on the parent profile. * When it is set by a device owner, it applies globally. When it is set by a profile owner - * on the primary user or by a profile owner of an organization-owned managed profile on - * the parent profile, it disables the primary user from configuring cell broadcasts. + * on the main user or by a profile owner of an organization-owned managed profile on + * the parent profile, it disables the user from configuring cell broadcasts. * * <p>Holders of the permission * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_MOBILE_NETWORK} @@ -1089,8 +1089,8 @@ public class UserManager { * * <p>The default value is <code>false</code>. * - * <p>This restriction has no effect on secondary users and managed profiles since only the - * primary user can configure cell broadcasts. + * <p>This restriction has no effect on non-Admin users since they cannot configure cell + * broadcasts. * * <p>Key for user restrictions. * <p>Type: Boolean @@ -1103,11 +1103,11 @@ public class UserManager { /** * Specifies if a user is disallowed from configuring mobile networks. * - * <p>This restriction can only be set by a device owner, a profile owner on the primary + * <p>This restriction can only be set by a device owner, a profile owner on the main * user or a profile owner of an organization-owned managed profile on the parent profile. * When it is set by a device owner, it applies globally. When it is set by a profile owner - * on the primary user or by a profile owner of an organization-owned managed profile on - * the parent profile, it disables the primary user from configuring mobile networks. + * on the main user or by a profile owner of an organization-owned managed profile on + * the parent profile, it disables the user from configuring mobile networks. * * <p>Holders of the permission * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_MOBILE_NETWORK} @@ -1115,8 +1115,8 @@ public class UserManager { * * <p>The default value is <code>false</code>. * - * <p>This restriction has no effect on secondary users and managed profiles since only the - * primary user can configure mobile networks. + * <p>This restriction has no effect on non-Admin users since they cannot configure mobile + * networks. * * <p>Key for user restrictions. * <p>Type: Boolean diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index c3a49305af87..6898fcef23ab 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -1296,6 +1296,22 @@ public final class Settings { public static final String ACTION_LOCKSCREEN_SETTINGS = "android.settings.LOCK_SCREEN_SETTINGS"; /** + * Activity Action: Show settings of notifications on lockscreen. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + * + * @hide + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_LOCKSCREEN_NOTIFICATIONS_SETTINGS = + "android.settings.LOCK_SCREEN_NOTIFICATIONS_SETTINGS"; + + /** * Activity Action: Show settings to allow pairing bluetooth devices. * <p> * In some cases, a matching Activity may not exist, so ensure you diff --git a/core/java/android/service/quickaccesswallet/QuickAccessWalletService.java b/core/java/android/service/quickaccesswallet/QuickAccessWalletService.java index 90136ae00f6a..ffe8086ca4a1 100644 --- a/core/java/android/service/quickaccesswallet/QuickAccessWalletService.java +++ b/core/java/android/service/quickaccesswallet/QuickAccessWalletService.java @@ -93,6 +93,10 @@ import android.util.Log; * must do its own state management (keeping in mind that the service's process might be killed * by the Android System when unbound; for example, if the device is running low in memory). * + * <p> The service also provides pending intents to override the system's Quick Access activities + * via the {@link #getTargetActivityPendingIntent} and the + * {@link #getGestureTargetActivityPendingIntent} method. + * * <p> * <a name="ErrorHandling"></a> * <h3>Error handling</h3> @@ -384,6 +388,10 @@ public abstract class QuickAccessWalletService extends Service { * * <p>The pending intent will be sent when the user performs a gesture to open Wallet. * The pending intent should launch an activity. + * + * <p> If the gesture is performed and this method returns null, the system will launch the + * activity specified by the {@link #getTargetActivityPendingIntent} method. If that method + * also returns null, the system will launch the system-provided card switcher activity. */ @Nullable @FlaggedApi(Flags.FLAG_LAUNCH_WALLET_OPTION_ON_POWER_DOUBLE_TAP) diff --git a/core/java/android/service/settings/preferences/GetValueRequest.java b/core/java/android/service/settings/preferences/GetValueRequest.java index 4f82800d1855..db5c57c49595 100644 --- a/core/java/android/service/settings/preferences/GetValueRequest.java +++ b/core/java/android/service/settings/preferences/GetValueRequest.java @@ -108,6 +108,7 @@ public final class GetValueRequest implements Parcelable { /** * Builder to construct {@link GetValueRequest}. */ + @FlaggedApi(Flags.FLAG_SETTINGS_CATALYST) public static final class Builder { private final String mScreenKey; private final String mPreferenceKey; diff --git a/core/java/android/service/settings/preferences/GetValueResult.java b/core/java/android/service/settings/preferences/GetValueResult.java index 369dea77cc85..791131588034 100644 --- a/core/java/android/service/settings/preferences/GetValueResult.java +++ b/core/java/android/service/settings/preferences/GetValueResult.java @@ -170,6 +170,7 @@ public final class GetValueResult implements Parcelable { /** * Builder to construct {@link GetValueResult}. */ + @FlaggedApi(Flags.FLAG_SETTINGS_CATALYST) public static final class Builder { @ResultCode private final int mResultCode; diff --git a/core/java/android/service/settings/preferences/MetadataRequest.java b/core/java/android/service/settings/preferences/MetadataRequest.java index ffecc6bec5b2..e0417152eedc 100644 --- a/core/java/android/service/settings/preferences/MetadataRequest.java +++ b/core/java/android/service/settings/preferences/MetadataRequest.java @@ -65,6 +65,7 @@ public final class MetadataRequest implements Parcelable { /** * Builder to construct {@link MetadataRequest}. */ + @FlaggedApi(Flags.FLAG_SETTINGS_CATALYST) public static final class Builder { /** Constructs an immutable {@link MetadataRequest} object. */ @NonNull diff --git a/core/java/android/service/settings/preferences/MetadataResult.java b/core/java/android/service/settings/preferences/MetadataResult.java index 6a65dcc9c757..e62fa8f38c93 100644 --- a/core/java/android/service/settings/preferences/MetadataResult.java +++ b/core/java/android/service/settings/preferences/MetadataResult.java @@ -131,6 +131,7 @@ public final class MetadataResult implements Parcelable { /** * Builder to construct {@link MetadataResult}. */ + @FlaggedApi(Flags.FLAG_SETTINGS_CATALYST) public static final class Builder { @ResultCode private final int mResultCode; diff --git a/core/java/android/service/settings/preferences/SetValueRequest.java b/core/java/android/service/settings/preferences/SetValueRequest.java index f7600aecdfaf..77581d9deffe 100644 --- a/core/java/android/service/settings/preferences/SetValueRequest.java +++ b/core/java/android/service/settings/preferences/SetValueRequest.java @@ -123,6 +123,7 @@ public final class SetValueRequest implements Parcelable { /** * Builder to construct {@link SetValueRequest}. */ + @FlaggedApi(Flags.FLAG_SETTINGS_CATALYST) public static final class Builder { private final String mScreenKey; private final String mPreferenceKey; diff --git a/core/java/android/service/settings/preferences/SetValueResult.java b/core/java/android/service/settings/preferences/SetValueResult.java index cb1776abd3bc..513f7a7d5bcc 100644 --- a/core/java/android/service/settings/preferences/SetValueResult.java +++ b/core/java/android/service/settings/preferences/SetValueResult.java @@ -156,6 +156,7 @@ public final class SetValueResult implements Parcelable { /** * Builder to construct {@link SetValueResult}. */ + @FlaggedApi(Flags.FLAG_SETTINGS_CATALYST) public static final class Builder { @ResultCode private final int mResultCode; diff --git a/core/java/android/service/settings/preferences/SettingsPreferenceMetadata.java b/core/java/android/service/settings/preferences/SettingsPreferenceMetadata.java index ea7d4a675713..30631f2fd71d 100644 --- a/core/java/android/service/settings/preferences/SettingsPreferenceMetadata.java +++ b/core/java/android/service/settings/preferences/SettingsPreferenceMetadata.java @@ -102,6 +102,7 @@ public final class SettingsPreferenceMetadata implements Parcelable { /** * Returns the breadcrumbs (navigation context) of Preference. * <p>May be empty. + * @hide restrict to platform; may be opened wider in the future */ @NonNull public List<String> getBreadcrumbs() { @@ -189,33 +190,32 @@ public final class SettingsPreferenceMetadata implements Parcelable { @IntDef(value = { NO_SENSITIVITY, EXPECT_POST_CONFIRMATION, - EXPECT_PRE_CONFIRMATION, + DEEPLINK_ONLY, NO_DIRECT_ACCESS, }) @Retention(RetentionPolicy.SOURCE) public @interface WriteSensitivity {} /** - * Indicates preference is not sensitive. + * Indicates preference is not write-sensitive. * <p>Its value is writable without explicit consent, assuming all necessary permissions are * granted. */ public static final int NO_SENSITIVITY = 0; /** - * Indicates preference is mildly sensitive. + * Indicates preference is mildly write-sensitive. * <p>In addition to necessary permissions, after writing its value the user should be * given the option to revert back. */ public static final int EXPECT_POST_CONFIRMATION = 1; /** - * Indicates preference is sensitive. - * <p>In addition to necessary permissions, the user should be prompted for confirmation prior - * to making a change. Otherwise it is suggested to provide a deeplink to the Preference's page - * instead, accessible via {@link #getLaunchIntent}. + * Indicates preference is write-sensitive. + * <p>This preference cannot be changed through this API; instead a deeplink to the Preference's + * page should be used instead, accessible via {@link #getLaunchIntent}. */ - public static final int EXPECT_PRE_CONFIRMATION = 2; + public static final int DEEPLINK_ONLY = 2; /** - * Indicates preference is highly sensitivity and carries significant user-risk. + * Indicates preference is highly write-sensitivity and carries significant user-risk. * <p>This Preference cannot be changed through this API and no direct deeplink is available. * Other Metadata is still available. */ @@ -303,6 +303,7 @@ public final class SettingsPreferenceMetadata implements Parcelable { /** * Builder to construct {@link SettingsPreferenceMetadata}. */ + @FlaggedApi(Flags.FLAG_SETTINGS_CATALYST) public static final class Builder { private final String mScreenKey; private final String mKey; @@ -355,6 +356,7 @@ public final class SettingsPreferenceMetadata implements Parcelable { /** * Sets the preference breadcrumbs (navigation context). + * @hide */ @NonNull public Builder setBreadcrumbs(@NonNull List<String> breadcrumbs) { diff --git a/core/java/android/service/settings/preferences/SettingsPreferenceValue.java b/core/java/android/service/settings/preferences/SettingsPreferenceValue.java index 08826ca9776b..eea93b321e47 100644 --- a/core/java/android/service/settings/preferences/SettingsPreferenceValue.java +++ b/core/java/android/service/settings/preferences/SettingsPreferenceValue.java @@ -170,6 +170,7 @@ public final class SettingsPreferenceValue implements Parcelable { /** * Builder to construct {@link SettingsPreferenceValue}. */ + @FlaggedApi(Flags.FLAG_SETTINGS_CATALYST) public static final class Builder { @Type private final int mType; diff --git a/core/java/android/tracing/flags.aconfig b/core/java/android/tracing/flags.aconfig index fb1bd1703ce6..6116d599baa0 100644 --- a/core/java/android/tracing/flags.aconfig +++ b/core/java/android/tracing/flags.aconfig @@ -70,3 +70,11 @@ flag { is_fixed_read_only: true bug: "352538294" } + +flag { + name: "system_server_large_perfetto_shmem_buffer" + namespace: "windowing_tools" + description: "Large perfetto shmem buffer" + is_fixed_read_only: true + bug: "382369925" +} diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java index 089b5c256b6e..6c50b5f945a5 100644 --- a/core/java/android/view/Choreographer.java +++ b/core/java/android/view/Choreographer.java @@ -16,6 +16,7 @@ package android.view; +import static android.view.flags.Flags.bufferStuffingRecovery; import static android.view.flags.Flags.FLAG_EXPECTED_PRESENTATION_TIME_API; import static android.view.DisplayEventReceiver.VSYNC_SOURCE_APP; import static android.view.DisplayEventReceiver.VSYNC_SOURCE_SURFACE_FLINGER; @@ -965,22 +966,24 @@ public final class Choreographer { // Evaluate if buffer stuffing recovery needs to start or end, and // what actions need to be taken for recovery. - switch (updateBufferStuffingState(frameTimeNanos, vsyncEventData)) { - case NONE: - // Without buffer stuffing recovery, offsetFrameTimeNanos is - // synonymous with frameTimeNanos. - break; - case OFFSET: - // Add animation offset. Used to update frame timeline with - // offset before jitter is calculated. - offsetFrameTimeNanos = frameTimeNanos - frameIntervalNanos; - break; - case DELAY_FRAME: - // Intentional frame delay to help reduce queued buffer count. - scheduleVsyncLocked(); - return; - default: - break; + if (bufferStuffingRecovery()) { + switch (updateBufferStuffingState(frameTimeNanos, vsyncEventData)) { + case NONE: + // Without buffer stuffing recovery, offsetFrameTimeNanos is + // synonymous with frameTimeNanos. + break; + case OFFSET: + // Add animation offset. Used to update frame timeline with + // offset before jitter is calculated. + offsetFrameTimeNanos = frameTimeNanos - frameIntervalNanos; + break; + case DELAY_FRAME: + // Intentional frame delay to help reduce queued buffer count. + scheduleVsyncLocked(); + return; + default: + break; + } } try { diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index 0c8a0d60a96a..ca0959af3ff8 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -1597,7 +1597,9 @@ public final class Display { // Although we only care about the HDR/SDR ratio changing, that can also come in the // form of the larger DISPLAY_CHANGED event mGlobal.registerDisplayListener(toRegister, executor, - DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_CHANGED + DisplayManagerGlobal + .INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED + | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE | DisplayManagerGlobal .INTERNAL_EVENT_FLAG_DISPLAY_HDR_SDR_RATIO_CHANGED, ActivityThread.currentPackageName()); diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java index ba098eb53246..e75b1b0bd17a 100644 --- a/core/java/android/view/DisplayInfo.java +++ b/core/java/android/view/DisplayInfo.java @@ -447,7 +447,18 @@ public final class DisplayInfo implements Parcelable { } public boolean equals(DisplayInfo other) { - return other != null + return equals(other, /* compareRefreshRate */ true); + } + + /** + * Compares if the two DisplayInfo objects are equal or not + * @param other The other DisplayInfo against which the comparison is to be done + * @param compareRefreshRate Indicates if the refresh rate is also to be considered in + * comparison + * @return + */ + public boolean equals(DisplayInfo other, boolean compareRefreshRate) { + boolean isEqualWithoutRefreshRate = other != null && layerStack == other.layerStack && flags == other.flags && type == other.type @@ -466,7 +477,6 @@ public final class DisplayInfo implements Parcelable { && logicalHeight == other.logicalHeight && Objects.equals(displayCutout, other.displayCutout) && rotation == other.rotation - && modeId == other.modeId && hasArrSupport == other.hasArrSupport && Objects.equals(frameRateCategoryRate, other.frameRateCategoryRate) && Arrays.equals(supportedRefreshRates, other.supportedRefreshRates) @@ -490,7 +500,6 @@ public final class DisplayInfo implements Parcelable { && ownerUid == other.ownerUid && Objects.equals(ownerPackageName, other.ownerPackageName) && removeMode == other.removeMode - && getRefreshRate() == other.getRefreshRate() && brightnessMinimum == other.brightnessMinimum && brightnessMaximum == other.brightnessMaximum && brightnessDefault == other.brightnessDefault @@ -504,6 +513,13 @@ public final class DisplayInfo implements Parcelable { && Objects.equals( thermalBrightnessThrottlingDataId, other.thermalBrightnessThrottlingDataId) && canHostTasks == other.canHostTasks; + + if (compareRefreshRate) { + return isEqualWithoutRefreshRate + && (getRefreshRate() == other.getRefreshRate()) + && (modeId == other.modeId); + } + return isEqualWithoutRefreshRate; } @Override diff --git a/core/java/android/view/KeyCharacterMap.java b/core/java/android/view/KeyCharacterMap.java index a8d4e2d2c70a..48dfdd4a95f4 100644 --- a/core/java/android/view/KeyCharacterMap.java +++ b/core/java/android/view/KeyCharacterMap.java @@ -16,6 +16,9 @@ package android.view; + +import static com.android.hardware.input.Flags.removeFallbackModifiers; + import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; @@ -458,7 +461,15 @@ public class KeyCharacterMap implements Parcelable { FallbackAction action = FallbackAction.obtain(); metaState = KeyEvent.normalizeMetaState(metaState); if (nativeGetFallbackAction(mPtr, keyCode, metaState, action)) { - action.metaState = KeyEvent.normalizeMetaState(action.metaState); + if (removeFallbackModifiers()) { + // Strip all modifiers. This is safe to do since only exact keyCode + metaState + // modifiers will trigger a fallback. + // E.g. Ctrl + Space -> language_switch (fallback generated) + // Ctrl + Alt + Space -> Ctrl + Alt + Space (no fallback generated) + action.metaState = 0; + } else { + action.metaState = KeyEvent.normalizeMetaState(action.metaState); + } return action; } action.recycle(); diff --git a/core/java/android/view/Surface.java b/core/java/android/view/Surface.java index 6e6e87bb9403..4fc1cfc0ca82 100644 --- a/core/java/android/view/Surface.java +++ b/core/java/android/view/Surface.java @@ -206,7 +206,8 @@ public class Surface implements Parcelable { @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = {"FRAME_RATE_COMPATIBILITY_"}, value = {FRAME_RATE_COMPATIBILITY_DEFAULT, FRAME_RATE_COMPATIBILITY_FIXED_SOURCE, - FRAME_RATE_COMPATIBILITY_GTE}) + FRAME_RATE_COMPATIBILITY_AT_LEAST, FRAME_RATE_COMPATIBILITY_EXACT, + FRAME_RATE_COMPATIBILITY_MIN}) public @interface FrameRateCompatibility {} // From native_window.h. Keep these in sync. @@ -219,7 +220,7 @@ public class Surface implements Parcelable { * In Android version {@link Build.VERSION_CODES#BAKLAVA} and above, use * {@link FRAME_RATE_COMPATIBILITY_DEFAULT} for game content. * For other cases, see {@link FRAME_RATE_COMPATIBILITY_FIXED_SOURCE} and - * {@link FRAME_RATE_COMPATIBILITY_GTE}. + * {@link FRAME_RATE_COMPATIBILITY_AT_LEAST}. */ public static final int FRAME_RATE_COMPATIBILITY_DEFAULT = 0; @@ -234,7 +235,7 @@ public class Surface implements Parcelable { public static final int FRAME_RATE_COMPATIBILITY_FIXED_SOURCE = 1; /** - * The surface requests a frame rate that is greater than or equal to the specified frame rate. + * The surface requests a frame rate that is at least the specified frame rate. * This value should be used for UIs, animations, scrolling and fling, and anything that is not * a game or video. * @@ -242,7 +243,7 @@ public class Surface implements Parcelable { * {@link FRAME_RATE_COMPATIBILITY_DEFAULT}. */ @FlaggedApi(com.android.graphics.surfaceflinger.flags.Flags.FLAG_ARR_SETFRAMERATE_GTE_ENUM) - public static final int FRAME_RATE_COMPATIBILITY_GTE = 2; + public static final int FRAME_RATE_COMPATIBILITY_AT_LEAST = 2; /** * This surface belongs to an app on the High Refresh Rate Deny list, and needs the display diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index f22505b80948..833f2d98554e 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -22,7 +22,6 @@ import static android.graphics.Matrix.MSKEW_X; import static android.graphics.Matrix.MSKEW_Y; import static android.graphics.Matrix.MTRANS_X; import static android.graphics.Matrix.MTRANS_Y; -import static android.view.flags.Flags.bufferStuffingRecovery; import static android.view.SurfaceControlProto.HASH_CODE; import static android.view.SurfaceControlProto.LAYER_ID; import static android.view.SurfaceControlProto.NAME; @@ -5118,11 +5117,9 @@ public final class SurfaceControl implements Parcelable { */ @NonNull public Transaction setRecoverableFromBufferStuffing(@NonNull SurfaceControl sc) { - if (bufferStuffingRecovery()) { - checkPreconditions(sc); - nativeSetFlags(mNativeObject, sc.mNativeObject, RECOVERABLE_FROM_BUFFER_STUFFING, - RECOVERABLE_FROM_BUFFER_STUFFING); - } + checkPreconditions(sc); + nativeSetFlags(mNativeObject, sc.mNativeObject, RECOVERABLE_FROM_BUFFER_STUFFING, + RECOVERABLE_FROM_BUFFER_STUFFING); return this; } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index d13f0e21bf80..d88b6d642ee6 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -27,7 +27,7 @@ import static android.view.Surface.FRAME_RATE_CATEGORY_LOW; import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL; import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE; import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; -import static android.view.Surface.FRAME_RATE_COMPATIBILITY_GTE; +import static android.view.Surface.FRAME_RATE_COMPATIBILITY_AT_LEAST; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED; import static android.view.accessibility.Flags.FLAG_DEPRECATE_ACCESSIBILITY_ANNOUNCEMENT_APIS; @@ -34199,7 +34199,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, && viewRootImpl.shouldCheckFrameRateCategory() && parent instanceof View && ((View) parent).getFrameContentVelocity() <= 0 - && !isInputMethodWindowType) { + && !isInputMethodWindowType + && viewRootImpl.getFrameRateCompatibility() != FRAME_RATE_COMPATIBILITY_AT_LEAST) { return FRAME_RATE_CATEGORY_HIGH_HINT | FRAME_RATE_CATEGORY_REASON_BOOST; } @@ -34251,7 +34252,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, compatibility = FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; frameRateToSet = frameRate; } else { - compatibility = FRAME_RATE_COMPATIBILITY_GTE; + compatibility = FRAME_RATE_COMPATIBILITY_AT_LEAST; frameRateToSet = velocityFrameRate; } viewRootImpl.votePreferredFrameRate(frameRateToSet, compatibility); diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 1d27574eca8c..16cdb64f62cc 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -38,7 +38,7 @@ import static android.view.Surface.FRAME_RATE_CATEGORY_LOW; import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL; import static android.view.Surface.FRAME_RATE_CATEGORY_NO_PREFERENCE; import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; -import static android.view.Surface.FRAME_RATE_COMPATIBILITY_GTE; +import static android.view.Surface.FRAME_RATE_COMPATIBILITY_AT_LEAST; import static android.view.View.FRAME_RATE_CATEGORY_REASON_BOOST; import static android.view.View.FRAME_RATE_CATEGORY_REASON_CONFLICTED; import static android.view.View.FRAME_RATE_CATEGORY_REASON_INTERMITTENT; @@ -1828,7 +1828,8 @@ public final class ViewRootImpl implements ViewParent, | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_STATE | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REMOVED : DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED - | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_CHANGED + | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED + | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REMOVED; DisplayManagerGlobal .getInstance() @@ -13271,7 +13272,7 @@ public final class ViewRootImpl implements ViewParent, * We set category to HIGH if the maximum frame rate is greater than 60. * Otherwise, we set category to NORMAL. * - * Use FRAME_RATE_COMPATIBILITY_GTE for velocity and FRAME_RATE_COMPATIBILITY_FIXED_SOURCE + * Use FRAME_RATE_COMPATIBILITY_AT_LEAST for velocity and FRAME_RATE_COMPATIBILITY_FIXED_SOURCE * for TextureView video play and user requested frame rate. * * @param frameRate the preferred frame rate of a View @@ -13282,7 +13283,7 @@ public final class ViewRootImpl implements ViewParent, if (frameRate <= 0) { return; } - if (frameRateCompatibility == FRAME_RATE_COMPATIBILITY_GTE && !mIsPressedGesture) { + if (frameRateCompatibility == FRAME_RATE_COMPATIBILITY_AT_LEAST && !mIsPressedGesture) { mIsTouchBoosting = false; mIsFrameRateBoosting = false; if (!sToolkitFrameRateVelocityMappingReadOnlyFlagValue) { diff --git a/core/java/android/view/contentcapture/ChildContentCaptureSession.java b/core/java/android/view/contentcapture/ChildContentCaptureSession.java index 70c899f1efc7..8baa55f8e377 100644 --- a/core/java/android/view/contentcapture/ChildContentCaptureSession.java +++ b/core/java/android/view/contentcapture/ChildContentCaptureSession.java @@ -142,6 +142,11 @@ final class ChildContentCaptureSession extends ContentCaptureSession { } @Override + void internalNotifySessionFlushEvent(int sessionId) { + getMainCaptureSession().internalNotifySessionFlushEvent(sessionId); + } + + @Override boolean isContentCaptureEnabled() { return getMainCaptureSession().isContentCaptureEnabled(); } diff --git a/core/java/android/view/contentcapture/ContentCaptureEvent.java b/core/java/android/view/contentcapture/ContentCaptureEvent.java index db4ac5de0b49..efd39163c3b8 100644 --- a/core/java/android/view/contentcapture/ContentCaptureEvent.java +++ b/core/java/android/view/contentcapture/ContentCaptureEvent.java @@ -18,7 +18,9 @@ package android.view.contentcapture; import static android.view.contentcapture.ContentCaptureHelper.getSanitizedString; import static android.view.contentcapture.ContentCaptureManager.DEBUG; import static android.view.contentcapture.ContentCaptureManager.NO_SESSION_ID; +import static android.view.contentcapture.flags.Flags.FLAG_CCAPI_BAKLAVA_ENABLED; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -137,6 +139,12 @@ public final class ContentCaptureEvent implements Parcelable { */ public static final int TYPE_WINDOW_BOUNDS_CHANGED = 10; + /** + * Called to flush a semantics meaningful view changes status to Intelligence Service. + */ + @FlaggedApi(FLAG_CCAPI_BAKLAVA_ENABLED) + public static final int TYPE_SESSION_FLUSH = 11; + /** @hide */ @IntDef(prefix = { "TYPE_" }, value = { TYPE_VIEW_APPEARED, @@ -149,6 +157,7 @@ public final class ContentCaptureEvent implements Parcelable { TYPE_SESSION_RESUMED, TYPE_VIEW_INSETS_CHANGED, TYPE_WINDOW_BOUNDS_CHANGED, + TYPE_SESSION_FLUSH, }) @Retention(RetentionPolicy.SOURCE) public @interface EventType{} @@ -697,6 +706,8 @@ public final class ContentCaptureEvent implements Parcelable { return "VIEW_INSETS_CHANGED"; case TYPE_WINDOW_BOUNDS_CHANGED: return "TYPE_WINDOW_BOUNDS_CHANGED"; + case TYPE_SESSION_FLUSH: + return "TYPE_SESSION_FLUSH"; default: return "UKNOWN_TYPE: " + type; } diff --git a/core/java/android/view/contentcapture/ContentCaptureSession.java b/core/java/android/view/contentcapture/ContentCaptureSession.java index 0ca36ba28e3a..9aeec20ec9b7 100644 --- a/core/java/android/view/contentcapture/ContentCaptureSession.java +++ b/core/java/android/view/contentcapture/ContentCaptureSession.java @@ -19,8 +19,10 @@ import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; import static android.view.contentcapture.ContentCaptureHelper.sDebug; import static android.view.contentcapture.ContentCaptureHelper.sVerbose; import static android.view.contentcapture.ContentCaptureManager.NO_SESSION_ID; +import static android.view.contentcapture.flags.Flags.FLAG_CCAPI_BAKLAVA_ENABLED; import android.annotation.CallSuper; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -548,6 +550,35 @@ public abstract class ContentCaptureSession implements AutoCloseable { abstract void internalNotifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets); + /** + * Flushes an internal buffer of UI events and signals System Intelligence (SI) that a + * semantically meaningful state has been reached. SI uses this signal to potentially + * rebuild the view hierarchy and understand the current state of the UI. + * + * <p>UI events are often batched together for performance reasons. A semantic batch + * represents a series of events that, when applied sequentially, result in a + * meaningful and complete UI state. + * + * <p>It is crucial to call {@code flush()} after completing a semantic batch to ensure + * SI can accurately reconstruct the view hierarchy. + * + * <p><b>Premature Flushing:</b> Calling {@code flush()} within a semantic batch may + * lead to SI failing to rebuild the view hierarchy correctly. This could manifest as + * incorrect ordering of sibling nodes. + * + * <p><b>Delayed Flushing:</b> While not immediately flushing after a semantic batch is + * generally safe, it's recommended to do so as soon as possible. In the worst-case + * scenario where a {@code flush()} is never called, SI will attempt to process the + * events after a short delay based on view appearance and disappearance events. + */ + @FlaggedApi(FLAG_CCAPI_BAKLAVA_ENABLED) + public void flush() { + internalNotifySessionFlushEvent(mId); + } + + /** @hide */ + abstract void internalNotifySessionFlushEvent(int sessionId); + /** @hide */ public void notifyViewTreeEvent(boolean started) { internalNotifyViewTreeEvent(mId, started); diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java index eb827dd5258d..2fb78c038ca2 100644 --- a/core/java/android/view/contentcapture/MainContentCaptureSession.java +++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java @@ -17,6 +17,7 @@ package android.view.contentcapture; import static android.view.contentcapture.ContentCaptureEvent.TYPE_CONTEXT_UPDATED; import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_FINISHED; +import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_FLUSH; import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_PAUSED; import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_RESUMED; import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_STARTED; @@ -623,6 +624,8 @@ public final class MainContentCaptureSession extends ContentCaptureSession { @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) @Override public void flush(@FlushReason int reason) { + // TODO: b/380381249 renaming the internal APIs to prevent confusions between this and the + // public API. runOnContentCaptureThread(() -> flushImpl(reason)); } @@ -890,6 +893,12 @@ public final class MainContentCaptureSession extends ContentCaptureSession { enqueueEvent(event); } + @Override + void internalNotifySessionFlushEvent(int sessionId) { + final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, TYPE_SESSION_FLUSH); + enqueueEvent(event, FORCE_FLUSH); + } + private List<ContentCaptureEvent> clearBufferEvents() { final ArrayList<ContentCaptureEvent> bufferEvents = new ArrayList<>(); ContentCaptureEvent event; diff --git a/core/java/android/view/contentcapture/OWNERS b/core/java/android/view/contentcapture/OWNERS index 9ac273f515e7..30f4cae4bf19 100644 --- a/core/java/android/view/contentcapture/OWNERS +++ b/core/java/android/view/contentcapture/OWNERS @@ -1,4 +1,5 @@ # Bug component: 544200 -hackz@google.com -shivanker@google.com +dariofreni@google.com +klikli@google.com +shikhamalhotra@google.com diff --git a/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig b/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig index 416a877d87ab..f709ed7f57cd 100644 --- a/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig +++ b/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig @@ -7,3 +7,10 @@ flag { description: "Feature flag for running content capture tasks on background thread" bug: "309411951" } + +flag { + name: "ccapi_baklava_enabled" + namespace: "machine_learning" + description: "Feature flag for baklava content capture API" + bug: "380381249" +} diff --git a/core/java/android/window/WindowTokenClientController.java b/core/java/android/window/WindowTokenClientController.java index 11019324acd8..fcd7dfbb1769 100644 --- a/core/java/android/window/WindowTokenClientController.java +++ b/core/java/android/window/WindowTokenClientController.java @@ -148,6 +148,9 @@ public class WindowTokenClientController { info = wms.attachWindowContextToDisplayContent(mAppThread, client, displayId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); + } catch (Exception e) { + Log.e(TAG, "Failed attachToDisplayContent", e); + return false; } if (info == null) { return false; diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig index 801698caff0e..b97bf0b54c1a 100644 --- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig +++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig @@ -2,10 +2,10 @@ package: "com.android.window.flags" container: "system" flag { - name: "disable_thin_letterboxing_policy" + name: "ignore_aspect_ratio_restrictions_for_resizeable_freeform_activities" namespace: "large_screen_experiences_app_compat" - description: "Whether reachability is disabled in case of thin letterboxing" - bug: "341027847" + description: "If a resizeable activity enters freeform mode, ignore all aspect ratio constraints." + bug: "381866902" metadata { purpose: PURPOSE_BUGFIX } @@ -58,13 +58,6 @@ flag { } flag { - name: "user_min_aspect_ratio_app_default" - namespace: "large_screen_experiences_app_compat" - description: "Whether the API PackageManager.USER_MIN_ASPECT_RATIO_APP_DEFAULT is available" - bug: "310816437" -} - -flag { name: "allow_hide_scm_button" namespace: "large_screen_experiences_app_compat" description: "Whether we should allow hiding the size compat restart button" @@ -80,16 +73,6 @@ flag { } flag { - name: "immersive_app_repositioning" - namespace: "large_screen_experiences_app_compat" - description: "Fix immersive apps changing size when repositioning" - bug: "334076352" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "camera_compat_for_freeform" namespace: "large_screen_experiences_app_compat" description: "Whether to apply Camera Compat treatment to fixed-orientation apps in freeform windowing mode" diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 9d11d149b0ed..c1ed51264e23 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -368,17 +368,6 @@ flag { } flag { - name: "disallow_app_progress_embedded_window" - namespace: "windowing_frontend" - description: "Pilfer pointers when app transfer input gesture to embedded window." - bug: "365504126" - is_fixed_read_only: true - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "predictive_back_system_override_callback" namespace: "windowing_frontend" description: "Provide pre-make predictive back API extension" @@ -430,4 +419,15 @@ flag { description: "Support insets definition and calculation relative to task bounds." bug: "277292497" is_fixed_read_only: true +} + +flag { + name: "exclude_drawing_app_theme_snapshot_from_lock" + namespace: "windowing_frontend" + description: "Do not hold wm lock when drawing app theme snapshot." + is_fixed_read_only: true + bug: "373502791" + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig index ae846441723a..abd93cfaf179 100644 --- a/core/java/android/window/flags/windowing_sdk.aconfig +++ b/core/java/android/window/flags/windowing_sdk.aconfig @@ -105,6 +105,13 @@ flag { flag { namespace: "windowing_sdk" + name: "activity_embedding_support_for_connected_displays" + description: "Enables activity embedding support for connected displays, including enabling AE optimization for Settings." + bug: "369438353" +} + +flag { + namespace: "windowing_sdk" name: "wlinfo_oncreate" description: "Makes WindowLayoutInfo accessible without racing in the Activity#onCreate()" bug: "337820752" @@ -148,3 +155,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "windowing_sdk" + name: "condense_configuration_change_for_simple_mode" + description: "Condense configuration change for simple mode" + bug: "356738240" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl index 2cfc680a3fe8..f01aa80fab4f 100644 --- a/core/java/com/android/internal/app/IAppOpsService.aidl +++ b/core/java/com/android/internal/app/IAppOpsService.aidl @@ -163,4 +163,5 @@ interface IAppOpsService { void finishOperationForDevice(IBinder clientId, int code, int uid, String packageName, @nullable String attributionTag, int virtualDeviceId); List<AppOpsManager.PackageOps> getPackagesForOpsForDevice(in int[] ops, String persistentDeviceId); + oneway void noteOperationsInBatch(in Map batchedNoteOps); } diff --git a/core/java/com/android/internal/app/ProcessMap.java b/core/java/com/android/internal/app/ProcessMap.java index 542b6d00ca37..b4945e7fd2ec 100644 --- a/core/java/com/android/internal/app/ProcessMap.java +++ b/core/java/com/android/internal/app/ProcessMap.java @@ -28,6 +28,11 @@ public class ProcessMap<E> { if (uids == null) return null; return uids.get(uid); } + + public SparseArray<E> get(String name) { + SparseArray<E> uids = mMap.get(name); + return uids; + } public E put(String name, int uid, E value) { SparseArray<E> uids = mMap.get(name); diff --git a/core/java/com/android/internal/jank/DisplayResolutionTracker.java b/core/java/com/android/internal/jank/DisplayResolutionTracker.java index 0c2fd4bbd7ae..5d66b3c10197 100644 --- a/core/java/com/android/internal/jank/DisplayResolutionTracker.java +++ b/core/java/com/android/internal/jank/DisplayResolutionTracker.java @@ -148,8 +148,8 @@ public class DisplayResolutionTracker { public void registerDisplayListener(DisplayManager.DisplayListener listener) { manager.registerDisplayListener(listener, handler, DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED - | DisplayManagerGlobal - .INTERNAL_EVENT_FLAG_DISPLAY_CHANGED, + | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED + | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE, ActivityThread.currentPackageName()); } diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index 3e2f30118b2a..f14e1f63cdf6 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -236,4 +236,7 @@ interface IStatusBarService /** Shows rear display educational dialog */ void showRearDisplayDialog(int currentBaseState); + + /** Unbundle a categorized notification */ + void unbundleNotification(String key); } diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 9bd52372e6c4..39ddea614ee4 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -25,6 +25,8 @@ import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED import static android.security.Flags.reportPrimaryAuthAttempts; import static android.security.Flags.shouldTrustManagerListenForPrimaryAuth; +import static com.android.internal.widget.flags.Flags.hideLastCharWithPhysicalInput; + import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -42,6 +44,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.UserInfo; +import android.hardware.input.InputManagerGlobal; import android.os.Build; import android.os.Handler; import android.os.Looper; @@ -59,6 +62,7 @@ import android.util.Log; import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.util.SparseLongArray; +import android.view.InputDevice; import com.android.internal.annotations.VisibleForTesting; import com.android.server.LocalServices; @@ -1097,12 +1101,20 @@ public class LockPatternUtils { return type == CREDENTIAL_TYPE_PATTERN; } + private boolean hasActivePointerDeviceAttached() { + return !getEnabledNonTouchInputDevices(InputDevice.SOURCE_CLASS_POINTER).isEmpty(); + } + /** * @return Whether the visible pattern is enabled. */ @UnsupportedAppUsage public boolean isVisiblePatternEnabled(int userId) { - return getBoolean(Settings.Secure.LOCK_PATTERN_VISIBLE, true, userId); + boolean defaultValue = true; + if (hideLastCharWithPhysicalInput()) { + defaultValue = !hasActivePointerDeviceAttached(); + } + return getBoolean(Settings.Secure.LOCK_PATTERN_VISIBLE, defaultValue, userId); } /** @@ -1116,11 +1128,39 @@ public class LockPatternUtils { return getString(Settings.Secure.LOCK_PATTERN_VISIBLE, userId) != null; } + private List<InputDevice> getEnabledNonTouchInputDevices(int source) { + final InputManagerGlobal inputManager = InputManagerGlobal.getInstance(); + final int[] inputIds = inputManager.getInputDeviceIds(); + List<InputDevice> matchingDevices = new ArrayList<InputDevice>(); + for (final int deviceId : inputIds) { + final InputDevice inputDevice = inputManager.getInputDevice(deviceId); + if (!inputDevice.isEnabled()) continue; + if (inputDevice.supportsSource(InputDevice.SOURCE_TOUCHSCREEN)) continue; + if (inputDevice.isVirtual()) continue; + if (!inputDevice.supportsSource(source)) continue; + matchingDevices.add(inputDevice); + } + return matchingDevices; + } + + private boolean hasPhysicalKeyboardActive() { + final List<InputDevice> keyboards = + getEnabledNonTouchInputDevices(InputDevice.SOURCE_KEYBOARD); + for (final InputDevice keyboard : keyboards) { + if (keyboard.isFullKeyboard()) return true; + } + return false; + } + /** * @return Whether enhanced pin privacy is enabled. */ public boolean isPinEnhancedPrivacyEnabled(int userId) { - return getBoolean(LOCK_PIN_ENHANCED_PRIVACY, false, userId); + boolean defaultValue = false; + if (hideLastCharWithPhysicalInput()) { + defaultValue = hasPhysicalKeyboardActive(); + } + return getBoolean(LOCK_PIN_ENHANCED_PRIVACY, defaultValue, userId); } /** diff --git a/core/jni/android_hardware_camera2_CameraDevice.cpp b/core/jni/android_hardware_camera2_CameraDevice.cpp index 493c7073416c..04cfed581750 100644 --- a/core/jni/android_hardware_camera2_CameraDevice.cpp +++ b/core/jni/android_hardware_camera2_CameraDevice.cpp @@ -30,6 +30,7 @@ #include <nativehelper/JNIHelp.h> #include "android_os_Parcel.h" #include "core_jni_helpers.h" +#include <android/binder_auto_utils.h> #include <android/binder_parcel_jni.h> #include <android/hardware/camera2/ICameraDeviceUser.h> #include <aidl/android/hardware/common/fmq/MQDescriptor.h> @@ -40,6 +41,7 @@ using namespace android; using ::android::AidlMessageQueue; +using ndk::ScopedAParcel; using ResultMetadataQueue = AidlMessageQueue<int8_t, SynchronizedReadWrite>; class FMQReader { @@ -75,15 +77,16 @@ extern "C" { static jlong CameraDevice_createFMQReader(JNIEnv *env, jclass thiz, jobject resultParcel) { - AParcel *resultAParcel = AParcel_fromJavaParcel(env, resultParcel); - if (resultAParcel == nullptr) { + ScopedAParcel sResultAParcel(AParcel_fromJavaParcel(env, resultParcel)); + if (sResultAParcel.get() == nullptr) { ALOGE("%s: Error creating result parcel", __FUNCTION__); return 0; } - AParcel_setDataPosition(resultAParcel, 0); + + AParcel_setDataPosition(sResultAParcel.get(), 0); MQDescriptor<int8_t, SynchronizedReadWrite> resultMQ; - if (resultMQ.readFromParcel(resultAParcel) != OK) { + if (resultMQ.readFromParcel(sResultAParcel.get()) != OK) { ALOGE("%s: read from result parcel failed", __FUNCTION__); return 0; } diff --git a/core/jni/android_util_Binder.cpp b/core/jni/android_util_Binder.cpp index 8003bb7d442b..639f5bff7614 100644 --- a/core/jni/android_util_Binder.cpp +++ b/core/jni/android_util_Binder.cpp @@ -1706,6 +1706,10 @@ static jboolean android_os_BinderProxy_removeFrozenStateChangeCallback(JNIEnv* e return res; } +static jboolean android_os_BinderProxy_frozenStateChangeCallbackSupported(JNIEnv*, jclass*) { + return ProcessState::isDriverFeatureEnabled(ProcessState::DriverFeature::FREEZE_NOTIFICATION); +} + static void BinderProxy_destroy(void* rawNativeData) { BinderProxyNativeData * nativeData = (BinderProxyNativeData *) rawNativeData; @@ -1750,6 +1754,8 @@ static const JNINativeMethod gBinderProxyMethods[] = { "(Landroid/os/IBinder$FrozenStateChangeCallback;)V", (void*)android_os_BinderProxy_addFrozenStateChangeCallback}, {"removeFrozenStateChangeCallbackNative", "(Landroid/os/IBinder$FrozenStateChangeCallback;)Z", (void*)android_os_BinderProxy_removeFrozenStateChangeCallback}, + {"isFrozenStateChangeCallbackSupportedNative", + "()Z", (void*)android_os_BinderProxy_frozenStateChangeCallbackSupported}, {"getNativeFinalizer", "()J", (void*)android_os_BinderProxy_getNativeFinalizer}, {"getExtension", "()Landroid/os/IBinder;", (void*)android_os_BinderProxy_getExtension}, }; diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index df989527efe4..ed05e6de0fe0 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -8780,6 +8780,20 @@ android:featureFlag="com.android.art.flags.executable_method_file_offsets" /> <!-- + @SystemApi + @FlaggedApi(android.content.pm.Flags.FLAG_UID_BASED_PROVIDER_LOOKUP) + Allows an app to resolve components (e.g ContentProviders) on behalf of + other UIDs + <p>Protection level: signature|privileged + @hide + --> + <permission + android:name="android.permission.RESOLVE_COMPONENT_FOR_UID" + android:protectionLevel="signature|privileged" + android:featureFlag="android.content.pm.uid_based_provider_lookup" /> + <uses-permission android:name="android.permission.RESOLVE_COMPONENT_FOR_UID" /> + + <!-- @TestApi Signature permission reserved for testing. This should never be used to gate any actual functionality. diff --git a/core/res/res/layout/notification_2025_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml index 76c810bdb2c1..e91e1115ac1c 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_base.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml @@ -157,39 +157,27 @@ android:maxDrawableHeight="@dimen/notification_right_icon_size" /> - <LinearLayout - android:id="@+id/notification_buttons_column" + <FrameLayout + android:id="@+id/expand_button_touch_container" android:layout_width="wrap_content" android:layout_height="match_parent" - android:layout_alignParentEnd="true" - android:orientation="vertical" + android:minWidth="@dimen/notification_content_margin_end" > - <include layout="@layout/notification_close_button" - android:layout_width="@dimen/notification_close_button_size" - android:layout_height="@dimen/notification_close_button_size" - android:layout_gravity="end" - android:layout_marginEnd="20dp" - /> - - <FrameLayout - android:id="@+id/expand_button_touch_container" + <include layout="@layout/notification_expand_button" android:layout_width="wrap_content" - android:layout_height="0dp" - android:layout_weight="1" - android:minWidth="@dimen/notification_content_margin_end" - > - - <include layout="@layout/notification_expand_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center_vertical|end" - /> - - </FrameLayout> + android:layout_height="wrap_content" + android:layout_gravity="center_vertical|end" + /> - </LinearLayout> + </FrameLayout> </LinearLayout> + <include layout="@layout/notification_close_button" + android:id="@+id/close_button" + android:layout_width="@dimen/notification_close_button_size" + android:layout_height="@dimen/notification_close_button_size" + android:layout_gravity="top|end" /> + </FrameLayout> diff --git a/core/res/res/layout/notification_2025_template_collapsed_media.xml b/core/res/res/layout/notification_2025_template_collapsed_media.xml index 2e0a7afc3cd1..2d367337bb6f 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_media.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml @@ -194,4 +194,11 @@ </FrameLayout> </LinearLayout> + + <include layout="@layout/notification_close_button" + android:id="@+id/close_button" + android:layout_width="@dimen/notification_close_button_size" + android:layout_height="@dimen/notification_close_button_size" + android:layout_gravity="top|end" /> + </com.android.internal.widget.MediaNotificationView> diff --git a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml index f644adefda9d..fbecb8c30b9c 100644 --- a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml +++ b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml @@ -199,6 +199,12 @@ </LinearLayout> + <include layout="@layout/notification_close_button" + android:id="@+id/close_button" + android:layout_width="@dimen/notification_close_button_size" + android:layout_height="@dimen/notification_close_button_size" + android:layout_gravity="top|end" /> + </com.android.internal.widget.NotificationMaxHeightFrameLayout> <LinearLayout diff --git a/core/res/res/layout/notification_2025_template_header.xml b/core/res/res/layout/notification_2025_template_header.xml index fc727e1c72f5..2d30d8a8bbb6 100644 --- a/core/res/res/layout/notification_2025_template_header.xml +++ b/core/res/res/layout/notification_2025_template_header.xml @@ -60,7 +60,7 @@ android:layout_height="match_parent" android:layout_alignParentStart="true" android:layout_centerVertical="true" - android:layout_toStartOf="@id/notification_buttons_column" + android:layout_toStartOf="@id/expand_button" android:layout_alignWithParentIfMissing="true" android:clipChildren="false" android:gravity="center_vertical" @@ -81,28 +81,17 @@ android:focusable="false" /> - <LinearLayout - android:id="@+id/notification_buttons_column" + <include layout="@layout/notification_expand_button" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentEnd="true" - android:orientation="vertical" - > - - <include layout="@layout/notification_close_button" - android:layout_width="@dimen/notification_close_button_size" - android:layout_height="@dimen/notification_close_button_size" - android:layout_gravity="end" - android:layout_marginEnd="20dp" - /> - - <include layout="@layout/notification_expand_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignParentEnd="true" - android:layout_centerVertical="true" - /> + android:layout_centerVertical="true" + android:layout_alignParentEnd="true" /> - </LinearLayout> + <include layout="@layout/notification_close_button" + android:id="@+id/close_button" + android:layout_width="@dimen/notification_close_button_size" + android:layout_height="@dimen/notification_close_button_size" + android:layout_alignParentTop="true" + android:layout_alignParentEnd="true" /> </NotificationHeaderView> diff --git a/core/res/res/values-watch/config.xml b/core/res/res/values-watch/config.xml index e28b6462bad7..e6295ea06177 100644 --- a/core/res/res/values-watch/config.xml +++ b/core/res/res/values-watch/config.xml @@ -100,4 +100,7 @@ <!-- Whether to enable scaling and fading animation to scrollviews while scrolling. P.S this is a change only intended for wear devices. --> <bool name="config_enableViewGroupScalingFading">true</bool> + + <!-- Allow the gesture to double tap the power button to trigger a target action. --> + <bool name="config_doubleTapPowerGestureEnabled">false</bool> </resources> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 565e28e0cc87..45a5d85a097d 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -2165,6 +2165,17 @@ config_enableGeofenceOverlay is false. --> <string name="config_geofenceProviderPackageName" translatable="false">@null</string> + <!-- Whether to enable GNSS assistance overlay which allows GnssAssistanceProvider to be + replaced by an app at run-time. When disabled, only the + config_gnssAssistanceProviderPackageName package will be searched for + GnssAssistanceProvider, otherwise any system package is eligible. Anyone who wants to + disable the overlay mechanism can set it to false. + --> + <bool name="config_enableGnssAssistanceOverlay" translatable="false">true</bool> + <!-- Package name providing GNSS assistance API support. Used only when + config_enableGnssAssistanceOverlay is false. --> + <string name="config_gnssAssistanceProviderPackageName" translatable="false">@null</string> + <!-- Whether to enable Hardware Activity-Recognition overlay which allows Hardware Activity-Recognition to be replaced by an app at run-time. When disabled, only the config_activityRecognitionHardwarePackageName package will be searched for diff --git a/core/res/res/values/config_watch.xml b/core/res/res/values/config_watch.xml index 629a343f1280..bcb1e0941b5a 100644 --- a/core/res/res/values/config_watch.xml +++ b/core/res/res/values/config_watch.xml @@ -15,8 +15,7 @@ --> <resources> - <!-- TODO(b/382103556): use predefined Material3 token --> <!-- For Wear Material3 --> - <dimen name="config_wearMaterial3_buttonCornerRadius">26dp</dimen> - <dimen name="config_wearMaterial3_bottomDialogCornerRadius">18dp</dimen> + <dimen name="config_wearMaterial3_buttonCornerRadius">@dimen/config_shapeCornerRadiusLarge</dimen> + <dimen name="config_wearMaterial3_bottomDialogCornerRadius">@dimen/config_shapeCornerRadiusMedium</dimen> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 73e06f6a2520..8195d38993c8 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2040,6 +2040,7 @@ <java-symbol type="bool" name="config_useGnssHardwareProvider" /> <java-symbol type="bool" name="config_enableGeocoderOverlay" /> <java-symbol type="bool" name="config_enableGeofenceOverlay" /> + <java-symbol type="bool" name="config_enableGnssAssistanceOverlay" /> <java-symbol type="bool" name="config_enableNetworkLocationOverlay" /> <java-symbol type="bool" name="config_sf_limitedAlpha" /> <java-symbol type="bool" name="config_unplugTurnsOnScreen" /> @@ -2223,6 +2224,7 @@ <java-symbol type="string" name="config_gnssLocationProviderPackageName" /> <java-symbol type="string" name="config_geocoderProviderPackageName" /> <java-symbol type="string" name="config_geofenceProviderPackageName" /> + <java-symbol type="string" name="config_gnssAssistanceProviderPackageName" /> <java-symbol type="string" name="config_networkLocationProviderPackageName" /> <java-symbol type="string" name="config_wimaxManagerClassname" /> <java-symbol type="string" name="config_wimaxNativeLibLocation" /> diff --git a/core/tests/coretests/res/color/color_with_lstar.xml b/core/tests/coretests/res/color/color_with_lstar.xml index dcc3d6db1b0a..7762fc069ed5 100644 --- a/core/tests/coretests/res/color/color_with_lstar.xml +++ b/core/tests/coretests/res/color/color_with_lstar.xml @@ -16,5 +16,5 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:color="#ff0000" android:lStar="50" /> + <item android:color="@color/testcolor2" android:lStar="50" /> </selector> diff --git a/core/tests/coretests/res/values/colors.xml b/core/tests/coretests/res/values/colors.xml index 029aa0dd8eb6..f01af8421515 100644 --- a/core/tests/coretests/res/values/colors.xml +++ b/core/tests/coretests/res/values/colors.xml @@ -25,6 +25,5 @@ <drawable name="yellow">#ffffff00</drawable> <color name="testcolor1">#ff00ff00</color> <color name="testcolor2">#ffff0000</color> - <color name="testcolor3">#fff00000</color> <color name="failColor">#ff0000ff</color> </resources> diff --git a/core/tests/coretests/src/android/app/BackgroundStartPrivilegesTest.java b/core/tests/coretests/src/android/app/BackgroundStartPrivilegesTest.java index cf6266c756ce..931d64640ea2 100644 --- a/core/tests/coretests/src/android/app/BackgroundStartPrivilegesTest.java +++ b/core/tests/coretests/src/android/app/BackgroundStartPrivilegesTest.java @@ -119,4 +119,15 @@ public class BackgroundStartPrivilegesTest { Arrays.asList(BSP_ALLOW_A, BSP_ALLOW_A, BSP_ALLOW_A, BSP_ALLOW_A))) .isEqualTo(BSP_ALLOW_A); } + + @Test + public void backgroundStartPrivilege_equals_works() { + assertThat(NONE).isEqualTo(NONE); + assertThat(ALLOW_BAL).isEqualTo(ALLOW_BAL); + assertThat(ALLOW_FGS).isEqualTo(ALLOW_FGS); + assertThat(BSP_ALLOW_A).isEqualTo(BSP_ALLOW_A); + assertThat(NONE).isNotEqualTo(ALLOW_BAL); + assertThat(ALLOW_FGS).isNotEqualTo(ALLOW_BAL); + assertThat(BSP_ALLOW_A).isNotEqualTo(BSP_ALLOW_B); + } } diff --git a/core/tests/coretests/src/android/app/NotificationTest.java b/core/tests/coretests/src/android/app/NotificationTest.java index 9effeec23890..ca6ad6fae46e 100644 --- a/core/tests/coretests/src/android/app/NotificationTest.java +++ b/core/tests/coretests/src/android/app/NotificationTest.java @@ -105,6 +105,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.R; import com.android.internal.util.ContrastColorUtil; +import com.android.internal.widget.NotificationProgressModel; import junit.framework.Assert; @@ -2414,7 +2415,7 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) - public void progressStyle_getProgressMax_nooSegments_returnsDefault() { + public void progressStyle_getProgressMax_noSegments_returnsDefault() { final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); progressStyle.setProgressSegments(Collections.emptyList()); assertThat(progressStyle.getProgressMax()).isEqualTo(100); @@ -2459,6 +2460,211 @@ public class NotificationTest { @Test @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_getProgressMax_onSegmentLimitExceeded_returnsSumOfSegmentLength() { + // GIVEN + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + // limit is 10 for ProgressStyle + for (int i = 0; i < 30; i++) { + progressStyle + .addProgressSegment(new Notification.ProgressStyle.Segment(10)); + } + + // THEN + assertThat(progressStyle.getProgressMax()).isEqualTo(300); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_addProgressSegment_dropsInvalidSegments() { + // GIVEN + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + // Segments should be a positive integer. + progressStyle + .addProgressSegment(new Notification.ProgressStyle.Segment(0)); + progressStyle + .addProgressSegment(new Notification.ProgressStyle.Segment(-1)); + + // THEN + assertThat(progressStyle.getProgressSegments()).isEmpty(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_setProgressSegment_dropsInvalidSegments() { + // GIVEN + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + // Segments should be a positive integer. + progressStyle + .setProgressSegments(List.of(new Notification.ProgressStyle.Segment(0), + new Notification.ProgressStyle.Segment(-1))); + + // THEN + assertThat(progressStyle.getProgressSegments()).isEmpty(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_addProgressPoint_dropsNegativePoints() { + // GIVEN + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + // Points should not be a negative integer. + progressStyle + .addProgressPoint(new Notification.ProgressStyle.Point(-1)) + .addProgressPoint(new Notification.ProgressStyle.Point(-100)); + + // THEN + assertThat(progressStyle.getProgressPoints()).isEmpty(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_setProgressPoint_dropsNegativePoints() { + // GIVEN + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + // Points should not be a negative integer. + progressStyle + .setProgressPoints(List.of(new Notification.ProgressStyle.Point(-1), + new Notification.ProgressStyle.Point(-100))); + + // THEN + assertThat(progressStyle.getProgressPoints()).isEmpty(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_createProgressModel_ignoresPointsExceedingMax() { + // GIVEN + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + progressStyle.addProgressSegment(new Notification.ProgressStyle.Segment(100)); + // Points should not larger than progress maximum. + progressStyle + .addProgressPoint(new Notification.ProgressStyle.Point(101)) + .addProgressPoint(new Notification.ProgressStyle.Point(500)); + + // THEN + assertThat(progressStyle.createProgressModel(Color.BLUE, Color.RED).getPoints()).isEmpty(); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_createProgressModel_ignoresOverLimitPoints() { + // GIVEN + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + progressStyle.addProgressSegment(new Notification.ProgressStyle.Segment(100)); + + // maximum 4 points are going to be rendered. + progressStyle + .addProgressPoint(new Notification.ProgressStyle.Point(0)) + .addProgressPoint(new Notification.ProgressStyle.Point(20)) + .addProgressPoint(new Notification.ProgressStyle.Point(150)) + .addProgressPoint(new Notification.ProgressStyle.Point(50)) + .addProgressPoint(new Notification.ProgressStyle.Point(70)) + .addProgressPoint(new Notification.ProgressStyle.Point(80)) + .addProgressPoint(new Notification.ProgressStyle.Point(90)) + .addProgressPoint(new Notification.ProgressStyle.Point(95)) + .addProgressPoint(new Notification.ProgressStyle.Point(100)); + final int backgroundColor = Color.RED; + final int defaultProgressColor = Color.BLUE; + final int expectedProgressColor = Notification.ProgressStyle.sanitizeProgressColor( + /* color = */Notification.COLOR_DEFAULT, + /* bg = */backgroundColor, + /* defaultColor = */defaultProgressColor); + + // THEN + assertThat(progressStyle.createProgressModel(defaultProgressColor, backgroundColor) + .getPoints()).isEqualTo( + List.of(new Notification.ProgressStyle.Point(0) + .setColor(expectedProgressColor), + new Notification.ProgressStyle.Point(20) + .setColor(expectedProgressColor), + new Notification.ProgressStyle.Point(50) + .setColor(expectedProgressColor), + new Notification.ProgressStyle.Point(70) + .setColor(expectedProgressColor) + ) + ); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_createProgressModel_mergeSegmentsOnOverflow() { + // GIVEN + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + + for (int i = 0; i < 15; i++) { + progressStyle + .addProgressSegment(new Notification.ProgressStyle.Segment(10)); + } + + final NotificationProgressModel progressModel = progressStyle.createProgressModel( + Color.BLUE, Color.RED); + + // THEN + assertThat(progressModel.getSegments().size()).isEqualTo(1); + assertThat(progressModel.getProgressMax()).isEqualTo(150); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_createProgressModel_useSegmentColorWhenAllMatch() { + // GIVEN + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + final int segmentColor = Color.YELLOW; + final int defaultProgressColor = Color.BLUE; + final int backgroundColor = Color.RED; + // contrast ensured color for segmentColor. + final int expectedSegmentColor = Notification.ProgressStyle.sanitizeProgressColor( + /* color = */ segmentColor, + /* bg = */ backgroundColor, + /* defaultColor = */ defaultProgressColor); + + for (int i = 0; i < 15; i++) { + progressStyle + .addProgressSegment(new Notification.ProgressStyle.Segment(10) + .setColor(segmentColor)); + } + + final NotificationProgressModel progressModel = progressStyle.createProgressModel( + defaultProgressColor, backgroundColor); + + // THEN + assertThat(progressModel.getSegments()) + .isEqualTo(List.of(new Notification.ProgressStyle.Segment(150) + .setColor(expectedSegmentColor))); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) + public void progressStyle_createProgressModel_useDefaultColorWhenAllNotMatch() { + // GIVEN + final Notification.ProgressStyle progressStyle = new Notification.ProgressStyle(); + final int defaultProgressColor = Color.BLUE; + final int backgroundColor = Color.RED; + // contrast ensured color for default progress color. + final int expectedSegmentColor = Notification.ProgressStyle.sanitizeProgressColor( + /* color = */ defaultProgressColor, + /* bg = */ backgroundColor, + /* defaultColor = */ defaultProgressColor); + + for (int i = 0; i < 15; i++) { + progressStyle + .addProgressSegment(new Notification.ProgressStyle.Segment(5) + .setColor(Color.BLUE)) + .addProgressSegment(new Notification.ProgressStyle.Segment(5) + .setColor(Color.CYAN)); + } + + final NotificationProgressModel progressModel = progressStyle.createProgressModel( + defaultProgressColor, backgroundColor); + + // THEN + assertThat(progressModel.getSegments()) + .isEqualTo(List.of(new Notification.ProgressStyle.Segment(150) + .setColor(expectedSegmentColor))); + } + + @Test + @EnableFlags(Flags.FLAG_API_RICH_ONGOING) public void progressStyle_indeterminate_defaultValueFalse() { final Notification.ProgressStyle progressStyle1 = new Notification.ProgressStyle(); diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java index 911b7ce22741..10a85bcbf488 100644 --- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java +++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionListenerControllerTest.java @@ -120,7 +120,8 @@ public class ClientTransactionListenerControllerTest { doReturn(newDisplayInfo).when(mIDisplayManager).getDisplayInfo(123); mDisplayManager.registerDisplayListener(mListener, mHandler, - DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_CHANGED, + DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED + | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE, null /* packageName */); mController.onDisplayChanged(123); diff --git a/core/tests/coretests/src/android/graphics/BitmapFactoryTest.java b/core/tests/coretests/src/android/graphics/BitmapFactoryTest.java index 564460e18294..84bdbe03df13 100644 --- a/core/tests/coretests/src/android/graphics/BitmapFactoryTest.java +++ b/core/tests/coretests/src/android/graphics/BitmapFactoryTest.java @@ -16,19 +16,27 @@ package android.graphics; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + import android.os.ParcelFileDescriptor; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; import java.io.ByteArrayOutputStream; import java.io.FileDescriptor; -public class BitmapFactoryTest extends TestCase { +@RunWith(AndroidJUnit4.class) +public class BitmapFactoryTest { // tests that we can decode bitmaps from MemoryFiles @SmallTest + @Test public void testBitmapParcelFileDescriptor() throws Exception { Bitmap bitmap1 = Bitmap.createBitmap( new int[] { Color.BLUE }, 1, 1, Bitmap.Config.RGB_565); diff --git a/core/tests/coretests/src/android/graphics/BitmapTest.java b/core/tests/coretests/src/android/graphics/BitmapTest.java index 2280cf1cccfa..0126d367eb20 100644 --- a/core/tests/coretests/src/android/graphics/BitmapTest.java +++ b/core/tests/coretests/src/android/graphics/BitmapTest.java @@ -16,19 +16,28 @@ package android.graphics; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + import android.hardware.HardwareBuffer; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; import java.nio.ByteBuffer; import java.nio.IntBuffer; import java.nio.ShortBuffer; -public class BitmapTest extends TestCase { +@SmallTest +@RunWith(AndroidJUnit4.class) +public class BitmapTest { - @SmallTest + @Test public void testBasic() throws Exception { Bitmap bm1 = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888); Bitmap bm2 = Bitmap.createBitmap(100, 200, Bitmap.Config.RGB_565); @@ -63,7 +72,7 @@ public class BitmapTest extends TestCase { assertTrue("getConfig", bm3.getConfig() == Bitmap.Config.ARGB_8888); } - @SmallTest + @Test public void testMutability() throws Exception { Bitmap bm1 = Bitmap.createBitmap(100, 200, Bitmap.Config.ARGB_8888); Bitmap bm2 = Bitmap.createBitmap(new int[100 * 200], 100, 200, @@ -82,7 +91,7 @@ public class BitmapTest extends TestCase { } } - @SmallTest + @Test public void testGetPixelsWithAlpha() throws Exception { int[] colors = new int[100]; for (int i = 0; i < 100; i++) { @@ -108,7 +117,7 @@ public class BitmapTest extends TestCase { } - @SmallTest + @Test public void testGetPixelsWithoutAlpha() throws Exception { int[] colors = new int[100]; for (int i = 0; i < 100; i++) { @@ -125,7 +134,7 @@ public class BitmapTest extends TestCase { } } - @SmallTest + @Test public void testSetPixelsWithAlpha() throws Exception { int[] colors = new int[100]; for (int i = 0; i < 100; i++) { @@ -151,7 +160,7 @@ public class BitmapTest extends TestCase { } } - @SmallTest + @Test public void testSetPixelsWithoutAlpha() throws Exception { int[] colors = new int[100]; for (int i = 0; i < 100; i++) { @@ -181,7 +190,7 @@ public class BitmapTest extends TestCase { return unpre; } - @SmallTest + @Test public void testSetPixelsWithNonOpaqueAlpha() throws Exception { int[] colors = new int[256]; for (int i = 0; i < 256; i++) { @@ -238,10 +247,13 @@ public class BitmapTest extends TestCase { } } - @SmallTest + private static final int GRAPHICS_USAGE = + GraphicBuffer.USAGE_HW_TEXTURE | GraphicBuffer.USAGE_SW_READ_OFTEN + | GraphicBuffer.USAGE_SW_WRITE_OFTEN; + + @Test public void testWrapHardwareBufferWithSrgbColorSpace() { - GraphicBuffer buffer = GraphicBuffer.create(10, 10, PixelFormat.RGBA_8888, - GraphicBuffer.USAGE_HW_TEXTURE | GraphicBuffer.USAGE_SOFTWARE_MASK); + GraphicBuffer buffer = GraphicBuffer.create(10, 10, PixelFormat.RGBA_8888, GRAPHICS_USAGE); Canvas canvas = buffer.lockCanvas(); canvas.drawColor(Color.YELLOW); buffer.unlockCanvasAndPost(canvas); @@ -252,10 +264,9 @@ public class BitmapTest extends TestCase { assertEquals(ColorSpace.get(ColorSpace.Named.SRGB), hardwareBitmap.getColorSpace()); } - @SmallTest + @Test public void testWrapHardwareBufferWithDisplayP3ColorSpace() { - GraphicBuffer buffer = GraphicBuffer.create(10, 10, PixelFormat.RGBA_8888, - GraphicBuffer.USAGE_HW_TEXTURE | GraphicBuffer.USAGE_SOFTWARE_MASK); + GraphicBuffer buffer = GraphicBuffer.create(10, 10, PixelFormat.RGBA_8888, GRAPHICS_USAGE); Canvas canvas = buffer.lockCanvas(); canvas.drawColor(Color.YELLOW); buffer.unlockCanvasAndPost(canvas); @@ -267,7 +278,7 @@ public class BitmapTest extends TestCase { assertEquals(ColorSpace.get(ColorSpace.Named.DISPLAY_P3), hardwareBitmap.getColorSpace()); } - @SmallTest + @Test public void testCopyWithDirectByteBuffer() { // Initialize Bitmap final int width = 2; @@ -305,7 +316,7 @@ public class BitmapTest extends TestCase { assertTrue(bm2.sameAs(bm1)); } - @SmallTest + @Test public void testCopyWithDirectShortBuffer() { // Initialize Bitmap final int width = 2; @@ -344,7 +355,7 @@ public class BitmapTest extends TestCase { assertTrue(bm2.sameAs(bm1)); } - @SmallTest + @Test public void testCopyWithDirectIntBuffer() { // Initialize Bitmap final int width = 2; @@ -383,7 +394,7 @@ public class BitmapTest extends TestCase { assertTrue(bm2.sameAs(bm1)); } - @SmallTest + @Test public void testCopyWithHeapByteBuffer() { // Initialize Bitmap final int width = 2; @@ -420,7 +431,7 @@ public class BitmapTest extends TestCase { assertTrue(bm2.sameAs(bm1)); } - @SmallTest + @Test public void testCopyWithHeapShortBuffer() { // Initialize Bitmap final int width = 2; @@ -457,7 +468,7 @@ public class BitmapTest extends TestCase { assertTrue(bm2.sameAs(bm1)); } - @SmallTest + @Test public void testCopyWithHeapIntBuffer() { // Initialize Bitmap final int width = 2; diff --git a/core/tests/coretests/src/android/graphics/ColorStateListTest.java b/core/tests/coretests/src/android/graphics/ColorStateListTest.java index ab41bd07ac6d..5cc915e45a6f 100644 --- a/core/tests/coretests/src/android/graphics/ColorStateListTest.java +++ b/core/tests/coretests/src/android/graphics/ColorStateListTest.java @@ -16,33 +16,41 @@ package android.graphics; +import static org.junit.Assert.assertEquals; + import android.content.res.ColorStateList; import android.content.res.Resources; -import android.test.AndroidTestCase; import android.util.proto.ProtoInputStream; import android.util.proto.ProtoOutputStream; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; import com.android.frameworks.coretests.R; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + /** - * Tests of {@link android.graphics.ColorStateList} + * Tests of {@link ColorStateList} */ -public class ColorStateListTest extends AndroidTestCase { +@SmallTest +@RunWith(AndroidJUnit4.class) +public class ColorStateListTest { private Resources mResources; private int mFailureColor; - @Override - protected void setUp() throws Exception { - super.setUp(); - mResources = mContext.getResources(); + @Before + public void setUp() throws Exception { + mResources = InstrumentationRegistry.getInstrumentation().getContext().getResources(); mFailureColor = mResources.getColor(R.color.failColor); } - @SmallTest + @Test public void testStateIsInList() throws Exception { ColorStateList colorStateList = mResources.getColorStateList(R.color.color1); int[] focusedState = {android.R.attr.state_focused}; @@ -50,7 +58,7 @@ public class ColorStateListTest extends AndroidTestCase { assertEquals(mResources.getColor(R.color.testcolor1), focusColor); } - @SmallTest + @Test public void testStateIsInList_proto() throws Exception { ColorStateList colorStateList = recreateFromProto( mResources.getColorStateList(R.color.color1)); @@ -59,7 +67,7 @@ public class ColorStateListTest extends AndroidTestCase { assertEquals(mResources.getColor(R.color.testcolor1), focusColor); } - @SmallTest + @Test public void testEmptyState() throws Exception { ColorStateList colorStateList = mResources.getColorStateList(R.color.color1); int[] emptyState = {}; @@ -67,7 +75,7 @@ public class ColorStateListTest extends AndroidTestCase { assertEquals(mResources.getColor(R.color.testcolor2), defaultColor); } - @SmallTest + @Test public void testEmptyState_proto() throws Exception { ColorStateList colorStateList = recreateFromProto( mResources.getColorStateList(R.color.color1)); @@ -76,22 +84,23 @@ public class ColorStateListTest extends AndroidTestCase { assertEquals(mResources.getColor(R.color.testcolor2), defaultColor); } - @SmallTest + @Test public void testGetColor() throws Exception { int defaultColor = mResources.getColor(R.color.color1); assertEquals(mResources.getColor(R.color.testcolor2), defaultColor); } - @SmallTest + @Test public void testGetColorWhenListHasNoDefault() throws Exception { int defaultColor = mResources.getColor(R.color.color_no_default); assertEquals(mResources.getColor(R.color.testcolor1), defaultColor); } - @SmallTest + @Test public void testLstar() throws Exception { + var cl = ColorStateList.valueOf(mResources.getColor(R.color.testcolor2)).withLStar(50.0f); int defaultColor = mResources.getColor(R.color.color_with_lstar); - assertEquals(mResources.getColor(R.color.testcolor3), defaultColor); + assertEquals(cl.getDefaultColor(), defaultColor); } private ColorStateList recreateFromProto(ColorStateList colorStateList) throws Exception { diff --git a/core/tests/coretests/src/android/graphics/FontFileUtilTest.java b/core/tests/coretests/src/android/graphics/FontFileUtilTest.java index 52cc4cac4816..063bdf52fbd2 100644 --- a/core/tests/coretests/src/android/graphics/FontFileUtilTest.java +++ b/core/tests/coretests/src/android/graphics/FontFileUtilTest.java @@ -30,9 +30,11 @@ import android.util.Log; import android.util.Pair; import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import org.junit.Test; +import org.junit.runner.RunWith; import java.io.File; import java.io.FileInputStream; @@ -43,6 +45,7 @@ import java.nio.ByteBuffer; import java.nio.channels.FileChannel; @SmallTest +@RunWith(AndroidJUnit4.class) public class FontFileUtilTest { private static final String TAG = "FontFileUtilTest"; private static final String CACHE_FILE_PREFIX = ".font"; diff --git a/core/tests/coretests/src/android/graphics/PaintFontVariationTest.java b/core/tests/coretests/src/android/graphics/PaintFontVariationTest.java index 8a54e5b998e7..816bde603d36 100644 --- a/core/tests/coretests/src/android/graphics/PaintFontVariationTest.java +++ b/core/tests/coretests/src/android/graphics/PaintFontVariationTest.java @@ -21,10 +21,9 @@ import static com.google.common.truth.Truth.assertThat; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; -import android.test.InstrumentationTestCase; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.text.flags.Flags; @@ -37,7 +36,7 @@ import org.junit.runner.RunWith; */ @SmallTest @RunWith(AndroidJUnit4.class) -public class PaintFontVariationTest extends InstrumentationTestCase { +public class PaintFontVariationTest { @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); diff --git a/core/tests/coretests/src/android/graphics/PaintTest.java b/core/tests/coretests/src/android/graphics/PaintTest.java index 878ba703c8fe..56760d77e28b 100644 --- a/core/tests/coretests/src/android/graphics/PaintTest.java +++ b/core/tests/coretests/src/android/graphics/PaintTest.java @@ -18,19 +18,26 @@ package android.graphics; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; -import android.test.InstrumentationTestCase; import android.text.TextUtils; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; import com.android.text.flags.Flags; import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; import java.util.Arrays; import java.util.HashSet; @@ -38,13 +45,14 @@ import java.util.HashSet; /** * PaintTest tests {@link Paint}. */ -public class PaintTest extends InstrumentationTestCase { +@RunWith(AndroidJUnit4.class) +public class PaintTest { @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private static final String FONT_PATH = "fonts/HintedAdvanceWidthTest-Regular.ttf"; - static void assertEquals(String message, float[] expected, float[] actual) { + static void assertFloatArrayEquals(String message, float[] expected, float[] actual) { if (expected.length != actual.length) { fail(message + " expected array length:<" + expected.length + "> but was:<" + actual.length + ">"); @@ -88,9 +96,10 @@ public class PaintTest extends InstrumentationTestCase { }; @SmallTest + @Test public void testHintingWidth() { final Typeface fontTypeface = Typeface.createFromAsset( - getInstrumentation().getContext().getAssets(), FONT_PATH); + InstrumentationRegistry.getInstrumentation().getContext().getAssets(), FONT_PATH); Paint paint = new Paint(); paint.setTypeface(fontTypeface); @@ -103,12 +112,14 @@ public class PaintTest extends InstrumentationTestCase { paint.setHinting(Paint.HINTING_OFF); paint.getTextWidths(String.valueOf(testCase.mText), widths); - assertEquals("Text width of '" + testCase.mText + "' without hinting is not expected.", + assertFloatArrayEquals( + "Text width of '" + testCase.mText + "' without hinting is not expected.", testCase.mWidthWithoutHinting, widths); paint.setHinting(Paint.HINTING_ON); paint.getTextWidths(String.valueOf(testCase.mText), widths); - assertEquals("Text width of '" + testCase.mText + "' with hinting is not expected.", + assertFloatArrayEquals( + "Text width of '" + testCase.mText + "' with hinting is not expected.", testCase.mWidthWithHinting, widths); } } @@ -131,9 +142,11 @@ public class PaintTest extends InstrumentationTestCase { return sb.toString(); } + @Test public void testHasGlyph_variationSelectors() { final Typeface fontTypeface = Typeface.createFromAsset( - getInstrumentation().getContext().getAssets(), "fonts/hasGlyphTestFont.ttf"); + InstrumentationRegistry.getInstrumentation().getContext().getAssets(), + "fonts/hasGlyphTestFont.ttf"); Paint p = new Paint(); p.setTypeface(fontTypeface); @@ -175,6 +188,7 @@ public class PaintTest extends InstrumentationTestCase { } } + @Test public void testGetTextRunAdvances() { { // LTR @@ -231,6 +245,7 @@ public class PaintTest extends InstrumentationTestCase { } } + @Test public void testGetTextRunAdvances_invalid() { Paint p = new Paint(); char[] text = "test".toCharArray(); @@ -284,6 +299,7 @@ public class PaintTest extends InstrumentationTestCase { } } + @Test public void testMeasureTextBidi() { Paint p = new Paint(); { @@ -340,18 +356,21 @@ public class PaintTest extends InstrumentationTestCase { } } + @Test public void testSetGetWordSpacing() { Paint p = new Paint(); - assertEquals(0.0f, p.getWordSpacing()); // The default value should be 0. + assertEquals(0.0f, p.getWordSpacing(), 0.0f); // The default value should be 0. p.setWordSpacing(1.0f); - assertEquals(1.0f, p.getWordSpacing()); + assertEquals(1.0f, p.getWordSpacing(), 0.0f); p.setWordSpacing(-2.0f); - assertEquals(-2.0f, p.getWordSpacing()); + assertEquals(-2.0f, p.getWordSpacing(), 0.0f); } + @Test public void testGetUnderlinePositionAndThickness() { final Typeface fontTypeface = Typeface.createFromAsset( - getInstrumentation().getContext().getAssets(), "fonts/underlineTestFont.ttf"); + InstrumentationRegistry.getInstrumentation().getContext().getAssets(), + "fonts/underlineTestFont.ttf"); final Paint p = new Paint(); final int textSize = 100; p.setTextSize(textSize); @@ -391,6 +410,7 @@ public class PaintTest extends InstrumentationTestCase { return ccByChars; } + @Test public void testCluster() { final Paint p = new Paint(); p.setTextSize(100); @@ -417,6 +437,7 @@ public class PaintTest extends InstrumentationTestCase { } @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS) + @Test public void testDerivedFromSameTypeface() { final Paint p = new Paint(); @@ -432,6 +453,7 @@ public class PaintTest extends InstrumentationTestCase { } @RequiresFlagsEnabled(Flags.FLAG_TYPEFACE_CACHE_FOR_VAR_SETTINGS) + @Test public void testDerivedFromChained() { final Paint p = new Paint(); diff --git a/core/tests/coretests/src/android/graphics/ThreadBitmapTest.java b/core/tests/coretests/src/android/graphics/ThreadBitmapTest.java index e1ca7dfb7cc2..fbaf502596f7 100644 --- a/core/tests/coretests/src/android/graphics/ThreadBitmapTest.java +++ b/core/tests/coretests/src/android/graphics/ThreadBitmapTest.java @@ -16,17 +16,17 @@ package android.graphics; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; -import junit.framework.TestCase; +import org.junit.Test; +import org.junit.runner.RunWith; -public class ThreadBitmapTest extends TestCase { - - @Override - protected void setUp() throws Exception { - } +@RunWith(AndroidJUnit4.class) +public class ThreadBitmapTest { @LargeTest + @Test public void testCreation() { for (int i = 0; i < 200; i++) { @@ -44,4 +44,3 @@ public class ThreadBitmapTest extends TestCase { public void run() {} } } - diff --git a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java index 14292725506e..2b6eda8f0988 100644 --- a/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java +++ b/core/tests/coretests/src/android/graphics/TypefaceSystemFallbackTest.java @@ -39,6 +39,8 @@ import androidx.test.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.text.flags.Flags; + import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -514,9 +516,14 @@ public class TypefaceSystemFallbackTest { assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); paint.setElegantTextHeight(false); - assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); - assertEquals(GLYPH_3EM_WIDTH, paint.measureText("b"), 0.0f); - assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); + if (Flags.deprecateElegantTextHeightApi()) { + // Calling setElegantTextHeight is no-op. + assertTrue(paint.isElegantTextHeight()); + } else { + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); + } } @Test @@ -553,9 +560,14 @@ public class TypefaceSystemFallbackTest { assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); paint.setElegantTextHeight(false); - assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); - assertEquals(GLYPH_1EM_WIDTH, paint.measureText("b"), 0.0f); - assertEquals(GLYPH_3EM_WIDTH, paint.measureText("c"), 0.0f); + if (Flags.deprecateElegantTextHeightApi()) { + // Calling setElegantTextHeight is no-op. + assertTrue(paint.isElegantTextHeight()); + } else { + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("c"), 0.0f); + } testTypeface = fontMap.get("sans-serif"); assertNotNull(testTypeface); @@ -566,9 +578,14 @@ public class TypefaceSystemFallbackTest { assertEquals(GLYPH_1EM_WIDTH, paint.measureText("c"), 0.0f); paint.setElegantTextHeight(false); - assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); - assertEquals(GLYPH_1EM_WIDTH, paint.measureText("b"), 0.0f); - assertEquals(GLYPH_3EM_WIDTH, paint.measureText("c"), 0.0f); + if (Flags.deprecateElegantTextHeightApi()) { + // Calling setElegantTextHeight is no-op. + assertTrue(paint.isElegantTextHeight()); + } else { + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("a"), 0.0f); + assertEquals(GLYPH_1EM_WIDTH, paint.measureText("b"), 0.0f); + assertEquals(GLYPH_3EM_WIDTH, paint.measureText("c"), 0.0f); + } } @Test diff --git a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java index 7a5b3064b3a3..a270848b98a3 100644 --- a/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java +++ b/core/tests/coretests/src/android/hardware/display/DisplayManagerGlobalTest.java @@ -73,9 +73,13 @@ public class DisplayManagerGlobalTest { public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private static final long DISPLAY_CHANGE_EVENTS = + DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED + | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE; + private static final long ALL_DISPLAY_EVENTS = DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED - | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_CHANGED + | DISPLAY_CHANGE_EVENTS | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REMOVED; @Mock @@ -127,7 +131,7 @@ public class DisplayManagerGlobalTest { final DisplayInfo newDisplayInfo = new DisplayInfo(); newDisplayInfo.rotation++; doReturn(newDisplayInfo).when(mDisplayManager).getDisplayInfo(displayId); - callback.onDisplayEvent(displayId, DisplayManagerGlobal.EVENT_DISPLAY_CHANGED); + callback.onDisplayEvent(displayId, DisplayManagerGlobal.EVENT_DISPLAY_BASIC_CHANGED); waitForHandler(); Mockito.verify(mDisplayListener).onDisplayChanged(eq(displayId)); Mockito.verifyNoMoreInteractions(mDisplayListener); @@ -186,8 +190,8 @@ public class DisplayManagerGlobalTest { mDisplayManagerGlobal.registerDisplayListener(mDisplayListener, mHandler, ALL_DISPLAY_EVENTS - & ~DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_CHANGED, null); - callback.onDisplayEvent(displayId, DisplayManagerGlobal.EVENT_DISPLAY_CHANGED); + & ~DISPLAY_CHANGE_EVENTS, null); + callback.onDisplayEvent(displayId, DisplayManagerGlobal.EVENT_DISPLAY_BASIC_CHANGED); waitForHandler(); Mockito.verifyZeroInteractions(mDisplayListener); @@ -257,8 +261,7 @@ public class DisplayManagerGlobalTest { | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REMOVED, null /* packageName */); mDisplayManagerGlobal.registerDisplayListener(mDisplayListener2, mHandler, - DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_CHANGED, - null /* packageName */); + DISPLAY_CHANGE_EVENTS, null /* packageName */); mDisplayManagerGlobal.handleDisplayChangeFromWindowManager(321); waitForHandler(); @@ -304,8 +307,7 @@ public class DisplayManagerGlobalTest { assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED, mDisplayManagerGlobal .mapFlagsToInternalEventFlag(DisplayManager.EVENT_FLAG_DISPLAY_ADDED, 0)); - assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_CHANGED, - mDisplayManagerGlobal + assertEquals(DISPLAY_CHANGE_EVENTS, mDisplayManagerGlobal .mapFlagsToInternalEventFlag(DisplayManager.EVENT_FLAG_DISPLAY_CHANGED, 0)); assertEquals(DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REMOVED, mDisplayManagerGlobal diff --git a/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt b/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt index 4a227d8ff1ef..255d09b854bd 100644 --- a/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt +++ b/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt @@ -86,7 +86,7 @@ class DisplayTopologyTest { verifyDisplay(display1, displayId1, width1, height1, noOfChildren = 1) val display2 = display1.children[0] - verifyDisplay(display1.children[0], displayId2, width2, height2, POSITION_TOP, + verifyDisplay(display2, displayId2, width2, height2, POSITION_TOP, offset = width1 / 2 - width2 / 2, noOfChildren = 1) var display = display2 @@ -99,6 +99,76 @@ class DisplayTopologyTest { } @Test + fun updateDisplay() { + val displayId = 1 + val width = 800f + val height = 600f + + val newWidth = 1000f + val newHeight = 500f + topology.addDisplay(displayId, width, height) + assertThat(topology.updateDisplay(displayId, newWidth, newHeight)).isTrue() + + assertThat(topology.primaryDisplayId).isEqualTo(displayId) + verifyDisplay(topology.root!!, displayId, newWidth, newHeight, noOfChildren = 0) + } + + @Test + fun updateDisplay_notUpdated() { + val displayId = 1 + val width = 800f + val height = 600f + topology.addDisplay(displayId, width, height) + + // Same size + assertThat(topology.updateDisplay(displayId, width, height)).isFalse() + + // Display doesn't exist + assertThat(topology.updateDisplay(/* displayId= */ 100, width, height)).isFalse() + + assertThat(topology.primaryDisplayId).isEqualTo(displayId) + verifyDisplay(topology.root!!, displayId, width, height, noOfChildren = 0) + } + + @Test + fun updateDisplayDoesNotAffectDefaultTopology() { + val width1 = 700f + val height = 600f + topology.addDisplay(/* displayId= */ 1, width1, height) + + val width2 = 800f + val noOfDisplays = 30 + for (i in 2..noOfDisplays) { + topology.addDisplay(/* displayId= */ i, width2, height) + } + + val displaysToUpdate = arrayOf(3, 7, 18) + val newWidth = 1000f + val newHeight = 1500f + for (i in displaysToUpdate) { + assertThat(topology.updateDisplay(/* displayId= */ i, newWidth, newHeight)).isTrue() + } + + assertThat(topology.primaryDisplayId).isEqualTo(1) + + val display1 = topology.root!! + verifyDisplay(display1, id = 1, width1, height, noOfChildren = 1) + + val display2 = display1.children[0] + verifyDisplay(display2, id = 2, width2, height, POSITION_TOP, + offset = width1 / 2 - width2 / 2, noOfChildren = 1) + + var display = display2 + for (i in 3..noOfDisplays) { + display = display.children[0] + // The last display should have no children + verifyDisplay(display, id = i, if (i in displaysToUpdate) newWidth else width2, + if (i in displaysToUpdate) newHeight else height, POSITION_RIGHT, offset = 0f, + noOfChildren = if (i < noOfDisplays) 1 else 0) + } + } + + @Test fun removeDisplays() { val displayId1 = 1 val width1 = 800f @@ -117,7 +187,7 @@ class DisplayTopologyTest { } var removedDisplays = arrayOf(20) - topology.removeDisplay(20) + assertThat(topology.removeDisplay(20)).isTrue() assertThat(topology.primaryDisplayId).isEqualTo(displayId1) @@ -139,11 +209,11 @@ class DisplayTopologyTest { noOfChildren = if (i < noOfDisplays) 1 else 0) } - topology.removeDisplay(22) + assertThat(topology.removeDisplay(22)).isTrue() removedDisplays += 22 - topology.removeDisplay(23) + assertThat(topology.removeDisplay(23)).isTrue() removedDisplays += 23 - topology.removeDisplay(25) + assertThat(topology.removeDisplay(25)).isTrue() removedDisplays += 25 assertThat(topology.primaryDisplayId).isEqualTo(displayId1) @@ -174,7 +244,7 @@ class DisplayTopologyTest { val height = 600f topology.addDisplay(displayId, width, height) - topology.removeDisplay(displayId) + assertThat(topology.removeDisplay(displayId)).isTrue() assertThat(topology.primaryDisplayId).isEqualTo(Display.INVALID_DISPLAY) assertThat(topology.root).isNull() @@ -187,7 +257,7 @@ class DisplayTopologyTest { val height = 600f topology.addDisplay(displayId, width, height) - topology.removeDisplay(3) + assertThat(topology.removeDisplay(3)).isFalse() assertThat(topology.primaryDisplayId).isEqualTo(displayId) verifyDisplay(topology.root!!, displayId, width, height, noOfChildren = 0) @@ -203,7 +273,7 @@ class DisplayTopologyTest { topology = DisplayTopology(/* root= */ null, displayId2) topology.addDisplay(displayId1, width, height) topology.addDisplay(displayId2, width, height) - topology.removeDisplay(displayId2) + assertThat(topology.removeDisplay(displayId2)).isTrue() assertThat(topology.primaryDisplayId).isEqualTo(displayId1) verifyDisplay(topology.root!!, displayId1, width, height, noOfChildren = 0) diff --git a/core/tests/coretests/src/android/view/ViewFrameRateTest.java b/core/tests/coretests/src/android/view/ViewFrameRateTest.java index fb1efa86c236..8b4f714fbf65 100644 --- a/core/tests/coretests/src/android/view/ViewFrameRateTest.java +++ b/core/tests/coretests/src/android/view/ViewFrameRateTest.java @@ -1171,6 +1171,88 @@ public class ViewFrameRateTest { waitForAfterDraw(); } + @Test + public void ignoreHeuristicWhenFling() throws Throwable { + if (!ViewProperties.vrr_enabled().orElse(true)) { + return; + } + + waitForFrameRateCategoryToSettle(); + FrameLayout host = new FrameLayout(mActivity); + View childView = new View(mActivity); + float velocity = 1000; + + TranslateAnimation translateAnimation = new TranslateAnimation( + Animation.RELATIVE_TO_PARENT, 0f, // fromXDelta + Animation.RELATIVE_TO_PARENT, 0f, // toXDelta + Animation.RELATIVE_TO_PARENT, 1f, // fromYDelta (100%p) + Animation.RELATIVE_TO_PARENT, 0f // toYDelta + ); + translateAnimation.setDuration(100); + + mActivityRule.runOnUiThread(() -> { + ViewGroup.LayoutParams fullSize = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + mActivity.setContentView(host, fullSize); + host.setFrameContentVelocity(velocity); + ViewGroupOverlay overlay = host.getOverlay(); + overlay.add(childView); + assertEquals(velocity, host.getFrameContentVelocity()); + assertEquals(host.getFrameContentVelocity(), + ((View) childView.getParent()).getFrameContentVelocity()); + + mMovingView.startAnimation(translateAnimation); + + // The frame rate should be "Normal" during fling gestures, + // even if there's a moving View. + assertEquals(FRAME_RATE_CATEGORY_NORMAL, + mViewRoot.getLastPreferredFrameRateCategory()); + }); + waitForAfterDraw(); + } + + @Test + public void ignoreHeuristicWhenFlingMovementFirst() throws Throwable { + if (!ViewProperties.vrr_enabled().orElse(true)) { + return; + } + + waitForFrameRateCategoryToSettle(); + FrameLayout host = new FrameLayout(mActivity); + View childView = new View(mActivity); + float velocity = 1000; + + TranslateAnimation translateAnimation = new TranslateAnimation( + Animation.RELATIVE_TO_PARENT, 0f, // fromXDelta + Animation.RELATIVE_TO_PARENT, 0f, // toXDelta + Animation.RELATIVE_TO_PARENT, 1f, // fromYDelta (100%p) + Animation.RELATIVE_TO_PARENT, 0f // toYDelta + ); + translateAnimation.setDuration(100); + + mActivityRule.runOnUiThread(() -> { + mMovingView.startAnimation(translateAnimation); + + ViewGroup.LayoutParams fullSize = new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + mActivity.setContentView(host, fullSize); + host.setFrameContentVelocity(velocity); + ViewGroupOverlay overlay = host.getOverlay(); + overlay.add(childView); + assertEquals(velocity, host.getFrameContentVelocity()); + assertEquals(host.getFrameContentVelocity(), + ((View) childView.getParent()).getFrameContentVelocity()); + + // The frame rate should be "Normal" during fling gestures, + // even if there's a moving View. + assertEquals(FRAME_RATE_CATEGORY_NORMAL, + mViewRoot.getLastPreferredFrameRateCategory()); + }); + waitForAfterDraw(); + } + private void runAfterDraw(@NonNull Runnable runnable) { Handler handler = new Handler(Looper.getMainLooper()); mAfterDrawLatch = new CountDownLatch(1); diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index ed9fc1c9e547..18ab52dba8f3 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -25,7 +25,7 @@ import static android.view.Surface.FRAME_RATE_CATEGORY_HIGH_HINT; import static android.view.Surface.FRAME_RATE_CATEGORY_LOW; import static android.view.Surface.FRAME_RATE_CATEGORY_NORMAL; import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; -import static android.view.Surface.FRAME_RATE_COMPATIBILITY_GTE; +import static android.view.Surface.FRAME_RATE_COMPATIBILITY_AT_LEAST; import static android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; import static android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; @@ -861,10 +861,10 @@ public class ViewRootImplTest { assertEquals(mViewRootImpl.getFrameRateCompatibility(), FRAME_RATE_COMPATIBILITY_FIXED_SOURCE); assertFalse(mViewRootImpl.isFrameRateConflicted()); - mViewRootImpl.votePreferredFrameRate(24, FRAME_RATE_COMPATIBILITY_GTE); + mViewRootImpl.votePreferredFrameRate(24, FRAME_RATE_COMPATIBILITY_AT_LEAST); if (toolkitFrameRateVelocityMappingReadOnly()) { assertEquals(24, mViewRootImpl.getPreferredFrameRate(), 0.1); - assertEquals(FRAME_RATE_COMPATIBILITY_GTE, + assertEquals(FRAME_RATE_COMPATIBILITY_AT_LEAST, mViewRootImpl.getFrameRateCompatibility()); assertFalse(mViewRootImpl.isFrameRateConflicted()); } else { @@ -888,10 +888,10 @@ public class ViewRootImplTest { sInstrumentation.runOnMainSync(() -> { assertFalse(mViewRootImpl.isFrameRateConflicted()); - mViewRootImpl.votePreferredFrameRate(60, FRAME_RATE_COMPATIBILITY_GTE); + mViewRootImpl.votePreferredFrameRate(60, FRAME_RATE_COMPATIBILITY_AT_LEAST); if (toolkitFrameRateVelocityMappingReadOnly()) { assertEquals(60, mViewRootImpl.getPreferredFrameRate(), 0.1); - assertEquals(FRAME_RATE_COMPATIBILITY_GTE, + assertEquals(FRAME_RATE_COMPATIBILITY_AT_LEAST, mViewRootImpl.getFrameRateCompatibility()); } else { assertEquals(FRAME_RATE_CATEGORY_HIGH, @@ -904,7 +904,7 @@ public class ViewRootImplTest { mViewRootImpl.getFrameRateCompatibility()); // Should be false since 60 is a divisor of 120. assertFalse(mViewRootImpl.isFrameRateConflicted()); - mViewRootImpl.votePreferredFrameRate(60, FRAME_RATE_COMPATIBILITY_GTE); + mViewRootImpl.votePreferredFrameRate(60, FRAME_RATE_COMPATIBILITY_AT_LEAST); assertEquals(120, mViewRootImpl.getPreferredFrameRate(), 0.1); // compatibility should be remained the same (FRAME_RATE_COMPATIBILITY_FIXED_SOURCE) // since the frame rate 60 is smaller than 120. diff --git a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java index 5a4561d7c6ea..f87b6994900f 100644 --- a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java +++ b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureSessionTest.java @@ -266,6 +266,11 @@ public class ContentCaptureSessionTest { } @Override + void internalNotifySessionFlushEvent(int sessionId) { + throw new UnsupportedOperationException("should not have been called"); + } + + @Override void internalNotifyChildSessionStarted(int parentSessionId, int childSessionId, @NonNull ContentCaptureContext clientContext) { throw new UnsupportedOperationException("should not have been called"); diff --git a/core/tests/coretests/src/com/android/internal/widget/LockPatternUtilsTest.java b/core/tests/coretests/src/com/android/internal/widget/LockPatternUtilsTest.java index 00b4f464de50..d1fbc77cbd46 100644 --- a/core/tests/coretests/src/com/android/internal/widget/LockPatternUtilsTest.java +++ b/core/tests/coretests/src/com/android/internal/widget/LockPatternUtilsTest.java @@ -31,6 +31,7 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.doNothing; @@ -44,20 +45,27 @@ import android.content.ComponentName; import android.content.Context; import android.content.ContextWrapper; import android.content.pm.UserInfo; +import android.hardware.input.IInputManager; +import android.hardware.input.InputManagerGlobal; import android.os.Looper; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.IgnoreUnderRavenwood; +import android.platform.test.flag.junit.SetFlagsRule; import android.platform.test.ravenwood.RavenwoodRule; import android.provider.Settings; import android.test.mock.MockContentResolver; +import android.view.InputDevice; import androidx.test.InstrumentationRegistry; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import com.android.internal.util.test.FakeSettingsProvider; +import com.android.internal.widget.flags.Flags; import com.google.android.collect.Lists; @@ -76,6 +84,8 @@ import java.util.concurrent.CompletableFuture; public class LockPatternUtilsTest { @Rule public final RavenwoodRule mRavenwood = new RavenwoodRule(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private ILockSettings mLockSettings; private static final int USER_ID = 1; @@ -395,4 +405,156 @@ public class LockPatternUtilsTest { } }; } + + private InputManagerGlobal.TestSession configureExternalHardwareTest(InputDevice[] devices) + throws RemoteException { + final Context context = new ContextWrapper(InstrumentationRegistry.getTargetContext()); + final ILockSettings ils = mock(ILockSettings.class); + when(ils.getBoolean(anyString(), anyBoolean(), anyInt())).thenThrow(RemoteException.class); + mLockPatternUtils = new LockPatternUtils(context, ils); + + IInputManager inputManagerMock = mock(IInputManager.class); + + int[] deviceIds = new int[devices.length]; + + for (int i = 0; i < devices.length; i++) { + when(inputManagerMock.getInputDevice(i)).thenReturn(devices[i]); + } + + when(inputManagerMock.getInputDeviceIds()).thenReturn(deviceIds); + InputManagerGlobal.TestSession session = + InputManagerGlobal.createTestSession(inputManagerMock); + + return session; + } + + @Test + @EnableFlags(Flags.FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void isPinEnhancedPrivacyEnabled_noDevicesAttached() throws RemoteException { + InputManagerGlobal.TestSession session = configureExternalHardwareTest(new InputDevice[0]); + assertFalse(mLockPatternUtils.isPinEnhancedPrivacyEnabled(USER_ID)); + session.close(); + } + + @Test + @EnableFlags(Flags.FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void isPinEnhancedPrivacyEnabled_noEnabledDeviceAttached() throws RemoteException { + InputDevice.Builder builder = new InputDevice.Builder(); + builder.setEnabled(false); + InputManagerGlobal.TestSession session = + configureExternalHardwareTest(new InputDevice[]{builder.build()}); + assertFalse(mLockPatternUtils.isPinEnhancedPrivacyEnabled(USER_ID)); + session.close(); + } + + @Test + @EnableFlags(Flags.FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void isPinEnhancedPrivacyEnabled_withoutHwKeyboard() throws RemoteException { + InputDevice.Builder builder = new InputDevice.Builder(); + builder.setEnabled(true).setSources(InputDevice.SOURCE_TOUCHSCREEN); + InputManagerGlobal.TestSession session = + configureExternalHardwareTest(new InputDevice[]{builder.build()}); + assertFalse(mLockPatternUtils.isPinEnhancedPrivacyEnabled(USER_ID)); + session.close(); + } + + @Test + @EnableFlags(Flags.FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void isPinEnhancedPrivacyEnabled_withoutFullHwKeyboard() throws RemoteException { + InputDevice.Builder builder = new InputDevice.Builder(); + builder + .setEnabled(true) + .setSources(InputDevice.SOURCE_KEYBOARD) + .setKeyboardType(InputDevice.KEYBOARD_TYPE_NON_ALPHABETIC); + InputManagerGlobal.TestSession session = + configureExternalHardwareTest(new InputDevice[]{builder.build()}); + assertFalse(mLockPatternUtils.isPinEnhancedPrivacyEnabled(USER_ID)); + session.close(); + } + + @Test + @DisableFlags(Flags.FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void isPinEnhancedPrivacyEnabled_withHwKeyboardOldDefault() throws RemoteException { + InputDevice.Builder builder = new InputDevice.Builder(); + builder + .setEnabled(true) + .setSources(InputDevice.SOURCE_KEYBOARD) + .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC); + InputManagerGlobal.TestSession session = + configureExternalHardwareTest(new InputDevice[]{builder.build()}); + assertFalse(mLockPatternUtils.isPinEnhancedPrivacyEnabled(USER_ID)); + session.close(); + } + + @Test + @EnableFlags(Flags.FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void isPinEnhancedPrivacyEnabled_withHwKeyboard() throws RemoteException { + InputDevice.Builder builder = new InputDevice.Builder(); + builder + .setEnabled(true) + .setSources(InputDevice.SOURCE_KEYBOARD) + .setKeyboardType(InputDevice.KEYBOARD_TYPE_ALPHABETIC); + InputManagerGlobal.TestSession session = + configureExternalHardwareTest(new InputDevice[]{builder.build()}); + assertTrue(mLockPatternUtils.isPinEnhancedPrivacyEnabled(USER_ID)); + session.close(); + } + + @Test + @EnableFlags(Flags.FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void isVisiblePatternEnabled_noDevices() throws RemoteException { + InputManagerGlobal.TestSession session = configureExternalHardwareTest(new InputDevice[0]); + assertTrue(mLockPatternUtils.isVisiblePatternEnabled(USER_ID)); + session.close(); + } + + @Test + @EnableFlags(Flags.FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void isVisiblePatternEnabled_noEnabledDevices() throws RemoteException { + InputDevice.Builder builder = new InputDevice.Builder(); + builder.setEnabled(false); + InputManagerGlobal.TestSession session = + configureExternalHardwareTest(new InputDevice[]{builder.build()}); + assertTrue(mLockPatternUtils.isVisiblePatternEnabled(USER_ID)); + session.close(); + } + + @Test + @EnableFlags(Flags.FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void isVisiblePatternEnabled_noPointingDevices() throws RemoteException { + InputDevice.Builder builder = new InputDevice.Builder(); + builder + .setEnabled(true) + .setSources(InputDevice.SOURCE_TOUCHSCREEN); + InputManagerGlobal.TestSession session = + configureExternalHardwareTest(new InputDevice[]{builder.build()}); + assertTrue(mLockPatternUtils.isVisiblePatternEnabled(USER_ID)); + session.close(); + } + + @Test + @EnableFlags(Flags.FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void isVisiblePatternEnabled_externalPointingDevice() throws RemoteException { + InputDevice.Builder builder = new InputDevice.Builder(); + builder + .setEnabled(true) + .setSources(InputDevice.SOURCE_CLASS_POINTER); + InputManagerGlobal.TestSession session = + configureExternalHardwareTest(new InputDevice[]{builder.build()}); + assertFalse(mLockPatternUtils.isVisiblePatternEnabled(USER_ID)); + session.close(); + } + + @Test + @DisableFlags(Flags.FLAG_HIDE_LAST_CHAR_WITH_PHYSICAL_INPUT) + public void isVisiblePatternEnabled_externalPointingDeviceOldDefault() throws RemoteException { + InputDevice.Builder builder = new InputDevice.Builder(); + builder + .setEnabled(true) + .setSources(InputDevice.SOURCE_CLASS_POINTER); + InputManagerGlobal.TestSession session = + configureExternalHardwareTest(new InputDevice[]{builder.build()}); + assertTrue(mLockPatternUtils.isVisiblePatternEnabled(USER_ID)); + session.close(); + } } diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 2398e7134b34..f136e065a405 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -125,6 +125,7 @@ applications that come with the platform <permission name="android.permission.SUBSTITUTE_NOTIFICATION_APP_NAME"/> <permission name="android.permission.PACKAGE_USAGE_STATS"/> <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> + <permission name="android.permission.RESOLVE_COMPONENT_FOR_UID"/> </privapp-permissions> <privapp-permissions package="com.android.phone"> @@ -609,6 +610,8 @@ applications that come with the platform <permission name="android.permission.MANAGE_INTRUSION_DETECTION_STATE" /> <!-- Permission required for CTS test - KeyguardLockedStateApiTest --> <permission name="android.permission.SUBSCRIBE_TO_KEYGUARD_LOCKED_STATE" /> + <!-- Permission required for CTS test - CtsContentProviderMultiUserTest --> + <permission name="android.permission.RESOLVE_COMPONENT_FOR_UID"/> </privapp-permissions> <privapp-permissions package="com.android.statementservice"> diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 5f1fb4b44613..500548500927 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -171,3 +171,10 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "task_view_repository" + namespace: "multitasking" + description: "Factor task-view state tracking out of taskviewtransitions" + bug: "384976265" +} diff --git a/libs/WindowManager/Shell/multivalentTests/Android.bp b/libs/WindowManager/Shell/multivalentTests/Android.bp index 41d1b5c15369..eecf199a3ec2 100644 --- a/libs/WindowManager/Shell/multivalentTests/Android.bp +++ b/libs/WindowManager/Shell/multivalentTests/Android.bp @@ -55,6 +55,7 @@ android_robolectric_test { "truth", "flag-junit-base", "flag-junit", + "testables", ], auto_gen_config: true, } @@ -77,6 +78,7 @@ android_test { "truth", "platform-test-annotations", "platform-test-rules", + "testables", ], libs: [ "android.test.base.stubs.system", diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt index 0d8f80935f5a..3e01256fd67c 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt @@ -16,6 +16,8 @@ package com.android.wm.shell.bubbles.bar +import android.animation.AnimatorTestRule +import android.app.ActivityManager import android.content.Context import android.graphics.Insets import android.graphics.Rect @@ -23,7 +25,6 @@ import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.widget.FrameLayout -import androidx.core.animation.AnimatorTestRule import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -36,27 +37,34 @@ import com.android.wm.shell.bubbles.BubbleExpandedViewManager import com.android.wm.shell.bubbles.BubbleLogger import com.android.wm.shell.bubbles.BubbleOverflow import com.android.wm.shell.bubbles.BubblePositioner +import com.android.wm.shell.bubbles.BubbleTaskView import com.android.wm.shell.bubbles.DeviceConfig import com.android.wm.shell.bubbles.FakeBubbleExpandedViewManager import com.android.wm.shell.bubbles.FakeBubbleFactory -import com.android.wm.shell.bubbles.FakeBubbleTaskViewFactory +import com.android.wm.shell.taskview.TaskView +import com.android.wm.shell.taskview.TaskViewTaskController import com.google.common.truth.Truth.assertThat import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit import org.junit.After import org.junit.Before -import org.junit.ClassRule +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever /** Tests for [BubbleBarAnimationHelper] */ @SmallTest @RunWith(AndroidJUnit4::class) class BubbleBarAnimationHelperTest { - companion object { - @JvmField @ClassRule val animatorTestRule: AnimatorTestRule = AnimatorTestRule() + @get:Rule val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this) + companion object { const val SCREEN_WIDTH = 2000 const val SCREEN_HEIGHT = 1000 } @@ -148,6 +156,26 @@ class BubbleBarAnimationHelperTest { } @Test + fun animateSwitch_bubbleToBubble_updateTaskBounds() { + val fromBubble = createBubble("from").initialize(container) + val toBubbleTaskController = mock<TaskViewTaskController>() + val toBubble = createBubble("to", toBubbleTaskController).initialize(container) + + getInstrumentation().runOnMainSync { + animationHelper.animateSwitch(fromBubble, toBubble) {} + // Start the animation, but don't finish + animatorTestRule.advanceTimeBy(100) + } + getInstrumentation().waitForIdleSync() + // Clear invocations to ensure that bounds update happens after animation ends + clearInvocations(toBubbleTaskController) + getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(900) } + getInstrumentation().waitForIdleSync() + + verify(toBubbleTaskController).setWindowBounds(any()) + } + + @Test fun animateSwitch_bubbleToOverflow_oldHiddenNewShown() { val fromBubble = createBubble(key = "from").initialize(container) val overflow = createOverflow().initialize(container) @@ -193,13 +221,43 @@ class BubbleBarAnimationHelperTest { assertThat(toBubble.bubbleBarExpandedView?.isSurfaceZOrderedOnTop).isFalse() } - private fun createBubble(key: String): Bubble { + @Test + fun animateToRestPosition_updateTaskBounds() { + val taskController = mock<TaskViewTaskController>() + val bubble = createBubble("key", taskController).initialize(container) + + getInstrumentation().runOnMainSync { + animationHelper.animateExpansion(bubble) {} + animatorTestRule.advanceTimeBy(1000) + } + getInstrumentation().waitForIdleSync() + getInstrumentation().runOnMainSync { + animationHelper.animateToRestPosition() + animatorTestRule.advanceTimeBy(100) + } + // Clear invocations to ensure that bounds update happens after animation ends + clearInvocations(taskController) + getInstrumentation().runOnMainSync { animatorTestRule.advanceTimeBy(900) } + getInstrumentation().waitForIdleSync() + + verify(taskController).setWindowBounds(any()) + } + + private fun createBubble( + key: String, + taskViewTaskController: TaskViewTaskController = mock<TaskViewTaskController>(), + ): Bubble { + val taskView = TaskView(context, taskViewTaskController) + val taskInfo = mock<ActivityManager.RunningTaskInfo>() + whenever(taskViewTaskController.taskInfo).thenReturn(taskInfo) + val bubbleTaskView = BubbleTaskView(taskView, mainExecutor) + val bubbleBarExpandedView = FakeBubbleFactory.createExpandedView( context, bubblePositioner, expandedViewManager, - FakeBubbleTaskViewFactory(context, mainExecutor).create(), + bubbleTaskView, mainExecutor, bgExecutor, bubbleLogger, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java index 3e8a9b64dac6..3188e5b9c6d2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java @@ -463,6 +463,7 @@ public class BubbleBarAnimationHelper { super.onAnimationEnd(animation); bbev.resetPivot(); bbev.setDragging(false); + updateExpandedView(bbev); } }); startNewAnimator(animatorSet); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java index c74bf53268f9..9ebb7f5aa270 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java @@ -643,7 +643,9 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged t.setPosition(animatingLeash, x, endY); t.setAlpha(animatingLeash, 1.f); } - dispatchEndPositioning(mDisplayId, mCancelled, t); + if (!android.view.inputmethod.Flags.refactorInsetsController()) { + dispatchEndPositioning(mDisplayId, mCancelled, t); + } if (mAnimationDirection == DIRECTION_HIDE && !mCancelled) { ImeTracker.forLogging().onProgress(mStatsToken, ImeTracker.PHASE_WM_ANIMATION_RUNNING); @@ -659,6 +661,14 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged ImeTracker.forLogging().onCancelled(mStatsToken, ImeTracker.PHASE_WM_ANIMATION_RUNNING); } + if (android.view.inputmethod.Flags.refactorInsetsController()) { + // In split screen, we also set {@link + // WindowContainer#mExcludeInsetsTypes} but this should only happen after + // the IME client visibility was set. Otherwise the insets will we + // dispatched too early, and we get a flicker. Thus, only dispatching it + // after reporting that the IME is hidden to system server. + dispatchEndPositioning(mDisplayId, mCancelled, t); + } if (DEBUG_IME_VISIBILITY) { EventLog.writeEvent(IMF_IME_REMOTE_ANIM_END, mStatsToken != null ? mStatsToken.getTag() : ImeTracker.TOKEN_NONE, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index 21c44c9b92ee..4bcec702281d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -571,9 +571,9 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // For flexible split, expand app offscreen as well if (mDividerSnapAlgorithm.areOffscreenRatiosSupported()) { if (position <= mDividerSnapAlgorithm.getMiddleTarget().position) { - bounds1.top = bounds1.bottom - bounds2.width(); + bounds1.top = bounds1.bottom - bounds2.height(); } else { - bounds2.bottom = bounds2.top + bounds1.width(); + bounds2.bottom = bounds2.top + bounds1.height(); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt index dfa2d9b6bb63..9a60cfeed7c1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt @@ -38,6 +38,7 @@ import androidx.core.util.putAll import com.android.internal.protolog.ProtoLog import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.TaskUpdate import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON @@ -236,6 +237,7 @@ class DesktopModeLoggerTransitionObserver( ) { // Sessions is finishing, log task updates followed by an exit event identifyAndLogTaskUpdates( + transitionInfo, preTransitionVisibleFreeformTasks, postTransitionVisibleFreeformTasks, ) @@ -252,12 +254,14 @@ class DesktopModeLoggerTransitionObserver( desktopModeEventLogger.logSessionEnter(getEnterReason(transitionInfo)) identifyAndLogTaskUpdates( + transitionInfo, preTransitionVisibleFreeformTasks, postTransitionVisibleFreeformTasks, ) } else if (isSessionActive) { // Session is neither starting, nor finishing, log task updates if there are any identifyAndLogTaskUpdates( + transitionInfo, preTransitionVisibleFreeformTasks, postTransitionVisibleFreeformTasks, ) @@ -270,6 +274,7 @@ class DesktopModeLoggerTransitionObserver( /** Compare the old and new state of taskInfos and identify and log the changes */ private fun identifyAndLogTaskUpdates( + transitionInfo: TransitionInfo, preTransitionVisibleFreeformTasks: SparseArray<TaskInfo>, postTransitionVisibleFreeformTasks: SparseArray<TaskInfo>, ) { @@ -304,9 +309,19 @@ class DesktopModeLoggerTransitionObserver( // find old tasks that were removed preTransitionVisibleFreeformTasks.forEach { taskId, taskInfo -> if (!postTransitionVisibleFreeformTasks.containsKey(taskId)) { - desktopModeEventLogger.logTaskRemoved( - buildTaskUpdateForTask(taskInfo, postTransitionVisibleFreeformTasks.size()) - ) + val minimizeReason = + if (transitionInfo.type == Transitions.TRANSIT_MINIMIZE) { + MinimizeReason.MINIMIZE_BUTTON + } else { + null + } + val taskUpdate = + buildTaskUpdateForTask( + taskInfo, + postTransitionVisibleFreeformTasks.size(), + minimizeReason, + ) + desktopModeEventLogger.logTaskRemoved(taskUpdate) Trace.setCounter( Trace.TRACE_TAG_WINDOW_MANAGER, VISIBLE_TASKS_COUNTER_NAME, @@ -320,7 +335,11 @@ class DesktopModeLoggerTransitionObserver( } } - private fun buildTaskUpdateForTask(taskInfo: TaskInfo, visibleTasks: Int): TaskUpdate { + private fun buildTaskUpdateForTask( + taskInfo: TaskInfo, + visibleTasks: Int, + minimizeReason: MinimizeReason? = null, + ): TaskUpdate { val screenBounds = taskInfo.configuration.windowConfiguration.bounds val positionInParent = taskInfo.positionInParent return TaskUpdate( @@ -331,6 +350,7 @@ class DesktopModeLoggerTransitionObserver( taskX = positionInParent.x, taskY = positionInParent.y, visibleTaskCount = visibleTasks, + minimizeReason = minimizeReason, ) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt index 606a729305b4..90191345147c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt @@ -82,7 +82,7 @@ fun calculateInitialBounds( // For portrait resizeable activities, respect apps fullscreen width but // apply ideal size height. Size( - taskInfo.appCompatTaskInfo.topActivityLetterboxAppWidth, + taskInfo.appCompatTaskInfo.topActivityAppBounds.width(), idealSize.height, ) } else { @@ -104,7 +104,7 @@ fun calculateInitialBounds( // apply custom app width. Size( customPortraitWidthForLandscapeApp, - taskInfo.appCompatTaskInfo.topActivityLetterboxAppHeight, + taskInfo.appCompatTaskInfo.topActivityAppBounds.height(), ) } else { // For portrait resizeable activities, simply apply ideal size. @@ -196,13 +196,8 @@ fun maximizeSizeGivenAspectRatio( /** Calculates the aspect ratio of an activity from its fullscreen bounds. */ fun calculateAspectRatio(taskInfo: RunningTaskInfo): Float { - val appLetterboxWidth = taskInfo.appCompatTaskInfo.topActivityLetterboxAppWidth - val appLetterboxHeight = taskInfo.appCompatTaskInfo.topActivityLetterboxAppHeight - if (taskInfo.appCompatTaskInfo.isTopActivityLetterboxed || !taskInfo.canChangeAspectRatio) { - return maxOf(appLetterboxWidth, appLetterboxHeight) / - minOf(appLetterboxWidth, appLetterboxHeight).toFloat() - } - val appBounds = taskInfo.configuration.windowConfiguration.appBounds ?: return 1f + if (taskInfo.appCompatTaskInfo.topActivityAppBounds.isEmpty) return 1f + val appBounds = taskInfo.appCompatTaskInfo.topActivityAppBounds return maxOf(appBounds.height(), appBounds.width()) / minOf(appBounds.height(), appBounds.width()).toFloat() } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt index 45faba6e341f..0330a5f0c4e7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt @@ -127,14 +127,20 @@ class DesktopTasksLimiter( override fun onTransitionStarting(transition: IBinder) { val mActiveTaskDetails = activeTransitionTokensAndTasks[transition] - if (mActiveTaskDetails != null && mActiveTaskDetails.transitionInfo != null) { - // Begin minimize window CUJ instrumentation. - interactionJankMonitor.begin( - mActiveTaskDetails.transitionInfo?.rootLeash, - context, - handler, - CUJ_DESKTOP_MODE_MINIMIZE_WINDOW, - ) + val info = mActiveTaskDetails?.transitionInfo ?: return + val minimizeChange = getMinimizeChange(info, mActiveTaskDetails.taskId) ?: return + // Begin minimize window CUJ instrumentation. + interactionJankMonitor.begin( + minimizeChange.leash, + context, + handler, + CUJ_DESKTOP_MODE_MINIMIZE_WINDOW, + ) + } + + private fun getMinimizeChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? { + return info.changes.find { change -> + change.taskInfo?.taskId == taskId && change.mode == TRANSIT_TO_BACK } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java index a611fe1db2ce..c4ff87d175a7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragSession.java @@ -74,7 +74,7 @@ public class DragSession { mInitialDragData = data; mInitialDragFlags = dragFlags; displayLayout = dispLayout; - hideDragSourceTaskId = data.getDescription().getExtras() != null + hideDragSourceTaskId = data != null && data.getDescription().getExtras() != null ? data.getDescription().getExtras().getInt(EXTRA_HIDE_DRAG_SOURCE_TASK_ID, -1) : -1; ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 7928e5ed4188..a7a5f09c88f8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -935,10 +935,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, // back to the decoration using // {@link DesktopModeWindowDecoration#setOnMaximizeOrRestoreClickListener}, which // should shared with the maximize menu's maximize/restore actions. + final DesktopRepository desktopRepository = mDesktopUserRepositories.getProfile( + decoration.mTaskInfo.userId); if (Flags.enableFullyImmersiveInDesktop() - && TaskInfoKt.getRequestingImmersive(decoration.mTaskInfo)) { - // Task is requesting immersive, so it should either enter or exit immersive, - // depending on immersive state. + && desktopRepository.isTaskInFullImmersiveState( + decoration.mTaskInfo.taskId)) { + // Task is in immersive and should exit. onEnterOrExitImmersive(decoration.mTaskInfo); } else { // Full immersive is disabled or task doesn't request/support it, so just diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt index 9972247df46d..ab1ac1a0efa3 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt @@ -44,6 +44,7 @@ import android.tools.flicker.assertors.assertions.AppWindowOnTopAtStart import android.tools.flicker.assertors.assertions.AppWindowRemainInsideDisplayBounds import android.tools.flicker.assertors.assertions.AppWindowReturnsToStartBoundsAndPosition import android.tools.flicker.assertors.assertions.LauncherWindowReplacesAppAsTopWindow +import android.tools.flicker.assertors.assertions.VisibleLayersShownMoreThanOneConsecutiveEntry import android.tools.flicker.config.AssertionTemplates import android.tools.flicker.config.FlickerConfigEntry import android.tools.flicker.config.ScenarioId @@ -463,7 +464,9 @@ class DesktopModeFlickerScenarios { } ), assertions = - AssertionTemplates.COMMON_ASSERTIONS + + AssertionTemplates.COMMON_ASSERTIONS.toMutableMap().also { + it.remove(VisibleLayersShownMoreThanOneConsecutiveEntry()) + } + listOf( AppWindowOnTopAtStart(DESKTOP_MODE_APP), AppWindowBecomesInvisible(DESKTOP_MODE_APP), @@ -489,7 +492,9 @@ class DesktopModeFlickerScenarios { } ), assertions = - AssertionTemplates.COMMON_ASSERTIONS + + AssertionTemplates.COMMON_ASSERTIONS.toMutableMap().also { + it.remove(VisibleLayersShownMoreThanOneConsecutiveEntry()) + } + listOf( AppWindowOnTopAtStart(DESKTOP_MODE_APP), AppWindowBecomesInvisible(DESKTOP_MODE_APP), diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsWithKeyboard.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsWithKeyboard.kt new file mode 100644 index 000000000000..56f1dcbf838c --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/MinimizeAppsWithKeyboard.kt @@ -0,0 +1,51 @@ +/* + * 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.wm.shell.flicker + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MINIMIZE_APP +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.MINIMIZE_LAST_APP +import com.android.wm.shell.scenarios.MinimizeAppWindows +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Minimize app windows by pressing META + -. + * + * Assert that the app windows gets hidden. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class MinimizeAppsWithKeyboard : MinimizeAppWindows(usingKeyboard = true) { + @ExpectedScenarios(["MINIMIZE_APP", "MINIMIZE_LAST_APP"]) + @Test + override fun minimizeAllAppWindows() = super.minimizeAllAppWindows() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig() + .use(FlickerServiceConfig.DEFAULT) + .use(MINIMIZE_APP) + .use(MINIMIZE_LAST_APP) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt index 971637b62604..835559cd0936 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAppWindows.kt @@ -43,7 +43,10 @@ import org.junit.Test */ @Ignore("Test Base Class") abstract class MinimizeAppWindows -constructor(private val rotation: Rotation = Rotation.ROTATION_0) { +constructor( + private val rotation: Rotation = Rotation.ROTATION_0, + private val usingKeyboard: Boolean = false +) { private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() private val tapl = LauncherInstrumentation() private val wmHelper = WindowManagerStateHelper(instrumentation) @@ -68,9 +71,9 @@ constructor(private val rotation: Rotation = Rotation.ROTATION_0) { @Test open fun minimizeAllAppWindows() { - testApp3.minimizeDesktopApp(wmHelper, device) - testApp2.minimizeDesktopApp(wmHelper, device) - testApp1.minimizeDesktopApp(wmHelper, device) + testApp3.minimizeDesktopApp(wmHelper, device, usingKeyboard = usingKeyboard) + testApp2.minimizeDesktopApp(wmHelper, device, usingKeyboard = usingKeyboard) + testApp1.minimizeDesktopApp(wmHelper, device, usingKeyboard = usingKeyboard) } @After diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt index abd707817621..c0ff2f0652b3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt @@ -45,6 +45,7 @@ import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UNSET_M import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UNSET_UNMINIMIZE_REASON import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UnminimizeReason import com.google.common.truth.Truth.assertThat +import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test @@ -77,6 +78,12 @@ class DesktopModeEventLoggerTest : ShellTestCase() { doReturn(DISPLAY_HEIGHT).whenever(displayLayout).height() } + @After + fun tearDown() { + clearInvocations(staticMockMarker(FrameworkStatsLog::class.java)) + clearInvocations(staticMockMarker(EventLogTags::class.java)) + } + @Test fun logSessionEnter_logsEnterReasonWithNewSessionId() { desktopModeEventLogger.logSessionEnter(EnterReason.KEYBOARD_SHORTCUT_ENTER) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt index 43684fb92b64..0154ff31c989 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt @@ -46,6 +46,7 @@ import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.EnterReason import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ExitReason +import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.TaskUpdate import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON @@ -566,7 +567,10 @@ class DesktopModeLoggerTransitionObserverTest : ShellTestCase() { assertFalse(transitionObserver.isSessionActive) verify(desktopModeEventLogger, times(1)).logSessionExit(eq(ExitReason.TASK_MINIMIZED)) - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(DEFAULT_TASK_UPDATE)) + verify(desktopModeEventLogger, times(1)) + .logTaskRemoved( + eq(DEFAULT_TASK_UPDATE.copy(minimizeReason = MinimizeReason.MINIMIZE_BUTTON)) + ) verifyZeroInteractions(desktopModeEventLogger) } 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 4f37180baa37..e1c2153014fa 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 @@ -4160,8 +4160,7 @@ class DesktopTasksControllerTest : ShellTestCase() { screenOrientation = SCREEN_ORIENTATION_LANDSCAPE configuration.windowConfiguration.appBounds = bounds } - appCompatTaskInfo.topActivityLetterboxAppWidth = bounds.width() - appCompatTaskInfo.topActivityLetterboxAppHeight = bounds.height() + appCompatTaskInfo.topActivityAppBounds.set(0, 0, bounds.width(), bounds.height()) isResizeable = false } @@ -4879,15 +4878,19 @@ class DesktopTasksControllerTest : ShellTestCase() { appCompatTaskInfo.isSystemFullscreenOverrideEnabled = enableSystemFullscreenOverride if (deviceOrientation == ORIENTATION_LANDSCAPE) { - configuration.windowConfiguration.appBounds = - Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT) - appCompatTaskInfo.topActivityLetterboxAppWidth = DISPLAY_DIMENSION_LONG - appCompatTaskInfo.topActivityLetterboxAppHeight = DISPLAY_DIMENSION_SHORT + appCompatTaskInfo.topActivityAppBounds.set( + 0, + 0, + DISPLAY_DIMENSION_LONG, + DISPLAY_DIMENSION_SHORT, + ) } else { - configuration.windowConfiguration.appBounds = - Rect(0, 0, DISPLAY_DIMENSION_SHORT, DISPLAY_DIMENSION_LONG) - appCompatTaskInfo.topActivityLetterboxAppWidth = DISPLAY_DIMENSION_SHORT - appCompatTaskInfo.topActivityLetterboxAppHeight = DISPLAY_DIMENSION_LONG + appCompatTaskInfo.topActivityAppBounds.set( + 0, + 0, + DISPLAY_DIMENSION_SHORT, + DISPLAY_DIMENSION_LONG, + ) } if (shouldLetterbox) { @@ -4897,17 +4900,15 @@ class DesktopTasksControllerTest : ShellTestCase() { screenOrientation == SCREEN_ORIENTATION_PORTRAIT ) { // Letterbox to portrait size - appCompatTaskInfo.setTopActivityLetterboxed(true) - appCompatTaskInfo.topActivityLetterboxAppWidth = 1200 - appCompatTaskInfo.topActivityLetterboxAppHeight = 1600 + appCompatTaskInfo.isTopActivityLetterboxed = true + appCompatTaskInfo.topActivityAppBounds.set(0, 0, 1200, 1600) } else if ( deviceOrientation == ORIENTATION_PORTRAIT && screenOrientation == SCREEN_ORIENTATION_LANDSCAPE ) { // Letterbox to landscape size - appCompatTaskInfo.setTopActivityLetterboxed(true) - appCompatTaskInfo.topActivityLetterboxAppWidth = 1600 - appCompatTaskInfo.topActivityLetterboxAppHeight = 1200 + appCompatTaskInfo.isTopActivityLetterboxed = true + appCompatTaskInfo.topActivityAppBounds.set(0, 0, 1600, 1200) } } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt index e6f1fcf7f14f..52602f22fd4b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt @@ -551,7 +551,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { .getTransitionObserver() .onTransitionReady( transition, - TransitionInfoBuilder(TRANSIT_OPEN).build(), + TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), StubTransaction() /* startTransaction */, StubTransaction(), /* finishTransaction */ ) @@ -583,7 +583,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { .getTransitionObserver() .onTransitionReady( transition, - TransitionInfoBuilder(TRANSIT_OPEN).build(), + TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), StubTransaction() /* startTransaction */, StubTransaction(), /* finishTransaction */ ) @@ -616,7 +616,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { .getTransitionObserver() .onTransitionReady( mergedTransition, - TransitionInfoBuilder(TRANSIT_OPEN).build(), + TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), StubTransaction() /* startTransaction */, StubTransaction(), /* finishTransaction */ ) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragSessionTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragSessionTest.kt index 3d59342f62d8..8ccca07142aa 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragSessionTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragSessionTest.kt @@ -59,6 +59,13 @@ class DragSessionTest : ShellTestCase() { } @Test + fun testNullClipData() { + // Start a new drag session with null data + val session = DragSession(activityTaskManager, displayLayout, null, 0) + assertThat(session.hideDragSourceTaskId).isEqualTo(-1) + } + + @Test fun testGetRunningTask() { // Set up running tasks val runningTasks = listOf( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index aead0a7afb53..ffe8e7135513 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -1054,26 +1054,6 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) - fun testImmersiveButtonClick_entersImmersiveMode() { - val onClickListenerCaptor = forClass(View.OnClickListener::class.java) - as ArgumentCaptor<View.OnClickListener> - val decor = createOpenTaskDecoration( - windowingMode = WINDOWING_MODE_FREEFORM, - onCaptionButtonClickListener = onClickListenerCaptor, - requestingImmersive = true, - ) - val view = mock(View::class.java) - whenever(view.id).thenReturn(R.id.maximize_window) - whenever(mockDesktopRepository.isTaskInFullImmersiveState(decor.mTaskInfo.taskId)) - .thenReturn(false) - - onClickListenerCaptor.value.onClick(view) - - verify(mockDesktopImmersiveController).moveTaskToImmersive(decor.mTaskInfo) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun testImmersiveRestoreButtonClick_exitsImmersiveMode() { val onClickListenerCaptor = forClass(View.OnClickListener::class.java) as ArgumentCaptor<View.OnClickListener> diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp index 1bc15d72bacc..cc4a29b31996 100644 --- a/libs/androidfw/Android.bp +++ b/libs/androidfw/Android.bp @@ -199,6 +199,7 @@ cc_test { // This is to suppress warnings/errors from gtest "-Wno-unnamed-type-template-args", ], + require_root: true, srcs: [ // Helpers/infra for testing. "tests/CommonHelpers.cpp", diff --git a/libs/androidfw/AssetManager.cpp b/libs/androidfw/AssetManager.cpp index e6182454ad8a..5955915c9fcd 100644 --- a/libs/androidfw/AssetManager.cpp +++ b/libs/androidfw/AssetManager.cpp @@ -1420,18 +1420,20 @@ void AssetManager::mergeInfoLocked(SortedVector<AssetDir::FileInfo>* pMergedInfo Mutex AssetManager::SharedZip::gLock; DefaultKeyedVector<String8, wp<AssetManager::SharedZip> > AssetManager::SharedZip::gOpen; -AssetManager::SharedZip::SharedZip(const String8& path, time_t modWhen) - : mPath(path), mZipFile(NULL), mModWhen(modWhen), - mResourceTableAsset(NULL), mResourceTable(NULL) -{ - if (kIsDebug) { - ALOGI("Creating SharedZip %p %s\n", this, mPath.c_str()); - } - ALOGV("+++ opening zip '%s'\n", mPath.c_str()); - mZipFile = ZipFileRO::open(mPath.c_str()); - if (mZipFile == NULL) { - ALOGD("failed to open Zip archive '%s'\n", mPath.c_str()); - } +AssetManager::SharedZip::SharedZip(const String8& path, ModDate modWhen) + : mPath(path), + mZipFile(NULL), + mModWhen(modWhen), + mResourceTableAsset(NULL), + mResourceTable(NULL) { + if (kIsDebug) { + ALOGI("Creating SharedZip %p %s\n", this, mPath.c_str()); + } + ALOGV("+++ opening zip '%s'\n", mPath.c_str()); + mZipFile = ZipFileRO::open(mPath.c_str()); + if (mZipFile == NULL) { + ALOGD("failed to open Zip archive '%s'\n", mPath.c_str()); + } } AssetManager::SharedZip::SharedZip(int fd, const String8& path) @@ -1453,7 +1455,7 @@ sp<AssetManager::SharedZip> AssetManager::SharedZip::get(const String8& path, bool createIfNotPresent) { AutoMutex _l(gLock); - time_t modWhen = getFileModDate(path.c_str()); + auto modWhen = getFileModDate(path.c_str()); sp<SharedZip> zip = gOpen.valueFor(path).promote(); if (zip != NULL && zip->mModWhen == modWhen) { return zip; @@ -1520,8 +1522,8 @@ ResTable* AssetManager::SharedZip::setResourceTable(ResTable* res) bool AssetManager::SharedZip::isUpToDate() { - time_t modWhen = getFileModDate(mPath.c_str()); - return mModWhen == modWhen; + auto modWhen = getFileModDate(mPath.c_str()); + return mModWhen == modWhen; } void AssetManager::SharedZip::addOverlay(const asset_path& ap) diff --git a/libs/androidfw/TypeWrappers.cpp b/libs/androidfw/TypeWrappers.cpp index 70d14a11830e..970463492c1a 100644 --- a/libs/androidfw/TypeWrappers.cpp +++ b/libs/androidfw/TypeWrappers.cpp @@ -16,8 +16,6 @@ #include <androidfw/TypeWrappers.h> -#include <algorithm> - namespace android { TypeVariant::TypeVariant(const ResTable_type* data) : data(data), mLength(dtohl(data->entryCount)) { @@ -31,30 +29,44 @@ TypeVariant::TypeVariant(const ResTable_type* data) : data(data), mLength(dtohl( ALOGE("Type's entry indices extend beyond its boundaries"); mLength = 0; } else { - mLength = ResTable_sparseTypeEntry{entryIndices[entryCount - 1]}.idx + 1; + mLength = dtohs(ResTable_sparseTypeEntry{entryIndices[entryCount - 1]}.idx) + 1; } } } TypeVariant::iterator& TypeVariant::iterator::operator++() { - mIndex++; + ++mIndex; if (mIndex > mTypeVariant->mLength) { mIndex = mTypeVariant->mLength; } - return *this; -} -static bool keyCompare(uint32_t entry, uint16_t index) { - return dtohs(ResTable_sparseTypeEntry{entry}.idx) < index; + const ResTable_type* type = mTypeVariant->data; + if ((type->flags & ResTable_type::FLAG_SPARSE) == 0) { + return *this; + } + + // Need to adjust |mSparseIndex| as well if we've passed its current element. + const uint32_t entryCount = dtohl(type->entryCount); + const auto entryIndices = reinterpret_cast<const uint32_t*>( + reinterpret_cast<uintptr_t>(type) + dtohs(type->header.headerSize)); + if (mSparseIndex >= entryCount) { + return *this; // done + } + const auto element = (const ResTable_sparseTypeEntry*)(entryIndices + mSparseIndex); + if (mIndex > dtohs(element->idx)) { + ++mSparseIndex; + } + + return *this; } const ResTable_entry* TypeVariant::iterator::operator*() const { - const ResTable_type* type = mTypeVariant->data; if (mIndex >= mTypeVariant->mLength) { - return NULL; + return nullptr; } - const uint32_t entryCount = dtohl(mTypeVariant->data->entryCount); + const ResTable_type* type = mTypeVariant->data; + const uint32_t entryCount = dtohl(type->entryCount); const uintptr_t containerEnd = reinterpret_cast<uintptr_t>(type) + dtohl(type->header.size); const uint32_t* const entryIndices = reinterpret_cast<const uint32_t*>( @@ -63,18 +75,19 @@ const ResTable_entry* TypeVariant::iterator::operator*() const { sizeof(uint16_t) : sizeof(uint32_t); if (reinterpret_cast<uintptr_t>(entryIndices) + (indexSize * entryCount) > containerEnd) { ALOGE("Type's entry indices extend beyond its boundaries"); - return NULL; + return nullptr; } uint32_t entryOffset; if (type->flags & ResTable_type::FLAG_SPARSE) { - auto iter = std::lower_bound(entryIndices, entryIndices + entryCount, mIndex, keyCompare); - if (iter == entryIndices + entryCount - || dtohs(ResTable_sparseTypeEntry{*iter}.idx) != mIndex) { - return NULL; + if (mSparseIndex >= entryCount) { + return nullptr; } - - entryOffset = static_cast<uint32_t>(dtohs(ResTable_sparseTypeEntry{*iter}.offset)) * 4u; + const auto element = (const ResTable_sparseTypeEntry*)(entryIndices + mSparseIndex); + if (dtohs(element->idx) != mIndex) { + return nullptr; + } + entryOffset = static_cast<uint32_t>(dtohs(element->offset)) * 4u; } else if (type->flags & ResTable_type::FLAG_OFFSET16) { auto entryIndices16 = reinterpret_cast<const uint16_t*>(entryIndices); entryOffset = offset_from16(entryIndices16[mIndex]); @@ -83,25 +96,25 @@ const ResTable_entry* TypeVariant::iterator::operator*() const { } if (entryOffset == ResTable_type::NO_ENTRY) { - return NULL; + return nullptr; } if ((entryOffset & 0x3) != 0) { ALOGE("Index %u points to entry with unaligned offset 0x%08x", mIndex, entryOffset); - return NULL; + return nullptr; } const ResTable_entry* entry = reinterpret_cast<const ResTable_entry*>( reinterpret_cast<uintptr_t>(type) + dtohl(type->entriesStart) + entryOffset); if (reinterpret_cast<uintptr_t>(entry) > containerEnd - sizeof(*entry)) { ALOGE("Entry offset at index %u points outside the Type's boundaries", mIndex); - return NULL; + return nullptr; } else if (reinterpret_cast<uintptr_t>(entry) + entry->size() > containerEnd) { ALOGE("Entry at index %u extends beyond Type's boundaries", mIndex); - return NULL; + return nullptr; } else if (entry->size() < sizeof(*entry)) { ALOGE("Entry at index %u is too small (%zu)", mIndex, entry->size()); - return NULL; + return nullptr; } return entry; } diff --git a/libs/androidfw/include/androidfw/AssetManager.h b/libs/androidfw/include/androidfw/AssetManager.h index ce0985b38986..376c881ea376 100644 --- a/libs/androidfw/include/androidfw/AssetManager.h +++ b/libs/androidfw/include/androidfw/AssetManager.h @@ -280,21 +280,21 @@ private: ~SharedZip(); private: - SharedZip(const String8& path, time_t modWhen); - SharedZip(int fd, const String8& path); - SharedZip(); // <-- not implemented + SharedZip(const String8& path, ModDate modWhen); + SharedZip(int fd, const String8& path); + SharedZip(); // <-- not implemented - String8 mPath; - ZipFileRO* mZipFile; - time_t mModWhen; + String8 mPath; + ZipFileRO* mZipFile; + ModDate mModWhen; - Asset* mResourceTableAsset; - ResTable* mResourceTable; + Asset* mResourceTableAsset; + ResTable* mResourceTable; - Vector<asset_path> mOverlays; + Vector<asset_path> mOverlays; - static Mutex gLock; - static DefaultKeyedVector<String8, wp<SharedZip> > gOpen; + static Mutex gLock; + static DefaultKeyedVector<String8, wp<SharedZip> > gOpen; }; /* diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h index e213fbd22ab0..ac75eb3bb98c 100644 --- a/libs/androidfw/include/androidfw/Idmap.h +++ b/libs/androidfw/include/androidfw/Idmap.h @@ -25,8 +25,9 @@ #include "android-base/macros.h" #include "android-base/unique_fd.h" #include "androidfw/ConfigDescription.h" -#include "androidfw/StringPiece.h" #include "androidfw/ResourceTypes.h" +#include "androidfw/StringPiece.h" +#include "androidfw/misc.h" #include "utils/ByteOrder.h" namespace android { @@ -213,7 +214,7 @@ class LoadedIdmap { android::base::unique_fd idmap_fd_; std::string_view overlay_apk_path_; std::string_view target_apk_path_; - time_t idmap_last_mod_time_; + ModDate idmap_last_mod_time_; private: DISALLOW_COPY_AND_ASSIGN(LoadedIdmap); diff --git a/libs/androidfw/include/androidfw/TypeWrappers.h b/libs/androidfw/include/androidfw/TypeWrappers.h index fb2fad619011..db641b78a4e4 100644 --- a/libs/androidfw/include/androidfw/TypeWrappers.h +++ b/libs/androidfw/include/androidfw/TypeWrappers.h @@ -27,24 +27,14 @@ struct TypeVariant { class iterator { public: - iterator& operator=(const iterator& rhs) { - mTypeVariant = rhs.mTypeVariant; - mIndex = rhs.mIndex; - return *this; - } - bool operator==(const iterator& rhs) const { return mTypeVariant == rhs.mTypeVariant && mIndex == rhs.mIndex; } - bool operator!=(const iterator& rhs) const { - return mTypeVariant != rhs.mTypeVariant || mIndex != rhs.mIndex; - } - iterator operator++(int) { - uint32_t prevIndex = mIndex; + iterator prev = *this; operator++(); - return iterator(mTypeVariant, prevIndex); + return prev; } const ResTable_entry* operator->() const { @@ -60,18 +50,26 @@ struct TypeVariant { private: friend struct TypeVariant; - iterator(const TypeVariant* tv, uint32_t index) - : mTypeVariant(tv), mIndex(index) {} + + enum class Kind { Begin, End }; + iterator(const TypeVariant* tv, Kind kind) + : mTypeVariant(tv) { + mSparseIndex = mIndex = kind == Kind::Begin ? 0 : tv->mLength; + // mSparseIndex here is technically past the number of sparse entries, but it is still + // ok as it is enough to infer that this is the end iterator. + } + const TypeVariant* mTypeVariant; uint32_t mIndex; + uint32_t mSparseIndex; }; iterator beginEntries() const { - return iterator(this, 0); + return iterator(this, iterator::Kind::Begin); } iterator endEntries() const { - return iterator(this, mLength); + return iterator(this, iterator::Kind::End); } const ResTable_type* data; diff --git a/libs/androidfw/include/androidfw/misc.h b/libs/androidfw/include/androidfw/misc.h index 077609d20d55..c9ba8a01a5e9 100644 --- a/libs/androidfw/include/androidfw/misc.h +++ b/libs/androidfw/include/androidfw/misc.h @@ -13,14 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#pragma once -#include <sys/types.h> +#include <time.h> // // Handy utility functions and portability code. // -#ifndef _LIBS_ANDROID_FW_MISC_H -#define _LIBS_ANDROID_FW_MISC_H namespace android { @@ -41,15 +40,35 @@ typedef enum FileType { } FileType; /* get the file's type; follows symlinks */ FileType getFileType(const char* fileName); -/* get the file's modification date; returns -1 w/errno set on failure */ -time_t getFileModDate(const char* fileName); + +// MinGW doesn't support nanosecond resolution in stat() modification time, and given +// that it only matters on the device it's ok to keep it at a seconds level there. +#ifdef _WIN32 +using ModDate = time_t; +inline constexpr ModDate kInvalidModDate = ModDate(-1); +inline constexpr unsigned long long kModDateResolutionNs = 1ull * 1000 * 1000 * 1000; +inline time_t toTimeT(ModDate m) { + return m; +} +#else +using ModDate = timespec; +inline constexpr ModDate kInvalidModDate = {-1, -1}; +inline constexpr unsigned long long kModDateResolutionNs = 1; +inline time_t toTimeT(ModDate m) { + return m.tv_sec; +} +#endif + +/* get the file's modification date; returns kInvalidModDate w/errno set on failure */ +ModDate getFileModDate(const char* fileName); /* same, but also returns -1 if the file has already been deleted */ -time_t getFileModDate(int fd); +ModDate getFileModDate(int fd); // Check if |path| or |fd| resides on a readonly filesystem. bool isReadonlyFilesystem(const char* path); bool isReadonlyFilesystem(int fd); -}; // namespace android +} // namespace android -#endif // _LIBS_ANDROID_FW_MISC_H +// Whoever uses getFileModDate() will need this as well +bool operator==(const timespec& l, const timespec& r); diff --git a/libs/androidfw/misc.cpp b/libs/androidfw/misc.cpp index 93dcaf549a90..32f3624a3aee 100644 --- a/libs/androidfw/misc.cpp +++ b/libs/androidfw/misc.cpp @@ -28,11 +28,13 @@ #include <sys/vfs.h> #endif // __linux__ -#include <cstring> -#include <cstdio> #include <errno.h> #include <sys/stat.h> +#include <cstdio> +#include <cstring> +#include <tuple> + namespace android { /* @@ -73,27 +75,34 @@ FileType getFileType(const char* fileName) } } -/* - * Get a file's modification date. - */ -time_t getFileModDate(const char* fileName) { - struct stat sb; - if (stat(fileName, &sb) < 0) { - return (time_t)-1; - } - return sb.st_mtime; +static ModDate getModDate(const struct stat& st) { +#ifdef _WIN32 + return st.st_mtime; +#elif defined(__APPLE__) + return st.st_mtimespec; +#else + return st.st_mtim; +#endif } -time_t getFileModDate(int fd) { - struct stat sb; - if (fstat(fd, &sb) < 0) { - return (time_t)-1; - } - if (sb.st_nlink <= 0) { - errno = ENOENT; - return (time_t)-1; - } - return sb.st_mtime; +ModDate getFileModDate(const char* fileName) { + struct stat sb; + if (stat(fileName, &sb) < 0) { + return kInvalidModDate; + } + return getModDate(sb); +} + +ModDate getFileModDate(int fd) { + struct stat sb; + if (fstat(fd, &sb) < 0) { + return kInvalidModDate; + } + if (sb.st_nlink <= 0) { + errno = ENOENT; + return kInvalidModDate; + } + return getModDate(sb); } #ifndef __linux__ @@ -124,4 +133,8 @@ bool isReadonlyFilesystem(int fd) { } #endif // __linux__ -}; // namespace android +} // namespace android + +bool operator==(const timespec& l, const timespec& r) { + return std::tie(l.tv_sec, l.tv_nsec) == std::tie(r.tv_sec, l.tv_nsec); +} diff --git a/libs/androidfw/tests/Idmap_test.cpp b/libs/androidfw/tests/Idmap_test.cpp index 60aa7d88925d..cb2e56f5f5e4 100644 --- a/libs/androidfw/tests/Idmap_test.cpp +++ b/libs/androidfw/tests/Idmap_test.cpp @@ -14,6 +14,9 @@ * limitations under the License. */ +#include <chrono> +#include <thread> + #include "android-base/file.h" #include "androidfw/ApkAssets.h" #include "androidfw/AssetManager2.h" @@ -27,6 +30,7 @@ #include "data/overlayable/R.h" #include "data/system/R.h" +using namespace std::chrono_literals; using ::testing::NotNull; namespace overlay = com::android::overlay; @@ -218,10 +222,13 @@ TEST_F(IdmapTest, OverlayAssetsIsUpToDate) { unlink(temp_file.path); ASSERT_FALSE(apk_assets->IsUpToDate()); - sleep(2); + + const auto sleep_duration = + std::chrono::nanoseconds(std::max(kModDateResolutionNs, 1'000'000ull)); + std::this_thread::sleep_for(sleep_duration); base::WriteStringToFile("hello", temp_file.path); - sleep(2); + std::this_thread::sleep_for(sleep_duration); ASSERT_FALSE(apk_assets->IsUpToDate()); } diff --git a/libs/androidfw/tests/TypeWrappers_test.cpp b/libs/androidfw/tests/TypeWrappers_test.cpp index ed30904ec179..d66e05805484 100644 --- a/libs/androidfw/tests/TypeWrappers_test.cpp +++ b/libs/androidfw/tests/TypeWrappers_test.cpp @@ -14,28 +14,42 @@ * limitations under the License. */ -#include <algorithm> #include <androidfw/ResourceTypes.h> #include <androidfw/TypeWrappers.h> -#include <utils/String8.h> +#include <androidfw/Util.h> + +#include <optional> +#include <vector> #include <gtest/gtest.h> namespace android { -// create a ResTable_type in memory with a vector of Res_value* -static ResTable_type* createTypeTable(std::vector<Res_value*>& values, - bool compact_entry = false, - bool short_offsets = false) +using ResValueVector = std::vector<std::optional<Res_value>>; + +// create a ResTable_type in memory +static util::unique_cptr<ResTable_type> createTypeTable( + const ResValueVector& in_values, bool compact_entry, bool short_offsets, bool sparse) { + ResValueVector sparse_values; + if (sparse) { + std::ranges::copy_if(in_values, std::back_inserter(sparse_values), + [](auto&& v) { return v.has_value(); }); + } + const ResValueVector& values = sparse ? sparse_values : in_values; + ResTable_type t{}; t.header.type = RES_TABLE_TYPE_TYPE; t.header.headerSize = sizeof(t); t.header.size = sizeof(t); t.id = 1; - t.flags = short_offsets ? ResTable_type::FLAG_OFFSET16 : 0; + t.flags = sparse + ? ResTable_type::FLAG_SPARSE + : short_offsets ? ResTable_type::FLAG_OFFSET16 : 0; - t.header.size += values.size() * (short_offsets ? sizeof(uint16_t) : sizeof(uint32_t)); + t.header.size += values.size() * + (sparse ? sizeof(ResTable_sparseTypeEntry) : + short_offsets ? sizeof(uint16_t) : sizeof(uint32_t)); t.entriesStart = t.header.size; t.entryCount = values.size(); @@ -53,9 +67,18 @@ static ResTable_type* createTypeTable(std::vector<Res_value*>& values, memcpy(p_header, &t, sizeof(t)); size_t i = 0, entry_offset = 0; - uint32_t k = 0; - for (auto const& v : values) { - if (short_offsets) { + uint32_t sparse_index = 0; + + for (auto const& v : in_values) { + if (sparse) { + if (!v) { + ++i; + continue; + } + const auto p = reinterpret_cast<ResTable_sparseTypeEntry*>(p_offsets) + sparse_index++; + p->idx = i; + p->offset = (entry_offset >> 2) & 0xffffu; + } else if (short_offsets) { uint16_t *p = reinterpret_cast<uint16_t *>(p_offsets) + i; *p = v ? (entry_offset >> 2) & 0xffffu : 0xffffu; } else { @@ -83,62 +106,92 @@ static ResTable_type* createTypeTable(std::vector<Res_value*>& values, } i++; } - return reinterpret_cast<ResTable_type*>(data); + return util::unique_cptr<ResTable_type>{reinterpret_cast<ResTable_type*>(data)}; } TEST(TypeVariantIteratorTest, shouldIterateOverTypeWithoutErrors) { - std::vector<Res_value *> values; - - Res_value *v1 = new Res_value{}; - values.push_back(v1); - - values.push_back(nullptr); - - Res_value *v2 = new Res_value{}; - values.push_back(v2); - - Res_value *v3 = new Res_value{ sizeof(Res_value), 0, Res_value::TYPE_STRING, 0x12345678}; - values.push_back(v3); + ResValueVector values; + + values.push_back(std::nullopt); + values.push_back(Res_value{}); + values.push_back(std::nullopt); + values.push_back(Res_value{}); + values.push_back(Res_value{ sizeof(Res_value), 0, Res_value::TYPE_STRING, 0x12345678}); + values.push_back(std::nullopt); + values.push_back(std::nullopt); + values.push_back(std::nullopt); + values.push_back(Res_value{ sizeof(Res_value), 0, Res_value::TYPE_STRING, 0x87654321}); // test for combinations of compact_entry and short_offsets - for (size_t i = 0; i < 4; i++) { - bool compact_entry = i & 0x1, short_offsets = i & 0x2; - ResTable_type* data = createTypeTable(values, compact_entry, short_offsets); - TypeVariant v(data); + for (size_t i = 0; i < 8; i++) { + bool compact_entry = i & 0x1, short_offsets = i & 0x2, sparse = i & 0x4; + auto data = createTypeTable(values, compact_entry, short_offsets, sparse); + TypeVariant v(data.get()); TypeVariant::iterator iter = v.beginEntries(); ASSERT_EQ(uint32_t(0), iter.index()); - ASSERT_TRUE(NULL != *iter); - ASSERT_EQ(uint32_t(0), iter->key()); + ASSERT_TRUE(NULL == *iter); ASSERT_NE(v.endEntries(), iter); - iter++; + ++iter; ASSERT_EQ(uint32_t(1), iter.index()); - ASSERT_TRUE(NULL == *iter); + ASSERT_TRUE(NULL != *iter); + ASSERT_EQ(uint32_t(1), iter->key()); ASSERT_NE(v.endEntries(), iter); iter++; ASSERT_EQ(uint32_t(2), iter.index()); + ASSERT_TRUE(NULL == *iter); + ASSERT_NE(v.endEntries(), iter); + + ++iter; + + ASSERT_EQ(uint32_t(3), iter.index()); ASSERT_TRUE(NULL != *iter); - ASSERT_EQ(uint32_t(2), iter->key()); + ASSERT_EQ(uint32_t(3), iter->key()); ASSERT_NE(v.endEntries(), iter); iter++; - ASSERT_EQ(uint32_t(3), iter.index()); + ASSERT_EQ(uint32_t(4), iter.index()); ASSERT_TRUE(NULL != *iter); ASSERT_EQ(iter->is_compact(), compact_entry); - ASSERT_EQ(uint32_t(3), iter->key()); + ASSERT_EQ(uint32_t(4), iter->key()); ASSERT_EQ(uint32_t(0x12345678), iter->value().data); ASSERT_EQ(Res_value::TYPE_STRING, iter->value().dataType); + ++iter; + + ASSERT_EQ(uint32_t(5), iter.index()); + ASSERT_TRUE(NULL == *iter); + ASSERT_NE(v.endEntries(), iter); + + ++iter; + + ASSERT_EQ(uint32_t(6), iter.index()); + ASSERT_TRUE(NULL == *iter); + ASSERT_NE(v.endEntries(), iter); + + ++iter; + + ASSERT_EQ(uint32_t(7), iter.index()); + ASSERT_TRUE(NULL == *iter); + ASSERT_NE(v.endEntries(), iter); + iter++; - ASSERT_EQ(v.endEntries(), iter); + ASSERT_EQ(uint32_t(8), iter.index()); + ASSERT_TRUE(NULL != *iter); + ASSERT_EQ(iter->is_compact(), compact_entry); + ASSERT_EQ(uint32_t(8), iter->key()); + ASSERT_EQ(uint32_t(0x87654321), iter->value().data); + ASSERT_EQ(Res_value::TYPE_STRING, iter->value().dataType); - free(data); + ++iter; + + ASSERT_EQ(v.endEntries(), iter); } } diff --git a/libs/protoutil/Android.bp b/libs/protoutil/Android.bp index 8af4b7e8f4c8..4fecf4de0312 100644 --- a/libs/protoutil/Android.bp +++ b/libs/protoutil/Android.bp @@ -59,7 +59,6 @@ cc_library { apex_available: [ "//apex_available:platform", "com.android.os.statsd", - "test_com.android.os.statsd", "com.android.uprobestats", ], } diff --git a/location/api/system-current.txt b/location/api/system-current.txt index 0c2f3adc2838..eb19ba84ee62 100644 --- a/location/api/system-current.txt +++ b/location/api/system-current.txt @@ -1,6 +1,29 @@ // Signature format: 2.0 package android.location { + @FlaggedApi("android.location.flags.gnss_assistance_interface") public final class AuxiliaryInformation implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public java.util.List<android.location.GnssSignalType> getAvailableSignalTypes(); + method @IntRange(from=0xfffffff9, to=6) public int getFrequencyChannelNumber(); + method public int getSatType(); + method @IntRange(from=1) public int getSvid(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field public static final int BDS_B1C_ORBIT_TYPE_GEO = 1; // 0x1 + field public static final int BDS_B1C_ORBIT_TYPE_IGSO = 2; // 0x2 + field public static final int BDS_B1C_ORBIT_TYPE_MEO = 3; // 0x3 + field public static final int BDS_B1C_ORBIT_TYPE_UNDEFINED = 0; // 0x0 + field @NonNull public static final android.os.Parcelable.Creator<android.location.AuxiliaryInformation> CREATOR; + } + + public static final class AuxiliaryInformation.Builder { + ctor public AuxiliaryInformation.Builder(); + method @NonNull public android.location.AuxiliaryInformation build(); + method @NonNull public android.location.AuxiliaryInformation.Builder setAvailableSignalTypes(@NonNull java.util.List<android.location.GnssSignalType>); + method @NonNull public android.location.AuxiliaryInformation.Builder setFrequencyChannelNumber(@IntRange(from=0xfffffff9, to=6) int); + method @NonNull public android.location.AuxiliaryInformation.Builder setSatType(int); + method @NonNull public android.location.AuxiliaryInformation.Builder setSvid(@IntRange(from=1) int); + } + public abstract class BatchedLocationCallback { ctor public BatchedLocationCallback(); method public void onLocationBatch(java.util.List<android.location.Location>); @@ -9,6 +32,7 @@ package android.location { @FlaggedApi("android.location.flags.gnss_assistance_interface") public final class BeidouAssistance implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.location.GnssAlmanac getAlmanac(); + method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation(); method @Nullable public android.location.KlobucharIonosphericModel getIonosphericModel(); method @Nullable public android.location.LeapSecondsModel getLeapSecondsModel(); method @NonNull public java.util.List<android.location.RealTimeIntegrityModel> getRealTimeIntegrityModels(); @@ -24,12 +48,13 @@ package android.location { ctor public BeidouAssistance.Builder(); method @NonNull public android.location.BeidouAssistance build(); method @NonNull public android.location.BeidouAssistance.Builder setAlmanac(@Nullable android.location.GnssAlmanac); + method @NonNull public android.location.BeidouAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation); method @NonNull public android.location.BeidouAssistance.Builder setIonosphericModel(@Nullable android.location.KlobucharIonosphericModel); method @NonNull public android.location.BeidouAssistance.Builder setLeapSecondsModel(@Nullable android.location.LeapSecondsModel); - method @NonNull public android.location.BeidouAssistance.Builder setRealTimeIntegrityModels(@Nullable java.util.List<android.location.RealTimeIntegrityModel>); - method @NonNull public android.location.BeidouAssistance.Builder setSatelliteCorrections(@Nullable java.util.List<android.location.GnssAssistance.GnssSatelliteCorrections>); - method @NonNull public android.location.BeidouAssistance.Builder setSatelliteEphemeris(@Nullable java.util.List<android.location.BeidouSatelliteEphemeris>); - method @NonNull public android.location.BeidouAssistance.Builder setTimeModels(@Nullable java.util.List<android.location.TimeModel>); + method @NonNull public android.location.BeidouAssistance.Builder setRealTimeIntegrityModels(@NonNull java.util.List<android.location.RealTimeIntegrityModel>); + method @NonNull public android.location.BeidouAssistance.Builder setSatelliteCorrections(@NonNull java.util.List<android.location.GnssAssistance.GnssSatelliteCorrections>); + method @NonNull public android.location.BeidouAssistance.Builder setSatelliteEphemeris(@NonNull java.util.List<android.location.BeidouSatelliteEphemeris>); + method @NonNull public android.location.BeidouAssistance.Builder setTimeModels(@NonNull java.util.List<android.location.TimeModel>); method @NonNull public android.location.BeidouAssistance.Builder setUtcModel(@Nullable android.location.UtcModel); } @@ -151,6 +176,7 @@ package android.location { @FlaggedApi("android.location.flags.gnss_assistance_interface") public final class GalileoAssistance implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.location.GnssAlmanac getAlmanac(); + method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation(); method @Nullable public android.location.KlobucharIonosphericModel getIonosphericModel(); method @Nullable public android.location.LeapSecondsModel getLeapSecondsModel(); method @NonNull public java.util.List<android.location.RealTimeIntegrityModel> getRealTimeIntegrityModels(); @@ -166,12 +192,13 @@ package android.location { ctor public GalileoAssistance.Builder(); method @NonNull public android.location.GalileoAssistance build(); method @NonNull public android.location.GalileoAssistance.Builder setAlmanac(@Nullable android.location.GnssAlmanac); + method @NonNull public android.location.GalileoAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation); method @NonNull public android.location.GalileoAssistance.Builder setIonosphericModel(@Nullable android.location.KlobucharIonosphericModel); method @NonNull public android.location.GalileoAssistance.Builder setLeapSecondsModel(@Nullable android.location.LeapSecondsModel); - method @NonNull public android.location.GalileoAssistance.Builder setRealTimeIntegrityModels(@Nullable java.util.List<android.location.RealTimeIntegrityModel>); - method @NonNull public android.location.GalileoAssistance.Builder setSatelliteCorrections(@Nullable java.util.List<android.location.GnssAssistance.GnssSatelliteCorrections>); - method @NonNull public android.location.GalileoAssistance.Builder setSatelliteEphemeris(@Nullable java.util.List<android.location.GalileoSatelliteEphemeris>); - method @NonNull public android.location.GalileoAssistance.Builder setTimeModels(@Nullable java.util.List<android.location.TimeModel>); + method @NonNull public android.location.GalileoAssistance.Builder setRealTimeIntegrityModels(@NonNull java.util.List<android.location.RealTimeIntegrityModel>); + method @NonNull public android.location.GalileoAssistance.Builder setSatelliteCorrections(@NonNull java.util.List<android.location.GnssAssistance.GnssSatelliteCorrections>); + method @NonNull public android.location.GalileoAssistance.Builder setSatelliteEphemeris(@NonNull java.util.List<android.location.GalileoSatelliteEphemeris>); + method @NonNull public android.location.GalileoAssistance.Builder setTimeModels(@NonNull java.util.List<android.location.TimeModel>); method @NonNull public android.location.GalileoAssistance.Builder setUtcModel(@Nullable android.location.UtcModel); } @@ -319,6 +346,7 @@ package android.location { @FlaggedApi("android.location.flags.gnss_assistance_interface") public final class GlonassAssistance implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.location.GlonassAlmanac getAlmanac(); + method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation(); method @NonNull public java.util.List<android.location.GnssAssistance.GnssSatelliteCorrections> getSatelliteCorrections(); method @NonNull public java.util.List<android.location.GlonassSatelliteEphemeris> getSatelliteEphemeris(); method @NonNull public java.util.List<android.location.TimeModel> getTimeModels(); @@ -331,9 +359,10 @@ package android.location { ctor public GlonassAssistance.Builder(); method @NonNull public android.location.GlonassAssistance build(); method @NonNull public android.location.GlonassAssistance.Builder setAlmanac(@Nullable android.location.GlonassAlmanac); - method @NonNull public android.location.GlonassAssistance.Builder setSatelliteCorrections(@Nullable java.util.List<android.location.GnssAssistance.GnssSatelliteCorrections>); - method @NonNull public android.location.GlonassAssistance.Builder setSatelliteEphemeris(@Nullable java.util.List<android.location.GlonassSatelliteEphemeris>); - method @NonNull public android.location.GlonassAssistance.Builder setTimeModels(@Nullable java.util.List<android.location.TimeModel>); + method @NonNull public android.location.GlonassAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation); + method @NonNull public android.location.GlonassAssistance.Builder setSatelliteCorrections(@NonNull java.util.List<android.location.GnssAssistance.GnssSatelliteCorrections>); + method @NonNull public android.location.GlonassAssistance.Builder setSatelliteEphemeris(@NonNull java.util.List<android.location.GlonassSatelliteEphemeris>); + method @NonNull public android.location.GlonassAssistance.Builder setTimeModels(@NonNull java.util.List<android.location.TimeModel>); method @NonNull public android.location.GlonassAssistance.Builder setUtcModel(@Nullable android.location.UtcModel); } @@ -688,6 +717,7 @@ package android.location { @FlaggedApi("android.location.flags.gnss_assistance_interface") public final class GpsAssistance implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.location.GnssAlmanac getAlmanac(); + method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation(); method @Nullable public android.location.KlobucharIonosphericModel getIonosphericModel(); method @Nullable public android.location.LeapSecondsModel getLeapSecondsModel(); method @NonNull public java.util.List<android.location.RealTimeIntegrityModel> getRealTimeIntegrityModels(); @@ -703,12 +733,13 @@ package android.location { ctor public GpsAssistance.Builder(); method @NonNull public android.location.GpsAssistance build(); method @NonNull public android.location.GpsAssistance.Builder setAlmanac(@Nullable android.location.GnssAlmanac); + method @NonNull public android.location.GpsAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation); method @NonNull public android.location.GpsAssistance.Builder setIonosphericModel(@Nullable android.location.KlobucharIonosphericModel); method @NonNull public android.location.GpsAssistance.Builder setLeapSecondsModel(@Nullable android.location.LeapSecondsModel); - method @NonNull public android.location.GpsAssistance.Builder setRealTimeIntegrityModels(@Nullable java.util.List<android.location.RealTimeIntegrityModel>); - method @NonNull public android.location.GpsAssistance.Builder setSatelliteCorrections(@Nullable java.util.List<android.location.GnssAssistance.GnssSatelliteCorrections>); - method @NonNull public android.location.GpsAssistance.Builder setSatelliteEphemeris(@Nullable java.util.List<android.location.GpsSatelliteEphemeris>); - method @NonNull public android.location.GpsAssistance.Builder setTimeModels(@Nullable java.util.List<android.location.TimeModel>); + method @NonNull public android.location.GpsAssistance.Builder setRealTimeIntegrityModels(@NonNull java.util.List<android.location.RealTimeIntegrityModel>); + method @NonNull public android.location.GpsAssistance.Builder setSatelliteCorrections(@NonNull java.util.List<android.location.GnssAssistance.GnssSatelliteCorrections>); + method @NonNull public android.location.GpsAssistance.Builder setSatelliteEphemeris(@NonNull java.util.List<android.location.GpsSatelliteEphemeris>); + method @NonNull public android.location.GpsAssistance.Builder setTimeModels(@NonNull java.util.List<android.location.TimeModel>); method @NonNull public android.location.GpsAssistance.Builder setUtcModel(@Nullable android.location.UtcModel); } @@ -1222,6 +1253,7 @@ package android.location { @FlaggedApi("android.location.flags.gnss_assistance_interface") public final class QzssAssistance implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.location.GnssAlmanac getAlmanac(); + method @Nullable public android.location.AuxiliaryInformation getAuxiliaryInformation(); method @Nullable public android.location.KlobucharIonosphericModel getIonosphericModel(); method @Nullable public android.location.LeapSecondsModel getLeapSecondsModel(); method @NonNull public java.util.List<android.location.RealTimeIntegrityModel> getRealTimeIntegrityModels(); @@ -1237,12 +1269,13 @@ package android.location { ctor public QzssAssistance.Builder(); method @NonNull public android.location.QzssAssistance build(); method @NonNull public android.location.QzssAssistance.Builder setAlmanac(@Nullable android.location.GnssAlmanac); + method @NonNull public android.location.QzssAssistance.Builder setAuxiliaryInformation(@Nullable android.location.AuxiliaryInformation); method @NonNull public android.location.QzssAssistance.Builder setIonosphericModel(@Nullable android.location.KlobucharIonosphericModel); method @NonNull public android.location.QzssAssistance.Builder setLeapSecondsModel(@Nullable android.location.LeapSecondsModel); - method @NonNull public android.location.QzssAssistance.Builder setRealTimeIntegrityModels(@Nullable java.util.List<android.location.RealTimeIntegrityModel>); - method @NonNull public android.location.QzssAssistance.Builder setSatelliteCorrections(@Nullable java.util.List<android.location.GnssAssistance.GnssSatelliteCorrections>); - method @NonNull public android.location.QzssAssistance.Builder setSatelliteEphemeris(@Nullable java.util.List<android.location.QzssSatelliteEphemeris>); - method @NonNull public android.location.QzssAssistance.Builder setTimeModels(@Nullable java.util.List<android.location.TimeModel>); + method @NonNull public android.location.QzssAssistance.Builder setRealTimeIntegrityModels(@NonNull java.util.List<android.location.RealTimeIntegrityModel>); + method @NonNull public android.location.QzssAssistance.Builder setSatelliteCorrections(@NonNull java.util.List<android.location.GnssAssistance.GnssSatelliteCorrections>); + method @NonNull public android.location.QzssAssistance.Builder setSatelliteEphemeris(@NonNull java.util.List<android.location.QzssSatelliteEphemeris>); + method @NonNull public android.location.QzssAssistance.Builder setTimeModels(@NonNull java.util.List<android.location.TimeModel>); method @NonNull public android.location.QzssAssistance.Builder setUtcModel(@Nullable android.location.UtcModel); } @@ -1273,11 +1306,11 @@ package android.location { method public int describeContents(); method @NonNull public String getAdvisoryNumber(); method @NonNull public String getAdvisoryType(); + method @NonNull public java.util.List<android.location.GnssSignalType> getBadSignalTypes(); + method @IntRange(from=1, to=206) public int getBadSvid(); method @IntRange(from=0) public long getEndDateSeconds(); method @IntRange(from=0) public long getPublishDateSeconds(); method @IntRange(from=0) public long getStartDateSeconds(); - method @IntRange(from=1, to=206) public int getSvid(); - method public boolean isUsable(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.location.RealTimeIntegrityModel> CREATOR; } @@ -1287,11 +1320,11 @@ package android.location { method @NonNull public android.location.RealTimeIntegrityModel build(); method @NonNull public android.location.RealTimeIntegrityModel.Builder setAdvisoryNumber(@NonNull String); method @NonNull public android.location.RealTimeIntegrityModel.Builder setAdvisoryType(@NonNull String); + method @NonNull public android.location.RealTimeIntegrityModel.Builder setBadSignalTypes(@NonNull java.util.List<android.location.GnssSignalType>); + method @NonNull public android.location.RealTimeIntegrityModel.Builder setBadSvid(@IntRange(from=1, to=206) int); method @NonNull public android.location.RealTimeIntegrityModel.Builder setEndDateSeconds(@IntRange(from=0) long); method @NonNull public android.location.RealTimeIntegrityModel.Builder setPublishDateSeconds(@IntRange(from=0) long); method @NonNull public android.location.RealTimeIntegrityModel.Builder setStartDateSeconds(@IntRange(from=0) long); - method @NonNull public android.location.RealTimeIntegrityModel.Builder setSvid(@IntRange(from=1, to=206) int); - method @NonNull public android.location.RealTimeIntegrityModel.Builder setUsable(boolean); } @FlaggedApi("android.location.flags.gnss_assistance_interface") public final class SatelliteEphemerisTime implements android.os.Parcelable { @@ -1459,6 +1492,13 @@ package android.location.provider { field public static final String ACTION_GEOCODE_PROVIDER = "com.android.location.service.GeocodeProvider"; } + @FlaggedApi("android.location.flags.gnss_assistance_interface") public abstract class GnssAssistanceProviderBase { + ctor public GnssAssistanceProviderBase(@NonNull android.content.Context, @NonNull String); + method @NonNull public final android.os.IBinder getBinder(); + method public abstract void onRequest(@NonNull android.os.OutcomeReceiver<android.location.GnssAssistance,java.lang.Throwable>); + field public static final String ACTION_GNSS_ASSISTANCE_PROVIDER = "android.location.provider.action.GNSS_ASSISTANCE_PROVIDER"; + } + public abstract class LocationProviderBase { ctor public LocationProviderBase(@NonNull android.content.Context, @NonNull String, @NonNull android.location.provider.ProviderProperties); method @Nullable public final android.os.IBinder getBinder(); diff --git a/location/java/android/location/AuxiliaryInformation.java b/location/java/android/location/AuxiliaryInformation.java new file mode 100644 index 000000000000..601c87e69b70 --- /dev/null +++ b/location/java/android/location/AuxiliaryInformation.java @@ -0,0 +1,274 @@ +/* + * 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.location; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.location.flags.Flags; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A class contains parameters to provide additional assistance information dependent on the GNSS + * constellation. + * + * @hide + */ +@FlaggedApi(Flags.FLAG_GNSS_ASSISTANCE_INTERFACE) +@SystemApi +public final class AuxiliaryInformation implements Parcelable { + + /** + * BDS B1C Satellite orbit type. + * + * <p>This is defined in BDS-SIS-ICD-B1I-3.0, section 3.1. + * + * @hide + */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + BDS_B1C_ORBIT_TYPE_UNDEFINED, + BDS_B1C_ORBIT_TYPE_GEO, + BDS_B1C_ORBIT_TYPE_IGSO, + BDS_B1C_ORBIT_TYPE_MEO + }) + public @interface BeidouB1CSatelliteOrbitType {} + + /** + * The following enumerations must be in sync with the values declared in + * AuxiliaryInformation.aidl. + */ + + /** The orbit type is undefined. */ + public static final int BDS_B1C_ORBIT_TYPE_UNDEFINED = 0; + + /** The orbit type is GEO. */ + public static final int BDS_B1C_ORBIT_TYPE_GEO = 1; + + /** The orbit type is IGSO. */ + public static final int BDS_B1C_ORBIT_TYPE_IGSO = 2; + + /** The orbit type is MEO. */ + public static final int BDS_B1C_ORBIT_TYPE_MEO = 3; + + /** + * Pseudo-random or satellite ID number for the satellite, a.k.a. Space Vehicle (SV), or OSN + * number for Glonass. + * + * <p>The distinction is made by looking at the constellation field. Values must be in the range + * of: + * + * <p>- GPS: 1-32 + * + * <p>- GLONASS: 1-25 + * + * <p>- QZSS: 183-206 + * + * <p>- Galileo: 1-36 + * + * <p>- Beidou: 1-63 + */ + private final int mSvid; + + /** The list of available signal types for the satellite. */ + @NonNull private final List<GnssSignalType> mAvailableSignalTypes; + + /** + * Glonass carrier frequency number of the satellite. This is required for Glonass. + * + * <p>This is defined in Glonass ICD v5.1 section 3.3.1.1. + */ + private final int mFrequencyChannelNumber; + + /** BDS B1C satellite orbit type. This is required for Beidou. */ + private final @BeidouB1CSatelliteOrbitType int mSatType; + + private AuxiliaryInformation(Builder builder) { + // Allow Svid beyond the range to support potential future extensibility. + Preconditions.checkArgument(builder.mSvid >= 1); + Preconditions.checkNotNull( + builder.mAvailableSignalTypes, "AvailableSignalTypes cannot be null"); + Preconditions.checkArgument(builder.mAvailableSignalTypes.size() > 0); + Preconditions.checkArgumentInRange( + builder.mFrequencyChannelNumber, -7, 6, "FrequencyChannelNumber"); + Preconditions.checkArgumentInRange( + builder.mSatType, BDS_B1C_ORBIT_TYPE_UNDEFINED, BDS_B1C_ORBIT_TYPE_MEO, "SatType"); + mSvid = builder.mSvid; + mAvailableSignalTypes = + Collections.unmodifiableList(new ArrayList<>(builder.mAvailableSignalTypes)); + mFrequencyChannelNumber = builder.mFrequencyChannelNumber; + mSatType = builder.mSatType; + } + + /** + * Returns the Pseudo-random or satellite ID number for the satellite, a.k.a. Space Vehicle + * (SV), or OSN number for Glonass. + * + * <p>The distinction is made by looking at the constellation field. Values must be in the range + * of: + * + * <p>- GPS: 1-32 + * + * <p>- GLONASS: 1-25 + * + * <p>- QZSS: 183-206 + * + * <p>- Galileo: 1-36 + * + * <p>- Beidou: 1-63 + */ + @IntRange(from = 1) + public int getSvid() { + return mSvid; + } + + /** Returns the list of available signal types for the satellite. */ + @NonNull + public List<GnssSignalType> getAvailableSignalTypes() { + return mAvailableSignalTypes; + } + + /** Returns the Glonass carrier frequency number of the satellite. */ + @IntRange(from = -7, to = 6) + public int getFrequencyChannelNumber() { + return mFrequencyChannelNumber; + } + + /** Returns the BDS B1C satellite orbit type. */ + @BeidouB1CSatelliteOrbitType + public int getSatType() { + return mSatType; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mSvid); + dest.writeTypedList(mAvailableSignalTypes); + dest.writeInt(mFrequencyChannelNumber); + dest.writeInt(mSatType); + } + + @Override + @NonNull + public String toString() { + StringBuilder builder = new StringBuilder("AuxiliaryInformation["); + builder.append("svid = ").append(mSvid); + builder.append(", availableSignalTypes = ").append(mAvailableSignalTypes); + builder.append(", frequencyChannelNumber = ").append(mFrequencyChannelNumber); + builder.append(", satType = ").append(mSatType); + builder.append("]"); + return builder.toString(); + } + + public static final @NonNull Parcelable.Creator<AuxiliaryInformation> CREATOR = + new Parcelable.Creator<AuxiliaryInformation>() { + @Override + public AuxiliaryInformation createFromParcel(@NonNull Parcel in) { + return new AuxiliaryInformation.Builder() + .setSvid(in.readInt()) + .setAvailableSignalTypes( + in.createTypedArrayList(GnssSignalType.CREATOR)) + .setFrequencyChannelNumber(in.readInt()) + .setSatType(in.readInt()) + .build(); + } + + @Override + public AuxiliaryInformation[] newArray(int size) { + return new AuxiliaryInformation[size]; + } + }; + + /** A builder class for {@link AuxiliaryInformation}. */ + public static final class Builder { + private int mSvid; + private List<GnssSignalType> mAvailableSignalTypes; + private int mFrequencyChannelNumber; + private @BeidouB1CSatelliteOrbitType int mSatType; + + /** + * Sets the Pseudo-random or satellite ID number for the satellite, a.k.a. Space Vehicle + * (SV), or OSN number for Glonass. + * + * <p>The distinction is made by looking at the constellation field. Values must be in the + * range of: + * + * <p>- GPS: 1-32 + * + * <p>- GLONASS: 1-25 + * + * <p>- QZSS: 183-206 + * + * <p>- Galileo: 1-36 + * + * <p>- Beidou: 1-63 + */ + @NonNull + public Builder setSvid(@IntRange(from = 1) int svid) { + mSvid = svid; + return this; + } + + /** + * Sets the list of available signal types for the satellite. + * + * <p>The list must be set and cannot be an empty list. + */ + @NonNull + public Builder setAvailableSignalTypes(@NonNull List<GnssSignalType> availableSignalTypes) { + mAvailableSignalTypes = availableSignalTypes; + return this; + } + + /** Sets the Glonass carrier frequency number of the satellite. */ + @NonNull + public Builder setFrequencyChannelNumber( + @IntRange(from = -7, to = 6) int frequencyChannelNumber) { + mFrequencyChannelNumber = frequencyChannelNumber; + return this; + } + + /** Sets the BDS B1C satellite orbit type. */ + @NonNull + public Builder setSatType(@BeidouB1CSatelliteOrbitType int satType) { + mSatType = satType; + return this; + } + + /** Builds a {@link AuxiliaryInformation} instance as specified by this builder. */ + @NonNull + public AuxiliaryInformation build() { + return new AuxiliaryInformation(this); + } + } +} diff --git a/location/java/android/location/BeidouAssistance.java b/location/java/android/location/BeidouAssistance.java index f55249e605a0..e35493ed1007 100644 --- a/location/java/android/location/BeidouAssistance.java +++ b/location/java/android/location/BeidouAssistance.java @@ -19,7 +19,6 @@ package android.location; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.location.GnssAssistance.GnssSatelliteCorrections; import android.location.flags.Flags; @@ -51,6 +50,9 @@ public final class BeidouAssistance implements Parcelable { /** The leap seconds model. */ @Nullable private final LeapSecondsModel mLeapSecondsModel; + /** The auxiliary information. */ + @Nullable private final AuxiliaryInformation mAuxiliaryInformation; + /** The list of time models. */ @NonNull private final List<TimeModel> mTimeModels; @@ -68,6 +70,7 @@ public final class BeidouAssistance implements Parcelable { mIonosphericModel = builder.mIonosphericModel; mUtcModel = builder.mUtcModel; mLeapSecondsModel = builder.mLeapSecondsModel; + mAuxiliaryInformation = builder.mAuxiliaryInformation; if (builder.mTimeModels != null) { mTimeModels = Collections.unmodifiableList(new ArrayList<>(builder.mTimeModels)); } else { @@ -117,6 +120,12 @@ public final class BeidouAssistance implements Parcelable { return mLeapSecondsModel; } + /** Returns the auxiliary information. */ + @Nullable + public AuxiliaryInformation getAuxiliaryInformation() { + return mAuxiliaryInformation; + } + /** Returns the list of time models. */ @NonNull public List<TimeModel> getTimeModels() { @@ -154,6 +163,7 @@ public final class BeidouAssistance implements Parcelable { builder.append(", ionosphericModel = ").append(mIonosphericModel); builder.append(", utcModel = ").append(mUtcModel); builder.append(", leapSecondsModel = ").append(mLeapSecondsModel); + builder.append(", auxiliaryInformation = ").append(mAuxiliaryInformation); builder.append(", timeModels = ").append(mTimeModels); builder.append(", satelliteEphemeris = ").append(mSatelliteEphemeris); builder.append(", realTimeIntegrityModels = ").append(mRealTimeIntegrityModels); @@ -168,6 +178,7 @@ public final class BeidouAssistance implements Parcelable { dest.writeTypedObject(mIonosphericModel, flags); dest.writeTypedObject(mUtcModel, flags); dest.writeTypedObject(mLeapSecondsModel, flags); + dest.writeTypedObject(mAuxiliaryInformation, flags); dest.writeTypedList(mTimeModels); dest.writeTypedList(mSatelliteEphemeris); dest.writeTypedList(mRealTimeIntegrityModels); @@ -184,6 +195,8 @@ public final class BeidouAssistance implements Parcelable { in.readTypedObject(KlobucharIonosphericModel.CREATOR)) .setUtcModel(in.readTypedObject(UtcModel.CREATOR)) .setLeapSecondsModel(in.readTypedObject(LeapSecondsModel.CREATOR)) + .setAuxiliaryInformation( + in.readTypedObject(AuxiliaryInformation.CREATOR)) .setTimeModels(in.createTypedArrayList(TimeModel.CREATOR)) .setSatelliteEphemeris( in.createTypedArrayList(BeidouSatelliteEphemeris.CREATOR)) @@ -206,6 +219,7 @@ public final class BeidouAssistance implements Parcelable { private KlobucharIonosphericModel mIonosphericModel; private UtcModel mUtcModel; private LeapSecondsModel mLeapSecondsModel; + private AuxiliaryInformation mAuxiliaryInformation; private List<TimeModel> mTimeModels; private List<BeidouSatelliteEphemeris> mSatelliteEphemeris; private List<RealTimeIntegrityModel> mRealTimeIntegrityModels; @@ -239,10 +253,17 @@ public final class BeidouAssistance implements Parcelable { return this; } + /** Sets the auxiliary information. */ + @NonNull + public Builder setAuxiliaryInformation( + @Nullable AuxiliaryInformation auxiliaryInformation) { + mAuxiliaryInformation = auxiliaryInformation; + return this; + } + /** Sets the list of time models. */ @NonNull - public Builder setTimeModels( - @Nullable @SuppressLint("NullableCollection") List<TimeModel> timeModels) { + public Builder setTimeModels(@NonNull List<TimeModel> timeModels) { mTimeModels = timeModels; return this; } @@ -250,8 +271,7 @@ public final class BeidouAssistance implements Parcelable { /** Sets the list of Beidou ephemeris. */ @NonNull public Builder setSatelliteEphemeris( - @Nullable @SuppressLint("NullableCollection") - List<BeidouSatelliteEphemeris> satelliteEphemeris) { + @NonNull List<BeidouSatelliteEphemeris> satelliteEphemeris) { mSatelliteEphemeris = satelliteEphemeris; return this; } @@ -259,8 +279,7 @@ public final class BeidouAssistance implements Parcelable { /** Sets the list of real time integrity models. */ @NonNull public Builder setRealTimeIntegrityModels( - @Nullable @SuppressLint("NullableCollection") - List<RealTimeIntegrityModel> realTimeIntegrityModels) { + @NonNull List<RealTimeIntegrityModel> realTimeIntegrityModels) { mRealTimeIntegrityModels = realTimeIntegrityModels; return this; } @@ -268,8 +287,7 @@ public final class BeidouAssistance implements Parcelable { /** Sets the list of Beidou satellite corrections. */ @NonNull public Builder setSatelliteCorrections( - @Nullable @SuppressLint("NullableCollection") - List<GnssSatelliteCorrections> satelliteCorrections) { + @NonNull List<GnssSatelliteCorrections> satelliteCorrections) { mSatelliteCorrections = satelliteCorrections; return this; } diff --git a/location/java/android/location/GalileoAssistance.java b/location/java/android/location/GalileoAssistance.java index 07c5bab856db..8a09e6634d09 100644 --- a/location/java/android/location/GalileoAssistance.java +++ b/location/java/android/location/GalileoAssistance.java @@ -19,7 +19,6 @@ package android.location; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.location.GnssAssistance.GnssSatelliteCorrections; import android.location.flags.Flags; @@ -51,6 +50,9 @@ public final class GalileoAssistance implements Parcelable { /** The leap seconds model. */ @Nullable private final LeapSecondsModel mLeapSecondsModel; + /** The auxiliary information. */ + @Nullable private final AuxiliaryInformation mAuxiliaryInformation; + /** The list of time models. */ @NonNull private final List<TimeModel> mTimeModels; @@ -68,6 +70,7 @@ public final class GalileoAssistance implements Parcelable { mIonosphericModel = builder.mIonosphericModel; mUtcModel = builder.mUtcModel; mLeapSecondsModel = builder.mLeapSecondsModel; + mAuxiliaryInformation = builder.mAuxiliaryInformation; if (builder.mTimeModels != null) { mTimeModels = Collections.unmodifiableList(new ArrayList<>(builder.mTimeModels)); } else { @@ -117,6 +120,12 @@ public final class GalileoAssistance implements Parcelable { return mLeapSecondsModel; } + /** Returns the auxiliary information. */ + @Nullable + public AuxiliaryInformation getAuxiliaryInformation() { + return mAuxiliaryInformation; + } + /** Returns the list of time models. */ @NonNull public List<TimeModel> getTimeModels() { @@ -152,6 +161,7 @@ public final class GalileoAssistance implements Parcelable { dest.writeTypedObject(mIonosphericModel, flags); dest.writeTypedObject(mUtcModel, flags); dest.writeTypedObject(mLeapSecondsModel, flags); + dest.writeTypedObject(mAuxiliaryInformation, flags); dest.writeTypedList(mTimeModels); dest.writeTypedList(mSatelliteEphemeris); dest.writeTypedList(mRealTimeIntegrityModels); @@ -166,6 +176,7 @@ public final class GalileoAssistance implements Parcelable { builder.append(", ionosphericModel = ").append(mIonosphericModel); builder.append(", utcModel = ").append(mUtcModel); builder.append(", leapSecondsModel = ").append(mLeapSecondsModel); + builder.append(", auxiliaryInformation = ").append(mAuxiliaryInformation); builder.append(", timeModels = ").append(mTimeModels); builder.append(", satelliteEphemeris = ").append(mSatelliteEphemeris); builder.append(", realTimeIntegrityModels = ").append(mRealTimeIntegrityModels); @@ -184,6 +195,8 @@ public final class GalileoAssistance implements Parcelable { in.readTypedObject(KlobucharIonosphericModel.CREATOR)) .setUtcModel(in.readTypedObject(UtcModel.CREATOR)) .setLeapSecondsModel(in.readTypedObject(LeapSecondsModel.CREATOR)) + .setAuxiliaryInformation( + in.readTypedObject(AuxiliaryInformation.CREATOR)) .setTimeModels(in.createTypedArrayList(TimeModel.CREATOR)) .setSatelliteEphemeris( in.createTypedArrayList(GalileoSatelliteEphemeris.CREATOR)) @@ -206,6 +219,7 @@ public final class GalileoAssistance implements Parcelable { private KlobucharIonosphericModel mIonosphericModel; private UtcModel mUtcModel; private LeapSecondsModel mLeapSecondsModel; + private AuxiliaryInformation mAuxiliaryInformation; private List<TimeModel> mTimeModels; private List<GalileoSatelliteEphemeris> mSatelliteEphemeris; private List<RealTimeIntegrityModel> mRealTimeIntegrityModels; @@ -239,10 +253,17 @@ public final class GalileoAssistance implements Parcelable { return this; } + /** Sets the auxiliary information. */ + @NonNull + public Builder setAuxiliaryInformation( + @Nullable AuxiliaryInformation auxiliaryInformation) { + mAuxiliaryInformation = auxiliaryInformation; + return this; + } + /** Sets the list of time models. */ @NonNull - public Builder setTimeModels( - @Nullable @SuppressLint("NullableCollection") List<TimeModel> timeModels) { + public Builder setTimeModels(@NonNull List<TimeModel> timeModels) { mTimeModels = timeModels; return this; } @@ -250,8 +271,7 @@ public final class GalileoAssistance implements Parcelable { /** Sets the list of Galileo ephemeris. */ @NonNull public Builder setSatelliteEphemeris( - @Nullable @SuppressLint("NullableCollection") - List<GalileoSatelliteEphemeris> satelliteEphemeris) { + @NonNull List<GalileoSatelliteEphemeris> satelliteEphemeris) { mSatelliteEphemeris = satelliteEphemeris; return this; } @@ -259,8 +279,7 @@ public final class GalileoAssistance implements Parcelable { /** Sets the list of real time integrity models. */ @NonNull public Builder setRealTimeIntegrityModels( - @Nullable @SuppressLint("NullableCollection") - List<RealTimeIntegrityModel> realTimeIntegrityModels) { + @NonNull List<RealTimeIntegrityModel> realTimeIntegrityModels) { mRealTimeIntegrityModels = realTimeIntegrityModels; return this; } @@ -268,8 +287,7 @@ public final class GalileoAssistance implements Parcelable { /** Sets the list of Galileo satellite corrections. */ @NonNull public Builder setSatelliteCorrections( - @Nullable @SuppressLint("NullableCollection") - List<GnssSatelliteCorrections> satelliteCorrections) { + @NonNull List<GnssSatelliteCorrections> satelliteCorrections) { mSatelliteCorrections = satelliteCorrections; return this; } diff --git a/location/java/android/location/GlonassAssistance.java b/location/java/android/location/GlonassAssistance.java index cc0820197d8d..c7ed1c52b403 100644 --- a/location/java/android/location/GlonassAssistance.java +++ b/location/java/android/location/GlonassAssistance.java @@ -19,7 +19,6 @@ package android.location; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.location.GnssAssistance.GnssSatelliteCorrections; import android.location.flags.Flags; @@ -45,6 +44,9 @@ public final class GlonassAssistance implements Parcelable { /** The UTC model. */ @Nullable private final UtcModel mUtcModel; + /** The auxiliary information. */ + @Nullable private final AuxiliaryInformation mAuxiliaryInformation; + /** The list of time models. */ @NonNull private final List<TimeModel> mTimeModels; @@ -57,6 +59,7 @@ public final class GlonassAssistance implements Parcelable { private GlonassAssistance(Builder builder) { mAlmanac = builder.mAlmanac; mUtcModel = builder.mUtcModel; + mAuxiliaryInformation = builder.mAuxiliaryInformation; if (builder.mTimeModels != null) { mTimeModels = Collections.unmodifiableList(new ArrayList<>(builder.mTimeModels)); } else { @@ -106,6 +109,12 @@ public final class GlonassAssistance implements Parcelable { return mSatelliteCorrections; } + /** Returns the auxiliary information. */ + @Nullable + public AuxiliaryInformation getAuxiliaryInformation() { + return mAuxiliaryInformation; + } + @Override public int describeContents() { return 0; @@ -115,6 +124,7 @@ public final class GlonassAssistance implements Parcelable { public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeTypedObject(mAlmanac, flags); dest.writeTypedObject(mUtcModel, flags); + dest.writeTypedObject(mAuxiliaryInformation, flags); dest.writeTypedList(mTimeModels); dest.writeTypedList(mSatelliteEphemeris); dest.writeTypedList(mSatelliteCorrections); @@ -126,6 +136,7 @@ public final class GlonassAssistance implements Parcelable { StringBuilder builder = new StringBuilder("GlonassAssistance["); builder.append("almanac = ").append(mAlmanac); builder.append(", utcModel = ").append(mUtcModel); + builder.append(", auxiliaryInformation = ").append(mAuxiliaryInformation); builder.append(", timeModels = ").append(mTimeModels); builder.append(", satelliteEphemeris = ").append(mSatelliteEphemeris); builder.append(", satelliteCorrections = ").append(mSatelliteCorrections); @@ -140,6 +151,8 @@ public final class GlonassAssistance implements Parcelable { return new GlonassAssistance.Builder() .setAlmanac(in.readTypedObject(GlonassAlmanac.CREATOR)) .setUtcModel(in.readTypedObject(UtcModel.CREATOR)) + .setAuxiliaryInformation( + in.readTypedObject(AuxiliaryInformation.CREATOR)) .setTimeModels(in.createTypedArrayList(TimeModel.CREATOR)) .setSatelliteEphemeris( in.createTypedArrayList(GlonassSatelliteEphemeris.CREATOR)) @@ -158,30 +171,36 @@ public final class GlonassAssistance implements Parcelable { public static final class Builder { private GlonassAlmanac mAlmanac; private UtcModel mUtcModel; + private AuxiliaryInformation mAuxiliaryInformation; private List<TimeModel> mTimeModels; private List<GlonassSatelliteEphemeris> mSatelliteEphemeris; private List<GnssSatelliteCorrections> mSatelliteCorrections; /** Sets the Glonass almanac. */ @NonNull - public Builder setAlmanac( - @Nullable @SuppressLint("NullableCollection") GlonassAlmanac almanac) { + public Builder setAlmanac(@Nullable GlonassAlmanac almanac) { mAlmanac = almanac; return this; } /** Sets the UTC model. */ @NonNull - public Builder setUtcModel( - @Nullable @SuppressLint("NullableCollection") UtcModel utcModel) { + public Builder setUtcModel(@Nullable UtcModel utcModel) { mUtcModel = utcModel; return this; } + /** Sets the auxiliary information. */ + @NonNull + public Builder setAuxiliaryInformation( + @Nullable AuxiliaryInformation auxiliaryInformation) { + mAuxiliaryInformation = auxiliaryInformation; + return this; + } + /** Sets the list of time models. */ @NonNull - public Builder setTimeModels( - @Nullable @SuppressLint("NullableCollection") List<TimeModel> timeModels) { + public Builder setTimeModels(@NonNull List<TimeModel> timeModels) { mTimeModels = timeModels; return this; } @@ -189,8 +208,7 @@ public final class GlonassAssistance implements Parcelable { /** Sets the list of Glonass satellite ephemeris. */ @NonNull public Builder setSatelliteEphemeris( - @Nullable @SuppressLint("NullableCollection") - List<GlonassSatelliteEphemeris> satelliteEphemeris) { + @NonNull List<GlonassSatelliteEphemeris> satelliteEphemeris) { mSatelliteEphemeris = satelliteEphemeris; return this; } @@ -198,8 +216,7 @@ public final class GlonassAssistance implements Parcelable { /** Sets the list of Glonass satellite corrections. */ @NonNull public Builder setSatelliteCorrections( - @Nullable @SuppressLint("NullableCollection") - List<GnssSatelliteCorrections> satelliteCorrections) { + @NonNull List<GnssSatelliteCorrections> satelliteCorrections) { mSatelliteCorrections = satelliteCorrections; return this; } diff --git a/location/java/android/location/GpsAssistance.java b/location/java/android/location/GpsAssistance.java index 5202fc4cd851..5a8802f057e2 100644 --- a/location/java/android/location/GpsAssistance.java +++ b/location/java/android/location/GpsAssistance.java @@ -51,6 +51,9 @@ public final class GpsAssistance implements Parcelable { /** The leap seconds model. */ @Nullable private final LeapSecondsModel mLeapSecondsModel; + /** The auxiliary information. */ + @Nullable private final AuxiliaryInformation mAuxiliaryInformation; + /** The list of time models. */ @NonNull private final List<TimeModel> mTimeModels; @@ -68,6 +71,7 @@ public final class GpsAssistance implements Parcelable { mIonosphericModel = builder.mIonosphericModel; mUtcModel = builder.mUtcModel; mLeapSecondsModel = builder.mLeapSecondsModel; + mAuxiliaryInformation = builder.mAuxiliaryInformation; if (builder.mTimeModels != null) { mTimeModels = Collections.unmodifiableList(new ArrayList<>(builder.mTimeModels)); } else { @@ -117,6 +121,12 @@ public final class GpsAssistance implements Parcelable { return mLeapSecondsModel; } + /** Returns the auxiliary information. */ + @Nullable + public AuxiliaryInformation getAuxiliaryInformation() { + return mAuxiliaryInformation; + } + /** Returns the list of time models. */ @NonNull public List<TimeModel> getTimeModels() { @@ -152,6 +162,8 @@ public final class GpsAssistance implements Parcelable { in.readTypedObject(KlobucharIonosphericModel.CREATOR)) .setUtcModel(in.readTypedObject(UtcModel.CREATOR)) .setLeapSecondsModel(in.readTypedObject(LeapSecondsModel.CREATOR)) + .setAuxiliaryInformation( + in.readTypedObject(AuxiliaryInformation.CREATOR)) .setTimeModels(in.createTypedArrayList(TimeModel.CREATOR)) .setSatelliteEphemeris( in.createTypedArrayList(GpsSatelliteEphemeris.CREATOR)) @@ -179,6 +191,7 @@ public final class GpsAssistance implements Parcelable { dest.writeTypedObject(mIonosphericModel, flags); dest.writeTypedObject(mUtcModel, flags); dest.writeTypedObject(mLeapSecondsModel, flags); + dest.writeTypedObject(mAuxiliaryInformation, flags); dest.writeTypedList(mTimeModels); dest.writeTypedList(mSatelliteEphemeris); dest.writeTypedList(mRealTimeIntegrityModels); @@ -193,6 +206,7 @@ public final class GpsAssistance implements Parcelable { builder.append(", ionosphericModel = ").append(mIonosphericModel); builder.append(", utcModel = ").append(mUtcModel); builder.append(", leapSecondsModel = ").append(mLeapSecondsModel); + builder.append(", auxiliaryInformation = ").append(mAuxiliaryInformation); builder.append(", timeModels = ").append(mTimeModels); builder.append(", satelliteEphemeris = ").append(mSatelliteEphemeris); builder.append(", realTimeIntegrityModels = ").append(mRealTimeIntegrityModels); @@ -207,6 +221,7 @@ public final class GpsAssistance implements Parcelable { private KlobucharIonosphericModel mIonosphericModel; private UtcModel mUtcModel; private LeapSecondsModel mLeapSecondsModel; + private AuxiliaryInformation mAuxiliaryInformation; private List<TimeModel> mTimeModels; private List<GpsSatelliteEphemeris> mSatelliteEphemeris; private List<RealTimeIntegrityModel> mRealTimeIntegrityModels; @@ -222,33 +237,36 @@ public final class GpsAssistance implements Parcelable { /** Sets the Klobuchar ionospheric model. */ @NonNull - public Builder setIonosphericModel( - @Nullable @SuppressLint("NullableCollection") - KlobucharIonosphericModel ionosphericModel) { + public Builder setIonosphericModel(@Nullable KlobucharIonosphericModel ionosphericModel) { mIonosphericModel = ionosphericModel; return this; } /** Sets the UTC model. */ @NonNull - public Builder setUtcModel( - @Nullable @SuppressLint("NullableCollection") UtcModel utcModel) { + public Builder setUtcModel(@Nullable UtcModel utcModel) { mUtcModel = utcModel; return this; } /** Sets the leap seconds model. */ @NonNull - public Builder setLeapSecondsModel( - @Nullable @SuppressLint("NullableCollection") LeapSecondsModel leapSecondsModel) { + public Builder setLeapSecondsModel(@Nullable LeapSecondsModel leapSecondsModel) { mLeapSecondsModel = leapSecondsModel; return this; } + /** Sets the auxiliary information. */ + @NonNull + public Builder setAuxiliaryInformation( + @Nullable AuxiliaryInformation auxiliaryInformation) { + mAuxiliaryInformation = auxiliaryInformation; + return this; + } + /** Sets the list of time models. */ @NonNull - public Builder setTimeModels( - @Nullable @SuppressLint("NullableCollection") List<TimeModel> timeModels) { + public Builder setTimeModels(@NonNull List<TimeModel> timeModels) { mTimeModels = timeModels; return this; } @@ -256,8 +274,7 @@ public final class GpsAssistance implements Parcelable { /** Sets the list of GPS ephemeris. */ @NonNull public Builder setSatelliteEphemeris( - @Nullable @SuppressLint("NullableCollection") - List<GpsSatelliteEphemeris> satelliteEphemeris) { + @NonNull List<GpsSatelliteEphemeris> satelliteEphemeris) { mSatelliteEphemeris = satelliteEphemeris; return this; } @@ -265,8 +282,7 @@ public final class GpsAssistance implements Parcelable { /** Sets the list of real time integrity models. */ @NonNull public Builder setRealTimeIntegrityModels( - @Nullable @SuppressLint("NullableCollection") - List<RealTimeIntegrityModel> realTimeIntegrityModels) { + @NonNull List<RealTimeIntegrityModel> realTimeIntegrityModels) { mRealTimeIntegrityModels = realTimeIntegrityModels; return this; } @@ -274,8 +290,7 @@ public final class GpsAssistance implements Parcelable { /** Sets the list of GPS satellite corrections. */ @NonNull public Builder setSatelliteCorrections( - @Nullable @SuppressLint("NullableCollection") - List<GnssSatelliteCorrections> satelliteCorrections) { + @NonNull List<GnssSatelliteCorrections> satelliteCorrections) { mSatelliteCorrections = satelliteCorrections; return this; } diff --git a/location/java/android/location/QzssAssistance.java b/location/java/android/location/QzssAssistance.java index 9383ce3c63b5..27c34370316e 100644 --- a/location/java/android/location/QzssAssistance.java +++ b/location/java/android/location/QzssAssistance.java @@ -19,7 +19,6 @@ package android.location; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; -import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.location.GnssAssistance.GnssSatelliteCorrections; import android.location.flags.Flags; @@ -51,6 +50,9 @@ public final class QzssAssistance implements Parcelable { /** The leap seconds model. */ @Nullable private final LeapSecondsModel mLeapSecondsModel; + /** The auxiliary information. */ + @Nullable private final AuxiliaryInformation mAuxiliaryInformation; + /** The list of time models. */ @NonNull private final List<TimeModel> mTimeModels; @@ -68,6 +70,7 @@ public final class QzssAssistance implements Parcelable { mIonosphericModel = builder.mIonosphericModel; mUtcModel = builder.mUtcModel; mLeapSecondsModel = builder.mLeapSecondsModel; + mAuxiliaryInformation = builder.mAuxiliaryInformation; if (builder.mTimeModels != null) { mTimeModels = Collections.unmodifiableList(new ArrayList<>(builder.mTimeModels)); } else { @@ -117,6 +120,12 @@ public final class QzssAssistance implements Parcelable { return mLeapSecondsModel; } + /** Returns the auxiliary information. */ + @Nullable + public AuxiliaryInformation getAuxiliaryInformation() { + return mAuxiliaryInformation; + } + /** Returns the list of time models. */ @NonNull public List<TimeModel> getTimeModels() { @@ -147,19 +156,23 @@ public final class QzssAssistance implements Parcelable { @NonNull public QzssAssistance createFromParcel(Parcel in) { return new QzssAssistance.Builder() - .setAlmanac(in.readTypedObject(GnssAlmanac.CREATOR)) - .setIonosphericModel(in.readTypedObject(KlobucharIonosphericModel.CREATOR)) - .setUtcModel(in.readTypedObject(UtcModel.CREATOR)) - .setLeapSecondsModel(in.readTypedObject(LeapSecondsModel.CREATOR)) - .setTimeModels(in.createTypedArrayList(TimeModel.CREATOR)) - .setSatelliteEphemeris( - in.createTypedArrayList(QzssSatelliteEphemeris.CREATOR)) - .setRealTimeIntegrityModels( - in.createTypedArrayList(RealTimeIntegrityModel.CREATOR)) - .setSatelliteCorrections( - in.createTypedArrayList(GnssSatelliteCorrections.CREATOR)) - .build(); + .setAlmanac(in.readTypedObject(GnssAlmanac.CREATOR)) + .setIonosphericModel( + in.readTypedObject(KlobucharIonosphericModel.CREATOR)) + .setUtcModel(in.readTypedObject(UtcModel.CREATOR)) + .setLeapSecondsModel(in.readTypedObject(LeapSecondsModel.CREATOR)) + .setAuxiliaryInformation( + in.readTypedObject(AuxiliaryInformation.CREATOR)) + .setTimeModels(in.createTypedArrayList(TimeModel.CREATOR)) + .setSatelliteEphemeris( + in.createTypedArrayList(QzssSatelliteEphemeris.CREATOR)) + .setRealTimeIntegrityModels( + in.createTypedArrayList(RealTimeIntegrityModel.CREATOR)) + .setSatelliteCorrections( + in.createTypedArrayList(GnssSatelliteCorrections.CREATOR)) + .build(); } + @Override public QzssAssistance[] newArray(int size) { return new QzssAssistance[size]; @@ -177,6 +190,7 @@ public final class QzssAssistance implements Parcelable { dest.writeTypedObject(mIonosphericModel, flags); dest.writeTypedObject(mUtcModel, flags); dest.writeTypedObject(mLeapSecondsModel, flags); + dest.writeTypedObject(mAuxiliaryInformation, flags); dest.writeTypedList(mTimeModels); dest.writeTypedList(mSatelliteEphemeris); dest.writeTypedList(mRealTimeIntegrityModels); @@ -191,6 +205,7 @@ public final class QzssAssistance implements Parcelable { builder.append(", ionosphericModel = ").append(mIonosphericModel); builder.append(", utcModel = ").append(mUtcModel); builder.append(", leapSecondsModel = ").append(mLeapSecondsModel); + builder.append(", auxiliaryInformation = ").append(mAuxiliaryInformation); builder.append(", timeModels = ").append(mTimeModels); builder.append(", satelliteEphemeris = ").append(mSatelliteEphemeris); builder.append(", realTimeIntegrityModels = ").append(mRealTimeIntegrityModels); @@ -205,6 +220,7 @@ public final class QzssAssistance implements Parcelable { private KlobucharIonosphericModel mIonosphericModel; private UtcModel mUtcModel; private LeapSecondsModel mLeapSecondsModel; + private AuxiliaryInformation mAuxiliaryInformation; private List<TimeModel> mTimeModels; private List<QzssSatelliteEphemeris> mSatelliteEphemeris; private List<RealTimeIntegrityModel> mRealTimeIntegrityModels; @@ -238,10 +254,17 @@ public final class QzssAssistance implements Parcelable { return this; } + /** Sets the auxiliary information. */ + @NonNull + public Builder setAuxiliaryInformation( + @Nullable AuxiliaryInformation auxiliaryInformation) { + mAuxiliaryInformation = auxiliaryInformation; + return this; + } + /** Sets the list of time models. */ @NonNull - public Builder setTimeModels( - @Nullable @SuppressLint("NullableCollection") List<TimeModel> timeModels) { + public Builder setTimeModels(@NonNull List<TimeModel> timeModels) { mTimeModels = timeModels; return this; } @@ -249,8 +272,7 @@ public final class QzssAssistance implements Parcelable { /** Sets the list of QZSS ephemeris. */ @NonNull public Builder setSatelliteEphemeris( - @Nullable @SuppressLint("NullableCollection") - List<QzssSatelliteEphemeris> satelliteEphemeris) { + @NonNull List<QzssSatelliteEphemeris> satelliteEphemeris) { mSatelliteEphemeris = satelliteEphemeris; return this; } @@ -258,8 +280,7 @@ public final class QzssAssistance implements Parcelable { /** Sets the list of real time integrity model. */ @NonNull public Builder setRealTimeIntegrityModels( - @Nullable @SuppressLint("NullableCollection") - List<RealTimeIntegrityModel> realTimeIntegrityModels) { + @NonNull List<RealTimeIntegrityModel> realTimeIntegrityModels) { mRealTimeIntegrityModels = realTimeIntegrityModels; return this; } @@ -267,8 +288,7 @@ public final class QzssAssistance implements Parcelable { /** Sets the list of QZSS satellite correction. */ @NonNull public Builder setSatelliteCorrections( - @Nullable @SuppressLint("NullableCollection") - List<GnssSatelliteCorrections> satelliteCorrections) { + @NonNull List<GnssSatelliteCorrections> satelliteCorrections) { mSatelliteCorrections = satelliteCorrections; return this; } diff --git a/location/java/android/location/RealTimeIntegrityModel.java b/location/java/android/location/RealTimeIntegrityModel.java index d268926e56e2..f065def35f7a 100644 --- a/location/java/android/location/RealTimeIntegrityModel.java +++ b/location/java/android/location/RealTimeIntegrityModel.java @@ -26,6 +26,10 @@ import android.os.Parcelable; import com.android.internal.util.Preconditions; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + /** * A class contains the real time integrity status of a GNSS satellite based on notice advisory. * @@ -35,8 +39,7 @@ import com.android.internal.util.Preconditions; @SystemApi public final class RealTimeIntegrityModel implements Parcelable { /** - * Pseudo-random or satellite ID number for the satellite, - * a.k.a. Space Vehicle (SV), or OSN number for Glonass. + * Bad satellite ID number or OSN number for Glonass. * * <p>The distinction is made by looking at the constellation field. Values * must be in the range of: @@ -47,10 +50,14 @@ public final class RealTimeIntegrityModel implements Parcelable { * <p> - Galileo: 1-36 * <p> - Beidou: 1-63 */ - private final int mSvid; + private final int mBadSvid; - /** Indicates whether the satellite is currently usable for navigation. */ - private final boolean mUsable; + /** + * The type of the bad signal or signals. + * + * <p>An empty list means that all signals on the specific SV are not healthy. + */ + @NonNull private final List<GnssSignalType> mBadSignalTypes; /** UTC timestamp (in seconds) when the advisory was published. */ private final long mPublishDateSeconds; @@ -81,14 +88,19 @@ public final class RealTimeIntegrityModel implements Parcelable { private RealTimeIntegrityModel(Builder builder) { // Allow SV ID beyond the range to support potential future extensibility. - Preconditions.checkArgument(builder.mSvid >= 1); + Preconditions.checkArgument(builder.mBadSvid >= 1); Preconditions.checkArgument(builder.mPublishDateSeconds > 0); Preconditions.checkArgument(builder.mStartDateSeconds > 0); Preconditions.checkArgument(builder.mEndDateSeconds > 0); Preconditions.checkNotNull(builder.mAdvisoryType, "AdvisoryType cannot be null"); Preconditions.checkNotNull(builder.mAdvisoryNumber, "AdvisoryNumber cannot be null"); - mSvid = builder.mSvid; - mUsable = builder.mUsable; + if (builder.mBadSignalTypes == null) { + mBadSignalTypes = new ArrayList<>(); + } else { + mBadSignalTypes = Collections.unmodifiableList( + new ArrayList<>(builder.mBadSignalTypes)); + } + mBadSvid = builder.mBadSvid; mPublishDateSeconds = builder.mPublishDateSeconds; mStartDateSeconds = builder.mStartDateSeconds; mEndDateSeconds = builder.mEndDateSeconds; @@ -110,13 +122,18 @@ public final class RealTimeIntegrityModel implements Parcelable { * <p> - Beidou: 1-63 */ @IntRange(from = 1, to = 206) - public int getSvid() { - return mSvid; + public int getBadSvid() { + return mBadSvid; } - /** Returns whether the satellite is usable or not. */ - public boolean isUsable() { - return mUsable; + /** + * Returns the type of the bad signal or signals. + * + * <p>An empty list means that all signals on the specific SV are not healthy. + */ + @NonNull + public List<GnssSignalType> getBadSignalTypes() { + return mBadSignalTypes; } /** Returns the UTC timestamp (in seconds) when the advisory was published */ @@ -156,8 +173,9 @@ public final class RealTimeIntegrityModel implements Parcelable { public RealTimeIntegrityModel createFromParcel(Parcel in) { RealTimeIntegrityModel realTimeIntegrityModel = new RealTimeIntegrityModel.Builder() - .setSvid(in.readInt()) - .setUsable(in.readBoolean()) + .setBadSvid(in.readInt()) + .setBadSignalTypes( + in.createTypedArrayList(GnssSignalType.CREATOR)) .setPublishDateSeconds(in.readLong()) .setStartDateSeconds(in.readLong()) .setEndDateSeconds(in.readLong()) @@ -180,8 +198,8 @@ public final class RealTimeIntegrityModel implements Parcelable { @Override public void writeToParcel(@NonNull Parcel parcel, int flags) { - parcel.writeInt(mSvid); - parcel.writeBoolean(mUsable); + parcel.writeInt(mBadSvid); + parcel.writeTypedList(mBadSignalTypes); parcel.writeLong(mPublishDateSeconds); parcel.writeLong(mStartDateSeconds); parcel.writeLong(mEndDateSeconds); @@ -193,8 +211,8 @@ public final class RealTimeIntegrityModel implements Parcelable { @NonNull public String toString() { StringBuilder builder = new StringBuilder("RealTimeIntegrityModel["); - builder.append("svid = ").append(mSvid); - builder.append(", usable = ").append(mUsable); + builder.append("badSvid = ").append(mBadSvid); + builder.append(", badSignalTypes = ").append(mBadSignalTypes); builder.append(", publishDateSeconds = ").append(mPublishDateSeconds); builder.append(", startDateSeconds = ").append(mStartDateSeconds); builder.append(", endDateSeconds = ").append(mEndDateSeconds); @@ -206,8 +224,8 @@ public final class RealTimeIntegrityModel implements Parcelable { /** Builder for {@link RealTimeIntegrityModel} */ public static final class Builder { - private int mSvid; - private boolean mUsable; + private int mBadSvid; + private List<GnssSignalType> mBadSignalTypes; private long mPublishDateSeconds; private long mStartDateSeconds; private long mEndDateSeconds; @@ -215,8 +233,7 @@ public final class RealTimeIntegrityModel implements Parcelable { private String mAdvisoryNumber; /** - * Sets the Pseudo-random or satellite ID number for the satellite, - * a.k.a. Space Vehicle (SV), or OSN number for Glonass. + * Sets the bad satellite ID number or OSN number for Glonass. * * <p>The distinction is made by looking at the constellation field. Values * must be in the range of: @@ -228,15 +245,19 @@ public final class RealTimeIntegrityModel implements Parcelable { * <p> - Beidou: 1-63 */ @NonNull - public Builder setSvid(@IntRange(from = 1, to = 206) int svid) { - mSvid = svid; + public Builder setBadSvid(@IntRange(from = 1, to = 206) int badSvid) { + mBadSvid = badSvid; return this; } - /** Sets whether the satellite is usable or not. */ + /** + * Sets the type of the bad signal or signals. + * + * <p>An empty list means that all signals on the specific SV are not healthy. + */ @NonNull - public Builder setUsable(boolean usable) { - mUsable = usable; + public Builder setBadSignalTypes(@NonNull List<GnssSignalType> badSignalTypes) { + mBadSignalTypes = badSignalTypes; return this; } diff --git a/location/java/android/location/flags/location.aconfig b/location/java/android/location/flags/location.aconfig index c02cc808d60c..1b38982f48c1 100644 --- a/location/java/android/location/flags/location.aconfig +++ b/location/java/android/location/flags/location.aconfig @@ -167,4 +167,4 @@ flag { namespace: "location" description: "Flag for GNSS assistance interface" bug: "209078566" -}
\ No newline at end of file +} diff --git a/location/java/android/location/provider/GnssAssistanceProviderBase.java b/location/java/android/location/provider/GnssAssistanceProviderBase.java new file mode 100644 index 000000000000..f4b26d5033a5 --- /dev/null +++ b/location/java/android/location/provider/GnssAssistanceProviderBase.java @@ -0,0 +1,152 @@ +/* + * 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.location.provider; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.content.Context; +import android.content.Intent; +import android.location.GnssAssistance; +import android.location.flags.Flags; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.OutcomeReceiver; +import android.os.RemoteException; +import android.util.Log; + +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; + + +/** + * Base class for GNSS assistance providers outside the system server. + * + * <p>GNSS assistance providers should be wrapped in a non-exported service which returns the result + * of {@link #getBinder()} from the service's {@link android.app.Service#onBind(Intent)} method. The + * service should not be exported so that components other than the system server cannot bind to it. + * Alternatively, the service may be guarded by a permission that only system server can obtain. The + * service may specify metadata on its capabilities: + * + * <ul> + * <li>"serviceVersion": An integer version code to help tie break if multiple services are + * capable of implementing the geocode provider. All else equal, the service with the highest + * version code will be chosen. Assumed to be 0 if not specified. + * <li>"serviceIsMultiuser": A boolean property, indicating if the service wishes to take + * responsibility for handling changes to the current user on the device. If true, the service + * will always be bound from the system user. If false, the service will always be bound from + * the current user. If the current user changes, the old binding will be released, and a new + * binding established under the new user. Assumed to be false if not specified. + * </ul> + * + * <p>The service should have an intent filter in place for the GNSS assistance provider as + * specified by the constant in this class. + * + * <p>GNSS assistance providers are identified by their UID / package name / attribution tag. Based + * on this identity, geocode providers may be given some special privileges. + * + * @hide + */ +@FlaggedApi(Flags.FLAG_GNSS_ASSISTANCE_INTERFACE) +@SystemApi +public abstract class GnssAssistanceProviderBase { + + /** + * The action the wrapping service should have in its intent filter to implement the GNSS + * Assistance provider. + */ + public static final String ACTION_GNSS_ASSISTANCE_PROVIDER = + "android.location.provider.action.GNSS_ASSISTANCE_PROVIDER"; + + final String mTag; + @Nullable + final String mAttributionTag; + final IBinder mBinder; + + /** + * Subclasses should pass in a context and an arbitrary tag that may be used for logcat logging + * of errors, and thus should uniquely identify the class. + */ + public GnssAssistanceProviderBase(@NonNull Context context, @NonNull String tag) { + mTag = tag; + mAttributionTag = context.getAttributionTag(); + mBinder = new GnssAssistanceProviderBase.Service(); + } + + /** + * Returns the IBinder instance that should be returned from the {@link + * android.app.Service#onBind(Intent)} method of the wrapping service. + */ + @NonNull + public final IBinder getBinder() { + return mBinder; + } + + /** + * Requests GNSS assistance data of the given arguments. The given callback must be invoked + * once. + */ + public abstract void onRequest( + @NonNull OutcomeReceiver<GnssAssistance, Throwable> callback); + + private class Service extends IGnssAssistanceProvider.Stub { + @Override + public void request(IGnssAssistanceCallback callback) { + try { + onRequest(new GnssAssistanceProviderBase.SingleUseCallback(callback)); + } catch (RuntimeException e) { + // exceptions on one-way binder threads are dropped - move to a different thread + Log.w(mTag, e); + new Handler(Looper.getMainLooper()) + .post( + () -> { + throw new AssertionError(e); + }); + } + } + } + + private static class SingleUseCallback implements + OutcomeReceiver<GnssAssistance, Throwable> { + + private final AtomicReference<IGnssAssistanceCallback> mCallback; + + SingleUseCallback(IGnssAssistanceCallback callback) { + mCallback = new AtomicReference<>(callback); + } + + @Override + public void onError(Throwable e) { + try { + Objects.requireNonNull(mCallback.getAndSet(null)).onError(); + } catch (RemoteException r) { + throw r.rethrowFromSystemServer(); + } + } + + @Override + public void onResult(GnssAssistance result) { + try { + Objects.requireNonNull(mCallback.getAndSet(null)).onResult(result); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } +} diff --git a/core/tests/coretests/src/android/graphics/GraphicsTests.java b/location/java/android/location/provider/IGnssAssistanceCallback.aidl index 70f5976843bc..ea38d08df6c2 100644 --- a/core/tests/coretests/src/android/graphics/GraphicsTests.java +++ b/location/java/android/location/provider/IGnssAssistanceCallback.aidl @@ -1,5 +1,5 @@ /* - * Copyright (C) 2008 The Android Open Source Project + * 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. @@ -14,15 +14,15 @@ * limitations under the License. */ -package android.graphics; +package android.location.provider; -import junit.framework.TestSuite; +import android.location.GnssAssistance; -public class GraphicsTests { - public static TestSuite suite() { - TestSuite suite = new TestSuite(GraphicsTests.class.getName()); - - suite.addTestSuite(BitmapTest.class); - return suite; - } +/** + * Binder interface for GNSS assistance callbacks. + * @hide + */ +oneway interface IGnssAssistanceCallback { + void onError(); + void onResult(in GnssAssistance result); } diff --git a/location/java/android/location/provider/IGnssAssistanceProvider.aidl b/location/java/android/location/provider/IGnssAssistanceProvider.aidl new file mode 100644 index 000000000000..1796e9edb347 --- /dev/null +++ b/location/java/android/location/provider/IGnssAssistanceProvider.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.location.provider; + +import android.location.provider.IGnssAssistanceCallback; + +/** + * Binder interface for services that implement GNSS assistance providers. Do not implement this + * directly, extend {@link GnssAssistanceProviderBase} instead. + * @hide + */ +oneway interface IGnssAssistanceProvider { + void request(in IGnssAssistanceCallback callback); +} diff --git a/media/java/android/media/FadeManagerConfiguration.java b/media/java/android/media/FadeManagerConfiguration.java index 6d84e7066839..b91a5b57ac6b 100644 --- a/media/java/android/media/FadeManagerConfiguration.java +++ b/media/java/android/media/FadeManagerConfiguration.java @@ -673,6 +673,7 @@ public final class FadeManagerConfiguration implements Parcelable { return config != null ? config.getDuration() : DURATION_NOT_SET; } + @Nullable private VolumeShaper.Configuration getVolumeShaperConfigFromWrapper( FadeVolumeShaperConfigsWrapper wrapper, boolean isFadeIn) { // if no volume shaper config is available, return null diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java index 302969f58ba8..19d39234d1c6 100644 --- a/media/java/android/media/MediaCodecInfo.java +++ b/media/java/android/media/MediaCodecInfo.java @@ -245,12 +245,7 @@ public final class MediaCodecInfo { * {@link MediaCodecInfo#getCapabilitiesForType getCapabilitiesForType()}, passing a MIME type. */ public static final class CodecCapabilities { - public CodecCapabilities() { - } - - // CLASSIFICATION - private String mMime; - private int mMaxSupportedInstances; + private static final String TAG = "CodecCapabilities"; // LEGACY FIELDS @@ -628,12 +623,6 @@ public final class MediaCodecInfo { */ public int[] colorFormats; // NOTE this array is modifiable by user - // FEATURES - - private int mFlagsSupported; - private int mFlagsRequired; - private int mFlagsVerified; - /** * <b>video decoder only</b>: codec supports seamless resolution changes. */ @@ -823,122 +812,680 @@ public final class MediaCodecInfo { @FlaggedApi(FLAG_NULL_OUTPUT_SURFACE) public static final String FEATURE_DetachedSurface = "detached-surface"; - /** - * Query codec feature capabilities. - * <p> - * These features are supported to be used by the codec. These - * include optional features that can be turned on, as well as - * features that are always on. - */ - public final boolean isFeatureSupported(String name) { - return checkFeature(name, mFlagsSupported); - } + /** package private */ interface CodecCapsIntf { + public CodecCapsIntf dup(); - /** - * Query codec feature requirements. - * <p> - * These features are required to be used by the codec, and as such, - * they are always turned on. - */ - public final boolean isFeatureRequired(String name) { - return checkFeature(name, mFlagsRequired); + public boolean isFeatureSupported(String name); + + public boolean isFeatureRequired(String name); + + public boolean isFormatSupported(MediaFormat format); + + public MediaFormat getDefaultFormat(); + + public String getMimeType(); + + public int getMaxSupportedInstances(); + + public AudioCapabilities getAudioCapabilities(); + + public VideoCapabilities getVideoCapabilities(); + + public EncoderCapabilities getEncoderCapabilities(); + + public boolean isRegular(); + + public CodecProfileLevel[] getProfileLevels(); + + public int[] getColorFormats(); } - // Flags are used for feature list creation so separate this into a private - // static class to delay reading the flags only when constructing the list. - private static class FeatureList { - private static Feature[] getDecoderFeatures() { - ArrayList<Feature> features = new ArrayList(); - features.add(new Feature(FEATURE_AdaptivePlayback, (1 << 0), true)); - features.add(new Feature(FEATURE_SecurePlayback, (1 << 1), false)); - features.add(new Feature(FEATURE_TunneledPlayback, (1 << 2), false)); - features.add(new Feature(FEATURE_PartialFrame, (1 << 3), false)); - features.add(new Feature(FEATURE_FrameParsing, (1 << 4), false)); - features.add(new Feature(FEATURE_MultipleFrames, (1 << 5), false)); - features.add(new Feature(FEATURE_DynamicTimestamp, (1 << 6), false)); - features.add(new Feature(FEATURE_LowLatency, (1 << 7), true)); - if (GetFlag(() -> android.media.codec.Flags.dynamicColorAspects())) { - features.add(new Feature(FEATURE_DynamicColorAspects, (1 << 8), true)); + /* package private */ static final class CodecCapsLegacyImpl implements CodecCapsIntf { + // errors while reading profile levels - accessed from sister capabilities + int mError; + + private CodecProfileLevel[] mProfileLevels; + private int[] mColorFormats; + + // CLASSIFICATION + private String mMime; + private int mMaxSupportedInstances; + + // FEATURES + private int mFlagsSupported; + private int mFlagsRequired; + private int mFlagsVerified; + + // NEW-STYLE CAPABILITIES + private AudioCapabilities mAudioCaps; + private VideoCapabilities mVideoCaps; + private EncoderCapabilities mEncoderCaps; + private MediaFormat mDefaultFormat; + + private MediaFormat mCapabilitiesInfo; + + public CodecProfileLevel[] getProfileLevels() { + return mProfileLevels; + } + + public int[] getColorFormats() { + return mColorFormats; + } + + public CodecCapsLegacyImpl() {} + + public CodecCapsLegacyImpl dup() { + CodecCapsLegacyImpl caps = new CodecCapsLegacyImpl(); + + caps.mProfileLevels = Arrays.copyOf(mProfileLevels, mProfileLevels.length); + caps.mColorFormats = Arrays.copyOf(mColorFormats, mColorFormats.length); + + caps.mMime = mMime; + caps.mMaxSupportedInstances = mMaxSupportedInstances; + caps.mFlagsRequired = mFlagsRequired; + caps.mFlagsSupported = mFlagsSupported; + caps.mFlagsVerified = mFlagsVerified; + caps.mAudioCaps = mAudioCaps; + caps.mVideoCaps = mVideoCaps; + caps.mEncoderCaps = mEncoderCaps; + caps.mDefaultFormat = mDefaultFormat; + caps.mCapabilitiesInfo = mCapabilitiesInfo; + + return caps; + } + + public final boolean isFeatureSupported(String name) { + return checkFeature(name, mFlagsSupported); + } + + public final boolean isFeatureRequired(String name) { + return checkFeature(name, mFlagsRequired); + } + + // Flags are used for feature list creation so separate this into a private + // static class to delay reading the flags only when constructing the list. + private static class FeatureList { + private static Feature[] getDecoderFeatures() { + ArrayList<Feature> features = new ArrayList(); + features.add(new Feature(FEATURE_AdaptivePlayback, (1 << 0), true)); + features.add(new Feature(FEATURE_SecurePlayback, (1 << 1), false)); + features.add(new Feature(FEATURE_TunneledPlayback, (1 << 2), false)); + features.add(new Feature(FEATURE_PartialFrame, (1 << 3), false)); + features.add(new Feature(FEATURE_FrameParsing, (1 << 4), false)); + features.add(new Feature(FEATURE_MultipleFrames, (1 << 5), false)); + features.add(new Feature(FEATURE_DynamicTimestamp, (1 << 6), false)); + features.add(new Feature(FEATURE_LowLatency, (1 << 7), true)); + if (GetFlag(() -> android.media.codec.Flags.dynamicColorAspects())) { + features.add(new Feature(FEATURE_DynamicColorAspects, (1 << 8), true)); + } + if (GetFlag(() -> android.media.codec.Flags.nullOutputSurface())) { + features.add(new Feature(FEATURE_DetachedSurface, (1 << 9), true)); + } + + // feature to exclude codec from REGULAR codec list + features.add(new Feature(FEATURE_SpecialCodec, (1 << 30), false, true)); + + return features.toArray(new Feature[0]); + }; + + private static Feature[] decoderFeatures = getDecoderFeatures(); + + private static Feature[] getEncoderFeatures() { + ArrayList<Feature> features = new ArrayList(); + + features.add(new Feature(FEATURE_IntraRefresh, (1 << 0), false)); + features.add(new Feature(FEATURE_MultipleFrames, (1 << 1), false)); + features.add(new Feature(FEATURE_DynamicTimestamp, (1 << 2), false)); + features.add(new Feature(FEATURE_QpBounds, (1 << 3), false)); + features.add(new Feature(FEATURE_EncodingStatistics, (1 << 4), false)); + features.add(new Feature(FEATURE_HdrEditing, (1 << 5), false)); + if (GetFlag(() -> android.media.codec.Flags.hlgEditing())) { + features.add(new Feature(FEATURE_HlgEditing, (1 << 6), true)); + } + if (GetFlag(() -> android.media.codec.Flags.regionOfInterest())) { + features.add(new Feature(FEATURE_Roi, (1 << 7), true)); + } + + // feature to exclude codec from REGULAR codec list + features.add(new Feature(FEATURE_SpecialCodec, (1 << 30), false, true)); + + return features.toArray(new Feature[0]); + }; + + private static Feature[] encoderFeatures = getEncoderFeatures(); + + public static Feature[] getFeatures(boolean isEncoder) { + if (isEncoder) { + return encoderFeatures; + } else { + return decoderFeatures; + } } - if (GetFlag(() -> android.media.codec.Flags.nullOutputSurface())) { - features.add(new Feature(FEATURE_DetachedSurface, (1 << 9), true)); + } + + /** @hide */ + public String[] validFeatures() { + Feature[] features = getValidFeatures(); + String[] res = new String[features.length]; + for (int i = 0; i < res.length; i++) { + if (!features[i].mInternal) { + res[i] = features[i].mName; + } } + return res; + } - // feature to exclude codec from REGULAR codec list - features.add(new Feature(FEATURE_SpecialCodec, (1 << 30), false, true)); + private Feature[] getValidFeatures() { + return FeatureList.getFeatures(isEncoder()); + } - return features.toArray(new Feature[0]); - }; + private boolean checkFeature(String name, int flags) { + for (Feature feat: getValidFeatures()) { + if (feat.mName.equals(name)) { + return (flags & feat.mValue) != 0; + } + } + return false; + } - private static Feature[] decoderFeatures = getDecoderFeatures(); + public boolean isRegular() { + // regular codecs only require default features + for (Feature feat: getValidFeatures()) { + if (!feat.mDefault && isFeatureRequired(feat.mName)) { + return false; + } + } + return true; + } - private static Feature[] getEncoderFeatures() { - ArrayList<Feature> features = new ArrayList(); + public final boolean isFormatSupported(MediaFormat format) { + final Map<String, Object> map = format.getMap(); + final String mime = (String) map.get(MediaFormat.KEY_MIME); - features.add(new Feature(FEATURE_IntraRefresh, (1 << 0), false)); - features.add(new Feature(FEATURE_MultipleFrames, (1 << 1), false)); - features.add(new Feature(FEATURE_DynamicTimestamp, (1 << 2), false)); - features.add(new Feature(FEATURE_QpBounds, (1 << 3), false)); - features.add(new Feature(FEATURE_EncodingStatistics, (1 << 4), false)); - features.add(new Feature(FEATURE_HdrEditing, (1 << 5), false)); - if (GetFlag(() -> android.media.codec.Flags.hlgEditing())) { - features.add(new Feature(FEATURE_HlgEditing, (1 << 6), true)); + // mime must match if present + if (mime != null && !mMime.equalsIgnoreCase(mime)) { + return false; } - if (GetFlag(() -> android.media.codec.Flags.regionOfInterest())) { - features.add(new Feature(FEATURE_Roi, (1 << 7), true)); + + // check feature support + for (Feature feat: getValidFeatures()) { + if (feat.mInternal) { + continue; + } + + Integer yesNo = (Integer) map.get(MediaFormat.KEY_FEATURE_ + feat.mName); + if (yesNo == null) { + continue; + } + if ((yesNo == 1 && !isFeatureSupported(feat.mName)) + || (yesNo == 0 && isFeatureRequired(feat.mName))) { + return false; + } } - // feature to exclude codec from REGULAR codec list - features.add(new Feature(FEATURE_SpecialCodec, (1 << 30), false, true)); + Integer profile = (Integer) map.get(MediaFormat.KEY_PROFILE); + Integer level = (Integer) map.get(MediaFormat.KEY_LEVEL); - return features.toArray(new Feature[0]); - }; + if (profile != null) { + if (!supportsProfileLevel(profile, level)) { + return false; + } - private static Feature[] encoderFeatures = getEncoderFeatures(); + // If we recognize this profile, check that this format is supported by the + // highest level supported by the codec for that profile. (Ignore specified + // level beyond the above profile/level check as level is only used as a + // guidance. E.g. AVC Level 1 CIF format is supported if codec supports + // level 1.1 even though max size for Level 1 is QCIF. However, MPEG2 Simple + // Profile 1080p format is not supported even if codec supports Main Profile + // Level High, as Simple Profile does not support 1080p. + CodecCapsLegacyImpl levelCaps = null; + int maxLevel = 0; + for (CodecProfileLevel pl : mProfileLevels) { + if (pl.profile == profile && pl.level > maxLevel) { + // H.263 levels are not completely ordered: + // Level45 support only implies Level10 support + if (!mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_H263) + || pl.level != CodecProfileLevel.H263Level45 + || maxLevel == CodecProfileLevel.H263Level10) { + maxLevel = pl.level; + } + } + } + levelCaps = createFromProfileLevel(mMime, profile, maxLevel); + // We must remove the profile from this format otherwise + // levelCaps.isFormatSupported will get into this same condition and loop + // forever. Furthermore, since levelCaps does not contain features and bitrate + // specific keys, keep only keys relevant for a level check. + Map<String, Object> levelCriticalFormatMap = new HashMap<>(map); + final Set<String> criticalKeys = isVideo() + ? VideoCapabilities.VideoCapsLegacyImpl.VIDEO_LEVEL_CRITICAL_FORMAT_KEYS + : isAudio() + ? AudioCapabilities.AudioCapsLegacyImpl.AUDIO_LEVEL_CRITICAL_FORMAT_KEYS + : null; + + // critical keys will always contain KEY_MIME, but should also contain others + // to be meaningful + if (criticalKeys != null && criticalKeys.size() > 1 && levelCaps != null) { + levelCriticalFormatMap.keySet().retainAll(criticalKeys); + + MediaFormat levelCriticalFormat = new MediaFormat(levelCriticalFormatMap); + if (!levelCaps.isFormatSupported(levelCriticalFormat)) { + return false; + } + } + } + if (mAudioCaps != null && !mAudioCaps.supportsFormat(format)) { + return false; + } + if (mVideoCaps != null && !mVideoCaps.supportsFormat(format)) { + return false; + } + if (mEncoderCaps != null && !mEncoderCaps.supportsFormat(format)) { + return false; + } + return true; + } - public static Feature[] getFeatures(boolean isEncoder) { - if (isEncoder) { - return encoderFeatures; - } else { - return decoderFeatures; + private static boolean supportsBitrate( + Range<Integer> bitrateRange, MediaFormat format) { + Map<String, Object> map = format.getMap(); + + // consider max bitrate over average bitrate for support + Integer maxBitrate = (Integer)map.get(MediaFormat.KEY_MAX_BIT_RATE); + Integer bitrate = (Integer)map.get(MediaFormat.KEY_BIT_RATE); + if (bitrate == null) { + bitrate = maxBitrate; + } else if (maxBitrate != null) { + bitrate = Math.max(bitrate, maxBitrate); + } + + if (bitrate != null && bitrate > 0) { + return bitrateRange.contains(bitrate); } + + return true; } - } - /** @hide */ - public String[] validFeatures() { - Feature[] features = getValidFeatures(); - String[] res = new String[features.length]; - for (int i = 0; i < res.length; i++) { - if (!features[i].mInternal) { - res[i] = features[i].mName; + private boolean supportsProfileLevel(int profile, Integer level) { + for (CodecProfileLevel pl: mProfileLevels) { + if (pl.profile != profile) { + continue; + } + + // No specific level requested + if (level == null) { + return true; + } + + // AAC doesn't use levels + if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AAC)) { + return true; + } + + // DTS doesn't use levels + if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS) + || mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_HD) + || mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_UHD)) { + return true; + } + + // H.263 levels are not completely ordered: + // Level45 support only implies Level10 support + if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_H263)) { + if (pl.level != level && pl.level == CodecProfileLevel.H263Level45 + && level > CodecProfileLevel.H263Level10) { + continue; + } + } + + // MPEG4 levels are not completely ordered: + // Level1 support only implies Level0 (and not Level0b) support + if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_MPEG4)) { + if (pl.level != level && pl.level == CodecProfileLevel.MPEG4Level1 + && level > CodecProfileLevel.MPEG4Level0) { + continue; + } + } + + // HEVC levels incorporate both tiers and levels. Verify tier support. + if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_HEVC)) { + boolean supportsHighTier = + (pl.level & CodecProfileLevel.HEVCHighTierLevels) != 0; + boolean checkingHighTier + = (level & CodecProfileLevel.HEVCHighTierLevels) != 0; + // high tier levels are only supported by other high tier levels + if (checkingHighTier && !supportsHighTier) { + continue; + } + } + + if (pl.level >= level) { + // if we recognize the listed profile/level, we must also recognize the + // profile/level arguments. + if (createFromProfileLevel(mMime, profile, pl.level) != null) { + return createFromProfileLevel(mMime, profile, level) != null; + } + return true; + } } + return false; + } + + public MediaFormat getDefaultFormat() { + return mDefaultFormat; + } + + public String getMimeType() { + return mMime; + } + + public int getMaxSupportedInstances() { + return mMaxSupportedInstances; } - return res; - } - private Feature[] getValidFeatures() { - return FeatureList.getFeatures(isEncoder()); + private boolean isAudio() { + return mAudioCaps != null; + } + + public AudioCapabilities getAudioCapabilities() { + return mAudioCaps; + } + + private boolean isEncoder() { + return mEncoderCaps != null; + } + + public EncoderCapabilities getEncoderCapabilities() { + return mEncoderCaps; + } + + private boolean isVideo() { + return mVideoCaps != null; + } + + public VideoCapabilities getVideoCapabilities() { + return mVideoCaps; + } + + public static CodecCapsLegacyImpl createFromProfileLevel( + String mime, int profile, int level) { + CodecProfileLevel pl = new CodecProfileLevel(); + pl.profile = profile; + pl.level = level; + MediaFormat defaultFormat = new MediaFormat(); + defaultFormat.setString(MediaFormat.KEY_MIME, mime); + + CodecCapsLegacyImpl ret = new CodecCapsLegacyImpl( + new CodecProfileLevel[] { pl }, new int[0], true /* encoder */, + defaultFormat, new MediaFormat() /* info */); + if (ret.mError != 0) { + return null; + } + return ret; + } + + /* package private */ CodecCapsLegacyImpl( + CodecProfileLevel[] profLevs, int[] colFmts, + boolean encoder, + Map<String, Object>defaultFormatMap, + Map<String, Object>capabilitiesMap) { + this(profLevs, colFmts, encoder, + new MediaFormat(defaultFormatMap), + new MediaFormat(capabilitiesMap)); + } + + /* package private */ CodecCapsLegacyImpl( + CodecProfileLevel[] profLevs, int[] colFmts, boolean encoder, + MediaFormat defaultFormat, MediaFormat info) { + final Map<String, Object> map = info.getMap(); + mColorFormats = colFmts; + mFlagsVerified = 0; // TODO: remove as it is unused + mDefaultFormat = defaultFormat; + mCapabilitiesInfo = info; + mMime = mDefaultFormat.getString(MediaFormat.KEY_MIME); + + /* VP9 introduced profiles around 2016, so some VP9 codecs may not advertise any + supported profiles. Determine the level for them using the info they provide. */ + if (profLevs.length == 0 + && mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP9)) { + CodecProfileLevel profLev = new CodecProfileLevel(); + profLev.profile = CodecProfileLevel.VP9Profile0; + profLev.level = VideoCapabilities.VideoCapsLegacyImpl.equivalentVP9Level(info); + profLevs = new CodecProfileLevel[] { profLev }; + } + mProfileLevels = profLevs; + + if (mMime.toLowerCase().startsWith("audio/")) { + mAudioCaps = AudioCapabilities.create(info, this); + mAudioCaps.getDefaultFormat(mDefaultFormat); + } else if (mMime.toLowerCase().startsWith("video/") + || mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC)) { + mVideoCaps = VideoCapabilities.create(info, this); + } + if (encoder) { + mEncoderCaps = EncoderCapabilities.create(info, this); + mEncoderCaps.getDefaultFormat(mDefaultFormat); + } + + final Map<String, Object> global = MediaCodecList.getGlobalSettings(); + mMaxSupportedInstances = Utils.parseIntSafely( + global.get("max-concurrent-instances"), DEFAULT_MAX_SUPPORTED_INSTANCES); + + int maxInstances = Utils.parseIntSafely( + map.get("max-concurrent-instances"), mMaxSupportedInstances); + mMaxSupportedInstances = + Range.create(1, MAX_SUPPORTED_INSTANCES_LIMIT).clamp(maxInstances); + + for (Feature feat: getValidFeatures()) { + String key = MediaFormat.KEY_FEATURE_ + feat.mName; + Integer yesNo = (Integer)map.get(key); + if (yesNo == null) { + continue; + } + if (yesNo > 0) { + mFlagsRequired |= feat.mValue; + } + mFlagsSupported |= feat.mValue; + if (!feat.mInternal) { + mDefaultFormat.setInteger(key, 1); + } + // TODO restrict features by mFlagsVerified once all codecs reliably verify them + } + } } - private boolean checkFeature(String name, int flags) { - for (Feature feat: getValidFeatures()) { - if (feat.mName.equals(name)) { - return (flags & feat.mValue) != 0; + /* package private */ static final class CodecCapsNativeImpl implements CodecCapsIntf { + private long mNativeContext; // accessed by native methods + + private CodecProfileLevel[] mProfileLevels; + private int[] mColorFormats; + + private MediaFormat mDefaultFormat; + private AudioCapabilities mAudioCaps; + private VideoCapabilities mVideoCaps; + private EncoderCapabilities mEncoderCaps; + + public static CodecCapsNativeImpl createFromProfileLevel( + String mime, int profile, int level) { + return native_createFromProfileLevel(mime, profile, level); + } + + /** + * Constructor used by JNI. + * + * The Java CodecCapabilities object keeps these subobjects to avoid recontructing. + */ + /* package private */ CodecCapsNativeImpl(CodecProfileLevel[] profLevs, int[] colFmts, + MediaFormat defaultFormat, AudioCapabilities audioCaps, + VideoCapabilities videoCaps, EncoderCapabilities encoderCaps) { + mProfileLevels = profLevs; + mColorFormats = colFmts; + mDefaultFormat = defaultFormat; + mAudioCaps = audioCaps; + mVideoCaps = videoCaps; + mEncoderCaps = encoderCaps; + } + + public CodecCapsNativeImpl dup() { + CodecCapsNativeImpl impl = native_dup(); + return impl; + } + + @Override + protected void finalize() { + native_finalize(); + } + + public CodecProfileLevel[] getProfileLevels() { + return mProfileLevels; + } + + public int[] getColorFormats() { + return mColorFormats; + } + + public final boolean isFeatureSupported(String name) { + return native_isFeatureSupported(name); + } + + public final boolean isFeatureRequired(String name) { + return native_isFeatureRequired(name); + } + + public boolean isRegular() { + return native_isRegular(); + } + + public final boolean isFormatSupported(MediaFormat format) { + if (format == null) { + throw new NullPointerException(); + } + + Map<String, Object> formatMap = format.getMap(); + String[] keys = new String[formatMap.size()]; + Object[] values = new Object[formatMap.size()]; + + int i = 0; + for (Map.Entry<String, Object> entry: formatMap.entrySet()) { + keys[i] = entry.getKey(); + values[i] = entry.getValue(); + ++i; } + + return native_isFormatSupported(keys, values); } - return false; + + public MediaFormat getDefaultFormat() { + return mDefaultFormat; + } + + public String getMimeType() { + return native_getMimeType(); + } + + public int getMaxSupportedInstances() { + return native_getMaxSupportedInstances(); + } + + public AudioCapabilities getAudioCapabilities() { + return mAudioCaps; + } + + public EncoderCapabilities getEncoderCapabilities() { + return mEncoderCaps; + } + + public VideoCapabilities getVideoCapabilities() { + return mVideoCaps; + } + + private static native void native_init(); + private static native CodecCapsNativeImpl native_createFromProfileLevel( + String mime, int profile, int level); + private native CodecCapsNativeImpl native_dup(); + private native void native_finalize(); + private native int native_getMaxSupportedInstances(); + private native String native_getMimeType(); + private native final boolean native_isFeatureRequired(String name); + private native final boolean native_isFeatureSupported(String name); + private native final boolean native_isFormatSupported(@Nullable String[] keys, + @Nullable Object[] values); + private native boolean native_isRegular(); + + static { + System.loadLibrary("media_jni"); + native_init(); + } + } + + private CodecCapsIntf mImpl; + + /** + * Retrieve the codec capabilities for a certain {@code mime type}, {@code + * profile} and {@code level}. If the type, or profile-level combination + * is not understood by the framework, it returns null. + * <p class=note> In {@link android.os.Build.VERSION_CODES#M}, calling this + * method without calling any method of the {@link MediaCodecList} class beforehand + * results in a {@link NullPointerException}.</p> + */ + public static CodecCapabilities createFromProfileLevel( + String mime, int profile, int level) { + CodecCapsIntf impl; + if (GetFlag(() -> android.media.codec.Flags.nativeCapabilites())) { + impl = CodecCapsNativeImpl.createFromProfileLevel(mime, profile, level); + } else { + impl = CodecCapsLegacyImpl.createFromProfileLevel(mime, profile, level); + } + return new CodecCapabilities(impl); + } + + public CodecCapabilities() { + mImpl = new CodecCapsLegacyImpl(); + } + + /** package private */ CodecCapabilities(CodecCapsIntf impl) { + mImpl = impl; + profileLevels = mImpl.getProfileLevels(); + colorFormats = mImpl.getColorFormats(); + } + + /** @hide */ + public CodecCapabilities dup() { + CodecCapabilities caps = new CodecCapabilities(); + + // profileLevels and colorFormats may be modified by client. + caps.profileLevels = Arrays.copyOf(profileLevels, profileLevels.length); + caps.colorFormats = Arrays.copyOf(colorFormats, colorFormats.length); + + caps.mImpl = mImpl.dup(); + + return caps; + } + + /** + * Query codec feature capabilities. + * <p> + * These features are supported to be used by the codec. These + * include optional features that can be turned on, as well as + * features that are always on. + */ + public final boolean isFeatureSupported(String name) { + return mImpl.isFeatureSupported(name); + } + + /** + * Query codec feature requirements. + * <p> + * These features are required to be used by the codec, and as such, + * they are always turned on. + */ + public final boolean isFeatureRequired(String name) { + return mImpl.isFeatureRequired(name); } /** @hide */ public boolean isRegular() { - // regular codecs only require default features - for (Feature feat: getValidFeatures()) { - if (!feat.mDefault && isFeatureRequired(feat.mName)) { - return false; - } - } - return true; + return mImpl.isRegular(); } /** @@ -1047,384 +1594,573 @@ public final class MediaCodecInfo { * and feature requests. */ public final boolean isFormatSupported(MediaFormat format) { - final Map<String, Object> map = format.getMap(); - final String mime = (String)map.get(MediaFormat.KEY_MIME); + return mImpl.isFormatSupported(format); + } - // mime must match if present - if (mime != null && !mMime.equalsIgnoreCase(mime)) { - return false; + /** + * Returns a MediaFormat object with default values for configurations that have + * defaults. + */ + public MediaFormat getDefaultFormat() { + return mImpl.getDefaultFormat(); + } + + /** + * Returns the mime type for which this codec-capability object was created. + */ + public String getMimeType() { + return mImpl.getMimeType(); + } + + /** + * Returns the max number of the supported concurrent codec instances. + * <p> + * This is a hint for an upper bound. Applications should not expect to successfully + * operate more instances than the returned value, but the actual number of + * concurrently operable instances may be less as it depends on the available + * resources at time of use. + */ + public int getMaxSupportedInstances() { + return mImpl.getMaxSupportedInstances(); + } + + /** + * Returns the audio capabilities or {@code null} if this is not an audio codec. + */ + public AudioCapabilities getAudioCapabilities() { + return mImpl.getAudioCapabilities(); + } + + /** + * Returns the encoding capabilities or {@code null} if this is not an encoder. + */ + public EncoderCapabilities getEncoderCapabilities() { + return mImpl.getEncoderCapabilities(); + } + + /** + * Returns the video capabilities or {@code null} if this is not a video codec. + */ + public VideoCapabilities getVideoCapabilities() { + return mImpl.getVideoCapabilities(); + } + } + + /** + * A class that supports querying the audio capabilities of a codec. + */ + public static final class AudioCapabilities { + private static final String TAG = "AudioCapabilities"; + + /* package private */ interface AudioCapsIntf { + public Range<Integer> getBitrateRange(); + + public int[] getSupportedSampleRates(); + + public Range<Integer>[] getSupportedSampleRateRanges(); + + public int getMaxInputChannelCount(); + + public int getMinInputChannelCount(); + + public Range<Integer>[] getInputChannelCountRanges(); + + public boolean isSampleRateSupported(int sampleRate); + + public void getDefaultFormat(MediaFormat format); + + public boolean supportsFormat(MediaFormat format); + } + + /* package private */ static final class AudioCapsLegacyImpl implements AudioCapsIntf { + private CodecCapabilities.CodecCapsLegacyImpl mParent; + private Range<Integer> mBitrateRange; + + private int[] mSampleRates; + private Range<Integer>[] mSampleRateRanges; + private Range<Integer>[] mInputChannelRanges; + + private static final int MAX_INPUT_CHANNEL_COUNT = 30; + + public Range<Integer> getBitrateRange() { + return mBitrateRange; } - // check feature support - for (Feature feat: getValidFeatures()) { - if (feat.mInternal) { - continue; - } + public int[] getSupportedSampleRates() { + return mSampleRates != null ? Arrays.copyOf(mSampleRates, mSampleRates.length) + : null; + } + + public Range<Integer>[] getSupportedSampleRateRanges() { + return Arrays.copyOf(mSampleRateRanges, mSampleRateRanges.length); + } - Integer yesNo = (Integer)map.get(MediaFormat.KEY_FEATURE_ + feat.mName); - if (yesNo == null) { - continue; + public int getMaxInputChannelCount() { + int overall_max = 0; + for (int i = mInputChannelRanges.length - 1; i >= 0; i--) { + int lmax = mInputChannelRanges[i].getUpper(); + if (lmax > overall_max) { + overall_max = lmax; + } } - if ((yesNo == 1 && !isFeatureSupported(feat.mName)) || - (yesNo == 0 && isFeatureRequired(feat.mName))) { - return false; + return overall_max; + } + + public int getMinInputChannelCount() { + int overall_min = MAX_INPUT_CHANNEL_COUNT; + for (int i = mInputChannelRanges.length - 1; i >= 0; i--) { + int lmin = mInputChannelRanges[i].getLower(); + if (lmin < overall_min) { + overall_min = lmin; + } } + return overall_min; } - Integer profile = (Integer)map.get(MediaFormat.KEY_PROFILE); - Integer level = (Integer)map.get(MediaFormat.KEY_LEVEL); + public Range<Integer>[] getInputChannelCountRanges() { + return Arrays.copyOf(mInputChannelRanges, mInputChannelRanges.length); + } - if (profile != null) { - if (!supportsProfileLevel(profile, level)) { - return false; + /* no public constructor */ + private AudioCapsLegacyImpl() { } + + public static AudioCapsLegacyImpl create( + MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + if (GetFlag(() -> android.media.codec.Flags.nativeCapabilites())) { + Log.d(TAG, "Legacy implementation is called while native flag is on."); } - // If we recognize this profile, check that this format is supported by the - // highest level supported by the codec for that profile. (Ignore specified - // level beyond the above profile/level check as level is only used as a - // guidance. E.g. AVC Level 1 CIF format is supported if codec supports level 1.1 - // even though max size for Level 1 is QCIF. However, MPEG2 Simple Profile - // 1080p format is not supported even if codec supports Main Profile Level High, - // as Simple Profile does not support 1080p. - CodecCapabilities levelCaps = null; - int maxLevel = 0; - for (CodecProfileLevel pl : profileLevels) { - if (pl.profile == profile && pl.level > maxLevel) { - // H.263 levels are not completely ordered: - // Level45 support only implies Level10 support - if (!mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_H263) - || pl.level != CodecProfileLevel.H263Level45 - || maxLevel == CodecProfileLevel.H263Level10) { - maxLevel = pl.level; - } + AudioCapsLegacyImpl caps = new AudioCapsLegacyImpl(); + caps.init(info, parent); + return caps; + } + + private void init(MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + mParent = parent; + initWithPlatformLimits(); + applyLevelLimits(); + parseFromInfo(info); + } + + private void initWithPlatformLimits() { + mBitrateRange = Range.create(0, Integer.MAX_VALUE); + mInputChannelRanges = new Range[] {Range.create(1, MAX_INPUT_CHANNEL_COUNT)}; + // mBitrateRange = Range.create(1, 320000); + final int minSampleRate = SystemProperties. + getInt("ro.mediacodec.min_sample_rate", 7350); + final int maxSampleRate = SystemProperties. + getInt("ro.mediacodec.max_sample_rate", 192000); + mSampleRateRanges = new Range[] { Range.create(minSampleRate, maxSampleRate) }; + mSampleRates = null; + } + + private boolean supports(Integer sampleRate, Integer inputChannels) { + // channels and sample rates are checked orthogonally + if (inputChannels != null) { + int ix = Utils.binarySearchDistinctRanges( + mInputChannelRanges, inputChannels); + if (ix < 0) { + return false; } } - levelCaps = createFromProfileLevel(mMime, profile, maxLevel); - // We must remove the profile from this format otherwise levelCaps.isFormatSupported - // will get into this same condition and loop forever. Furthermore, since levelCaps - // does not contain features and bitrate specific keys, keep only keys relevant for - // a level check. - Map<String, Object> levelCriticalFormatMap = new HashMap<>(map); - final Set<String> criticalKeys = - isVideo() ? VideoCapabilities.VIDEO_LEVEL_CRITICAL_FORMAT_KEYS : - isAudio() ? AudioCapabilities.AUDIO_LEVEL_CRITICAL_FORMAT_KEYS : - null; - - // critical keys will always contain KEY_MIME, but should also contain others to be - // meaningful - if (criticalKeys != null && criticalKeys.size() > 1 && levelCaps != null) { - levelCriticalFormatMap.keySet().retainAll(criticalKeys); - - MediaFormat levelCriticalFormat = new MediaFormat(levelCriticalFormatMap); - if (!levelCaps.isFormatSupported(levelCriticalFormat)) { + if (sampleRate != null) { + int ix = Utils.binarySearchDistinctRanges( + mSampleRateRanges, sampleRate); + if (ix < 0) { return false; } } + return true; } - if (mAudioCaps != null && !mAudioCaps.supportsFormat(format)) { - return false; - } - if (mVideoCaps != null && !mVideoCaps.supportsFormat(format)) { - return false; - } - if (mEncoderCaps != null && !mEncoderCaps.supportsFormat(format)) { - return false; - } - return true; - } - private static boolean supportsBitrate( - Range<Integer> bitrateRange, MediaFormat format) { - Map<String, Object> map = format.getMap(); + public boolean isSampleRateSupported(int sampleRate) { + return supports(sampleRate, null); + } - // consider max bitrate over average bitrate for support - Integer maxBitrate = (Integer)map.get(MediaFormat.KEY_MAX_BIT_RATE); - Integer bitrate = (Integer)map.get(MediaFormat.KEY_BIT_RATE); - if (bitrate == null) { - bitrate = maxBitrate; - } else if (maxBitrate != null) { - bitrate = Math.max(bitrate, maxBitrate); + /** modifies rates */ + private void limitSampleRates(int[] rates) { + Arrays.sort(rates); + ArrayList<Range<Integer>> ranges = new ArrayList<Range<Integer>>(); + for (int rate: rates) { + if (supports(rate, null /* channels */)) { + ranges.add(Range.create(rate, rate)); + } + } + mSampleRateRanges = ranges.toArray(new Range[ranges.size()]); + createDiscreteSampleRates(); } - if (bitrate != null && bitrate > 0) { - return bitrateRange.contains(bitrate); + private void createDiscreteSampleRates() { + mSampleRates = new int[mSampleRateRanges.length]; + for (int i = 0; i < mSampleRateRanges.length; i++) { + mSampleRates[i] = mSampleRateRanges[i].getLower(); + } } - return true; - } + /** modifies rateRanges */ + private void limitSampleRates(Range<Integer>[] rateRanges) { + sortDistinctRanges(rateRanges); + mSampleRateRanges = intersectSortedDistinctRanges(mSampleRateRanges, rateRanges); - private boolean supportsProfileLevel(int profile, Integer level) { - for (CodecProfileLevel pl: profileLevels) { - if (pl.profile != profile) { - continue; + // check if all values are discrete + for (Range<Integer> range: mSampleRateRanges) { + if (!range.getLower().equals(range.getUpper())) { + mSampleRates = null; + return; + } } + createDiscreteSampleRates(); + } - // No specific level requested - if (level == null) { - return true; + private void applyLevelLimits() { + int[] sampleRates = null; + Range<Integer> sampleRateRange = null, bitRates = null; + int maxChannels = MAX_INPUT_CHANNEL_COUNT; + CodecProfileLevel[] profileLevels = mParent.getProfileLevels(); + String mime = mParent.getMimeType(); + + if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_MPEG)) { + sampleRates = new int[] { + 8000, 11025, 12000, + 16000, 22050, 24000, + 32000, 44100, 48000 }; + bitRates = Range.create(8000, 320000); + maxChannels = 2; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_NB)) { + sampleRates = new int[] { 8000 }; + bitRates = Range.create(4750, 12200); + maxChannels = 1; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_WB)) { + sampleRates = new int[] { 16000 }; + bitRates = Range.create(6600, 23850); + maxChannels = 1; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AAC)) { + sampleRates = new int[] { + 7350, 8000, + 11025, 12000, 16000, + 22050, 24000, 32000, + 44100, 48000, 64000, + 88200, 96000 }; + bitRates = Range.create(8000, 510000); + maxChannels = 48; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_VORBIS)) { + bitRates = Range.create(32000, 500000); + sampleRateRange = Range.create(8000, 192000); + maxChannels = 255; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_OPUS)) { + bitRates = Range.create(6000, 510000); + sampleRates = new int[] { 8000, 12000, 16000, 24000, 48000 }; + maxChannels = 255; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_RAW)) { + sampleRateRange = Range.create(1, 192000); + bitRates = Range.create(1, 10000000); + maxChannels = AudioSystem.OUT_CHANNEL_COUNT_MAX; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_FLAC)) { + sampleRateRange = Range.create(1, 655350); + // lossless codec, so bitrate is ignored + maxChannels = 255; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_ALAW) + || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_MLAW)) { + sampleRates = new int[] { 8000 }; + bitRates = Range.create(64000, 64000); + // platform allows multiple channels for this format + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_MSGSM)) { + sampleRates = new int[] { 8000 }; + bitRates = Range.create(13000, 13000); + maxChannels = 1; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AC3)) { + maxChannels = 6; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_EAC3)) { + maxChannels = 16; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_EAC3_JOC)) { + sampleRates = new int[] { 48000 }; + bitRates = Range.create(32000, 6144000); + maxChannels = 16; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AC4)) { + sampleRates = new int[] { 44100, 48000, 96000, 192000 }; + bitRates = Range.create(16000, 2688000); + maxChannels = 24; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS)) { + sampleRates = new int[] { 44100, 48000 }; + bitRates = Range.create(96000, 1524000); + maxChannels = 6; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_HD)) { + for (CodecProfileLevel profileLevel: profileLevels) { + switch (profileLevel.profile) { + case CodecProfileLevel.DTS_HDProfileLBR: + sampleRates = new int[]{ 22050, 24000, 44100, 48000 }; + bitRates = Range.create(32000, 768000); + break; + case CodecProfileLevel.DTS_HDProfileHRA: + case CodecProfileLevel.DTS_HDProfileMA: + sampleRates + = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; + bitRates = Range.create(96000, 24500000); + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + mParent.mError |= ERROR_UNRECOGNIZED; + sampleRates + = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; + bitRates = Range.create(96000, 24500000); + } + } + maxChannels = 8; + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_UHD)) { + for (CodecProfileLevel profileLevel: profileLevels) { + switch (profileLevel.profile) { + case CodecProfileLevel.DTS_UHDProfileP2: + sampleRates = new int[]{ 48000 }; + bitRates = Range.create(96000, 768000); + maxChannels = 10; + break; + case CodecProfileLevel.DTS_UHDProfileP1: + sampleRates + = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; + bitRates = Range.create(96000, 24500000); + maxChannels = 32; + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + mParent.mError |= ERROR_UNRECOGNIZED; + sampleRates + = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; + bitRates = Range.create(96000, 24500000); + maxChannels = 32; + } + } + } else { + Log.w(TAG, "Unsupported mime " + mime); + mParent.mError |= ERROR_UNSUPPORTED; } - // AAC doesn't use levels - if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AAC)) { - return true; + // restrict ranges + if (sampleRates != null) { + limitSampleRates(sampleRates); + } else if (sampleRateRange != null) { + limitSampleRates(new Range[] { sampleRateRange }); } - // DTS doesn't use levels - if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS) - || mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_HD) - || mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_UHD)) { - return true; + Range<Integer> channelRange = Range.create(1, maxChannels); + + applyLimits(new Range[] { channelRange }, bitRates); + } + + private void applyLimits(Range<Integer>[] inputChannels, Range<Integer> bitRates) { + + // clamp & make a local copy + Range<Integer>[] myInputChannels = new Range[inputChannels.length]; + for (int i = 0; i < inputChannels.length; i++) { + int lower = inputChannels[i].clamp(1); + int upper = inputChannels[i].clamp(MAX_INPUT_CHANNEL_COUNT); + myInputChannels[i] = Range.create(lower, upper); } - // H.263 levels are not completely ordered: - // Level45 support only implies Level10 support - if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_H263)) { - if (pl.level != level && pl.level == CodecProfileLevel.H263Level45 - && level > CodecProfileLevel.H263Level10) { - continue; - } + // sort, intersect with existing, & save channel list + sortDistinctRanges(myInputChannels); + Range<Integer>[] joinedChannelList = + intersectSortedDistinctRanges(myInputChannels, mInputChannelRanges); + mInputChannelRanges = joinedChannelList; + + if (bitRates != null) { + mBitrateRange = mBitrateRange.intersect(bitRates); } + } - // MPEG4 levels are not completely ordered: - // Level1 support only implies Level0 (and not Level0b) support - if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_MPEG4)) { - if (pl.level != level && pl.level == CodecProfileLevel.MPEG4Level1 - && level > CodecProfileLevel.MPEG4Level0) { - continue; + private void parseFromInfo(MediaFormat info) { + int maxInputChannels = MAX_INPUT_CHANNEL_COUNT; + Range<Integer>[] channels = new Range[] { Range.create(1, maxInputChannels)}; + Range<Integer> bitRates = POSITIVE_INTEGERS; + + if (info.containsKey("sample-rate-ranges")) { + String[] rateStrings = info.getString("sample-rate-ranges").split(","); + Range<Integer>[] rateRanges = new Range[rateStrings.length]; + for (int i = 0; i < rateStrings.length; i++) { + rateRanges[i] = Utils.parseIntRange(rateStrings[i], null); } + limitSampleRates(rateRanges); } - // HEVC levels incorporate both tiers and levels. Verify tier support. - if (mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_HEVC)) { - boolean supportsHighTier = - (pl.level & CodecProfileLevel.HEVCHighTierLevels) != 0; - boolean checkingHighTier = (level & CodecProfileLevel.HEVCHighTierLevels) != 0; - // high tier levels are only supported by other high tier levels - if (checkingHighTier && !supportsHighTier) { - continue; + // we will prefer channel-ranges over max-channel-count + if (info.containsKey("channel-ranges")) { + String[] channelStrings = info.getString("channel-ranges").split(","); + Range<Integer>[] channelRanges = new Range[channelStrings.length]; + for (int i = 0; i < channelStrings.length; i++) { + channelRanges[i] = Utils.parseIntRange(channelStrings[i], null); + } + channels = channelRanges; + } else if (info.containsKey("channel-range")) { + Range<Integer> oneRange = Utils.parseIntRange(info.getString("channel-range"), + null); + channels = new Range[] { oneRange }; + } else if (info.containsKey("max-channel-count")) { + maxInputChannels = Utils.parseIntSafely( + info.getString("max-channel-count"), maxInputChannels); + if (maxInputChannels == 0) { + channels = new Range[] {Range.create(0, 0)}; + } else { + channels = new Range[] {Range.create(1, maxInputChannels)}; } + } else if ((mParent.mError & ERROR_UNSUPPORTED) != 0) { + maxInputChannels = 0; + channels = new Range[] {Range.create(0, 0)}; } - if (pl.level >= level) { - // if we recognize the listed profile/level, we must also recognize the - // profile/level arguments. - if (createFromProfileLevel(mMime, profile, pl.level) != null) { - return createFromProfileLevel(mMime, profile, level) != null; - } - return true; + if (info.containsKey("bitrate-range")) { + bitRates = bitRates.intersect( + Utils.parseIntRange(info.getString("bitrate-range"), bitRates)); } + + applyLimits(channels, bitRates); } - return false; - } - // errors while reading profile levels - accessed from sister capabilities - int mError; + /** @hide */ + public void getDefaultFormat(MediaFormat format) { + // report settings that have only a single choice + if (mBitrateRange.getLower().equals(mBitrateRange.getUpper())) { + format.setInteger(MediaFormat.KEY_BIT_RATE, mBitrateRange.getLower()); + } + if (getMaxInputChannelCount() == 1) { + // mono-only format + format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); + } + if (mSampleRates != null && mSampleRates.length == 1) { + format.setInteger(MediaFormat.KEY_SAMPLE_RATE, mSampleRates[0]); + } + } - private static final String TAG = "CodecCapabilities"; + /* package private */ + // must not contain KEY_PROFILE + static final Set<String> AUDIO_LEVEL_CRITICAL_FORMAT_KEYS = Set.of( + // We don't set level-specific limits for audio codecs today. Key candidates + // would be sample rate, bit rate or channel count. + // MediaFormat.KEY_SAMPLE_RATE, + // MediaFormat.KEY_CHANNEL_COUNT, + // MediaFormat.KEY_BIT_RATE, + MediaFormat.KEY_MIME); + + /** @hide */ + public boolean supportsFormat(MediaFormat format) { + Map<String, Object> map = format.getMap(); + Integer sampleRate = (Integer)map.get(MediaFormat.KEY_SAMPLE_RATE); + Integer channels = (Integer)map.get(MediaFormat.KEY_CHANNEL_COUNT); + + if (!supports(sampleRate, channels)) { + return false; + } - // NEW-STYLE CAPABILITIES - private AudioCapabilities mAudioCaps; - private VideoCapabilities mVideoCaps; - private EncoderCapabilities mEncoderCaps; - private MediaFormat mDefaultFormat; + if (!CodecCapabilities.CodecCapsLegacyImpl.supportsBitrate(mBitrateRange, format)) { + return false; + } - /** - * Returns a MediaFormat object with default values for configurations that have - * defaults. - */ - public MediaFormat getDefaultFormat() { - return mDefaultFormat; + // nothing to do for: + // KEY_CHANNEL_MASK: codecs don't get this + // KEY_IS_ADTS: required feature for all AAC decoders + return true; + } } - /** - * Returns the mime type for which this codec-capability object was created. - */ - public String getMimeType() { - return mMime; - } + /* package private */ static final class AudioCapsNativeImpl implements AudioCapsIntf { + private long mNativeContext; // accessed by native methods - /** - * Returns the max number of the supported concurrent codec instances. - * <p> - * This is a hint for an upper bound. Applications should not expect to successfully - * operate more instances than the returned value, but the actual number of - * concurrently operable instances may be less as it depends on the available - * resources at time of use. - */ - public int getMaxSupportedInstances() { - return mMaxSupportedInstances; - } + private Range<Integer> mBitrateRange; + private int[] mSampleRates; + private Range<Integer>[] mSampleRateRanges; + private Range<Integer>[] mInputChannelRanges; - private boolean isAudio() { - return mAudioCaps != null; - } + /** + * Constructor used by JNI. + * + * The Java AudioCapabilities object keeps these subobjects to avoid recontruction. + */ + /* package private */ AudioCapsNativeImpl(Range<Integer> bitrateRange, + int[] sampleRates, Range<Integer>[] sampleRateRanges, + Range<Integer>[] inputChannelRanges) { + mBitrateRange = bitrateRange; + mSampleRates = sampleRates; + mSampleRateRanges = sampleRateRanges; + mInputChannelRanges = inputChannelRanges; + } - /** - * Returns the audio capabilities or {@code null} if this is not an audio codec. - */ - public AudioCapabilities getAudioCapabilities() { - return mAudioCaps; - } + /* no public constructor */ + private AudioCapsNativeImpl() { } - private boolean isEncoder() { - return mEncoderCaps != null; - } + public Range<Integer> getBitrateRange() { + return mBitrateRange; + } - /** - * Returns the encoding capabilities or {@code null} if this is not an encoder. - */ - public EncoderCapabilities getEncoderCapabilities() { - return mEncoderCaps; - } + public int[] getSupportedSampleRates() { + return mSampleRates != null ? Arrays.copyOf(mSampleRates, mSampleRates.length) + : null; + } - private boolean isVideo() { - return mVideoCaps != null; - } + public Range<Integer>[] getSupportedSampleRateRanges() { + return Arrays.copyOf(mSampleRateRanges, mSampleRateRanges.length); + } - /** - * Returns the video capabilities or {@code null} if this is not a video codec. - */ - public VideoCapabilities getVideoCapabilities() { - return mVideoCaps; - } + public Range<Integer>[] getInputChannelCountRanges() { + return Arrays.copyOf(mInputChannelRanges, mInputChannelRanges.length); + } - /** @hide */ - public CodecCapabilities dup() { - CodecCapabilities caps = new CodecCapabilities(); + public int getMaxInputChannelCount() { + return native_getMaxInputChannelCount(); + } - // profileLevels and colorFormats may be modified by client. - caps.profileLevels = Arrays.copyOf(profileLevels, profileLevels.length); - caps.colorFormats = Arrays.copyOf(colorFormats, colorFormats.length); + public int getMinInputChannelCount() { + return native_getMinInputChannelCount(); + } - caps.mMime = mMime; - caps.mMaxSupportedInstances = mMaxSupportedInstances; - caps.mFlagsRequired = mFlagsRequired; - caps.mFlagsSupported = mFlagsSupported; - caps.mFlagsVerified = mFlagsVerified; - caps.mAudioCaps = mAudioCaps; - caps.mVideoCaps = mVideoCaps; - caps.mEncoderCaps = mEncoderCaps; - caps.mDefaultFormat = mDefaultFormat; - caps.mCapabilitiesInfo = mCapabilitiesInfo; + public boolean isSampleRateSupported(int sampleRate) { + return native_isSampleRateSupported(sampleRate); + } - return caps; - } + // This API is for internal Java implementation only. Should not be called. + public void getDefaultFormat(MediaFormat format) { + throw new UnsupportedOperationException( + "Java Implementation should not call native implemenatation"); + } - /** - * Retrieve the codec capabilities for a certain {@code mime type}, {@code - * profile} and {@code level}. If the type, or profile-level combination - * is not understood by the framework, it returns null. - * <p class=note> In {@link android.os.Build.VERSION_CODES#M}, calling this - * method without calling any method of the {@link MediaCodecList} class beforehand - * results in a {@link NullPointerException}.</p> - */ - public static CodecCapabilities createFromProfileLevel( - String mime, int profile, int level) { - CodecProfileLevel pl = new CodecProfileLevel(); - pl.profile = profile; - pl.level = level; - MediaFormat defaultFormat = new MediaFormat(); - defaultFormat.setString(MediaFormat.KEY_MIME, mime); - - CodecCapabilities ret = new CodecCapabilities( - new CodecProfileLevel[] { pl }, new int[0], true /* encoder */, - defaultFormat, new MediaFormat() /* info */); - if (ret.mError != 0) { - return null; + // This API is for internal Java implementation only. Should not be called. + public boolean supportsFormat(MediaFormat format) { + throw new UnsupportedOperationException( + "Java Implementation should not call native implemenatation"); } - return ret; - } - /* package private */ CodecCapabilities( - CodecProfileLevel[] profLevs, int[] colFmts, - boolean encoder, - Map<String, Object>defaultFormatMap, - Map<String, Object>capabilitiesMap) { - this(profLevs, colFmts, encoder, - new MediaFormat(defaultFormatMap), - new MediaFormat(capabilitiesMap)); - } + private native int native_getMaxInputChannelCount(); + private native int native_getMinInputChannelCount(); + private native boolean native_isSampleRateSupported(int sampleRate); + private static native void native_init(); - private MediaFormat mCapabilitiesInfo; - - /* package private */ CodecCapabilities( - CodecProfileLevel[] profLevs, int[] colFmts, boolean encoder, - MediaFormat defaultFormat, MediaFormat info) { - final Map<String, Object> map = info.getMap(); - colorFormats = colFmts; - mFlagsVerified = 0; // TODO: remove as it is unused - mDefaultFormat = defaultFormat; - mCapabilitiesInfo = info; - mMime = mDefaultFormat.getString(MediaFormat.KEY_MIME); - - /* VP9 introduced profiles around 2016, so some VP9 codecs may not advertise any - supported profiles. Determine the level for them using the info they provide. */ - if (profLevs.length == 0 && mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP9)) { - CodecProfileLevel profLev = new CodecProfileLevel(); - profLev.profile = CodecProfileLevel.VP9Profile0; - profLev.level = VideoCapabilities.equivalentVP9Level(info); - profLevs = new CodecProfileLevel[] { profLev }; - } - profileLevels = profLevs; - - if (mMime.toLowerCase().startsWith("audio/")) { - mAudioCaps = AudioCapabilities.create(info, this); - mAudioCaps.getDefaultFormat(mDefaultFormat); - } else if (mMime.toLowerCase().startsWith("video/") - || mMime.equalsIgnoreCase(MediaFormat.MIMETYPE_IMAGE_ANDROID_HEIC)) { - mVideoCaps = VideoCapabilities.create(info, this); - } - if (encoder) { - mEncoderCaps = EncoderCapabilities.create(info, this); - mEncoderCaps.getDefaultFormat(mDefaultFormat); - } - - final Map<String, Object> global = MediaCodecList.getGlobalSettings(); - mMaxSupportedInstances = Utils.parseIntSafely( - global.get("max-concurrent-instances"), DEFAULT_MAX_SUPPORTED_INSTANCES); - - int maxInstances = Utils.parseIntSafely( - map.get("max-concurrent-instances"), mMaxSupportedInstances); - mMaxSupportedInstances = - Range.create(1, MAX_SUPPORTED_INSTANCES_LIMIT).clamp(maxInstances); - - for (Feature feat: getValidFeatures()) { - String key = MediaFormat.KEY_FEATURE_ + feat.mName; - Integer yesNo = (Integer)map.get(key); - if (yesNo == null) { - continue; - } - if (yesNo > 0) { - mFlagsRequired |= feat.mValue; - } - mFlagsSupported |= feat.mValue; - if (!feat.mInternal) { - mDefaultFormat.setInteger(key, 1); - } - // TODO restrict features by mFlagsVerified once all codecs reliably verify them + static { + System.loadLibrary("media_jni"); + native_init(); } } - } - /** - * A class that supports querying the audio capabilities of a codec. - */ - public static final class AudioCapabilities { - private static final String TAG = "AudioCapabilities"; - private CodecCapabilities mParent; - private Range<Integer> mBitrateRange; + private AudioCapsIntf mImpl; + + /** @hide */ + public static AudioCapabilities create( + MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + AudioCapsLegacyImpl impl = AudioCapsLegacyImpl.create(info, parent); + AudioCapabilities caps = new AudioCapabilities(impl); + return caps; + } - private int[] mSampleRates; - private Range<Integer>[] mSampleRateRanges; - private Range<Integer>[] mInputChannelRanges; + /* package private */ AudioCapabilities(AudioCapsIntf impl) { + mImpl = impl; + } - private static final int MAX_INPUT_CHANNEL_COUNT = 30; + /* no public constructor */ + private AudioCapabilities() { } /** * Returns the range of supported bitrates in bits/second. */ public Range<Integer> getBitrateRange() { - return mBitrateRange; + return mImpl.getBitrateRange(); } /** @@ -1433,7 +2169,7 @@ public final class MediaCodecInfo { * {@code null}. The array is sorted in ascending order. */ public int[] getSupportedSampleRates() { - return mSampleRates != null ? Arrays.copyOf(mSampleRates, mSampleRates.length) : null; + return mImpl.getSupportedSampleRates(); } /** @@ -1442,7 +2178,21 @@ public final class MediaCodecInfo { * distinct. */ public Range<Integer>[] getSupportedSampleRateRanges() { - return Arrays.copyOf(mSampleRateRanges, mSampleRateRanges.length); + return mImpl.getSupportedSampleRateRanges(); + } + + /* + * Returns an array of ranges representing the number of input channels supported. + * The codec supports any number of input channels within this range. + * + * This supersedes the {@link #getMaxInputChannelCount} method. + * + * For many codecs, this will be a single range [1..N], for some N. + */ + @SuppressLint("ArrayReturn") + @NonNull + public Range<Integer>[] getInputChannelCountRanges() { + return mImpl.getInputChannelCountRanges(); } /** @@ -1462,14 +2212,7 @@ public final class MediaCodecInfo { */ @IntRange(from = 1, to = 255) public int getMaxInputChannelCount() { - int overall_max = 0; - for (int i = mInputChannelRanges.length - 1; i >= 0; i--) { - int lmax = mInputChannelRanges[i].getUpper(); - if (lmax > overall_max) { - overall_max = lmax; - } - } - return overall_max; + return mImpl.getMaxInputChannelCount(); } /** @@ -1481,364 +2224,24 @@ public final class MediaCodecInfo { */ @IntRange(from = 1, to = 255) public int getMinInputChannelCount() { - int overall_min = MAX_INPUT_CHANNEL_COUNT; - for (int i = mInputChannelRanges.length - 1; i >= 0; i--) { - int lmin = mInputChannelRanges[i].getLower(); - if (lmin < overall_min) { - overall_min = lmin; - } - } - return overall_min; - } - - /* - * Returns an array of ranges representing the number of input channels supported. - * The codec supports any number of input channels within this range. - * - * This supersedes the {@link #getMaxInputChannelCount} method. - * - * For many codecs, this will be a single range [1..N], for some N. - */ - @SuppressLint("ArrayReturn") - @NonNull - public Range<Integer>[] getInputChannelCountRanges() { - return Arrays.copyOf(mInputChannelRanges, mInputChannelRanges.length); - } - - /* no public constructor */ - private AudioCapabilities() { } - - /** @hide */ - public static AudioCapabilities create( - MediaFormat info, CodecCapabilities parent) { - AudioCapabilities caps = new AudioCapabilities(); - caps.init(info, parent); - return caps; - } - - private void init(MediaFormat info, CodecCapabilities parent) { - mParent = parent; - initWithPlatformLimits(); - applyLevelLimits(); - parseFromInfo(info); - } - - private void initWithPlatformLimits() { - mBitrateRange = Range.create(0, Integer.MAX_VALUE); - mInputChannelRanges = new Range[] {Range.create(1, MAX_INPUT_CHANNEL_COUNT)}; - // mBitrateRange = Range.create(1, 320000); - final int minSampleRate = SystemProperties. - getInt("ro.mediacodec.min_sample_rate", 7350); - final int maxSampleRate = SystemProperties. - getInt("ro.mediacodec.max_sample_rate", 192000); - mSampleRateRanges = new Range[] { Range.create(minSampleRate, maxSampleRate) }; - mSampleRates = null; - } - - private boolean supports(Integer sampleRate, Integer inputChannels) { - // channels and sample rates are checked orthogonally - if (inputChannels != null) { - int ix = Utils.binarySearchDistinctRanges( - mInputChannelRanges, inputChannels); - if (ix < 0) { - return false; - } - } - if (sampleRate != null) { - int ix = Utils.binarySearchDistinctRanges( - mSampleRateRanges, sampleRate); - if (ix < 0) { - return false; - } - } - return true; + return mImpl.getMinInputChannelCount(); } /** * Query whether the sample rate is supported by the codec. */ public boolean isSampleRateSupported(int sampleRate) { - return supports(sampleRate, null); - } - - /** modifies rates */ - private void limitSampleRates(int[] rates) { - Arrays.sort(rates); - ArrayList<Range<Integer>> ranges = new ArrayList<Range<Integer>>(); - for (int rate: rates) { - if (supports(rate, null /* channels */)) { - ranges.add(Range.create(rate, rate)); - } - } - mSampleRateRanges = ranges.toArray(new Range[ranges.size()]); - createDiscreteSampleRates(); - } - - private void createDiscreteSampleRates() { - mSampleRates = new int[mSampleRateRanges.length]; - for (int i = 0; i < mSampleRateRanges.length; i++) { - mSampleRates[i] = mSampleRateRanges[i].getLower(); - } - } - - /** modifies rateRanges */ - private void limitSampleRates(Range<Integer>[] rateRanges) { - sortDistinctRanges(rateRanges); - mSampleRateRanges = intersectSortedDistinctRanges(mSampleRateRanges, rateRanges); - - // check if all values are discrete - for (Range<Integer> range: mSampleRateRanges) { - if (!range.getLower().equals(range.getUpper())) { - mSampleRates = null; - return; - } - } - createDiscreteSampleRates(); - } - - private void applyLevelLimits() { - int[] sampleRates = null; - Range<Integer> sampleRateRange = null, bitRates = null; - int maxChannels = MAX_INPUT_CHANNEL_COUNT; - CodecProfileLevel[] profileLevels = mParent.profileLevels; - String mime = mParent.getMimeType(); - - if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_MPEG)) { - sampleRates = new int[] { - 8000, 11025, 12000, - 16000, 22050, 24000, - 32000, 44100, 48000 }; - bitRates = Range.create(8000, 320000); - maxChannels = 2; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_NB)) { - sampleRates = new int[] { 8000 }; - bitRates = Range.create(4750, 12200); - maxChannels = 1; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_WB)) { - sampleRates = new int[] { 16000 }; - bitRates = Range.create(6600, 23850); - maxChannels = 1; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AAC)) { - sampleRates = new int[] { - 7350, 8000, - 11025, 12000, 16000, - 22050, 24000, 32000, - 44100, 48000, 64000, - 88200, 96000 }; - bitRates = Range.create(8000, 510000); - maxChannels = 48; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_VORBIS)) { - bitRates = Range.create(32000, 500000); - sampleRateRange = Range.create(8000, 192000); - maxChannels = 255; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_OPUS)) { - bitRates = Range.create(6000, 510000); - sampleRates = new int[] { 8000, 12000, 16000, 24000, 48000 }; - maxChannels = 255; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_RAW)) { - sampleRateRange = Range.create(1, 192000); - bitRates = Range.create(1, 10000000); - maxChannels = AudioSystem.OUT_CHANNEL_COUNT_MAX; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_FLAC)) { - sampleRateRange = Range.create(1, 655350); - // lossless codec, so bitrate is ignored - maxChannels = 255; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_ALAW) - || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_MLAW)) { - sampleRates = new int[] { 8000 }; - bitRates = Range.create(64000, 64000); - // platform allows multiple channels for this format - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_MSGSM)) { - sampleRates = new int[] { 8000 }; - bitRates = Range.create(13000, 13000); - maxChannels = 1; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AC3)) { - maxChannels = 6; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_EAC3)) { - maxChannels = 16; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_EAC3_JOC)) { - sampleRates = new int[] { 48000 }; - bitRates = Range.create(32000, 6144000); - maxChannels = 16; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AC4)) { - sampleRates = new int[] { 44100, 48000, 96000, 192000 }; - bitRates = Range.create(16000, 2688000); - maxChannels = 24; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS)) { - sampleRates = new int[] { 44100, 48000 }; - bitRates = Range.create(96000, 1524000); - maxChannels = 6; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_HD)) { - for (CodecProfileLevel profileLevel: profileLevels) { - switch (profileLevel.profile) { - case CodecProfileLevel.DTS_HDProfileLBR: - sampleRates = new int[]{ 22050, 24000, 44100, 48000 }; - bitRates = Range.create(32000, 768000); - break; - case CodecProfileLevel.DTS_HDProfileHRA: - case CodecProfileLevel.DTS_HDProfileMA: - sampleRates = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; - bitRates = Range.create(96000, 24500000); - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - mParent.mError |= ERROR_UNRECOGNIZED; - sampleRates = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; - bitRates = Range.create(96000, 24500000); - } - } - maxChannels = 8; - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_DTS_UHD)) { - for (CodecProfileLevel profileLevel: profileLevels) { - switch (profileLevel.profile) { - case CodecProfileLevel.DTS_UHDProfileP2: - sampleRates = new int[]{ 48000 }; - bitRates = Range.create(96000, 768000); - maxChannels = 10; - break; - case CodecProfileLevel.DTS_UHDProfileP1: - sampleRates = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; - bitRates = Range.create(96000, 24500000); - maxChannels = 32; - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - mParent.mError |= ERROR_UNRECOGNIZED; - sampleRates = new int[]{ 44100, 48000, 88200, 96000, 176400, 192000 }; - bitRates = Range.create(96000, 24500000); - maxChannels = 32; - } - } - } else { - Log.w(TAG, "Unsupported mime " + mime); - mParent.mError |= ERROR_UNSUPPORTED; - } - - // restrict ranges - if (sampleRates != null) { - limitSampleRates(sampleRates); - } else if (sampleRateRange != null) { - limitSampleRates(new Range[] { sampleRateRange }); - } - - Range<Integer> channelRange = Range.create(1, maxChannels); - - applyLimits(new Range[] { channelRange }, bitRates); - } - - private void applyLimits(Range<Integer>[] inputChannels, Range<Integer> bitRates) { - - // clamp & make a local copy - Range<Integer>[] myInputChannels = new Range[inputChannels.length]; - for (int i = 0; i < inputChannels.length; i++) { - int lower = inputChannels[i].clamp(1); - int upper = inputChannels[i].clamp(MAX_INPUT_CHANNEL_COUNT); - myInputChannels[i] = Range.create(lower, upper); - } - - // sort, intersect with existing, & save channel list - sortDistinctRanges(myInputChannels); - Range<Integer>[] joinedChannelList = - intersectSortedDistinctRanges(myInputChannels, mInputChannelRanges); - mInputChannelRanges = joinedChannelList; - - if (bitRates != null) { - mBitrateRange = mBitrateRange.intersect(bitRates); - } - } - - private void parseFromInfo(MediaFormat info) { - int maxInputChannels = MAX_INPUT_CHANNEL_COUNT; - Range<Integer>[] channels = new Range[] { Range.create(1, maxInputChannels)}; - Range<Integer> bitRates = POSITIVE_INTEGERS; - - if (info.containsKey("sample-rate-ranges")) { - String[] rateStrings = info.getString("sample-rate-ranges").split(","); - Range<Integer>[] rateRanges = new Range[rateStrings.length]; - for (int i = 0; i < rateStrings.length; i++) { - rateRanges[i] = Utils.parseIntRange(rateStrings[i], null); - } - limitSampleRates(rateRanges); - } - - // we will prefer channel-ranges over max-channel-count - if (info.containsKey("channel-ranges")) { - String[] channelStrings = info.getString("channel-ranges").split(","); - Range<Integer>[] channelRanges = new Range[channelStrings.length]; - for (int i = 0; i < channelStrings.length; i++) { - channelRanges[i] = Utils.parseIntRange(channelStrings[i], null); - } - channels = channelRanges; - } else if (info.containsKey("channel-range")) { - Range<Integer> oneRange = Utils.parseIntRange(info.getString("channel-range"), - null); - channels = new Range[] { oneRange }; - } else if (info.containsKey("max-channel-count")) { - maxInputChannels = Utils.parseIntSafely( - info.getString("max-channel-count"), maxInputChannels); - if (maxInputChannels == 0) { - channels = new Range[] {Range.create(0, 0)}; - } else { - channels = new Range[] {Range.create(1, maxInputChannels)}; - } - } else if ((mParent.mError & ERROR_UNSUPPORTED) != 0) { - maxInputChannels = 0; - channels = new Range[] {Range.create(0, 0)}; - } - - if (info.containsKey("bitrate-range")) { - bitRates = bitRates.intersect( - Utils.parseIntRange(info.getString("bitrate-range"), bitRates)); - } - - applyLimits(channels, bitRates); + return mImpl.isSampleRateSupported(sampleRate); } /** @hide */ public void getDefaultFormat(MediaFormat format) { - // report settings that have only a single choice - if (mBitrateRange.getLower().equals(mBitrateRange.getUpper())) { - format.setInteger(MediaFormat.KEY_BIT_RATE, mBitrateRange.getLower()); - } - if (getMaxInputChannelCount() == 1) { - // mono-only format - format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); - } - if (mSampleRates != null && mSampleRates.length == 1) { - format.setInteger(MediaFormat.KEY_SAMPLE_RATE, mSampleRates[0]); - } + mImpl.getDefaultFormat(format); } - /* package private */ - // must not contain KEY_PROFILE - static final Set<String> AUDIO_LEVEL_CRITICAL_FORMAT_KEYS = Set.of( - // We don't set level-specific limits for audio codecs today. Key candidates would - // be sample rate, bit rate or channel count. - // MediaFormat.KEY_SAMPLE_RATE, - // MediaFormat.KEY_CHANNEL_COUNT, - // MediaFormat.KEY_BIT_RATE, - MediaFormat.KEY_MIME); - /** @hide */ public boolean supportsFormat(MediaFormat format) { - Map<String, Object> map = format.getMap(); - Integer sampleRate = (Integer)map.get(MediaFormat.KEY_SAMPLE_RATE); - Integer channels = (Integer)map.get(MediaFormat.KEY_CHANNEL_COUNT); - - if (!supports(sampleRate, channels)) { - return false; - } - - if (!CodecCapabilities.supportsBitrate(mBitrateRange, format)) { - return false; - } - - // nothing to do for: - // KEY_CHANNEL_MASK: codecs don't get this - // KEY_IS_ADTS: required feature for all AAC decoders - return true; + return mImpl.supportsFormat(format); } } @@ -1898,304 +2301,6 @@ public final class MediaCodecInfo { */ public static final class VideoCapabilities { private static final String TAG = "VideoCapabilities"; - private CodecCapabilities mParent; - private Range<Integer> mBitrateRange; - - private Range<Integer> mHeightRange; - private Range<Integer> mWidthRange; - private Range<Integer> mBlockCountRange; - private Range<Integer> mHorizontalBlockRange; - private Range<Integer> mVerticalBlockRange; - private Range<Rational> mAspectRatioRange; - private Range<Rational> mBlockAspectRatioRange; - private Range<Long> mBlocksPerSecondRange; - private Map<Size, Range<Long>> mMeasuredFrameRates; - private List<PerformancePoint> mPerformancePoints; - private Range<Integer> mFrameRateRange; - - private int mBlockWidth; - private int mBlockHeight; - private int mWidthAlignment; - private int mHeightAlignment; - private int mSmallerDimensionUpperLimit; - - private boolean mAllowMbOverride; // allow XML to override calculated limits - - /** - * Returns the range of supported bitrates in bits/second. - */ - public Range<Integer> getBitrateRange() { - return mBitrateRange; - } - - /** - * Returns the range of supported video widths. - * <p class=note> - * 32-bit processes will not support resolutions larger than 4096x4096 due to - * the limited address space. - */ - public Range<Integer> getSupportedWidths() { - return mWidthRange; - } - - /** - * Returns the range of supported video heights. - * <p class=note> - * 32-bit processes will not support resolutions larger than 4096x4096 due to - * the limited address space. - */ - public Range<Integer> getSupportedHeights() { - return mHeightRange; - } - - /** - * Returns the alignment requirement for video width (in pixels). - * - * This is a power-of-2 value that video width must be a - * multiple of. - */ - public int getWidthAlignment() { - return mWidthAlignment; - } - - /** - * Returns the alignment requirement for video height (in pixels). - * - * This is a power-of-2 value that video height must be a - * multiple of. - */ - public int getHeightAlignment() { - return mHeightAlignment; - } - - /** - * Return the upper limit on the smaller dimension of width or height. - * <p></p> - * Some codecs have a limit on the smaller dimension, whether it be - * the width or the height. E.g. a codec may only be able to handle - * up to 1920x1080 both in landscape and portrait mode (1080x1920). - * In this case the maximum width and height are both 1920, but the - * smaller dimension limit will be 1080. For other codecs, this is - * {@code Math.min(getSupportedWidths().getUpper(), - * getSupportedHeights().getUpper())}. - * - * @hide - */ - public int getSmallerDimensionUpperLimit() { - return mSmallerDimensionUpperLimit; - } - - /** - * Returns the range of supported frame rates. - * <p> - * This is not a performance indicator. Rather, it expresses the - * limits specified in the coding standard, based on the complexities - * of encoding material for later playback at a certain frame rate, - * or the decoding of such material in non-realtime. - */ - public Range<Integer> getSupportedFrameRates() { - return mFrameRateRange; - } - - /** - * Returns the range of supported video widths for a video height. - * @param height the height of the video - */ - public Range<Integer> getSupportedWidthsFor(int height) { - try { - Range<Integer> range = mWidthRange; - if (!mHeightRange.contains(height) - || (height % mHeightAlignment) != 0) { - throw new IllegalArgumentException("unsupported height"); - } - final int heightInBlocks = Utils.divUp(height, mBlockHeight); - - // constrain by block count and by block aspect ratio - final int minWidthInBlocks = Math.max( - Utils.divUp(mBlockCountRange.getLower(), heightInBlocks), - (int)Math.ceil(mBlockAspectRatioRange.getLower().doubleValue() - * heightInBlocks)); - final int maxWidthInBlocks = Math.min( - mBlockCountRange.getUpper() / heightInBlocks, - (int)(mBlockAspectRatioRange.getUpper().doubleValue() - * heightInBlocks)); - range = range.intersect( - (minWidthInBlocks - 1) * mBlockWidth + mWidthAlignment, - maxWidthInBlocks * mBlockWidth); - - // constrain by smaller dimension limit - if (height > mSmallerDimensionUpperLimit) { - range = range.intersect(1, mSmallerDimensionUpperLimit); - } - - // constrain by aspect ratio - range = range.intersect( - (int)Math.ceil(mAspectRatioRange.getLower().doubleValue() - * height), - (int)(mAspectRatioRange.getUpper().doubleValue() * height)); - return range; - } catch (IllegalArgumentException e) { - // height is not supported because there are no suitable widths - Log.v(TAG, "could not get supported widths for " + height); - throw new IllegalArgumentException("unsupported height"); - } - } - - /** - * Returns the range of supported video heights for a video width - * @param width the width of the video - */ - public Range<Integer> getSupportedHeightsFor(int width) { - try { - Range<Integer> range = mHeightRange; - if (!mWidthRange.contains(width) - || (width % mWidthAlignment) != 0) { - throw new IllegalArgumentException("unsupported width"); - } - final int widthInBlocks = Utils.divUp(width, mBlockWidth); - - // constrain by block count and by block aspect ratio - final int minHeightInBlocks = Math.max( - Utils.divUp(mBlockCountRange.getLower(), widthInBlocks), - (int)Math.ceil(widthInBlocks / - mBlockAspectRatioRange.getUpper().doubleValue())); - final int maxHeightInBlocks = Math.min( - mBlockCountRange.getUpper() / widthInBlocks, - (int)(widthInBlocks / - mBlockAspectRatioRange.getLower().doubleValue())); - range = range.intersect( - (minHeightInBlocks - 1) * mBlockHeight + mHeightAlignment, - maxHeightInBlocks * mBlockHeight); - - // constrain by smaller dimension limit - if (width > mSmallerDimensionUpperLimit) { - range = range.intersect(1, mSmallerDimensionUpperLimit); - } - - // constrain by aspect ratio - range = range.intersect( - (int)Math.ceil(width / - mAspectRatioRange.getUpper().doubleValue()), - (int)(width / mAspectRatioRange.getLower().doubleValue())); - return range; - } catch (IllegalArgumentException e) { - // width is not supported because there are no suitable heights - Log.v(TAG, "could not get supported heights for " + width); - throw new IllegalArgumentException("unsupported width"); - } - } - - /** - * Returns the range of supported video frame rates for a video size. - * <p> - * This is not a performance indicator. Rather, it expresses the limits specified in - * the coding standard, based on the complexities of encoding material of a given - * size for later playback at a certain frame rate, or the decoding of such material - * in non-realtime. - - * @param width the width of the video - * @param height the height of the video - */ - public Range<Double> getSupportedFrameRatesFor(int width, int height) { - Range<Integer> range = mHeightRange; - if (!supports(width, height, null)) { - throw new IllegalArgumentException("unsupported size"); - } - final int blockCount = - Utils.divUp(width, mBlockWidth) * Utils.divUp(height, mBlockHeight); - - return Range.create( - Math.max(mBlocksPerSecondRange.getLower() / (double) blockCount, - (double) mFrameRateRange.getLower()), - Math.min(mBlocksPerSecondRange.getUpper() / (double) blockCount, - (double) mFrameRateRange.getUpper())); - } - - private int getBlockCount(int width, int height) { - return Utils.divUp(width, mBlockWidth) * Utils.divUp(height, mBlockHeight); - } - - @NonNull - private Size findClosestSize(int width, int height) { - int targetBlockCount = getBlockCount(width, height); - Size closestSize = null; - int minDiff = Integer.MAX_VALUE; - for (Size size : mMeasuredFrameRates.keySet()) { - int diff = Math.abs(targetBlockCount - - getBlockCount(size.getWidth(), size.getHeight())); - if (diff < minDiff) { - minDiff = diff; - closestSize = size; - } - } - return closestSize; - } - - private Range<Double> estimateFrameRatesFor(int width, int height) { - Size size = findClosestSize(width, height); - Range<Long> range = mMeasuredFrameRates.get(size); - Double ratio = getBlockCount(size.getWidth(), size.getHeight()) - / (double)Math.max(getBlockCount(width, height), 1); - return Range.create(range.getLower() * ratio, range.getUpper() * ratio); - } - - /** - * Returns the range of achievable video frame rates for a video size. - * May return {@code null}, if the codec did not publish any measurement - * data. - * <p> - * This is a performance estimate provided by the device manufacturer based on statistical - * sampling of full-speed decoding and encoding measurements in various configurations - * of common video sizes supported by the codec. As such it should only be used to - * compare individual codecs on the device. The value is not suitable for comparing - * different devices or even different android releases for the same device. - * <p> - * <em>On {@link android.os.Build.VERSION_CODES#M} release</em> the returned range - * corresponds to the fastest frame rates achieved in the tested configurations. As - * such, it should not be used to gauge guaranteed or even average codec performance - * on the device. - * <p> - * <em>On {@link android.os.Build.VERSION_CODES#N} release</em> the returned range - * corresponds closer to sustained performance <em>in tested configurations</em>. - * One can expect to achieve sustained performance higher than the lower limit more than - * 50% of the time, and higher than half of the lower limit at least 90% of the time - * <em>in tested configurations</em>. - * Conversely, one can expect performance lower than twice the upper limit at least - * 90% of the time. - * <p class=note> - * Tested configurations use a single active codec. For use cases where multiple - * codecs are active, applications can expect lower and in most cases significantly lower - * performance. - * <p class=note> - * The returned range value is interpolated from the nearest frame size(s) tested. - * Codec performance is severely impacted by other activity on the device as well - * as environmental factors (such as battery level, temperature or power source), and can - * vary significantly even in a steady environment. - * <p class=note> - * Use this method in cases where only codec performance matters, e.g. to evaluate if - * a codec has any chance of meeting a performance target. Codecs are listed - * in {@link MediaCodecList} in the preferred order as defined by the device - * manufacturer. As such, applications should use the first suitable codec in the - * list to achieve the best balance between power use and performance. - * - * @param width the width of the video - * @param height the height of the video - * - * @throws IllegalArgumentException if the video size is not supported. - */ - @Nullable - public Range<Double> getAchievableFrameRatesFor(int width, int height) { - if (!supports(width, height, null)) { - throw new IllegalArgumentException("unsupported size"); - } - - if (mMeasuredFrameRates == null || mMeasuredFrameRates.size() <= 0) { - Log.w(TAG, "Codec did not publish any measurement data."); - return null; - } - - return estimateFrameRatesFor(width, height); - } /** * Video performance points are a set of standard performance points defined by number of @@ -2225,6 +2330,24 @@ public final class MediaCodecInfo { } /** + * Width in macroblocks. + * + * @hide + */ + /** package private */ int getWidth() { + return mWidth; + } + + /** + * Height in macroblocks. + * + * @hide + */ + /** package private */ int getHeight() { + return mHeight; + } + + /** * Maximum frame rate in frames per second. * * @hide @@ -2244,6 +2367,24 @@ public final class MediaCodecInfo { return mMaxMacroBlockRate; } + /** + * Codec block width in macroblocks. + * + * @hide + */ + /** package private */ int getBlockWidth() { + return mBlockSize.getWidth(); + } + + /** + * Codec block height in macroblocks. + * + * @hide + */ + /** package private */ int getBlockHeight() { + return mBlockSize.getHeight(); + } + /** Convert to a debug string */ public String toString() { int blockWidth = 16 * mBlockSize.getWidth(); @@ -2331,6 +2472,20 @@ public final class MediaCodecInfo { this(width, height, frameRate, frameRate /* maxFrameRate */, new Size(16, 16)); } + /* package private */ PerformancePoint(int width, int height, int maxFrameRate, + long maxMacroBlockRate, int blockSizeWidth, int blockSizeHeight) { + mWidth = width; + mHeight = height; + mMaxFrameRate = maxFrameRate; + mMaxMacroBlockRate = maxMacroBlockRate; + mBlockSize = new Size(blockSizeWidth, blockSizeHeight); + } + + private PerformancePoint(PerformancePoint pp) { + this(pp.mWidth, pp.mHeight, pp.mMaxFrameRate, pp.mMaxMacroBlockRate, + pp.mBlockSize.getWidth(), pp.mBlockSize.getHeight()); + } + /** Saturates a long value to int */ private int saturateLongToInt(long value) { if (value < Integer.MIN_VALUE) { @@ -2384,14 +2539,18 @@ public final class MediaCodecInfo { * @return {@code true} if the performance point covers the other. */ public boolean covers(@NonNull PerformancePoint other) { - // convert performance points to common block size - Size commonSize = getCommonBlockSize(other); - PerformancePoint aligned = new PerformancePoint(this, commonSize); - PerformancePoint otherAligned = new PerformancePoint(other, commonSize); + if (GetFlag(() -> android.media.codec.Flags.nativeCapabilites())) { + return native_covers(other); + } else { + // convert performance points to common block size + Size commonSize = getCommonBlockSize(other); + PerformancePoint aligned = new PerformancePoint(this, commonSize); + PerformancePoint otherAligned = new PerformancePoint(other, commonSize); - return (aligned.getMaxMacroBlocks() >= otherAligned.getMaxMacroBlocks() - && aligned.mMaxFrameRate >= otherAligned.mMaxFrameRate - && aligned.mMaxMacroBlockRate >= otherAligned.mMaxMacroBlockRate); + return (aligned.getMaxMacroBlocks() >= otherAligned.getMaxMacroBlocks() + && aligned.mMaxFrameRate >= otherAligned.mMaxFrameRate + && aligned.mMaxMacroBlockRate >= otherAligned.mMaxMacroBlockRate); + } } private @NonNull Size getCommonBlockSize(@NonNull PerformancePoint other) { @@ -2405,17 +2564,28 @@ public final class MediaCodecInfo { if (o instanceof PerformancePoint) { // convert performance points to common block size PerformancePoint other = (PerformancePoint)o; - Size commonSize = getCommonBlockSize(other); - PerformancePoint aligned = new PerformancePoint(this, commonSize); - PerformancePoint otherAligned = new PerformancePoint(other, commonSize); + if (GetFlag(() -> android.media.codec.Flags.nativeCapabilites())) { + return native_equals(other); + } else { + Size commonSize = getCommonBlockSize(other); + PerformancePoint aligned = new PerformancePoint(this, commonSize); + PerformancePoint otherAligned = new PerformancePoint(other, commonSize); - return (aligned.getMaxMacroBlocks() == otherAligned.getMaxMacroBlocks() - && aligned.mMaxFrameRate == otherAligned.mMaxFrameRate - && aligned.mMaxMacroBlockRate == otherAligned.mMaxMacroBlockRate); + return (aligned.getMaxMacroBlocks() == otherAligned.getMaxMacroBlocks() + && aligned.mMaxFrameRate == otherAligned.mMaxFrameRate + && aligned.mMaxMacroBlockRate == otherAligned.mMaxMacroBlockRate); + } } return false; } + private native boolean native_covers(PerformancePoint other); + private native boolean native_equals(PerformancePoint other); + + static { + System.loadLibrary("media_jni"); + } + /** 480p 24fps */ @NonNull public static final PerformancePoint SD_24 = new PerformancePoint(720, 480, 24); @@ -2520,1351 +2690,1870 @@ public final class MediaCodecInfo { public static final PerformancePoint UHD_240 = new PerformancePoint(3840, 2160, 240); } - /** - * Returns the supported performance points. May return {@code null} if the codec did not - * publish any performance point information (e.g. the vendor codecs have not been updated - * to the latest android release). May return an empty list if the codec published that - * if does not guarantee any performance points. - * <p> - * This is a performance guarantee provided by the device manufacturer for hardware codecs - * based on hardware capabilities of the device. - * <p> - * The returned list is sorted first by decreasing number of pixels, then by decreasing - * width, and finally by decreasing frame rate. - * Performance points assume a single active codec. For use cases where multiple - * codecs are active, should use that highest pixel count, and add the frame rates of - * each individual codec. - * <p class=note> - * 32-bit processes will not support resolutions larger than 4096x4096 due to - * the limited address space, but performance points will be presented as is. - * In other words, even though a component publishes a performance point for - * a resolution higher than 4096x4096, it does not mean that the resolution is supported - * for 32-bit processes. - */ - @Nullable - public List<PerformancePoint> getSupportedPerformancePoints() { - return mPerformancePoints; - } + /* package private */ interface VideoCapsIntf { + public Range<Integer> getBitrateRange(); - /** - * Returns whether a given video size ({@code width} and - * {@code height}) and {@code frameRate} combination is supported. - */ - public boolean areSizeAndRateSupported( - int width, int height, double frameRate) { - return supports(width, height, frameRate); - } + public Range<Integer> getSupportedWidths(); - /** - * Returns whether a given video size ({@code width} and - * {@code height}) is supported. - */ - public boolean isSizeSupported(int width, int height) { - return supports(width, height, null); - } + public Range<Integer> getSupportedHeights(); - private boolean supports(Integer width, Integer height, Number rate) { - boolean ok = true; + public int getWidthAlignment(); - if (ok && width != null) { - ok = mWidthRange.contains(width) - && (width % mWidthAlignment == 0); - } - if (ok && height != null) { - ok = mHeightRange.contains(height) - && (height % mHeightAlignment == 0); - } - if (ok && rate != null) { - ok = mFrameRateRange.contains(Utils.intRangeFor(rate.doubleValue())); - } - if (ok && height != null && width != null) { - ok = Math.min(height, width) <= mSmallerDimensionUpperLimit; + public int getHeightAlignment(); - final int widthInBlocks = Utils.divUp(width, mBlockWidth); - final int heightInBlocks = Utils.divUp(height, mBlockHeight); - final int blockCount = widthInBlocks * heightInBlocks; - ok = ok && mBlockCountRange.contains(blockCount) - && mBlockAspectRatioRange.contains( - new Rational(widthInBlocks, heightInBlocks)) - && mAspectRatioRange.contains(new Rational(width, height)); - if (ok && rate != null) { - double blocksPerSec = blockCount * rate.doubleValue(); - ok = mBlocksPerSecondRange.contains( - Utils.longRangeFor(blocksPerSec)); - } - } - return ok; - } + public int getSmallerDimensionUpperLimit(); - /* package private */ - // must not contain KEY_PROFILE - static final Set<String> VIDEO_LEVEL_CRITICAL_FORMAT_KEYS = Set.of( - MediaFormat.KEY_WIDTH, - MediaFormat.KEY_HEIGHT, - MediaFormat.KEY_FRAME_RATE, - MediaFormat.KEY_BIT_RATE, - MediaFormat.KEY_MIME); + public Range<Integer> getSupportedFrameRates(); - /** - * @hide - * @throws java.lang.ClassCastException */ - public boolean supportsFormat(MediaFormat format) { - final Map<String, Object> map = format.getMap(); - Integer width = (Integer)map.get(MediaFormat.KEY_WIDTH); - Integer height = (Integer)map.get(MediaFormat.KEY_HEIGHT); - Number rate = (Number)map.get(MediaFormat.KEY_FRAME_RATE); + public Range<Integer> getSupportedWidthsFor(int height); - if (!supports(width, height, rate)) { - return false; - } + public Range<Integer> getSupportedHeightsFor(int width); - if (!CodecCapabilities.supportsBitrate(mBitrateRange, format)) { - return false; - } + public Range<Double> getSupportedFrameRatesFor(int width, int height); - // we ignore color-format for now as it is not reliably reported by codec - return true; - } + public Range<Double> getAchievableFrameRatesFor(int width, int height); - /* no public constructor */ - private VideoCapabilities() { } + public boolean areSizeAndRateSupported(int width, int height, double frameRate); - /** @hide */ - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) - public static VideoCapabilities create( - MediaFormat info, CodecCapabilities parent) { - VideoCapabilities caps = new VideoCapabilities(); - caps.init(info, parent); - return caps; - } + public boolean isSizeSupported(int width, int height); - private void init(MediaFormat info, CodecCapabilities parent) { - mParent = parent; - initWithPlatformLimits(); - applyLevelLimits(); - parseFromInfo(info); - updateLimits(); - } + public boolean supportsFormat(MediaFormat format); - /** @hide */ - public Size getBlockSize() { - return new Size(mBlockWidth, mBlockHeight); + public List<PerformancePoint> getSupportedPerformancePoints(); } - /** @hide */ - public Range<Integer> getBlockCountRange() { - return mBlockCountRange; - } + /* package private */ static final class VideoCapsLegacyImpl implements VideoCapsIntf { + /* package private */ + // must not contain KEY_PROFILE + static final Set<String> VIDEO_LEVEL_CRITICAL_FORMAT_KEYS = Set.of( + MediaFormat.KEY_WIDTH, + MediaFormat.KEY_HEIGHT, + MediaFormat.KEY_FRAME_RATE, + MediaFormat.KEY_BIT_RATE, + MediaFormat.KEY_MIME); + + private CodecCapabilities.CodecCapsLegacyImpl mParent; + private Range<Integer> mBitrateRange; + + private Range<Integer> mHeightRange; + private Range<Integer> mWidthRange; + private Range<Integer> mBlockCountRange; + private Range<Integer> mHorizontalBlockRange; + private Range<Integer> mVerticalBlockRange; + private Range<Rational> mAspectRatioRange; + private Range<Rational> mBlockAspectRatioRange; + private Range<Long> mBlocksPerSecondRange; + private Map<Size, Range<Long>> mMeasuredFrameRates; + private List<PerformancePoint> mPerformancePoints; + private Range<Integer> mFrameRateRange; + + private int mBlockWidth; + private int mBlockHeight; + private int mWidthAlignment; + private int mHeightAlignment; + private int mSmallerDimensionUpperLimit; + + private boolean mAllowMbOverride; // allow XML to override calculated limits + + /* no public constructor */ + private VideoCapsLegacyImpl() { } + + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) + public static VideoCapsLegacyImpl create( + MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + if (GetFlag(() -> android.media.codec.Flags.nativeCapabilites())) { + Log.d(TAG, "Legacy implementation is called while native flag is on."); + } - /** @hide */ - public Range<Long> getBlocksPerSecondRange() { - return mBlocksPerSecondRange; - } + VideoCapsLegacyImpl caps = new VideoCapsLegacyImpl(); + caps.init(info, parent); + return caps; + } - /** @hide */ - public Range<Rational> getAspectRatioRange(boolean blocks) { - return blocks ? mBlockAspectRatioRange : mAspectRatioRange; - } + private void init(MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + mParent = parent; + initWithPlatformLimits(); + applyLevelLimits(); + parseFromInfo(info); + updateLimits(); + } - private void initWithPlatformLimits() { - mBitrateRange = BITRATE_RANGE; + public Range<Integer> getBitrateRange() { + return mBitrateRange; + } - mWidthRange = getSizeRange(); - mHeightRange = getSizeRange(); - mFrameRateRange = FRAME_RATE_RANGE; + public Range<Integer> getSupportedWidths() { + return mWidthRange; + } - mHorizontalBlockRange = getSizeRange(); - mVerticalBlockRange = getSizeRange(); + public Range<Integer> getSupportedHeights() { + return mHeightRange; + } - // full positive ranges are supported as these get calculated - mBlockCountRange = POSITIVE_INTEGERS; - mBlocksPerSecondRange = POSITIVE_LONGS; + public int getWidthAlignment() { + return mWidthAlignment; + } - mBlockAspectRatioRange = POSITIVE_RATIONALS; - mAspectRatioRange = POSITIVE_RATIONALS; + public int getHeightAlignment() { + return mHeightAlignment; + } - mWidthAlignment = 1; - mHeightAlignment = 1; - mBlockWidth = 1; - mBlockHeight = 1; - mSmallerDimensionUpperLimit = getSizeRange().getUpper(); - } + /** @hide */ + public int getSmallerDimensionUpperLimit() { + return mSmallerDimensionUpperLimit; + } + + public Range<Integer> getSupportedFrameRates() { + return mFrameRateRange; + } - private @Nullable List<PerformancePoint> getPerformancePoints(Map<String, Object> map) { - Vector<PerformancePoint> ret = new Vector<>(); - final String prefix = "performance-point-"; - Set<String> keys = map.keySet(); - for (String key : keys) { - // looking for: performance-point-WIDTHxHEIGHT-range - if (!key.startsWith(prefix)) { - continue; + public Range<Integer> getSupportedWidthsFor(int height) { + try { + Range<Integer> range = mWidthRange; + if (!mHeightRange.contains(height) + || (height % mHeightAlignment) != 0) { + throw new IllegalArgumentException("unsupported height"); + } + final int heightInBlocks = Utils.divUp(height, mBlockHeight); + + // constrain by block count and by block aspect ratio + final int minWidthInBlocks = Math.max( + Utils.divUp(mBlockCountRange.getLower(), heightInBlocks), + (int) Math.ceil(mBlockAspectRatioRange.getLower().doubleValue() + * heightInBlocks)); + final int maxWidthInBlocks = Math.min( + mBlockCountRange.getUpper() / heightInBlocks, + (int) (mBlockAspectRatioRange.getUpper().doubleValue() + * heightInBlocks)); + range = range.intersect( + (minWidthInBlocks - 1) * mBlockWidth + mWidthAlignment, + maxWidthInBlocks * mBlockWidth); + + // constrain by smaller dimension limit + if (height > mSmallerDimensionUpperLimit) { + range = range.intersect(1, mSmallerDimensionUpperLimit); + } + + // constrain by aspect ratio + range = range.intersect( + (int) Math.ceil(mAspectRatioRange.getLower().doubleValue() + * height), + (int) (mAspectRatioRange.getUpper().doubleValue() * height)); + return range; + } catch (IllegalArgumentException e) { + // height is not supported because there are no suitable widths + Log.v(TAG, "could not get supported widths for " + height); + throw new IllegalArgumentException("unsupported height"); } - String subKey = key.substring(prefix.length()); - if (subKey.equals("none") && ret.size() == 0) { - // This means that component knowingly did not publish performance points. - // This is different from when the component forgot to publish performance - // points. - return Collections.unmodifiableList(ret); + } + + public Range<Integer> getSupportedHeightsFor(int width) { + try { + Range<Integer> range = mHeightRange; + if (!mWidthRange.contains(width) + || (width % mWidthAlignment) != 0) { + throw new IllegalArgumentException("unsupported width"); + } + final int widthInBlocks = Utils.divUp(width, mBlockWidth); + + // constrain by block count and by block aspect ratio + final int minHeightInBlocks = Math.max( + Utils.divUp(mBlockCountRange.getLower(), widthInBlocks), + (int) Math.ceil(widthInBlocks + / mBlockAspectRatioRange.getUpper().doubleValue())); + final int maxHeightInBlocks = Math.min( + mBlockCountRange.getUpper() / widthInBlocks, + (int) (widthInBlocks + / mBlockAspectRatioRange.getLower().doubleValue())); + range = range.intersect( + (minHeightInBlocks - 1) * mBlockHeight + mHeightAlignment, + maxHeightInBlocks * mBlockHeight); + + // constrain by smaller dimension limit + if (width > mSmallerDimensionUpperLimit) { + range = range.intersect(1, mSmallerDimensionUpperLimit); + } + + // constrain by aspect ratio + range = range.intersect( + (int) Math.ceil(width + / mAspectRatioRange.getUpper().doubleValue()), + (int) (width / mAspectRatioRange.getLower().doubleValue())); + return range; + } catch (IllegalArgumentException e) { + // width is not supported because there are no suitable heights + Log.v(TAG, "could not get supported heights for " + width); + throw new IllegalArgumentException("unsupported width"); } - String[] temp = key.split("-"); - if (temp.length != 4) { - continue; + } + + public Range<Double> getSupportedFrameRatesFor(int width, int height) { + Range<Integer> range = mHeightRange; + if (!supports(width, height, null)) { + throw new IllegalArgumentException("unsupported size"); } - String sizeStr = temp[2]; - Size size = Utils.parseSize(sizeStr, null); - if (size == null || size.getWidth() * size.getHeight() <= 0) { - continue; + final int blockCount = + Utils.divUp(width, mBlockWidth) * Utils.divUp(height, mBlockHeight); + + return Range.create( + Math.max(mBlocksPerSecondRange.getLower() / (double) blockCount, + (double) mFrameRateRange.getLower()), + Math.min(mBlocksPerSecondRange.getUpper() / (double) blockCount, + (double) mFrameRateRange.getUpper())); + } + + private int getBlockCount(int width, int height) { + return Utils.divUp(width, mBlockWidth) * Utils.divUp(height, mBlockHeight); + } + + @NonNull + private Size findClosestSize(int width, int height) { + int targetBlockCount = getBlockCount(width, height); + Size closestSize = null; + int minDiff = Integer.MAX_VALUE; + for (Size size : mMeasuredFrameRates.keySet()) { + int diff = Math.abs(targetBlockCount - + getBlockCount(size.getWidth(), size.getHeight())); + if (diff < minDiff) { + minDiff = diff; + closestSize = size; + } } - Range<Long> range = Utils.parseLongRange(map.get(key), null); - if (range == null || range.getLower() < 0 || range.getUpper() < 0) { - continue; + return closestSize; + } + + private Range<Double> estimateFrameRatesFor(int width, int height) { + Size size = findClosestSize(width, height); + Range<Long> range = mMeasuredFrameRates.get(size); + Double ratio = getBlockCount(size.getWidth(), size.getHeight()) + / (double)Math.max(getBlockCount(width, height), 1); + return Range.create(range.getLower() * ratio, range.getUpper() * ratio); + } + + /** @throws IllegalArgumentException if the video size is not supported. */ + @Nullable + public Range<Double> getAchievableFrameRatesFor(int width, int height) { + if (!supports(width, height, null)) { + throw new IllegalArgumentException("unsupported size"); } - PerformancePoint given = new PerformancePoint( - size.getWidth(), size.getHeight(), range.getLower().intValue(), - range.getUpper().intValue(), new Size(mBlockWidth, mBlockHeight)); - PerformancePoint rotated = new PerformancePoint( - size.getHeight(), size.getWidth(), range.getLower().intValue(), - range.getUpper().intValue(), new Size(mBlockWidth, mBlockHeight)); - ret.add(given); - if (!given.covers(rotated)) { - ret.add(rotated); + + if (mMeasuredFrameRates == null || mMeasuredFrameRates.size() <= 0) { + Log.w(TAG, "Codec did not publish any measurement data."); + return null; } + + return estimateFrameRatesFor(width, height); } - // check if the component specified no performance point indication - if (ret.size() == 0) { - return null; + @Nullable + public List<PerformancePoint> getSupportedPerformancePoints() { + return mPerformancePoints; } - // sort reversed by area first, then by frame rate - ret.sort((a, b) -> - -((a.getMaxMacroBlocks() != b.getMaxMacroBlocks()) ? - (a.getMaxMacroBlocks() < b.getMaxMacroBlocks() ? -1 : 1) : - (a.getMaxMacroBlockRate() != b.getMaxMacroBlockRate()) ? - (a.getMaxMacroBlockRate() < b.getMaxMacroBlockRate() ? -1 : 1) : - (a.getMaxFrameRate() != b.getMaxFrameRate()) ? - (a.getMaxFrameRate() < b.getMaxFrameRate() ? -1 : 1) : 0)); + public boolean areSizeAndRateSupported( + int width, int height, double frameRate) { + return supports(width, height, frameRate); + } - return Collections.unmodifiableList(ret); - } + public boolean isSizeSupported(int width, int height) { + return supports(width, height, null); + } + + private boolean supports(Integer width, Integer height, Number rate) { + boolean ok = true; - private Map<Size, Range<Long>> getMeasuredFrameRates(Map<String, Object> map) { - Map<Size, Range<Long>> ret = new HashMap<Size, Range<Long>>(); - final String prefix = "measured-frame-rate-"; - Set<String> keys = map.keySet(); - for (String key : keys) { - // looking for: measured-frame-rate-WIDTHxHEIGHT-range - if (!key.startsWith(prefix)) { - continue; + if (ok && width != null) { + ok = mWidthRange.contains(width) + && (width % mWidthAlignment == 0); } - String subKey = key.substring(prefix.length()); - String[] temp = key.split("-"); - if (temp.length != 5) { - continue; + if (ok && height != null) { + ok = mHeightRange.contains(height) + && (height % mHeightAlignment == 0); } - String sizeStr = temp[3]; - Size size = Utils.parseSize(sizeStr, null); - if (size == null || size.getWidth() * size.getHeight() <= 0) { - continue; + if (ok && rate != null) { + ok = mFrameRateRange.contains(Utils.intRangeFor(rate.doubleValue())); } - Range<Long> range = Utils.parseLongRange(map.get(key), null); - if (range == null || range.getLower() < 0 || range.getUpper() < 0) { - continue; + if (ok && height != null && width != null) { + ok = Math.min(height, width) <= mSmallerDimensionUpperLimit; + + final int widthInBlocks = Utils.divUp(width, mBlockWidth); + final int heightInBlocks = Utils.divUp(height, mBlockHeight); + final int blockCount = widthInBlocks * heightInBlocks; + ok = ok && mBlockCountRange.contains(blockCount) + && mBlockAspectRatioRange.contains( + new Rational(widthInBlocks, heightInBlocks)) + && mAspectRatioRange.contains(new Rational(width, height)); + if (ok && rate != null) { + double blocksPerSec = blockCount * rate.doubleValue(); + ok = mBlocksPerSecondRange.contains( + Utils.longRangeFor(blocksPerSec)); + } } - ret.put(size, range); + return ok; } - return ret; - } - private static Pair<Range<Integer>, Range<Integer>> parseWidthHeightRanges(Object o) { - Pair<Size, Size> range = Utils.parseSizeRange(o); - if (range != null) { - try { - return Pair.create( - Range.create(range.first.getWidth(), range.second.getWidth()), - Range.create(range.first.getHeight(), range.second.getHeight())); - } catch (IllegalArgumentException e) { - Log.w(TAG, "could not parse size range '" + o + "'"); + /** + * @hide + * @throws java.lang.ClassCastException */ + public boolean supportsFormat(MediaFormat format) { + final Map<String, Object> map = format.getMap(); + Integer width = (Integer)map.get(MediaFormat.KEY_WIDTH); + Integer height = (Integer)map.get(MediaFormat.KEY_HEIGHT); + Number rate = (Number)map.get(MediaFormat.KEY_FRAME_RATE); + + if (!supports(width, height, rate)) { + return false; + } + + if (!CodecCapabilities.CodecCapsLegacyImpl.supportsBitrate(mBitrateRange, format)) { + return false; } + + // we ignore color-format for now as it is not reliably reported by codec + return true; } - return null; - } - /** @hide */ - public static int equivalentVP9Level(MediaFormat info) { - final Map<String, Object> map = info.getMap(); - - Size blockSize = Utils.parseSize(map.get("block-size"), new Size(8, 8)); - int BS = blockSize.getWidth() * blockSize.getHeight(); - - Range<Integer> counts = Utils.parseIntRange(map.get("block-count-range"), null); - int FS = counts == null ? 0 : BS * counts.getUpper(); - - Range<Long> blockRates = - Utils.parseLongRange(map.get("blocks-per-second-range"), null); - long SR = blockRates == null ? 0 : BS * blockRates.getUpper(); - - Pair<Range<Integer>, Range<Integer>> dimensionRanges = - parseWidthHeightRanges(map.get("size-range")); - int D = dimensionRanges == null ? 0 : Math.max( - dimensionRanges.first.getUpper(), dimensionRanges.second.getUpper()); - - Range<Integer> bitRates = Utils.parseIntRange(map.get("bitrate-range"), null); - int BR = bitRates == null ? 0 : Utils.divUp(bitRates.getUpper(), 1000); - - if (SR <= 829440 && FS <= 36864 && BR <= 200 && D <= 512) - return CodecProfileLevel.VP9Level1; - if (SR <= 2764800 && FS <= 73728 && BR <= 800 && D <= 768) - return CodecProfileLevel.VP9Level11; - if (SR <= 4608000 && FS <= 122880 && BR <= 1800 && D <= 960) - return CodecProfileLevel.VP9Level2; - if (SR <= 9216000 && FS <= 245760 && BR <= 3600 && D <= 1344) - return CodecProfileLevel.VP9Level21; - if (SR <= 20736000 && FS <= 552960 && BR <= 7200 && D <= 2048) - return CodecProfileLevel.VP9Level3; - if (SR <= 36864000 && FS <= 983040 && BR <= 12000 && D <= 2752) - return CodecProfileLevel.VP9Level31; - if (SR <= 83558400 && FS <= 2228224 && BR <= 18000 && D <= 4160) - return CodecProfileLevel.VP9Level4; - if (SR <= 160432128 && FS <= 2228224 && BR <= 30000 && D <= 4160) - return CodecProfileLevel.VP9Level41; - if (SR <= 311951360 && FS <= 8912896 && BR <= 60000 && D <= 8384) - return CodecProfileLevel.VP9Level5; - if (SR <= 588251136 && FS <= 8912896 && BR <= 120000 && D <= 8384) - return CodecProfileLevel.VP9Level51; - if (SR <= 1176502272 && FS <= 8912896 && BR <= 180000 && D <= 8384) - return CodecProfileLevel.VP9Level52; - if (SR <= 1176502272 && FS <= 35651584 && BR <= 180000 && D <= 16832) - return CodecProfileLevel.VP9Level6; - if (SR <= 2353004544L && FS <= 35651584 && BR <= 240000 && D <= 16832) - return CodecProfileLevel.VP9Level61; - if (SR <= 4706009088L && FS <= 35651584 && BR <= 480000 && D <= 16832) - return CodecProfileLevel.VP9Level62; - // returning largest level - return CodecProfileLevel.VP9Level62; - } + /** @hide */ + public Size getBlockSize() { + return new Size(mBlockWidth, mBlockHeight); + } - private void parseFromInfo(MediaFormat info) { - final Map<String, Object> map = info.getMap(); - Size blockSize = new Size(mBlockWidth, mBlockHeight); - Size alignment = new Size(mWidthAlignment, mHeightAlignment); - Range<Integer> counts = null, widths = null, heights = null; - Range<Integer> frameRates = null, bitRates = null; - Range<Long> blockRates = null; - Range<Rational> ratios = null, blockRatios = null; - - blockSize = Utils.parseSize(map.get("block-size"), blockSize); - alignment = Utils.parseSize(map.get("alignment"), alignment); - counts = Utils.parseIntRange(map.get("block-count-range"), null); - blockRates = - Utils.parseLongRange(map.get("blocks-per-second-range"), null); - mMeasuredFrameRates = getMeasuredFrameRates(map); - mPerformancePoints = getPerformancePoints(map); - Pair<Range<Integer>, Range<Integer>> sizeRanges = - parseWidthHeightRanges(map.get("size-range")); - if (sizeRanges != null) { - widths = sizeRanges.first; - heights = sizeRanges.second; - } - // for now this just means using the smaller max size as 2nd - // upper limit. - // for now we are keeping the profile specific "width/height - // in macroblocks" limits. - if (map.containsKey("feature-can-swap-width-height")) { - if (widths != null) { - mSmallerDimensionUpperLimit = - Math.min(widths.getUpper(), heights.getUpper()); - widths = heights = widths.extend(heights); - } else { - Log.w(TAG, "feature can-swap-width-height is best used with size-range"); - mSmallerDimensionUpperLimit = - Math.min(mWidthRange.getUpper(), mHeightRange.getUpper()); - mWidthRange = mHeightRange = mWidthRange.extend(mHeightRange); - } + /** @hide */ + public Range<Integer> getBlockCountRange() { + return mBlockCountRange; } - ratios = Utils.parseRationalRange( - map.get("block-aspect-ratio-range"), null); - blockRatios = Utils.parseRationalRange( - map.get("pixel-aspect-ratio-range"), null); - frameRates = Utils.parseIntRange(map.get("frame-rate-range"), null); - if (frameRates != null) { - try { - frameRates = frameRates.intersect(FRAME_RATE_RANGE); - } catch (IllegalArgumentException e) { - Log.w(TAG, "frame rate range (" + frameRates - + ") is out of limits: " + FRAME_RATE_RANGE); - frameRates = null; - } + /** @hide */ + public Range<Long> getBlocksPerSecondRange() { + return mBlocksPerSecondRange; } - bitRates = Utils.parseIntRange(map.get("bitrate-range"), null); - if (bitRates != null) { - try { - bitRates = bitRates.intersect(BITRATE_RANGE); - } catch (IllegalArgumentException e) { - Log.w(TAG, "bitrate range (" + bitRates - + ") is out of limits: " + BITRATE_RANGE); - bitRates = null; - } + + /** @hide */ + public Range<Rational> getAspectRatioRange(boolean blocks) { + return blocks ? mBlockAspectRatioRange : mAspectRatioRange; } - checkPowerOfTwo( - blockSize.getWidth(), "block-size width must be power of two"); - checkPowerOfTwo( - blockSize.getHeight(), "block-size height must be power of two"); + private void initWithPlatformLimits() { + mBitrateRange = BITRATE_RANGE; - checkPowerOfTwo( - alignment.getWidth(), "alignment width must be power of two"); - checkPowerOfTwo( - alignment.getHeight(), "alignment height must be power of two"); + mWidthRange = getSizeRange(); + mHeightRange = getSizeRange(); + mFrameRateRange = FRAME_RATE_RANGE; - // update block-size and alignment - applyMacroBlockLimits( - Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, - Long.MAX_VALUE, blockSize.getWidth(), blockSize.getHeight(), - alignment.getWidth(), alignment.getHeight()); + mHorizontalBlockRange = getSizeRange(); + mVerticalBlockRange = getSizeRange(); - if ((mParent.mError & ERROR_UNSUPPORTED) != 0 || mAllowMbOverride) { - // codec supports profiles that we don't know. - // Use supplied values clipped to platform limits - if (widths != null) { - mWidthRange = getSizeRange().intersect(widths); - } - if (heights != null) { - mHeightRange = getSizeRange().intersect(heights); - } - if (counts != null) { - mBlockCountRange = POSITIVE_INTEGERS.intersect( - Utils.factorRange(counts, mBlockWidth * mBlockHeight - / blockSize.getWidth() / blockSize.getHeight())); + // full positive ranges are supported as these get calculated + mBlockCountRange = POSITIVE_INTEGERS; + mBlocksPerSecondRange = POSITIVE_LONGS; + + mBlockAspectRatioRange = POSITIVE_RATIONALS; + mAspectRatioRange = POSITIVE_RATIONALS; + + mWidthAlignment = 1; + mHeightAlignment = 1; + mBlockWidth = 1; + mBlockHeight = 1; + mSmallerDimensionUpperLimit = getSizeRange().getUpper(); + } + + private @Nullable List<PerformancePoint> getPerformancePoints(Map<String, Object> map) { + Vector<PerformancePoint> ret = new Vector<>(); + final String prefix = "performance-point-"; + Set<String> keys = map.keySet(); + for (String key : keys) { + // looking for: performance-point-WIDTHxHEIGHT-range + if (!key.startsWith(prefix)) { + continue; + } + String subKey = key.substring(prefix.length()); + if (subKey.equals("none") && ret.size() == 0) { + // This means that component knowingly did not publish performance points. + // This is different from when the component forgot to publish performance + // points. + return Collections.unmodifiableList(ret); + } + String[] temp = key.split("-"); + if (temp.length != 4) { + continue; + } + String sizeStr = temp[2]; + Size size = Utils.parseSize(sizeStr, null); + if (size == null || size.getWidth() * size.getHeight() <= 0) { + continue; + } + Range<Long> range = Utils.parseLongRange(map.get(key), null); + if (range == null || range.getLower() < 0 || range.getUpper() < 0) { + continue; + } + PerformancePoint given = new PerformancePoint( + size.getWidth(), size.getHeight(), range.getLower().intValue(), + range.getUpper().intValue(), new Size(mBlockWidth, mBlockHeight)); + PerformancePoint rotated = new PerformancePoint( + size.getHeight(), size.getWidth(), range.getLower().intValue(), + range.getUpper().intValue(), new Size(mBlockWidth, mBlockHeight)); + ret.add(given); + if (!given.covers(rotated)) { + ret.add(rotated); + } } - if (blockRates != null) { - mBlocksPerSecondRange = POSITIVE_LONGS.intersect( - Utils.factorRange(blockRates, mBlockWidth * mBlockHeight - / blockSize.getWidth() / blockSize.getHeight())); + + // check if the component specified no performance point indication + if (ret.size() == 0) { + return null; } - if (blockRatios != null) { - mBlockAspectRatioRange = POSITIVE_RATIONALS.intersect( - Utils.scaleRange(blockRatios, - mBlockHeight / blockSize.getHeight(), - mBlockWidth / blockSize.getWidth())); + + // sort reversed by area first, then by frame rate + ret.sort((a, b) -> + -((a.getMaxMacroBlocks() != b.getMaxMacroBlocks()) ? + (a.getMaxMacroBlocks() < b.getMaxMacroBlocks() ? -1 : 1) : + (a.getMaxMacroBlockRate() != b.getMaxMacroBlockRate()) ? + (a.getMaxMacroBlockRate() < b.getMaxMacroBlockRate() ? -1 : 1) : + (a.getMaxFrameRate() != b.getMaxFrameRate()) ? + (a.getMaxFrameRate() < b.getMaxFrameRate() ? -1 : 1) : 0)); + + return Collections.unmodifiableList(ret); + } + + private Map<Size, Range<Long>> getMeasuredFrameRates(Map<String, Object> map) { + Map<Size, Range<Long>> ret = new HashMap<Size, Range<Long>>(); + final String prefix = "measured-frame-rate-"; + Set<String> keys = map.keySet(); + for (String key : keys) { + // looking for: measured-frame-rate-WIDTHxHEIGHT-range + if (!key.startsWith(prefix)) { + continue; + } + String subKey = key.substring(prefix.length()); + String[] temp = key.split("-"); + if (temp.length != 5) { + continue; + } + String sizeStr = temp[3]; + Size size = Utils.parseSize(sizeStr, null); + if (size == null || size.getWidth() * size.getHeight() <= 0) { + continue; + } + Range<Long> range = Utils.parseLongRange(map.get(key), null); + if (range == null || range.getLower() < 0 || range.getUpper() < 0) { + continue; + } + ret.put(size, range); } - if (ratios != null) { - mAspectRatioRange = POSITIVE_RATIONALS.intersect(ratios); + return ret; + } + + private static Pair<Range<Integer>, Range<Integer>> parseWidthHeightRanges(Object o) { + Pair<Size, Size> range = Utils.parseSizeRange(o); + if (range != null) { + try { + return Pair.create( + Range.create(range.first.getWidth(), range.second.getWidth()), + Range.create(range.first.getHeight(), range.second.getHeight())); + } catch (IllegalArgumentException e) { + Log.w(TAG, "could not parse size range '" + o + "'"); + } } - if (frameRates != null) { - mFrameRateRange = FRAME_RATE_RANGE.intersect(frameRates); + return null; + } + + /** @hide */ + public static int equivalentVP9Level(MediaFormat info) { + final Map<String, Object> map = info.getMap(); + + Size blockSize = Utils.parseSize(map.get("block-size"), new Size(8, 8)); + int BS = blockSize.getWidth() * blockSize.getHeight(); + + Range<Integer> counts = Utils.parseIntRange(map.get("block-count-range"), null); + int FS = counts == null ? 0 : BS * counts.getUpper(); + + Range<Long> blockRates = + Utils.parseLongRange(map.get("blocks-per-second-range"), null); + long SR = blockRates == null ? 0 : BS * blockRates.getUpper(); + + Pair<Range<Integer>, Range<Integer>> dimensionRanges = + parseWidthHeightRanges(map.get("size-range")); + int D = dimensionRanges == null ? 0 : Math.max( + dimensionRanges.first.getUpper(), dimensionRanges.second.getUpper()); + + Range<Integer> bitRates = Utils.parseIntRange(map.get("bitrate-range"), null); + int BR = bitRates == null ? 0 : Utils.divUp(bitRates.getUpper(), 1000); + + if (SR <= 829440 && FS <= 36864 && BR <= 200 && D <= 512) + return CodecProfileLevel.VP9Level1; + if (SR <= 2764800 && FS <= 73728 && BR <= 800 && D <= 768) + return CodecProfileLevel.VP9Level11; + if (SR <= 4608000 && FS <= 122880 && BR <= 1800 && D <= 960) + return CodecProfileLevel.VP9Level2; + if (SR <= 9216000 && FS <= 245760 && BR <= 3600 && D <= 1344) + return CodecProfileLevel.VP9Level21; + if (SR <= 20736000 && FS <= 552960 && BR <= 7200 && D <= 2048) + return CodecProfileLevel.VP9Level3; + if (SR <= 36864000 && FS <= 983040 && BR <= 12000 && D <= 2752) + return CodecProfileLevel.VP9Level31; + if (SR <= 83558400 && FS <= 2228224 && BR <= 18000 && D <= 4160) + return CodecProfileLevel.VP9Level4; + if (SR <= 160432128 && FS <= 2228224 && BR <= 30000 && D <= 4160) + return CodecProfileLevel.VP9Level41; + if (SR <= 311951360 && FS <= 8912896 && BR <= 60000 && D <= 8384) + return CodecProfileLevel.VP9Level5; + if (SR <= 588251136 && FS <= 8912896 && BR <= 120000 && D <= 8384) + return CodecProfileLevel.VP9Level51; + if (SR <= 1176502272 && FS <= 8912896 && BR <= 180000 && D <= 8384) + return CodecProfileLevel.VP9Level52; + if (SR <= 1176502272 && FS <= 35651584 && BR <= 180000 && D <= 16832) + return CodecProfileLevel.VP9Level6; + if (SR <= 2353004544L && FS <= 35651584 && BR <= 240000 && D <= 16832) + return CodecProfileLevel.VP9Level61; + if (SR <= 4706009088L && FS <= 35651584 && BR <= 480000 && D <= 16832) + return CodecProfileLevel.VP9Level62; + // returning largest level + return CodecProfileLevel.VP9Level62; + } + + private void parseFromInfo(MediaFormat info) { + final Map<String, Object> map = info.getMap(); + Size blockSize = new Size(mBlockWidth, mBlockHeight); + Size alignment = new Size(mWidthAlignment, mHeightAlignment); + Range<Integer> counts = null, widths = null, heights = null; + Range<Integer> frameRates = null, bitRates = null; + Range<Long> blockRates = null; + Range<Rational> ratios = null, blockRatios = null; + + blockSize = Utils.parseSize(map.get("block-size"), blockSize); + alignment = Utils.parseSize(map.get("alignment"), alignment); + counts = Utils.parseIntRange(map.get("block-count-range"), null); + blockRates = + Utils.parseLongRange(map.get("blocks-per-second-range"), null); + mMeasuredFrameRates = getMeasuredFrameRates(map); + mPerformancePoints = getPerformancePoints(map); + Pair<Range<Integer>, Range<Integer>> sizeRanges = + parseWidthHeightRanges(map.get("size-range")); + if (sizeRanges != null) { + widths = sizeRanges.first; + heights = sizeRanges.second; } - if (bitRates != null) { - // only allow bitrate override if unsupported profiles were encountered - if ((mParent.mError & ERROR_UNSUPPORTED) != 0) { - mBitrateRange = BITRATE_RANGE.intersect(bitRates); + // for now this just means using the smaller max size as 2nd + // upper limit. + // for now we are keeping the profile specific "width/height + // in macroblocks" limits. + if (map.containsKey("feature-can-swap-width-height")) { + if (widths != null) { + mSmallerDimensionUpperLimit = + Math.min(widths.getUpper(), heights.getUpper()); + widths = heights = widths.extend(heights); } else { - mBitrateRange = mBitrateRange.intersect(bitRates); + Log.w(TAG, "feature can-swap-width-height is best used with size-range"); + mSmallerDimensionUpperLimit = + Math.min(mWidthRange.getUpper(), mHeightRange.getUpper()); + mWidthRange = mHeightRange = mWidthRange.extend(mHeightRange); } } - } else { - // no unsupported profile/levels, so restrict values to known limits - if (widths != null) { - mWidthRange = mWidthRange.intersect(widths); - } - if (heights != null) { - mHeightRange = mHeightRange.intersect(heights); - } - if (counts != null) { - mBlockCountRange = mBlockCountRange.intersect( - Utils.factorRange(counts, mBlockWidth * mBlockHeight - / blockSize.getWidth() / blockSize.getHeight())); - } - if (blockRates != null) { - mBlocksPerSecondRange = mBlocksPerSecondRange.intersect( - Utils.factorRange(blockRates, mBlockWidth * mBlockHeight - / blockSize.getWidth() / blockSize.getHeight())); - } - if (blockRatios != null) { - mBlockAspectRatioRange = mBlockAspectRatioRange.intersect( - Utils.scaleRange(blockRatios, - mBlockHeight / blockSize.getHeight(), - mBlockWidth / blockSize.getWidth())); - } - if (ratios != null) { - mAspectRatioRange = mAspectRatioRange.intersect(ratios); - } + + ratios = Utils.parseRationalRange( + map.get("block-aspect-ratio-range"), null); + blockRatios = Utils.parseRationalRange( + map.get("pixel-aspect-ratio-range"), null); + frameRates = Utils.parseIntRange(map.get("frame-rate-range"), null); if (frameRates != null) { - mFrameRateRange = mFrameRateRange.intersect(frameRates); + try { + frameRates = frameRates.intersect(FRAME_RATE_RANGE); + } catch (IllegalArgumentException e) { + Log.w(TAG, "frame rate range (" + frameRates + + ") is out of limits: " + FRAME_RATE_RANGE); + frameRates = null; + } } + bitRates = Utils.parseIntRange(map.get("bitrate-range"), null); if (bitRates != null) { - mBitrateRange = mBitrateRange.intersect(bitRates); + try { + bitRates = bitRates.intersect(BITRATE_RANGE); + } catch (IllegalArgumentException e) { + Log.w(TAG, "bitrate range (" + bitRates + + ") is out of limits: " + BITRATE_RANGE); + bitRates = null; + } } - } - updateLimits(); - } - private void applyBlockLimits( - int blockWidth, int blockHeight, - Range<Integer> counts, Range<Long> rates, Range<Rational> ratios) { - checkPowerOfTwo(blockWidth, "blockWidth must be a power of two"); - checkPowerOfTwo(blockHeight, "blockHeight must be a power of two"); - - final int newBlockWidth = Math.max(blockWidth, mBlockWidth); - final int newBlockHeight = Math.max(blockHeight, mBlockHeight); - - // factor will always be a power-of-2 - int factor = - newBlockWidth * newBlockHeight / mBlockWidth / mBlockHeight; - if (factor != 1) { - mBlockCountRange = Utils.factorRange(mBlockCountRange, factor); - mBlocksPerSecondRange = Utils.factorRange( - mBlocksPerSecondRange, factor); - mBlockAspectRatioRange = Utils.scaleRange( - mBlockAspectRatioRange, - newBlockHeight / mBlockHeight, - newBlockWidth / mBlockWidth); - mHorizontalBlockRange = Utils.factorRange( - mHorizontalBlockRange, newBlockWidth / mBlockWidth); - mVerticalBlockRange = Utils.factorRange( - mVerticalBlockRange, newBlockHeight / mBlockHeight); - } - factor = newBlockWidth * newBlockHeight / blockWidth / blockHeight; - if (factor != 1) { - counts = Utils.factorRange(counts, factor); - rates = Utils.factorRange(rates, factor); - ratios = Utils.scaleRange( - ratios, newBlockHeight / blockHeight, - newBlockWidth / blockWidth); - } - mBlockCountRange = mBlockCountRange.intersect(counts); - mBlocksPerSecondRange = mBlocksPerSecondRange.intersect(rates); - mBlockAspectRatioRange = mBlockAspectRatioRange.intersect(ratios); - mBlockWidth = newBlockWidth; - mBlockHeight = newBlockHeight; - } + checkPowerOfTwo( + blockSize.getWidth(), "block-size width must be power of two"); + checkPowerOfTwo( + blockSize.getHeight(), "block-size height must be power of two"); - private void applyAlignment(int widthAlignment, int heightAlignment) { - checkPowerOfTwo(widthAlignment, "widthAlignment must be a power of two"); - checkPowerOfTwo(heightAlignment, "heightAlignment must be a power of two"); + checkPowerOfTwo( + alignment.getWidth(), "alignment width must be power of two"); + checkPowerOfTwo( + alignment.getHeight(), "alignment height must be power of two"); - if (widthAlignment > mBlockWidth || heightAlignment > mBlockHeight) { - // maintain assumption that 0 < alignment <= block-size - applyBlockLimits( - Math.max(widthAlignment, mBlockWidth), - Math.max(heightAlignment, mBlockHeight), - POSITIVE_INTEGERS, POSITIVE_LONGS, POSITIVE_RATIONALS); + // update block-size and alignment + applyMacroBlockLimits( + Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, + Long.MAX_VALUE, blockSize.getWidth(), blockSize.getHeight(), + alignment.getWidth(), alignment.getHeight()); + + if ((mParent.mError & ERROR_UNSUPPORTED) != 0 || mAllowMbOverride) { + // codec supports profiles that we don't know. + // Use supplied values clipped to platform limits + if (widths != null) { + mWidthRange = getSizeRange().intersect(widths); + } + if (heights != null) { + mHeightRange = getSizeRange().intersect(heights); + } + if (counts != null) { + mBlockCountRange = POSITIVE_INTEGERS.intersect( + Utils.factorRange(counts, mBlockWidth * mBlockHeight + / blockSize.getWidth() / blockSize.getHeight())); + } + if (blockRates != null) { + mBlocksPerSecondRange = POSITIVE_LONGS.intersect( + Utils.factorRange(blockRates, mBlockWidth * mBlockHeight + / blockSize.getWidth() / blockSize.getHeight())); + } + if (blockRatios != null) { + mBlockAspectRatioRange = POSITIVE_RATIONALS.intersect( + Utils.scaleRange(blockRatios, + mBlockHeight / blockSize.getHeight(), + mBlockWidth / blockSize.getWidth())); + } + if (ratios != null) { + mAspectRatioRange = POSITIVE_RATIONALS.intersect(ratios); + } + if (frameRates != null) { + mFrameRateRange = FRAME_RATE_RANGE.intersect(frameRates); + } + if (bitRates != null) { + // only allow bitrate override if unsupported profiles were encountered + if ((mParent.mError & ERROR_UNSUPPORTED) != 0) { + mBitrateRange = BITRATE_RANGE.intersect(bitRates); + } else { + mBitrateRange = mBitrateRange.intersect(bitRates); + } + } + } else { + // no unsupported profile/levels, so restrict values to known limits + if (widths != null) { + mWidthRange = mWidthRange.intersect(widths); + } + if (heights != null) { + mHeightRange = mHeightRange.intersect(heights); + } + if (counts != null) { + mBlockCountRange = mBlockCountRange.intersect( + Utils.factorRange(counts, mBlockWidth * mBlockHeight + / blockSize.getWidth() / blockSize.getHeight())); + } + if (blockRates != null) { + mBlocksPerSecondRange = mBlocksPerSecondRange.intersect( + Utils.factorRange(blockRates, mBlockWidth * mBlockHeight + / blockSize.getWidth() / blockSize.getHeight())); + } + if (blockRatios != null) { + mBlockAspectRatioRange = mBlockAspectRatioRange.intersect( + Utils.scaleRange(blockRatios, + mBlockHeight / blockSize.getHeight(), + mBlockWidth / blockSize.getWidth())); + } + if (ratios != null) { + mAspectRatioRange = mAspectRatioRange.intersect(ratios); + } + if (frameRates != null) { + mFrameRateRange = mFrameRateRange.intersect(frameRates); + } + if (bitRates != null) { + mBitrateRange = mBitrateRange.intersect(bitRates); + } + } + updateLimits(); } - mWidthAlignment = Math.max(widthAlignment, mWidthAlignment); - mHeightAlignment = Math.max(heightAlignment, mHeightAlignment); + private void applyBlockLimits( + int blockWidth, int blockHeight, + Range<Integer> counts, Range<Long> rates, Range<Rational> ratios) { + checkPowerOfTwo(blockWidth, "blockWidth must be a power of two"); + checkPowerOfTwo(blockHeight, "blockHeight must be a power of two"); + + final int newBlockWidth = Math.max(blockWidth, mBlockWidth); + final int newBlockHeight = Math.max(blockHeight, mBlockHeight); + + // factor will always be a power-of-2 + int factor = + newBlockWidth * newBlockHeight / mBlockWidth / mBlockHeight; + if (factor != 1) { + mBlockCountRange = Utils.factorRange(mBlockCountRange, factor); + mBlocksPerSecondRange = Utils.factorRange( + mBlocksPerSecondRange, factor); + mBlockAspectRatioRange = Utils.scaleRange( + mBlockAspectRatioRange, + newBlockHeight / mBlockHeight, + newBlockWidth / mBlockWidth); + mHorizontalBlockRange = Utils.factorRange( + mHorizontalBlockRange, newBlockWidth / mBlockWidth); + mVerticalBlockRange = Utils.factorRange( + mVerticalBlockRange, newBlockHeight / mBlockHeight); + } + factor = newBlockWidth * newBlockHeight / blockWidth / blockHeight; + if (factor != 1) { + counts = Utils.factorRange(counts, factor); + rates = Utils.factorRange(rates, factor); + ratios = Utils.scaleRange( + ratios, newBlockHeight / blockHeight, + newBlockWidth / blockWidth); + } + mBlockCountRange = mBlockCountRange.intersect(counts); + mBlocksPerSecondRange = mBlocksPerSecondRange.intersect(rates); + mBlockAspectRatioRange = mBlockAspectRatioRange.intersect(ratios); + mBlockWidth = newBlockWidth; + mBlockHeight = newBlockHeight; + } - mWidthRange = Utils.alignRange(mWidthRange, mWidthAlignment); - mHeightRange = Utils.alignRange(mHeightRange, mHeightAlignment); - } + private void applyAlignment(int widthAlignment, int heightAlignment) { + checkPowerOfTwo(widthAlignment, "widthAlignment must be a power of two"); + checkPowerOfTwo(heightAlignment, "heightAlignment must be a power of two"); - private void updateLimits() { - // pixels -> blocks <- counts - mHorizontalBlockRange = mHorizontalBlockRange.intersect( - Utils.factorRange(mWidthRange, mBlockWidth)); - mHorizontalBlockRange = mHorizontalBlockRange.intersect( - Range.create( - mBlockCountRange.getLower() / mVerticalBlockRange.getUpper(), - mBlockCountRange.getUpper() / mVerticalBlockRange.getLower())); - mVerticalBlockRange = mVerticalBlockRange.intersect( - Utils.factorRange(mHeightRange, mBlockHeight)); - mVerticalBlockRange = mVerticalBlockRange.intersect( - Range.create( - mBlockCountRange.getLower() / mHorizontalBlockRange.getUpper(), - mBlockCountRange.getUpper() / mHorizontalBlockRange.getLower())); - mBlockCountRange = mBlockCountRange.intersect( - Range.create( - mHorizontalBlockRange.getLower() - * mVerticalBlockRange.getLower(), - mHorizontalBlockRange.getUpper() - * mVerticalBlockRange.getUpper())); - mBlockAspectRatioRange = mBlockAspectRatioRange.intersect( - new Rational( - mHorizontalBlockRange.getLower(), mVerticalBlockRange.getUpper()), - new Rational( - mHorizontalBlockRange.getUpper(), mVerticalBlockRange.getLower())); - - // blocks -> pixels - mWidthRange = mWidthRange.intersect( - (mHorizontalBlockRange.getLower() - 1) * mBlockWidth + mWidthAlignment, - mHorizontalBlockRange.getUpper() * mBlockWidth); - mHeightRange = mHeightRange.intersect( - (mVerticalBlockRange.getLower() - 1) * mBlockHeight + mHeightAlignment, - mVerticalBlockRange.getUpper() * mBlockHeight); - mAspectRatioRange = mAspectRatioRange.intersect( - new Rational(mWidthRange.getLower(), mHeightRange.getUpper()), - new Rational(mWidthRange.getUpper(), mHeightRange.getLower())); - - mSmallerDimensionUpperLimit = Math.min( - mSmallerDimensionUpperLimit, - Math.min(mWidthRange.getUpper(), mHeightRange.getUpper())); - - // blocks -> rate - mBlocksPerSecondRange = mBlocksPerSecondRange.intersect( - mBlockCountRange.getLower() * (long)mFrameRateRange.getLower(), - mBlockCountRange.getUpper() * (long)mFrameRateRange.getUpper()); - mFrameRateRange = mFrameRateRange.intersect( - (int)(mBlocksPerSecondRange.getLower() - / mBlockCountRange.getUpper()), - (int)(mBlocksPerSecondRange.getUpper() - / (double)mBlockCountRange.getLower())); - } + if (widthAlignment > mBlockWidth || heightAlignment > mBlockHeight) { + // maintain assumption that 0 < alignment <= block-size + applyBlockLimits( + Math.max(widthAlignment, mBlockWidth), + Math.max(heightAlignment, mBlockHeight), + POSITIVE_INTEGERS, POSITIVE_LONGS, POSITIVE_RATIONALS); + } - private void applyMacroBlockLimits( - int maxHorizontalBlocks, int maxVerticalBlocks, - int maxBlocks, long maxBlocksPerSecond, - int blockWidth, int blockHeight, - int widthAlignment, int heightAlignment) { - applyMacroBlockLimits( - 1 /* minHorizontalBlocks */, 1 /* minVerticalBlocks */, - maxHorizontalBlocks, maxVerticalBlocks, - maxBlocks, maxBlocksPerSecond, - blockWidth, blockHeight, widthAlignment, heightAlignment); - } + mWidthAlignment = Math.max(widthAlignment, mWidthAlignment); + mHeightAlignment = Math.max(heightAlignment, mHeightAlignment); - private void applyMacroBlockLimits( - int minHorizontalBlocks, int minVerticalBlocks, - int maxHorizontalBlocks, int maxVerticalBlocks, - int maxBlocks, long maxBlocksPerSecond, - int blockWidth, int blockHeight, - int widthAlignment, int heightAlignment) { - applyAlignment(widthAlignment, heightAlignment); - applyBlockLimits( - blockWidth, blockHeight, Range.create(1, maxBlocks), - Range.create(1L, maxBlocksPerSecond), - Range.create( - new Rational(1, maxVerticalBlocks), - new Rational(maxHorizontalBlocks, 1))); - mHorizontalBlockRange = - mHorizontalBlockRange.intersect( - Utils.divUp(minHorizontalBlocks, (mBlockWidth / blockWidth)), - maxHorizontalBlocks / (mBlockWidth / blockWidth)); - mVerticalBlockRange = - mVerticalBlockRange.intersect( - Utils.divUp(minVerticalBlocks, (mBlockHeight / blockHeight)), - maxVerticalBlocks / (mBlockHeight / blockHeight)); - } + mWidthRange = Utils.alignRange(mWidthRange, mWidthAlignment); + mHeightRange = Utils.alignRange(mHeightRange, mHeightAlignment); + } - private void applyLevelLimits() { - long maxBlocksPerSecond = 0; - int maxBlocks = 0; - int maxBps = 0; - int maxDPBBlocks = 0; - - int errors = ERROR_NONE_SUPPORTED; - CodecProfileLevel[] profileLevels = mParent.profileLevels; - String mime = mParent.getMimeType(); - - if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AVC)) { - maxBlocks = 99; - maxBlocksPerSecond = 1485; - maxBps = 64000; - maxDPBBlocks = 396; - for (CodecProfileLevel profileLevel: profileLevels) { - int MBPS = 0, FS = 0, BR = 0, DPB = 0; - boolean supported = true; - switch (profileLevel.level) { - case CodecProfileLevel.AVCLevel1: - MBPS = 1485; FS = 99; BR = 64; DPB = 396; break; - case CodecProfileLevel.AVCLevel1b: - MBPS = 1485; FS = 99; BR = 128; DPB = 396; break; - case CodecProfileLevel.AVCLevel11: - MBPS = 3000; FS = 396; BR = 192; DPB = 900; break; - case CodecProfileLevel.AVCLevel12: - MBPS = 6000; FS = 396; BR = 384; DPB = 2376; break; - case CodecProfileLevel.AVCLevel13: - MBPS = 11880; FS = 396; BR = 768; DPB = 2376; break; - case CodecProfileLevel.AVCLevel2: - MBPS = 11880; FS = 396; BR = 2000; DPB = 2376; break; - case CodecProfileLevel.AVCLevel21: - MBPS = 19800; FS = 792; BR = 4000; DPB = 4752; break; - case CodecProfileLevel.AVCLevel22: - MBPS = 20250; FS = 1620; BR = 4000; DPB = 8100; break; - case CodecProfileLevel.AVCLevel3: - MBPS = 40500; FS = 1620; BR = 10000; DPB = 8100; break; - case CodecProfileLevel.AVCLevel31: - MBPS = 108000; FS = 3600; BR = 14000; DPB = 18000; break; - case CodecProfileLevel.AVCLevel32: - MBPS = 216000; FS = 5120; BR = 20000; DPB = 20480; break; - case CodecProfileLevel.AVCLevel4: - MBPS = 245760; FS = 8192; BR = 20000; DPB = 32768; break; - case CodecProfileLevel.AVCLevel41: - MBPS = 245760; FS = 8192; BR = 50000; DPB = 32768; break; - case CodecProfileLevel.AVCLevel42: - MBPS = 522240; FS = 8704; BR = 50000; DPB = 34816; break; - case CodecProfileLevel.AVCLevel5: - MBPS = 589824; FS = 22080; BR = 135000; DPB = 110400; break; - case CodecProfileLevel.AVCLevel51: - MBPS = 983040; FS = 36864; BR = 240000; DPB = 184320; break; - case CodecProfileLevel.AVCLevel52: - MBPS = 2073600; FS = 36864; BR = 240000; DPB = 184320; break; - case CodecProfileLevel.AVCLevel6: - MBPS = 4177920; FS = 139264; BR = 240000; DPB = 696320; break; - case CodecProfileLevel.AVCLevel61: - MBPS = 8355840; FS = 139264; BR = 480000; DPB = 696320; break; - case CodecProfileLevel.AVCLevel62: - MBPS = 16711680; FS = 139264; BR = 800000; DPB = 696320; break; - default: - Log.w(TAG, "Unrecognized level " - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - switch (profileLevel.profile) { - case CodecProfileLevel.AVCProfileConstrainedHigh: - case CodecProfileLevel.AVCProfileHigh: - BR *= 1250; break; - case CodecProfileLevel.AVCProfileHigh10: - BR *= 3000; break; - case CodecProfileLevel.AVCProfileExtended: - case CodecProfileLevel.AVCProfileHigh422: - case CodecProfileLevel.AVCProfileHigh444: - Log.w(TAG, "Unsupported profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNSUPPORTED; - supported = false; - // fall through - treat as base profile - case CodecProfileLevel.AVCProfileConstrainedBaseline: - case CodecProfileLevel.AVCProfileBaseline: - case CodecProfileLevel.AVCProfileMain: - BR *= 1000; break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - BR *= 1000; - } - if (supported) { - errors &= ~ERROR_NONE_SUPPORTED; - } - maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); - maxBlocks = Math.max(FS, maxBlocks); - maxBps = Math.max(BR, maxBps); - maxDPBBlocks = Math.max(maxDPBBlocks, DPB); - } + private void updateLimits() { + // pixels -> blocks <- counts + mHorizontalBlockRange = mHorizontalBlockRange.intersect( + Utils.factorRange(mWidthRange, mBlockWidth)); + mHorizontalBlockRange = mHorizontalBlockRange.intersect( + Range.create( + mBlockCountRange.getLower() / mVerticalBlockRange.getUpper(), + mBlockCountRange.getUpper() / mVerticalBlockRange.getLower())); + mVerticalBlockRange = mVerticalBlockRange.intersect( + Utils.factorRange(mHeightRange, mBlockHeight)); + mVerticalBlockRange = mVerticalBlockRange.intersect( + Range.create( + mBlockCountRange.getLower() / mHorizontalBlockRange.getUpper(), + mBlockCountRange.getUpper() / mHorizontalBlockRange.getLower())); + mBlockCountRange = mBlockCountRange.intersect( + Range.create( + mHorizontalBlockRange.getLower() + * mVerticalBlockRange.getLower(), + mHorizontalBlockRange.getUpper() + * mVerticalBlockRange.getUpper())); + mBlockAspectRatioRange = mBlockAspectRatioRange.intersect( + new Rational( + mHorizontalBlockRange.getLower(), mVerticalBlockRange.getUpper()), + new Rational( + mHorizontalBlockRange.getUpper(), mVerticalBlockRange.getLower())); + + // blocks -> pixels + mWidthRange = mWidthRange.intersect( + (mHorizontalBlockRange.getLower() - 1) * mBlockWidth + mWidthAlignment, + mHorizontalBlockRange.getUpper() * mBlockWidth); + mHeightRange = mHeightRange.intersect( + (mVerticalBlockRange.getLower() - 1) * mBlockHeight + mHeightAlignment, + mVerticalBlockRange.getUpper() * mBlockHeight); + mAspectRatioRange = mAspectRatioRange.intersect( + new Rational(mWidthRange.getLower(), mHeightRange.getUpper()), + new Rational(mWidthRange.getUpper(), mHeightRange.getLower())); + + mSmallerDimensionUpperLimit = Math.min( + mSmallerDimensionUpperLimit, + Math.min(mWidthRange.getUpper(), mHeightRange.getUpper())); + + // blocks -> rate + mBlocksPerSecondRange = mBlocksPerSecondRange.intersect( + mBlockCountRange.getLower() * (long)mFrameRateRange.getLower(), + mBlockCountRange.getUpper() * (long)mFrameRateRange.getUpper()); + mFrameRateRange = mFrameRateRange.intersect( + (int)(mBlocksPerSecondRange.getLower() + / mBlockCountRange.getUpper()), + (int)(mBlocksPerSecondRange.getUpper() + / (double)mBlockCountRange.getLower())); + } - int maxLengthInBlocks = (int)(Math.sqrt(maxBlocks * 8)); + private void applyMacroBlockLimits( + int maxHorizontalBlocks, int maxVerticalBlocks, + int maxBlocks, long maxBlocksPerSecond, + int blockWidth, int blockHeight, + int widthAlignment, int heightAlignment) { applyMacroBlockLimits( - maxLengthInBlocks, maxLengthInBlocks, + 1 /* minHorizontalBlocks */, 1 /* minVerticalBlocks */, + maxHorizontalBlocks, maxVerticalBlocks, maxBlocks, maxBlocksPerSecond, - 16 /* blockWidth */, 16 /* blockHeight */, - 1 /* widthAlignment */, 1 /* heightAlignment */); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_MPEG2)) { - int maxWidth = 11, maxHeight = 9, maxRate = 15; - maxBlocks = 99; - maxBlocksPerSecond = 1485; - maxBps = 64000; - for (CodecProfileLevel profileLevel: profileLevels) { - int MBPS = 0, FS = 0, BR = 0, FR = 0, W = 0, H = 0; - boolean supported = true; - switch (profileLevel.profile) { - case CodecProfileLevel.MPEG2ProfileSimple: - switch (profileLevel.level) { - case CodecProfileLevel.MPEG2LevelML: - FR = 30; W = 45; H = 36; MBPS = 40500; FS = 1620; BR = 15000; break; - default: - Log.w(TAG, "Unrecognized profile/level " - + profileLevel.profile + "/" - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - break; - case CodecProfileLevel.MPEG2ProfileMain: - switch (profileLevel.level) { - case CodecProfileLevel.MPEG2LevelLL: - FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 4000; break; - case CodecProfileLevel.MPEG2LevelML: - FR = 30; W = 45; H = 36; MBPS = 40500; FS = 1620; BR = 15000; break; - case CodecProfileLevel.MPEG2LevelH14: - FR = 60; W = 90; H = 68; MBPS = 183600; FS = 6120; BR = 60000; break; - case CodecProfileLevel.MPEG2LevelHL: - FR = 60; W = 120; H = 68; MBPS = 244800; FS = 8160; BR = 80000; break; - case CodecProfileLevel.MPEG2LevelHP: - FR = 60; W = 120; H = 68; MBPS = 489600; FS = 8160; BR = 80000; break; - default: - Log.w(TAG, "Unrecognized profile/level " - + profileLevel.profile + "/" - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - break; - case CodecProfileLevel.MPEG2Profile422: - case CodecProfileLevel.MPEG2ProfileSNR: - case CodecProfileLevel.MPEG2ProfileSpatial: - case CodecProfileLevel.MPEG2ProfileHigh: - Log.i(TAG, "Unsupported profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNSUPPORTED; - supported = false; - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + blockWidth, blockHeight, widthAlignment, heightAlignment); + } + + private void applyMacroBlockLimits( + int minHorizontalBlocks, int minVerticalBlocks, + int maxHorizontalBlocks, int maxVerticalBlocks, + int maxBlocks, long maxBlocksPerSecond, + int blockWidth, int blockHeight, + int widthAlignment, int heightAlignment) { + applyAlignment(widthAlignment, heightAlignment); + applyBlockLimits( + blockWidth, blockHeight, Range.create(1, maxBlocks), + Range.create(1L, maxBlocksPerSecond), + Range.create( + new Rational(1, maxVerticalBlocks), + new Rational(maxHorizontalBlocks, 1))); + mHorizontalBlockRange = + mHorizontalBlockRange.intersect( + Utils.divUp(minHorizontalBlocks, (mBlockWidth / blockWidth)), + maxHorizontalBlocks / (mBlockWidth / blockWidth)); + mVerticalBlockRange = + mVerticalBlockRange.intersect( + Utils.divUp(minVerticalBlocks, (mBlockHeight / blockHeight)), + maxVerticalBlocks / (mBlockHeight / blockHeight)); + } + + private void applyLevelLimits() { + long maxBlocksPerSecond = 0; + int maxBlocks = 0; + int maxBps = 0; + int maxDPBBlocks = 0; + + int errors = ERROR_NONE_SUPPORTED; + CodecProfileLevel[] profileLevels = mParent.getProfileLevels(); + String mime = mParent.getMimeType(); + + if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AVC)) { + maxBlocks = 99; + maxBlocksPerSecond = 1485; + maxBps = 64000; + maxDPBBlocks = 396; + for (CodecProfileLevel profileLevel: profileLevels) { + int MBPS = 0, FS = 0, BR = 0, DPB = 0; + boolean supported = true; + switch (profileLevel.level) { + case CodecProfileLevel.AVCLevel1: + MBPS = 1485; FS = 99; BR = 64; DPB = 396; break; + case CodecProfileLevel.AVCLevel1b: + MBPS = 1485; FS = 99; BR = 128; DPB = 396; break; + case CodecProfileLevel.AVCLevel11: + MBPS = 3000; FS = 396; BR = 192; DPB = 900; break; + case CodecProfileLevel.AVCLevel12: + MBPS = 6000; FS = 396; BR = 384; DPB = 2376; break; + case CodecProfileLevel.AVCLevel13: + MBPS = 11880; FS = 396; BR = 768; DPB = 2376; break; + case CodecProfileLevel.AVCLevel2: + MBPS = 11880; FS = 396; BR = 2000; DPB = 2376; break; + case CodecProfileLevel.AVCLevel21: + MBPS = 19800; FS = 792; BR = 4000; DPB = 4752; break; + case CodecProfileLevel.AVCLevel22: + MBPS = 20250; FS = 1620; BR = 4000; DPB = 8100; break; + case CodecProfileLevel.AVCLevel3: + MBPS = 40500; FS = 1620; BR = 10000; DPB = 8100; break; + case CodecProfileLevel.AVCLevel31: + MBPS = 108000; FS = 3600; BR = 14000; DPB = 18000; break; + case CodecProfileLevel.AVCLevel32: + MBPS = 216000; FS = 5120; BR = 20000; DPB = 20480; break; + case CodecProfileLevel.AVCLevel4: + MBPS = 245760; FS = 8192; BR = 20000; DPB = 32768; break; + case CodecProfileLevel.AVCLevel41: + MBPS = 245760; FS = 8192; BR = 50000; DPB = 32768; break; + case CodecProfileLevel.AVCLevel42: + MBPS = 522240; FS = 8704; BR = 50000; DPB = 34816; break; + case CodecProfileLevel.AVCLevel5: + MBPS = 589824; FS = 22080; BR = 135000; DPB = 110400; break; + case CodecProfileLevel.AVCLevel51: + MBPS = 983040; FS = 36864; BR = 240000; DPB = 184320; break; + case CodecProfileLevel.AVCLevel52: + MBPS = 2073600; FS = 36864; BR = 240000; DPB = 184320; break; + case CodecProfileLevel.AVCLevel6: + MBPS = 4177920; FS = 139264; BR = 240000; DPB = 696320; break; + case CodecProfileLevel.AVCLevel61: + MBPS = 8355840; FS = 139264; BR = 480000; DPB = 696320; break; + case CodecProfileLevel.AVCLevel62: + MBPS = 16711680; FS = 139264; BR = 800000; DPB = 696320; break; + default: + Log.w(TAG, "Unrecognized level " + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + switch (profileLevel.profile) { + case CodecProfileLevel.AVCProfileConstrainedHigh: + case CodecProfileLevel.AVCProfileHigh: + BR *= 1250; break; + case CodecProfileLevel.AVCProfileHigh10: + BR *= 3000; break; + case CodecProfileLevel.AVCProfileExtended: + case CodecProfileLevel.AVCProfileHigh422: + case CodecProfileLevel.AVCProfileHigh444: + Log.w(TAG, "Unsupported profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNSUPPORTED; + supported = false; + // fall through - treat as base profile + case CodecProfileLevel.AVCProfileConstrainedBaseline: + case CodecProfileLevel.AVCProfileBaseline: + case CodecProfileLevel.AVCProfileMain: + BR *= 1000; break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + BR *= 1000; + } + if (supported) { + errors &= ~ERROR_NONE_SUPPORTED; + } + maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); + maxBlocks = Math.max(FS, maxBlocks); + maxBps = Math.max(BR, maxBps); + maxDPBBlocks = Math.max(maxDPBBlocks, DPB); } - if (supported) { - errors &= ~ERROR_NONE_SUPPORTED; + + int maxLengthInBlocks = (int)(Math.sqrt(maxBlocks * 8)); + applyMacroBlockLimits( + maxLengthInBlocks, maxLengthInBlocks, + maxBlocks, maxBlocksPerSecond, + 16 /* blockWidth */, 16 /* blockHeight */, + 1 /* widthAlignment */, 1 /* heightAlignment */); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_MPEG2)) { + int maxWidth = 11, maxHeight = 9, maxRate = 15; + maxBlocks = 99; + maxBlocksPerSecond = 1485; + maxBps = 64000; + for (CodecProfileLevel profileLevel: profileLevels) { + int MBPS = 0, FS = 0, BR = 0, FR = 0, W = 0, H = 0; + boolean supported = true; + switch (profileLevel.profile) { + case CodecProfileLevel.MPEG2ProfileSimple: + switch (profileLevel.level) { + case CodecProfileLevel.MPEG2LevelML: + FR = 30; W = 45; H = 36; MBPS = 40500; FS = 1620; BR = 15000; break; + default: + Log.w(TAG, "Unrecognized profile/level " + + profileLevel.profile + "/" + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + break; + case CodecProfileLevel.MPEG2ProfileMain: + switch (profileLevel.level) { + case CodecProfileLevel.MPEG2LevelLL: + FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 4000; break; + case CodecProfileLevel.MPEG2LevelML: + FR = 30; W = 45; H = 36; MBPS = 40500; FS = 1620; BR = 15000; break; + case CodecProfileLevel.MPEG2LevelH14: + FR = 60; W = 90; H = 68; MBPS = 183600; FS = 6120; BR = 60000; break; + case CodecProfileLevel.MPEG2LevelHL: + FR = 60; W = 120; H = 68; MBPS = 244800; FS = 8160; BR = 80000; break; + case CodecProfileLevel.MPEG2LevelHP: + FR = 60; W = 120; H = 68; MBPS = 489600; FS = 8160; BR = 80000; break; + default: + Log.w(TAG, "Unrecognized profile/level " + + profileLevel.profile + "/" + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + break; + case CodecProfileLevel.MPEG2Profile422: + case CodecProfileLevel.MPEG2ProfileSNR: + case CodecProfileLevel.MPEG2ProfileSpatial: + case CodecProfileLevel.MPEG2ProfileHigh: + Log.i(TAG, "Unsupported profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNSUPPORTED; + supported = false; + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + if (supported) { + errors &= ~ERROR_NONE_SUPPORTED; + } + maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); + maxBlocks = Math.max(FS, maxBlocks); + maxBps = Math.max(BR * 1000, maxBps); + maxWidth = Math.max(W, maxWidth); + maxHeight = Math.max(H, maxHeight); + maxRate = Math.max(FR, maxRate); } - maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); - maxBlocks = Math.max(FS, maxBlocks); - maxBps = Math.max(BR * 1000, maxBps); - maxWidth = Math.max(W, maxWidth); - maxHeight = Math.max(H, maxHeight); - maxRate = Math.max(FR, maxRate); - } - applyMacroBlockLimits(maxWidth, maxHeight, - maxBlocks, maxBlocksPerSecond, - 16 /* blockWidth */, 16 /* blockHeight */, - 1 /* widthAlignment */, 1 /* heightAlignment */); - mFrameRateRange = mFrameRateRange.intersect(12, maxRate); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_MPEG4)) { - int maxWidth = 11, maxHeight = 9, maxRate = 15; - maxBlocks = 99; - maxBlocksPerSecond = 1485; - maxBps = 64000; - for (CodecProfileLevel profileLevel: profileLevels) { - int MBPS = 0, FS = 0, BR = 0, FR = 0, W = 0, H = 0; - boolean strict = false; // true: W, H and FR are individual max limits - boolean supported = true; - switch (profileLevel.profile) { - case CodecProfileLevel.MPEG4ProfileSimple: - switch (profileLevel.level) { - case CodecProfileLevel.MPEG4Level0: - strict = true; - FR = 15; W = 11; H = 9; MBPS = 1485; FS = 99; BR = 64; break; - case CodecProfileLevel.MPEG4Level1: - FR = 30; W = 11; H = 9; MBPS = 1485; FS = 99; BR = 64; break; - case CodecProfileLevel.MPEG4Level0b: - strict = true; - FR = 15; W = 11; H = 9; MBPS = 1485; FS = 99; BR = 128; break; - case CodecProfileLevel.MPEG4Level2: - FR = 30; W = 22; H = 18; MBPS = 5940; FS = 396; BR = 128; break; - case CodecProfileLevel.MPEG4Level3: - FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 384; break; - case CodecProfileLevel.MPEG4Level4a: - FR = 30; W = 40; H = 30; MBPS = 36000; FS = 1200; BR = 4000; break; - case CodecProfileLevel.MPEG4Level5: - FR = 30; W = 45; H = 36; MBPS = 40500; FS = 1620; BR = 8000; break; - case CodecProfileLevel.MPEG4Level6: - FR = 30; W = 80; H = 45; MBPS = 108000; FS = 3600; BR = 12000; break; - default: - Log.w(TAG, "Unrecognized profile/level " - + profileLevel.profile + "/" - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - break; - case CodecProfileLevel.MPEG4ProfileAdvancedSimple: - switch (profileLevel.level) { - case CodecProfileLevel.MPEG4Level0: - case CodecProfileLevel.MPEG4Level1: - FR = 30; W = 11; H = 9; MBPS = 2970; FS = 99; BR = 128; break; - case CodecProfileLevel.MPEG4Level2: - FR = 30; W = 22; H = 18; MBPS = 5940; FS = 396; BR = 384; break; - case CodecProfileLevel.MPEG4Level3: - FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 768; break; - case CodecProfileLevel.MPEG4Level3b: - FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 1500; break; - case CodecProfileLevel.MPEG4Level4: - FR = 30; W = 44; H = 36; MBPS = 23760; FS = 792; BR = 3000; break; - case CodecProfileLevel.MPEG4Level5: - FR = 30; W = 45; H = 36; MBPS = 48600; FS = 1620; BR = 8000; break; - default: - Log.w(TAG, "Unrecognized profile/level " - + profileLevel.profile + "/" - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - break; - case CodecProfileLevel.MPEG4ProfileMain: // 2-4 - case CodecProfileLevel.MPEG4ProfileNbit: // 2 - case CodecProfileLevel.MPEG4ProfileAdvancedRealTime: // 1-4 - case CodecProfileLevel.MPEG4ProfileCoreScalable: // 1-3 - case CodecProfileLevel.MPEG4ProfileAdvancedCoding: // 1-4 - case CodecProfileLevel.MPEG4ProfileCore: // 1-2 - case CodecProfileLevel.MPEG4ProfileAdvancedCore: // 1-4 - case CodecProfileLevel.MPEG4ProfileSimpleScalable: // 0-2 - case CodecProfileLevel.MPEG4ProfileHybrid: // 1-2 - - // Studio profiles are not supported by our codecs. - - // Only profiles that can decode simple object types are considered. - // The following profiles are not able to. - case CodecProfileLevel.MPEG4ProfileBasicAnimated: // 1-2 - case CodecProfileLevel.MPEG4ProfileScalableTexture: // 1 - case CodecProfileLevel.MPEG4ProfileSimpleFace: // 1-2 - case CodecProfileLevel.MPEG4ProfileAdvancedScalable: // 1-3 - case CodecProfileLevel.MPEG4ProfileSimpleFBA: // 1-2 - Log.i(TAG, "Unsupported profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNSUPPORTED; - supported = false; - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + applyMacroBlockLimits(maxWidth, maxHeight, + maxBlocks, maxBlocksPerSecond, + 16 /* blockWidth */, 16 /* blockHeight */, + 1 /* widthAlignment */, 1 /* heightAlignment */); + mFrameRateRange = mFrameRateRange.intersect(12, maxRate); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_MPEG4)) { + int maxWidth = 11, maxHeight = 9, maxRate = 15; + maxBlocks = 99; + maxBlocksPerSecond = 1485; + maxBps = 64000; + for (CodecProfileLevel profileLevel: profileLevels) { + int MBPS = 0, FS = 0, BR = 0, FR = 0, W = 0, H = 0; + boolean strict = false; // true: W, H and FR are individual max limits + boolean supported = true; + switch (profileLevel.profile) { + case CodecProfileLevel.MPEG4ProfileSimple: + switch (profileLevel.level) { + case CodecProfileLevel.MPEG4Level0: + strict = true; + FR = 15; W = 11; H = 9; MBPS = 1485; FS = 99; BR = 64; break; + case CodecProfileLevel.MPEG4Level1: + FR = 30; W = 11; H = 9; MBPS = 1485; FS = 99; BR = 64; break; + case CodecProfileLevel.MPEG4Level0b: + strict = true; + FR = 15; W = 11; H = 9; MBPS = 1485; FS = 99; BR = 128; break; + case CodecProfileLevel.MPEG4Level2: + FR = 30; W = 22; H = 18; MBPS = 5940; FS = 396; BR = 128; break; + case CodecProfileLevel.MPEG4Level3: + FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 384; break; + case CodecProfileLevel.MPEG4Level4a: + FR = 30; W = 40; H = 30; MBPS = 36000; FS = 1200; BR = 4000; break; + case CodecProfileLevel.MPEG4Level5: + FR = 30; W = 45; H = 36; MBPS = 40500; FS = 1620; BR = 8000; break; + case CodecProfileLevel.MPEG4Level6: + FR = 30; W = 80; H = 45; MBPS = 108000; FS = 3600; BR = 12000; break; + default: + Log.w(TAG, "Unrecognized profile/level " + + profileLevel.profile + "/" + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + break; + case CodecProfileLevel.MPEG4ProfileAdvancedSimple: + switch (profileLevel.level) { + case CodecProfileLevel.MPEG4Level0: + case CodecProfileLevel.MPEG4Level1: + FR = 30; W = 11; H = 9; MBPS = 2970; FS = 99; BR = 128; break; + case CodecProfileLevel.MPEG4Level2: + FR = 30; W = 22; H = 18; MBPS = 5940; FS = 396; BR = 384; break; + case CodecProfileLevel.MPEG4Level3: + FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 768; break; + case CodecProfileLevel.MPEG4Level3b: + FR = 30; W = 22; H = 18; MBPS = 11880; FS = 396; BR = 1500; break; + case CodecProfileLevel.MPEG4Level4: + FR = 30; W = 44; H = 36; MBPS = 23760; FS = 792; BR = 3000; break; + case CodecProfileLevel.MPEG4Level5: + FR = 30; W = 45; H = 36; MBPS = 48600; FS = 1620; BR = 8000; break; + default: + Log.w(TAG, "Unrecognized profile/level " + + profileLevel.profile + "/" + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + break; + case CodecProfileLevel.MPEG4ProfileMain: // 2-4 + case CodecProfileLevel.MPEG4ProfileNbit: // 2 + case CodecProfileLevel.MPEG4ProfileAdvancedRealTime: // 1-4 + case CodecProfileLevel.MPEG4ProfileCoreScalable: // 1-3 + case CodecProfileLevel.MPEG4ProfileAdvancedCoding: // 1-4 + case CodecProfileLevel.MPEG4ProfileCore: // 1-2 + case CodecProfileLevel.MPEG4ProfileAdvancedCore: // 1-4 + case CodecProfileLevel.MPEG4ProfileSimpleScalable: // 0-2 + case CodecProfileLevel.MPEG4ProfileHybrid: // 1-2 + + // Studio profiles are not supported by our codecs. + + // Only profiles that can decode simple object types are considered. + // The following profiles are not able to. + case CodecProfileLevel.MPEG4ProfileBasicAnimated: // 1-2 + case CodecProfileLevel.MPEG4ProfileScalableTexture: // 1 + case CodecProfileLevel.MPEG4ProfileSimpleFace: // 1-2 + case CodecProfileLevel.MPEG4ProfileAdvancedScalable: // 1-3 + case CodecProfileLevel.MPEG4ProfileSimpleFBA: // 1-2 + Log.i(TAG, "Unsupported profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNSUPPORTED; + supported = false; + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + if (supported) { + errors &= ~ERROR_NONE_SUPPORTED; + } + maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); + maxBlocks = Math.max(FS, maxBlocks); + maxBps = Math.max(BR * 1000, maxBps); + if (strict) { + maxWidth = Math.max(W, maxWidth); + maxHeight = Math.max(H, maxHeight); + maxRate = Math.max(FR, maxRate); + } else { + // assuming max 60 fps frame rate and 1:2 aspect ratio + int maxDim = (int)Math.sqrt(FS * 2); + maxWidth = Math.max(maxDim, maxWidth); + maxHeight = Math.max(maxDim, maxHeight); + maxRate = Math.max(Math.max(FR, 60), maxRate); + } } - if (supported) { + applyMacroBlockLimits(maxWidth, maxHeight, + maxBlocks, maxBlocksPerSecond, + 16 /* blockWidth */, 16 /* blockHeight */, + 1 /* widthAlignment */, 1 /* heightAlignment */); + mFrameRateRange = mFrameRateRange.intersect(12, maxRate); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_H263)) { + int maxWidth = 11, maxHeight = 9, maxRate = 15; + int minWidth = maxWidth, minHeight = maxHeight; + int minAlignment = 16; + maxBlocks = 99; + maxBlocksPerSecond = 1485; + maxBps = 64000; + for (CodecProfileLevel profileLevel: profileLevels) { + int MBPS = 0, BR = 0, FR = 0, W = 0, H = 0, minW = minWidth, minH = minHeight; + boolean strict = false; // true: support only sQCIF, QCIF (maybe CIF) + switch (profileLevel.level) { + case CodecProfileLevel.H263Level10: + strict = true; // only supports sQCIF & QCIF + FR = 15; W = 11; H = 9; BR = 1; MBPS = W * H * FR; break; + case CodecProfileLevel.H263Level20: + strict = true; // only supports sQCIF, QCIF & CIF + FR = 30; W = 22; H = 18; BR = 2; MBPS = W * H * 15; break; + case CodecProfileLevel.H263Level30: + strict = true; // only supports sQCIF, QCIF & CIF + FR = 30; W = 22; H = 18; BR = 6; MBPS = W * H * FR; break; + case CodecProfileLevel.H263Level40: + strict = true; // only supports sQCIF, QCIF & CIF + FR = 30; W = 22; H = 18; BR = 32; MBPS = W * H * FR; break; + case CodecProfileLevel.H263Level45: + // only implies level 10 support + strict = profileLevel.profile == CodecProfileLevel.H263ProfileBaseline + || profileLevel.profile == + CodecProfileLevel.H263ProfileBackwardCompatible; + if (!strict) { + minW = 1; minH = 1; minAlignment = 4; + } + FR = 15; W = 11; H = 9; BR = 2; MBPS = W * H * FR; break; + case CodecProfileLevel.H263Level50: + // only supports 50fps for H > 15 + minW = 1; minH = 1; minAlignment = 4; + FR = 60; W = 22; H = 18; BR = 64; MBPS = W * H * 50; break; + case CodecProfileLevel.H263Level60: + // only supports 50fps for H > 15 + minW = 1; minH = 1; minAlignment = 4; + FR = 60; W = 45; H = 18; BR = 128; MBPS = W * H * 50; break; + case CodecProfileLevel.H263Level70: + // only supports 50fps for H > 30 + minW = 1; minH = 1; minAlignment = 4; + FR = 60; W = 45; H = 36; BR = 256; MBPS = W * H * 50; break; + default: + Log.w(TAG, "Unrecognized profile/level " + profileLevel.profile + + "/" + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + switch (profileLevel.profile) { + case CodecProfileLevel.H263ProfileBackwardCompatible: + case CodecProfileLevel.H263ProfileBaseline: + case CodecProfileLevel.H263ProfileH320Coding: + case CodecProfileLevel.H263ProfileHighCompression: + case CodecProfileLevel.H263ProfileHighLatency: + case CodecProfileLevel.H263ProfileInterlace: + case CodecProfileLevel.H263ProfileInternet: + case CodecProfileLevel.H263ProfileISWV2: + case CodecProfileLevel.H263ProfileISWV3: + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + if (strict) { + // Strict levels define sub-QCIF min size and enumerated sizes. We + // cannot express support for "only sQCIF & QCIF (& CIF)" using + // VideoCapabilities but we can express "only QCIF (& CIF)", so set + // minimume size at QCIF.minW = 8; minH = 6; + minW = 11; minH = 9; + } else { + // any support for non-strict levels (including unrecognized profiles or + // levels) allow custom frame size support beyond supported limits + // (other than bitrate) + mAllowMbOverride = true; + } errors &= ~ERROR_NONE_SUPPORTED; - } - maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); - maxBlocks = Math.max(FS, maxBlocks); - maxBps = Math.max(BR * 1000, maxBps); - if (strict) { + maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); + maxBlocks = Math.max(W * H, maxBlocks); + maxBps = Math.max(BR * 64000, maxBps); maxWidth = Math.max(W, maxWidth); maxHeight = Math.max(H, maxHeight); maxRate = Math.max(FR, maxRate); - } else { - // assuming max 60 fps frame rate and 1:2 aspect ratio - int maxDim = (int)Math.sqrt(FS * 2); - maxWidth = Math.max(maxDim, maxWidth); - maxHeight = Math.max(maxDim, maxHeight); - maxRate = Math.max(Math.max(FR, 60), maxRate); - } - } - applyMacroBlockLimits(maxWidth, maxHeight, - maxBlocks, maxBlocksPerSecond, - 16 /* blockWidth */, 16 /* blockHeight */, - 1 /* widthAlignment */, 1 /* heightAlignment */); - mFrameRateRange = mFrameRateRange.intersect(12, maxRate); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_H263)) { - int maxWidth = 11, maxHeight = 9, maxRate = 15; - int minWidth = maxWidth, minHeight = maxHeight; - int minAlignment = 16; - maxBlocks = 99; - maxBlocksPerSecond = 1485; - maxBps = 64000; - for (CodecProfileLevel profileLevel: profileLevels) { - int MBPS = 0, BR = 0, FR = 0, W = 0, H = 0, minW = minWidth, minH = minHeight; - boolean strict = false; // true: support only sQCIF, QCIF (maybe CIF) - switch (profileLevel.level) { - case CodecProfileLevel.H263Level10: - strict = true; // only supports sQCIF & QCIF - FR = 15; W = 11; H = 9; BR = 1; MBPS = W * H * FR; break; - case CodecProfileLevel.H263Level20: - strict = true; // only supports sQCIF, QCIF & CIF - FR = 30; W = 22; H = 18; BR = 2; MBPS = W * H * 15; break; - case CodecProfileLevel.H263Level30: - strict = true; // only supports sQCIF, QCIF & CIF - FR = 30; W = 22; H = 18; BR = 6; MBPS = W * H * FR; break; - case CodecProfileLevel.H263Level40: - strict = true; // only supports sQCIF, QCIF & CIF - FR = 30; W = 22; H = 18; BR = 32; MBPS = W * H * FR; break; - case CodecProfileLevel.H263Level45: - // only implies level 10 support - strict = profileLevel.profile == CodecProfileLevel.H263ProfileBaseline - || profileLevel.profile == - CodecProfileLevel.H263ProfileBackwardCompatible; - if (!strict) { - minW = 1; minH = 1; minAlignment = 4; - } - FR = 15; W = 11; H = 9; BR = 2; MBPS = W * H * FR; break; - case CodecProfileLevel.H263Level50: - // only supports 50fps for H > 15 - minW = 1; minH = 1; minAlignment = 4; - FR = 60; W = 22; H = 18; BR = 64; MBPS = W * H * 50; break; - case CodecProfileLevel.H263Level60: - // only supports 50fps for H > 15 - minW = 1; minH = 1; minAlignment = 4; - FR = 60; W = 45; H = 18; BR = 128; MBPS = W * H * 50; break; - case CodecProfileLevel.H263Level70: - // only supports 50fps for H > 30 - minW = 1; minH = 1; minAlignment = 4; - FR = 60; W = 45; H = 36; BR = 256; MBPS = W * H * 50; break; - default: - Log.w(TAG, "Unrecognized profile/level " + profileLevel.profile - + "/" + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - switch (profileLevel.profile) { - case CodecProfileLevel.H263ProfileBackwardCompatible: - case CodecProfileLevel.H263ProfileBaseline: - case CodecProfileLevel.H263ProfileH320Coding: - case CodecProfileLevel.H263ProfileHighCompression: - case CodecProfileLevel.H263ProfileHighLatency: - case CodecProfileLevel.H263ProfileInterlace: - case CodecProfileLevel.H263ProfileInternet: - case CodecProfileLevel.H263ProfileISWV2: - case CodecProfileLevel.H263ProfileISWV3: - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - if (strict) { - // Strict levels define sub-QCIF min size and enumerated sizes. We cannot - // express support for "only sQCIF & QCIF (& CIF)" using VideoCapabilities - // but we can express "only QCIF (& CIF)", so set minimume size at QCIF. - // minW = 8; minH = 6; - minW = 11; minH = 9; - } else { - // any support for non-strict levels (including unrecognized profiles or - // levels) allow custom frame size support beyond supported limits - // (other than bitrate) - mAllowMbOverride = true; + minWidth = Math.min(minW, minWidth); + minHeight = Math.min(minH, minHeight); } - errors &= ~ERROR_NONE_SUPPORTED; - maxBlocksPerSecond = Math.max(MBPS, maxBlocksPerSecond); - maxBlocks = Math.max(W * H, maxBlocks); - maxBps = Math.max(BR * 64000, maxBps); - maxWidth = Math.max(W, maxWidth); - maxHeight = Math.max(H, maxHeight); - maxRate = Math.max(FR, maxRate); - minWidth = Math.min(minW, minWidth); - minHeight = Math.min(minH, minHeight); - } - // unless we encountered custom frame size support, limit size to QCIF and CIF - // using aspect ratio. - if (!mAllowMbOverride) { - mBlockAspectRatioRange = - Range.create(new Rational(11, 9), new Rational(11, 9)); - } - applyMacroBlockLimits( - minWidth, minHeight, - maxWidth, maxHeight, - maxBlocks, maxBlocksPerSecond, - 16 /* blockWidth */, 16 /* blockHeight */, - minAlignment /* widthAlignment */, minAlignment /* heightAlignment */); - mFrameRateRange = Range.create(1, maxRate); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP8)) { - maxBlocks = Integer.MAX_VALUE; - maxBlocksPerSecond = Integer.MAX_VALUE; - - // TODO: set to 100Mbps for now, need a number for VP8 - maxBps = 100000000; - - // profile levels are not indicative for VPx, but verify - // them nonetheless - for (CodecProfileLevel profileLevel: profileLevels) { - switch (profileLevel.level) { - case CodecProfileLevel.VP8Level_Version0: - case CodecProfileLevel.VP8Level_Version1: - case CodecProfileLevel.VP8Level_Version2: - case CodecProfileLevel.VP8Level_Version3: - break; - default: - Log.w(TAG, "Unrecognized level " - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + // unless we encountered custom frame size support, limit size to QCIF and CIF + // using aspect ratio. + if (!mAllowMbOverride) { + mBlockAspectRatioRange = + Range.create(new Rational(11, 9), new Rational(11, 9)); } - switch (profileLevel.profile) { - case CodecProfileLevel.VP8ProfileMain: - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + applyMacroBlockLimits( + minWidth, minHeight, + maxWidth, maxHeight, + maxBlocks, maxBlocksPerSecond, + 16 /* blockWidth */, 16 /* blockHeight */, + minAlignment /* widthAlignment */, minAlignment /* heightAlignment */); + mFrameRateRange = Range.create(1, maxRate); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP8)) { + maxBlocks = Integer.MAX_VALUE; + maxBlocksPerSecond = Integer.MAX_VALUE; + + // TODO: set to 100Mbps for now, need a number for VP8 + maxBps = 100000000; + + // profile levels are not indicative for VPx, but verify + // them nonetheless + for (CodecProfileLevel profileLevel: profileLevels) { + switch (profileLevel.level) { + case CodecProfileLevel.VP8Level_Version0: + case CodecProfileLevel.VP8Level_Version1: + case CodecProfileLevel.VP8Level_Version2: + case CodecProfileLevel.VP8Level_Version3: + break; + default: + Log.w(TAG, "Unrecognized level " + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + switch (profileLevel.profile) { + case CodecProfileLevel.VP8ProfileMain: + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + errors &= ~ERROR_NONE_SUPPORTED; } - errors &= ~ERROR_NONE_SUPPORTED; - } - final int blockSize = 16; - applyMacroBlockLimits(Short.MAX_VALUE, Short.MAX_VALUE, - maxBlocks, maxBlocksPerSecond, blockSize, blockSize, - 1 /* widthAlignment */, 1 /* heightAlignment */); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP9)) { - maxBlocksPerSecond = 829440; - maxBlocks = 36864; - maxBps = 200000; - int maxDim = 512; - - for (CodecProfileLevel profileLevel: profileLevels) { - long SR = 0; // luma sample rate - int FS = 0; // luma picture size - int BR = 0; // bit rate kbps - int D = 0; // luma dimension - switch (profileLevel.level) { - case CodecProfileLevel.VP9Level1: - SR = 829440; FS = 36864; BR = 200; D = 512; break; - case CodecProfileLevel.VP9Level11: - SR = 2764800; FS = 73728; BR = 800; D = 768; break; - case CodecProfileLevel.VP9Level2: - SR = 4608000; FS = 122880; BR = 1800; D = 960; break; - case CodecProfileLevel.VP9Level21: - SR = 9216000; FS = 245760; BR = 3600; D = 1344; break; - case CodecProfileLevel.VP9Level3: - SR = 20736000; FS = 552960; BR = 7200; D = 2048; break; - case CodecProfileLevel.VP9Level31: - SR = 36864000; FS = 983040; BR = 12000; D = 2752; break; - case CodecProfileLevel.VP9Level4: - SR = 83558400; FS = 2228224; BR = 18000; D = 4160; break; - case CodecProfileLevel.VP9Level41: - SR = 160432128; FS = 2228224; BR = 30000; D = 4160; break; - case CodecProfileLevel.VP9Level5: - SR = 311951360; FS = 8912896; BR = 60000; D = 8384; break; - case CodecProfileLevel.VP9Level51: - SR = 588251136; FS = 8912896; BR = 120000; D = 8384; break; - case CodecProfileLevel.VP9Level52: - SR = 1176502272; FS = 8912896; BR = 180000; D = 8384; break; - case CodecProfileLevel.VP9Level6: - SR = 1176502272; FS = 35651584; BR = 180000; D = 16832; break; - case CodecProfileLevel.VP9Level61: - SR = 2353004544L; FS = 35651584; BR = 240000; D = 16832; break; - case CodecProfileLevel.VP9Level62: - SR = 4706009088L; FS = 35651584; BR = 480000; D = 16832; break; - default: - Log.w(TAG, "Unrecognized level " - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - switch (profileLevel.profile) { - case CodecProfileLevel.VP9Profile0: - case CodecProfileLevel.VP9Profile1: - case CodecProfileLevel.VP9Profile2: - case CodecProfileLevel.VP9Profile3: - case CodecProfileLevel.VP9Profile2HDR: - case CodecProfileLevel.VP9Profile3HDR: - case CodecProfileLevel.VP9Profile2HDR10Plus: - case CodecProfileLevel.VP9Profile3HDR10Plus: - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + final int blockSize = 16; + applyMacroBlockLimits(Short.MAX_VALUE, Short.MAX_VALUE, + maxBlocks, maxBlocksPerSecond, blockSize, blockSize, + 1 /* widthAlignment */, 1 /* heightAlignment */); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_VP9)) { + maxBlocksPerSecond = 829440; + maxBlocks = 36864; + maxBps = 200000; + int maxDim = 512; + + for (CodecProfileLevel profileLevel: profileLevels) { + long SR = 0; // luma sample rate + int FS = 0; // luma picture size + int BR = 0; // bit rate kbps + int D = 0; // luma dimension + switch (profileLevel.level) { + case CodecProfileLevel.VP9Level1: + SR = 829440; FS = 36864; BR = 200; D = 512; break; + case CodecProfileLevel.VP9Level11: + SR = 2764800; FS = 73728; BR = 800; D = 768; break; + case CodecProfileLevel.VP9Level2: + SR = 4608000; FS = 122880; BR = 1800; D = 960; break; + case CodecProfileLevel.VP9Level21: + SR = 9216000; FS = 245760; BR = 3600; D = 1344; break; + case CodecProfileLevel.VP9Level3: + SR = 20736000; FS = 552960; BR = 7200; D = 2048; break; + case CodecProfileLevel.VP9Level31: + SR = 36864000; FS = 983040; BR = 12000; D = 2752; break; + case CodecProfileLevel.VP9Level4: + SR = 83558400; FS = 2228224; BR = 18000; D = 4160; break; + case CodecProfileLevel.VP9Level41: + SR = 160432128; FS = 2228224; BR = 30000; D = 4160; break; + case CodecProfileLevel.VP9Level5: + SR = 311951360; FS = 8912896; BR = 60000; D = 8384; break; + case CodecProfileLevel.VP9Level51: + SR = 588251136; FS = 8912896; BR = 120000; D = 8384; break; + case CodecProfileLevel.VP9Level52: + SR = 1176502272; FS = 8912896; BR = 180000; D = 8384; break; + case CodecProfileLevel.VP9Level6: + SR = 1176502272; FS = 35651584; BR = 180000; D = 16832; break; + case CodecProfileLevel.VP9Level61: + SR = 2353004544L; FS = 35651584; BR = 240000; D = 16832; break; + case CodecProfileLevel.VP9Level62: + SR = 4706009088L; FS = 35651584; BR = 480000; D = 16832; break; + default: + Log.w(TAG, "Unrecognized level " + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + switch (profileLevel.profile) { + case CodecProfileLevel.VP9Profile0: + case CodecProfileLevel.VP9Profile1: + case CodecProfileLevel.VP9Profile2: + case CodecProfileLevel.VP9Profile3: + case CodecProfileLevel.VP9Profile2HDR: + case CodecProfileLevel.VP9Profile3HDR: + case CodecProfileLevel.VP9Profile2HDR10Plus: + case CodecProfileLevel.VP9Profile3HDR10Plus: + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + errors &= ~ERROR_NONE_SUPPORTED; + maxBlocksPerSecond = Math.max(SR, maxBlocksPerSecond); + maxBlocks = Math.max(FS, maxBlocks); + maxBps = Math.max(BR * 1000, maxBps); + maxDim = Math.max(D, maxDim); } - errors &= ~ERROR_NONE_SUPPORTED; - maxBlocksPerSecond = Math.max(SR, maxBlocksPerSecond); - maxBlocks = Math.max(FS, maxBlocks); - maxBps = Math.max(BR * 1000, maxBps); - maxDim = Math.max(D, maxDim); - } - final int blockSize = 8; - int maxLengthInBlocks = Utils.divUp(maxDim, blockSize); - maxBlocks = Utils.divUp(maxBlocks, blockSize * blockSize); - maxBlocksPerSecond = Utils.divUp(maxBlocksPerSecond, blockSize * blockSize); + final int blockSize = 8; + int maxLengthInBlocks = Utils.divUp(maxDim, blockSize); + maxBlocks = Utils.divUp(maxBlocks, blockSize * blockSize); + maxBlocksPerSecond = Utils.divUp(maxBlocksPerSecond, blockSize * blockSize); + + applyMacroBlockLimits( + maxLengthInBlocks, maxLengthInBlocks, + maxBlocks, maxBlocksPerSecond, + blockSize, blockSize, + 1 /* widthAlignment */, 1 /* heightAlignment */); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_HEVC)) { + // CTBs are at least 8x8 so use 8x8 block size + maxBlocks = 36864 >> 6; // 192x192 pixels == 576 8x8 blocks + maxBlocksPerSecond = maxBlocks * 15; + maxBps = 128000; + for (CodecProfileLevel profileLevel: profileLevels) { + double FR = 0; + int FS = 0; + int BR = 0; + switch (profileLevel.level) { + /* The HEVC spec talks only in a very convoluted manner about the + existence of levels 1-3.1 for High tier, which could also be + understood as 'decoders and encoders should treat these levels + as if they were Main tier', so we do that. */ + case CodecProfileLevel.HEVCMainTierLevel1: + case CodecProfileLevel.HEVCHighTierLevel1: + FR = 15; FS = 36864; BR = 128; break; + case CodecProfileLevel.HEVCMainTierLevel2: + case CodecProfileLevel.HEVCHighTierLevel2: + FR = 30; FS = 122880; BR = 1500; break; + case CodecProfileLevel.HEVCMainTierLevel21: + case CodecProfileLevel.HEVCHighTierLevel21: + FR = 30; FS = 245760; BR = 3000; break; + case CodecProfileLevel.HEVCMainTierLevel3: + case CodecProfileLevel.HEVCHighTierLevel3: + FR = 30; FS = 552960; BR = 6000; break; + case CodecProfileLevel.HEVCMainTierLevel31: + case CodecProfileLevel.HEVCHighTierLevel31: + FR = 33.75; FS = 983040; BR = 10000; break; + case CodecProfileLevel.HEVCMainTierLevel4: + FR = 30; FS = 2228224; BR = 12000; break; + case CodecProfileLevel.HEVCHighTierLevel4: + FR = 30; FS = 2228224; BR = 30000; break; + case CodecProfileLevel.HEVCMainTierLevel41: + FR = 60; FS = 2228224; BR = 20000; break; + case CodecProfileLevel.HEVCHighTierLevel41: + FR = 60; FS = 2228224; BR = 50000; break; + case CodecProfileLevel.HEVCMainTierLevel5: + FR = 30; FS = 8912896; BR = 25000; break; + case CodecProfileLevel.HEVCHighTierLevel5: + FR = 30; FS = 8912896; BR = 100000; break; + case CodecProfileLevel.HEVCMainTierLevel51: + FR = 60; FS = 8912896; BR = 40000; break; + case CodecProfileLevel.HEVCHighTierLevel51: + FR = 60; FS = 8912896; BR = 160000; break; + case CodecProfileLevel.HEVCMainTierLevel52: + FR = 120; FS = 8912896; BR = 60000; break; + case CodecProfileLevel.HEVCHighTierLevel52: + FR = 120; FS = 8912896; BR = 240000; break; + case CodecProfileLevel.HEVCMainTierLevel6: + FR = 30; FS = 35651584; BR = 60000; break; + case CodecProfileLevel.HEVCHighTierLevel6: + FR = 30; FS = 35651584; BR = 240000; break; + case CodecProfileLevel.HEVCMainTierLevel61: + FR = 60; FS = 35651584; BR = 120000; break; + case CodecProfileLevel.HEVCHighTierLevel61: + FR = 60; FS = 35651584; BR = 480000; break; + case CodecProfileLevel.HEVCMainTierLevel62: + FR = 120; FS = 35651584; BR = 240000; break; + case CodecProfileLevel.HEVCHighTierLevel62: + FR = 120; FS = 35651584; BR = 800000; break; + default: + Log.w(TAG, "Unrecognized level " + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + switch (profileLevel.profile) { + case CodecProfileLevel.HEVCProfileMain: + case CodecProfileLevel.HEVCProfileMain10: + case CodecProfileLevel.HEVCProfileMainStill: + case CodecProfileLevel.HEVCProfileMain10HDR10: + case CodecProfileLevel.HEVCProfileMain10HDR10Plus: + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } - applyMacroBlockLimits( - maxLengthInBlocks, maxLengthInBlocks, - maxBlocks, maxBlocksPerSecond, - blockSize, blockSize, - 1 /* widthAlignment */, 1 /* heightAlignment */); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_HEVC)) { - // CTBs are at least 8x8 so use 8x8 block size - maxBlocks = 36864 >> 6; // 192x192 pixels == 576 8x8 blocks - maxBlocksPerSecond = maxBlocks * 15; - maxBps = 128000; - for (CodecProfileLevel profileLevel: profileLevels) { - double FR = 0; - int FS = 0; - int BR = 0; - switch (profileLevel.level) { - /* The HEVC spec talks only in a very convoluted manner about the - existence of levels 1-3.1 for High tier, which could also be - understood as 'decoders and encoders should treat these levels - as if they were Main tier', so we do that. */ - case CodecProfileLevel.HEVCMainTierLevel1: - case CodecProfileLevel.HEVCHighTierLevel1: - FR = 15; FS = 36864; BR = 128; break; - case CodecProfileLevel.HEVCMainTierLevel2: - case CodecProfileLevel.HEVCHighTierLevel2: - FR = 30; FS = 122880; BR = 1500; break; - case CodecProfileLevel.HEVCMainTierLevel21: - case CodecProfileLevel.HEVCHighTierLevel21: - FR = 30; FS = 245760; BR = 3000; break; - case CodecProfileLevel.HEVCMainTierLevel3: - case CodecProfileLevel.HEVCHighTierLevel3: - FR = 30; FS = 552960; BR = 6000; break; - case CodecProfileLevel.HEVCMainTierLevel31: - case CodecProfileLevel.HEVCHighTierLevel31: - FR = 33.75; FS = 983040; BR = 10000; break; - case CodecProfileLevel.HEVCMainTierLevel4: - FR = 30; FS = 2228224; BR = 12000; break; - case CodecProfileLevel.HEVCHighTierLevel4: - FR = 30; FS = 2228224; BR = 30000; break; - case CodecProfileLevel.HEVCMainTierLevel41: - FR = 60; FS = 2228224; BR = 20000; break; - case CodecProfileLevel.HEVCHighTierLevel41: - FR = 60; FS = 2228224; BR = 50000; break; - case CodecProfileLevel.HEVCMainTierLevel5: - FR = 30; FS = 8912896; BR = 25000; break; - case CodecProfileLevel.HEVCHighTierLevel5: - FR = 30; FS = 8912896; BR = 100000; break; - case CodecProfileLevel.HEVCMainTierLevel51: - FR = 60; FS = 8912896; BR = 40000; break; - case CodecProfileLevel.HEVCHighTierLevel51: - FR = 60; FS = 8912896; BR = 160000; break; - case CodecProfileLevel.HEVCMainTierLevel52: - FR = 120; FS = 8912896; BR = 60000; break; - case CodecProfileLevel.HEVCHighTierLevel52: - FR = 120; FS = 8912896; BR = 240000; break; - case CodecProfileLevel.HEVCMainTierLevel6: - FR = 30; FS = 35651584; BR = 60000; break; - case CodecProfileLevel.HEVCHighTierLevel6: - FR = 30; FS = 35651584; BR = 240000; break; - case CodecProfileLevel.HEVCMainTierLevel61: - FR = 60; FS = 35651584; BR = 120000; break; - case CodecProfileLevel.HEVCHighTierLevel61: - FR = 60; FS = 35651584; BR = 480000; break; - case CodecProfileLevel.HEVCMainTierLevel62: - FR = 120; FS = 35651584; BR = 240000; break; - case CodecProfileLevel.HEVCHighTierLevel62: - FR = 120; FS = 35651584; BR = 800000; break; - default: - Log.w(TAG, "Unrecognized level " - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + /* DPB logic: + if (width * height <= FS / 4) DPB = 16; + else if (width * height <= FS / 2) DPB = 12; + else if (width * height <= FS * 0.75) DPB = 8; + else DPB = 6; + */ + + FS >>= 6; // convert pixels to blocks + errors &= ~ERROR_NONE_SUPPORTED; + maxBlocksPerSecond = Math.max((int)(FR * FS), maxBlocksPerSecond); + maxBlocks = Math.max(FS, maxBlocks); + maxBps = Math.max(BR * 1000, maxBps); } - switch (profileLevel.profile) { - case CodecProfileLevel.HEVCProfileMain: - case CodecProfileLevel.HEVCProfileMain10: - case CodecProfileLevel.HEVCProfileMainStill: - case CodecProfileLevel.HEVCProfileMain10HDR10: - case CodecProfileLevel.HEVCProfileMain10HDR10Plus: - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; + + int maxLengthInBlocks = (int)(Math.sqrt(maxBlocks * 8)); + applyMacroBlockLimits( + maxLengthInBlocks, maxLengthInBlocks, + maxBlocks, maxBlocksPerSecond, + 8 /* blockWidth */, 8 /* blockHeight */, + 1 /* widthAlignment */, 1 /* heightAlignment */); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AV1)) { + maxBlocksPerSecond = 829440; + maxBlocks = 36864; + maxBps = 200000; + int maxDim = 512; + + // Sample rate, Picture Size, Bit rate and luma dimension for AV1 Codec, + // corresponding to the definitions in + // "AV1 Bitstream & Decoding Process Specification", Annex A + // found at https://aomedia.org/av1-bitstream-and-decoding-process-specification/ + for (CodecProfileLevel profileLevel: profileLevels) { + long SR = 0; // luma sample rate + int FS = 0; // luma picture size + int BR = 0; // bit rate kbps + int D = 0; // luma D + switch (profileLevel.level) { + case CodecProfileLevel.AV1Level2: + SR = 5529600; FS = 147456; BR = 1500; D = 2048; break; + case CodecProfileLevel.AV1Level21: + case CodecProfileLevel.AV1Level22: + case CodecProfileLevel.AV1Level23: + SR = 10454400; FS = 278784; BR = 3000; D = 2816; break; + + case CodecProfileLevel.AV1Level3: + SR = 24969600; FS = 665856; BR = 6000; D = 4352; break; + case CodecProfileLevel.AV1Level31: + case CodecProfileLevel.AV1Level32: + case CodecProfileLevel.AV1Level33: + SR = 39938400; FS = 1065024; BR = 10000; D = 5504; break; + + case CodecProfileLevel.AV1Level4: + SR = 77856768; FS = 2359296; BR = 12000; D = 6144; break; + case CodecProfileLevel.AV1Level41: + case CodecProfileLevel.AV1Level42: + case CodecProfileLevel.AV1Level43: + SR = 155713536; FS = 2359296; BR = 20000; D = 6144; break; + + case CodecProfileLevel.AV1Level5: + SR = 273715200; FS = 8912896; BR = 30000; D = 8192; break; + case CodecProfileLevel.AV1Level51: + SR = 547430400; FS = 8912896; BR = 40000; D = 8192; break; + case CodecProfileLevel.AV1Level52: + SR = 1094860800; FS = 8912896; BR = 60000; D = 8192; break; + case CodecProfileLevel.AV1Level53: + SR = 1176502272; FS = 8912896; BR = 60000; D = 8192; break; + + case CodecProfileLevel.AV1Level6: + SR = 1176502272; FS = 35651584; BR = 60000; D = 16384; break; + case CodecProfileLevel.AV1Level61: + SR = 2189721600L; FS = 35651584; BR = 100000; D = 16384; break; + case CodecProfileLevel.AV1Level62: + SR = 4379443200L; FS = 35651584; BR = 160000; D = 16384; break; + case CodecProfileLevel.AV1Level63: + SR = 4706009088L; FS = 35651584; BR = 160000; D = 16384; break; + + default: + Log.w(TAG, "Unrecognized level " + + profileLevel.level + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + switch (profileLevel.profile) { + case CodecProfileLevel.AV1ProfileMain8: + case CodecProfileLevel.AV1ProfileMain10: + case CodecProfileLevel.AV1ProfileMain10HDR10: + case CodecProfileLevel.AV1ProfileMain10HDR10Plus: + break; + default: + Log.w(TAG, "Unrecognized profile " + + profileLevel.profile + " for " + mime); + errors |= ERROR_UNRECOGNIZED; + } + errors &= ~ERROR_NONE_SUPPORTED; + maxBlocksPerSecond = Math.max(SR, maxBlocksPerSecond); + maxBlocks = Math.max(FS, maxBlocks); + maxBps = Math.max(BR * 1000, maxBps); + maxDim = Math.max(D, maxDim); } - /* DPB logic: - if (width * height <= FS / 4) DPB = 16; - else if (width * height <= FS / 2) DPB = 12; - else if (width * height <= FS * 0.75) DPB = 8; - else DPB = 6; - */ - - FS >>= 6; // convert pixels to blocks - errors &= ~ERROR_NONE_SUPPORTED; - maxBlocksPerSecond = Math.max((int)(FR * FS), maxBlocksPerSecond); - maxBlocks = Math.max(FS, maxBlocks); - maxBps = Math.max(BR * 1000, maxBps); + final int blockSize = 8; + int maxLengthInBlocks = Utils.divUp(maxDim, blockSize); + maxBlocks = Utils.divUp(maxBlocks, blockSize * blockSize); + maxBlocksPerSecond = Utils.divUp(maxBlocksPerSecond, blockSize * blockSize); + applyMacroBlockLimits( + maxLengthInBlocks, maxLengthInBlocks, + maxBlocks, maxBlocksPerSecond, + blockSize, blockSize, + 1 /* widthAlignment */, 1 /* heightAlignment */); + } else { + Log.w(TAG, "Unsupported mime " + mime); + // using minimal bitrate here. should be overridden by + // info from media_codecs.xml + maxBps = 64000; + errors |= ERROR_UNSUPPORTED; } + mBitrateRange = Range.create(1, maxBps); + mParent.mError |= errors; + } + } - int maxLengthInBlocks = (int)(Math.sqrt(maxBlocks * 8)); - applyMacroBlockLimits( - maxLengthInBlocks, maxLengthInBlocks, - maxBlocks, maxBlocksPerSecond, - 8 /* blockWidth */, 8 /* blockHeight */, - 1 /* widthAlignment */, 1 /* heightAlignment */); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_AV1)) { - maxBlocksPerSecond = 829440; - maxBlocks = 36864; - maxBps = 200000; - int maxDim = 512; - - // Sample rate, Picture Size, Bit rate and luma dimension for AV1 Codec, - // corresponding to the definitions in - // "AV1 Bitstream & Decoding Process Specification", Annex A - // found at https://aomedia.org/av1-bitstream-and-decoding-process-specification/ - for (CodecProfileLevel profileLevel: profileLevels) { - long SR = 0; // luma sample rate - int FS = 0; // luma picture size - int BR = 0; // bit rate kbps - int D = 0; // luma D - switch (profileLevel.level) { - case CodecProfileLevel.AV1Level2: - SR = 5529600; FS = 147456; BR = 1500; D = 2048; break; - case CodecProfileLevel.AV1Level21: - case CodecProfileLevel.AV1Level22: - case CodecProfileLevel.AV1Level23: - SR = 10454400; FS = 278784; BR = 3000; D = 2816; break; - - case CodecProfileLevel.AV1Level3: - SR = 24969600; FS = 665856; BR = 6000; D = 4352; break; - case CodecProfileLevel.AV1Level31: - case CodecProfileLevel.AV1Level32: - case CodecProfileLevel.AV1Level33: - SR = 39938400; FS = 1065024; BR = 10000; D = 5504; break; - - case CodecProfileLevel.AV1Level4: - SR = 77856768; FS = 2359296; BR = 12000; D = 6144; break; - case CodecProfileLevel.AV1Level41: - case CodecProfileLevel.AV1Level42: - case CodecProfileLevel.AV1Level43: - SR = 155713536; FS = 2359296; BR = 20000; D = 6144; break; - - case CodecProfileLevel.AV1Level5: - SR = 273715200; FS = 8912896; BR = 30000; D = 8192; break; - case CodecProfileLevel.AV1Level51: - SR = 547430400; FS = 8912896; BR = 40000; D = 8192; break; - case CodecProfileLevel.AV1Level52: - SR = 1094860800; FS = 8912896; BR = 60000; D = 8192; break; - case CodecProfileLevel.AV1Level53: - SR = 1176502272; FS = 8912896; BR = 60000; D = 8192; break; - - case CodecProfileLevel.AV1Level6: - SR = 1176502272; FS = 35651584; BR = 60000; D = 16384; break; - case CodecProfileLevel.AV1Level61: - SR = 2189721600L; FS = 35651584; BR = 100000; D = 16384; break; - case CodecProfileLevel.AV1Level62: - SR = 4379443200L; FS = 35651584; BR = 160000; D = 16384; break; - case CodecProfileLevel.AV1Level63: - SR = 4706009088L; FS = 35651584; BR = 160000; D = 16384; break; - - default: - Log.w(TAG, "Unrecognized level " - + profileLevel.level + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - switch (profileLevel.profile) { - case CodecProfileLevel.AV1ProfileMain8: - case CodecProfileLevel.AV1ProfileMain10: - case CodecProfileLevel.AV1ProfileMain10HDR10: - case CodecProfileLevel.AV1ProfileMain10HDR10Plus: - break; - default: - Log.w(TAG, "Unrecognized profile " - + profileLevel.profile + " for " + mime); - errors |= ERROR_UNRECOGNIZED; - } - errors &= ~ERROR_NONE_SUPPORTED; - maxBlocksPerSecond = Math.max(SR, maxBlocksPerSecond); - maxBlocks = Math.max(FS, maxBlocks); - maxBps = Math.max(BR * 1000, maxBps); - maxDim = Math.max(D, maxDim); + /* package private */ static final class VideoCapsNativeImpl implements VideoCapsIntf { + private long mNativeContext; // accessed by native methods + + private Range<Integer> mBitrateRange; + private Range<Integer> mHeightRange; + private Range<Integer> mWidthRange; + private Range<Integer> mFrameRateRange; + private List<PerformancePoint> mPerformancePoints; + + private int mWidthAlignment; + private int mHeightAlignment; + + // Used by JNI to construct Java VideoCapsNativeImpl + /** package private */ VideoCapsNativeImpl(Range<Integer> bitrateRange, + Range<Integer> widthRange, Range<Integer> heightRange, + Range<Integer> frameRateRange, List<PerformancePoint> performancePoints, + int widthAlignment, int heightAlignment) { + mBitrateRange = new Range<Integer>(bitrateRange.getLower(), + bitrateRange.getUpper()); + mWidthRange = new Range<Integer>(widthRange.getLower(), widthRange.getUpper()); + mHeightRange = new Range<Integer>(heightRange.getLower(), heightRange.getUpper()); + mFrameRateRange = new Range<Integer>(frameRateRange.getLower(), + frameRateRange.getUpper()); + mPerformancePoints = new ArrayList<PerformancePoint>(); + for (PerformancePoint pp : performancePoints) { + mPerformancePoints.add(new PerformancePoint(pp)); } + mWidthAlignment = widthAlignment; + mHeightAlignment = heightAlignment; + } - final int blockSize = 8; - int maxLengthInBlocks = Utils.divUp(maxDim, blockSize); - maxBlocks = Utils.divUp(maxBlocks, blockSize * blockSize); - maxBlocksPerSecond = Utils.divUp(maxBlocksPerSecond, blockSize * blockSize); - applyMacroBlockLimits( - maxLengthInBlocks, maxLengthInBlocks, - maxBlocks, maxBlocksPerSecond, - blockSize, blockSize, - 1 /* widthAlignment */, 1 /* heightAlignment */); - } else { - Log.w(TAG, "Unsupported mime " + mime); - // using minimal bitrate here. should be overriden by - // info from media_codecs.xml - maxBps = 64000; - errors |= ERROR_UNSUPPORTED; - } - mBitrateRange = Range.create(1, maxBps); - mParent.mError |= errors; + /* no public constructor */ + private VideoCapsNativeImpl() { } + + public Range<Integer> getBitrateRange() { + return mBitrateRange; + } + + public Range<Integer> getSupportedWidths() { + return mWidthRange; + } + + public Range<Integer> getSupportedHeights() { + return mHeightRange; + } + + public int getWidthAlignment() { + return mWidthAlignment; + } + + public int getHeightAlignment() { + return mHeightAlignment; + } + + /** @hide */ + public int getSmallerDimensionUpperLimit() { + return native_getSmallerDimensionUpperLimit(); + } + + public Range<Integer> getSupportedFrameRates() { + return mFrameRateRange; + } + + @Nullable + public List<PerformancePoint> getSupportedPerformancePoints() { + return mPerformancePoints; + } + + public Range<Integer> getSupportedWidthsFor(int height) { + return native_getSupportedWidthsFor(height); + } + + public Range<Integer> getSupportedHeightsFor(int width) { + return native_getSupportedHeightsFor(width); + } + + public Range<Double> getSupportedFrameRatesFor(int width, int height) { + return native_getSupportedFrameRatesFor(width, height); + } + + /** @throws IllegalArgumentException if the video size is not supported. */ + @Nullable + public Range<Double> getAchievableFrameRatesFor(int width, int height) { + return native_getAchievableFrameRatesFor(width, height); + } + + public boolean areSizeAndRateSupported(int width, int height, double frameRate) { + return native_areSizeAndRateSupported(width, height, frameRate); + } + + public boolean isSizeSupported(int width, int height) { + return native_isSizeSupported(width, height); + } + + /** @hide */ + public boolean supportsFormat(MediaFormat format) { + throw new UnsupportedOperationException( + "Java Implementation should not call native implemenatation"); + } + + private native Range<Integer> native_getSupportedWidthsFor(int height); + private native Range<Integer> native_getSupportedHeightsFor(int width); + private native Range<Double> native_getSupportedFrameRatesFor(int width, int height); + private native Range<Double> native_getAchievableFrameRatesFor(int width, int height); + private native boolean native_areSizeAndRateSupported( + int width, int height, double frameRate); + private native boolean native_isSizeSupported(int width, int height); + private native int native_getSmallerDimensionUpperLimit(); + + private static native void native_init(); + + static { + System.loadLibrary("media_jni"); + native_init(); + } } - } - /** - * A class that supports querying the encoding capabilities of a codec. - */ - public static final class EncoderCapabilities { + private VideoCapsIntf mImpl; + + /** @hide */ + public static VideoCapabilities create( + MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + VideoCapsLegacyImpl impl = VideoCapsLegacyImpl.create(info, parent); + VideoCapabilities caps = new VideoCapabilities(impl); + return caps; + } + + /* package private */ VideoCapabilities(VideoCapsIntf impl) { + mImpl = impl; + } + + /* no public constructor */ + private VideoCapabilities() { } + /** - * Returns the supported range of quality values. + * Returns the range of supported bitrates in bits/second. + */ + public Range<Integer> getBitrateRange() { + return mImpl.getBitrateRange(); + } + + /** + * Returns the range of supported video widths. + * <p class=note> + * 32-bit processes will not support resolutions larger than 4096x4096 due to + * the limited address space. + */ + public Range<Integer> getSupportedWidths() { + return mImpl.getSupportedWidths(); + } + + /** + * Returns the range of supported video heights. + * <p class=note> + * 32-bit processes will not support resolutions larger than 4096x4096 due to + * the limited address space. + */ + public Range<Integer> getSupportedHeights() { + return mImpl.getSupportedHeights(); + } + + /** + * Returns the alignment requirement for video width (in pixels). * - * Quality is implementation-specific. As a general rule, a higher quality - * setting results in a better image quality and a lower compression ratio. + * This is a power-of-2 value that video width must be a + * multiple of. */ - public Range<Integer> getQualityRange() { - return mQualityRange; + public int getWidthAlignment() { + return mImpl.getWidthAlignment(); } /** - * Returns the supported range of encoder complexity values. + * Returns the alignment requirement for video height (in pixels). + * + * This is a power-of-2 value that video height must be a + * multiple of. + */ + public int getHeightAlignment() { + return mImpl.getWidthAlignment(); + } + + /** + * Return the upper limit on the smaller dimension of width or height. + * <p></p> + * Some codecs have a limit on the smaller dimension, whether it be + * the width or the height. E.g. a codec may only be able to handle + * up to 1920x1080 both in landscape and portrait mode (1080x1920). + * In this case the maximum width and height are both 1920, but the + * smaller dimension limit will be 1080. For other codecs, this is + * {@code Math.min(getSupportedWidths().getUpper(), + * getSupportedHeights().getUpper())}. + * + * @hide + */ + public int getSmallerDimensionUpperLimit() { + return mImpl.getSmallerDimensionUpperLimit(); + } + + /** + * Returns the range of supported frame rates. * <p> - * Some codecs may support multiple complexity levels, where higher - * complexity values use more encoder tools (e.g. perform more - * intensive calculations) to improve the quality or the compression - * ratio. Use a lower value to save power and/or time. + * This is not a performance indicator. Rather, it expresses the + * limits specified in the coding standard, based on the complexities + * of encoding material for later playback at a certain frame rate, + * or the decoding of such material in non-realtime. */ - public Range<Integer> getComplexityRange() { - return mComplexityRange; + public Range<Integer> getSupportedFrameRates() { + return mImpl.getSupportedFrameRates(); + } + + /** + * Returns the range of supported video widths for a video height. + * @param height the height of the video + */ + public Range<Integer> getSupportedWidthsFor(int height) { + return mImpl.getSupportedWidthsFor(height); + } + + /** + * Returns the range of supported video heights for a video width + * @param width the width of the video + */ + public Range<Integer> getSupportedHeightsFor(int width) { + return mImpl.getSupportedHeightsFor(width); + } + + /** + * Returns the range of supported video frame rates for a video size. + * <p> + * This is not a performance indicator. Rather, it expresses the limits specified in + * the coding standard, based on the complexities of encoding material of a given + * size for later playback at a certain frame rate, or the decoding of such material + * in non-realtime. + + * @param width the width of the video + * @param height the height of the video + */ + public Range<Double> getSupportedFrameRatesFor(int width, int height) { + return mImpl.getSupportedFrameRatesFor(width, height); } + /** + * Returns the range of achievable video frame rates for a video size. + * May return {@code null}, if the codec did not publish any measurement + * data. + * <p> + * This is a performance estimate provided by the device manufacturer based on statistical + * sampling of full-speed decoding and encoding measurements in various configurations + * of common video sizes supported by the codec. As such it should only be used to + * compare individual codecs on the device. The value is not suitable for comparing + * different devices or even different android releases for the same device. + * <p> + * <em>On {@link android.os.Build.VERSION_CODES#M} release</em> the returned range + * corresponds to the fastest frame rates achieved in the tested configurations. As + * such, it should not be used to gauge guaranteed or even average codec performance + * on the device. + * <p> + * <em>On {@link android.os.Build.VERSION_CODES#N} release</em> the returned range + * corresponds closer to sustained performance <em>in tested configurations</em>. + * One can expect to achieve sustained performance higher than the lower limit more than + * 50% of the time, and higher than half of the lower limit at least 90% of the time + * <em>in tested configurations</em>. + * Conversely, one can expect performance lower than twice the upper limit at least + * 90% of the time. + * <p class=note> + * Tested configurations use a single active codec. For use cases where multiple + * codecs are active, applications can expect lower and in most cases significantly lower + * performance. + * <p class=note> + * The returned range value is interpolated from the nearest frame size(s) tested. + * Codec performance is severely impacted by other activity on the device as well + * as environmental factors (such as battery level, temperature or power source), and can + * vary significantly even in a steady environment. + * <p class=note> + * Use this method in cases where only codec performance matters, e.g. to evaluate if + * a codec has any chance of meeting a performance target. Codecs are listed + * in {@link MediaCodecList} in the preferred order as defined by the device + * manufacturer. As such, applications should use the first suitable codec in the + * list to achieve the best balance between power use and performance. + * + * @param width the width of the video + * @param height the height of the video + * + * @throws IllegalArgumentException if the video size is not supported. + */ + @Nullable + public Range<Double> getAchievableFrameRatesFor(int width, int height) { + return mImpl.getAchievableFrameRatesFor(width, height); + } + + /** + * Returns the supported performance points. May return {@code null} if the codec did not + * publish any performance point information (e.g. the vendor codecs have not been updated + * to the latest android release). May return an empty list if the codec published that + * if does not guarantee any performance points. + * <p> + * This is a performance guarantee provided by the device manufacturer for hardware codecs + * based on hardware capabilities of the device. + * <p> + * The returned list is sorted first by decreasing number of pixels, then by decreasing + * width, and finally by decreasing frame rate. + * Performance points assume a single active codec. For use cases where multiple + * codecs are active, should use that highest pixel count, and add the frame rates of + * each individual codec. + * <p class=note> + * 32-bit processes will not support resolutions larger than 4096x4096 due to + * the limited address space, but performance points will be presented as is. + * In other words, even though a component publishes a performance point for + * a resolution higher than 4096x4096, it does not mean that the resolution is supported + * for 32-bit processes. + */ + @Nullable + public List<PerformancePoint> getSupportedPerformancePoints() { + return mImpl.getSupportedPerformancePoints(); + } + + /** + * Returns whether a given video size ({@code width} and + * {@code height}) and {@code frameRate} combination is supported. + */ + public boolean areSizeAndRateSupported(int width, int height, double frameRate) { + return mImpl.areSizeAndRateSupported(width, height, frameRate); + } + + /** + * Returns whether a given video size ({@code width} and + * {@code height}) is supported. + */ + public boolean isSizeSupported(int width, int height) { + return mImpl.isSizeSupported(width, height); + } + + /** + * @hide + * @throws java.lang.ClassCastException + * @throws java.lang.UnsupportedOperationException + */ + public boolean supportsFormat(MediaFormat format) { + return mImpl.supportsFormat(format); + } + } + + /** + * A class that supports querying the encoding capabilities of a codec. + */ + public static final class EncoderCapabilities { + private static final String TAG = "EncoderCapabilities"; + /** Constant quality mode */ public static final int BITRATE_MODE_CQ = 0; /** Variable bitrate mode */ @@ -3874,188 +4563,314 @@ public final class MediaCodecInfo { /** Constant bitrate mode with frame drops */ public static final int BITRATE_MODE_CBR_FD = 3; - private static final Feature[] bitrates = new Feature[] { - new Feature("VBR", BITRATE_MODE_VBR, true), - new Feature("CBR", BITRATE_MODE_CBR, false), - new Feature("CQ", BITRATE_MODE_CQ, false), - new Feature("CBR-FD", BITRATE_MODE_CBR_FD, false) - }; - - private static int parseBitrateMode(String mode) { - for (Feature feat: bitrates) { - if (feat.mName.equalsIgnoreCase(mode)) { - return feat.mValue; - } - } - return 0; - } + /* package private */ interface EncoderCapsIntf { + public Range<Integer> getQualityRange(); - /** - * Query whether a bitrate mode is supported. - */ - public boolean isBitrateModeSupported(int mode) { - for (Feature feat: bitrates) { - if (mode == feat.mValue) { - return (mBitControl & (1 << mode)) != 0; - } - } - return false; - } + public Range<Integer> getComplexityRange(); - private Range<Integer> mQualityRange; - private Range<Integer> mComplexityRange; - private CodecCapabilities mParent; + public boolean isBitrateModeSupported(int mode); - /* no public constructor */ - private EncoderCapabilities() { } + public void getDefaultFormat(MediaFormat format); - /** @hide */ - public static EncoderCapabilities create( - MediaFormat info, CodecCapabilities parent) { - EncoderCapabilities caps = new EncoderCapabilities(); - caps.init(info, parent); - return caps; + public boolean supportsFormat(MediaFormat format); } - private void init(MediaFormat info, CodecCapabilities parent) { - // no support for complexity or quality yet - mParent = parent; - mComplexityRange = Range.create(0, 0); - mQualityRange = Range.create(0, 0); - mBitControl = (1 << BITRATE_MODE_VBR); + /* package private */ static final class EncoderCapsLegacyImpl implements EncoderCapsIntf { + private CodecCapabilities.CodecCapsLegacyImpl mParent; - applyLevelLimits(); - parseFromInfo(info); - } + private Range<Integer> mQualityRange; + private Range<Integer> mComplexityRange; - private void applyLevelLimits() { - String mime = mParent.getMimeType(); - if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_FLAC)) { - mComplexityRange = Range.create(0, 8); - mBitControl = (1 << BITRATE_MODE_CQ); - } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_NB) - || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_WB) - || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_ALAW) - || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_MLAW) - || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_MSGSM)) { - mBitControl = (1 << BITRATE_MODE_CBR); + public Range<Integer> getQualityRange() { + return mQualityRange; } - } - private int mBitControl; - private Integer mDefaultComplexity; - private Integer mDefaultQuality; - private String mQualityScale; + public Range<Integer> getComplexityRange() { + return mComplexityRange; + } - private void parseFromInfo(MediaFormat info) { - Map<String, Object> map = info.getMap(); + private static final Feature[] bitrates = new Feature[] { + new Feature("VBR", BITRATE_MODE_VBR, true), + new Feature("CBR", BITRATE_MODE_CBR, false), + new Feature("CQ", BITRATE_MODE_CQ, false), + new Feature("CBR-FD", BITRATE_MODE_CBR_FD, false) + }; - if (info.containsKey("complexity-range")) { - mComplexityRange = Utils - .parseIntRange(info.getString("complexity-range"), mComplexityRange); - // TODO should we limit this to level limits? + private static int parseBitrateMode(String mode) { + for (Feature feat: bitrates) { + if (feat.mName.equalsIgnoreCase(mode)) { + return feat.mValue; + } + } + return 0; } - if (info.containsKey("quality-range")) { - mQualityRange = Utils - .parseIntRange(info.getString("quality-range"), mQualityRange); + + public boolean isBitrateModeSupported(int mode) { + for (Feature feat: bitrates) { + if (mode == feat.mValue) { + return (mBitControl & (1 << mode)) != 0; + } + } + return false; } - if (info.containsKey("feature-bitrate-modes")) { - mBitControl = 0; - for (String mode: info.getString("feature-bitrate-modes").split(",")) { - mBitControl |= (1 << parseBitrateMode(mode)); + + /* no public constructor */ + private EncoderCapsLegacyImpl() { } + + /** @hide */ + public static EncoderCapsLegacyImpl create( + MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + if (GetFlag(() -> android.media.codec.Flags.nativeCapabilites())) { + Log.d(TAG, "Legacy implementation is called while native flag is on."); } + + EncoderCapsLegacyImpl caps = new EncoderCapsLegacyImpl(); + caps.init(info, parent); + return caps; } - try { - mDefaultComplexity = Integer.parseInt((String)map.get("complexity-default")); - } catch (NumberFormatException e) { } + private void init(MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + // no support for complexity or quality yet + mParent = parent; + mComplexityRange = Range.create(0, 0); + mQualityRange = Range.create(0, 0); + mBitControl = (1 << BITRATE_MODE_VBR); - try { - mDefaultQuality = Integer.parseInt((String)map.get("quality-default")); - } catch (NumberFormatException e) { } + applyLevelLimits(); + parseFromInfo(info); + } - mQualityScale = (String)map.get("quality-scale"); - } + private void applyLevelLimits() { + String mime = mParent.getMimeType(); + if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_FLAC)) { + mComplexityRange = Range.create(0, 8); + mBitControl = (1 << BITRATE_MODE_CQ); + } else if (mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_NB) + || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_AMR_WB) + || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_ALAW) + || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_G711_MLAW) + || mime.equalsIgnoreCase(MediaFormat.MIMETYPE_AUDIO_MSGSM)) { + mBitControl = (1 << BITRATE_MODE_CBR); + } + } + + private int mBitControl; + private Integer mDefaultComplexity; + private Integer mDefaultQuality; + private String mQualityScale; - private boolean supports( - Integer complexity, Integer quality, Integer profile) { - boolean ok = true; - if (ok && complexity != null) { - ok = mComplexityRange.contains(complexity); + private void parseFromInfo(MediaFormat info) { + Map<String, Object> map = info.getMap(); + + if (info.containsKey("complexity-range")) { + mComplexityRange = Utils + .parseIntRange(info.getString("complexity-range"), mComplexityRange); + // TODO should we limit this to level limits? + } + if (info.containsKey("quality-range")) { + mQualityRange = Utils + .parseIntRange(info.getString("quality-range"), mQualityRange); + } + if (info.containsKey("feature-bitrate-modes")) { + mBitControl = 0; + for (String mode: info.getString("feature-bitrate-modes").split(",")) { + mBitControl |= (1 << parseBitrateMode(mode)); + } + } + + try { + mDefaultComplexity = Integer.parseInt((String)map.get("complexity-default")); + } catch (NumberFormatException e) { } + + try { + mDefaultQuality = Integer.parseInt((String)map.get("quality-default")); + } catch (NumberFormatException e) { } + + mQualityScale = (String)map.get("quality-scale"); } - if (ok && quality != null) { - ok = mQualityRange.contains(quality); + + private boolean supports( + Integer complexity, Integer quality, Integer profile) { + boolean ok = true; + if (ok && complexity != null) { + ok = mComplexityRange.contains(complexity); + } + if (ok && quality != null) { + ok = mQualityRange.contains(quality); + } + if (ok && profile != null) { + for (CodecProfileLevel pl: mParent.getProfileLevels()) { + if (pl.profile == profile) { + profile = null; + break; + } + } + ok = profile == null; + } + return ok; } - if (ok && profile != null) { - for (CodecProfileLevel pl: mParent.profileLevels) { - if (pl.profile == profile) { - profile = null; + + /** @hide */ + public void getDefaultFormat(MediaFormat format) { + // don't list trivial quality/complexity as default for now + if (!mQualityRange.getUpper().equals(mQualityRange.getLower()) + && mDefaultQuality != null) { + format.setInteger(MediaFormat.KEY_QUALITY, mDefaultQuality); + } + if (!mComplexityRange.getUpper().equals(mComplexityRange.getLower()) + && mDefaultComplexity != null) { + format.setInteger(MediaFormat.KEY_COMPLEXITY, mDefaultComplexity); + } + // bitrates are listed in order of preference + for (Feature feat: bitrates) { + if ((mBitControl & (1 << feat.mValue)) != 0) { + format.setInteger(MediaFormat.KEY_BITRATE_MODE, feat.mValue); break; } } - ok = profile == null; } - return ok; - } - /** @hide */ - public void getDefaultFormat(MediaFormat format) { - // don't list trivial quality/complexity as default for now - if (!mQualityRange.getUpper().equals(mQualityRange.getLower()) - && mDefaultQuality != null) { - format.setInteger(MediaFormat.KEY_QUALITY, mDefaultQuality); - } - if (!mComplexityRange.getUpper().equals(mComplexityRange.getLower()) - && mDefaultComplexity != null) { - format.setInteger(MediaFormat.KEY_COMPLEXITY, mDefaultComplexity); - } - // bitrates are listed in order of preference - for (Feature feat: bitrates) { - if ((mBitControl & (1 << feat.mValue)) != 0) { - format.setInteger(MediaFormat.KEY_BITRATE_MODE, feat.mValue); - break; + /** @hide */ + public boolean supportsFormat(MediaFormat format) { + final Map<String, Object> map = format.getMap(); + final String mime = mParent.getMimeType(); + + Integer mode = (Integer)map.get(MediaFormat.KEY_BITRATE_MODE); + if (mode != null && !isBitrateModeSupported(mode)) { + return false; + } + + Integer complexity = (Integer)map.get(MediaFormat.KEY_COMPLEXITY); + if (MediaFormat.MIMETYPE_AUDIO_FLAC.equalsIgnoreCase(mime)) { + Integer flacComplexity = + (Integer)map.get(MediaFormat.KEY_FLAC_COMPRESSION_LEVEL); + if (complexity == null) { + complexity = flacComplexity; + } else if (flacComplexity != null && !complexity.equals(flacComplexity)) { + throw new IllegalArgumentException( + "conflicting values for complexity and " + + "flac-compression-level"); + } } + + // other audio parameters + Integer profile = (Integer)map.get(MediaFormat.KEY_PROFILE); + if (MediaFormat.MIMETYPE_AUDIO_AAC.equalsIgnoreCase(mime)) { + Integer aacProfile = (Integer)map.get(MediaFormat.KEY_AAC_PROFILE); + if (profile == null) { + profile = aacProfile; + } else if (aacProfile != null && !aacProfile.equals(profile)) { + throw new IllegalArgumentException( + "conflicting values for profile and aac-profile"); + } + } + + Integer quality = (Integer)map.get(MediaFormat.KEY_QUALITY); + + return supports(complexity, quality, profile); } } - /** @hide */ - public boolean supportsFormat(MediaFormat format) { - final Map<String, Object> map = format.getMap(); - final String mime = mParent.getMimeType(); + /* package private */ static final class EncoderCapsNativeImpl implements EncoderCapsIntf { + private long mNativeContext; // accessed by native methods - Integer mode = (Integer)map.get(MediaFormat.KEY_BITRATE_MODE); - if (mode != null && !isBitrateModeSupported(mode)) { - return false; + private Range<Integer> mQualityRange; + private Range<Integer> mComplexityRange; + + /* no public constructor */ + private EncoderCapsNativeImpl() { } + + // Constructor called from native + /* package private */ EncoderCapsNativeImpl(Range<Integer> qualityRange, + Range<Integer> complexityRange) { + mQualityRange = qualityRange; + mComplexityRange = complexityRange; } - Integer complexity = (Integer)map.get(MediaFormat.KEY_COMPLEXITY); - if (MediaFormat.MIMETYPE_AUDIO_FLAC.equalsIgnoreCase(mime)) { - Integer flacComplexity = - (Integer)map.get(MediaFormat.KEY_FLAC_COMPRESSION_LEVEL); - if (complexity == null) { - complexity = flacComplexity; - } else if (flacComplexity != null && !complexity.equals(flacComplexity)) { - throw new IllegalArgumentException( - "conflicting values for complexity and " + - "flac-compression-level"); - } + public Range<Integer> getQualityRange() { + return mQualityRange; } - // other audio parameters - Integer profile = (Integer)map.get(MediaFormat.KEY_PROFILE); - if (MediaFormat.MIMETYPE_AUDIO_AAC.equalsIgnoreCase(mime)) { - Integer aacProfile = (Integer)map.get(MediaFormat.KEY_AAC_PROFILE); - if (profile == null) { - profile = aacProfile; - } else if (aacProfile != null && !aacProfile.equals(profile)) { - throw new IllegalArgumentException( - "conflicting values for profile and aac-profile"); - } + public Range<Integer> getComplexityRange() { + return mComplexityRange; + } + + public boolean isBitrateModeSupported(int mode) { + return native_isBitrateModeSupported(mode); } - Integer quality = (Integer)map.get(MediaFormat.KEY_QUALITY); + // This API is for internal Java implementation only. Should not be called. + public void getDefaultFormat(MediaFormat format) { + throw new UnsupportedOperationException( + "Java Implementation should not call native implemenatation"); + } + + // This API is for internal Java implementation only. Should not be called. + public boolean supportsFormat(MediaFormat format) { + throw new UnsupportedOperationException( + "Java Implementation should not call native implemenatation"); + } - return supports(complexity, quality, profile); + private native boolean native_isBitrateModeSupported(int mode); + private static native void native_init(); + + static { + System.loadLibrary("media_jni"); + native_init(); + } + } + + private EncoderCapsIntf mImpl; + + /** @hide */ + public static EncoderCapabilities create( + MediaFormat info, CodecCapabilities.CodecCapsLegacyImpl parent) { + EncoderCapsLegacyImpl impl = EncoderCapsLegacyImpl.create(info, parent); + EncoderCapabilities caps = new EncoderCapabilities(impl); + return caps; + } + + /* package private */ EncoderCapabilities(EncoderCapsIntf impl) { + mImpl = impl; + } + + /** + * Returns the supported range of quality values. + * + * Quality is implementation-specific. As a general rule, a higher quality + * setting results in a better image quality and a lower compression ratio. + */ + public Range<Integer> getQualityRange() { + return mImpl.getQualityRange(); + } + + /** + * Returns the supported range of encoder complexity values. + * <p> + * Some codecs may support multiple complexity levels, where higher + * complexity values use more encoder tools (e.g. perform more + * intensive calculations) to improve the quality or the compression + * ratio. Use a lower value to save power and/or time. + */ + public Range<Integer> getComplexityRange() { + return mImpl.getComplexityRange(); + } + + /** + * Query whether a bitrate mode is supported. + */ + public boolean isBitrateModeSupported(int mode) { + return mImpl.isBitrateModeSupported(mode); + } + + /** @hide */ + public void getDefaultFormat(MediaFormat format) { + mImpl.getDefaultFormat(format); + } + + /** @hide */ + public boolean supportsFormat(MediaFormat format) { + return mImpl.supportsFormat(format); } }; @@ -4960,4 +5775,19 @@ public final class MediaCodecInfo { mName, mCanonicalName, mFlags, caps.toArray(new CodecCapabilities[caps.size()])); } + + /* package private */ class GenericHelper { + private static Range<Integer> constructIntegerRange(int lower, int upper) { + return Range.create(Integer.valueOf(lower), Integer.valueOf(upper)); + } + + private static Range<Double> constructDoubleRange(double lower, double upper) { + return Range.create(Double.valueOf(lower), Double.valueOf(upper)); + } + + private static List<VideoCapabilities.PerformancePoint> + constructPerformancePointList(VideoCapabilities.PerformancePoint[] array) { + return Arrays.asList(array); + } + } } diff --git a/media/java/android/media/MediaRoute2ProviderService.java b/media/java/android/media/MediaRoute2ProviderService.java index 09f40e005b4c..f42017dc835a 100644 --- a/media/java/android/media/MediaRoute2ProviderService.java +++ b/media/java/android/media/MediaRoute2ProviderService.java @@ -226,6 +226,10 @@ public abstract class MediaRoute2ProviderService extends Service { @GuardedBy("mSessionLock") private final ArrayMap<String, MediaStreams> mOngoingMediaStreams = new ArrayMap<>(); + @GuardedBy("mSessionLock") + private final ArrayMap<String, RoutingSessionInfo> mPendingSystemSessionReleases = + new ArrayMap<>(); + public MediaRoute2ProviderService() { mHandler = new Handler(Looper.getMainLooper()); } @@ -358,7 +362,9 @@ public abstract class MediaRoute2ProviderService extends Service { * @return a {@link MediaStreams} instance that holds the media streams to route as part of the * newly created routing session. May be null if system media capture failed, in which case * you can ignore the return value, as you will receive a call to {@link #onReleaseSession} - * where you can clean up this session + * where you can clean up this session. {@link AudioRecord#startRecording()} must be called + * immediately on {@link MediaStreams#getAudioRecord()} after calling this method, in order + * to start streaming audio to the receiver. * @hide */ // TODO: b/362507305 - Unhide once the implementation and CTS are in place. @@ -417,7 +423,7 @@ public abstract class MediaRoute2ProviderService extends Service { } AudioFormat audioFormat = formats.mAudioFormat; - var mediaStreamsBuilder = new MediaStreams.Builder(); + var mediaStreamsBuilder = new MediaStreams.Builder(sessionInfo); if (audioFormat != null) { populateAudioStream(audioFormat, uid, mediaStreamsBuilder); } @@ -458,7 +464,6 @@ public abstract class MediaRoute2ProviderService extends Service { if (uid != Process.INVALID_UID) { audioMixingRuleBuilder.addMixRule(AudioMixingRule.RULE_MATCH_UID, uid); } - AudioMix mix = new AudioMix.Builder(audioMixingRuleBuilder.build()) .setFormat(audioFormat) @@ -471,7 +476,11 @@ public abstract class MediaRoute2ProviderService extends Service { Log.e(TAG, "Couldn't fetch the audio manager."); return; } - audioManager.registerAudioPolicy(audioPolicy); + int audioPolicyResult = audioManager.registerAudioPolicy(audioPolicy); + if (audioPolicyResult != AudioManager.SUCCESS) { + Log.e(TAG, "Failed to register the audio policy."); + return; + } var audioRecord = audioPolicy.createAudioRecordSink(mix); if (audioRecord == null) { Log.e(TAG, "Audio record creation failed."); @@ -521,8 +530,14 @@ public abstract class MediaRoute2ProviderService extends Service { RoutingSessionInfo sessionInfo; synchronized (mSessionLock) { sessionInfo = mSessionInfos.remove(sessionId); - maybeReleaseMediaStreams(sessionId); - + if (Flags.enableMirroringInMediaRouter2()) { + if (sessionInfo == null) { + sessionInfo = maybeReleaseMediaStreams(sessionId); + } + if (sessionInfo == null) { + sessionInfo = mPendingSystemSessionReleases.remove(sessionId); + } + } if (sessionInfo == null) { Log.w(TAG, "notifySessionReleased: Ignoring unknown session info."); return; @@ -539,18 +554,26 @@ public abstract class MediaRoute2ProviderService extends Service { } } - /** Releases any system media routing resources associated with the given {@code sessionId}. */ - private void maybeReleaseMediaStreams(String sessionId) { + /** + * Releases any system media routing resources associated with the given {@code sessionId}. + * + * @return The {@link RoutingSessionInfo} that corresponds to the released media streams, or + * null if no streams were released. + */ + @Nullable + private RoutingSessionInfo maybeReleaseMediaStreams(String sessionId) { if (!Flags.enableMirroringInMediaRouter2()) { - return; + return null; } synchronized (mSessionLock) { var streams = mOngoingMediaStreams.remove(sessionId); if (streams != null) { releaseAudioStream(streams.mAudioPolicy, streams.mAudioRecord); // TODO: b/380431086: Release the video stream once implemented. + return streams.mSessionInfo; } } + return null; } // We cannot reach the code that requires MODIFY_AUDIO_ROUTING without holding it. @@ -1019,12 +1042,16 @@ public abstract class MediaRoute2ProviderService extends Service { if (!checkCallerIsSystem()) { return; } - if (!checkSessionIdIsValid(sessionId, "releaseSession")) { - return; + synchronized (mSessionLock) { + // We proactively release the system media routing session resources when the + // system requests it, to ensure it happens immediately. + RoutingSessionInfo releasedSession = maybeReleaseMediaStreams(sessionId); + if (releasedSession != null) { + mPendingSystemSessionReleases.put(sessionId, releasedSession); + } else if (!checkSessionIdIsValid(sessionId, "releaseSession")) { + return; + } } - // We proactively release the system media routing once the system requests it, to - // ensure it happens immediately. - maybeReleaseMediaStreams(sessionId); addRequestId(requestId); mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onReleaseSession, @@ -1047,9 +1074,19 @@ public abstract class MediaRoute2ProviderService extends Service { @Nullable private final AudioPolicy mAudioPolicy; @Nullable private final AudioRecord mAudioRecord; + /** + * Holds the last {@link RoutingSessionInfo} associated with these streams. + * + * @hide + */ + @GuardedBy("MediaRoute2ProviderService.this.mSessionLock") + @NonNull + private RoutingSessionInfo mSessionInfo; + // TODO: b/380431086: Add the video equivalent. private MediaStreams(Builder builder) { + this.mSessionInfo = builder.mSessionInfo; this.mAudioPolicy = builder.mAudioPolicy; this.mAudioRecord = builder.mAudioRecord; } @@ -1070,9 +1107,19 @@ public abstract class MediaRoute2ProviderService extends Service { */ public static final class Builder { + @NonNull private RoutingSessionInfo mSessionInfo; @Nullable private AudioPolicy mAudioPolicy; @Nullable private AudioRecord mAudioRecord; + /** + * Constructor. + * + * @param sessionInfo The {@link RoutingSessionInfo} associated with these streams. + */ + Builder(@NonNull RoutingSessionInfo sessionInfo) { + mSessionInfo = requireNonNull(sessionInfo); + } + /** Populates system media audio-related structures. */ public Builder setAudioStream( @NonNull AudioPolicy audioPolicy, @NonNull AudioRecord audioRecord) { diff --git a/media/jni/Android.bp b/media/jni/Android.bp index f09dc7218d7d..af545d5a4bc4 100644 --- a/media/jni/Android.bp +++ b/media/jni/Android.bp @@ -25,6 +25,7 @@ cc_library_shared { min_sdk_version: "", srcs: [ + "android_media_CodecCapabilities.cpp", "android_media_ImageWriter.cpp", "android_media_ImageReader.cpp", "android_media_JetPlayer.cpp", @@ -64,6 +65,7 @@ cc_library_shared { "libbinder", "libmedia", "libmedia_codeclist", + "libmedia_codeclist_capabilities", "libmedia_jni_utils", "libmedia_omx", "libmediametrics", diff --git a/media/jni/android_media_CodecCapabilities.cpp b/media/jni/android_media_CodecCapabilities.cpp new file mode 100644 index 000000000000..df0c826d8d87 --- /dev/null +++ b/media/jni/android_media_CodecCapabilities.cpp @@ -0,0 +1,1015 @@ +/* + * Copyright 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. + */ + +//#define LOG_NDEBUG 0 +#define LOG_TAG "MediaCodec-JNI" + +#include "android_media_CodecCapabilities.h" +#include "android_media_Streams.h" +#include "android_runtime/AndroidRuntime.h" +#include "jni.h" + +#include <media/AudioCapabilities.h> +#include <media/CodecCapabilities.h> +#include <media/EncoderCapabilities.h> +#include <media/VideoCapabilities.h> +#include <media/stagefright/foundation/ADebug.h> +#include <media/stagefright/foundation/AMessage.h> +#include <nativehelper/JNIHelp.h> +#include <nativehelper/ScopedLocalRef.h> +#include <utils/Log.h> + +namespace android { + +struct fields_t { + jfieldID audioCapsContext; + jfieldID videoCapsContext; + jfieldID encoderCapsContext; + jfieldID codecCapsContext; +}; +static fields_t fields; + +// JCodecCapabilities + +JCodecCapabilities::JCodecCapabilities(std::shared_ptr<CodecCapabilities> codecCaps) + : mCodecCaps(codecCaps) {} + +std::shared_ptr<CodecCapabilities> JCodecCapabilities::getCodecCaps() const { + return mCodecCaps; +} + +int32_t JCodecCapabilities::getMaxSupportedInstances() const { + return mCodecCaps->getMaxSupportedInstances(); +} + +std::string JCodecCapabilities::getMediaType() const { + return mCodecCaps->getMediaType(); +} + +bool JCodecCapabilities::isFeatureRequired(const std::string& name) const { + return mCodecCaps->isFeatureRequired(name); +} + +bool JCodecCapabilities::isFeatureSupported(const std::string& name) const { + return mCodecCaps->isFeatureSupported(name); +} + +bool JCodecCapabilities::isFormatSupported(const sp<AMessage> &format) const { + return mCodecCaps->isFormatSupported(format); +} + +bool JCodecCapabilities::isRegular() const { + return mCodecCaps->isRegular(); +} + +// Setter + +static sp<JCodecCapabilities> setCodecCapabilities(JNIEnv *env, jobject thiz, + const sp<JCodecCapabilities>& jCodecCaps) { + sp<JCodecCapabilities> old + = (JCodecCapabilities*)env->GetLongField(thiz, fields.codecCapsContext); + if (jCodecCaps != NULL) { + jCodecCaps->incStrong(thiz); + } + if (old != NULL) { + old->decStrong(thiz); + } + env->SetLongField(thiz, fields.codecCapsContext, (jlong)jCodecCaps.get()); + return old; +} + +// Getters + +static AudioCapabilities* getAudioCapabilities(JNIEnv *env, jobject thiz) { + AudioCapabilities* const p = (AudioCapabilities*)env->GetLongField( + thiz, fields.audioCapsContext); + return p; +} + +static VideoCapabilities* getVideoCapabilities(JNIEnv *env, jobject thiz) { + VideoCapabilities* const p = (VideoCapabilities*)env->GetLongField( + thiz, fields.videoCapsContext); + return p; +} + +static EncoderCapabilities* getEncoderCapabilities(JNIEnv *env, jobject thiz) { + EncoderCapabilities* const p = (EncoderCapabilities*)env->GetLongField( + thiz, fields.encoderCapsContext); + return p; +} + +static sp<JCodecCapabilities> getCodecCapabilities(JNIEnv *env, jobject thiz) { + JCodecCapabilities* const p = (JCodecCapabilities*)env->GetLongField( + thiz, fields.codecCapsContext); + return sp<JCodecCapabilities>(p); +} + +// Utils + +static jobject convertToJavaIntRange(JNIEnv *env, const Range<int32_t>& range) { + jclass helperClazz = env->FindClass("android/media/MediaCodecInfo$GenericHelper"); + jmethodID constructIntegerRangeID = env->GetStaticMethodID(helperClazz, "constructIntegerRange", + "(II)Landroid/util/Range;"); + jobject jRange = env->CallStaticObjectMethod(helperClazz, constructIntegerRangeID, + range.lower(), range.upper()); + + return jRange; +} + +static jobject convertToJavaDoubleRange(JNIEnv *env, const Range<double>& range) { + jclass helperClazz = env->FindClass("android/media/MediaCodecInfo$GenericHelper"); + jmethodID constructDoubleRangeID = env->GetStaticMethodID(helperClazz, "constructDoubleRange", + "(DD)Landroid/util/Range;"); + jobject jRange = env->CallStaticObjectMethod(helperClazz, constructDoubleRangeID, + range.lower(), range.upper()); + return jRange; +} + +static jobjectArray convertToJavaIntRangeArray(JNIEnv *env, + const std::vector<Range<int32_t>>& ranges) { + jclass rangeClazz = env->FindClass("android/util/Range"); + CHECK(rangeClazz != NULL); + jobjectArray jRanges = env->NewObjectArray(ranges.size(), rangeClazz, NULL); + for (int i = 0; i < ranges.size(); i++) { + Range<int32_t> range = ranges.at(i); + jobject jRange = convertToJavaIntRange(env, range); + env->SetObjectArrayElement(jRanges, i, jRange); + env->DeleteLocalRef(jRange); + jRange = NULL; + } + return jRanges; +} + +// Converters between Java objects and native instances + +// The Java AudioCapabilities object keep bitrateRange, sampleRates, sampleRateRanges +// and inputChannelRanges in it to prevent reconstruction when called the getters functions. +static jobject convertToJavaAudioCapabilities( + JNIEnv *env, std::shared_ptr<AudioCapabilities> audioCaps) { + if (audioCaps == nullptr) { + return NULL; + } + + // construct Java bitrateRange + const Range<int32_t>& bitrateRange = audioCaps->getBitrateRange(); + jobject jBitrateRange = convertToJavaIntRange(env, bitrateRange); + + // construct Java sampleRates array + const std::vector<int32_t>& sampleRates = audioCaps->getSupportedSampleRates(); + jintArray jSampleRates = env->NewIntArray(sampleRates.size()); + for (size_t i = 0; i < sampleRates.size(); ++i) { + jint val = sampleRates.at(i); + env->SetIntArrayRegion(jSampleRates, i, 1, &val); + } + + // construct Java sampleRateRanges + const std::vector<Range<int32_t>>& sampleRateRanges = audioCaps->getSupportedSampleRateRanges(); + jobjectArray jSampleRateRanges = convertToJavaIntRangeArray(env, sampleRateRanges); + + // construct Java inputChannelRanges + const std::vector<Range<int32_t>>& inputChannelRanges = audioCaps->getInputChannelCountRanges(); + jobjectArray jInputChannelRanges = convertToJavaIntRangeArray(env, inputChannelRanges); + + // construct Java AudioCapsNativeImpl + jclass audioCapsImplClazz + = env->FindClass("android/media/MediaCodecInfo$AudioCapabilities$AudioCapsNativeImpl"); + CHECK(audioCapsImplClazz != NULL); + jmethodID audioCapsImplConstructID = env->GetMethodID(audioCapsImplClazz, "<init>", + "(Landroid/util/Range;" + "[I" + "[Landroid/util/Range;" + "[Landroid/util/Range;)V"); + jobject jAudioCapsImpl = env->NewObject(audioCapsImplClazz, audioCapsImplConstructID, + jBitrateRange, jSampleRates, jSampleRateRanges, jInputChannelRanges); + // The native AudioCapabilities won't be destructed until process ends. + env->SetLongField(jAudioCapsImpl, fields.audioCapsContext, (jlong)audioCaps.get()); + + // construct Java AudioCapabilities + jclass audioCapsClazz + = env->FindClass("android/media/MediaCodecInfo$AudioCapabilities"); + CHECK(audioCapsClazz != NULL); + jmethodID audioCapsConstructID = env->GetMethodID(audioCapsClazz, "<init>", + "(Landroid/media/MediaCodecInfo$AudioCapabilities$AudioCapsIntf;)V"); + jobject jAudioCaps = env->NewObject(audioCapsClazz, audioCapsConstructID, jAudioCapsImpl); + + env->DeleteLocalRef(jBitrateRange); + jBitrateRange = NULL; + + env->DeleteLocalRef(jSampleRates); + jSampleRates = NULL; + + env->DeleteLocalRef(jSampleRateRanges); + jSampleRateRanges = NULL; + + env->DeleteLocalRef(jInputChannelRanges); + jInputChannelRanges = NULL; + + env->DeleteLocalRef(jAudioCapsImpl); + jAudioCapsImpl = NULL; + + return jAudioCaps; +} + +// convert native PerformancePoints to Java objects +static jobject convertToJavaPerformancePoints(JNIEnv *env, + const std::vector<VideoCapabilities::PerformancePoint>& performancePoints) { + jclass performancePointClazz = env->FindClass( + "android/media/MediaCodecInfo$VideoCapabilities$PerformancePoint"); + CHECK(performancePointClazz != NULL); + jmethodID performancePointConstructID = env->GetMethodID(performancePointClazz, "<init>", + "(IIIJII)V"); + + jobjectArray jPerformancePoints = env->NewObjectArray(performancePoints.size(), + performancePointClazz, NULL); + int i = 0; + for (auto it = performancePoints.begin(); it != performancePoints.end(); ++it, ++i) { + jobject jPerformancePoint = env->NewObject(performancePointClazz, + performancePointConstructID, it->getWidth(), + it->getHeight(), it->getMaxFrameRate(), + it->getMaxMacroBlockRate(), it->getBlockSize().getWidth(), + it->getBlockSize().getHeight()); + + env->SetObjectArrayElement(jPerformancePoints, i, jPerformancePoint); + + env->DeleteLocalRef(jPerformancePoint); + } + + jclass helperClazz = env->FindClass("android/media/MediaCodecInfo$GenericHelper"); + CHECK(helperClazz != NULL); + jmethodID asListID = env->GetStaticMethodID(helperClazz, "constructPerformancePointList", + "([Landroid/media/MediaCodecInfo$VideoCapabilities$PerformancePoint;)Ljava/util/List;"); + CHECK(asListID != NULL); + jobject jList = env->CallStaticObjectMethod(helperClazz, asListID, jPerformancePoints); + + return jList; +} + +static VideoCapabilities::PerformancePoint convertToNativePerformancePoint( + JNIEnv *env, jobject pp) { + if (pp == NULL) { + jniThrowException(env, "java/lang/NullPointerException", NULL); + } + + jclass clazz = env->FindClass( + "android/media/MediaCodecInfo$VideoCapabilities$PerformancePoint"); + CHECK(clazz != NULL); + CHECK(env->IsInstanceOf(pp, clazz)); + + jmethodID getWidthID = env->GetMethodID(clazz, "getWidth", "()I"); + CHECK(getWidthID != NULL); + jint width = env->CallIntMethod(pp, getWidthID); + + jmethodID getHeightID = env->GetMethodID(clazz, "getHeight", "()I"); + CHECK(getHeightID != NULL); + jint height = env->CallIntMethod(pp, getHeightID); + + jmethodID getMaxFrameRateID = env->GetMethodID(clazz, "getMaxFrameRate", "()I"); + CHECK(getMaxFrameRateID != NULL); + jint maxFrameRate = env->CallIntMethod(pp, getMaxFrameRateID); + + jmethodID getMaxMacroBlockRateID = env->GetMethodID(clazz, "getMaxMacroBlockRate", "()J"); + CHECK(getMaxMacroBlockRateID != NULL); + jlong maxMacroBlockRate = env->CallLongMethod(pp, getMaxMacroBlockRateID); + + jmethodID getBlockWidthID = env->GetMethodID(clazz, "getBlockWidth", "()I"); + CHECK(getBlockWidthID != NULL); + jint blockWidth = env->CallIntMethod(pp, getBlockWidthID); + + jmethodID getBlockHeightID = env->GetMethodID(clazz, "getBlockHeight", "()I"); + CHECK(getBlockHeightID != NULL); + jint blockHeight = env->CallIntMethod(pp, getBlockHeightID); + + return VideoCapabilities::PerformancePoint(VideoSize(blockWidth, blockHeight), + width, height, maxFrameRate, maxMacroBlockRate); +} + +static jobject convertToJavaVideoCapabilities(JNIEnv *env, + std::shared_ptr<VideoCapabilities> videoCaps) { + if (videoCaps == nullptr) { + return NULL; + } + + // get Java bitrateRange + const Range<int32_t>& bitrateRange = videoCaps->getBitrateRange(); + jobject jBitrateRange = convertToJavaIntRange(env, bitrateRange); + + // get Java widthRange + const Range<int32_t>& widthRange = videoCaps->getSupportedWidths(); + jobject jWidthRange = convertToJavaIntRange(env, widthRange); + + // get Java heightRange + const Range<int32_t>& heightRange = videoCaps->getSupportedHeights(); + jobject jHeightRange = convertToJavaIntRange(env, heightRange); + + // get Java frameRateRange + const Range<int32_t>& frameRateRange = videoCaps->getSupportedFrameRates(); + jobject jFrameRateRange = convertToJavaIntRange(env, frameRateRange); + + // get Java performancePoints + const std::vector<VideoCapabilities::PerformancePoint>& performancePoints + = videoCaps->getSupportedPerformancePoints(); + jobject jPerformancePoints = convertToJavaPerformancePoints(env, performancePoints); + + // get width alignment + int32_t widthAlignment = videoCaps->getWidthAlignment(); + + // get height alignment + int32_t heightAlignment = videoCaps->getHeightAlignment(); + + // get Java VideoCapsNativeImpl + jclass videoCapsImplClazz = env->FindClass( + "android/media/MediaCodecInfo$VideoCapabilities$VideoCapsNativeImpl"); + CHECK(videoCapsImplClazz != NULL); + jmethodID videoCapsImplConstructID = env->GetMethodID(videoCapsImplClazz, "<init>", + "(Landroid/util/Range;" + "Landroid/util/Range;" + "Landroid/util/Range;" + "Landroid/util/Range;" + "Ljava/util/List;II)V"); + jobject jVideoCapsImpl = env->NewObject(videoCapsImplClazz, videoCapsImplConstructID, + jBitrateRange, jWidthRange, jHeightRange, jFrameRateRange, jPerformancePoints, + widthAlignment, heightAlignment); + // The native VideoCapabilities won't be destructed until process ends. + env->SetLongField(jVideoCapsImpl, fields.videoCapsContext, (jlong)videoCaps.get()); + + // get Java VideoCapabilities + jclass videoCapsClazz + = env->FindClass("android/media/MediaCodecInfo$VideoCapabilities"); + CHECK(videoCapsClazz != NULL); + jmethodID videoCapsConstructID = env->GetMethodID(videoCapsClazz, "<init>", + "(Landroid/media/MediaCodecInfo$VideoCapabilities$VideoCapsIntf;)V"); + jobject jVideoCaps = env->NewObject(videoCapsClazz, videoCapsConstructID, jVideoCapsImpl); + + env->DeleteLocalRef(jBitrateRange); + jBitrateRange = NULL; + + env->DeleteLocalRef(jWidthRange); + jWidthRange = NULL; + + env->DeleteLocalRef(jHeightRange); + jHeightRange = NULL; + + env->DeleteLocalRef(jFrameRateRange); + jFrameRateRange = NULL; + + env->DeleteLocalRef(jPerformancePoints); + jPerformancePoints = NULL; + + env->DeleteLocalRef(jVideoCapsImpl); + jVideoCapsImpl = NULL; + + return jVideoCaps; +} + +static jobject convertToJavaEncoderCapabilities(JNIEnv *env, + std::shared_ptr<EncoderCapabilities> encoderCaps) { + if (encoderCaps == nullptr) { + return NULL; + } + + // get quality range + const Range<int>& qualityRange = encoderCaps->getQualityRange(); + jobject jQualityRange = convertToJavaIntRange(env, qualityRange); + + // get complexity range + const Range<int>& complexityRange = encoderCaps->getComplexityRange(); + jobject jComplexityRange = convertToJavaIntRange(env, complexityRange); + + // construct java EncoderCapsNativeImpl + jclass encoderCapsImplClazz = env->FindClass( + "android/media/MediaCodecInfo$EncoderCapabilities$EncoderCapsNativeImpl"); + CHECK(encoderCapsImplClazz != NULL); + jmethodID encoderCapsImplConstructID = env->GetMethodID(encoderCapsImplClazz, "<init>", + "(Landroid/util/Range;Landroid/util/Range;)V"); + jobject jEncoderCapsImpl = env->NewObject(encoderCapsImplClazz, encoderCapsImplConstructID, + jQualityRange, jComplexityRange); + // The native EncoderCapabilities won't be destructed until process ends. + env->SetLongField(jEncoderCapsImpl, fields.encoderCapsContext, (jlong)encoderCaps.get()); + + // construct java EncoderCapabilities object + jclass encoderCapsClazz + = env->FindClass("android/media/MediaCodecInfo$EncoderCapabilities"); + CHECK(encoderCapsClazz != NULL); + jmethodID encoderCapsConstructID = env->GetMethodID(encoderCapsClazz, "<init>", + "(Landroid/media/MediaCodecInfo$EncoderCapabilities$EncoderCapsIntf;)V"); + jobject jEncoderCaps = env->NewObject(encoderCapsClazz, encoderCapsConstructID, + jEncoderCapsImpl); + + env->DeleteLocalRef(jQualityRange); + jQualityRange = NULL; + + env->DeleteLocalRef(jComplexityRange); + jComplexityRange = NULL; + + env->DeleteLocalRef(jEncoderCapsImpl); + jEncoderCapsImpl = NULL; + + return jEncoderCaps; +} + +// Java CodecCapsNativeImpl keeps the defaultFormat, profileLevels, colorFormats, audioCapabilities, +// videoCapabilities and encoderCapabilities in it to prevent reconsturction when called by getter. +static jobject convertToJavaCodecCapsNativeImpl( + JNIEnv *env, std::shared_ptr<CodecCapabilities> codecCaps) { + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return NULL; + } + + // Construct defaultFormat + sp<AMessage> defaultFormat = codecCaps->getDefaultFormat(); + + jobject formatMap = NULL; + if (ConvertMessageToMap(env, defaultFormat, &formatMap)) { + return NULL; + } + + ScopedLocalRef<jclass> mediaFormatClass{env, env->FindClass("android/media/MediaFormat")}; + ScopedLocalRef<jobject> jDefaultFormat{env, env->NewObject( + mediaFormatClass.get(), + env->GetMethodID(mediaFormatClass.get(), "<init>", "(Ljava/util/Map;)V"), + formatMap)}; + + env->DeleteLocalRef(formatMap); + formatMap = NULL; + + // Construct Java ProfileLevelArray + std::vector<ProfileLevel> profileLevels = codecCaps->getProfileLevels(); + + jclass profileLevelClazz = + env->FindClass("android/media/MediaCodecInfo$CodecProfileLevel"); + CHECK(profileLevelClazz != NULL); + + jobjectArray profileLevelArray = + env->NewObjectArray(profileLevels.size(), profileLevelClazz, NULL); + + jfieldID profileField = + env->GetFieldID(profileLevelClazz, "profile", "I"); + jfieldID levelField = + env->GetFieldID(profileLevelClazz, "level", "I"); + + for (size_t i = 0; i < profileLevels.size(); ++i) { + const ProfileLevel &src = profileLevels.at(i); + + jobject profileLevelObj = env->AllocObject(profileLevelClazz); + + env->SetIntField(profileLevelObj, profileField, src.mProfile); + env->SetIntField(profileLevelObj, levelField, src.mLevel); + + env->SetObjectArrayElement(profileLevelArray, i, profileLevelObj); + + env->DeleteLocalRef(profileLevelObj); + profileLevelObj = NULL; + } + + // Construct ColorFormatArray + std::vector<uint32_t> colorFormats = codecCaps->getColorFormats(); + + jintArray colorFormatsArray = env->NewIntArray(colorFormats.size()); + env->SetIntArrayRegion(colorFormatsArray, 0, colorFormats.size(), + reinterpret_cast<jint*>(colorFormats.data())); + + // Construct and set AudioCapabilities + std::shared_ptr<AudioCapabilities> audioCaps = codecCaps->getAudioCapabilities(); + jobject jAudioCaps = convertToJavaAudioCapabilities(env, audioCaps); + + // Set VideoCapabilities + std::shared_ptr<VideoCapabilities> videoCaps = codecCaps->getVideoCapabilities(); + jobject jVideoCaps = convertToJavaVideoCapabilities(env, videoCaps); + + // Set EncoderCapabilities + std::shared_ptr<EncoderCapabilities> encoderCaps = codecCaps->getEncoderCapabilities(); + jobject jEncoderCaps = convertToJavaEncoderCapabilities(env, encoderCaps); + + // Construct CodecCapsNativeImpl + jclass codecCapsImplClazz = + env->FindClass("android/media/MediaCodecInfo$CodecCapabilities$CodecCapsNativeImpl"); + CHECK(codecCapsImplClazz != NULL); + jmethodID codecCapsImplConstructID = env->GetMethodID(codecCapsImplClazz, "<init>", + "([Landroid/media/MediaCodecInfo$CodecProfileLevel;[I" + "Landroid/media/MediaFormat;" + "Landroid/media/MediaCodecInfo$AudioCapabilities;" + "Landroid/media/MediaCodecInfo$VideoCapabilities;" + "Landroid/media/MediaCodecInfo$EncoderCapabilities;)V"); + jobject javaCodecCapsImpl = env->NewObject(codecCapsImplClazz, codecCapsImplConstructID, + profileLevelArray, colorFormatsArray, jDefaultFormat.get(), + jAudioCaps, jVideoCaps, jEncoderCaps); + + // Construct JCodecCapabilities and hold the codecCaps in it + sp<JCodecCapabilities> jCodecCaps = sp<JCodecCapabilities>::make(codecCaps); + setCodecCapabilities(env, javaCodecCapsImpl, jCodecCaps); + + env->DeleteLocalRef(profileLevelArray); + profileLevelArray = NULL; + + env->DeleteLocalRef(colorFormatsArray); + colorFormatsArray = NULL; + + env->DeleteLocalRef(jAudioCaps); + jAudioCaps = NULL; + + env->DeleteLocalRef(jVideoCaps); + jVideoCaps = NULL; + + env->DeleteLocalRef(jEncoderCaps); + jEncoderCaps = NULL; + + return javaCodecCapsImpl; +} + +jobject convertToJavaCodecCapabiliites( + JNIEnv *env, std::shared_ptr<CodecCapabilities> codecCaps) { + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return NULL; + } + + jobject javaCodecCapsImpl = convertToJavaCodecCapsNativeImpl(env, codecCaps); + + // Construct CodecCapabilities + jclass codecCapsClazz = env->FindClass("android/media/MediaCodecInfo$CodecCapabilities"); + CHECK(codecCapsClazz != NULL); + + jmethodID codecCapsConstructID = env->GetMethodID(codecCapsClazz, "<init>", + "(Landroid/media/MediaCodecInfo$CodecCapabilities$CodecCapsIntf;)V"); + jobject javaCodecCaps = env->NewObject(codecCapsClazz, codecCapsConstructID, javaCodecCapsImpl); + + return javaCodecCaps; +} + +} // namespace android + +// ---------------------------------------------------------------------------- + +using namespace android; + +// AudioCapabilities + +static void android_media_AudioCapabilities_native_init(JNIEnv *env, jobject /* thiz */) { + jclass audioCapsImplClazz + = env->FindClass("android/media/MediaCodecInfo$AudioCapabilities$AudioCapsNativeImpl"); + if (audioCapsImplClazz == NULL) { + return; + } + + fields.audioCapsContext = env->GetFieldID(audioCapsImplClazz, "mNativeContext", "J"); + if (fields.audioCapsContext == NULL) { + return; + } + + env->DeleteLocalRef(audioCapsImplClazz); +} + +static jint android_media_AudioCapabilities_getMaxInputChannelCount(JNIEnv *env, jobject thiz) { + AudioCapabilities* const audioCaps = getAudioCapabilities(env, thiz); + if (audioCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + int32_t maxInputChannelCount = audioCaps->getMaxInputChannelCount(); + return maxInputChannelCount; +} + +static jint android_media_AudioCapabilities_getMinInputChannelCount(JNIEnv *env, jobject thiz) { + AudioCapabilities* const audioCaps = getAudioCapabilities(env, thiz); + if (audioCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + int32_t minInputChannelCount = audioCaps->getMinInputChannelCount(); + return minInputChannelCount; +} + +static jboolean android_media_AudioCapabilities_isSampleRateSupported(JNIEnv *env, jobject thiz, + int sampleRate) { + AudioCapabilities* const audioCaps = getAudioCapabilities(env, thiz); + if (audioCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + bool res = audioCaps->isSampleRateSupported(sampleRate); + return res; +} + +// PerformancePoint + +static jboolean android_media_VideoCapabilities_PerformancePoint_covers(JNIEnv *env, jobject thiz, + jobject other) { + VideoCapabilities::PerformancePoint pp0 = convertToNativePerformancePoint(env, thiz); + VideoCapabilities::PerformancePoint pp1 = convertToNativePerformancePoint(env, other); + + bool res = pp0.covers(pp1); + return res; +} + +static jboolean android_media_VideoCapabilities_PerformancePoint_equals(JNIEnv *env, jobject thiz, + jobject other) { + VideoCapabilities::PerformancePoint pp0 = convertToNativePerformancePoint(env, thiz); + VideoCapabilities::PerformancePoint pp1 = convertToNativePerformancePoint(env, other); + + bool res = pp0.equals(pp1); + return res; +} + +// VideoCapabilities + +static void android_media_VideoCapabilities_native_init(JNIEnv *env, jobject /* thiz */) { + jclass clazz + = env->FindClass("android/media/MediaCodecInfo$VideoCapabilities$VideoCapsNativeImpl"); + if (clazz == NULL) { + return; + } + + fields.videoCapsContext = env->GetFieldID(clazz, "mNativeContext", "J"); + if (fields.videoCapsContext == NULL) { + return; + } + + env->DeleteLocalRef(clazz); +} + +static jboolean android_media_VideoCapabilities_areSizeAndRateSupported(JNIEnv *env, jobject thiz, + int32_t width, int32_t height, double frameRate) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + bool res = videoCaps->areSizeAndRateSupported(width, height, frameRate); + return res; +} + +static jboolean android_media_VideoCapabilities_isSizeSupported(JNIEnv *env, jobject thiz, + int32_t width, int32_t height) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + bool res = videoCaps->isSizeSupported(width, height); + return res; +} + +static jobject android_media_VideoCapabilities_getAchievableFrameRatesFor(JNIEnv *env, jobject thiz, + int32_t width, int32_t height) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return NULL; + } + + std::optional<Range<double>> frameRates = videoCaps->getAchievableFrameRatesFor(width, height); + if (!frameRates) { + return NULL; + } + jobject jFrameRates = convertToJavaDoubleRange(env, frameRates.value()); + return jFrameRates; +} + +static jobject android_media_VideoCapabilities_getSupportedFrameRatesFor(JNIEnv *env, jobject thiz, + int32_t width, int32_t height) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return NULL; + } + + std::optional<Range<double>> frameRates = videoCaps->getSupportedFrameRatesFor(width, height); + if (!frameRates) { + return NULL; + } + jobject jFrameRates = convertToJavaDoubleRange(env, frameRates.value()); + return jFrameRates; +} + +static jobject android_media_VideoCapabilities_getSupportedWidthsFor(JNIEnv *env, jobject thiz, + int32_t height) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return NULL; + } + + std::optional<Range<int32_t>> supportedWidths = videoCaps->getSupportedWidthsFor(height); + if (!supportedWidths) { + return NULL; + } + jobject jSupportedWidths = convertToJavaIntRange(env, supportedWidths.value()); + + return jSupportedWidths; +} + +static jobject android_media_VideoCapabilities_getSupportedHeightsFor(JNIEnv *env, jobject thiz, + int32_t width) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return NULL; + } + + std::optional<Range<int32_t>> supportedHeights = videoCaps->getSupportedHeightsFor(width); + if (!supportedHeights) { + return NULL; + } + jobject jSupportedHeights = convertToJavaIntRange(env, supportedHeights.value()); + + return jSupportedHeights; +} + +static jint android_media_VideoCapabilities_getSmallerDimensionUpperLimit(JNIEnv *env, + jobject thiz) { + VideoCapabilities* const videoCaps = getVideoCapabilities(env, thiz); + if (videoCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + int smallerDimensionUpperLimit = videoCaps->getSmallerDimensionUpperLimit(); + return smallerDimensionUpperLimit; +} + +// EncoderCapabilities + +static void android_media_EncoderCapabilities_native_init(JNIEnv *env, jobject /* thiz */) { + jclass clazz = env->FindClass( + "android/media/MediaCodecInfo$EncoderCapabilities$EncoderCapsNativeImpl"); + if (clazz == NULL) { + return; + } + + fields.encoderCapsContext = env->GetFieldID(clazz, "mNativeContext", "J"); + if (fields.encoderCapsContext == NULL) { + return; + } + + env->DeleteLocalRef(clazz); +} + +static jboolean android_media_EncoderCapabilities_isBitrateModeSupported(JNIEnv *env, jobject thiz, + int mode) { + EncoderCapabilities* const encoderCaps = getEncoderCapabilities(env, thiz); + if (encoderCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + bool res = encoderCaps->isBitrateModeSupported(mode); + return res; +} + +// CodecCapabilities + +static void android_media_CodecCapabilities_native_init(JNIEnv *env, jobject /* thiz */) { + jclass codecCapsClazz + = env->FindClass("android/media/MediaCodecInfo$CodecCapabilities$CodecCapsNativeImpl"); + if (codecCapsClazz == NULL) { + return; + } + + fields.codecCapsContext = env->GetFieldID(codecCapsClazz, "mNativeContext", "J"); + if (fields.codecCapsContext == NULL) { + return; + } + + env->DeleteLocalRef(codecCapsClazz); +} + +static jobject android_media_CodecCapabilities_createFromProfileLevel(JNIEnv *env, + jobject /* thiz */, jstring mediaType, jint profile, jint level) { + if (mediaType == NULL) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return NULL; + } + + const char *mediaTypeStr = env->GetStringUTFChars(mediaType, nullptr); + if (mediaTypeStr == nullptr) { + return NULL; + } + + std::shared_ptr<CodecCapabilities> codecCaps = CodecCapabilities::CreateFromProfileLevel( + mediaTypeStr, profile, level); + + jobject javaCodecCapsImpl = convertToJavaCodecCapsNativeImpl(env, codecCaps); + + env->ReleaseStringUTFChars(mediaType, mediaTypeStr); + + return javaCodecCapsImpl; +} + +static jobject android_media_CodecCapabilities_native_dup(JNIEnv *env, jobject thiz) { + sp<JCodecCapabilities> jCodecCaps = getCodecCapabilities(env, thiz); + + // As the CodecCaps objects are ready ony, it is ok to use the default copy constructor. + // The duplicate CodecCaps will share the same subobjects with the existing one. + // The lifetime of subobjects are managed by the shared pointer and sp. + std::shared_ptr<CodecCapabilities> duplicate + = std::make_shared<CodecCapabilities>(*(jCodecCaps->getCodecCaps())); + + jobject javaCodecCapsImpl = convertToJavaCodecCapsNativeImpl(env, duplicate); + + return javaCodecCapsImpl; +} + +static void android_media_CodecCapabilities_native_finalize(JNIEnv *env, jobject thiz) { + ALOGV("native_finalize"); + setCodecCapabilities(env, thiz, NULL); +} + +static jint android_media_CodecCapabilities_getMaxSupportedInstances(JNIEnv *env, jobject thiz) { + sp<JCodecCapabilities> codecCaps = getCodecCapabilities(env, thiz); + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return 0; + } + + int maxSupportedInstances = codecCaps->getMaxSupportedInstances(); + return maxSupportedInstances; +} + +static jstring android_media_CodecCapabilities_getMimeType(JNIEnv *env, jobject thiz) { + sp<JCodecCapabilities> codecCaps = getCodecCapabilities(env, thiz); + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return NULL; + } + + std::string mediaType = codecCaps->getMediaType(); + return env->NewStringUTF(mediaType.c_str()); +} + +static jboolean android_media_CodecCapabilities_isFeatureRequired( + JNIEnv *env, jobject thiz, jstring name) { + sp<JCodecCapabilities> codecCaps = getCodecCapabilities(env, thiz); + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return false; + } + + if (name == NULL) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return -ENOENT; + } + + const char *nameStr = env->GetStringUTFChars(name, NULL); + if (nameStr == NULL) { + // Out of memory exception already pending. + return -ENOENT; + } + + bool isFeatureRequired = codecCaps->isFeatureRequired(nameStr); + + env->ReleaseStringUTFChars(name, nameStr); + + return isFeatureRequired; +} + +static jboolean android_media_CodecCapabilities_isFeatureSupported( + JNIEnv *env, jobject thiz, jstring name) { + sp<JCodecCapabilities> codecCaps = getCodecCapabilities(env, thiz); + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return false; + } + + if (name == NULL) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return -ENOENT; + } + + const char *nameStr = env->GetStringUTFChars(name, NULL); + if (nameStr == NULL) { + // Out of memory exception already pending. + return -ENOENT; + } + + bool isFeatureSupported = codecCaps->isFeatureSupported(nameStr); + + env->ReleaseStringUTFChars(name, nameStr); + + return isFeatureSupported; +} + +static jboolean android_media_CodecCapabilities_isFormatSupported(JNIEnv *env, jobject thiz, + jobjectArray keys, jobjectArray values) { + sp<JCodecCapabilities> codecCaps = getCodecCapabilities(env, thiz); + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return false; + } + + sp<AMessage> format; + status_t err = ConvertKeyValueArraysToMessage(env, keys, values, &format); + if (err != OK) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return -ENOENT;; + } + + return codecCaps->isFormatSupported(format); +} + +static jboolean android_media_CodecCapabilities_isRegular(JNIEnv *env, jobject thiz) { + sp<JCodecCapabilities> codecCaps = getCodecCapabilities(env, thiz); + if (codecCaps == nullptr) { + jniThrowException(env, "java/lang/IllegalStateException", NULL); + return false; + } + + bool res = codecCaps->isRegular(); + return res; +} + +// ---------------------------------------------------------------------------- + +static const JNINativeMethod gAudioCapsMethods[] = { + {"native_init", "()V", (void *)android_media_AudioCapabilities_native_init}, + {"native_getMaxInputChannelCount", "()I", (void *)android_media_AudioCapabilities_getMaxInputChannelCount}, + {"native_getMinInputChannelCount", "()I", (void *)android_media_AudioCapabilities_getMinInputChannelCount}, + {"native_isSampleRateSupported", "(I)Z", (void *)android_media_AudioCapabilities_isSampleRateSupported} +}; + +static const JNINativeMethod gPerformancePointMethods[] = { + {"native_covers", "(Landroid/media/MediaCodecInfo$VideoCapabilities$PerformancePoint;)Z", (void *)android_media_VideoCapabilities_PerformancePoint_covers}, + {"native_equals", "(Landroid/media/MediaCodecInfo$VideoCapabilities$PerformancePoint;)Z", (void *)android_media_VideoCapabilities_PerformancePoint_equals}, +}; + +static const JNINativeMethod gVideoCapsMethods[] = { + {"native_init", "()V", (void *)android_media_VideoCapabilities_native_init}, + {"native_areSizeAndRateSupported", "(IID)Z", (void *)android_media_VideoCapabilities_areSizeAndRateSupported}, + {"native_isSizeSupported", "(II)Z", (void *)android_media_VideoCapabilities_isSizeSupported}, + {"native_getAchievableFrameRatesFor", "(II)Landroid/util/Range;", (void *)android_media_VideoCapabilities_getAchievableFrameRatesFor}, + {"native_getSupportedFrameRatesFor", "(II)Landroid/util/Range;", (void *)android_media_VideoCapabilities_getSupportedFrameRatesFor}, + {"native_getSupportedWidthsFor", "(I)Landroid/util/Range;", (void *)android_media_VideoCapabilities_getSupportedWidthsFor}, + {"native_getSupportedHeightsFor", "(I)Landroid/util/Range;", (void *)android_media_VideoCapabilities_getSupportedHeightsFor}, + {"native_getSmallerDimensionUpperLimit", "()I", (void *)android_media_VideoCapabilities_getSmallerDimensionUpperLimit} +}; + +static const JNINativeMethod gEncoderCapsMethods[] = { + {"native_init", "()V", (void *)android_media_EncoderCapabilities_native_init}, + {"native_isBitrateModeSupported", "(I)Z", (void *)android_media_EncoderCapabilities_isBitrateModeSupported} +}; + +static const JNINativeMethod gCodecCapsMethods[] = { + { "native_init", "()V", (void *)android_media_CodecCapabilities_native_init }, + { "native_createFromProfileLevel", "(Ljava/lang/String;II)Landroid/media/MediaCodecInfo$CodecCapabilities$CodecCapsNativeImpl;", (void *)android_media_CodecCapabilities_createFromProfileLevel }, + { "native_dup", "()Landroid/media/MediaCodecInfo$CodecCapabilities$CodecCapsNativeImpl;", (void *)android_media_CodecCapabilities_native_dup }, + { "native_finalize", "()V", (void *)android_media_CodecCapabilities_native_finalize }, + { "native_getMaxSupportedInstances", "()I", (void *)android_media_CodecCapabilities_getMaxSupportedInstances }, + { "native_getMimeType", "()Ljava/lang/String;", (void *)android_media_CodecCapabilities_getMimeType }, + { "native_isFeatureRequired", "(Ljava/lang/String;)Z", (void *)android_media_CodecCapabilities_isFeatureRequired }, + { "native_isFeatureSupported", "(Ljava/lang/String;)Z", (void *)android_media_CodecCapabilities_isFeatureSupported }, + { "native_isFormatSupported", "([Ljava/lang/String;[Ljava/lang/Object;)Z", (void *)android_media_CodecCapabilities_isFormatSupported }, + { "native_isRegular", "()Z", (void *)android_media_CodecCapabilities_isRegular }, +}; + +int register_android_media_CodecCapabilities(JNIEnv *env) { + int result = AndroidRuntime::registerNativeMethods(env, + "android/media/MediaCodecInfo$AudioCapabilities$AudioCapsNativeImpl", + gAudioCapsMethods, NELEM(gAudioCapsMethods)); + if (result != JNI_OK) { + return result; + } + + result = AndroidRuntime::registerNativeMethods(env, + "android/media/MediaCodecInfo$VideoCapabilities$PerformancePoint", + gPerformancePointMethods, NELEM(gPerformancePointMethods)); + if (result != JNI_OK) { + return result; + } + + result = AndroidRuntime::registerNativeMethods(env, + "android/media/MediaCodecInfo$VideoCapabilities$VideoCapsNativeImpl", + gVideoCapsMethods, NELEM(gVideoCapsMethods)); + if (result != JNI_OK) { + return result; + } + + result = AndroidRuntime::registerNativeMethods(env, + "android/media/MediaCodecInfo$EncoderCapabilities$EncoderCapsNativeImpl", + gEncoderCapsMethods, NELEM(gEncoderCapsMethods)); + if (result != JNI_OK) { + return result; + } + + result = AndroidRuntime::registerNativeMethods(env, + "android/media/MediaCodecInfo$CodecCapabilities$CodecCapsNativeImpl", + gCodecCapsMethods, NELEM(gCodecCapsMethods)); + return result; +}
\ No newline at end of file diff --git a/media/jni/android_media_CodecCapabilities.h b/media/jni/android_media_CodecCapabilities.h new file mode 100644 index 000000000000..5cca0b503740 --- /dev/null +++ b/media/jni/android_media_CodecCapabilities.h @@ -0,0 +1,47 @@ +/* + * Copyright 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. + */ + +#ifndef _ANDROID_MEDIA_CODECCAPABILITIES_H_ +#define _ANDROID_MEDIA_CODECCAPABILITIES_H_ + +#include "jni.h" + +#include <media/CodecCapabilities.h> + +namespace android { + +struct JCodecCapabilities : public RefBase { + JCodecCapabilities(std::shared_ptr<CodecCapabilities> codecCaps); + + std::shared_ptr<CodecCapabilities> getCodecCaps() const; + + int32_t getMaxSupportedInstances() const; + std::string getMediaType() const; + bool isFeatureRequired(const std::string& name) const; + bool isFeatureSupported(const std::string& name) const; + bool isFormatSupported(const sp<AMessage> &format) const; + bool isRegular() const; + +private: + std::shared_ptr<CodecCapabilities> mCodecCaps; +}; + +jobject convertToJavaCodecCapabiliites( + JNIEnv *env, std::shared_ptr<CodecCapabilities> codecCaps); + +} + +#endif // _ANDROID_MEDIA_CODECCAPABILITIES_H_
\ No newline at end of file diff --git a/media/jni/android_media_MediaCodec.cpp b/media/jni/android_media_MediaCodec.cpp index 8419ce761a4a..1790670903a4 100644 --- a/media/jni/android_media_MediaCodec.cpp +++ b/media/jni/android_media_MediaCodec.cpp @@ -16,12 +16,14 @@ //#define LOG_NDEBUG 0 #define LOG_TAG "MediaCodec-JNI" +#include <android_media_codec.h> #include <utils/Log.h> #include <type_traits> #include "android_media_MediaCodec.h" +#include "android_media_CodecCapabilities.h" #include "android_media_MediaCodecLinearBlock.h" #include "android_media_MediaCrypto.h" #include "android_media_MediaDescrambler.h" @@ -138,6 +140,8 @@ static struct { static struct { jclass capsClazz; jmethodID capsCtorId; + jclass cpasImplClazz; + jmethodID capsImplCtorId; jclass profileLevelClazz; jfieldID profileField; jfieldID levelField; @@ -996,10 +1000,12 @@ static jobject getCodecCapabilitiesObject( env->SetIntArrayRegion(colorFormatsArray.get(), i, 1, &val); } - return env->NewObject( - gCodecInfo.capsClazz, gCodecInfo.capsCtorId, + jobject javaCodecCapsImpl = env->NewObject( + gCodecInfo.cpasImplClazz, gCodecInfo.capsImplCtorId, profileLevelArray.get(), colorFormatsArray.get(), isEncoder, defaultFormatRef.get(), detailsRef.get()); + + return env->NewObject(gCodecInfo.capsClazz, gCodecInfo.capsCtorId, javaCodecCapsImpl); } status_t JMediaCodec::getCodecInfo(JNIEnv *env, jobject *codecInfoObject) const { @@ -1027,11 +1033,18 @@ status_t JMediaCodec::getCodecInfo(JNIEnv *env, jobject *codecInfoObject) const env->NewObjectArray(mediaTypes.size(), gCodecInfo.capsClazz, NULL)); for (size_t i = 0; i < mediaTypes.size(); i++) { - const sp<MediaCodecInfo::Capabilities> caps = - codecInfo->getCapabilitiesFor(mediaTypes[i].c_str()); - - ScopedLocalRef<jobject> capsObj(env, getCodecCapabilitiesObject( - env, mediaTypes[i].c_str(), isEncoder, caps)); + jobject jCodecCaps = NULL; + if (android::media::codec::provider_->native_capabilites()) { + const std::shared_ptr<CodecCapabilities> codecCaps + = codecInfo->getCodecCapsFor(mediaTypes[i].c_str()); + jCodecCaps = convertToJavaCodecCapabiliites(env, codecCaps); + } else { + const sp<MediaCodecInfo::Capabilities> caps = + codecInfo->getCapabilitiesFor(mediaTypes[i].c_str()); + jCodecCaps = getCodecCapabilitiesObject( + env, mediaTypes[i].c_str(), isEncoder, caps); + } + ScopedLocalRef<jobject> capsObj(env, jCodecCaps); env->SetObjectArrayElement(capsArrayObj.get(), i, capsObj.get()); } @@ -3877,10 +3890,20 @@ static void android_media_MediaCodec_native_init(JNIEnv *env, jclass) { gCodecInfo.capsClazz = (jclass)env->NewGlobalRef(clazz.get()); method = env->GetMethodID(clazz.get(), "<init>", + "(Landroid/media/MediaCodecInfo$CodecCapabilities$CodecCapsIntf;)V"); + CHECK(method != NULL); + gCodecInfo.capsCtorId = method; + + clazz.reset(env->FindClass( + "android/media/MediaCodecInfo$CodecCapabilities$CodecCapsLegacyImpl")); + CHECK(clazz.get() != NULL); + gCodecInfo.cpasImplClazz = (jclass)env->NewGlobalRef(clazz.get()); + + method = env->GetMethodID(clazz.get(), "<init>", "([Landroid/media/MediaCodecInfo$CodecProfileLevel;[IZ" "Ljava/util/Map;Ljava/util/Map;)V"); CHECK(method != NULL); - gCodecInfo.capsCtorId = method; + gCodecInfo.capsImplCtorId = method; clazz.reset(env->FindClass("android/media/MediaCodecInfo$CodecProfileLevel")); CHECK(clazz.get() != NULL); diff --git a/media/jni/android_media_MediaCodecList.cpp b/media/jni/android_media_MediaCodecList.cpp index 07866ac34e4c..3522b35539ab 100644 --- a/media/jni/android_media_MediaCodecList.cpp +++ b/media/jni/android_media_MediaCodecList.cpp @@ -16,6 +16,9 @@ //#define LOG_NDEBUG 0 #define LOG_TAG "MediaCodec-JNI" + +#include <android_media_codec.h> + #include <utils/Log.h> #include <media/stagefright/foundation/ADebug.h> @@ -32,6 +35,7 @@ #include "android_runtime/AndroidRuntime.h" #include "jni.h" #include <nativehelper/JNIHelp.h> +#include "android_media_CodecCapabilities.h" #include "android_media_Streams.h" using namespace android; @@ -245,95 +249,113 @@ static jobject android_media_MediaCodecList_getCodecCapabilities( return NULL; } - Vector<MediaCodecInfo::ProfileLevel> profileLevels; - Vector<uint32_t> colorFormats; + jobject caps; + if (android::media::codec::provider_->native_capabilites()) { + std::shared_ptr<CodecCapabilities> codecCaps = info.info->getCodecCapsFor(typeStr); + caps = android::convertToJavaCodecCapabiliites(env, codecCaps); + } else { + Vector<MediaCodecInfo::ProfileLevel> profileLevels; + Vector<uint32_t> colorFormats; + + sp<AMessage> defaultFormat = new AMessage(); + defaultFormat->setString("mime", typeStr); + + // TODO query default-format also from codec/codec list + const sp<MediaCodecInfo::Capabilities> &capabilities = + info.info->getCapabilitiesFor(typeStr); + env->ReleaseStringUTFChars(type, typeStr); + typeStr = NULL; + if (capabilities == NULL) { + jniThrowException(env, "java/lang/IllegalArgumentException", NULL); + return NULL; + } - sp<AMessage> defaultFormat = new AMessage(); - defaultFormat->setString("mime", typeStr); + capabilities->getSupportedColorFormats(&colorFormats); + capabilities->getSupportedProfileLevels(&profileLevels); + sp<AMessage> details = capabilities->getDetails(); + bool isEncoder = info.info->isEncoder(); - // TODO query default-format also from codec/codec list - const sp<MediaCodecInfo::Capabilities> &capabilities = - info.info->getCapabilitiesFor(typeStr); - env->ReleaseStringUTFChars(type, typeStr); - typeStr = NULL; - if (capabilities == NULL) { - jniThrowException(env, "java/lang/IllegalArgumentException", NULL); - return NULL; - } + jobject defaultFormatObj = NULL; + if (ConvertMessageToMap(env, defaultFormat, &defaultFormatObj)) { + return NULL; + } - capabilities->getSupportedColorFormats(&colorFormats); - capabilities->getSupportedProfileLevels(&profileLevels); - sp<AMessage> details = capabilities->getDetails(); - bool isEncoder = info.info->isEncoder(); + jobject infoObj = NULL; + if (ConvertMessageToMap(env, details, &infoObj)) { + env->DeleteLocalRef(defaultFormatObj); + return NULL; + } - jobject defaultFormatObj = NULL; - if (ConvertMessageToMap(env, defaultFormat, &defaultFormatObj)) { - return NULL; - } + jclass capsImplClazz = env->FindClass( + "android/media/MediaCodecInfo$CodecCapabilities$CodecCapsLegacyImpl"); + CHECK(capsImplClazz != NULL); - jobject infoObj = NULL; - if (ConvertMessageToMap(env, details, &infoObj)) { - env->DeleteLocalRef(defaultFormatObj); - return NULL; - } + jclass profileLevelClazz = + env->FindClass("android/media/MediaCodecInfo$CodecProfileLevel"); + CHECK(profileLevelClazz != NULL); - jclass capsClazz = - env->FindClass("android/media/MediaCodecInfo$CodecCapabilities"); - CHECK(capsClazz != NULL); + jobjectArray profileLevelArray = + env->NewObjectArray(profileLevels.size(), profileLevelClazz, NULL); - jclass profileLevelClazz = - env->FindClass("android/media/MediaCodecInfo$CodecProfileLevel"); - CHECK(profileLevelClazz != NULL); + jfieldID profileField = + env->GetFieldID(profileLevelClazz, "profile", "I"); - jobjectArray profileLevelArray = - env->NewObjectArray(profileLevels.size(), profileLevelClazz, NULL); + jfieldID levelField = + env->GetFieldID(profileLevelClazz, "level", "I"); - jfieldID profileField = - env->GetFieldID(profileLevelClazz, "profile", "I"); + for (size_t i = 0; i < profileLevels.size(); ++i) { + const MediaCodecInfo::ProfileLevel &src = profileLevels.itemAt(i); - jfieldID levelField = - env->GetFieldID(profileLevelClazz, "level", "I"); + jobject profileLevelObj = env->AllocObject(profileLevelClazz); - for (size_t i = 0; i < profileLevels.size(); ++i) { - const MediaCodecInfo::ProfileLevel &src = profileLevels.itemAt(i); + env->SetIntField(profileLevelObj, profileField, src.mProfile); + env->SetIntField(profileLevelObj, levelField, src.mLevel); - jobject profileLevelObj = env->AllocObject(profileLevelClazz); + env->SetObjectArrayElement(profileLevelArray, i, profileLevelObj); - env->SetIntField(profileLevelObj, profileField, src.mProfile); - env->SetIntField(profileLevelObj, levelField, src.mLevel); + env->DeleteLocalRef(profileLevelObj); + profileLevelObj = NULL; + } - env->SetObjectArrayElement(profileLevelArray, i, profileLevelObj); + jintArray colorFormatsArray = env->NewIntArray(colorFormats.size()); - env->DeleteLocalRef(profileLevelObj); - profileLevelObj = NULL; - } + for (size_t i = 0; i < colorFormats.size(); ++i) { + jint val = colorFormats.itemAt(i); + env->SetIntArrayRegion(colorFormatsArray, i, 1, &val); + } - jintArray colorFormatsArray = env->NewIntArray(colorFormats.size()); + jmethodID capsImplConstructID = env->GetMethodID(capsImplClazz, "<init>", + "([Landroid/media/MediaCodecInfo$CodecProfileLevel;[IZ" + "Ljava/util/Map;Ljava/util/Map;)V"); - for (size_t i = 0; i < colorFormats.size(); ++i) { - jint val = colorFormats.itemAt(i); - env->SetIntArrayRegion(colorFormatsArray, i, 1, &val); - } + jobject capsImpl = env->NewObject(capsImplClazz, capsImplConstructID, + profileLevelArray, colorFormatsArray, isEncoder, + defaultFormatObj, infoObj); + + jclass capsClazz = env->FindClass( + "android/media/MediaCodecInfo$CodecCapabilities"); + CHECK(capsClazz != NULL); - jmethodID capsConstructID = env->GetMethodID(capsClazz, "<init>", - "([Landroid/media/MediaCodecInfo$CodecProfileLevel;[IZ" - "Ljava/util/Map;Ljava/util/Map;)V"); + jmethodID capsConstructID = env->GetMethodID(capsClazz, "<init>", + "(Landroid/media/MediaCodecInfo$CodecCapabilities$CodecCapsIntf;)V"); - jobject caps = env->NewObject(capsClazz, capsConstructID, - profileLevelArray, colorFormatsArray, isEncoder, - defaultFormatObj, infoObj); + caps = env->NewObject(capsClazz, capsConstructID, capsImpl); - env->DeleteLocalRef(profileLevelArray); - profileLevelArray = NULL; + env->DeleteLocalRef(profileLevelArray); + profileLevelArray = NULL; - env->DeleteLocalRef(colorFormatsArray); - colorFormatsArray = NULL; + env->DeleteLocalRef(colorFormatsArray); + colorFormatsArray = NULL; - env->DeleteLocalRef(defaultFormatObj); - defaultFormatObj = NULL; + env->DeleteLocalRef(defaultFormatObj); + defaultFormatObj = NULL; + + env->DeleteLocalRef(infoObj); + infoObj = NULL; - env->DeleteLocalRef(infoObj); - infoObj = NULL; + env->DeleteLocalRef(capsImpl); + capsImpl = NULL; + } return caps; } diff --git a/media/jni/android_media_MediaPlayer.cpp b/media/jni/android_media_MediaPlayer.cpp index a94230014437..f159282ed2c2 100644 --- a/media/jni/android_media_MediaPlayer.cpp +++ b/media/jni/android_media_MediaPlayer.cpp @@ -1479,6 +1479,7 @@ static int register_android_media_MediaPlayer(JNIEnv *env) extern int register_android_media_ImageReader(JNIEnv *env); extern int register_android_media_ImageWriter(JNIEnv *env); extern int register_android_media_JetPlayer(JNIEnv *env); +extern int register_android_media_CodecCapabilities(JNIEnv *env); extern int register_android_media_Crypto(JNIEnv *env); extern int register_android_media_Drm(JNIEnv *env); extern int register_android_media_Descrambler(JNIEnv *env); @@ -1593,6 +1594,11 @@ jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) goto bail; } + if (register_android_media_CodecCapabilities(env) < 0) { + ALOGE("ERROR: CodecCapabilities native registration failed"); + goto bail; + } + if (register_android_media_Crypto(env) < 0) { ALOGE("ERROR: MediaCodec native registration failed"); goto bail; diff --git a/nfc/tests/src/android/nfc/NfcManagerTest.java b/nfc/tests/src/android/nfc/NfcManagerTest.java new file mode 100644 index 000000000000..06314cc03d37 --- /dev/null +++ b/nfc/tests/src/android/nfc/NfcManagerTest.java @@ -0,0 +1,71 @@ +/* + * 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.nfc; + +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.when; + +import android.content.Context; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.dx.mockito.inline.extended.ExtendedMockito; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +@RunWith(AndroidJUnit4.class) +public class NfcManagerTest { + + private MockitoSession mMockitoSession; + private NfcManager mNfcManager; + @Mock + private Context mContext; + + @Before + public void setUp() { + mMockitoSession = ExtendedMockito.mockitoSession() + .mockStatic(NfcAdapter.class) + .strictness(Strictness.LENIENT) + .startMocking(); + MockitoAnnotations.initMocks(this); + + when(NfcAdapter.getNfcAdapter(any())).thenReturn(mock(NfcAdapter.class)); + when(mContext.getApplicationContext()).thenReturn(mContext); + mNfcManager = new NfcManager(mContext); + } + + @After + public void tearDown() { + mMockitoSession.finishMocking(); + } + + @Test + public void testGetDefaultAdapter() { + NfcAdapter nfcAdapter = mNfcManager.getDefaultAdapter(); + assertThat(nfcAdapter).isNotNull(); + } +} diff --git a/nfc/tests/src/android/nfc/cardemulation/CardemulationTest.java b/nfc/tests/src/android/nfc/cardemulation/CardemulationTest.java new file mode 100644 index 000000000000..6be95adbeba0 --- /dev/null +++ b/nfc/tests/src/android/nfc/cardemulation/CardemulationTest.java @@ -0,0 +1,113 @@ +/* + * 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.nfc.cardemulation; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.PackageManager; +import android.nfc.INfcCardEmulation; +import android.nfc.NfcAdapter; +import android.os.RemoteException; +import android.os.UserHandle; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.dx.mockito.inline.extended.ExtendedMockito; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +@RunWith(AndroidJUnit4.class) +public class CardemulationTest { + + private CardEmulation mCardEmulation; + @Mock + private Context mContext; + @Mock + private INfcCardEmulation mINfcCardEmulation; + @Mock + private NfcAdapter mNfcAdapter; + @Mock + private PackageManager mPackageManager; + private MockitoSession mMockitoSession; + + @Before + public void setUp() { + mMockitoSession = ExtendedMockito.mockitoSession() + .mockStatic(NfcAdapter.class) + .strictness(Strictness.LENIENT) + .startMocking(); + MockitoAnnotations.initMocks(this); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_NFC_HOST_CARD_EMULATION)) + .thenReturn(true); + when(mContext.getApplicationContext()).thenReturn(mContext); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + assertThat(mNfcAdapter).isNotNull(); + when(mNfcAdapter.getCardEmulationService()).thenReturn(mINfcCardEmulation); + when(mNfcAdapter.getContext()).thenReturn(mContext); + mCardEmulation = CardEmulation.getInstance(mNfcAdapter); + } + + @After + public void tearDown() { + mMockitoSession.finishMocking(); + } + + @Test + public void testIsDefaultServiceForCategory() throws RemoteException { + ComponentName componentName = mock(ComponentName.class); + UserHandle userHandle = mock(UserHandle.class); + when(userHandle.getIdentifier()).thenReturn(1); + when(mContext.getUser()).thenReturn(userHandle); + when(mINfcCardEmulation.isDefaultServiceForCategory(1, componentName, + "payment")).thenReturn(true); + boolean result = mCardEmulation.isDefaultServiceForCategory(componentName, + "payment"); + assertThat(result).isTrue(); + verify(mINfcCardEmulation).isDefaultServiceForCategory(1, componentName, + "payment"); + + } + + @Test + public void testIsDefaultServiceForAid() throws RemoteException { + ComponentName componentName = mock(ComponentName.class); + UserHandle userHandle = mock(UserHandle.class); + when(userHandle.getIdentifier()).thenReturn(1); + when(mContext.getUser()).thenReturn(userHandle); + when(mINfcCardEmulation.isDefaultServiceForAid(1, componentName, + "payment")).thenReturn(true); + boolean result = mCardEmulation.isDefaultServiceForAid(componentName, + "payment"); + assertThat(result).isTrue(); + verify(mINfcCardEmulation).isDefaultServiceForAid(1, componentName, + "payment"); + } +} diff --git a/nfc/tests/src/android/nfc/tech/NfcATest.java b/nfc/tests/src/android/nfc/tech/NfcATest.java new file mode 100644 index 000000000000..40076ebd0a0a --- /dev/null +++ b/nfc/tests/src/android/nfc/tech/NfcATest.java @@ -0,0 +1,185 @@ +/* + * 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.nfc.tech; + +import static android.nfc.tech.NfcA.EXTRA_ATQA; +import static android.nfc.tech.NfcA.EXTRA_SAK; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.nfc.ErrorCodes; +import android.nfc.INfcTag; +import android.nfc.Tag; +import android.nfc.TransceiveResult; +import android.os.Bundle; +import android.os.RemoteException; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.IOException; + +public class NfcATest { + @Mock + private Tag mMockTag; + @Mock + private INfcTag mMockTagService; + @Mock + private Bundle mMockBundle; + private NfcA mNfcA; + private final byte[] mSampleArray = new byte[] {1, 2, 3}; + + @Before + public void setUp() throws RemoteException { + MockitoAnnotations.initMocks(this); + when(mMockBundle.getShort(EXTRA_SAK)).thenReturn((short) 1); + when(mMockBundle.getByteArray(EXTRA_ATQA)).thenReturn(mSampleArray); + when(mMockTag.getTechExtras(TagTechnology.NFC_A)).thenReturn(mMockBundle); + + mNfcA = new NfcA(mMockTag); + } + + @Test + public void testGetNfcAWithTech() { + Tag mockTag = mock(Tag.class); + when(mockTag.getTechExtras(TagTechnology.NFC_A)).thenReturn(mMockBundle); + when(mockTag.hasTech(TagTechnology.NFC_A)).thenReturn(true); + + assertNotNull(NfcA.get(mockTag)); + verify(mockTag).getTechExtras(TagTechnology.NFC_A); + verify(mockTag).hasTech(TagTechnology.NFC_A); + } + + @Test + public void testGetNfcAWithoutTech() { + when(mMockTag.hasTech(TagTechnology.NFC_A)).thenReturn(false); + assertNull(NfcA.get(mMockTag)); + } + + @Test + public void testGetAtga() { + assertNotNull(mNfcA.getAtqa()); + } + + @Test + public void testGetSak() { + assertEquals((short) 1, mNfcA.getSak()); + } + + @Test + public void testTransceive() throws IOException, RemoteException { + TransceiveResult mockTransceiveResult = mock(TransceiveResult.class); + when(mMockTag.getConnectedTechnology()).thenReturn(TagTechnology.NFC_A); + when(mMockTag.getTagService()).thenReturn(mMockTagService); + when(mMockTag.getServiceHandle()).thenReturn(1); + when(mMockTagService.transceive(1, mSampleArray, true)) + .thenReturn(mockTransceiveResult); + when(mockTransceiveResult.getResponseOrThrow()).thenReturn(mSampleArray); + + mNfcA.transceive(mSampleArray); + verify(mMockTag).getTagService(); + verify(mMockTag).getServiceHandle(); + } + + @Test + public void testGetMaxTransceiveLength() throws RemoteException { + when(mMockTag.getTagService()).thenReturn(mMockTagService); + when(mMockTagService.getMaxTransceiveLength(TagTechnology.NFC_A)).thenReturn(1); + + mNfcA.getMaxTransceiveLength(); + verify(mMockTag).getTagService(); + } + + @Test + public void testSetTimeout() { + when(mMockTag.getTagService()).thenReturn(mMockTagService); + try { + when(mMockTagService.setTimeout(TagTechnology.NFC_A, 1000)).thenReturn( + ErrorCodes.SUCCESS); + + mNfcA.setTimeout(1000); + verify(mMockTag).getTagService(); + verify(mMockTagService).setTimeout(TagTechnology.NFC_A, 1000); + } catch (Exception e) { + fail("Unexpected exception during valid setTimeout: " + e.getMessage()); + } + } + + @Test + public void testSetTimeoutInvalidTimeout() { + when(mMockTag.getTagService()).thenReturn(mMockTagService); + try { + when(mMockTagService.setTimeout(TagTechnology.NFC_A, -1)).thenReturn( + ErrorCodes.ERROR_TIMEOUT); + + assertThrows(IllegalArgumentException.class, () -> mNfcA.setTimeout(-1)); + } catch (Exception e) { + fail("Unexpected exception during invalid setTimeout: " + e.getMessage()); + } + } + + @Test + public void testSetTimeoutRemoteException() { + when(mMockTag.getTagService()).thenReturn(mMockTagService); + try { + when(mMockTagService.setTimeout(TagTechnology.NFC_A, 1000)).thenThrow( + new RemoteException()); + + mNfcA.setTimeout(1000); // Should not throw an exception but log it + verify(mMockTag).getTagService(); + verify(mMockTagService).setTimeout(TagTechnology.NFC_A, 1000); + } catch (Exception e) { + fail("Unexpected exception during RemoteException in setTimeout: " + e.getMessage()); + } + + } + + @Test + public void testGetTimeout() { + when(mMockTag.getTagService()).thenReturn(mMockTagService); + try { + when(mMockTagService.getTimeout(TagTechnology.NFC_A)).thenReturn(2000); + + assertEquals(2000, mNfcA.getTimeout()); + verify(mMockTag).getTagService(); + verify(mMockTagService).getTimeout(TagTechnology.NFC_A); + } catch (Exception e) { + fail("Unexpected exception during valid getTimeout: " + e.getMessage()); + } + } + + @Test + public void testGetTimeoutRemoteException() { + when(mMockTag.getTagService()).thenReturn(mMockTagService); + try { + when(mMockTagService.getTimeout(TagTechnology.NFC_A)).thenThrow(new RemoteException()); + + assertEquals(0, mNfcA.getTimeout()); + } catch (Exception e) { + fail("Unexpected exception during RemoteException in getTimeout: " + e.getMessage()); + } + } +} diff --git a/nfc/tests/src/android/nfc/tech/NfcBTest.java b/nfc/tests/src/android/nfc/tech/NfcBTest.java new file mode 100644 index 000000000000..98d6070e760c --- /dev/null +++ b/nfc/tests/src/android/nfc/tech/NfcBTest.java @@ -0,0 +1,119 @@ +/* + * 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.nfc.tech; + +import static android.nfc.tech.NfcB.EXTRA_APPDATA; +import static android.nfc.tech.NfcB.EXTRA_PROTINFO; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.nfc.INfcTag; +import android.nfc.Tag; +import android.nfc.TransceiveResult; +import android.os.Bundle; +import android.os.RemoteException; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.IOException; + +public class NfcBTest { + private final byte[] mSampleAppDate = new byte[] {1, 2, 3}; + private final byte[] mSampleProtInfo = new byte[] {3, 2, 1}; + @Mock + private Tag mMockTag; + @Mock + private Bundle mMockBundle; + @Mock + private INfcTag mMockTagService; + private NfcB mNfcB; + + @Before + public void setUp() throws RemoteException { + MockitoAnnotations.initMocks(this); + when(mMockBundle.getByteArray(EXTRA_APPDATA)).thenReturn(mSampleAppDate); + when(mMockBundle.getByteArray(EXTRA_PROTINFO)).thenReturn(mSampleProtInfo); + when(mMockTag.getTechExtras(TagTechnology.NFC_B)).thenReturn(mMockBundle); + + mNfcB = new NfcB(mMockTag); + } + + @Test + public void testGetApplicationData() { + assertNotNull(mNfcB.getApplicationData()); + } + + @Test + public void testGetProtocolInfo() { + assertNotNull(mNfcB.getProtocolInfo()); + } + + @Test + public void testGetNfcBInstance() { + Tag tag = mock(Tag.class); + when(tag.hasTech(TagTechnology.NFC_B)).thenReturn(true); + when(tag.getTechExtras(TagTechnology.NFC_B)).thenReturn(mMockBundle); + + assertNotNull(NfcB.get(tag)); + verify(tag).hasTech(TagTechnology.NFC_B); + verify(tag).getTechExtras(TagTechnology.NFC_B); + } + + @Test + public void testGetNfcBNullInstance() { + Tag tag = mock(Tag.class); + when(tag.hasTech(TagTechnology.NFC_B)).thenReturn(false); + + assertNull(NfcB.get(tag)); + verify(tag).hasTech(TagTechnology.NFC_B); + verify(tag, never()).getTechExtras(TagTechnology.NFC_B); + } + + + @Test + public void testTransceive() throws IOException, RemoteException { + byte[] sampleData = new byte[] {1, 2, 3, 4, 5}; + TransceiveResult mockTransceiveResult = mock(TransceiveResult.class); + when(mMockTag.getConnectedTechnology()).thenReturn(TagTechnology.NFC_B); + when(mMockTag.getTagService()).thenReturn(mMockTagService); + when(mMockTag.getServiceHandle()).thenReturn(1); + when(mMockTagService.transceive(1, sampleData, true)) + .thenReturn(mockTransceiveResult); + when(mockTransceiveResult.getResponseOrThrow()).thenReturn(sampleData); + + mNfcB.transceive(sampleData); + verify(mMockTag).getTagService(); + verify(mMockTag).getServiceHandle(); + } + + @Test + public void testGetMaxTransceiveLength() throws RemoteException { + when(mMockTag.getTagService()).thenReturn(mMockTagService); + when(mMockTagService.getMaxTransceiveLength(TagTechnology.NFC_B)).thenReturn(1); + + mNfcB.getMaxTransceiveLength(); + verify(mMockTag).getTagService(); + } +} diff --git a/nfc/tests/src/android/nfc/tech/NfcFTest.java b/nfc/tests/src/android/nfc/tech/NfcFTest.java new file mode 100644 index 000000000000..31a6943566e0 --- /dev/null +++ b/nfc/tests/src/android/nfc/tech/NfcFTest.java @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2010 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.nfc.tech; + +import static android.nfc.tech.NfcF.EXTRA_PMM; +import static android.nfc.tech.NfcF.EXTRA_SC; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.nfc.ErrorCodes; +import android.nfc.INfcTag; +import android.nfc.Tag; +import android.nfc.TransceiveResult; +import android.os.Bundle; +import android.os.RemoteException; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.IOException; + +public class NfcFTest { + private final byte[] mSampleSystemCode = new byte[] {1, 2, 3}; + private final byte[] mSampleManufacturer = new byte[] {3, 2, 1}; + @Mock + private Tag mMockTag; + @Mock + private INfcTag mMockTagService; + @Mock + private Bundle mMockBundle; + private NfcF mNfcF; + + @Before + public void setUp() throws RemoteException { + MockitoAnnotations.initMocks(this); + when(mMockBundle.getByteArray(EXTRA_SC)).thenReturn(mSampleSystemCode); + when(mMockBundle.getByteArray(EXTRA_PMM)).thenReturn(mSampleManufacturer); + when(mMockTag.getTechExtras(TagTechnology.NFC_F)).thenReturn(mMockBundle); + + mNfcF = new NfcF(mMockTag); + } + + @Test + public void testGetSystemCode() { + assertNotNull(mNfcF.getSystemCode()); + } + + @Test + public void testGetManufacturer() { + assertNotNull(mNfcF.getManufacturer()); + } + + @Test + public void testGetNfcFInstanceWithTech() { + Tag tag = mock(Tag.class); + when(tag.getTechExtras(TagTechnology.NFC_F)).thenReturn(mMockBundle); + when(tag.hasTech(TagTechnology.NFC_F)).thenReturn(true); + + assertNotNull(NfcF.get(tag)); + verify(tag).getTechExtras(TagTechnology.NFC_F); + verify(tag).hasTech(TagTechnology.NFC_F); + } + + @Test + public void testGetNfcFInstanceWithoutTech() { + Tag tag = mock(Tag.class); + when(tag.hasTech(TagTechnology.NFC_F)).thenReturn(false); + + assertNull(NfcF.get(tag)); + verify(tag).hasTech(TagTechnology.NFC_F); + verify(tag, never()).getTechExtras(TagTechnology.NFC_F); + } + + @Test + public void testTransceive() throws IOException, RemoteException { + byte[] sampleData = new byte[]{1, 2, 3, 4, 5}; + TransceiveResult mockTransceiveResult = mock(TransceiveResult.class); + when(mMockTag.getConnectedTechnology()).thenReturn(TagTechnology.NFC_F); + when(mMockTag.getTagService()).thenReturn(mMockTagService); + when(mMockTag.getServiceHandle()).thenReturn(1); + when(mMockTagService.transceive(1, sampleData, true)) + .thenReturn(mockTransceiveResult); + when(mockTransceiveResult.getResponseOrThrow()).thenReturn(sampleData); + + mNfcF.transceive(sampleData); + verify(mMockTag).getTagService(); + verify(mMockTag).getServiceHandle(); + } + + @Test + public void testGetMaxTransceiveLength() throws RemoteException { + when(mMockTag.getTagService()).thenReturn(mMockTagService); + when(mMockTagService.getMaxTransceiveLength(TagTechnology.NFC_F)).thenReturn(1); + + mNfcF.getMaxTransceiveLength(); + verify(mMockTag).getTagService(); + } + + @Test + public void testGetTimeout() { + when(mMockTag.getTagService()).thenReturn(mMockTagService); + try { + when(mMockTagService.getTimeout(TagTechnology.NFC_F)).thenReturn(2000); + + assertEquals(2000, mNfcF.getTimeout()); + verify(mMockTag).getTagService(); + verify(mMockTagService).getTimeout(TagTechnology.NFC_F); + } catch (Exception e) { + fail("Unexpected exception during valid getTimeout: " + e.getMessage()); + } + } + + @Test + public void testGetTimeoutRemoteException() { + when(mMockTag.getTagService()).thenReturn(mMockTagService); + try { + when(mMockTagService.getTimeout(TagTechnology.NFC_F)).thenThrow(new RemoteException()); + + assertEquals(0, mNfcF.getTimeout()); + } catch (Exception e) { + fail("Unexpected exception during RemoteException in getTimeout: " + e.getMessage()); + } + } + + @Test + public void testSetTimeout() { + when(mMockTag.getTagService()).thenReturn(mMockTagService); + try { + when(mMockTagService.setTimeout(TagTechnology.NFC_F, 1000)).thenReturn( + ErrorCodes.SUCCESS); + + mNfcF.setTimeout(1000); + verify(mMockTag).getTagService(); + verify(mMockTagService).setTimeout(TagTechnology.NFC_F, 1000); + } catch (Exception e) { + fail("Unexpected exception during valid setTimeout: " + e.getMessage()); + } + } + + @Test + public void testSetTimeoutInvalidTimeout() { + when(mMockTag.getTagService()).thenReturn(mMockTagService); + try { + when(mMockTagService.setTimeout(TagTechnology.NFC_F, -1)).thenReturn( + ErrorCodes.ERROR_TIMEOUT); + + assertThrows(IllegalArgumentException.class, () -> mNfcF.setTimeout(-1)); + } catch (Exception e) { + fail("Unexpected exception during invalid setTimeout: " + e.getMessage()); + } + } + + @Test + public void testSetTimeoutRemoteException() { + when(mMockTag.getTagService()).thenReturn(mMockTagService); + try { + when(mMockTagService.setTimeout(TagTechnology.NFC_F, 1000)).thenThrow( + new RemoteException()); + + mNfcF.setTimeout(1000); + verify(mMockTag).getTagService(); + verify(mMockTagService).setTimeout(TagTechnology.NFC_F, 1000); + } catch (Exception e) { + fail("Unexpected exception during RemoteException in setTimeout: " + e.getMessage()); + } + } +} diff --git a/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java index 31e1eb36ad8d..70f5bb32a5d5 100644 --- a/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java +++ b/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java @@ -2074,15 +2074,19 @@ public class PackageWatchdog { bootMitigationCounts.put(observer.name, observer.getBootMitigationCount()); } + FileOutputStream fileStream = null; + ObjectOutputStream objectStream = null; try { - FileOutputStream fileStream = new FileOutputStream(new File(filePath)); - ObjectOutputStream objectStream = new ObjectOutputStream(fileStream); + fileStream = new FileOutputStream(new File(filePath)); + objectStream = new ObjectOutputStream(fileStream); objectStream.writeObject(bootMitigationCounts); objectStream.flush(); - objectStream.close(); - fileStream.close(); } catch (Exception e) { Slog.i(TAG, "Could not save observers metadata to file: " + e); + return; + } finally { + IoUtils.closeQuietly(objectStream); + IoUtils.closeQuietly(fileStream); } } @@ -2233,23 +2237,32 @@ public class PackageWatchdog { void readAllObserversBootMitigationCountIfNecessary(String filePath) { File metadataFile = new File(filePath); if (metadataFile.exists()) { + FileInputStream fileStream = null; + ObjectInputStream objectStream = null; + HashMap<String, Integer> bootMitigationCounts = null; try { - FileInputStream fileStream = new FileInputStream(metadataFile); - ObjectInputStream objectStream = new ObjectInputStream(fileStream); - HashMap<String, Integer> bootMitigationCounts = + fileStream = new FileInputStream(metadataFile); + objectStream = new ObjectInputStream(fileStream); + bootMitigationCounts = (HashMap<String, Integer>) objectStream.readObject(); - objectStream.close(); - fileStream.close(); - - for (int i = 0; i < mAllObservers.size(); i++) { - final ObserverInternal observer = mAllObservers.valueAt(i); - if (bootMitigationCounts.containsKey(observer.name)) { - observer.setBootMitigationCount( - bootMitigationCounts.get(observer.name)); - } - } } catch (Exception e) { Slog.i(TAG, "Could not read observer metadata file: " + e); + return; + } finally { + IoUtils.closeQuietly(objectStream); + IoUtils.closeQuietly(fileStream); + } + + if (bootMitigationCounts == null || bootMitigationCounts.isEmpty()) { + Slog.i(TAG, "No observer in metadata file"); + return; + } + for (int i = 0; i < mAllObservers.size(); i++) { + final ObserverInternal observer = mAllObservers.valueAt(i); + if (bootMitigationCounts.containsKey(observer.name)) { + observer.setBootMitigationCount( + bootMitigationCounts.get(observer.name)); + } } } } diff --git a/packages/CrashRecovery/services/platform/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/platform/java/com/android/server/PackageWatchdog.java index ffae5176cebf..ac815f8aca85 100644 --- a/packages/CrashRecovery/services/platform/java/com/android/server/PackageWatchdog.java +++ b/packages/CrashRecovery/services/platform/java/com/android/server/PackageWatchdog.java @@ -2021,15 +2021,19 @@ public class PackageWatchdog { bootMitigationCounts.put(observer.name, observer.getBootMitigationCount()); } + FileOutputStream fileStream = null; + ObjectOutputStream objectStream = null; try { - FileOutputStream fileStream = new FileOutputStream(new File(filePath)); - ObjectOutputStream objectStream = new ObjectOutputStream(fileStream); + fileStream = new FileOutputStream(new File(filePath)); + objectStream = new ObjectOutputStream(fileStream); objectStream.writeObject(bootMitigationCounts); objectStream.flush(); - objectStream.close(); - fileStream.close(); } catch (Exception e) { Slog.i(TAG, "Could not save observers metadata to file: " + e); + return; + } finally { + IoUtils.closeQuietly(objectStream); + IoUtils.closeQuietly(fileStream); } } @@ -2180,23 +2184,32 @@ public class PackageWatchdog { void readAllObserversBootMitigationCountIfNecessary(String filePath) { File metadataFile = new File(filePath); if (metadataFile.exists()) { + FileInputStream fileStream = null; + ObjectInputStream objectStream = null; + HashMap<String, Integer> bootMitigationCounts = null; try { - FileInputStream fileStream = new FileInputStream(metadataFile); - ObjectInputStream objectStream = new ObjectInputStream(fileStream); - HashMap<String, Integer> bootMitigationCounts = + fileStream = new FileInputStream(metadataFile); + objectStream = new ObjectInputStream(fileStream); + bootMitigationCounts = (HashMap<String, Integer>) objectStream.readObject(); - objectStream.close(); - fileStream.close(); - - for (int i = 0; i < mAllObservers.size(); i++) { - final ObserverInternal observer = mAllObservers.valueAt(i); - if (bootMitigationCounts.containsKey(observer.name)) { - observer.setBootMitigationCount( - bootMitigationCounts.get(observer.name)); - } - } } catch (Exception e) { Slog.i(TAG, "Could not read observer metadata file: " + e); + return; + } finally { + IoUtils.closeQuietly(objectStream); + IoUtils.closeQuietly(fileStream); + } + + if (bootMitigationCounts == null || bootMitigationCounts.isEmpty()) { + Slog.i(TAG, "No observer in metadata file"); + return; + } + for (int i = 0; i < mAllObservers.size(); i++) { + final ObserverInternal observer = mAllObservers.valueAt(i); + if (bootMitigationCounts.containsKey(observer.name)) { + observer.setBootMitigationCount( + bootMitigationCounts.get(observer.name)); + } } } } diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml index e029f3a16066..4da73593bdea 100644 --- a/packages/PackageInstaller/AndroidManifest.xml +++ b/packages/PackageInstaller/AndroidManifest.xml @@ -24,6 +24,7 @@ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" /> <uses-permission android:name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER" /> + <uses-permission android:name="android.permission.RESOLVE_COMPONENT_FOR_UID" /> <uses-permission android:name="com.google.android.permission.INSTALL_WEARABLE_PACKAGES" /> diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java index 635ae20cfa38..6c06fab16502 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java @@ -26,6 +26,7 @@ import android.app.admin.DevicePolicyManager; import android.content.ContentResolver; import android.content.Intent; import android.content.pm.ApplicationInfo; +import android.content.pm.Flags; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionInfo; @@ -274,8 +275,20 @@ public class InstallStart extends Activity { } private boolean canPackageQuery(int callingUid, Uri packageUri) { - ProviderInfo info = mPackageManager.resolveContentProvider(packageUri.getAuthority(), - PackageManager.ComponentInfoFlags.of(0)); + ProviderInfo info; + try { + if (Flags.uidBasedProviderLookup()) { + info = mPackageManager.resolveContentProviderForUid(packageUri.getAuthority(), + PackageManager.ComponentInfoFlags.of(0), callingUid); + } else { + info = mPackageManager.resolveContentProvider(packageUri.getAuthority(), + PackageManager.ComponentInfoFlags.of(0)); + } + } catch (Exception e) { + Log.e(TAG, "Caller cannot access " + packageUri, e); + return false; + } + if (info == null) { return false; } diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsGlobalStore.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsGlobalStore.kt index 53507fe46d1f..6360af573d5c 100644 --- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsGlobalStore.kt +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsGlobalStore.kt @@ -82,5 +82,11 @@ class SettingsGlobalStore private constructor(contentResolver: ContentResolver) instance = it } } + + /** Returns the required permissions to read [Global] settings. */ + fun getReadPermissions() = arrayOf<String>() + + /** Returns the required permissions to write [Global] settings. */ + fun getWritePermissions() = arrayOf<String>() } } diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSecureStore.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSecureStore.kt index ca7fd7bb5f1e..c117b926d1eb 100644 --- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSecureStore.kt +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSecureStore.kt @@ -16,6 +16,7 @@ package com.android.settingslib.datastore +import android.Manifest import android.content.ContentResolver import android.content.Context import android.net.Uri @@ -82,5 +83,11 @@ class SettingsSecureStore private constructor(contentResolver: ContentResolver) instance = it } } + + /** Returns the required permissions to read [Secure] settings. */ + fun getReadPermissions() = arrayOf<String>() + + /** Returns the required permissions to write [Secure] settings. */ + fun getWritePermissions() = arrayOf(Manifest.permission.WRITE_SECURE_SETTINGS) } } diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSystemStore.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSystemStore.kt index 20a74d3b4a81..f5a2f940bc03 100644 --- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSystemStore.kt +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSystemStore.kt @@ -16,6 +16,7 @@ package com.android.settingslib.datastore +import android.Manifest import android.content.ContentResolver import android.content.Context import android.net.Uri @@ -82,5 +83,11 @@ class SettingsSystemStore private constructor(contentResolver: ContentResolver) instance = it } } + + /** Returns the required permissions to read [System] settings. */ + fun getReadPermissions() = arrayOf<String>() + + /** Returns the required permissions to write [System] settings. */ + fun getWritePermissions() = arrayOf(Manifest.permission.WRITE_SETTINGS) } } diff --git a/packages/SettingsLib/Graph/graph.proto b/packages/SettingsLib/Graph/graph.proto index f611793b619b..2aa619aa67f9 100644 --- a/packages/SettingsLib/Graph/graph.proto +++ b/packages/SettingsLib/Graph/graph.proto @@ -81,6 +81,10 @@ message PreferenceProto { optional PreferenceValueDescriptorProto value_descriptor = 15; // Indicate how sensitive of the preference. optional int32 sensitivity_level = 16; + // The required permissions to read preference value. + repeated string read_permissions = 17; + // The required permissions to write preference value. + repeated string write_permissions = 18; // Target of an Intent message ActionTarget { @@ -108,6 +112,7 @@ message PreferenceValueProto { oneof value { bool boolean_value = 1; int32 int_value = 2; + float float_value = 3; } } @@ -116,6 +121,7 @@ message PreferenceValueDescriptorProto { oneof type { bool boolean_type = 1; RangeValueProto range_value = 2; + bool float_type = 3; } } diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt index eaa79266b194..91dec03bf2af 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt @@ -42,6 +42,7 @@ import com.android.settingslib.graph.proto.PreferenceProto.ActionTarget import com.android.settingslib.graph.proto.PreferenceScreenProto import com.android.settingslib.graph.proto.TextProto import com.android.settingslib.metadata.BooleanValue +import com.android.settingslib.metadata.FloatPersistentPreference import com.android.settingslib.metadata.PersistentPreference import com.android.settingslib.metadata.PreferenceAvailabilityProvider import com.android.settingslib.metadata.PreferenceHierarchy @@ -390,7 +391,13 @@ fun PreferenceMetadata.toProto( } persistent = metadata.isPersistent(context) if (persistent) { - if (metadata is PersistentPreference<*>) sensitivityLevel = metadata.sensitivityLevel + if (metadata is PersistentPreference<*>) { + sensitivityLevel = metadata.sensitivityLevel + val readPermissions = metadata.getReadPermissions(context) + readPermissions.forEach { addReadPermissions(it) } + val writePermissions = metadata.getWritePermissions(context) + writePermissions.forEach { addWritePermissions(it) } + } if ( flags.includeValue() && enabled && @@ -399,15 +406,13 @@ fun PreferenceMetadata.toProto( metadata is PersistentPreference<*> && metadata.evalReadPermit(context, callingPid, callingUid) == ReadWritePermit.ALLOW ) { + val storage = metadata.storage(context) value = preferenceValueProto { when (metadata) { - is BooleanValue -> - metadata.storage(context).getBoolean(metadata.key)?.let { - booleanValue = it - } - is RangeValue -> { - metadata.storage(context).getInt(metadata.key)?.let { intValue = it } - } + is BooleanValue -> storage.getBoolean(metadata.key)?.let { booleanValue = it } + is RangeValue -> storage.getInt(metadata.key)?.let { intValue = it } + is FloatPersistentPreference -> + storage.getFloat(metadata.key)?.let { floatValue = it } else -> {} } } @@ -421,6 +426,7 @@ fun PreferenceMetadata.toProto( max = metadata.getMaxValue(context) step = metadata.getIncrementStep(context) } + is FloatPersistentPreference -> floatType = true else -> {} } } diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt index d72ba0805db3..83c430488317 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceSetterApi.kt @@ -159,6 +159,12 @@ class PreferenceSetterApiHandler( } storage.setInt(key, intValue) return PreferenceSetterResult.OK + } else if (value.hasFloatValue()) { + val floatValue = value.floatValue + val resultCode = metadata.checkWritePermit(floatValue) + if (resultCode != PreferenceSetterResult.OK) return resultCode + storage.setFloat(key, floatValue) + return PreferenceSetterResult.OK } } catch (e: Exception) { return PreferenceSetterResult.INTERNAL_ERROR diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt index dee32d9ed80e..adbe77318353 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt @@ -35,8 +35,9 @@ val PreferenceScreenProto.rootOrNull /** Kotlin DSL-style builder for [PreferenceScreenProto]. */ @JvmSynthetic -inline fun preferenceScreenProto(init: PreferenceScreenProto.Builder.() -> Unit) = - PreferenceScreenProto.newBuilder().also(init).build() +inline fun preferenceScreenProto( + init: PreferenceScreenProto.Builder.() -> Unit +): PreferenceScreenProto = PreferenceScreenProto.newBuilder().also(init).build() /** Returns preference or null. */ val PreferenceOrGroupProto.preferenceOrNull @@ -48,8 +49,9 @@ val PreferenceOrGroupProto.groupOrNull /** Kotlin DSL-style builder for [PreferenceOrGroupProto]. */ @JvmSynthetic -inline fun preferenceOrGroupProto(init: PreferenceOrGroupProto.Builder.() -> Unit) = - PreferenceOrGroupProto.newBuilder().also(init).build() +inline fun preferenceOrGroupProto( + init: PreferenceOrGroupProto.Builder.() -> Unit +): PreferenceOrGroupProto = PreferenceOrGroupProto.newBuilder().also(init).build() /** Returns preference or null. */ val PreferenceGroupProto.preferenceOrNull @@ -57,8 +59,9 @@ val PreferenceGroupProto.preferenceOrNull /** Kotlin DSL-style builder for [PreferenceGroupProto]. */ @JvmSynthetic -inline fun preferenceGroupProto(init: PreferenceGroupProto.Builder.() -> Unit) = - PreferenceGroupProto.newBuilder().also(init).build() +inline fun preferenceGroupProto( + init: PreferenceGroupProto.Builder.() -> Unit +): PreferenceGroupProto = PreferenceGroupProto.newBuilder().also(init).build() /** Returns title or null. */ val PreferenceProto.titleOrNull @@ -74,7 +77,7 @@ val PreferenceProto.actionTargetOrNull /** Kotlin DSL-style builder for [PreferenceProto]. */ @JvmSynthetic -inline fun preferenceProto(init: PreferenceProto.Builder.() -> Unit) = +inline fun preferenceProto(init: PreferenceProto.Builder.() -> Unit): PreferenceProto = PreferenceProto.newBuilder().also(init).build() /** Returns intent or null. */ @@ -83,39 +86,42 @@ val ActionTarget.intentOrNull /** Kotlin DSL-style builder for [ActionTarget]. */ @JvmSynthetic -inline fun actionTargetProto(init: ActionTarget.Builder.() -> Unit) = +inline fun actionTargetProto(init: ActionTarget.Builder.() -> Unit): ActionTarget = ActionTarget.newBuilder().also(init).build() /** Kotlin DSL-style builder for [PreferenceValueProto]. */ @JvmSynthetic -inline fun preferenceValueProto(init: PreferenceValueProto.Builder.() -> Unit) = - PreferenceValueProto.newBuilder().also(init).build() +inline fun preferenceValueProto( + init: PreferenceValueProto.Builder.() -> Unit +): PreferenceValueProto = PreferenceValueProto.newBuilder().also(init).build() /** Kotlin DSL-style builder for [PreferenceValueDescriptorProto]. */ @JvmSynthetic -inline fun preferenceValueDescriptorProto(init: PreferenceValueDescriptorProto.Builder.() -> Unit) = - PreferenceValueDescriptorProto.newBuilder().also(init).build() +inline fun preferenceValueDescriptorProto( + init: PreferenceValueDescriptorProto.Builder.() -> Unit +): PreferenceValueDescriptorProto = PreferenceValueDescriptorProto.newBuilder().also(init).build() /** Kotlin DSL-style builder for [RangeValueProto]. */ @JvmSynthetic -inline fun rangeValueProto(init: RangeValueProto.Builder.() -> Unit) = +inline fun rangeValueProto(init: RangeValueProto.Builder.() -> Unit): RangeValueProto = RangeValueProto.newBuilder().also(init).build() /** Kotlin DSL-style builder for [TextProto]. */ @JvmSynthetic -inline fun textProto(init: TextProto.Builder.() -> Unit) = TextProto.newBuilder().also(init).build() +inline fun textProto(init: TextProto.Builder.() -> Unit): TextProto = + TextProto.newBuilder().also(init).build() /** Kotlin DSL-style builder for [IntentProto]. */ @JvmSynthetic -inline fun intentProto(init: IntentProto.Builder.() -> Unit) = +inline fun intentProto(init: IntentProto.Builder.() -> Unit): IntentProto = IntentProto.newBuilder().also(init).build() /** Kotlin DSL-style builder for [BundleProto]. */ @JvmSynthetic -inline fun bundleProto(init: BundleProto.Builder.() -> Unit) = +inline fun bundleProto(init: BundleProto.Builder.() -> Unit): BundleProto = BundleProto.newBuilder().also(init).build() /** Kotlin DSL-style builder for [BundleValue]. */ @JvmSynthetic -inline fun bundleValueProto(init: BundleValue.Builder.() -> Unit) = +inline fun bundleValueProto(init: BundleValue.Builder.() -> Unit): BundleValue = BundleValue.newBuilder().also(init).build() diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java index adc4f316aca9..bc4f1f942dc0 100644 --- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java +++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/IllustrationPreference.java @@ -167,6 +167,7 @@ public class IllustrationPreference extends Preference implements GroupSectionDi if (mLottieDynamicColor) { LottieColorUtils.applyDynamicColors(getContext(), illustrationView); } + LottieColorUtils.applyMaterialColor(getContext(), illustrationView); if (mOnBindListener != null) { mOnBindListener.onBind(illustrationView); diff --git a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java index 98a72909b276..4421424c0e39 100644 --- a/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java +++ b/packages/SettingsLib/IllustrationPreference/src/com/android/settingslib/widget/LottieColorUtils.java @@ -21,14 +21,14 @@ import android.content.res.Configuration; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; -import com.android.settingslib.color.R; +import androidx.annotation.NonNull; + +import com.android.settingslib.widget.theme.R; import com.airbnb.lottie.LottieAnimationView; import com.airbnb.lottie.LottieProperty; import com.airbnb.lottie.model.KeyPath; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; /** @@ -37,52 +37,97 @@ import java.util.Map; */ public class LottieColorUtils { private static final Map<String, Integer> DARK_TO_LIGHT_THEME_COLOR_MAP; + private static final Map<String, Integer> MATERIAL_COLOR_MAP; static { - HashMap<String, Integer> map = new HashMap<>(); - map.put( - ".grey200", - R.color.settingslib_color_grey800); - map.put( - ".grey600", - R.color.settingslib_color_grey400); - map.put( - ".grey800", - R.color.settingslib_color_grey300); - map.put( - ".grey900", - R.color.settingslib_color_grey50); - map.put( - ".red100", - R.color.settingslib_color_red500); - map.put( - ".red200", - R.color.settingslib_color_red500); - map.put( - ".red400", - R.color.settingslib_color_red600); - map.put( - ".black", - android.R.color.white); - map.put( - ".blue200", - R.color.settingslib_color_blue700); - map.put( - ".blue400", - R.color.settingslib_color_blue600); - map.put( - ".green100", - R.color.settingslib_color_green500); - map.put( - ".green200", - R.color.settingslib_color_green500); - map.put( - ".green400", - R.color.settingslib_color_green600); - map.put( - ".cream", - R.color.settingslib_color_charcoal); - DARK_TO_LIGHT_THEME_COLOR_MAP = Collections.unmodifiableMap(map); + DARK_TO_LIGHT_THEME_COLOR_MAP = Map.ofEntries( + Map.entry(".grey200", + com.android.settingslib.color.R.color.settingslib_color_grey800), + Map.entry(".grey600", + com.android.settingslib.color.R.color.settingslib_color_grey400), + Map.entry(".grey800", + com.android.settingslib.color.R.color.settingslib_color_grey300), + Map.entry(".grey900", + com.android.settingslib.color.R.color.settingslib_color_grey50), + Map.entry(".red100", + com.android.settingslib.color.R.color.settingslib_color_red500), + Map.entry(".red200", + com.android.settingslib.color.R.color.settingslib_color_red500), + Map.entry(".red400", + com.android.settingslib.color.R.color.settingslib_color_red600), + Map.entry(".black", + android.R.color.white), + Map.entry(".blue200", + com.android.settingslib.color.R.color.settingslib_color_blue700), + Map.entry(".blue400", + com.android.settingslib.color.R.color.settingslib_color_blue600), + Map.entry(".green100", + com.android.settingslib.color.R.color.settingslib_color_green500), + Map.entry(".green200", + com.android.settingslib.color.R.color.settingslib_color_green500), + Map.entry(".green400", + com.android.settingslib.color.R.color.settingslib_color_green600), + Map.entry(".cream", + com.android.settingslib.color.R.color.settingslib_color_charcoal)); + + MATERIAL_COLOR_MAP = Map.ofEntries( + Map.entry(".primary", R.color.settingslib_materialColorPrimary), + Map.entry(".onPrimary", R.color.settingslib_materialColorOnPrimary), + Map.entry(".primaryContainer", R.color.settingslib_materialColorPrimaryContainer), + Map.entry(".onPrimaryContainer", + R.color.settingslib_materialColorOnPrimaryContainer), + Map.entry(".primaryInverse", R.color.settingslib_materialColorPrimaryInverse), + Map.entry(".primaryFixed", R.color.settingslib_materialColorPrimaryFixed), + Map.entry(".primaryFixedDim", R.color.settingslib_materialColorPrimaryFixedDim), + Map.entry(".onPrimaryFixed", R.color.settingslib_materialColorOnPrimaryFixed), + Map.entry(".onPrimaryFixedVariant", + R.color.settingslib_materialColorOnPrimaryFixedVariant), + Map.entry(".secondary", R.color.settingslib_materialColorSecondary), + Map.entry(".onSecondary", R.color.settingslib_materialColorOnSecondary), + Map.entry(".secondaryContainer", + R.color.settingslib_materialColorSecondaryContainer), + Map.entry(".onSecondaryContainer", + R.color.settingslib_materialColorOnSecondaryContainer), + Map.entry(".secondaryFixed", R.color.settingslib_materialColorSecondaryFixed), + Map.entry(".secondaryFixedDim", R.color.settingslib_materialColorSecondaryFixedDim), + Map.entry(".onSecondaryFixed", R.color.settingslib_materialColorOnSecondaryFixed), + Map.entry(".onSecondaryFixedVariant", + R.color.settingslib_materialColorOnSecondaryFixedVariant), + Map.entry(".tertiary", R.color.settingslib_materialColorTertiary), + Map.entry(".onTertiary", R.color.settingslib_materialColorOnTertiary), + Map.entry(".tertiaryContainer", R.color.settingslib_materialColorTertiaryContainer), + Map.entry(".onTertiaryContainer", + R.color.settingslib_materialColorOnTertiaryContainer), + Map.entry(".tertiaryFixed", R.color.settingslib_materialColorTertiaryFixed), + Map.entry(".tertiaryFixedDim", R.color.settingslib_materialColorTertiaryFixedDim), + Map.entry(".onTertiaryFixed", R.color.settingslib_materialColorOnTertiaryFixed), + Map.entry(".onTertiaryFixedVariant", + R.color.settingslib_materialColorOnTertiaryFixedVariant), + Map.entry(".error", R.color.settingslib_materialColorError), + Map.entry(".onError", R.color.settingslib_materialColorOnError), + Map.entry(".errorContainer", R.color.settingslib_materialColorErrorContainer), + Map.entry(".onErrorContainer", R.color.settingslib_materialColorOnErrorContainer), + Map.entry(".outline", R.color.settingslib_materialColorOutline), + Map.entry(".outlineVariant", R.color.settingslib_materialColorOutlineVariant), + Map.entry(".background", R.color.settingslib_materialColorBackground), + Map.entry(".onBackground", R.color.settingslib_materialColorOnBackground), + Map.entry(".surface", R.color.settingslib_materialColorSurface), + Map.entry(".onSurface", R.color.settingslib_materialColorOnSurface), + Map.entry(".surfaceVariant", R.color.settingslib_materialColorSurfaceVariant), + Map.entry(".onSurfaceVariant", R.color.settingslib_materialColorOnSurfaceVariant), + Map.entry(".surfaceInverse", R.color.settingslib_materialColorSurfaceInverse), + Map.entry(".onSurfaceInverse", R.color.settingslib_materialColorOnSurfaceInverse), + Map.entry(".surfaceBright", R.color.settingslib_materialColorSurfaceBright), + Map.entry(".surfaceDim", R.color.settingslib_materialColorSurfaceDim), + Map.entry(".surfaceContainer", R.color.settingslib_materialColorSurfaceContainer), + Map.entry(".surfaceContainerLow", + R.color.settingslib_materialColorSurfaceContainerLow), + Map.entry(".surfaceContainerLowest", + R.color.settingslib_materialColorSurfaceContainerLowest), + Map.entry(".surfaceContainerHigh", + R.color.settingslib_materialColorSurfaceContainerHigh), + Map.entry(".surfaceContainerHighest", + R.color.settingslib_materialColorSurfaceContainerHighest)); } private LottieColorUtils() { @@ -108,4 +153,20 @@ public class LottieColorUtils { frameInfo -> new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)); } } + + /** Applies material colors. */ + public static void applyMaterialColor(@NonNull Context context, + @NonNull LottieAnimationView lottieAnimationView) { + if (!SettingsThemeHelper.isExpressiveTheme(context)) { + return; + } + + for (String key : MATERIAL_COLOR_MAP.keySet()) { + final int color = context.getColor(MATERIAL_COLOR_MAP.get(key)); + lottieAnimationView.addValueCallback( + new KeyPath("**", key, "**"), + LottieProperty.COLOR_FILTER, + frameInfo -> new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)); + } + } } diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt index d3a731688c24..3dd6c47833fd 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PersistentPreference.kt @@ -201,3 +201,6 @@ interface RangeValue : ValueDescriptor { override fun isValidValue(context: Context, index: Int) = index in getMinValue(context)..getMaxValue(context) } + +/** A persistent preference that has a float value. */ +interface FloatPersistentPreference : PersistentPreference<Float> 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 03a2101544be..218983a55e1b 100644 --- a/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java +++ b/packages/SettingsLib/SelectorWithWidgetPreference/src/com/android/settingslib/widget/SelectorWithWidgetPreference.java @@ -31,7 +31,6 @@ import androidx.preference.CheckBoxPreference; import androidx.preference.PreferenceViewHolder; import com.android.settingslib.widget.preference.selector.R; -import com.android.settingslib.widget.selectorwithwidgetpreference.flags.Flags; /** * Selector preference (checkbox or radio button) with an optional additional widget. @@ -180,10 +179,8 @@ public class SelectorWithWidgetPreference extends CheckBoxPreference { : getContext().getString(R.string.settings_label)); } - if (Flags.allowSetTitleMaxLines()) { - TextView title = (TextView) holder.findViewById(android.R.id.title); - title.setMaxLines(mTitleMaxLines); - } + TextView title = (TextView) holder.findViewById(android.R.id.title); + title.setMaxLines(mTitleMaxLines); } /** @@ -244,16 +241,12 @@ public class SelectorWithWidgetPreference extends CheckBoxPreference { setLayoutResource(R.layout.preference_selector_with_widget); setIconSpaceReserved(false); - if (Flags.allowSetTitleMaxLines()) { - final TypedArray a = - context.obtainStyledAttributes( - attrs, R.styleable.SelectorWithWidgetPreference, defStyleAttr, - defStyleRes); - mTitleMaxLines = - a.getInt(R.styleable.SelectorWithWidgetPreference_titleMaxLines, - DEFAULT_MAX_LINES); - a.recycle(); - } + final TypedArray a = + context.obtainStyledAttributes( + attrs, R.styleable.SelectorWithWidgetPreference, defStyleAttr, defStyleRes); + mTitleMaxLines = + a.getInt(R.styleable.SelectorWithWidgetPreference_titleMaxLines, DEFAULT_MAX_LINES); + a.recycle(); } @VisibleForTesting(otherwise = VisibleForTesting.NONE) diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig index 1a043d5015b2..cc996c5a2120 100644 --- a/packages/SettingsLib/aconfig/settingslib.aconfig +++ b/packages/SettingsLib/aconfig/settingslib.aconfig @@ -97,6 +97,7 @@ flag { name: "settings_catalyst" namespace: "android_settings" description: "Settings catalyst project migration" + is_exported: true bug: "323791114" is_exported: true } @@ -106,6 +107,7 @@ flag { is_fixed_read_only: true namespace: "android_settings" description: "Enable WRITE_SYSTEM_PREFERENCE permission and appop" + is_exported: true bug: "375193223" is_exported: true } @@ -197,3 +199,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "disable_audio_sharing_auto_pick_fallback_in_ui" + namespace: "cross_device_experiences" + description: "Do not auto pick audio sharing fallback device in UI" + bug: "383469911" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index e1929b725a58..6cf9e83ef342 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -229,6 +229,8 @@ <string name="bluetooth_hearing_aid_right_active">Active (right only)</string> <!-- Connected device settings. Message when the left-side and right-side hearing aids device are active. [CHAR LIMIT=NONE] --> <string name="bluetooth_hearing_aid_left_and_right_active">Active (left and right)</string> + <!-- Connected device settings.: Message when changing remote ambient state failed. [CHAR LIMIT=NONE] --> + <string name="bluetooth_hearing_device_ambient_error">Couldn\u2019t update surroundings</string> <!-- Connected devices settings. Message when Bluetooth is connected and active for media only, showing remote device status and battery level. [CHAR LIMIT=NONE] --> <string name="bluetooth_active_media_only_battery_level">Active (media only). <xliff:g id="battery_level_as_percentage">%1$s</xliff:g> battery.</string> diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUi.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUi.java new file mode 100644 index 000000000000..881a97bfadcd --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUi.java @@ -0,0 +1,170 @@ +/* + * 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.settingslib.bluetooth; + +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT; + +import android.bluetooth.BluetoothDevice; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.List; +import java.util.Map; + +/** Interface for the ambient volume UI. */ +public interface AmbientVolumeUi { + + /** Interface definition for a callback to be invoked when event happens in AmbientVolumeUi. */ + interface AmbientVolumeUiListener { + /** Called when the expand icon is clicked. */ + void onExpandIconClick(); + + /** Called when the ambient volume icon is clicked. */ + void onAmbientVolumeIconClick(); + + /** Called when the slider of the specified side is changed. */ + void onSliderValueChange(int side, int value); + }; + + /** The rotation degree of the expand icon when the UI is in collapsed mode. */ + float ROTATION_COLLAPSED = 0f; + /** The rotation degree of the expand icon when the UI is in expanded mode. */ + float ROTATION_EXPANDED = 180f; + + /** + * The default ambient volume level for hearing device ambient volume icon + * + * <p> This icon visually represents the current ambient volume. It displays separate + * levels for the left and right sides, each with 5 levels ranging from 0 to 4. + * + * <p> To represent the combined left/right levels with a single value, the following + * calculation is used: + * finalLevel = (leftLevel * 5) + rightLevel + * For example: + * <ul> + * <li>If left level is 2 and right level is 3, the final level will be 13 (2 * 5 + 3)</li> + * <li>If both left and right levels are 0, the final level will be 0</li> + * <li>If both left and right levels are 4, the final level will be 24</li> + * </ul> + */ + int AMBIENT_VOLUME_LEVEL_DEFAULT = 24; + /** + * The minimum ambient volume level for hearing device ambient volume icon + * + * @see #AMBIENT_VOLUME_LEVEL_DEFAULT + */ + int AMBIENT_VOLUME_LEVEL_MIN = 0; + /** + * The maximum ambient volume level for hearing device ambient volume icon + * + * @see #AMBIENT_VOLUME_LEVEL_DEFAULT + */ + int AMBIENT_VOLUME_LEVEL_MAX = 24; + + /** + * Ths side identifier for slider in collapsed mode which can unified control the ambient + * volume of all devices in the same set. + */ + int SIDE_UNIFIED = 999; + + /** All valid side of the sliders in the UI. */ + List<Integer> VALID_SIDES = List.of(SIDE_UNIFIED, SIDE_LEFT, SIDE_RIGHT); + + /** Sets if the UI is visible. */ + void setVisible(boolean visible); + + /** + * Sets if the UI is expandable between expanded and collapsed mode. + * + * <p> If the UI is not expandable, it implies the UI will always stay in collapsed mode + */ + void setExpandable(boolean expandable); + + /** @return if the UI is expandable. */ + boolean isExpandable(); + + /** Sets if the UI is in expanded mode. */ + void setExpanded(boolean expanded); + + /** @return if the UI is in expanded mode. */ + boolean isExpanded(); + + /** + * Sets if the UI is capable to mute the ambient of the remote device. + * + * <p> If the value is {@code false}, it implies the remote device ambient will always be + * unmute and can not be mute from the UI + */ + void setMutable(boolean mutable); + + /** @return if the UI is capable to mute the ambient of remote device. */ + boolean isMutable(); + + /** Sets if the UI shows mute state. */ + void setMuted(boolean muted); + + /** @return if the UI shows mute state */ + boolean isMuted(); + + /** + * Sets listener on the UI. + * + * @see AmbientVolumeUiListener + */ + void setListener(@Nullable AmbientVolumeUiListener listener); + + /** + * Sets up sliders in the UI. + * + * <p> For each side of device, the UI should hava a corresponding slider to control it's + * ambient volume. + * <p> For all devices in the same set, the UI should have a slider to control all devices' + * ambient volume at once. + * @param sideToDeviceMap the side and device mapping of all devices in the same set + */ + void setupSliders(@NonNull Map<Integer, BluetoothDevice> sideToDeviceMap); + + /** + * Sets if the slider is enabled. + * + * @param side the side of the slider + * @param enabled the enabled state + */ + void setSliderEnabled(int side, boolean enabled); + + /** + * Sets the slider value. + * + * @param side the side of the slider + * @param value the ambient value + */ + void setSliderValue(int side, int value); + + /** + * Sets the slider's minimum and maximum value. + * + * @param side the side of the slider + * @param min the minimum ambient value + * @param max the maximum ambient value + */ + void setSliderRange(int side, int min, int max); + + /** Updates the UI according to current state. */ + void updateLayout(); +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUiController.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUiController.java new file mode 100644 index 000000000000..ce392b12516f --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/AmbientVolumeUiController.java @@ -0,0 +1,527 @@ +/* + * 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.settingslib.bluetooth; + +import static android.bluetooth.AudioInputControl.MUTE_NOT_MUTED; +import static android.bluetooth.AudioInputControl.MUTE_MUTED; +import static android.bluetooth.BluetoothDevice.BOND_BONDED; + +import static com.android.settingslib.bluetooth.AmbientVolumeUi.SIDE_UNIFIED; +import static com.android.settingslib.bluetooth.AmbientVolumeUi.VALID_SIDES; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_INVALID; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT; +import static com.android.settingslib.bluetooth.HearingDeviceLocalDataManager.Data.INVALID_VOLUME; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.util.ArraySet; +import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.settingslib.R; +import com.android.settingslib.utils.ThreadUtils; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; + +import java.util.Map; +import java.util.Set; + +/** This class controls ambient volume UI with local and remote ambient data. */ +public class AmbientVolumeUiController implements + HearingDeviceLocalDataManager.OnDeviceLocalDataChangeListener, + AmbientVolumeController.AmbientVolumeControlCallback, + AmbientVolumeUi.AmbientVolumeUiListener, BluetoothCallback, CachedBluetoothDevice.Callback { + + private static final boolean DEBUG = true; + private static final String TAG = "AmbientVolumeUiController"; + + private final Context mContext; + private final LocalBluetoothProfileManager mProfileManager; + private final BluetoothEventManager mEventManager; + private final AmbientVolumeUi mAmbientLayout; + private final AmbientVolumeController mVolumeController; + private final HearingDeviceLocalDataManager mLocalDataManager; + + private final Set<CachedBluetoothDevice> mCachedDevices = new ArraySet<>(); + private final BiMap<Integer, BluetoothDevice> mSideToDeviceMap = HashBiMap.create(); + private CachedBluetoothDevice mCachedDevice; + private boolean mShowUiWhenLocalDataExist = true; + + public AmbientVolumeUiController(@NonNull Context context, + @NonNull LocalBluetoothManager bluetoothManager, + @NonNull AmbientVolumeUi ambientLayout) { + mContext = context; + mProfileManager = bluetoothManager.getProfileManager(); + mEventManager = bluetoothManager.getEventManager(); + mAmbientLayout = ambientLayout; + mAmbientLayout.setListener(this); + mVolumeController = new AmbientVolumeController(mProfileManager, this); + mLocalDataManager = new HearingDeviceLocalDataManager(context); + mLocalDataManager.setOnDeviceLocalDataChangeListener(this, + ThreadUtils.getBackgroundExecutor()); + } + + @VisibleForTesting + public AmbientVolumeUiController(@NonNull Context context, + @NonNull LocalBluetoothManager bluetoothManager, + @NonNull AmbientVolumeUi ambientLayout, + @NonNull AmbientVolumeController volumeController, + @NonNull HearingDeviceLocalDataManager localDataManager) { + mContext = context; + mProfileManager = bluetoothManager.getProfileManager(); + mEventManager = bluetoothManager.getEventManager(); + mAmbientLayout = ambientLayout; + mVolumeController = volumeController; + mLocalDataManager = localDataManager; + } + + + @Override + public void onDeviceLocalDataChange(@NonNull String address, + @Nullable HearingDeviceLocalDataManager.Data data) { + if (data == null) { + // The local data is removed because the device is unpaired, do nothing + return; + } + if (DEBUG) { + Log.d(TAG, "onDeviceLocalDataChange, address:" + address + ", data:" + data); + } + for (BluetoothDevice device : mSideToDeviceMap.values()) { + if (device.getAnonymizedAddress().equals(address)) { + postOnMainThread(() -> loadLocalDataToUi(device)); + return; + } + } + } + + @Override + public void onVolumeControlServiceConnected() { + mCachedDevices.forEach(device -> mVolumeController.registerCallback( + ThreadUtils.getBackgroundExecutor(), device.getDevice())); + } + + @Override + public void onAmbientChanged(@NonNull BluetoothDevice device, int gainSettings) { + if (DEBUG) { + Log.d(TAG, "onAmbientChanged, value:" + gainSettings + ", device:" + device); + } + HearingDeviceLocalDataManager.Data data = mLocalDataManager.get(device); + final boolean expanded = mAmbientLayout.isExpanded(); + final boolean isInitiatedFromUi = (expanded && data.ambient() == gainSettings) + || (!expanded && data.groupAmbient() == gainSettings); + if (isInitiatedFromUi) { + // The change is initiated from UI, no need to update UI + return; + } + + // We have to check if we need to expand the controls by getting all remote + // device's ambient value, delay for a while to wait all remote devices update + // to the latest value to avoid unnecessary expand action. + postDelayedOnMainThread(this::refresh, 1200L); + } + + @Override + public void onMuteChanged(@NonNull BluetoothDevice device, int mute) { + if (DEBUG) { + Log.d(TAG, "onMuteChanged, mute:" + mute + ", device:" + device); + } + final boolean muted = mAmbientLayout.isMuted(); + boolean isInitiatedFromUi = (muted && mute == MUTE_MUTED) + || (!muted && mute == MUTE_NOT_MUTED); + if (isInitiatedFromUi) { + // The change is initiated from UI, no need to update UI + return; + } + + // We have to check if we need to mute the devices by getting all remote + // device's mute state, delay for a while to wait all remote devices update + // to the latest value. + postDelayedOnMainThread(this::refresh, 1200L); + } + + @Override + public void onCommandFailed(@NonNull BluetoothDevice device) { + Log.w(TAG, "onCommandFailed, device:" + device); + postOnMainThread(() -> { + showErrorToast(R.string.bluetooth_hearing_device_ambient_error); + refresh(); + }); + } + + @Override + public void onExpandIconClick() { + mSideToDeviceMap.forEach((s, d) -> { + if (!mAmbientLayout.isMuted()) { + // Apply previous collapsed/expanded volume to remote device + HearingDeviceLocalDataManager.Data data = mLocalDataManager.get(d); + int volume = mAmbientLayout.isExpanded() + ? data.ambient() : data.groupAmbient(); + mVolumeController.setAmbient(d, volume); + } + // Update new value to local data + mLocalDataManager.updateAmbientControlExpanded(d, + mAmbientLayout.isExpanded()); + }); + mLocalDataManager.flush(); + } + + @Override + public void onAmbientVolumeIconClick() { + if (!mAmbientLayout.isMuted()) { + loadLocalDataToUi(); + } + for (BluetoothDevice device : mSideToDeviceMap.values()) { + mVolumeController.setMuted(device, mAmbientLayout.isMuted()); + } + } + + @Override + public void onSliderValueChange(int side, int value) { + if (DEBUG) { + Log.d(TAG, "onSliderValueChange: side=" + side + ", value=" + value); + } + setVolumeIfValid(side, value); + + Runnable setAmbientRunnable = () -> { + if (side == SIDE_UNIFIED) { + mSideToDeviceMap.forEach((s, d) -> mVolumeController.setAmbient(d, value)); + } else { + final BluetoothDevice device = mSideToDeviceMap.get(side); + mVolumeController.setAmbient(device, value); + } + }; + + if (mAmbientLayout.isMuted()) { + // User drag on the volume slider when muted. Unmute the devices first. + mAmbientLayout.setMuted(false); + + for (BluetoothDevice device : mSideToDeviceMap.values()) { + mVolumeController.setMuted(device, false); + } + // Restore the value before muted + loadLocalDataToUi(); + // Delay set ambient on remote device since the immediately sequential command + // might get failed sometimes + postDelayedOnMainThread(setAmbientRunnable, 1000L); + } else { + setAmbientRunnable.run(); + } + } + + @Override + public void onProfileConnectionStateChanged(@NonNull CachedBluetoothDevice cachedDevice, + int state, int bluetoothProfile) { + if (bluetoothProfile == BluetoothProfile.VOLUME_CONTROL + && state == BluetoothProfile.STATE_CONNECTED + && mCachedDevices.contains(cachedDevice)) { + // After VCP connected, AICS may not ready yet and still return invalid value, delay + // a while to wait AICS ready as a workaround + postDelayedOnMainThread(this::refresh, 1000L); + } + } + + @Override + public void onDeviceAttributesChanged() { + mCachedDevices.forEach(device -> { + device.unregisterCallback(this); + mVolumeController.unregisterCallback(device.getDevice()); + }); + postOnMainThread(()-> { + loadDevice(mCachedDevice); + ThreadUtils.postOnBackgroundThread(()-> { + mCachedDevices.forEach(device -> { + device.registerCallback(ThreadUtils.getBackgroundExecutor(), this); + mVolumeController.registerCallback(ThreadUtils.getBackgroundExecutor(), + device.getDevice()); + }); + }); + }); + } + + /** + * Registers callbacks and listeners, this should be called when needs to start listening to + * events. + */ + public void start() { + mEventManager.registerCallback(this); + mLocalDataManager.start(); + mCachedDevices.forEach(device -> { + device.registerCallback(ThreadUtils.getBackgroundExecutor(), this); + mVolumeController.registerCallback(ThreadUtils.getBackgroundExecutor(), + device.getDevice()); + }); + } + + /** + * Unregisters callbacks and listeners, this should be called when no longer needs to listen to + * events. + */ + public void stop() { + mEventManager.unregisterCallback(this); + mLocalDataManager.stop(); + mCachedDevices.forEach(device -> { + device.unregisterCallback(this); + mVolumeController.unregisterCallback(device.getDevice()); + }); + } + + /** + * Loads all devices in the same set with {@code cachedDevice} and create corresponding sliders. + * + * <p>If the devices has valid ambient control points, the ambient volume UI will be visible. + * @param cachedDevice the remote device + */ + public void loadDevice(CachedBluetoothDevice cachedDevice) { + if (DEBUG) { + Log.d(TAG, "loadDevice, device=" + cachedDevice); + } + mCachedDevice = cachedDevice; + mSideToDeviceMap.clear(); + mCachedDevices.clear(); + boolean deviceSupportVcp = + cachedDevice != null && cachedDevice.getProfiles().stream().anyMatch( + p -> p instanceof VolumeControlProfile); + if (!deviceSupportVcp) { + mAmbientLayout.setVisible(false); + return; + } + + // load devices in the same set + if (VALID_SIDES.contains(cachedDevice.getDeviceSide()) + && cachedDevice.getBondState() == BOND_BONDED) { + mSideToDeviceMap.put(cachedDevice.getDeviceSide(), cachedDevice.getDevice()); + mCachedDevices.add(cachedDevice); + } + for (CachedBluetoothDevice memberDevice : cachedDevice.getMemberDevice()) { + if (VALID_SIDES.contains(memberDevice.getDeviceSide()) + && memberDevice.getBondState() == BOND_BONDED) { + mSideToDeviceMap.put(memberDevice.getDeviceSide(), memberDevice.getDevice()); + mCachedDevices.add(memberDevice); + } + } + + mAmbientLayout.setExpandable(mSideToDeviceMap.size() > 1); + mAmbientLayout.setupSliders(mSideToDeviceMap); + refresh(); + } + + /** Refreshes the ambient volume UI. */ + public void refresh() { + if (isAmbientControlAvailable()) { + mAmbientLayout.setVisible(true); + loadRemoteDataToUi(); + } else { + mAmbientLayout.setVisible(false); + } + } + + /** Sets if the ambient volume UI should be visible when local ambient data exist. */ + public void setShowUiWhenLocalDataExist(boolean shouldShow) { + mShowUiWhenLocalDataExist = shouldShow; + } + + /** Updates the ambient sliders according to current state. */ + private void updateSliderUi() { + boolean isAnySliderEnabled = false; + for (Map.Entry<Integer, BluetoothDevice> entry : mSideToDeviceMap.entrySet()) { + final int side = entry.getKey(); + final BluetoothDevice device = entry.getValue(); + final boolean enabled = isDeviceConnectedToVcp(device) + && mVolumeController.isAmbientControlAvailable(device); + isAnySliderEnabled |= enabled; + mAmbientLayout.setSliderEnabled(side, enabled); + } + mAmbientLayout.setSliderEnabled(SIDE_UNIFIED, isAnySliderEnabled); + mAmbientLayout.updateLayout(); + } + + /** Sets the ambient to the corresponding control slider. */ + private void setVolumeIfValid(int side, int volume) { + if (volume == INVALID_VOLUME) { + return; + } + mAmbientLayout.setSliderValue(side, volume); + // Update new value to local data + if (side == SIDE_UNIFIED) { + mSideToDeviceMap.forEach((s, d) -> mLocalDataManager.updateGroupAmbient(d, volume)); + } else { + mLocalDataManager.updateAmbient(mSideToDeviceMap.get(side), volume); + } + mLocalDataManager.flush(); + } + + private void loadLocalDataToUi() { + mSideToDeviceMap.forEach((s, d) -> loadLocalDataToUi(d)); + } + + private void loadLocalDataToUi(BluetoothDevice device) { + final HearingDeviceLocalDataManager.Data data = mLocalDataManager.get(device); + if (DEBUG) { + Log.d(TAG, "loadLocalDataToUi, data=" + data + ", device=" + device); + } + if (isDeviceConnectedToVcp(device) && !mAmbientLayout.isMuted()) { + final int side = mSideToDeviceMap.inverse().getOrDefault(device, SIDE_INVALID); + setVolumeIfValid(side, data.ambient()); + setVolumeIfValid(SIDE_UNIFIED, data.groupAmbient()); + } + setAmbientControlExpanded(data.ambientControlExpanded()); + updateSliderUi(); + } + + private void loadRemoteDataToUi() { + BluetoothDevice leftDevice = mSideToDeviceMap.get(SIDE_LEFT); + AmbientVolumeController.RemoteAmbientState leftState = + mVolumeController.refreshAmbientState(leftDevice); + BluetoothDevice rightDevice = mSideToDeviceMap.get(SIDE_RIGHT); + AmbientVolumeController.RemoteAmbientState rightState = + mVolumeController.refreshAmbientState(rightDevice); + if (DEBUG) { + Log.d(TAG, "loadRemoteDataToUi, left=" + leftState + ", right=" + rightState); + } + mSideToDeviceMap.forEach((side, device) -> { + int ambientMax = mVolumeController.getAmbientMax(device); + int ambientMin = mVolumeController.getAmbientMin(device); + if (ambientMin != ambientMax) { + mAmbientLayout.setSliderRange(side, ambientMin, ambientMax); + mAmbientLayout.setSliderRange(SIDE_UNIFIED, ambientMin, ambientMax); + } + }); + + // Update ambient volume + final int leftAmbient = leftState != null ? leftState.gainSetting() : INVALID_VOLUME; + final int rightAmbient = rightState != null ? rightState.gainSetting() : INVALID_VOLUME; + if (mAmbientLayout.isExpanded()) { + setVolumeIfValid(SIDE_LEFT, leftAmbient); + setVolumeIfValid(SIDE_RIGHT, rightAmbient); + } else { + if (leftAmbient != rightAmbient && leftAmbient != INVALID_VOLUME + && rightAmbient != INVALID_VOLUME) { + setVolumeIfValid(SIDE_LEFT, leftAmbient); + setVolumeIfValid(SIDE_RIGHT, rightAmbient); + setAmbientControlExpanded(true); + } else { + int unifiedAmbient = leftAmbient != INVALID_VOLUME ? leftAmbient : rightAmbient; + setVolumeIfValid(SIDE_UNIFIED, unifiedAmbient); + } + } + // Initialize local data between side and group value + initLocalAmbientDataIfNeeded(); + + // Update mute state + boolean mutable = true; + boolean muted = true; + if (isDeviceConnectedToVcp(leftDevice) && leftState != null) { + mutable &= leftState.isMutable(); + muted &= leftState.isMuted(); + } + if (isDeviceConnectedToVcp(rightDevice) && rightState != null) { + mutable &= rightState.isMutable(); + muted &= rightState.isMuted(); + } + mAmbientLayout.setMutable(mutable); + mAmbientLayout.setMuted(muted); + + // Ensure remote device mute state is synced + syncMuteStateIfNeeded(leftDevice, leftState, muted); + syncMuteStateIfNeeded(rightDevice, rightState, muted); + + updateSliderUi(); + } + + private void setAmbientControlExpanded(boolean expanded) { + mAmbientLayout.setExpanded(expanded); + mSideToDeviceMap.forEach((s, d) -> { + // Update new value to local data + mLocalDataManager.updateAmbientControlExpanded(d, expanded); + }); + mLocalDataManager.flush(); + } + + /** Checks if any device in the same set has valid ambient control points */ + private boolean isAmbientControlAvailable() { + for (BluetoothDevice device : mSideToDeviceMap.values()) { + if (mShowUiWhenLocalDataExist) { + // Found local ambient data + if (mLocalDataManager.get(device).hasAmbientData()) { + return true; + } + } + // Found remote ambient control points + if (mVolumeController.isAmbientControlAvailable(device)) { + return true; + } + } + return false; + } + + private void initLocalAmbientDataIfNeeded() { + int smallerVolumeAmongGroup = Integer.MAX_VALUE; + for (BluetoothDevice device : mSideToDeviceMap.values()) { + HearingDeviceLocalDataManager.Data data = mLocalDataManager.get(device); + if (data.ambient() != INVALID_VOLUME) { + smallerVolumeAmongGroup = Math.min(data.ambient(), smallerVolumeAmongGroup); + } else if (data.groupAmbient() != INVALID_VOLUME) { + // Initialize side ambient from group ambient value + mLocalDataManager.updateAmbient(device, data.groupAmbient()); + } + } + if (smallerVolumeAmongGroup != Integer.MAX_VALUE) { + for (BluetoothDevice device : mSideToDeviceMap.values()) { + HearingDeviceLocalDataManager.Data data = mLocalDataManager.get(device); + if (data.groupAmbient() == INVALID_VOLUME) { + // Initialize group ambient from smaller side ambient value + mLocalDataManager.updateGroupAmbient(device, smallerVolumeAmongGroup); + } + } + } + mLocalDataManager.flush(); + } + + private void syncMuteStateIfNeeded(@Nullable BluetoothDevice device, + @Nullable AmbientVolumeController.RemoteAmbientState state, boolean muted) { + if (isDeviceConnectedToVcp(device) && state != null && state.isMutable()) { + if (state.isMuted() != muted) { + mVolumeController.setMuted(device, muted); + } + } + } + + private boolean isDeviceConnectedToVcp(@Nullable BluetoothDevice device) { + return device != null && device.isConnected() + && mProfileManager.getVolumeControlProfile().getConnectionStatus(device) + == BluetoothProfile.STATE_CONNECTED; + } + + private void postOnMainThread(Runnable runnable) { + mContext.getMainThreadHandler().post(runnable); + } + + private void postDelayedOnMainThread(Runnable runnable, long delay) { + mContext.getMainThreadHandler().postDelayed(runnable, delay); + } + + private void showErrorToast(int stringResId) { + Toast.makeText(mContext, stringResId, Toast.LENGTH_SHORT).show(); + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java index 429e4c958f05..0c642d7b8f98 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java @@ -656,7 +656,8 @@ public class BluetoothUtils { @WorkerThread public static boolean isAudioSharingHysteresisModeFixAvailable(@Nullable Context context) { return (audioSharingHysteresisModeFix() && Flags.enableLeAudioSharing()) - || (context != null && isAudioSharingPreviewEnabled(context.getContentResolver())); + || (context != null && Flags.audioSharingDeveloperOption() + && getAudioSharingPreviewValue(context.getContentResolver())); } /** Returns if the le audio sharing is enabled. */ diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManager.java index 6725558cd2bd..3cd37320243f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManager.java @@ -148,6 +148,14 @@ public class HearingDeviceLocalDataManager { } } + /** Flushes the data into Settings . */ + public synchronized void flush() { + if (!mIsStarted) { + return; + } + putAmbientVolumeSettings(); + } + /** * Puts the local data of the corresponding hearing device. * @@ -274,9 +282,6 @@ public class HearingDeviceLocalDataManager { notifyIfDataChanged(mAddrToDataMap, updatedAddrToDataMap); mAddrToDataMap.clear(); mAddrToDataMap.putAll(updatedAddrToDataMap); - if (DEBUG) { - Log.v(TAG, "getLocalDataFromSettings, " + mAddrToDataMap + ", manager: " + this); - } } } @@ -287,12 +292,10 @@ public class HearingDeviceLocalDataManager { builder.append(KEY_ADDR).append("=").append(entry.getKey()); builder.append(entry.getValue().toSettingsFormat()).append(";"); } - if (DEBUG) { - Log.v(TAG, "putAmbientVolumeSettings, " + builder + ", manager: " + this); - } - Settings.Global.putStringForUser(mContext.getContentResolver(), - LOCAL_AMBIENT_VOLUME_SETTINGS, builder.toString(), - UserHandle.USER_SYSTEM); + ThreadUtils.postOnBackgroundThread(() -> { + Settings.Global.putStringForUser(mContext.getContentResolver(), + LOCAL_AMBIENT_VOLUME_SETTINGS, builder.toString(), UserHandle.USER_SYSTEM); + }); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index b52ed42d567f..2c99a2d4818c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -101,6 +101,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { public @interface BroadcastState {} private static final String SETTINGS_PKG = "com.android.settings"; + private static final String SYSUI_PKG = "com.android.systemui"; private static final String TAG = "LocalBluetoothLeBroadcast"; private static final boolean DEBUG = BluetoothUtils.D; @@ -216,6 +217,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } setLatestBroadcastId(broadcastId); setAppSourceName(mNewAppSourceName, /* updateContentResolver= */ true); + notifyBroadcastStateChange(BROADCAST_STATE_ON); } @Override @@ -232,7 +234,6 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = " + broadcastId); } setLatestBluetoothLeBroadcastMetadata(metadata); - notifyBroadcastStateChange(BROADCAST_STATE_ON); } @Override @@ -1247,8 +1248,9 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } private void notifyBroadcastStateChange(@BroadcastState int state) { - if (!mContext.getPackageName().equals(SETTINGS_PKG)) { - Log.d(TAG, "Skip notifyBroadcastStateChange, not triggered by Settings."); + String packageName = mContext.getPackageName(); + if (!packageName.equals(SETTINGS_PKG) && !packageName.equals(SYSUI_PKG)) { + Log.d(TAG, "Skip notifyBroadcastStateChange, not triggered by Settings or SystemUI."); return; } if (isWorkProfile(mContext)) { @@ -1257,8 +1259,8 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } Intent intent = new Intent(ACTION_LE_AUDIO_SHARING_STATE_CHANGE); intent.putExtra(EXTRA_LE_AUDIO_SHARING_STATE, state); - intent.setPackage(mContext.getPackageName()); - Log.d(TAG, "notifyBroadcastStateChange for state = " + state); + intent.setPackage(SETTINGS_PKG); + Log.d(TAG, "notifyBroadcastStateChange for state = " + state + " by pkg = " + packageName); mContext.sendBroadcast(intent); } diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java index cf452314163f..c9aac91f1320 100644 --- a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java +++ b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java @@ -21,6 +21,7 @@ import static android.net.wifi.WifiConfiguration.NetworkSelectionStatus.NETWORK_ import android.annotation.IntDef; import android.annotation.MainThread; +import android.app.ActivityManager; import android.app.AppGlobals; import android.content.Context; import android.content.pm.ApplicationInfo; @@ -1643,7 +1644,7 @@ public class AccessPoint implements Comparable<AccessPoint> { CharSequence appLabel = ""; ApplicationInfo appInfo = null; try { - int userId = UserHandle.getUserId(UserHandle.USER_CURRENT); + int userId = ActivityManager.getCurrentUser(); appInfo = packageManager.getApplicationInfoAsUser(packageName, 0 /* flags */, userId); } catch (PackageManager.NameNotFoundException e) { Log.e(TAG, "Failed to get app info", e); diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/AmbientVolumeUiControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/AmbientVolumeUiControllerTest.java new file mode 100644 index 000000000000..8b606e299971 --- /dev/null +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/AmbientVolumeUiControllerTest.java @@ -0,0 +1,315 @@ +/* + * 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.settingslib.bluetooth; + +import static android.bluetooth.AudioInputControl.MUTE_DISABLED; +import static android.bluetooth.AudioInputControl.MUTE_MUTED; +import static android.bluetooth.AudioInputControl.MUTE_NOT_MUTED; +import static android.bluetooth.BluetoothDevice.BOND_BONDED; + +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.never; +import static org.robolectric.Shadows.shadowOf; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothProfile; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; + +import androidx.test.core.app.ApplicationProvider; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; + +/** Tests for {@link AmbientVolumeUiController}. */ +@RunWith(RobolectricTestRunner.class) +public class AmbientVolumeUiControllerTest { + + @Rule + public MockitoRule mockito = MockitoJUnit.rule(); + + private static final String TEST_ADDRESS = "00:00:00:00:11"; + private static final String TEST_MEMBER_ADDRESS = "00:00:00:00:22"; + + @Mock + LocalBluetoothManager mBluetoothManager; + @Mock + LocalBluetoothProfileManager mProfileManager; + @Mock + BluetoothEventManager mEventManager; + @Mock + VolumeControlProfile mVolumeControlProfile; + @Mock + AmbientVolumeUi mAmbientLayout; + @Mock + private AmbientVolumeController mVolumeController; + @Mock + private HearingDeviceLocalDataManager mLocalDataManager; + @Mock + private CachedBluetoothDevice mCachedDevice; + @Mock + private CachedBluetoothDevice mCachedMemberDevice; + @Mock + private BluetoothDevice mDevice; + @Mock + private BluetoothDevice mMemberDevice; + @Mock + private Handler mTestHandler; + + @Spy + private final Context mContext = ApplicationProvider.getApplicationContext(); + private AmbientVolumeUiController mController; + + @Before + public void setUp() { + when(mBluetoothManager.getProfileManager()).thenReturn(mProfileManager); + when(mBluetoothManager.getEventManager()).thenReturn(mEventManager); + + mController = spy(new AmbientVolumeUiController(mContext, mBluetoothManager, + mAmbientLayout, mVolumeController, mLocalDataManager)); + + when(mProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControlProfile); + when(mVolumeControlProfile.getConnectionStatus(mDevice)).thenReturn( + BluetoothProfile.STATE_CONNECTED); + when(mVolumeControlProfile.getConnectionStatus(mMemberDevice)).thenReturn( + BluetoothProfile.STATE_CONNECTED); + when(mVolumeController.isAmbientControlAvailable(mDevice)).thenReturn(true); + when(mVolumeController.isAmbientControlAvailable(mMemberDevice)).thenReturn(true); + when(mLocalDataManager.get(any(BluetoothDevice.class))).thenReturn( + new HearingDeviceLocalDataManager.Data.Builder().build()); + + when(mContext.getMainThreadHandler()).thenReturn(mTestHandler); + Answer<Object> answer = invocationOnMock -> { + invocationOnMock.getArgument(0, Runnable.class).run(); + return null; + }; + when(mTestHandler.post(any(Runnable.class))).thenAnswer(answer); + when(mTestHandler.postDelayed(any(Runnable.class), anyLong())).thenAnswer(answer); + + prepareDevice(/* hasMember= */ true); + mController.loadDevice(mCachedDevice); + Mockito.reset(mController); + Mockito.reset(mAmbientLayout); + } + + @Test + public void loadDevice_deviceWithoutMember_controlNotExpandable() { + prepareDevice(/* hasMember= */ false); + + mController.loadDevice(mCachedDevice); + + verify(mAmbientLayout).setExpandable(false); + } + + @Test + public void loadDevice_deviceWithMember_controlExpandable() { + prepareDevice(/* hasMember= */ true); + + mController.loadDevice(mCachedDevice); + + verify(mAmbientLayout).setExpandable(true); + } + + @Test + public void loadDevice_deviceNotSupportVcp_ambientLayoutGone() { + when(mCachedDevice.getProfiles()).thenReturn(List.of()); + + mController.loadDevice(mCachedDevice); + + verify(mAmbientLayout).setVisible(false); + } + + @Test + public void loadDevice_ambientControlNotAvailable_ambientLayoutGone() { + when(mVolumeController.isAmbientControlAvailable(mDevice)).thenReturn(false); + when(mVolumeController.isAmbientControlAvailable(mMemberDevice)).thenReturn(false); + + mController.loadDevice(mCachedDevice); + + verify(mAmbientLayout).setVisible(false); + } + + @Test + public void loadDevice_supportVcpAndAmbientControlAvailable_ambientLayoutVisible() { + when(mCachedDevice.getProfiles()).thenReturn(List.of(mVolumeControlProfile)); + when(mVolumeController.isAmbientControlAvailable(mDevice)).thenReturn(true); + + mController.loadDevice(mCachedDevice); + + verify(mAmbientLayout).setVisible(true); + } + + @Test + public void start_callbackRegistered() { + mController.start(); + + verify(mEventManager).registerCallback(mController); + verify(mLocalDataManager).start(); + verify(mVolumeController).registerCallback(any(Executor.class), eq(mDevice)); + verify(mVolumeController).registerCallback(any(Executor.class), eq(mMemberDevice)); + verify(mCachedDevice).registerCallback(any(Executor.class), + any(CachedBluetoothDevice.Callback.class)); + verify(mCachedMemberDevice).registerCallback(any(Executor.class), + any(CachedBluetoothDevice.Callback.class)); + } + + @Test + public void stop_callbackUnregistered() { + mController.stop(); + + verify(mEventManager).unregisterCallback(mController); + verify(mLocalDataManager).stop(); + verify(mVolumeController).unregisterCallback(mDevice); + verify(mVolumeController).unregisterCallback(mMemberDevice); + verify(mCachedDevice).unregisterCallback(any(CachedBluetoothDevice.Callback.class)); + verify(mCachedMemberDevice).unregisterCallback(any(CachedBluetoothDevice.Callback.class)); + } + + @Test + public void onDeviceLocalDataChange_verifySetExpandedAndDataUpdated() { + final boolean testExpanded = true; + HearingDeviceLocalDataManager.Data data = new HearingDeviceLocalDataManager.Data.Builder() + .ambient(0).groupAmbient(0).ambientControlExpanded(testExpanded).build(); + when(mLocalDataManager.get(mDevice)).thenReturn(data); + + mController.onDeviceLocalDataChange(TEST_ADDRESS, data); + shadowOf(Looper.getMainLooper()).idle(); + + verify(mAmbientLayout).setExpanded(testExpanded); + verifyDeviceDataUpdated(mDevice); + } + + @Test + public void onAmbientChanged_refreshWhenNotInitiateFromUi() { + HearingDeviceLocalDataManager.Data data = new HearingDeviceLocalDataManager.Data.Builder() + .ambient(10).groupAmbient(10).ambientControlExpanded(true).build(); + when(mLocalDataManager.get(mDevice)).thenReturn(data); + when(mAmbientLayout.isExpanded()).thenReturn(true); + + mController.onAmbientChanged(mDevice, 10); + verify(mController, never()).refresh(); + + mController.onAmbientChanged(mDevice, 20); + verify(mController).refresh(); + } + + @Test + public void onMuteChanged_refreshWhenNotInitiateFromUi() { + AmbientVolumeController.RemoteAmbientState state = + new AmbientVolumeController.RemoteAmbientState(MUTE_NOT_MUTED, 0); + when(mVolumeController.refreshAmbientState(mDevice)).thenReturn(state); + when(mAmbientLayout.isExpanded()).thenReturn(false); + + mController.onMuteChanged(mDevice, MUTE_NOT_MUTED); + verify(mController, never()).refresh(); + + mController.onMuteChanged(mDevice, MUTE_MUTED); + verify(mController).refresh(); + } + + @Test + public void refresh_leftAndRightDifferentGainSetting_expandControl() { + prepareRemoteData(mDevice, 10, MUTE_NOT_MUTED); + prepareRemoteData(mMemberDevice, 20, MUTE_NOT_MUTED); + when(mAmbientLayout.isExpanded()).thenReturn(false); + + mController.refresh(); + + verify(mAmbientLayout).setExpanded(true); + } + + @Test + public void refresh_oneSideNotMutable_controlNotMutableAndNotMuted() { + prepareRemoteData(mDevice, 10, MUTE_DISABLED); + prepareRemoteData(mMemberDevice, 20, MUTE_NOT_MUTED); + + mController.refresh(); + + verify(mAmbientLayout).setMutable(false); + verify(mAmbientLayout).setMuted(false); + } + + @Test + public void refresh_oneSideNotMuted_controlNotMutedAndSyncToRemote() { + prepareRemoteData(mDevice, 10, MUTE_MUTED); + prepareRemoteData(mMemberDevice, 20, MUTE_NOT_MUTED); + + mController.refresh(); + + verify(mAmbientLayout).setMutable(true); + verify(mAmbientLayout).setMuted(false); + verify(mVolumeController).setMuted(mDevice, false); + } + + private void prepareDevice(boolean hasMember) { + when(mCachedDevice.getDeviceSide()).thenReturn(SIDE_LEFT); + when(mCachedDevice.getDevice()).thenReturn(mDevice); + when(mCachedDevice.getBondState()).thenReturn(BOND_BONDED); + when(mCachedDevice.getProfiles()).thenReturn(List.of(mVolumeControlProfile)); + when(mDevice.getAddress()).thenReturn(TEST_ADDRESS); + when(mDevice.getAnonymizedAddress()).thenReturn(TEST_ADDRESS); + when(mDevice.isConnected()).thenReturn(true); + if (hasMember) { + when(mCachedDevice.getMemberDevice()).thenReturn(Set.of(mCachedMemberDevice)); + when(mCachedMemberDevice.getDeviceSide()).thenReturn(SIDE_RIGHT); + when(mCachedMemberDevice.getDevice()).thenReturn(mMemberDevice); + when(mCachedMemberDevice.getBondState()).thenReturn(BOND_BONDED); + when(mCachedMemberDevice.getProfiles()).thenReturn(List.of(mVolumeControlProfile)); + when(mMemberDevice.getAddress()).thenReturn(TEST_MEMBER_ADDRESS); + when(mMemberDevice.getAnonymizedAddress()).thenReturn(TEST_MEMBER_ADDRESS); + when(mMemberDevice.isConnected()).thenReturn(true); + } else { + when(mCachedDevice.getMemberDevice()).thenReturn(Set.of()); + } + } + + private void prepareRemoteData(BluetoothDevice device, int gainSetting, int mute) { + when(mVolumeController.refreshAmbientState(device)).thenReturn( + new AmbientVolumeController.RemoteAmbientState(gainSetting, mute)); + } + + private void verifyDeviceDataUpdated(BluetoothDevice device) { + verify(mLocalDataManager).updateAmbient(eq(device), anyInt()); + verify(mLocalDataManager).updateGroupAmbient(eq(device), anyInt()); + verify(mLocalDataManager).updateAmbientControlExpanded(eq(device), + anyBoolean()); + } +} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java index fa5d54283a17..ab9f871444b4 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java @@ -970,8 +970,10 @@ public class BluetoothUtilsTest { when(cachedBluetoothDevice2.getGroupId()).thenReturn(2); BluetoothDevice device1 = mock(BluetoothDevice.class); + when(mCachedBluetoothDevice.getDevice()).thenReturn(device1); when(mDeviceManager.findDevice(device1)).thenReturn(mCachedBluetoothDevice); BluetoothDevice device2 = mock(BluetoothDevice.class); + when(cachedBluetoothDevice2.getDevice()).thenReturn(device2); when(mDeviceManager.findDevice(device2)).thenReturn(cachedBluetoothDevice2); when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(device1, device2)); @@ -991,8 +993,10 @@ public class BluetoothUtilsTest { when(cachedBluetoothDevice2.getGroupId()).thenReturn(2); BluetoothDevice device1 = mock(BluetoothDevice.class); + when(mCachedBluetoothDevice.getDevice()).thenReturn(device1); when(mDeviceManager.findDevice(device1)).thenReturn(mCachedBluetoothDevice); BluetoothDevice device2 = mock(BluetoothDevice.class); + when(cachedBluetoothDevice2.getDevice()).thenReturn(device2); when(mDeviceManager.findDevice(device2)).thenReturn(cachedBluetoothDevice2); when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(device1, device2)); @@ -1012,8 +1016,10 @@ public class BluetoothUtilsTest { when(cachedBluetoothDevice2.getGroupId()).thenReturn(2); BluetoothDevice device1 = mock(BluetoothDevice.class); + when(mCachedBluetoothDevice.getDevice()).thenReturn(device1); when(mDeviceManager.findDevice(device1)).thenReturn(mCachedBluetoothDevice); BluetoothDevice device2 = mock(BluetoothDevice.class); + when(cachedBluetoothDevice2.getDevice()).thenReturn(device2); when(mDeviceManager.findDevice(device2)).thenReturn(cachedBluetoothDevice2); when(mAssistant.getAllConnectedDevices()).thenReturn(ImmutableList.of(device2)); @@ -1035,10 +1041,13 @@ public class BluetoothUtilsTest { when(cachedBluetoothDevice3.getGroupId()).thenReturn(3); BluetoothDevice device1 = mock(BluetoothDevice.class); + when(mCachedBluetoothDevice.getDevice()).thenReturn(device1); when(mDeviceManager.findDevice(device1)).thenReturn(mCachedBluetoothDevice); BluetoothDevice device2 = mock(BluetoothDevice.class); + when(cachedBluetoothDevice2.getDevice()).thenReturn(device2); when(mDeviceManager.findDevice(device2)).thenReturn(cachedBluetoothDevice2); BluetoothDevice device3 = mock(BluetoothDevice.class); + when(cachedBluetoothDevice3.getDevice()).thenReturn(device3); when(mDeviceManager.findDevice(device3)).thenReturn(cachedBluetoothDevice3); when(mAssistant.getAllConnectedDevices()) @@ -1280,6 +1289,8 @@ public class BluetoothUtilsTest { mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_LE_AUDIO_SHARING); mSetFlagsRule.disableFlags(Flags.FLAG_AUDIO_SHARING_HYSTERESIS_MODE_FIX); mSetFlagsRule.enableFlags(Flags.FLAG_AUDIO_SHARING_DEVELOPER_OPTION); + Settings.Global.putInt(mContext.getContentResolver(), + BluetoothUtils.DEVELOPER_OPTION_PREVIEW_KEY, 1); assertThat(BluetoothUtils.isAudioSharingHysteresisModeFixAvailable(mContext)).isTrue(); } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManagerTest.java index 6d83588e0f6e..6485636079dd 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManagerTest.java @@ -31,6 +31,8 @@ import android.provider.Settings; import androidx.test.core.app.ApplicationProvider; +import com.android.settingslib.utils.ThreadUtils; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -49,7 +51,10 @@ import java.util.Map; /** Tests for {@link HearingDeviceLocalDataManager}. */ @RunWith(RobolectricTestRunner.class) -@Config(shadows = {HearingDeviceLocalDataManagerTest.ShadowGlobal.class}) +@Config(shadows = { + HearingDeviceLocalDataManagerTest.ShadowGlobal.class, + HearingDeviceLocalDataManagerTest.ShadowThreadUtils.class, +}) public class HearingDeviceLocalDataManagerTest { private static final String TEST_ADDRESS = "XX:XX:XX:XX:11:22"; @@ -249,4 +254,12 @@ public class HearingDeviceLocalDataManagerTest { return sDataMap.computeIfAbsent(cr, k -> new HashMap<>()); } } + + @Implements(value = ThreadUtils.class) + public static class ShadowThreadUtils { + @Implementation + protected static void postOnBackgroundThread(Runnable runnable) { + runnable.run(); + } + } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SelectorWithWidgetPreferenceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SelectorWithWidgetPreferenceTest.java index 2b8b3b74dab9..c939c770b63d 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SelectorWithWidgetPreferenceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/SelectorWithWidgetPreferenceTest.java @@ -21,9 +21,6 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import android.app.Application; -import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; @@ -33,10 +30,8 @@ import androidx.preference.PreferenceViewHolder; import androidx.test.core.app.ApplicationProvider; import com.android.settingslib.widget.preference.selector.R; -import com.android.settingslib.widget.selectorwithwidgetpreference.flags.Flags; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.robolectric.Robolectric; @@ -45,7 +40,6 @@ import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class SelectorWithWidgetPreferenceTest { - @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private Application mContext; private SelectorWithWidgetPreference mPreference; @@ -128,26 +122,6 @@ public class SelectorWithWidgetPreferenceTest { } @Test - @DisableFlags(Flags.FLAG_ALLOW_SET_TITLE_MAX_LINES) - public void onBindViewHolder_titleMaxLinesSet_flagOff_titleMaxLinesMatchesDefault() { - final int titleMaxLines = 5; - AttributeSet attributeSet = Robolectric.buildAttributeSet() - .addAttribute(R.attr.titleMaxLines, String.valueOf(titleMaxLines)) - .build(); - mPreference = new SelectorWithWidgetPreference(mContext, attributeSet); - View view = LayoutInflater.from(mContext) - .inflate(mPreference.getLayoutResource(), null /* root */); - PreferenceViewHolder preferenceViewHolder = - PreferenceViewHolder.createInstanceForTests(view); - - mPreference.onBindViewHolder(preferenceViewHolder); - - TextView title = (TextView) preferenceViewHolder.findViewById(android.R.id.title); - assertThat(title.getMaxLines()).isEqualTo(SelectorWithWidgetPreference.DEFAULT_MAX_LINES); - } - - @Test - @EnableFlags(Flags.FLAG_ALLOW_SET_TITLE_MAX_LINES) public void onBindViewHolder_noTitleMaxLinesSet_titleMaxLinesMatchesDefault() { AttributeSet attributeSet = Robolectric.buildAttributeSet().build(); mPreference = new SelectorWithWidgetPreference(mContext, attributeSet); @@ -163,7 +137,6 @@ public class SelectorWithWidgetPreferenceTest { } @Test - @EnableFlags(Flags.FLAG_ALLOW_SET_TITLE_MAX_LINES) public void onBindViewHolder_titleMaxLinesSet_titleMaxLinesUpdated() { final int titleMaxLines = 5; AttributeSet attributeSet = Robolectric.buildAttributeSet() diff --git a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java index 91ac34ac8233..de7c450d8d39 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/DeviceConfigService.java @@ -148,27 +148,7 @@ public final class DeviceConfigService extends Binder { // TODO(b/364399200): use filter to skip instead? return; } - - ArrayList<String> missingFiles = new ArrayList<String>(); - for (String fileName : sAconfigTextProtoFilesOnDevice) { - File aconfigFile = new File(fileName); - if (!aconfigFile.exists()) { - missingFiles.add(fileName); - } - } - - if (missingFiles.isEmpty()) { - pw.println("\nAconfig flags:"); - for (String name : MyShellCommand.listAllAconfigFlags(iprovider)) { - pw.println(name); - } - } else { - pw.println("\nFailed to dump aconfig flags due to missing files:"); - for (String fileName : missingFiles) { - pw.println(fileName); - } - } - } + } private static HashSet<String> getAconfigFlagNamesInDeviceConfig() { HashSet<String> nameSet = new HashSet<String>(); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java index 1c4def39eaa0..e01cb84f60ae 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupAgent.java @@ -16,6 +16,19 @@ package com.android.providers.settings; +import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_DEVICE_SPECIFIC_CONFIG; +import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_GLOBAL; +import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_LOCALE; +import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_LOCK_SETTINGS; +import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_NETWORK_POLICIES; +import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_SECURE; +import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_SIM_SPECIFIC_SETTINGS; +import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_SIM_SPECIFIC_SETTINGS_2; +import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_SOFTAP_CONFIG; +import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_SYSTEM; +import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_WIFI_NEW_CONFIG; +import static com.android.providers.settings.SettingsBackupRestoreKeys.KEY_WIFI_SETTINGS_BACKUP_DATA; + import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; @@ -99,22 +112,6 @@ public class SettingsBackupAgent extends BackupAgentHelper { private static final int NULL_SIZE = -1; private static final float FONT_SCALE_DEF_VALUE = 1.0f; - private static final String KEY_SYSTEM = "system"; - private static final String KEY_SECURE = "secure"; - private static final String KEY_GLOBAL = "global"; - private static final String KEY_LOCALE = "locale"; - private static final String KEY_LOCK_SETTINGS = "lock_settings"; - private static final String KEY_SOFTAP_CONFIG = "softap_config"; - private static final String KEY_NETWORK_POLICIES = "network_policies"; - private static final String KEY_WIFI_NEW_CONFIG = "wifi_new_config"; - private static final String KEY_DEVICE_SPECIFIC_CONFIG = "device_specific_config"; - private static final String KEY_SIM_SPECIFIC_SETTINGS = "sim_specific_settings"; - // Restoring sim-specific data backed up from newer Android version to Android 12 was causing a - // fatal crash. Creating a backup with a different key will prevent Android 12 versions from - // restoring this data. - private static final String KEY_SIM_SPECIFIC_SETTINGS_2 = "sim_specific_settings_2"; - private static final String KEY_WIFI_SETTINGS_BACKUP_DATA = "wifi_settings_backup_data"; - // Versioning of the state file. Increment this version // number any time the set of state items is altered. private static final int STATE_VERSION = 9; @@ -257,6 +254,7 @@ public class SettingsBackupAgent extends BackupAgentHelper { mWifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE); if (com.android.server.backup.Flags.enableMetricsSettingsBackupAgents()) { mBackupRestoreEventLogger = this.getBackupRestoreEventLogger(); + mSettingsHelper.setBackupRestoreEventLogger(mBackupRestoreEventLogger); numberOfSettingsPerKey = new HashMap<>(); areAgentMetricsEnabled = true; } @@ -412,9 +410,7 @@ public class SettingsBackupAgent extends BackupAgentHelper { mSettingsHelper .setLocaleData( localeData, - size, - mBackupRestoreEventLogger, - KEY_LOCALE); + size); break; case KEY_WIFI_CONFIG : @@ -552,8 +548,7 @@ public class SettingsBackupAgent extends BackupAgentHelper { if (nBytes > buffer.length) buffer = new byte[nBytes]; in.readFully(buffer, 0, nBytes); mSettingsHelper - .setLocaleData( - buffer, nBytes, mBackupRestoreEventLogger, KEY_LOCALE); + .setLocaleData(buffer, nBytes); // Restore older backups performing the necessary migrations. if (version < FULL_BACKUP_ADDED_WIFI_NEW) { diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupRestoreKeys.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupRestoreKeys.java new file mode 100644 index 000000000000..745c2fb5409d --- /dev/null +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsBackupRestoreKeys.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2019 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.providers.settings; + +import android.net.Uri; +import android.provider.Settings; + +/** + * Class to store the keys used for backup and restore. + */ +final class SettingsBackupRestoreKeys { + static final String KEY_UNKNOWN = "unknown"; + static final String KEY_SYSTEM = "system"; + static final String KEY_SECURE = "secure"; + static final String KEY_GLOBAL = "global"; + static final String KEY_LOCALE = "locale"; + static final String KEY_LOCK_SETTINGS = "lock_settings"; + static final String KEY_SOFTAP_CONFIG = "softap_config"; + static final String KEY_NETWORK_POLICIES = "network_policies"; + static final String KEY_WIFI_NEW_CONFIG = "wifi_new_config"; + static final String KEY_DEVICE_SPECIFIC_CONFIG = "device_specific_config"; + static final String KEY_SIM_SPECIFIC_SETTINGS = "sim_specific_settings"; + // Restoring sim-specific data backed up from newer Android version to Android 12 was causing a + // fatal crash. Creating a backup with a different key will prevent Android 12 versions from + // restoring this data. + static final String KEY_SIM_SPECIFIC_SETTINGS_2 = "sim_specific_settings_2"; + static final String KEY_WIFI_SETTINGS_BACKUP_DATA = "wifi_settings_backup_data"; + + /** + * Returns the key corresponding to the given URI. + * + * @param uri The URI of the setting's destination. + * @return The key corresponding to the given URI, or KEY_UNKNOWN if the URI is not recognized. + */ + static String getKeyFromUri(Uri uri) { + if (uri.equals(Settings.Secure.CONTENT_URI)) { + return KEY_SECURE; + } else if (uri.equals(Settings.System.CONTENT_URI)) { + return KEY_SYSTEM; + } else if (uri.equals(Settings.Global.CONTENT_URI)) { + return KEY_GLOBAL; + } else { + return KEY_UNKNOWN; + } + } + +}
\ No newline at end of file diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java index 924c151a99a0..ab8d739feb43 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java @@ -17,6 +17,7 @@ package com.android.providers.settings; import android.annotation.NonNull; +import android.annotation.Nullable; import android.app.ActivityManager; import android.app.IActivityManager; import android.app.backup.BackupRestoreEventLogger; @@ -84,6 +85,7 @@ public class SettingsHelper { private Context mContext; private AudioManager mAudioManager; private TelephonyManager mTelephonyManager; + @Nullable private BackupRestoreEventLogger mBackupRestoreEventLogger; /** * A few settings elements are special in that a restore of those values needs to @@ -741,11 +743,8 @@ public class SettingsHelper { * * @param data the comma separated BCP-47 language tags in bytes. * @param size the size of the data in bytes. - * @param backupRestoreEventLogger the logger to log the restore event. - * @param dataType the data type of the setting for logging purposes. */ - /* package */ void setLocaleData( - byte[] data, int size, BackupRestoreEventLogger backupRestoreEventLogger, String dataType) { + /* package */ void setLocaleData(byte[] data, int size) { final Configuration conf = mContext.getResources().getConfiguration(); // Replace "_" with "-" to deal with older backups. @@ -772,15 +771,15 @@ public class SettingsHelper { am.updatePersistentConfigurationWithAttribution(config, mContext.getOpPackageName(), mContext.getAttributionTag()); - if (Flags.enableMetricsSettingsBackupAgents()) { - backupRestoreEventLogger - .logItemsRestored(dataType, localeList.size()); + if (Flags.enableMetricsSettingsBackupAgents() && mBackupRestoreEventLogger != null) { + mBackupRestoreEventLogger + .logItemsRestored(SettingsBackupRestoreKeys.KEY_LOCALE, localeList.size()); } } catch (RemoteException e) { - if (Flags.enableMetricsSettingsBackupAgents()) { - backupRestoreEventLogger + if (Flags.enableMetricsSettingsBackupAgents() && mBackupRestoreEventLogger != null) { + mBackupRestoreEventLogger .logItemsRestoreFailed( - dataType, + SettingsBackupRestoreKeys.KEY_LOCALE, localeList.size(), ERROR_REMOTE_EXCEPTION_SETTING_LOCALE_DATA); } @@ -795,4 +794,13 @@ public class SettingsHelper { AudioManager am = new AudioManager(mContext); am.reloadAudioSettings(); } + + /** + * Sets the backup restore event logger. + * + * @param backupRestoreEventLogger the logger to log B&R metrics. + */ + void setBackupRestoreEventLogger(BackupRestoreEventLogger backupRestoreEventLogger) { + mBackupRestoreEventLogger = backupRestoreEventLogger; + } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 55f48e3e367f..f1f03c31f718 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -120,6 +120,7 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.accessibility.util.AccessibilityUtils; import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.PackageMonitor; import com.android.internal.display.RefreshRateSettingsUtils; import com.android.internal.os.BackgroundThread; @@ -2914,6 +2915,14 @@ public class SettingsProvider extends ContentProvider { }; } + @VisibleForTesting + void injectServices(UserManager userManager, IPackageManager packageManager, + SystemConfigManager sysConfigManager) { + mUserManager = userManager; + mPackageManager = packageManager; + mSysConfigManager = sysConfigManager; + } + private static final class Arguments { private static final Pattern WHERE_PATTERN_WITH_PARAM_NO_BRACKETS = Pattern.compile("[\\s]*name[\\s]*=[\\s]*\\?[\\s]*"); @@ -3080,6 +3089,7 @@ public class SettingsProvider extends ContentProvider { private static final String SSAID_USER_KEY = "userkey"; + @GuardedBy("mLock") private final SparseArray<SettingsState> mSettingsStates = new SparseArray<>(); private GenerationRegistry mGenerationRegistry; @@ -3992,6 +4002,14 @@ public class SettingsProvider extends ContentProvider { } } + @VisibleForTesting + void injectSettings(SettingsState settings, int type, int userId) { + int key = makeKey(type, userId); + synchronized (mLock) { + mSettingsStates.put(key, settings); + } + } + private final class MyHandler extends Handler { private static final int MSG_NOTIFY_URI_CHANGED = 1; private static final int MSG_NOTIFY_DATA_CHANGED = 2; @@ -4023,12 +4041,21 @@ public class SettingsProvider extends ContentProvider { } } - private final class UpgradeController { + @VisibleForTesting + final class UpgradeController { private static final int SETTINGS_VERSION = 226; private final int mUserId; + private final Injector mInjector; + public UpgradeController(int userId) { + this(/* injector= */ null, userId); + } + + @VisibleForTesting + UpgradeController(Injector injector, int userId) { + mInjector = injector == null ? new Injector() : injector; mUserId = userId; } @@ -6136,8 +6163,8 @@ public class SettingsProvider extends ContentProvider { systemSettings.getSettingLocked(Settings.System.PEAK_REFRESH_RATE); final Setting minRefreshRateSetting = systemSettings.getSettingLocked(Settings.System.MIN_REFRESH_RATE); - float highestRefreshRate = RefreshRateSettingsUtils - .findHighestRefreshRateForDefaultDisplay(getContext()); + float highestRefreshRate = + mInjector.findHighestRefreshRateForDefaultDisplay(getContext()); if (!peakRefreshRateSetting.isNull()) { try { @@ -6318,6 +6345,14 @@ public class SettingsProvider extends ContentProvider { private long getBitMask(int capability) { return 1 << (capability - 1); } + + @VisibleForTesting + static class Injector { + float findHighestRefreshRateForDefaultDisplay(Context context) { + return RefreshRateSettingsUtils.findHighestRefreshRateForDefaultDisplay( + context); + } + } } /** diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java index 5cd534e62ea9..bf3afeda448e 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsState.java @@ -107,7 +107,7 @@ import static com.android.aconfig_new_storage.Flags.enableAconfigStorageDaemon; * the same lock to grab the current state to write to disk. * </p> */ -final class SettingsState { +public class SettingsState { private static final boolean DEBUG = false; private static final boolean DEBUG_PERSISTENCE = false; @@ -1838,7 +1838,7 @@ final class SettingsState { } } - class Setting { + public class Setting { private String name; private String value; private String defaultValue; diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupRestoreKeysTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupRestoreKeysTest.java new file mode 100644 index 000000000000..ef537e8c7fc0 --- /dev/null +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsBackupRestoreKeysTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2019 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.providers.settings; + +import static com.google.common.truth.Truth.assertThat; + +import android.net.Uri; +import android.provider.Settings; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Tests for {@link SettingsBackupRestoreKeys}. + */ +@RunWith(AndroidJUnit4.class) +public class SettingsBackupRestoreKeysTest { + + @Test + public void getKeyFromUri_secureUri_returnsSecureKey() { + assertThat(SettingsBackupRestoreKeys.getKeyFromUri(Settings.Secure.CONTENT_URI)) + .isEqualTo(SettingsBackupRestoreKeys.KEY_SECURE); + } + + @Test + public void getKeyFromUri_systemUri_returnsSystemKey() { + assertThat(SettingsBackupRestoreKeys.getKeyFromUri(Settings.System.CONTENT_URI)) + .isEqualTo(SettingsBackupRestoreKeys.KEY_SYSTEM); + } + + @Test + public void getKeyFromUri_globalUri_returnsGlobalKey() { + assertThat(SettingsBackupRestoreKeys.getKeyFromUri(Settings.Global.CONTENT_URI)) + .isEqualTo(SettingsBackupRestoreKeys.KEY_GLOBAL); + } + + @Test + public void getKeyFromUri_unknownUri_returnsUnknownKey() { + assertThat(SettingsBackupRestoreKeys.getKeyFromUri(Uri.parse("content://unknown"))) + .isEqualTo(SettingsBackupRestoreKeys.KEY_UNKNOWN); + } +}
\ No newline at end of file diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderMultiUsersTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderMultiUsersTest.java index 9cce43160b52..119b2870b622 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderMultiUsersTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsProviderMultiUsersTest.java @@ -16,7 +16,7 @@ package com.android.providers.settings; -import static android.provider.Settings.Secure.ACCESSIBILITY_ENABLED; +import static android.provider.Settings.Secure.CONTENT_CAPTURE_ENABLED; import static android.provider.Settings.Secure.SYNC_PARENT_SOUNDS; import static android.provider.Settings.System.RINGTONE; @@ -67,7 +67,7 @@ public class SettingsProviderMultiUsersTest { private static final String SPACE_SYSTEM = "system"; private static final String SPACE_SECURE = "secure"; - private static final String CLONE_TO_MANAGED_PROFILE_SETTING = ACCESSIBILITY_ENABLED; + private static final String CLONE_TO_MANAGED_PROFILE_SETTING = CONTENT_CAPTURE_ENABLED; private static final String CLONE_FROM_PARENT_SETTINGS = RINGTONE; private static final String SYNC_FROM_PARENT_SETTINGS = SYNC_PARENT_SOUNDS; diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/UpgradeControllerTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/UpgradeControllerTest.java new file mode 100644 index 000000000000..26ff376f828e --- /dev/null +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/UpgradeControllerTest.java @@ -0,0 +1,175 @@ +/* + * 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.providers.settings; + +import static android.provider.Settings.System.MIN_REFRESH_RATE; +import static android.provider.Settings.System.PEAK_REFRESH_RATE; + +import static com.android.providers.settings.SettingsProvider.SETTINGS_TYPE_GLOBAL; +import static com.android.providers.settings.SettingsProvider.SETTINGS_TYPE_SECURE; +import static com.android.providers.settings.SettingsProvider.SETTINGS_TYPE_SYSTEM; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.ContextWrapper; +import android.content.pm.IPackageManager; +import android.os.Looper; +import android.os.SystemConfigManager; +import android.os.UserHandle; +import android.os.UserManager; + +import androidx.test.core.app.ApplicationProvider; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class UpgradeControllerTest { + private static final int USER_ID = UserHandle.USER_SYSTEM; + private static final float HIGHEST_REFRESH_RATE = 130f; + + private final Context mContext = + spy(new ContextWrapper(ApplicationProvider.getApplicationContext())); + private final SettingsProvider.SettingsRegistry.UpgradeController.Injector mInjector = + new SettingsProvider.SettingsRegistry.UpgradeController.Injector() { + @Override + float findHighestRefreshRateForDefaultDisplay(Context context) { + return HIGHEST_REFRESH_RATE; + } + }; + private final SettingsProvider mSettingsProvider = new SettingsProvider() { + @Override + public boolean onCreate() { + return true; + } + }; + private final SettingsProvider.SettingsRegistry mSettingsRegistry = + mSettingsProvider.new SettingsRegistry(Looper.getMainLooper()); + private final SettingsProvider.SettingsRegistry.UpgradeController mUpgradeController = + mSettingsRegistry.new UpgradeController(mInjector, USER_ID); + + @Mock + private UserManager mUserManager; + + @Mock + private IPackageManager mPackageManager; + + @Mock + private SystemConfigManager mSysConfigManager; + + @Mock + private SettingsState mSystemSettings; + + @Mock + private SettingsState mSecureSettings; + + @Mock + private SettingsState mGlobalSettings; + + @Mock + private SettingsState.Setting mMockSetting; + + @Mock + private SettingsState.Setting mPeakRefreshRateSetting; + + @Mock + private SettingsState.Setting mMinRefreshRateSetting; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mSettingsProvider.attachInfoForTesting(mContext, /* info= */ null); + mSettingsProvider.injectServices(mUserManager, mPackageManager, mSysConfigManager); + when(mSystemSettings.getSettingLocked(any())).thenReturn(mMockSetting); + when(mSecureSettings.getSettingLocked(any())).thenReturn(mMockSetting); + when(mGlobalSettings.getSettingLocked(any())).thenReturn(mMockSetting); + when(mMockSetting.isNull()).thenReturn(true); + when(mMockSetting.getValue()).thenReturn("0"); + + when(mSystemSettings.getSettingLocked(PEAK_REFRESH_RATE)) + .thenReturn(mPeakRefreshRateSetting); + when(mSystemSettings.getSettingLocked(MIN_REFRESH_RATE)) + .thenReturn(mMinRefreshRateSetting); + + mSettingsRegistry.injectSettings(mSystemSettings, SETTINGS_TYPE_SYSTEM, USER_ID); + mSettingsRegistry.injectSettings(mSecureSettings, SETTINGS_TYPE_SECURE, USER_ID); + mSettingsRegistry.injectSettings(mGlobalSettings, SETTINGS_TYPE_GLOBAL, USER_ID); + + // Lowest version so that all upgrades are run + when(mSecureSettings.getVersionLocked()).thenReturn(118); + } + + @Test + public void testUpgrade_refreshRateSettings_defaultValues() { + when(mPeakRefreshRateSetting.isNull()).thenReturn(true); + when(mMinRefreshRateSetting.isNull()).thenReturn(true); + + mUpgradeController.upgradeIfNeededLocked(); + + // Should remain unchanged + verify(mSystemSettings, never()).insertSettingLocked(eq(PEAK_REFRESH_RATE), + /* value= */ any(), /* tag= */ any(), /* makeDefault= */ anyBoolean(), + /* packageName= */ any()); + verify(mSystemSettings, never()).insertSettingLocked(eq(MIN_REFRESH_RATE), + /* value= */ any(), /* tag= */ any(), /* makeDefault= */ anyBoolean(), + /* packageName= */ any()); + } + + @Test + public void testUpgrade_refreshRateSettings_enabled() { + when(mPeakRefreshRateSetting.isNull()).thenReturn(false); + when(mMinRefreshRateSetting.isNull()).thenReturn(false); + when(mPeakRefreshRateSetting.getValue()).thenReturn(String.valueOf(HIGHEST_REFRESH_RATE)); + when(mMinRefreshRateSetting.getValue()).thenReturn(String.valueOf(HIGHEST_REFRESH_RATE)); + + mUpgradeController.upgradeIfNeededLocked(); + + // Highest refresh rate gets converted to infinity + verify(mSystemSettings).insertSettingLocked(eq(PEAK_REFRESH_RATE), + eq(String.valueOf(Float.POSITIVE_INFINITY)), /* tag= */ any(), + /* makeDefault= */ anyBoolean(), /* packageName= */ any()); + verify(mSystemSettings).insertSettingLocked(eq(MIN_REFRESH_RATE), + eq(String.valueOf(Float.POSITIVE_INFINITY)), /* tag= */ any(), + /* makeDefault= */ anyBoolean(), /* packageName= */ any()); + } + + @Test + public void testUpgrade_refreshRateSettings_disabled() { + when(mPeakRefreshRateSetting.isNull()).thenReturn(false); + when(mMinRefreshRateSetting.isNull()).thenReturn(false); + when(mPeakRefreshRateSetting.getValue()).thenReturn("70f"); + when(mMinRefreshRateSetting.getValue()).thenReturn("70f"); + + mUpgradeController.upgradeIfNeededLocked(); + + // Should remain unchanged + verify(mSystemSettings, never()).insertSettingLocked(eq(PEAK_REFRESH_RATE), + /* value= */ any(), /* tag= */ any(), /* makeDefault= */ anyBoolean(), + /* packageName= */ any()); + verify(mSystemSettings, never()).insertSettingLocked(eq(MIN_REFRESH_RATE), + /* value= */ any(), /* tag= */ any(), /* makeDefault= */ anyBoolean(), + /* packageName= */ any()); + } +} diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index fb4293a9b5ea..46bd88fcdc93 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -994,6 +994,9 @@ <uses-permission android:name="android.permission.ACCESS_TEXT_CLASSIFIER_BY_TYPE" android:featureFlag="android.permission.flags.text_classifier_choice_api_enabled"/> + <!-- Permission required for CTS test - CtsContentProviderMultiUserTest --> + <uses-permission android:name="android.permission.RESOLVE_COMPONENT_FOR_UID" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 3d250fd82473..0600fb3abd6f 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -85,6 +85,7 @@ filegroup { filegroup { name: "SystemUI-tests-broken-robofiles-run", srcs: [ + "tests/src/**/systemui/dreams/touch/CommunalTouchHandlerTest.java", "tests/src/**/systemui/shade/NotificationShadeWindowViewControllerTest.kt", "tests/src/**/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt", "tests/src/**/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt", @@ -418,6 +419,9 @@ android_library { "androidx.slice_slice-view", ], manifest: "AndroidManifest-res.xml", + flags_packages: [ + "com_android_systemui_flags", + ], } android_library { diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 1b1c91de1e56..5ff2d1b07347 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -496,6 +496,13 @@ flag { } flag { + name: "status_bar_popup_chips" + namespace: "systemui" + description: "Show rich ongoing processes as chips in the status bar" + bug: "372964148" +} + +flag { name: "promote_notifications_automatically" namespace: "systemui" description: "Flag to automatically turn certain notifications into promoted notifications so " @@ -1229,6 +1236,21 @@ flag { } flag { + name: "glanceable_hub_v2_resources" + namespace: "systemui" + description: "Read only flag for rolling out glanceable hub v2 resource values" + bug: "375689917" + is_fixed_read_only: true +} + +flag { + name: "glanceable_hub_back_action" + namespace: "systemui" + description: "Support back action from glanceable hub" + bug: "382771533" +} + +flag { name: "dream_overlay_updated_font" namespace: "systemui" description: "Flag to enable updated font settings for dream overlay" diff --git a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java index ca2b9578f2be..7d27a562f536 100644 --- a/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java +++ b/packages/SystemUI/animation/lib/src/com/android/systemui/animation/OriginRemoteTransition.java @@ -195,7 +195,10 @@ public class OriginRemoteTransition extends IRemoteTransition.Stub { // Create the origin leash and add to the transition root leash. mOriginLeash = new SurfaceControl.Builder().setName("OriginTransition-origin-leash").build(); - mStartTransaction + + // Create temporary transaction to build + final SurfaceControl.Transaction tmpTransaction = new SurfaceControl.Transaction(); + tmpTransaction .reparent(mOriginLeash, rootLeash) .show(mOriginLeash) .setCornerRadius(mOriginLeash, windowRadius) @@ -208,14 +211,14 @@ public class OriginRemoteTransition extends IRemoteTransition.Stub { int mode = change.getMode(); SurfaceControl leash = change.getLeash(); // Reparent leash to the transition root. - mStartTransaction.reparent(leash, rootLeash); + tmpTransaction.reparent(leash, rootLeash); if (TransitionUtil.isOpeningMode(mode)) { openingSurfaces.add(change.getLeash()); // For opening surfaces, ending bounds are base bound. Apply corner radius if // it's full screen. Rect bounds = change.getEndAbsBounds(); if (displayBounds.equals(bounds)) { - mStartTransaction + tmpTransaction .setCornerRadius(leash, windowRadius) .setWindowCrop(leash, bounds.width(), bounds.height()); } @@ -226,28 +229,53 @@ public class OriginRemoteTransition extends IRemoteTransition.Stub { // it's full screen. Rect bounds = change.getStartAbsBounds(); if (displayBounds.equals(bounds)) { - mStartTransaction + tmpTransaction .setCornerRadius(leash, windowRadius) .setWindowCrop(leash, bounds.width(), bounds.height()); } } } + if (openingSurfaces.isEmpty() && closingSurfaces.isEmpty()) { + logD("prepareUIs: no opening/closing surfaces available, nothing to prepare."); + return false; + } + // Set relative order: // ---- App1 ---- // ---- origin ---- // ---- App2 ---- + if (mIsEntry) { - mStartTransaction - .setRelativeLayer(mOriginLeash, closingSurfaces.get(0), 1) - .setRelativeLayer( - openingSurfaces.get(openingSurfaces.size() - 1), mOriginLeash, 1); + if (!closingSurfaces.isEmpty()) { + tmpTransaction + .setRelativeLayer(mOriginLeash, closingSurfaces.get(0), 1); + } else { + logW("Missing closing surface is entry transition"); + } + if (!openingSurfaces.isEmpty()) { + tmpTransaction + .setRelativeLayer( + openingSurfaces.get(openingSurfaces.size() - 1), mOriginLeash, 1); + } else { + logW("Missing opening surface is entry transition"); + } + } else { - mStartTransaction - .setRelativeLayer(mOriginLeash, openingSurfaces.get(0), 1) - .setRelativeLayer( - closingSurfaces.get(closingSurfaces.size() - 1), mOriginLeash, 1); + if (!openingSurfaces.isEmpty()) { + tmpTransaction + .setRelativeLayer(mOriginLeash, openingSurfaces.get(0), 1); + } else { + logW("Missing opening surface is exit transition"); + } + if (!closingSurfaces.isEmpty()) { + tmpTransaction.setRelativeLayer( + closingSurfaces.get(closingSurfaces.size() - 1), mOriginLeash, 1); + } else { + logW("Missing closing surface is exit transition"); + } } + mStartTransaction.merge(tmpTransaction); // Attach origin UIComponent to origin leash. mOriginTransaction = mOrigin.newTransaction(); @@ -300,6 +328,7 @@ public class OriginRemoteTransition extends IRemoteTransition.Stub { } private void cancel() { + logD("cancel()"); if (mAnimator != null) { mAnimator.cancel(); } @@ -311,6 +340,10 @@ public class OriginRemoteTransition extends IRemoteTransition.Stub { } } + private static void logW(String msg) { + Log.w(TAG, msg); + } + private static void logE(String msg) { Log.e(TAG, msg); } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt index a17a1d46554f..0a0003ee9a8a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt @@ -19,17 +19,20 @@ package com.android.systemui.communal.ui.compose import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import com.android.compose.animation.scene.SceneScope +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.communal.smartspace.SmartspaceInteractionHandler import com.android.systemui.communal.ui.compose.section.AmbientStatusBarSection import com.android.systemui.communal.ui.compose.section.CommunalPopupSection @@ -39,8 +42,11 @@ import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.keyguard.ui.composable.blueprint.BlueprintAlignmentLines import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection import com.android.systemui.keyguard.ui.composable.section.LockSection +import com.android.systemui.res.R import com.android.systemui.statusbar.phone.SystemUIDialogFactory import javax.inject.Inject +import kotlin.math.min +import kotlin.math.roundToInt /** Renders the content of the glanceable hub. */ class CommunalContent @@ -48,6 +54,7 @@ class CommunalContent constructor( private val viewModel: CommunalViewModel, private val interactionHandler: SmartspaceInteractionHandler, + private val communalSettingsInteractor: CommunalSettingsInteractor, private val dialogFactory: SystemUIDialogFactory, private val lockSection: LockSection, private val bottomAreaSection: BottomAreaSection, @@ -77,11 +84,20 @@ constructor( sceneScope = this@Content, ) } - with(lockSection) { - LockIcon( - overrideColor = MaterialTheme.colorScheme.onPrimaryContainer, + if (communalSettingsInteractor.isV2FlagEnabled()) { + Icon( + painter = painterResource(id = R.drawable.ic_lock), + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, modifier = Modifier.element(Communal.Elements.LockIcon), ) + } else { + with(lockSection) { + LockIcon( + overrideColor = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.element(Communal.Elements.LockIcon), + ) + } } with(bottomAreaSection) { IndicationArea( @@ -98,14 +114,42 @@ constructor( val noMinConstraints = constraints.copy(minWidth = 0, minHeight = 0) - val lockIconPlaceable = lockIconMeasurable.measure(noMinConstraints) + val lockIconPlaceable = + if (communalSettingsInteractor.isV2FlagEnabled()) { + val lockIconSizeInt = lockIconSize.roundToPx() + lockIconMeasurable.measure( + Constraints.fixed(width = lockIconSizeInt, height = lockIconSizeInt) + ) + } else { + lockIconMeasurable.measure(noMinConstraints) + } val lockIconBounds = - IntRect( - left = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Left], - top = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Top], - right = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Right], - bottom = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Bottom], - ) + if (communalSettingsInteractor.isV2FlagEnabled()) { + val lockIconDistanceFromBottom = + min( + (constraints.maxHeight * lockIconPercentDistanceFromBottom) + .roundToInt(), + lockIconMinDistanceFromBottom.roundToPx(), + ) + val x = constraints.maxWidth / 2 - lockIconPlaceable.width / 2 + val y = + constraints.maxHeight - + lockIconDistanceFromBottom - + lockIconPlaceable.height + IntRect( + left = x, + top = y, + right = x + lockIconPlaceable.width, + bottom = y + lockIconPlaceable.height, + ) + } else { + IntRect( + left = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Left], + top = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Top], + right = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Right], + bottom = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Bottom], + ) + } val bottomAreaPlaceable = bottomAreaMeasurable.measure(noMinConstraints) @@ -129,12 +173,17 @@ constructor( val bottomAreaTop = constraints.maxHeight - bottomAreaPlaceable.height bottomAreaPlaceable.place(x = 0, y = bottomAreaTop) + + val screensaverButtonPaddingInt = screensaverButtonPadding.roundToPx() screensaverButtonPlaceable?.place( x = constraints.maxWidth - screensaverButtonSizeInt - - Dimensions.ItemSpacing.roundToPx(), - y = lockIconBounds.top, + screensaverButtonPaddingInt, + y = + constraints.maxHeight - + screensaverButtonSizeInt - + screensaverButtonPaddingInt, ) } } @@ -142,6 +191,12 @@ constructor( } companion object { - val screensaverButtonSize: Dp = 64.dp + private val screensaverButtonSize: Dp = 64.dp + private val screensaverButtonPadding: Dp = 24.dp + // TODO(b/382739998): Remove these hardcoded values once lock icon size and bottom area + // position are sorted. + private val lockIconSize: Dp = 54.dp + private val lockIconPercentDistanceFromBottom = 0.1f + private val lockIconMinDistanceFromBottom = 70.dp } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 5dbedc7045e4..bf3360f0ea14 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -931,7 +931,9 @@ private fun BoxScope.CommunalHubLazyGrid( Modifier.requiredSize(dpSize) .thenIf(!isItemDragging) { Modifier.animateItem( - placementSpec = spring(stiffness = Spring.StiffnessMediumLow) + placementSpec = spring(stiffness = Spring.StiffnessMediumLow), + // See b/376495198 - not supported with AndroidView + fadeOutSpec = null, ) } .thenIf(isItemDragging) { Modifier.zIndex(1f) }, @@ -980,11 +982,14 @@ private fun BoxScope.CommunalHubLazyGrid( size = size, selected = false, modifier = - Modifier.requiredSize(dpSize).animateItem().thenIf( - communalResponsiveGrid() - ) { - Modifier.graphicsLayer { alpha = itemAlpha?.value ?: 1f } - }, + Modifier.requiredSize(dpSize) + .animateItem( + // See b/376495198 - not supported with AndroidView + fadeOutSpec = null + ) + .thenIf(communalResponsiveGrid()) { + Modifier.graphicsLayer { alpha = itemAlpha?.value ?: 1f } + }, index = index, contentListState = contentListState, interactionHandler = interactionHandler, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt index 5c7ca97474b7..0344ab8e0196 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt @@ -38,7 +38,6 @@ import com.android.compose.animation.scene.SceneScope import com.android.compose.modifiers.thenIf import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn import com.android.systemui.keyguard.ui.composable.modifier.burnInAware @@ -90,14 +89,10 @@ constructor( ) { init { - if (!MigrateClocksToBlueprint.isEnabled) { - throw IllegalStateException("this requires MigrateClocksToBlueprint.isEnabled") - } // This scene container section moves the NSSL to the SharedNotificationContainer. // This also requires that SharedNotificationContainer gets moved to the // SceneWindowRootView by the SceneWindowRootViewBinder. Prior to Scene Container, - // but when the KeyguardShadeMigrationNssl flag is enabled, NSSL is moved into this - // container by the NotificationStackScrollLayoutSection. + // NSSL is moved into this container by the NotificationStackScrollLayoutSection. // Ensure stackScrollLayout is a child of sharedNotificationContainer. if (stackScrollLayout.parent != sharedNotificationContainer) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt index db33e7c628d7..79cf24b9c547 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt @@ -35,9 +35,8 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.MutableSceneTransitionLayoutState -import com.android.compose.animation.scene.SceneScope -import com.android.compose.animation.scene.SceneTransitionLayout import com.android.compose.modifiers.thenIf import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.composable.blueprint.ClockScenes.largeClockScene @@ -61,7 +60,7 @@ constructor( private val clockInteractor: KeyguardClockInteractor, ) { @Composable - fun SceneScope.DefaultClockLayout( + fun ContentScope.DefaultClockLayout( smartSpacePaddingTop: (Resources) -> Int, isShadeLayoutWide: Boolean, modifier: Modifier = Modifier, @@ -95,7 +94,7 @@ constructor( } Column(modifier) { - SceneTransitionLayout(state) { + NestedSceneTransitionLayout(state, Modifier) { scene(splitShadeLargeClockScene) { LargeClockWithSmartSpace( smartSpacePaddingTop = smartSpacePaddingTop, @@ -134,7 +133,7 @@ constructor( } @Composable - private fun SceneScope.SmallClockWithSmartSpace( + private fun ContentScope.SmallClockWithSmartSpace( smartSpacePaddingTop: (Resources) -> Int, modifier: Modifier = Modifier, ) { @@ -159,7 +158,7 @@ constructor( } @Composable - private fun SceneScope.LargeClockWithSmartSpace( + private fun ContentScope.LargeClockWithSmartSpace( smartSpacePaddingTop: (Resources) -> Int, shouldOffSetClockToOneHalf: Boolean = false, ) { @@ -200,7 +199,7 @@ constructor( } @Composable - private fun SceneScope.WeatherLargeClockWithSmartSpace( + private fun ContentScope.WeatherLargeClockWithSmartSpace( smartSpacePaddingTop: (Resources) -> Int, modifier: Modifier = Modifier, ) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt index 2af5ffaee7ed..5790c4af0d77 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeOverlay.kt @@ -19,6 +19,7 @@ package com.android.systemui.notifications.ui.composable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.layoutId import com.android.compose.animation.scene.ContentScope @@ -84,7 +85,11 @@ constructor( viewModel.notificationsPlaceholderViewModelFactory.create() } - OverlayShade(modifier = modifier, onScrimClicked = viewModel::onScrimClicked) { + OverlayShade( + panelAlignment = Alignment.TopStart, + modifier = modifier, + onScrimClicked = viewModel::onScrimClicked, + ) { Column { if (viewModel.showHeader) { val burnIn = rememberBurnIn(clockInteractor) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt index b1a19456ab7d..f6c5f588aa95 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeOverlay.kt @@ -99,7 +99,11 @@ constructor( val viewModel = rememberViewModel("QuickSettingsShadeOverlay") { contentViewModelFactory.create() } - OverlayShade(modifier = modifier, onScrimClicked = viewModel::onScrimClicked) { + OverlayShade( + panelAlignment = Alignment.TopEnd, + modifier = modifier, + onScrimClicked = viewModel::onScrimClicked, + ) { Column { ExpandedShadeHeader( viewModelFactory = viewModel.shadeHeaderViewModelFactory, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt index 55fafd5cfeca..8907aec7fd48 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainerTransitions.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.gestures.Orientation import com.android.compose.animation.scene.ProgressConverter import com.android.compose.animation.scene.TransitionKey import com.android.compose.animation.scene.transitions -import com.android.systemui.bouncer.ui.composable.Bouncer import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes @@ -110,17 +109,13 @@ val SceneContainerTransitions = transitions { // Overlay transitions - // TODO(b/376659778): Remove this transition once nested STLs are supported. - from(Scenes.Gone, to = Overlays.NotificationsShade) { - toNotificationsShadeTransition(translateClock = true) - } to(Overlays.NotificationsShade) { toNotificationsShadeTransition() } to(Overlays.QuickSettingsShade) { toQuickSettingsShadeTransition() } from(Overlays.NotificationsShade, to = Overlays.QuickSettingsShade) { notificationsShadeToQuickSettingsShadeTransition() } from(Scenes.Gone, to = Overlays.NotificationsShade, key = SlightlyFasterShadeCollapse) { - toNotificationsShadeTransition(translateClock = true, durationScale = 0.9) + toNotificationsShadeTransition(durationScale = 0.9) } from(Scenes.Gone, to = Overlays.QuickSettingsShade, key = SlightlyFasterShadeCollapse) { toQuickSettingsShadeTransition(durationScale = 0.9) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt index 6bdb36331709..3d62151baf2f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/ToNotificationsShadeTransition.kt @@ -29,10 +29,7 @@ import com.android.systemui.shade.ui.composable.OverlayShade import com.android.systemui.shade.ui.composable.Shade import kotlin.time.Duration.Companion.milliseconds -fun TransitionBuilder.toNotificationsShadeTransition( - translateClock: Boolean = false, - durationScale: Double = 1.0, -) { +fun TransitionBuilder.toNotificationsShadeTransition(durationScale: Double = 1.0) { spec = tween(durationMillis = (DefaultDuration * durationScale).inWholeMilliseconds.toInt()) swipeSpec = spring( @@ -45,11 +42,6 @@ fun TransitionBuilder.toNotificationsShadeTransition( elevateInContent = Overlays.NotificationsShade, ) scaleSize(OverlayShade.Elements.Panel, height = 0f) - // TODO(b/376659778): This is a temporary hack to have a shared element transition with the - // lockscreen clock. Remove once nested STLs are supported. - if (!translateClock) { - translate(ClockElementKeys.smallClockElementKey) - } // Avoid translating the status bar with the shade panel. translate(NotificationsShade.Elements.StatusBar) // Slide in the shade panel from the top edge. diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt index 8a5c96da5ac6..cfbe6671db02 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt @@ -20,6 +20,9 @@ package com.android.systemui.shade.ui.composable import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.rememberScrollableState +import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.PaddingValues @@ -41,30 +44,51 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp +import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.LowestZIndexContentPicker -import com.android.compose.animation.scene.SceneScope +import com.android.compose.animation.scene.effect.rememberOffsetOverscrollEffect import com.android.compose.windowsizeclass.LocalWindowSizeClass import com.android.systemui.res.R /** Renders a lightweight shade UI container, as an overlay. */ @Composable -fun SceneScope.OverlayShade( +fun ContentScope.OverlayShade( + panelAlignment: Alignment, onScrimClicked: () -> Unit, modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { - Box(modifier) { + // TODO(b/384653288) This should be removed when b/378470603 is done. + val idleEffect = rememberOffsetOverscrollEffect(Orientation.Vertical) + Box( + modifier + .overscroll(idleEffect) + .nestedScroll( + remember { + object : NestedScrollConnection { + override suspend fun onPreFling(available: Velocity): Velocity { + return available + } + } + } + ) + .scrollable(rememberScrollableState { 0f }, Orientation.Vertical, idleEffect) + ) { Scrim(onClicked = onScrimClicked) - Box(modifier = Modifier.fillMaxSize().panelPadding(), contentAlignment = Alignment.TopEnd) { + Box(modifier = Modifier.fillMaxSize().panelPadding(), contentAlignment = panelAlignment) { Panel( modifier = Modifier.element(OverlayShade.Elements.Panel) @@ -77,7 +101,7 @@ fun SceneScope.OverlayShade( } @Composable -private fun SceneScope.Scrim(onClicked: () -> Unit, modifier: Modifier = Modifier) { +private fun ContentScope.Scrim(onClicked: () -> Unit, modifier: Modifier = Modifier) { Spacer( modifier = modifier @@ -89,7 +113,7 @@ private fun SceneScope.Scrim(onClicked: () -> Unit, modifier: Modifier = Modifie } @Composable -private fun SceneScope.Panel(modifier: Modifier = Modifier, content: @Composable () -> Unit) { +private fun ContentScope.Panel(modifier: Modifier = Modifier, content: @Composable () -> Unit) { Box(modifier = modifier.clip(OverlayShade.Shapes.RoundedCornerPanel)) { Spacer( modifier = @@ -180,7 +204,6 @@ object OverlayShade { object Dimensions { val PanelCornerRadius = 46.dp - val OverscrollLimit = 32.dp } object Shapes { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt index fa5f72bc0997..1480db9de701 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt @@ -37,12 +37,14 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.semantics.CustomAccessibilityAction @@ -66,6 +68,10 @@ import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState +import kotlin.math.round +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map @Composable fun VolumeSlider( @@ -196,9 +202,17 @@ private fun LegacyVolumeSlider( ) } } - - // Perform haptics due to UI composition - hapticsViewModel?.onValueChange(value) + var lastDiscreteStep by remember { mutableFloatStateOf(round(value)) } + LaunchedEffect(value) { + snapshotFlow { value } + .map { round(it) } + .filter { it != lastDiscreteStep } + .distinctUntilChanged() + .collect { discreteStep -> + lastDiscreteStep = discreteStep + hapticsViewModel?.onValueChange(discreteStep) + } + } PlatformSlider( modifier = diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SharedElement.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SharedElement.kt index 599a152a23bd..167928b38e90 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SharedElement.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SharedElement.kt @@ -30,7 +30,8 @@ internal fun Element.shouldBeRenderedBy(content: ContentKey): Boolean { // the transition is running. If the [renderAuthority.size] is 1 it means that that this element // is currently composed only in one nesting level, which means that the render authority // is determined by "classic" shared element code. - return renderAuthority.size == 1 || renderAuthority.first() == content + return renderAuthority.size > 0 && + (renderAuthority.size == 1 || renderAuthority.first() == content) } /** diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt index 801a2d6170cc..b76656d78cc4 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt @@ -71,7 +71,6 @@ constructor( } var hasCustomPositionUpdatedAnimation: Boolean = false - var migratedClocks: Boolean = false private val time = Calendar.getInstance() @@ -228,11 +227,7 @@ constructor( override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { logger.d("onMeasure") - if ( - migratedClocks && - !isSingleLineInternal && - MeasureSpec.getMode(heightMeasureSpec) == EXACTLY - ) { + if (!isSingleLineInternal && MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) { // Call straight into TextView.setTextSize to avoid setting lastUnconstrainedTextSize val size = min(lastUnconstrainedTextSize, MeasureSpec.getSize(heightMeasureSpec) / 2F) super.setTextSize(COMPLEX_UNIT_PX, size) @@ -248,7 +243,7 @@ constructor( } } - if (migratedClocks && hasCustomPositionUpdatedAnimation) { + if (hasCustomPositionUpdatedAnimation) { // Expand width to avoid clock being clipped during stepping animation val targetWidth = measuredWidth + MeasureSpec.getSize(widthMeasureSpec) / 2 @@ -582,12 +577,10 @@ constructor( } override fun onRtlPropertiesChanged(layoutDirection: Int) { - if (migratedClocks) { - if (layoutDirection == LAYOUT_DIRECTION_RTL) { - textAlignment = TEXT_ALIGNMENT_TEXT_END - } else { - textAlignment = TEXT_ALIGNMENT_TEXT_START - } + if (layoutDirection == LAYOUT_DIRECTION_RTL) { + textAlignment = TEXT_ALIGNMENT_TEXT_END + } else { + textAlignment = TEXT_ALIGNMENT_TEXT_START } super.onRtlPropertiesChanged(layoutDirection) } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt index ad9eba841c86..74d595ce65e6 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -20,7 +20,6 @@ import android.graphics.Rect import android.icu.text.NumberFormat import android.util.TypedValue import android.view.LayoutInflater -import android.view.View import android.widget.FrameLayout import androidx.annotation.VisibleForTesting import com.android.systemui.customization.R @@ -55,7 +54,6 @@ class DefaultClockController( private val layoutInflater: LayoutInflater, private val resources: Resources, private val settings: ClockSettings?, - private val migratedClocks: Boolean = false, messageBuffers: ClockMessageBuffers? = null, ) : ClockController { override val smallClock: DefaultClockFaceController @@ -67,7 +65,6 @@ class DefaultClockController( private val burmeseLineSpacing = resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale_burmese) private val defaultLineSpacing = resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale) - protected var onSecondaryDisplay: Boolean = false override val events: DefaultClockEvents override val config: ClockConfig by lazy { @@ -175,10 +172,7 @@ class DefaultClockController( recomputePadding(targetRegion) } - override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) { - this@DefaultClockController.onSecondaryDisplay = onSecondaryDisplay - recomputePadding(null) - } + override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {} } open fun recomputePadding(targetRegion: Rect?) {} @@ -197,32 +191,11 @@ class DefaultClockController( override val config = ClockFaceConfig(hasCustomPositionUpdatedAnimation = true) init { - view.migratedClocks = migratedClocks view.hasCustomPositionUpdatedAnimation = true animations = LargeClockAnimations(view, 0f, 0f) } - override fun recomputePadding(targetRegion: Rect?) { - if (migratedClocks) { - return - } - // We center the view within the targetRegion instead of within the parent - // view by computing the difference and adding that to the padding. - val lp = view.getLayoutParams() as FrameLayout.LayoutParams - lp.topMargin = - if (onSecondaryDisplay) { - // On the secondary display we don't want any additional top/bottom margin. - 0 - } else { - val parent = view.parent - val yDiff = - if (targetRegion != null && parent is View && parent.isLaidOut()) - targetRegion.centerY() - parent.height / 2f - else 0f - (-0.5f * view.bottom + yDiff).toInt() - } - view.setLayoutParams(lp) - } + override fun recomputePadding(targetRegion: Rect?) {} /** See documentation at [AnimatableClockView.offsetGlyphsForStepClockAnimation]. */ fun offsetGlyphsForStepClockAnimation(fromLeft: Int, direction: Int, fraction: Float) { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt index e8987257bb47..c73e1c33f88a 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt @@ -47,7 +47,6 @@ class DefaultClockProvider( val ctx: Context, val layoutInflater: LayoutInflater, val resources: Resources, - private val migratedClocks: Boolean = false, private val isClockReactiveVariantsEnabled: Boolean = false, ) : ClockProvider { private var messageBuffers: ClockMessageBuffers? = null @@ -83,14 +82,7 @@ class DefaultClockProvider( FLEX_DESIGN, ) } else { - DefaultClockController( - ctx, - layoutInflater, - resources, - settings, - migratedClocks, - messageBuffers, - ) + DefaultClockController(ctx, layoutInflater, resources, settings, messageBuffers) } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt index 21d41ae744a7..4a47f1bc12bf 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt @@ -140,8 +140,8 @@ class FlexClockFaceController( } /** - * targetRegion passed to all customized clock applies counter translationY of - * KeyguardStatusView and keyguard_large_clock_top_margin from default clock + * targetRegion passed to all customized clock applies counter translationY of Keyguard and + * keyguard_large_clock_top_margin from default clock */ override fun onTargetRegionChanged(targetRegion: Rect?) { // When a clock needs to be aligned with screen, like weather clock diff --git a/packages/SystemUI/docs/clock-plugins.md b/packages/SystemUI/docs/clock-plugins.md index fee82dfcf2e3..813038ee81ec 100644 --- a/packages/SystemUI/docs/clock-plugins.md +++ b/packages/SystemUI/docs/clock-plugins.md @@ -43,12 +43,6 @@ present in the source tree, although it will likely be removed in a later patch. SystemUI event dispatchers to the clock controllers. It maintains a set of event listeners, but otherwise attempts to do as little work as possible. It does maintain some state where necessary. -[KeyguardClockSwitchController](../src/com/android/keyguard/KeyguardClockSwitchController.java) is -the primary controller for the [KeyguardClockSwitch](../src/com/android/keyguard/KeyguardClockSwitch.java), -which serves as the view parent within SystemUI. Together they ensure the correct clock (either -large or small) is shown, handle animation between clock sizes, and control some sizing/layout -parameters for the clocks. - ### Creating a custom clock In order to create a custom clock, a partner must: - Write an implementation of ClockProviderPlugin and the subinterfaces relevant to your use-case. diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.kt index 85bdf9264467..cea1e9600741 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardDisplayManagerTest.kt @@ -163,6 +163,16 @@ class KeyguardDisplayManagerTest : SysuiTestCase() { } @Test + fun testShow_rearDisplayOuterDefaultActive_occluded() { + displayTracker.allDisplays = arrayOf(defaultDisplay, secondaryDisplay) + + whenever(deviceStateHelper.isRearDisplayOuterDefaultActive(secondaryDisplay)) + .thenReturn(true) + whenever(keyguardStateController.isOccluded).thenReturn(true) + verify(presentationFactory, never()).create(eq(secondaryDisplay)) + } + + @Test fun testShow_presentationCreated() { displayTracker.allDisplays = arrayOf(defaultDisplay, secondaryDisplay) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeLayoutTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeLayoutTest.java new file mode 100644 index 000000000000..455329f54864 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeLayoutTest.java @@ -0,0 +1,221 @@ +/* + * 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.accessibility.hearingaid; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT; +import static com.android.systemui.accessibility.hearingaid.AmbientVolumeLayout.ROTATION_COLLAPSED; +import static com.android.systemui.accessibility.hearingaid.AmbientVolumeLayout.ROTATION_EXPANDED; +import static com.android.systemui.accessibility.hearingaid.AmbientVolumeLayout.SIDE_UNIFIED; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.mock; + +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.util.ArrayMap; +import android.view.View; +import android.widget.ImageView; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.settingslib.bluetooth.AmbientVolumeUi; +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.Map; + +/** Tests for {@link AmbientVolumeLayout}. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class AmbientVolumeLayoutTest extends SysuiTestCase { + + private static final int TEST_LEFT_VOLUME_LEVEL = 1; + private static final int TEST_RIGHT_VOLUME_LEVEL = 2; + private static final int TEST_UNIFIED_VOLUME_LEVEL = 3; + + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Spy + private Context mContext = ApplicationProvider.getApplicationContext(); + @Mock + private AmbientVolumeUi.AmbientVolumeUiListener mListener; + + private AmbientVolumeLayout mLayout; + private ImageView mExpandIcon; + private ImageView mVolumeIcon; + private final Map<Integer, BluetoothDevice> mSideToDeviceMap = new ArrayMap<>(); + + @Before + public void setUp() { + mLayout = new AmbientVolumeLayout(mContext); + mLayout.setListener(mListener); + mLayout.setExpandable(true); + mLayout.setMutable(true); + + prepareDevices(); + mLayout.setupSliders(mSideToDeviceMap); + mLayout.getSliders().forEach((side, slider) -> { + slider.setMin(0); + slider.setMax(4); + if (side == SIDE_LEFT) { + slider.setValue(TEST_LEFT_VOLUME_LEVEL); + } else if (side == SIDE_RIGHT) { + slider.setValue(TEST_RIGHT_VOLUME_LEVEL); + } else if (side == SIDE_UNIFIED) { + slider.setValue(TEST_UNIFIED_VOLUME_LEVEL); + } + }); + + mExpandIcon = mLayout.getExpandIcon(); + mVolumeIcon = mLayout.getVolumeIcon(); + } + + @Test + public void setExpandable_expandable_expandIconVisible() { + mLayout.setExpandable(true); + + assertThat(mExpandIcon.getVisibility()).isEqualTo(VISIBLE); + } + + @Test + public void setExpandable_notExpandable_expandIconGone() { + mLayout.setExpandable(false); + + assertThat(mExpandIcon.getVisibility()).isEqualTo(View.GONE); + } + + @Test + public void setExpanded_expanded_assertControlUiCorrect() { + mLayout.setExpanded(true); + + assertControlUiCorrect(); + } + + @Test + public void setExpanded_notExpanded_assertControlUiCorrect() { + mLayout.setExpanded(false); + + assertControlUiCorrect(); + } + + @Test + public void setMutable_mutable_clickOnMuteIconChangeMuteState() { + mLayout.setMutable(true); + mLayout.setMuted(false); + + mVolumeIcon.callOnClick(); + + assertThat(mLayout.isMuted()).isTrue(); + } + + @Test + public void setMutable_notMutable_clickOnMuteIconWontChangeMuteState() { + mLayout.setMutable(false); + mLayout.setMuted(false); + + mVolumeIcon.callOnClick(); + + assertThat(mLayout.isMuted()).isFalse(); + } + + @Test + public void updateLayout_mute_volumeIconIsCorrect() { + mLayout.setMuted(true); + mLayout.updateLayout(); + + assertThat(mVolumeIcon.getDrawable().getLevel()).isEqualTo(0); + } + + @Test + public void updateLayout_unmuteAndExpanded_volumeIconIsCorrect() { + mLayout.setMuted(false); + mLayout.setExpanded(true); + mLayout.updateLayout(); + + int expectedLevel = calculateVolumeLevel(TEST_LEFT_VOLUME_LEVEL, TEST_RIGHT_VOLUME_LEVEL); + assertThat(mVolumeIcon.getDrawable().getLevel()).isEqualTo(expectedLevel); + } + + @Test + public void updateLayout_unmuteAndNotExpanded_volumeIconIsCorrect() { + mLayout.setMuted(false); + mLayout.setExpanded(false); + mLayout.updateLayout(); + + int expectedLevel = calculateVolumeLevel(TEST_UNIFIED_VOLUME_LEVEL, + TEST_UNIFIED_VOLUME_LEVEL); + assertThat(mVolumeIcon.getDrawable().getLevel()).isEqualTo(expectedLevel); + } + + @Test + public void setSliderEnabled_expandedAndLeftIsDisabled_volumeIconIsCorrect() { + mLayout.setExpanded(true); + mLayout.setSliderEnabled(SIDE_LEFT, false); + + int expectedLevel = calculateVolumeLevel(0, TEST_RIGHT_VOLUME_LEVEL); + assertThat(mVolumeIcon.getDrawable().getLevel()).isEqualTo(expectedLevel); + } + + @Test + public void setSliderValue_expandedAndLeftValueChanged_volumeIconIsCorrect() { + mLayout.setExpanded(true); + mLayout.setSliderValue(SIDE_LEFT, 4); + + int expectedLevel = calculateVolumeLevel(4, TEST_RIGHT_VOLUME_LEVEL); + assertThat(mVolumeIcon.getDrawable().getLevel()).isEqualTo(expectedLevel); + } + + private int calculateVolumeLevel(int left, int right) { + return left * 5 + right; + } + + private void assertControlUiCorrect() { + final boolean expanded = mLayout.isExpanded(); + final Map<Integer, AmbientVolumeSlider> sliders = mLayout.getSliders(); + if (expanded) { + assertThat(sliders.get(SIDE_UNIFIED).getVisibility()).isEqualTo(GONE); + assertThat(sliders.get(SIDE_LEFT).getVisibility()).isEqualTo(VISIBLE); + assertThat(sliders.get(SIDE_RIGHT).getVisibility()).isEqualTo(VISIBLE); + assertThat(mExpandIcon.getRotation()).isEqualTo(ROTATION_EXPANDED); + } else { + assertThat(sliders.get(SIDE_UNIFIED).getVisibility()).isEqualTo(VISIBLE); + assertThat(sliders.get(SIDE_LEFT).getVisibility()).isEqualTo(GONE); + assertThat(sliders.get(SIDE_RIGHT).getVisibility()).isEqualTo(GONE); + assertThat(mExpandIcon.getRotation()).isEqualTo(ROTATION_COLLAPSED); + } + } + + private void prepareDevices() { + mSideToDeviceMap.put(SIDE_LEFT, mock(BluetoothDevice.class)); + mSideToDeviceMap.put(SIDE_RIGHT, mock(BluetoothDevice.class)); + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeSliderTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeSliderTest.java new file mode 100644 index 000000000000..78dfda88a526 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeSliderTest.java @@ -0,0 +1,91 @@ +/* + * 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.accessibility.hearingaid; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** Tests for {@link AmbientVolumeLayout}. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class AmbientVolumeSliderTest extends SysuiTestCase { + + @Rule + public final MockitoRule mMockitoRule = MockitoJUnit.rule(); + @Spy + private Context mContext = ApplicationProvider.getApplicationContext(); + + private AmbientVolumeSlider mSlider; + + @Before + public void setUp() { + mSlider = new AmbientVolumeSlider(mContext); + } + + @Test + public void setTitle_titleCorrect() { + final String testTitle = "test"; + mSlider.setTitle(testTitle); + + assertThat(mSlider.getTitle()).isEqualTo(testTitle); + } + + @Test + public void getVolumeLevel_valueMin_volumeLevelIsZero() { + prepareSlider(/* min= */ 0, /* max= */ 100, /* value= */ 0); + + // The volume level is divided into 5 levels: + // Level 0 corresponds to the minimum volume value. The range between the minimum and + // maximum volume is divided into 4 equal intervals, represented by levels 1 to 4. + assertThat(mSlider.getVolumeLevel()).isEqualTo(0); + } + + @Test + public void getVolumeLevel_valueMax_volumeLevelIsFour() { + prepareSlider(/* min= */ 0, /* max= */ 100, /* value= */ 100); + + assertThat(mSlider.getVolumeLevel()).isEqualTo(4); + } + + @Test + public void getVolumeLevel_volumeLevelIsCorrect() { + prepareSlider(/* min= */ 0, /* max= */ 100, /* value= */ 73); + + assertThat(mSlider.getVolumeLevel()).isEqualTo(3); + } + + private void prepareSlider(float min, float max, float value) { + mSlider.setMin(min); + mSlider.setMax(max); + mSlider.setValue(value); + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java index ad12c61ab5d1..43d0d69c428f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java @@ -16,8 +16,11 @@ package com.android.systemui.accessibility.hearingaid; +import static android.bluetooth.BluetoothDevice.BOND_BONDED; import static android.bluetooth.BluetoothHapClient.PRESET_INDEX_UNAVAILABLE; +import static android.bluetooth.BluetoothProfile.STATE_CONNECTED; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT; import static com.android.systemui.accessibility.hearingaid.HearingDevicesDialogDelegate.LIVE_CAPTION_INTENT; import static com.google.common.truth.Truth.assertThat; @@ -31,6 +34,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.bluetooth.AudioInputControl; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHapPresetInfo; import android.bluetooth.BluetoothProfile; @@ -61,6 +65,7 @@ import com.android.settingslib.bluetooth.HapClientProfile; import com.android.settingslib.bluetooth.LocalBluetoothAdapter; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; +import com.android.settingslib.bluetooth.VolumeControlProfile; import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.DialogTransitionAnimator; @@ -90,6 +95,7 @@ import java.util.List; @TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest public class HearingDevicesDialogDelegateTest extends SysuiTestCase { + @Rule public MockitoRule mockito = MockitoJUnit.rule(); @@ -120,6 +126,8 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { @Mock private HapClientProfile mHapClientProfile; @Mock + private VolumeControlProfile mVolumeControlProfile; + @Mock private CachedBluetoothDeviceManager mCachedDeviceManager; @Mock private BluetoothEventManager mBluetoothEventManager; @@ -151,21 +159,25 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { when(mLocalBluetoothManager.getBluetoothAdapter()).thenReturn(mLocalBluetoothAdapter); when(mLocalBluetoothManager.getProfileManager()).thenReturn(mProfileManager); when(mProfileManager.getHapClientProfile()).thenReturn(mHapClientProfile); + when(mProfileManager.getVolumeControlProfile()).thenReturn(mVolumeControlProfile); when(mLocalBluetoothAdapter.isEnabled()).thenReturn(true); when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager); when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(List.of(mCachedDevice)); when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager); when(mSysUiState.setFlag(anyLong(), anyBoolean())).thenReturn(mSysUiState); - when(mDevice.getBondState()).thenReturn(BluetoothDevice.BOND_BONDED); + when(mDevice.getBondState()).thenReturn(BOND_BONDED); when(mDevice.isConnected()).thenReturn(true); when(mCachedDevice.getDevice()).thenReturn(mDevice); when(mCachedDevice.getAddress()).thenReturn(DEVICE_ADDRESS); when(mCachedDevice.getName()).thenReturn(DEVICE_NAME); - when(mCachedDevice.getProfiles()).thenReturn(List.of(mHapClientProfile)); + when(mCachedDevice.getProfiles()).thenReturn( + List.of(mHapClientProfile, mVolumeControlProfile)); when(mCachedDevice.isActiveDevice(BluetoothProfile.HEARING_AID)).thenReturn(true); when(mCachedDevice.isConnectedHearingAidDevice()).thenReturn(true); when(mCachedDevice.isConnectedHapClientDevice()).thenReturn(true); when(mCachedDevice.getDrawableWithDescription()).thenReturn(new Pair<>(mDrawable, "")); + when(mCachedDevice.getBondState()).thenReturn(BOND_BONDED); + when(mCachedDevice.getDeviceSide()).thenReturn(SIDE_LEFT); when(mHearingDeviceItem.getCachedBluetoothDevice()).thenReturn(mCachedDevice); mContext.setMockPackageManager(mPackageManager); @@ -292,6 +304,46 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { } @Test + @EnableFlags(com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_AMBIENT_VOLUME_CONTROL) + public void showDialog_deviceNotSupportVcp_ambientLayoutGone() { + when(mCachedDevice.getProfiles()).thenReturn(List.of()); + + setUpDeviceDialogWithoutPairNewDeviceButton(); + mDialog.show(); + + ViewGroup ambientLayout = getAmbientLayout(mDialog); + assertThat(ambientLayout.getVisibility()).isEqualTo(View.GONE); + } + + @Test + @EnableFlags(com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_AMBIENT_VOLUME_CONTROL) + public void showDialog_ambientControlNotAvailable_ambientLayoutGone() { + when(mVolumeControlProfile.getAudioInputControlServices(mDevice)).thenReturn(List.of()); + + setUpDeviceDialogWithoutPairNewDeviceButton(); + mDialog.show(); + + ViewGroup ambientLayout = getAmbientLayout(mDialog); + assertThat(ambientLayout.getVisibility()).isEqualTo(View.GONE); + } + + @Test + @EnableFlags(com.android.settingslib.flags.Flags.FLAG_HEARING_DEVICES_AMBIENT_VOLUME_CONTROL) + public void showDialog_supportVcpAndAmbientControlAvailable_ambientLayoutVisible() { + when(mCachedDevice.getProfiles()).thenReturn(List.of(mVolumeControlProfile)); + AudioInputControl audioInputControl = prepareAudioInputControl(); + when(mVolumeControlProfile.getAudioInputControlServices(mDevice)).thenReturn( + List.of(audioInputControl)); + when(mVolumeControlProfile.getConnectionStatus(mDevice)).thenReturn(STATE_CONNECTED); + + setUpDeviceDialogWithoutPairNewDeviceButton(); + mDialog.show(); + + ViewGroup ambientLayout = getAmbientLayout(mDialog); + assertThat(ambientLayout.getVisibility()).isEqualTo(View.VISIBLE); + } + + @Test public void onActiveDeviceChanged_presetExist_presetSelected() { setUpDeviceDialogWithoutPairNewDeviceButton(); mDialog.show(); @@ -368,6 +420,10 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { return dialog.requireViewById(R.id.preset_layout); } + private ViewGroup getAmbientLayout(SystemUIDialog dialog) { + return dialog.requireViewById(R.id.ambient_layout); + } + private int countChildWithoutSpace(ViewGroup viewGroup) { int spaceCount = 0; @@ -388,6 +444,16 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { assertThat(toolsLayout.getVisibility()).isEqualTo(targetVisibility); } + private AudioInputControl prepareAudioInputControl() { + AudioInputControl audioInputControl = mock(AudioInputControl.class); + when(audioInputControl.getAudioInputType()).thenReturn( + AudioInputControl.AUDIO_INPUT_TYPE_AMBIENT); + when(audioInputControl.getGainMode()).thenReturn(AudioInputControl.GAIN_MODE_MANUAL); + when(audioInputControl.getAudioInputStatus()).thenReturn( + AudioInputControl.AUDIO_INPUT_STATUS_ACTIVE); + return audioInputControl; + } + @After public void reset() { if (mDialogDelegate != null) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt index fa5af510fec1..77e386963129 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt @@ -127,6 +127,7 @@ class ShadeTouchHandlerTest(flags: FlagsParameterization) : SysuiTestCase() { @Test @DisableFlags( Flags.FLAG_COMMUNAL_HUB, + Flags.FLAG_GLANCEABLE_HUB_V2, Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX, Flags.FLAG_SCENE_CONTAINER, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt index 41cc6ee182cf..271cd3a4f202 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.back.domain.interactor +import android.platform.test.annotations.EnableFlags import android.platform.test.annotations.RequiresFlagsDisabled import android.platform.test.annotations.RequiresFlagsEnabled import android.platform.test.flag.junit.DeviceFlagsValueProvider @@ -31,6 +32,7 @@ import androidx.test.filters.SmallTest import com.android.internal.statusbar.IStatusBarService import com.android.systemui.Flags import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.domain.interactor.CommunalBackActionInteractor import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope @@ -93,6 +95,7 @@ class BackActionInteractorTest : SysuiTestCase() { @Mock private lateinit var onBackInvokedDispatcher: WindowOnBackInvokedDispatcher @Mock private lateinit var iStatusBarService: IStatusBarService @Mock private lateinit var headsUpManager: HeadsUpManager + @Mock private lateinit var communalBackActionInteractor: CommunalBackActionInteractor private val keyguardRepository = FakeKeyguardRepository() private val windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor by lazy { @@ -117,6 +120,7 @@ class BackActionInteractorTest : SysuiTestCase() { windowRootViewVisibilityInteractor, shadeBackActionInteractor, qsController, + communalBackActionInteractor, ) } @@ -306,6 +310,19 @@ class BackActionInteractorTest : SysuiTestCase() { verify(shadeBackActionInteractor).onBackProgressed(0.4f) } + @Test + @EnableFlags(Flags.FLAG_GLANCEABLE_HUB_BACK_ACTION) + fun onBackAction_communalCanBeDismissed_communalBackActionInteractorCalled() { + backActionInteractor.start() + windowRootViewVisibilityInteractor.setIsLockscreenOrShadeVisible(true) + powerInteractor.setAwakeForTest() + val callback = getBackInvokedCallback() + whenever(communalBackActionInteractor.canBeDismissed()).thenReturn(true) + callback.onBackInvoked() + + verify(communalBackActionInteractor).onBackPressed() + } + private fun getBackInvokedCallback(): OnBackInvokedCallback { testScope.runCurrent() val captor = argumentCaptor<OnBackInvokedCallback>() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/model b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt index 08f139c6a3af..9c9d5adcfcc9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/model +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequestTest.kt @@ -51,7 +51,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { title = title, subtitle = subtitle, description = description, - contentView = contentView + contentView = contentView, ), BiometricUserInfo(USER_ID), BiometricOperationInfo(OPERATION_ID), @@ -101,9 +101,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { val fpPros = fingerprintSensorPropertiesInternal().first() val request = BiometricPromptRequest.Biometric( - promptInfo( - logoBitmap = logoBitmap, - ), + promptInfo(logoBitmap = logoBitmap), BiometricUserInfo(USER_ID), BiometricOperationInfo(OPERATION_ID), BiometricModalities(fingerprintProperties = fpPros), @@ -162,7 +160,7 @@ class BiometricPromptRequestTest : SysuiTestCase() { BiometricUserInfo(USER_ID), BiometricOperationInfo(OPERATION_ID), stealth, - ) + ), ) for (request in toCheck) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalMetricsStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalMetricsStartableTest.kt index 370adee44a42..03bf79bc7a38 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalMetricsStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalMetricsStartableTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.communal import android.app.StatsManager import android.app.StatsManager.StatsPullAtomCallback import android.content.pm.UserInfo +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.util.StatsEvent import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -32,6 +33,7 @@ import com.android.systemui.communal.shared.log.CommunalMetricsLogger import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.settings.fakeUserTracker import com.android.systemui.shared.system.SysUiStatsLog @@ -75,10 +77,7 @@ class CommunalMetricsStartableTest : SysuiTestCase() { // Set up an existing user, which is required for widgets to show val userInfos = listOf(UserInfo(0, "main", UserInfo.FLAG_MAIN)) userRepository.setUserInfos(userInfos) - userTracker.set( - userInfos = userInfos, - selectedUserIndex = 0, - ) + userTracker.set(userInfos = userInfos, selectedUserIndex = 0) underTest = CommunalMetricsStartable( @@ -90,14 +89,16 @@ class CommunalMetricsStartableTest : SysuiTestCase() { ) } + @DisableFlags(Flags.FLAG_GLANCEABLE_HUB_V2) @Test - fun start_communalFlagDisabled_doNotSetPullAtomCallback() { - kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, false) + fun start_communalFlagDisabled_doNotSetPullAtomCallback() = + kosmos.runTest { + fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, false) - underTest.start() + underTest.start() - verify(statsManager, never()).setPullAtomCallback(anyInt(), anyOrNull(), any(), any()) - } + verify(statsManager, never()).setPullAtomCallback(anyInt(), anyOrNull(), any(), any()) + } @Test fun onPullAtom_atomTagDoesNotMatch_pullSkip() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt index b66727e492cf..038ea9ccaaa9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt @@ -26,8 +26,8 @@ import android.content.res.mainResources import android.os.UserManager.USER_TYPE_PROFILE_MANAGED import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import android.provider.Settings -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 @@ -35,10 +35,13 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.communal.data.model.DisabledReason import com.android.systemui.communal.data.repository.CommunalSettingsRepositoryImpl.Companion.GLANCEABLE_HUB_BACKGROUND_SETTING +import com.android.systemui.communal.domain.interactor.setCommunalV2Enabled import com.android.systemui.communal.shared.model.CommunalBackgroundType import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.android.systemui.util.mockito.nullable @@ -51,15 +54,21 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.eq +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(AndroidJUnit4::class) -class CommunalSettingsRepositoryImplTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiTestCase() { private val kosmos = testKosmos().apply { mainResources = mContext.orCreateTestableResources.resources } private val testScope = kosmos.testScope private lateinit var underTest: CommunalSettingsRepository + init { + mSetFlagsRule.setFlagsParameterization(flags!!) + } + @Before fun setUp() { kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true) @@ -164,17 +173,17 @@ class CommunalSettingsRepositoryImplTest : SysuiTestCase() { assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_INVALID_USER) } - @EnableFlags(FLAG_COMMUNAL_HUB) + @EnableFlags(FLAG_COMMUNAL_HUB, FLAG_GLANCEABLE_HUB_V2) @Test fun classicFlagIsDisabled() = - testScope.runTest { - kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, false) + kosmos.runTest { + setCommunalV2Enabled(false) val enabledState by collectLastValue(underTest.getEnabledState(PRIMARY_USER)) assertThat(enabledState?.enabled).isFalse() assertThat(enabledState).containsExactly(DisabledReason.DISABLED_REASON_FLAG) } - @DisableFlags(FLAG_COMMUNAL_HUB) + @DisableFlags(FLAG_COMMUNAL_HUB, FLAG_GLANCEABLE_HUB_V2) @Test fun communalHubFlagIsDisabled() = testScope.runTest { @@ -295,6 +304,34 @@ class CommunalSettingsRepositoryImplTest : SysuiTestCase() { } } + @Test + fun screensaverDisabledByUser() = + testScope.runTest { + val enabledState by collectLastValue(underTest.getScreensaverEnabledState(PRIMARY_USER)) + + kosmos.fakeSettings.putIntForUser( + Settings.Secure.SCREENSAVER_ENABLED, + 0, + PRIMARY_USER.id, + ) + + assertThat(enabledState).isFalse() + } + + @Test + fun screensaverEnabledByUser() = + testScope.runTest { + val enabledState by collectLastValue(underTest.getScreensaverEnabledState(PRIMARY_USER)) + + kosmos.fakeSettings.putIntForUser( + Settings.Secure.SCREENSAVER_ENABLED, + 1, + PRIMARY_USER.id, + ) + + assertThat(enabledState).isTrue() + } + private fun setKeyguardFeaturesDisabled(user: UserInfo, disabledFlags: Int) { whenever(kosmos.devicePolicyManager.getKeyguardDisabledFeatures(nullable(), eq(user.id))) .thenReturn(disabledFlags) @@ -310,5 +347,11 @@ class CommunalSettingsRepositoryImplTest : SysuiTestCase() { val SECONDARY_USER = UserInfo(/* id= */ 1, /* name= */ "secondary user", /* flags= */ 0) val WORK_PROFILE = UserInfo(10, "work", /* iconPath= */ "", /* flags= */ 0, USER_TYPE_PROFILE_MANAGED) + + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf(FLAG_GLANCEABLE_HUB_V2) + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorTest.kt new file mode 100644 index 000000000000..c365f1cb3872 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorTest.kt @@ -0,0 +1,63 @@ +/* + * 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.communal.domain.interactor + +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_COMMUNAL_HUB +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.communalSceneRepository +import com.android.systemui.communal.shared.model.CommunalScenes +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class CommunalBackActionInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + + private var Kosmos.underTest by Fixture { communalBackActionInteractor } + + @Test + @EnableFlags(FLAG_COMMUNAL_HUB) + fun communalShowing_canBeDismissed() = + kosmos.runTest { + setCommunalAvailable(true) + assertThat(underTest.canBeDismissed()).isEqualTo(false) + communalInteractor.changeScene(CommunalScenes.Communal, "test") + runCurrent() + assertThat(underTest.canBeDismissed()).isEqualTo(true) + } + + @Test + @EnableFlags(FLAG_COMMUNAL_HUB) + fun onBackPressed_invokesSceneChange() = + kosmos.runTest { + underTest.onBackPressed() + runCurrent() + assertThat(communalSceneRepository.currentScene.value).isEqualTo(CommunalScenes.Blank) + } +} 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 b9e646fee98f..7ae0577bd289 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 @@ -35,6 +35,7 @@ import com.android.compose.animation.scene.ObservableTransitionState 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.SysuiTestCase import com.android.systemui.broadcast.broadcastDispatcher import com.android.systemui.communal.data.model.CommunalSmartspaceTimer @@ -232,7 +233,7 @@ class CommunalInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { @Test fun isCommunalAvailable_communalDisabled_false() = testScope.runTest { - mSetFlagsRule.disableFlags(FLAG_COMMUNAL_HUB) + mSetFlagsRule.disableFlags(FLAG_COMMUNAL_HUB, FLAG_GLANCEABLE_HUB_V2) val isAvailable by collectLastValue(underTest.isCommunalAvailable) assertThat(isAvailable).isFalse() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt index 0bfcd242828d..8a9c42d9b64e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt @@ -130,19 +130,6 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { } @Test - fun tutorialState_startedAndCommunalSceneShowing_stateWillNotUpdate() = - testScope.runTest { - val tutorialSettingState by - collectLastValue(communalTutorialRepository.tutorialSettingState) - - communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_STARTED) - - goToCommunal() - - assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_STARTED) - } - - @Test fun tutorialState_completedAndCommunalSceneShowing_stateWillNotUpdate() = testScope.runTest { val tutorialSettingState by diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelTest.kt index 88206850eb60..b78080885b0a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.communal.ui.viewmodel import android.platform.test.annotations.EnableFlags +import android.provider.Settings import android.service.dream.dreamManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -29,12 +30,16 @@ import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn +import com.android.systemui.plugins.activityStarter import com.android.systemui.statusbar.policy.batteryController import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.util.settings.fakeSettings import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mockito.verify import org.mockito.kotlin.any import org.mockito.kotlin.whenever @@ -56,10 +61,9 @@ class CommunalToDreamButtonViewModelTest : SysuiTestCase() { } @Test - fun shouldShowDreamButtonOnHub_trueWhenCanDream() = + fun shouldShowDreamButtonOnHub_trueWhenPluggedIn() = with(kosmos) { runTest { - whenever(dreamManager.canStartDreaming(any())).thenReturn(true) whenever(batteryController.isPluggedIn()).thenReturn(true) val shouldShowButton by collectLastValue(underTest.shouldShowDreamButtonOnHub) @@ -68,11 +72,10 @@ class CommunalToDreamButtonViewModelTest : SysuiTestCase() { } @Test - fun shouldShowDreamButtonOnHub_falseWhenCannotDream() = + fun shouldShowDreamButtonOnHub_falseWhenNotPluggedIn() = with(kosmos) { runTest { - whenever(dreamManager.canStartDreaming(any())).thenReturn(false) - whenever(batteryController.isPluggedIn()).thenReturn(true) + whenever(batteryController.isPluggedIn()).thenReturn(false) val shouldShowButton by collectLastValue(underTest.shouldShowDreamButtonOnHub) assertThat(shouldShowButton).isFalse() @@ -80,25 +83,40 @@ class CommunalToDreamButtonViewModelTest : SysuiTestCase() { } @Test - fun shouldShowDreamButtonOnHub_falseWhenNotPluggedIn() = + fun onShowDreamButtonTap_dreamsEnabled_startsDream() = with(kosmos) { runTest { - whenever(dreamManager.canStartDreaming(any())).thenReturn(true) - whenever(batteryController.isPluggedIn()).thenReturn(false) + val currentUser = fakeUserRepository.asMainUser() + kosmos.fakeSettings.putIntForUser( + Settings.Secure.SCREENSAVER_ENABLED, + 1, + currentUser.id, + ) + runCurrent() - val shouldShowButton by collectLastValue(underTest.shouldShowDreamButtonOnHub) - assertThat(shouldShowButton).isFalse() + underTest.onShowDreamButtonTap() + runCurrent() + + verify(dreamManager).startDream() } } @Test - fun onShowDreamButtonTap_startsDream() = + fun onShowDreamButtonTap_dreamsDisabled_startsActivity() = with(kosmos) { runTest { + val currentUser = fakeUserRepository.asMainUser() + kosmos.fakeSettings.putIntForUser( + Settings.Secure.SCREENSAVER_ENABLED, + 0, + currentUser.id, + ) + runCurrent() + underTest.onShowDreamButtonTap() runCurrent() - verify(dreamManager).startDream() + verify(activityStarter).postStartActivityDismissingKeyguard(any(), anyInt()) } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/ControlActionCoordinatorImplTest.kt index 928514657257..c8661cf59051 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/controller/ControlActionCoordinatorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/controls/ui/ControlActionCoordinatorImplTest.kt @@ -16,9 +16,9 @@ package com.android.systemui.controls.ui +import android.view.HapticFeedbackConstants import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import android.view.HapticFeedbackConstants import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastSender import com.android.systemui.controls.ControlsMetricsLogger @@ -29,6 +29,7 @@ import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.concurrency.DelayableExecutor import com.android.wm.shell.taskview.TaskViewFactory +import java.util.Optional import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -45,31 +46,20 @@ import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations -import java.util.Optional @SmallTest @RunWith(AndroidJUnit4::class) class ControlActionCoordinatorImplTest : SysuiTestCase() { - @Mock - private lateinit var vibratorHelper: VibratorHelper - @Mock - private lateinit var keyguardStateController: KeyguardStateController - @Mock - private lateinit var bgExecutor: DelayableExecutor - @Mock - private lateinit var uiExecutor: DelayableExecutor - @Mock - private lateinit var activityStarter: ActivityStarter - @Mock - private lateinit var broadcastSender: BroadcastSender - @Mock - private lateinit var taskViewFactory: Optional<TaskViewFactory> - @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private lateinit var cvh: ControlViewHolder - @Mock - private lateinit var metricsLogger: ControlsMetricsLogger - @Mock - private lateinit var controlsSettingsDialogManager: ControlsSettingsDialogManager + @Mock private lateinit var vibratorHelper: VibratorHelper + @Mock private lateinit var keyguardStateController: KeyguardStateController + @Mock private lateinit var bgExecutor: DelayableExecutor + @Mock private lateinit var uiExecutor: DelayableExecutor + @Mock private lateinit var activityStarter: ActivityStarter + @Mock private lateinit var broadcastSender: BroadcastSender + @Mock private lateinit var taskViewFactory: Optional<TaskViewFactory> + @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var cvh: ControlViewHolder + @Mock private lateinit var metricsLogger: ControlsMetricsLogger + @Mock private lateinit var controlsSettingsDialogManager: ControlsSettingsDialogManager companion object { fun <T> any(): T = Mockito.any<T>() @@ -89,18 +79,21 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true) controlsSettingsRepository.setCanShowControlsInLockscreen(true) - coordinator = spy(ControlActionCoordinatorImpl( - mContext, - bgExecutor, - uiExecutor, - activityStarter, - broadcastSender, - keyguardStateController, - taskViewFactory, - metricsLogger, - vibratorHelper, - controlsSettingsRepository, - )) + coordinator = + spy( + ControlActionCoordinatorImpl( + mContext, + bgExecutor, + uiExecutor, + activityStarter, + broadcastSender, + keyguardStateController, + taskViewFactory, + metricsLogger, + vibratorHelper, + controlsSettingsRepository, + ) + ) coordinator.activityContext = mContext `when`(cvh.cws.ci.controlId).thenReturn(ID) @@ -198,19 +191,15 @@ class ControlActionCoordinatorImplTest : SysuiTestCase() { fun drag_isEdge_performsSegmentTickHaptics() { coordinator.drag(cvh, true) - verify(vibratorHelper).performHapticFeedback( - any(), - eq(HapticFeedbackConstants.SEGMENT_TICK) - ) + verify(vibratorHelper) + .performHapticFeedback(any(), eq(HapticFeedbackConstants.SEGMENT_TICK)) } @Test fun drag_isNotEdge_performsFrequentTickHaptics() { coordinator.drag(cvh, false) - verify(vibratorHelper).performHapticFeedback( - any(), - eq(HapticFeedbackConstants.SEGMENT_FREQUENT_TICK) - ) + verify(vibratorHelper) + .performHapticFeedback(any(), eq(HapticFeedbackConstants.SEGMENT_FREQUENT_TICK)) } } 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 b07097d61b96..5921e9479bd9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt @@ -92,10 +92,10 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Mockito import org.mockito.Mockito.clearInvocations -import org.mockito.Mockito.isNull import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.firstValue @@ -768,7 +768,7 @@ class DreamOverlayServiceTest(flags: FlagsParameterization?) : SysuiTestCase() { runCurrent() verify(mDreamOverlayCallback).onRedirectWake(true) client.onWakeRequested() - verify(mCommunalInteractor).changeScene(eq(CommunalScenes.Communal), any(), isNull()) + verify(mCommunalInteractor).changeScene(eq(CommunalScenes.Communal), any(), anyOrNull()) verify(mUiEventLogger).log(CommunalUiEvent.DREAM_TO_COMMUNAL_HUB_DREAM_AWAKE_START) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt index c950523f7854..c950523f7854 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt index 9cfd328a9484..f2a6c11b872e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/data/repository/ContextualEducationRepositoryTest.kt @@ -36,6 +36,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -43,6 +44,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@Ignore("b/384284415") class ContextualEducationRepositoryTest : SysuiTestCase() { private lateinit var underTest: UserContextualEducationRepository diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt index ee3e241b5754..56e8185ab580 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt @@ -81,7 +81,7 @@ class CameraQuickAffordanceConfigTest : SysuiTestCase() { // Then verify(cameraGestureHelper) .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(true), result) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt index 50ac26196978..fde9b8ce6a50 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt @@ -197,7 +197,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { val dndMode = currentModes!!.single() assertThat(dndMode.isActive).isFalse() - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false), result) } @Test @@ -222,7 +222,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { ) // then - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false), result) assertEquals(ZEN_MODE_OFF, spyZenMode.value) assertNull(spyConditionId.value) } @@ -244,7 +244,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { val dndMode = currentModes!!.single() assertThat(dndMode.isActive).isTrue() assertThat(zenModeRepository.getModeActiveDuration(dndMode.id)).isNull() - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false), result) } @Test @@ -268,7 +268,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { ) // then - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false), result) assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, spyZenMode.value) assertNull(spyConditionId.value) } @@ -285,7 +285,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { val result = underTest.onTriggered(null) runCurrent() - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false), result) val dndMode = currentModes!!.single() assertThat(dndMode.isActive).isTrue() assertThat(zenModeRepository.getModeActiveDuration(dndMode.id)) @@ -313,7 +313,7 @@ class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() { ) // then - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false), result) assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, spyZenMode.value) assertEquals(conditionUri, spyConditionId.value) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceHapticViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceHapticViewModelTest.kt new file mode 100644 index 000000000000..18946f9d7e07 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceHapticViewModelTest.kt @@ -0,0 +1,116 @@ +/* + * 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.keyguard.data.quickaffordance + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.domain.interactor.keyguardQuickAffordanceHapticViewModelFactory +import com.android.systemui.keyguard.domain.interactor.keyguardQuickAffordanceInteractor +import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceHapticViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel +import com.android.systemui.kosmos.testScope +import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class KeyguardQuickAffordanceHapticViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + private val configKey = "$slotId::home" + private val keyguardQuickAffordanceInteractor = kosmos.keyguardQuickAffordanceInteractor + private val viewModelFlow = + MutableStateFlow(KeyguardQuickAffordanceViewModel(configKey = configKey, slotId = slotId)) + + private val underTest = + kosmos.keyguardQuickAffordanceHapticViewModelFactory.create(viewModelFlow) + + @Test + fun whenLaunchingFromTriggeredResult_hapticStateIsLaunch() = + testScope.runTest { + // GIVEN that the result from triggering the affordance launched an activity or dialog + val hapticState by collectLastValue(underTest.quickAffordanceHapticState) + keyguardQuickAffordanceInteractor.setLaunchingFromTriggeredResult( + KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult(true, configKey) + ) + runCurrent() + + // THEN the haptic state indicates that a launch haptics must play + assertThat(hapticState) + .isEqualTo(KeyguardQuickAffordanceHapticViewModel.HapticState.LAUNCH) + } + + @Test + fun whenNotLaunchFromTriggeredResult_hapticStateDoesNotEmit() = + testScope.runTest { + // GIVEN that the result from triggering the affordance did not launch an activity or + // dialog + val hapticState by collectLastValue(underTest.quickAffordanceHapticState) + keyguardQuickAffordanceInteractor.setLaunchingFromTriggeredResult( + KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult(false, configKey) + ) + runCurrent() + + // THEN there is no haptic state to play any feedback + assertThat(hapticState) + .isEqualTo(KeyguardQuickAffordanceHapticViewModel.HapticState.NO_HAPTICS) + } + + @Test + fun onQuickAffordanceTogglesToActivated_hapticStateIsToggleOn() = + testScope.runTest { + // GIVEN that an affordance toggles from deactivated to activated + val hapticState by collectLastValue(underTest.quickAffordanceHapticState) + toggleQuickAffordance(on = true) + + // THEN the haptic state reflects that a toggle on haptics should play + assertThat(hapticState) + .isEqualTo(KeyguardQuickAffordanceHapticViewModel.HapticState.TOGGLE_ON) + } + + @Test + fun onQuickAffordanceTogglesToDeactivated_hapticStateIsToggleOff() = + testScope.runTest { + // GIVEN that an affordance toggles from activated to deactivated + val hapticState by collectLastValue(underTest.quickAffordanceHapticState) + toggleQuickAffordance(on = false) + + // THEN the haptic state reflects that a toggle off haptics should play + assertThat(hapticState) + .isEqualTo(KeyguardQuickAffordanceHapticViewModel.HapticState.TOGGLE_OFF) + } + + private fun TestScope.toggleQuickAffordance(on: Boolean) { + underTest.updateActivatedHistory(!on) + runCurrent() + underTest.updateActivatedHistory(on) + runCurrent() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt index b15352bfe6ab..173b4e56075c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfigTest.kt @@ -49,14 +49,10 @@ import org.mockito.MockitoAnnotations class MuteQuickAffordanceConfigTest : SysuiTestCase() { private lateinit var underTest: MuteQuickAffordanceConfig - @Mock - private lateinit var ringerModeTracker: RingerModeTracker - @Mock - private lateinit var audioManager: AudioManager - @Mock - private lateinit var userTracker: UserTracker - @Mock - private lateinit var userFileManager: UserFileManager + @Mock private lateinit var ringerModeTracker: RingerModeTracker + @Mock private lateinit var audioManager: AudioManager + @Mock private lateinit var userTracker: UserTracker + @Mock private lateinit var userFileManager: UserFileManager private lateinit var testDispatcher: TestDispatcher private lateinit var testScope: TestScope @@ -70,9 +66,12 @@ class MuteQuickAffordanceConfigTest : SysuiTestCase() { whenever(userTracker.userContext).thenReturn(context) whenever(userFileManager.getSharedPreferences(any(), any(), any())) - .thenReturn(context.getSharedPreferences("mutequickaffordancetest", Context.MODE_PRIVATE)) + .thenReturn( + context.getSharedPreferences("mutequickaffordancetest", Context.MODE_PRIVATE) + ) - underTest = MuteQuickAffordanceConfig( + underTest = + MuteQuickAffordanceConfig( context, userTracker, userFileManager, @@ -81,64 +80,71 @@ class MuteQuickAffordanceConfigTest : SysuiTestCase() { testScope.backgroundScope, testDispatcher, testDispatcher, - ) + ) } @Test - fun pickerState_volumeFixed_notAvailable() = testScope.runTest { - //given - whenever(audioManager.isVolumeFixed).thenReturn(true) + fun pickerState_volumeFixed_notAvailable() = + testScope.runTest { + // given + whenever(audioManager.isVolumeFixed).thenReturn(true) - //when - val result = underTest.getPickerScreenState() + // when + val result = underTest.getPickerScreenState() - //then - assertEquals(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice, result) - } + // then + assertEquals( + KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice, + result, + ) + } @Test - fun pickerState_volumeNotFixed_available() = testScope.runTest { - //given - whenever(audioManager.isVolumeFixed).thenReturn(false) + fun pickerState_volumeNotFixed_available() = + testScope.runTest { + // given + whenever(audioManager.isVolumeFixed).thenReturn(false) - //when - val result = underTest.getPickerScreenState() + // when + val result = underTest.getPickerScreenState() - //then - assertEquals(KeyguardQuickAffordanceConfig.PickerScreenState.Default(), result) - } + // then + assertEquals(KeyguardQuickAffordanceConfig.PickerScreenState.Default(), result) + } @Test - fun triggered_stateWasPreviouslyNORMAL_currentlySILENT_moveToPreviousState() = testScope.runTest { - //given - val ringerModeCapture = argumentCaptor<Int>() - whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL) - underTest.onTriggered(null) - whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_SILENT) - - //when - val result = underTest.onTriggered(null) - runCurrent() - verify(audioManager, times(2)).ringerModeInternal = ringerModeCapture.capture() - - //then - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) - assertEquals(AudioManager.RINGER_MODE_NORMAL, ringerModeCapture.value) - } + fun triggered_stateWasPreviouslyNORMAL_currentlySILENT_moveToPreviousState() = + testScope.runTest { + // given + val ringerModeCapture = argumentCaptor<Int>() + whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL) + underTest.onTriggered(null) + whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_SILENT) + + // when + val result = underTest.onTriggered(null) + runCurrent() + verify(audioManager, times(2)).ringerModeInternal = ringerModeCapture.capture() + + // then + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false), result) + assertEquals(AudioManager.RINGER_MODE_NORMAL, ringerModeCapture.value) + } @Test - fun triggered_stateIsNotSILENT_moveToSILENTringer() = testScope.runTest { - //given - val ringerModeCapture = argumentCaptor<Int>() - whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL) - - //when - val result = underTest.onTriggered(null) - runCurrent() - verify(audioManager).ringerModeInternal = ringerModeCapture.capture() - - //then - assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled, result) - assertEquals(AudioManager.RINGER_MODE_SILENT, ringerModeCapture.value) - } -}
\ No newline at end of file + fun triggered_stateIsNotSILENT_moveToSILENTringer() = + testScope.runTest { + // given + val ringerModeCapture = argumentCaptor<Int>() + whenever(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL) + + // when + val result = underTest.onTriggered(null) + runCurrent() + verify(audioManager).ringerModeInternal = ringerModeCapture.capture() + + // then + assertEquals(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false), result) + assertEquals(AudioManager.RINGER_MODE_SILENT, ringerModeCapture.value) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt index e9b36b8b3b57..9bdc363b3a38 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt @@ -88,9 +88,7 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { Icon.Loaded( drawable = ICON, contentDescription = - ContentDescription.Resource( - res = R.string.accessibility_wallet_button, - ), + ContentDescription.Resource(res = R.string.accessibility_wallet_button), ) ) } @@ -118,9 +116,7 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { Icon.Loaded( drawable = ICON, contentDescription = - ContentDescription.Resource( - res = R.string.accessibility_wallet_button, - ), + ContentDescription.Resource(res = R.string.accessibility_wallet_button), ) ) } @@ -163,13 +159,9 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { } assertThat(underTest.onTriggered(expandable)) - .isEqualTo(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled) + .isEqualTo(KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(true)) verify(walletController) - .startQuickAccessUiIntent( - activityStarter, - animationController, - /* hasCard= */ true, - ) + .startQuickAccessUiIntent(activityStarter, animationController, /* hasCard= */ true) } @Test @@ -184,9 +176,7 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { @Test fun getPickerScreenState_unavailable() = testScope.runTest { - setUpState( - isWalletServiceAvailable = false, - ) + setUpState(isWalletServiceAvailable = false) assertThat(underTest.getPickerScreenState()) .isEqualTo(KeyguardQuickAffordanceConfig.PickerScreenState.UnavailableOnDevice) @@ -195,9 +185,7 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { @Test fun getPickerScreenState_disabledWhenTheFeatureIsNotEnabled() = testScope.runTest { - setUpState( - isWalletFeatureAvailable = false, - ) + setUpState(isWalletFeatureAvailable = false) assertThat(underTest.getPickerScreenState()) .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Disabled::class.java) @@ -206,9 +194,7 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { @Test fun getPickerScreenState_disabledWhenThereIsNoCard() = testScope.runTest { - setUpState( - hasSelectedCard = false, - ) + setUpState(hasSelectedCard = false) assertThat(underTest.getPickerScreenState()) .isInstanceOf(KeyguardQuickAffordanceConfig.PickerScreenState.Disabled::class.java) @@ -219,7 +205,7 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { isWalletServiceAvailable: Boolean = true, isWalletQuerySuccessful: Boolean = true, hasSelectedCard: Boolean = true, - cardType: Int = WalletCard.CARD_TYPE_UNKNOWN + cardType: Int = WalletCard.CARD_TYPE_UNKNOWN, ) { val walletClient: QuickAccessWalletClient = mock() whenever(walletClient.tileIcon).thenReturn(ICON) @@ -242,11 +228,11 @@ class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() { /*cardType= */ cardType, /*cardImage= */ mock(), /*contentDescription= */ CARD_DESCRIPTION, - /*pendingIntent= */ mock() + /*pendingIntent= */ mock(), ) .build() ), - 0 + 0, ) } else { GetWalletCardsResponse(emptyList(), 0) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index 46d1ebe75899..9de0215022e9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -764,6 +764,28 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { assertThat(launchingAffordance).isFalse() } + @Test + fun onQuickAffordanceTriggered_updatesLaunchingFromTriggeredResult() = + testScope.runTest { + // WHEN selecting and triggering a quick affordance at a slot + val key = homeControls.key + val slot = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START + val encodedKey = "$slot::$key" + val actionLaunched = true + homeControls.onTriggeredResult = + KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(actionLaunched) + underTest.select(slot, key) + runCurrent() + underTest.onQuickAffordanceTriggered(encodedKey, expandable = null, slot) + + // THEN the latest triggered result shows that an action launched for the same key and + // slot + val launchingFromTriggeredResult by + collectLastValue(underTest.launchingFromTriggeredResult) + assertThat(launchingFromTriggeredResult?.launched).isEqualTo(actionLaunched) + assertThat(launchingFromTriggeredResult?.configKey).isEqualTo(encodedKey) + } + companion object { private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337 private val ICON: Icon = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt index ad5eeabf83d2..26fe379f00bf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractorTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.kosmos.testScope import com.android.systemui.scene.data.repository.sceneContainerRepository +import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -118,6 +119,50 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { ) } + /** STL: Ls -> overlay, then settle with Idle(overlay). */ + @Test + fun transition_overlay_from_ls_scene_end_in_gone() = + testScope.runTest { + sceneTransitions.value = + ObservableTransitionState.Transition.ShowOrHideOverlay( + overlay = Overlays.NotificationsShade, + fromContent = Scenes.Lockscreen, + toContent = Overlays.NotificationsShade, + currentScene = Scenes.Lockscreen, + currentOverlays = flowOf(emptySet()), + progress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + previewProgress = flowOf(0f), + isInPreviewStage = flowOf(false), + ) + + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.RUNNING, + progress = 0f, + ) + + progress.value = 0.4f + assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f) + + sceneTransitions.value = + ObservableTransitionState.Idle( + Scenes.Lockscreen, + setOf(Overlays.NotificationsShade), + ) + assertTransition( + step = currentStep!!, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.UNDEFINED, + state = TransitionState.FINISHED, + progress = 1f, + ) + } + /** * STL: Ls -> Gone, then settle with Idle(Ls). KTF in this scenario needs to invert the * transition LS -> UNDEFINED to UNDEFINED -> LS as there is no mechanism in KTF to @@ -259,6 +304,47 @@ class LockscreenSceneTransitionInteractorTest : SysuiTestCase() { ) } + /** STL: Ls with overlay, then settle with Idle(Ls). */ + @Test + fun transition_overlay_to_ls_scene_end_in_ls() = + testScope.runTest { + val currentStep by collectLastValue(kosmos.realKeyguardTransitionRepository.transitions) + sceneTransitions.value = + ObservableTransitionState.Transition.ShowOrHideOverlay( + overlay = Overlays.NotificationsShade, + fromContent = Overlays.NotificationsShade, + toContent = Scenes.Lockscreen, + currentScene = Scenes.Lockscreen, + currentOverlays = flowOf(setOf(Overlays.NotificationsShade)), + progress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + previewProgress = flowOf(0f), + isInPreviewStage = flowOf(false), + ) + + assertTransition( + step = currentStep!!, + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.RUNNING, + progress = 0f, + ) + + progress.value = 0.4f + assertTransition(step = currentStep!!, state = TransitionState.RUNNING, progress = 0.4f) + + sceneTransitions.value = ObservableTransitionState.Idle(Scenes.Lockscreen) + + assertTransition( + step = currentStep!!, + from = KeyguardState.UNDEFINED, + to = KeyguardState.LOCKSCREEN, + state = TransitionState.FINISHED, + progress = 1f, + ) + } + /** STL: Gone -> Ls (AOD), will transition to AOD once */ @Test fun transition_to_ls_scene_with_changed_next_scene_is_respected_just_once() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt index 0e3b03f74c02..be504cc0f704 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelTest.kt @@ -18,11 +18,8 @@ package com.android.systemui.keyguard.ui.viewmodel -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags as AConfigFlags import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.DisableSceneContainer @@ -75,7 +72,6 @@ class AodBurnInViewModelTest : SysuiTestCase() { private val burnInFlow = MutableStateFlow(BurnInModel()) @Before - @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) @DisableSceneContainer fun setUp() { MockitoAnnotations.initMocks(this) @@ -219,50 +215,6 @@ class AodBurnInViewModelTest : SysuiTestCase() { } @Test - @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - fun translationAndScale_whenFullyDozing_MigrationFlagOff_staysOutOfTopInset() = - testScope.runTest { - underTest.updateBurnInParams(burnInParameters.copy(minViewY = 100, topInset = 80)) - val movement by collectLastValue(underTest.movement) - assertThat(movement?.translationX).isEqualTo(0) - - // Set to dozing (on AOD) - keyguardTransitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.GONE, - to = KeyguardState.AOD, - value = 1f, - transitionState = TransitionState.FINISHED, - ), - validateStep = false, - ) - - // Trigger a change to the burn-in model - burnInFlow.value = BurnInModel(translationX = 20, translationY = -30, scale = 0.5f) - assertThat(movement?.translationX).isEqualTo(20) - // -20 instead of -30, due to inset of 80 - assertThat(movement?.translationY).isEqualTo(-20) - assertThat(movement?.scale).isEqualTo(0.5f) - assertThat(movement?.scaleClockOnly).isEqualTo(true) - - // Set to the beginning of GONE->AOD transition - keyguardTransitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.GONE, - to = KeyguardState.AOD, - value = 0f, - transitionState = TransitionState.STARTED, - ), - validateStep = false, - ) - assertThat(movement?.translationX).isEqualTo(0) - assertThat(movement?.translationY).isEqualTo(0) - assertThat(movement?.scale).isEqualTo(1f) - assertThat(movement?.scaleClockOnly).isEqualTo(true) - } - - @Test - @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun translationAndScale_whenFullyDozing_MigrationFlagOn_staysOutOfTopInset() = testScope.runTest { underTest.updateBurnInParams(burnInParameters.copy(minViewY = 100, topInset = 80)) @@ -334,7 +286,6 @@ class AodBurnInViewModelTest : SysuiTestCase() { @Test @DisableSceneContainer - @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun translationAndScale_sceneContainerOff_weatherLargeClock() = testBurnInViewModelForClocks( isSmallClock = false, @@ -344,7 +295,6 @@ class AodBurnInViewModelTest : SysuiTestCase() { @Test @DisableSceneContainer - @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun translationAndScale_sceneContainerOff_weatherSmallClock() = testBurnInViewModelForClocks( isSmallClock = true, @@ -354,7 +304,6 @@ class AodBurnInViewModelTest : SysuiTestCase() { @Test @DisableSceneContainer - @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun translationAndScale_sceneContainerOff_nonWeatherLargeClock() = testBurnInViewModelForClocks( isSmallClock = false, @@ -364,7 +313,6 @@ class AodBurnInViewModelTest : SysuiTestCase() { @Test @DisableSceneContainer - @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun translationAndScale_sceneContainerOff_nonWeatherSmallClock() = testBurnInViewModelForClocks( isSmallClock = true, @@ -373,7 +321,6 @@ class AodBurnInViewModelTest : SysuiTestCase() { ) @Test - @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) @EnableSceneContainer fun translationAndScale_sceneContainerOn_weatherLargeClock() = testBurnInViewModelForClocks( @@ -383,7 +330,6 @@ class AodBurnInViewModelTest : SysuiTestCase() { ) @Test - @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) @EnableSceneContainer fun translationAndScale_sceneContainerOn_weatherSmallClock() = testBurnInViewModelForClocks( @@ -393,7 +339,6 @@ class AodBurnInViewModelTest : SysuiTestCase() { ) @Test - @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) @EnableSceneContainer fun translationAndScale_sceneContainerOn_nonWeatherLargeClock() = testBurnInViewModelForClocks( @@ -403,7 +348,6 @@ class AodBurnInViewModelTest : SysuiTestCase() { ) @Test - @EnableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) @EnableSceneContainer @Ignore("b/367659687") fun translationAndScale_sceneContainerOn_nonWeatherSmallClock() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt index 95ffc962797d..789477e38b55 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt @@ -19,12 +19,10 @@ package com.android.systemui.keyguard.ui.viewmodel -import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import android.view.View import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState -import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.communalSceneRepository import com.android.systemui.communal.shared.model.CommunalScenes @@ -73,7 +71,6 @@ import platform.test.runner.parameterized.Parameters @SmallTest @RunWith(ParameterizedAndroidJunit4::class) -@EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/FgsManagerControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/FgsManagerControllerTest.java index 004aeb069a14..3d9fb0a9be16 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/FgsManagerControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/FgsManagerControllerTest.java @@ -56,6 +56,7 @@ import com.android.systemui.animation.DialogTransitionAnimator; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dump.DumpManager; import com.android.systemui.settings.UserTracker; +import com.android.systemui.shade.domain.interactor.FakeShadeDialogContextInteractor; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.util.DeviceConfigProxyFake; import com.android.systemui.util.concurrency.FakeExecutor; @@ -84,6 +85,7 @@ public class FgsManagerControllerTest extends SysuiTestCase { FakeExecutor mMainExecutor; FakeExecutor mBackgroundExecutor; DeviceConfigProxyFake mDeviceConfigProxyFake; + FakeShadeDialogContextInteractor mFakeShadeDialogContextInteractor; @Mock IActivityManager mIActivityManager; @@ -118,6 +120,7 @@ public class FgsManagerControllerTest extends SysuiTestCase { public void setUp() throws RemoteException { MockitoAnnotations.initMocks(this); + mFakeShadeDialogContextInteractor = new FakeShadeDialogContextInteractor(mContext); mDeviceConfigProxyFake = new DeviceConfigProxyFake(); mSystemClock = new FakeSystemClock(); mMainExecutor = new FakeExecutor(mSystemClock); @@ -346,7 +349,6 @@ public class FgsManagerControllerTest extends SysuiTestCase { SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_USER_VISIBLE_JOBS, "true", false); FgsManagerController fmc = new FgsManagerControllerImpl( - mContext, mContext.getResources(), mMainExecutor, mBackgroundExecutor, @@ -359,7 +361,8 @@ public class FgsManagerControllerTest extends SysuiTestCase { mDialogTransitionAnimator, mBroadcastDispatcher, mDumpManager, - mSystemUIDialogFactory + mSystemUIDialogFactory, + mFakeShadeDialogContextInteractor ); fmc.init(); Assert.assertTrue(fmc.getIncludesUserVisibleJobs()); @@ -374,7 +377,6 @@ public class FgsManagerControllerTest extends SysuiTestCase { SystemUiDeviceConfigFlags.TASK_MANAGER_SHOW_USER_VISIBLE_JOBS, "false", false); fmc = new FgsManagerControllerImpl( - mContext, mContext.getResources(), mMainExecutor, mBackgroundExecutor, @@ -387,7 +389,8 @@ public class FgsManagerControllerTest extends SysuiTestCase { mDialogTransitionAnimator, mBroadcastDispatcher, mDumpManager, - mSystemUIDialogFactory + mSystemUIDialogFactory, + mFakeShadeDialogContextInteractor ); fmc.init(); Assert.assertFalse(fmc.getIncludesUserVisibleJobs()); @@ -487,7 +490,6 @@ public class FgsManagerControllerTest extends SysuiTestCase { ArgumentCaptor.forClass(BroadcastReceiver.class); FgsManagerController result = new FgsManagerControllerImpl( - mContext, mContext.getResources(), mMainExecutor, mBackgroundExecutor, @@ -500,7 +502,8 @@ public class FgsManagerControllerTest extends SysuiTestCase { mDialogTransitionAnimator, mBroadcastDispatcher, mDumpManager, - mSystemUIDialogFactory + mSystemUIDialogFactory, + mFakeShadeDialogContextInteractor ); result.init(); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt index a8e390c25a4d..a8e390c25a4d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/toolbar/EditModeButtonViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeButtonViewModelTest.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt index fbbdc46a7873..0e823ccf1df5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/DataSaverTileTest.kt @@ -36,6 +36,7 @@ import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIconWithRes import com.android.systemui.res.R +import com.android.systemui.shade.domain.interactor.FakeShadeDialogContextInteractor import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.policy.DataSaverController import com.android.systemui.util.mockito.whenever @@ -96,6 +97,7 @@ class DataSaverTileTest(flags: FlagsParameterization) : SysuiTestCase() { dataSaverController, mDialogTransitionAnimator, systemUIDialogFactory, + FakeShadeDialogContextInteractor(mContext), ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt index 056efb34a0b1..c47a412e226a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/base/viewmodel/QSTileViewModelImplTest.kt @@ -117,7 +117,6 @@ class QSTileViewModelImplTest : SysuiTestCase() { "test_spec:\n" + " QSTileState(" + "icon=Resource(res=0, contentDescription=Resource(res=0)), " + - "iconRes=null, " + "label=test_data, " + "activationState=INACTIVE, " + "secondaryLabel=null, " + diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/InternetAdapterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/InternetAdapterTest.java index b88861756889..5c6657b98801 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/InternetAdapterTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/dialog/InternetAdapterTest.java @@ -8,6 +8,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.anyString; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -231,7 +232,8 @@ public class InternetAdapterTest extends SysuiTestCase { mViewHolder.onWifiClick(mWifiEntry, mock(View.class)); - verify(mSpyContext).startActivity(any()); + verify(mInternetDialogController).startActivityForDialog(any()); + verify(mSpyContext, never()).startActivity(any()); } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/AirplaneModeMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/AirplaneModeMapperTest.kt index 00460bfe83b2..557f4ea262a3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/AirplaneModeMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/AirplaneModeMapperTest.kt @@ -93,8 +93,7 @@ class AirplaneModeMapperTest : SysuiTestCase() { ): QSTileState { val label = context.getString(R.string.airplane_mode) return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt index 632aae035ede..24e46686e23d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt @@ -178,8 +178,7 @@ class AlarmTileMapperTest : SysuiTestCase() { ): QSTileState { val label = context.getString(R.string.status_bar_alarm) return QSTileState( - Icon.Loaded(context.getDrawable(R.drawable.ic_alarm)!!, null), - R.drawable.ic_alarm, + Icon.Loaded(context.getDrawable(R.drawable.ic_alarm)!!, null, R.drawable.ic_alarm), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapperTest.kt index 5385f945946c..2ddaddd5ad35 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapperTest.kt @@ -253,8 +253,7 @@ class BatterySaverTileMapperTest : SysuiTestCase() { ): QSTileState { val label = context.getString(R.string.battery_detail_switch_title) return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapperTest.kt index 356b98eb192e..a3c159820a94 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapperTest.kt @@ -77,8 +77,11 @@ class ColorCorrectionTileMapperTest : SysuiTestCase() { ): QSTileState { val label = context.getString(R.string.quick_settings_color_correction_label) return QSTileState( - Icon.Loaded(context.getDrawable(R.drawable.ic_qs_color_correction)!!, null), - R.drawable.ic_qs_color_correction, + Icon.Loaded( + context.getDrawable(R.drawable.ic_qs_color_correction)!!, + null, + R.drawable.ic_qs_color_correction, + ), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt index 8236c4c1e638..608adf183163 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt @@ -253,7 +253,6 @@ class CustomTileMapperTest : SysuiTestCase() { ): QSTileState { return QSTileState( icon?.let { com.android.systemui.common.shared.model.Icon.Loaded(icon, null) }, - null, "test label", activationState, "test subtitle", diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapperTest.kt index 587585ccee2e..a115c1235210 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapperTest.kt @@ -73,7 +73,11 @@ class FlashlightMapperTest : SysuiTestCase() { mapper.map(qsTileConfig, FlashlightTileModel.FlashlightAvailable(true)) val expectedIcon = - Icon.Loaded(context.getDrawable(R.drawable.qs_flashlight_icon_on)!!, null) + Icon.Loaded( + context.getDrawable(R.drawable.qs_flashlight_icon_on)!!, + null, + R.drawable.qs_flashlight_icon_on, + ) val actualIcon = tileState.icon assertThat(actualIcon).isEqualTo(expectedIcon) } @@ -84,7 +88,11 @@ class FlashlightMapperTest : SysuiTestCase() { mapper.map(qsTileConfig, FlashlightTileModel.FlashlightAvailable(false)) val expectedIcon = - Icon.Loaded(context.getDrawable(R.drawable.qs_flashlight_icon_off)!!, null) + Icon.Loaded( + context.getDrawable(R.drawable.qs_flashlight_icon_off)!!, + null, + R.drawable.qs_flashlight_icon_off, + ) val actualIcon = tileState.icon assertThat(actualIcon).isEqualTo(expectedIcon) } @@ -95,7 +103,11 @@ class FlashlightMapperTest : SysuiTestCase() { mapper.map(qsTileConfig, FlashlightTileModel.FlashlightTemporarilyUnavailable) val expectedIcon = - Icon.Loaded(context.getDrawable(R.drawable.qs_flashlight_icon_off)!!, null) + Icon.Loaded( + context.getDrawable(R.drawable.qs_flashlight_icon_off)!!, + null, + R.drawable.qs_flashlight_icon_off, + ) val actualIcon = tileState.icon assertThat(actualIcon).isEqualTo(expectedIcon) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapperTest.kt index e81771ec38d5..8f8f7105415f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapperTest.kt @@ -58,8 +58,11 @@ class FontScalingTileMapperTest : SysuiTestCase() { private fun createFontScalingTileState(): QSTileState = QSTileState( - Icon.Loaded(context.getDrawable(R.drawable.ic_qs_font_scaling)!!, null), - R.drawable.ic_qs_font_scaling, + Icon.Loaded( + context.getDrawable(R.drawable.ic_qs_font_scaling)!!, + null, + R.drawable.ic_qs_font_scaling, + ), context.getString(R.string.quick_settings_font_scaling_label), QSTileState.ActivationState.ACTIVE, null, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/HearingDevicesTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/HearingDevicesTileMapperTest.kt index 12d604ff6a7c..3d3447da15a1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/HearingDevicesTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/HearingDevicesTileMapperTest.kt @@ -102,8 +102,7 @@ class HearingDevicesTileMapperTest : SysuiTestCase() { val label = context.getString(R.string.quick_settings_hearing_devices_label) val iconRes = R.drawable.qs_hearing_devices_icon return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt index 9dcf49e02697..b087bbc29bf7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt @@ -82,7 +82,6 @@ class InternetTileMapperTest : SysuiTestCase() { QSTileState.ActivationState.ACTIVE, context.getString(R.string.quick_settings_networks_available), Icon.Loaded(signalDrawable, null), - null, context.getString(R.string.quick_settings_internet_label), ) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) @@ -120,8 +119,11 @@ class InternetTileMapperTest : SysuiTestCase() { createInternetTileState( QSTileState.ActivationState.ACTIVE, inputModel.secondaryLabel.loadText(context).toString(), - Icon.Loaded(context.getDrawable(expectedSatIcon!!.res)!!, null), - expectedSatIcon.res, + Icon.Loaded( + context.getDrawable(expectedSatIcon!!.res)!!, + null, + expectedSatIcon.res, + ), expectedSatIcon.contentDescription.loadContentDescription(context).toString(), ) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) @@ -144,8 +146,7 @@ class InternetTileMapperTest : SysuiTestCase() { createInternetTileState( QSTileState.ActivationState.ACTIVE, context.getString(R.string.quick_settings_networks_available), - Icon.Loaded(context.getDrawable(wifiRes)!!, contentDescription = null), - wifiRes, + Icon.Loaded(context.getDrawable(wifiRes)!!, null, wifiRes), context.getString(R.string.quick_settings_internet_label), ) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) @@ -171,8 +172,8 @@ class InternetTileMapperTest : SysuiTestCase() { Icon.Loaded( context.getDrawable(R.drawable.ic_qs_no_internet_unavailable)!!, contentDescription = null, + R.drawable.ic_qs_no_internet_unavailable, ), - R.drawable.ic_qs_no_internet_unavailable, context.getString(R.string.quick_settings_networks_unavailable), ) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) @@ -182,13 +183,11 @@ class InternetTileMapperTest : SysuiTestCase() { activationState: QSTileState.ActivationState, secondaryLabel: String, icon: Icon, - iconRes: Int? = null, contentDescription: String, ): QSTileState { val label = context.getString(R.string.quick_settings_internet_label) return QSTileState( icon, - iconRes, label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt index 30fce73e04da..780d58c83e5b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapperTest.kt @@ -90,8 +90,7 @@ class ColorInversionTileMapperTest : SysuiTestCase() { ): QSTileState { val label = context.getString(R.string.quick_settings_inversion_label) return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/location/domain/LocationTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/location/domain/LocationTileMapperTest.kt index 37e8a6053682..4ebe23dcdef1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/location/domain/LocationTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/location/domain/LocationTileMapperTest.kt @@ -69,7 +69,12 @@ class LocationTileMapperTest : SysuiTestCase() { fun mapsEnabledDataToOnIconState() { val tileState: QSTileState = mapper.map(qsTileConfig, LocationTileModel(true)) - val expectedIcon = Icon.Loaded(context.getDrawable(R.drawable.qs_location_icon_on)!!, null) + val expectedIcon = + Icon.Loaded( + context.getDrawable(R.drawable.qs_location_icon_on)!!, + null, + R.drawable.qs_location_icon_on, + ) val actualIcon = tileState.icon Truth.assertThat(actualIcon).isEqualTo(expectedIcon) } @@ -78,7 +83,12 @@ class LocationTileMapperTest : SysuiTestCase() { fun mapsDisabledDataToOffIconState() { val tileState: QSTileState = mapper.map(qsTileConfig, LocationTileModel(false)) - val expectedIcon = Icon.Loaded(context.getDrawable(R.drawable.qs_location_icon_off)!!, null) + val expectedIcon = + Icon.Loaded( + context.getDrawable(R.drawable.qs_location_icon_off)!!, + null, + R.drawable.qs_location_icon_off, + ) val actualIcon = tileState.icon Truth.assertThat(actualIcon).isEqualTo(expectedIcon) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt index de3dc5730421..44e6b4d2d0f6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractorTest.kt @@ -28,6 +28,7 @@ import com.android.internal.R import com.android.settingslib.notification.modes.TestModeBuilder import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestableContext +import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.asIcon import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues @@ -63,7 +64,7 @@ class ModesTileDataInteractorTest : SysuiTestCase() { fun setUp() { context.orCreateTestableResources.apply { addOverride(MODES_DRAWABLE_ID, MODES_DRAWABLE) - addOverride(R.drawable.ic_zen_mode_type_bedtime, BEDTIME_DRAWABLE) + addOverride(BEDTIME_DRAWABLE_ID, BEDTIME_DRAWABLE) } val customPackageContext = SysuiTestableContext(context) @@ -145,24 +146,24 @@ class ModesTileDataInteractorTest : SysuiTestCase() { // Tile starts with the generic Modes icon. runCurrent() assertThat(tileData?.icon).isEqualTo(MODES_ICON) - assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID) + assertThat(tileData?.icon!!.res).isEqualTo(MODES_DRAWABLE_ID) // Add an inactive mode -> Still modes icon zenModeRepository.addMode(id = "Mode", active = false) runCurrent() assertThat(tileData?.icon).isEqualTo(MODES_ICON) - assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID) + assertThat(tileData?.icon!!.res).isEqualTo(MODES_DRAWABLE_ID) // Add an active mode with a default icon: icon should be the mode icon, and the // iconResId is also populated, because we know it's a system icon. zenModeRepository.addMode( id = "Bedtime with default icon", type = AutomaticZenRule.TYPE_BEDTIME, - active = true + active = true, ) runCurrent() assertThat(tileData?.icon).isEqualTo(BEDTIME_ICON) - assertThat(tileData?.iconResId).isEqualTo(R.drawable.ic_zen_mode_type_bedtime) + assertThat(tileData?.icon!!.res).isEqualTo(BEDTIME_DRAWABLE_ID) // Add another, less-prioritized mode that has a *custom* icon: for now, icon should // remain the first mode icon @@ -177,20 +178,20 @@ class ModesTileDataInteractorTest : SysuiTestCase() { ) runCurrent() assertThat(tileData?.icon).isEqualTo(BEDTIME_ICON) - assertThat(tileData?.iconResId).isEqualTo(R.drawable.ic_zen_mode_type_bedtime) + assertThat(tileData?.icon!!.res).isEqualTo(BEDTIME_DRAWABLE_ID) // Deactivate more important mode: icon should be the less important, still active mode // And because it's a package-provided icon, iconResId is not populated. zenModeRepository.deactivateMode("Bedtime with default icon") runCurrent() assertThat(tileData?.icon).isEqualTo(CUSTOM_ICON) - assertThat(tileData?.iconResId).isNull() + assertThat(tileData?.icon!!.res).isNull() // Deactivate remaining mode: back to the default modes icon zenModeRepository.deactivateMode("Driving with custom icon") runCurrent() assertThat(tileData?.icon).isEqualTo(MODES_ICON) - assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID) + assertThat(tileData?.icon!!.res).isEqualTo(MODES_DRAWABLE_ID) } @Test @@ -205,18 +206,18 @@ class ModesTileDataInteractorTest : SysuiTestCase() { runCurrent() assertThat(tileData?.icon).isEqualTo(MODES_ICON) - assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID) + assertThat(tileData?.icon!!.res).isEqualTo(MODES_DRAWABLE_ID) // Activate a Mode -> Icon doesn't change. zenModeRepository.addMode(id = "Mode", active = true) runCurrent() assertThat(tileData?.icon).isEqualTo(MODES_ICON) - assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID) + assertThat(tileData?.icon!!.res).isEqualTo(MODES_DRAWABLE_ID) zenModeRepository.deactivateMode(id = "Mode") runCurrent() assertThat(tileData?.icon).isEqualTo(MODES_ICON) - assertThat(tileData?.iconResId).isEqualTo(MODES_DRAWABLE_ID) + assertThat(tileData?.icon!!.res).isEqualTo(MODES_DRAWABLE_ID) } @EnableFlags(Flags.FLAG_MODES_UI) @@ -256,15 +257,17 @@ class ModesTileDataInteractorTest : SysuiTestCase() { val TEST_USER = UserHandle.of(1)!! const val CUSTOM_PACKAGE = "com.some.mode.owner.package" - val MODES_DRAWABLE_ID = R.drawable.ic_zen_priority_modes + const val MODES_DRAWABLE_ID = R.drawable.ic_zen_priority_modes const val CUSTOM_DRAWABLE_ID = 12345 + const val BEDTIME_DRAWABLE_ID = R.drawable.ic_zen_mode_type_bedtime + val MODES_DRAWABLE = TestStubDrawable("modes_icon") val BEDTIME_DRAWABLE = TestStubDrawable("bedtime") val CUSTOM_DRAWABLE = TestStubDrawable("custom") - val MODES_ICON = MODES_DRAWABLE.asIcon() - val BEDTIME_ICON = BEDTIME_DRAWABLE.asIcon() + val MODES_ICON = Icon.Loaded(MODES_DRAWABLE, null, MODES_DRAWABLE_ID) + val BEDTIME_ICON = Icon.Loaded(BEDTIME_DRAWABLE, null, BEDTIME_DRAWABLE_ID) val CUSTOM_ICON = CUSTOM_DRAWABLE.asIcon() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt index 88b00468573f..04e094f25f5d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileUserActionInteractorTest.kt @@ -156,6 +156,10 @@ class ModesTileUserActionInteractorTest : SysuiTestCase() { } private fun modelOf(isActivated: Boolean, activeModeNames: List<String>): ModesTileModel { - return ModesTileModel(isActivated, activeModeNames, TestStubDrawable("icon").asIcon(), 123) + return ModesTileModel( + isActivated, + activeModeNames, + TestStubDrawable("icon").asIcon(res = 123), + ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt index 4e91d16bf1ec..d73044f6b479 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapperTest.kt @@ -99,18 +99,11 @@ class ModesTileMapperTest : SysuiTestCase() { @Test fun state_modelHasIconResId_includesIconResId() { - val icon = TestStubDrawable("res123").asIcon() - val model = - ModesTileModel( - isActivated = false, - activeModes = emptyList(), - icon = icon, - iconResId = 123, - ) + val icon = TestStubDrawable("res123").asIcon(res = 123) + val model = ModesTileModel(isActivated = false, activeModes = emptyList(), icon = icon) val state = underTest.map(config, model) assertThat(state.icon).isEqualTo(icon) - assertThat(state.iconRes).isEqualTo(123) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt index 1457f533f5ec..7c853261aa1c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapperTest.kt @@ -289,8 +289,7 @@ class NightDisplayTileMapperTest : SysuiTestCase() { if (TextUtils.isEmpty(secondaryLabel)) label else TextUtils.concat(label, ", ", secondaryLabel) return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/notes/domain/NotesTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/notes/domain/NotesTileMapperTest.kt index 2ac3e081b8f4..b6caa22a3012 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/notes/domain/NotesTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/notes/domain/NotesTileMapperTest.kt @@ -58,8 +58,11 @@ class NotesTileMapperTest : SysuiTestCase() { private fun createNotesTileState(): QSTileState = QSTileState( - Icon.Loaded(context.getDrawable(R.drawable.ic_qs_notes)!!, null), - R.drawable.ic_qs_notes, + Icon.Loaded( + context.getDrawable(R.drawable.ic_qs_notes)!!, + null, + R.drawable.ic_qs_notes, + ), context.getString(R.string.quick_settings_notes_label), QSTileState.ActivationState.INACTIVE, /* secondaryLabel= */ null, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapperTest.kt index 7782d2b279a8..5b39810e3477 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapperTest.kt @@ -66,11 +66,7 @@ class OneHandedModeTileMapperTest : SysuiTestCase() { val outputState = mapper.map(config, inputModel) val expectedState = - createOneHandedModeTileState( - QSTileState.ActivationState.INACTIVE, - subtitleArray[1], - com.android.internal.R.drawable.ic_qs_one_handed_mode, - ) + createOneHandedModeTileState(QSTileState.ActivationState.INACTIVE, subtitleArray[1]) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) } @@ -81,23 +77,21 @@ class OneHandedModeTileMapperTest : SysuiTestCase() { val outputState = mapper.map(config, inputModel) val expectedState = - createOneHandedModeTileState( - QSTileState.ActivationState.ACTIVE, - subtitleArray[2], - com.android.internal.R.drawable.ic_qs_one_handed_mode, - ) + createOneHandedModeTileState(QSTileState.ActivationState.ACTIVE, subtitleArray[2]) QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState) } private fun createOneHandedModeTileState( activationState: QSTileState.ActivationState, secondaryLabel: String, - iconRes: Int, ): QSTileState { val label = context.getString(R.string.quick_settings_onehanded_label) return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded( + context.getDrawable(com.android.internal.R.drawable.ic_qs_one_handed_mode)!!, + null, + com.android.internal.R.drawable.ic_qs_one_handed_mode, + ), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapperTest.kt index ed33250a3392..c572ff60b61a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapperTest.kt @@ -93,8 +93,8 @@ class QRCodeScannerTileMapperTest : SysuiTestCase() { Icon.Loaded( context.getDrawable(com.android.systemui.res.R.drawable.ic_qr_code_scanner)!!, null, + com.android.systemui.res.R.drawable.ic_qr_code_scanner, ), - com.android.systemui.res.R.drawable.ic_qr_code_scanner, label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapperTest.kt index 85111fd07663..00017f9059de 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapperTest.kt @@ -85,8 +85,7 @@ class ReduceBrightColorsTileMapperTest : SysuiTestCase() { R.drawable.qs_extra_dim_icon_on else R.drawable.qs_extra_dim_icon_off return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, context.resources diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapperTest.kt index 53671ba38eb6..74010143166b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapperTest.kt @@ -180,8 +180,7 @@ class RotationLockTileMapperTest : SysuiTestCase() { ): QSTileState { val label = context.getString(R.string.quick_settings_rotation_unlocked_label) return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapperTest.kt index 9a450653aa8f..1fb5048dd4c9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapperTest.kt @@ -91,8 +91,7 @@ class DataSaverTileMapperTest : SysuiTestCase() { else context.resources.getStringArray(R.array.tile_states_saver)[0] return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/ui/ScreenRecordTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/ui/ScreenRecordTileMapperTest.kt index cd683c44a59c..363255695131 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/ui/ScreenRecordTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/ui/ScreenRecordTileMapperTest.kt @@ -110,8 +110,7 @@ class ScreenRecordTileMapperTest : SysuiTestCase() { val label = context.getString(R.string.quick_settings_screen_record_label) return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapperTest.kt index c569403960d0..e4cd0e0ec215 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapperTest.kt @@ -146,8 +146,7 @@ class SensorPrivacyToggleTileMapperTest : SysuiTestCase() { else context.getString(R.string.quick_settings_mic_label) return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapperTest.kt index 0d2ebe42b7ad..8f5f2d3e6689 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapperTest.kt @@ -69,8 +69,7 @@ class UiModeNightTileMapperTest : SysuiTestCase() { expandedAccessibilityClass: KClass<out View>? = Switch::class, ): QSTileState { return QSTileState( - Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes, + Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label, activationState, secondaryLabel, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt index 86321ea04703..2c81f39a03ec 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt @@ -109,8 +109,7 @@ class WorkModeTileMapperTest : SysuiTestCase() { val label = testLabel val iconRes = com.android.internal.R.drawable.stat_sys_managed_profile_status return QSTileState( - icon = Icon.Loaded(context.getDrawable(iconRes)!!, null), - iconRes = iconRes, + icon = Icon.Loaded(context.getDrawable(iconRes)!!, null, iconRes), label = label, activationState = activationState, secondaryLabel = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt index 039a1dc05c79..518a0a5a1446 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/user/UserSwitchDialogControllerTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.PseudoGridView import com.android.systemui.qs.QSUserSwitcherEvent import com.android.systemui.qs.tiles.UserDetailView +import com.android.systemui.shade.domain.interactor.FakeShadeDialogContextInteractor import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.mockito.capture import com.android.systemui.util.mockito.eq @@ -84,6 +85,7 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { mDialogTransitionAnimator, uiEventLogger, dialogFactory, + FakeShadeDialogContextInteractor(mContext), ) } @@ -91,32 +93,32 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { fun showDialog_callsDialogShow() { val launchController = mock<DialogTransitionAnimator.Controller>() `when`(launchExpandable.dialogTransitionController(any())).thenReturn(launchController) - controller.showDialog(context, launchExpandable) + controller.showDialog(launchExpandable) verify(mDialogTransitionAnimator).show(eq(dialog), eq(launchController), anyBoolean()) verify(uiEventLogger).log(QSUserSwitcherEvent.QS_USER_DETAIL_OPEN) } @Test fun dialog_showForAllUsers() { - controller.showDialog(context, launchExpandable) + controller.showDialog(launchExpandable) verify(dialog).setShowForAllUsers(true) } @Test fun dialog_cancelOnTouchOutside() { - controller.showDialog(context, launchExpandable) + controller.showDialog(launchExpandable) verify(dialog).setCanceledOnTouchOutside(true) } @Test fun adapterAndGridLinked() { - controller.showDialog(context, launchExpandable) + controller.showDialog(launchExpandable) verify(userDetailViewAdapter).linkToViewGroup(any<PseudoGridView>()) } @Test fun doneButtonLogsCorrectly() { - controller.showDialog(context, launchExpandable) + controller.showDialog(launchExpandable) verify(dialog).setPositiveButton(anyInt(), capture(clickCaptor)) @@ -129,7 +131,7 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { fun clickSettingsButton_noFalsing_opensSettings() { `when`(falsingManager.isFalseTap(anyInt())).thenReturn(false) - controller.showDialog(context, launchExpandable) + controller.showDialog(launchExpandable) verify(dialog) .setNeutralButton(anyInt(), capture(clickCaptor), eq(false) /* dismissOnClick */) @@ -150,7 +152,7 @@ class UserSwitchDialogControllerTest : SysuiTestCase() { fun clickSettingsButton_Falsing_notOpensSettings() { `when`(falsingManager.isFalseTap(anyInt())).thenReturn(true) - controller.showDialog(context, launchExpandable) + controller.showDialog(launchExpandable) verify(dialog) .setNeutralButton(anyInt(), capture(clickCaptor), eq(false) /* dismissOnClick */) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index e56b965d9402..3187cca6ca45 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -37,7 +37,6 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModel -import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.flags.EnableSceneContainer @@ -48,9 +47,9 @@ import com.android.systemui.keyguard.ui.viewmodel.lockscreenUserActionsViewModel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.currentValue -import com.android.systemui.kosmos.runCurrent import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.verifyCurrent import com.android.systemui.lifecycle.activateIn import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest @@ -77,12 +76,9 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import kotlinx.coroutines.test.advanceTimeBy -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.verify /** * Integration test cases for the Scene Framework. @@ -137,10 +133,10 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { sceneContainerViewModel.activateIn(testScope) assertWithMessage("Initial scene key mismatch!") - .that(sceneContainerViewModel.currentScene.value) + .that(currentValue(sceneContainerViewModel.currentScene)) .isEqualTo(sceneContainerConfig.initialSceneKey) assertWithMessage("Initial scene container visibility mismatch!") - .that(sceneContainerViewModel.isVisible) + .that(currentValue { sceneContainerViewModel.isVisible }) .isTrue() } @@ -337,7 +333,6 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { .that(bouncerActionButton) .isNotNull() kosmos.bouncerSceneContentViewModel.onActionButtonClicked(bouncerActionButton!!) - runCurrent() // TODO(b/369765704): Assert that an activity was started once we use ActivityStarter. } @@ -358,9 +353,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { .that(bouncerActionButton) .isNotNull() kosmos.bouncerSceneContentViewModel.onActionButtonClicked(bouncerActionButton!!) - runCurrent() - verify(mockTelecomManager).showInCallScreen(any()) + verifyCurrent(mockTelecomManager).showInCallScreen(any()) } @Test @@ -413,7 +407,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { * the UI must gradually transition between scenes. */ private fun Kosmos.getCurrentSceneInUi(): SceneKey { - return when (val state = transitionState.value) { + return when (val state = currentValue(transitionState)) { is ObservableTransitionState.Idle -> state.currentScene is ObservableTransitionState.Transition.ChangeScene -> state.fromScene is ObservableTransitionState.Transition.ShowOrHideOverlay -> state.currentScene @@ -436,7 +430,6 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { // is not an observable that can trigger a new evaluation. fakeDeviceEntryRepository.setLockscreenEnabled(enableLockscreen) fakeAuthenticationRepository.setAuthenticationMethod(authMethod) - testScope.runCurrent() } /** Emulates a phone call in progress. */ @@ -447,7 +440,6 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { setIsInCall(true) setCallState(TelephonyManager.CALL_STATE_OFFHOOK) } - testScope.runCurrent() } /** @@ -480,24 +472,21 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { isInitiatedByUserInput = false, isUserInputOngoing = flowOf(false), ) - testScope.runCurrent() // Report progress of transition. - while (progressFlow.value < 1f) { + while (currentValue(progressFlow) < 1f) { progressFlow.value += 0.2f - testScope.runCurrent() } // End the transition and report the change. transitionState.value = ObservableTransitionState.Idle(to) fakeSceneDataSource.unpause(force = true) - testScope.runCurrent() assertWithMessage("Visibility mismatch after scene transition from $from to $to!") - .that(sceneContainerViewModel.isVisible) + .that(currentValue { sceneContainerViewModel.isVisible }) .isEqualTo(expectedVisible) - assertThat(sceneContainerViewModel.currentScene.value).isEqualTo(to) + assertThat(currentValue(sceneContainerViewModel.currentScene)).isEqualTo(to) bouncerSceneJob = if (to == Scenes.Bouncer) { @@ -510,7 +499,6 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { bouncerSceneJob?.cancel() null } - testScope.runCurrent() } /** @@ -556,13 +544,12 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { ) powerInteractor.setAwakeForTest() - testScope.runCurrent() } /** Unlocks the device by entering the correct PIN. Ends up in the Gone scene. */ private fun Kosmos.unlockDevice() { assertWithMessage("Cannot unlock a device that's already unlocked!") - .that(deviceEntryInteractor.isUnlocked.value) + .that(currentValue(deviceEntryInteractor.isUnlocked)) .isFalse() emulateUserDrivenTransition(Scenes.Bouncer) @@ -595,7 +582,6 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { pinBouncerViewModel.onPinButtonClicked(digit) } pinBouncerViewModel.onAuthenticateButtonClicked() - testScope.runCurrent() } /** @@ -625,26 +611,23 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { } pinBouncerViewModel.onAuthenticateButtonClicked() fakeMobileConnectionsRepository.isAnySimSecure.value = false - testScope.runCurrent() setAuthMethod(authMethodAfterSimUnlock, enableLockscreen) - testScope.runCurrent() } /** Changes device wakefulness state from asleep to awake, going through intermediary states. */ private fun Kosmos.wakeUpDevice() { - val wakefulnessModel = powerInteractor.detailedWakefulness.value + val wakefulnessModel = currentValue(powerInteractor.detailedWakefulness) assertWithMessage("Cannot wake up device as it's already awake!") .that(wakefulnessModel.isAwake()) .isFalse() powerInteractor.setAwakeForTest() - testScope.runCurrent() } /** Changes device wakefulness state from awake to asleep, going through intermediary states. */ private suspend fun Kosmos.putDeviceToSleep(waitForLock: Boolean = true) { - val wakefulnessModel = powerInteractor.detailedWakefulness.value + val wakefulnessModel = currentValue(powerInteractor.detailedWakefulness) assertWithMessage("Cannot put device to sleep as it's already asleep!") .that(wakefulnessModel.isAwake()) .isTrue() @@ -659,22 +642,18 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { ) .toLong() ) - } else { - testScope.runCurrent() } } /** Emulates the dismissal of the IME (soft keyboard). */ private fun Kosmos.dismissIme() { - (bouncerSceneContentViewModel.authMethodViewModel.value as? PasswordBouncerViewModel)?.let { - it.onImeDismissed() - testScope.runCurrent() - } + (currentValue(bouncerSceneContentViewModel.authMethodViewModel) + as? PasswordBouncerViewModel) + ?.let { it.onImeDismissed() } } private fun Kosmos.introduceLockedSim() { setAuthMethod(AuthenticationMethodModel.Sim) fakeMobileConnectionsRepository.isAnySimSecure.value = true - testScope.runCurrent() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt index 929537dcf757..0361ffe475a2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt @@ -212,18 +212,6 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { } @Test - @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - fun testDragDownHelperCalledWhenDraggingDown() = - testScope.runTest { - whenever(dragDownHelper.isDraggingDown).thenReturn(true) - val now = SystemClock.elapsedRealtime() - val ev = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, 0f, 0f, 0 /* meta */) - underTest.onTouchEvent(ev) - verify(dragDownHelper).onTouchEvent(ev) - ev.recycle() - } - - @Test fun testNoInterceptTouch() = testScope.runTest { captureInteractionEventHandler() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt index aa8b4f136683..4423426945eb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt @@ -117,7 +117,6 @@ class DefaultClockProviderTest : SysuiTestCase() { verify(mockLargeClockView).onTimeZoneChanged(notNull()) verify(mockSmallClockView).refreshTime() verify(mockLargeClockView).refreshTime() - verify(mockLargeClockView).setLayoutParams(any()) } @Test @@ -163,7 +162,6 @@ class DefaultClockProviderTest : SysuiTestCase() { clock.largeClock.events.onFontSettingChanged(200f) verify(mockLargeClockView).setTextSize(eq(TypedValue.COMPLEX_UNIT_PX), eq(200f)) - verify(mockLargeClockView).setLayoutParams(any()) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt index 165e943a0cc0..40f13bbbf908 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModelTest.kt @@ -16,10 +16,12 @@ package com.android.systemui.statusbar.chips.notification.ui.viewmodel +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository @@ -71,6 +73,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_noNotifs_empty() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -81,6 +84,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_notifMissingStatusBarChipIconView_empty() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -99,6 +103,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_onePromotedNotif_statusBarIconViewMatches() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -122,6 +127,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_onePromotedNotif_connectedDisplaysFlagEnabled_statusBarIconMatches() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -145,6 +151,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_onePromotedNotif_colorMatches() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -175,6 +182,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_onlyForPromotedNotifs() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -208,6 +216,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { @Test @EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_connectedDisplaysFlagEnabled_onlyForPromotedNotifs() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -242,6 +251,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_hasShortCriticalText_usesTextInsteadOfTime() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -272,6 +282,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_noTime_isIconOnly() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -294,6 +305,36 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) + fun chips_basicTime_hiddenIfAutomaticallyPromoted() = + kosmos.runTest { + val latest by collectLastValue(underTest.chips) + + val promotedContentBuilder = + PromotedNotificationContentModel.Builder("notif").apply { + this.time = + PromotedNotificationContentModel.When( + time = 6543L, + mode = PromotedNotificationContentModel.When.Mode.BasicTime, + ) + } + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + statusBarChipIcon = mock<StatusBarIconView>(), + promotedContent = promotedContentBuilder.build(), + ) + ) + ) + + assertThat(latest).hasSize(1) + assertThat(latest!![0]) + .isInstanceOf(OngoingActivityChipModel.Shown.IconOnly::class.java) + } + + @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_basicTime_isShortTimeDelta() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -322,6 +363,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_countUpTime_isTimer() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -349,6 +391,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_countDownTime_isTimer() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -376,6 +419,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_noHeadsUp_showsTime() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -407,6 +451,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_hasHeadsUpByUser_onlyShowsIcon() = kosmos.runTest { val latest by collectLastValue(underTest.chips) @@ -442,6 +487,7 @@ class NotifChipsViewModelTest : SysuiTestCase() { } @Test + @DisableFlags(FLAG_PROMOTE_NOTIFICATIONS_AUTOMATICALLY) fun chips_clickingChipNotifiesInteractor() = kosmos.runTest { val latest by collectLastValue(underTest.chips) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt new file mode 100644 index 000000000000..14787e169979 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelTest.kt @@ -0,0 +1,46 @@ +/* + * 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.statusbar.featurepods.popups.ui.viewmodel + +import android.platform.test.annotations.EnableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.chips.notification.shared.StatusBarPopupChips +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@EnableFlags(StatusBarPopupChips.FLAG_NAME) +@RunWith(AndroidJUnit4::class) +class StatusBarPopupChipsViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val underTest = kosmos.statusBarPopupChipsViewModel + + @Test + fun popupChips_allHidden_empty() = + testScope.runTest { + val latest by collectLastValue(underTest.popupChips) + assertThat(latest).isEmpty() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt index abd0a284ae3d..3359db0a22e6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorImplTest.kt @@ -43,7 +43,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) -class PromotedNotificationContentExtractorTest : SysuiTestCase() { +class PromotedNotificationContentExtractorImplTest : SysuiTestCase() { private val kosmos = testKosmos() private val provider = @@ -54,7 +54,7 @@ class PromotedNotificationContentExtractorTest : SysuiTestCase() { @Test @DisableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) fun shouldNotExtract_bothFlagsDisabled() { - val notif = createEntry().also { provider.promotedEntries.add(it) } + val notif = createEntry() val content = extractContent(notif) assertThat(content).isNull() } @@ -63,7 +63,7 @@ class PromotedNotificationContentExtractorTest : SysuiTestCase() { @EnableFlags(PromotedNotificationUi.FLAG_NAME) @DisableFlags(StatusBarNotifChips.FLAG_NAME) fun shouldExtract_promotedNotificationUiFlagEnabled() { - val entry = createEntry().also { provider.promotedEntries.add(it) } + val entry = createEntry() val content = extractContent(entry) assertThat(content).isNotNull() } @@ -72,7 +72,7 @@ class PromotedNotificationContentExtractorTest : SysuiTestCase() { @EnableFlags(StatusBarNotifChips.FLAG_NAME) @DisableFlags(PromotedNotificationUi.FLAG_NAME) fun shouldExtract_statusBarNotifChipsFlagEnabled() { - val entry = createEntry().also { provider.promotedEntries.add(it) } + val entry = createEntry() val content = extractContent(entry) assertThat(content).isNotNull() } @@ -80,7 +80,7 @@ class PromotedNotificationContentExtractorTest : SysuiTestCase() { @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) fun shouldExtract_bothFlagsEnabled() { - val entry = createEntry().also { provider.promotedEntries.add(it) } + val entry = createEntry() val content = extractContent(entry) assertThat(content).isNotNull() } @@ -88,22 +88,19 @@ class PromotedNotificationContentExtractorTest : SysuiTestCase() { @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) fun shouldNotExtract_providerDidNotPromote() { - val entry = createEntry().also { provider.promotedEntries.remove(it) } + val entry = createEntry(promoted = false) val content = extractContent(entry) assertThat(content).isNull() } @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractContent_commonFields() { - val entry = - createEntry { - setSubText(TEST_SUB_TEXT) - setContentTitle(TEST_CONTENT_TITLE) - setContentText(TEST_CONTENT_TEXT) - setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) - } - .also { provider.promotedEntries.add(it) } + fun extractsContent_commonFields() { + val entry = createEntry { + setSubText(TEST_SUB_TEXT) + setContentTitle(TEST_CONTENT_TITLE) + setContentText(TEST_CONTENT_TEXT) + } val content = extractContent(entry) @@ -117,9 +114,7 @@ class PromotedNotificationContentExtractorTest : SysuiTestCase() { @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) @DisableFlags(android.app.Flags.FLAG_API_RICH_ONGOING) fun extractContent_apiFlagOff_shortCriticalTextNotExtracted() { - val entry = - createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) } - .also { provider.promotedEntries.add(it) } + val entry = createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) } val content = extractContent(entry) @@ -134,9 +129,7 @@ class PromotedNotificationContentExtractorTest : SysuiTestCase() { android.app.Flags.FLAG_API_RICH_ONGOING, ) fun extractContent_apiFlagOn_shortCriticalTextExtracted() { - val entry = - createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) } - .also { provider.promotedEntries.add(it) } + val entry = createEntry { setShortCriticalText(TEST_SHORT_CRITICAL_TEXT) } val content = extractContent(entry) @@ -151,7 +144,7 @@ class PromotedNotificationContentExtractorTest : SysuiTestCase() { android.app.Flags.FLAG_API_RICH_ONGOING, ) fun extractContent_noShortCriticalTextSet_textIsNull() { - val entry = createEntry {}.also { provider.promotedEntries.add(it) } + val entry = createEntry { setShortCriticalText(null) } val content = extractContent(entry) @@ -161,9 +154,8 @@ class PromotedNotificationContentExtractorTest : SysuiTestCase() { @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) - fun extractContent_fromBigPictureStyle() { - val entry = - createEntry { setStyle(BigPictureStyle()) }.also { provider.promotedEntries.add(it) } + fun extractsContent_fromBigPictureStyle() { + val entry = createEntry { setStyle(BigPictureStyle()) } val content = extractContent(entry) @@ -174,8 +166,7 @@ class PromotedNotificationContentExtractorTest : SysuiTestCase() { @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) fun extractContent_fromBigTextStyle() { - val entry = - createEntry { setStyle(BigTextStyle()) }.also { provider.promotedEntries.add(it) } + val entry = createEntry { setStyle(BigTextStyle()) } val content = extractContent(entry) @@ -187,11 +178,13 @@ class PromotedNotificationContentExtractorTest : SysuiTestCase() { @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) fun extractContent_fromCallStyle() { val hangUpIntent = - PendingIntent.getBroadcast(context, 0, Intent("hangup"), PendingIntent.FLAG_IMMUTABLE) - - val entry = - createEntry { setStyle(CallStyle.forOngoingCall(TEST_PERSON, hangUpIntent)) } - .also { provider.promotedEntries.add(it) } + PendingIntent.getBroadcast( + context, + 0, + Intent("hangup_action"), + PendingIntent.FLAG_IMMUTABLE, + ) + val entry = createEntry { setStyle(CallStyle.forOngoingCall(TEST_PERSON, hangUpIntent)) } val content = extractContent(entry) @@ -202,11 +195,9 @@ class PromotedNotificationContentExtractorTest : SysuiTestCase() { @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) fun extractContent_fromProgressStyle() { - val entry = - createEntry { - setStyle(ProgressStyle().addProgressSegment(Segment(100)).setProgress(75)) - } - .also { provider.promotedEntries.add(it) } + val entry = createEntry { + setStyle(ProgressStyle().addProgressSegment(Segment(100)).setProgress(75)) + } val content = extractContent(entry) @@ -220,13 +211,9 @@ class PromotedNotificationContentExtractorTest : SysuiTestCase() { @Test @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) fun extractContent_fromIneligibleStyle() { - val entry = - createEntry { - setStyle( - MessagingStyle(TEST_PERSON).addMessage("message text", 0L, TEST_PERSON) - ) - } - .also { provider.promotedEntries.add(it) } + val entry = createEntry { + setStyle(MessagingStyle(TEST_PERSON).addMessage("message text", 0L, TEST_PERSON)) + } val content = extractContent(entry) @@ -239,9 +226,14 @@ class PromotedNotificationContentExtractorTest : SysuiTestCase() { return underTest.extractContent(entry, recoveredBuilder) } - private fun createEntry(builderBlock: Notification.Builder.() -> Unit = {}): NotificationEntry { - val notif = Notification.Builder(context, "a").also(builderBlock).build() - return NotificationEntryBuilder().setNotification(notif).build() + private fun createEntry( + promoted: Boolean = true, + builderBlock: Notification.Builder.() -> Unit = {}, + ): NotificationEntry { + val notif = Notification.Builder(context, "channel").also(builderBlock).build() + return NotificationEntryBuilder().setNotification(notif).build().also { + provider.shouldPromoteForEntry[it] = promoted + } } companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java index 6eb2764165b5..a49a66fe26b2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java @@ -16,12 +16,18 @@ package com.android.systemui.statusbar.notification.row; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP; +import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -30,14 +36,15 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Notification; +import android.app.Person; import android.content.Context; +import android.graphics.drawable.Icon; import android.os.AsyncTask; import android.os.CancellationSignal; import android.os.Handler; @@ -61,12 +68,13 @@ import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips; import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; import com.android.systemui.statusbar.notification.collection.NotificationEntry; -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor; +import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi; import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; +import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction; import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor; import com.android.systemui.statusbar.policy.InflatedSmartReplyState; import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder; @@ -105,7 +113,8 @@ public class NotificationContentInflaterTest extends SysuiTestCase { @Mock private NotifLayoutInflaterFactory.Provider mNotifLayoutInflaterFactoryProvider; @Mock private HeadsUpStyleProvider mHeadsUpStyleProvider; @Mock private NotifLayoutInflaterFactory mNotifLayoutInflaterFactory; - @Mock private PromotedNotificationContentExtractor mPromotedNotificationContentExtractor; + private final FakePromotedNotificationContentExtractor mPromotedNotificationContentExtractor = + new FakePromotedNotificationContentExtractor(); private final SmartReplyStateInflater mSmartReplyStateInflater = new SmartReplyStateInflater() { @@ -155,8 +164,8 @@ public class NotificationContentInflaterTest extends SysuiTestCase { @Test public void testIncreasedHeadsUpBeingUsed() { - BindParams params = new BindParams(); - params.usesIncreasedHeadsUpHeight = true; + BindParams params = new BindParams(false, false, /* usesIncreasedHeadsUpHeight */ true, + REDACTION_TYPE_NONE); Notification.Builder builder = spy(mBuilder); mNotificationInflater.inflateNotificationViews( mRow.getEntry(), @@ -166,14 +175,15 @@ public class NotificationContentInflaterTest extends SysuiTestCase { FLAG_CONTENT_VIEW_ALL, builder, mContext, + mContext, mSmartReplyStateInflater); verify(builder).createHeadsUpContentView(true); } @Test public void testIncreasedHeightBeingUsed() { - BindParams params = new BindParams(); - params.usesIncreasedHeight = true; + BindParams params = new BindParams(false, /* usesIncreasedHeight */ true, false, + REDACTION_TYPE_NONE); Notification.Builder builder = spy(mBuilder); mNotificationInflater.inflateNotificationViews( mRow.getEntry(), @@ -183,6 +193,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { FLAG_CONTENT_VIEW_ALL, builder, mContext, + mContext, mSmartReplyStateInflater); verify(builder).createContentView(true); } @@ -207,7 +218,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { mRow.getEntry().getSbn().getNotification().contentView = new RemoteViews(mContext.getPackageName(), com.android.systemui.res.R.layout.status_bar); inflateAndWait(true /* expectingException */, mNotificationInflater, FLAG_CONTENT_VIEW_ALL, - mRow); + REDACTION_TYPE_NONE, mRow); assertTrue(mRow.getPrivateLayout().getChildCount() == 0); verify(mRow, times(0)).onNotificationUpdated(); } @@ -227,7 +238,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { mRow.getEntry(), mRow, FLAG_CONTENT_VIEW_ALL, - new BindParams(), + new BindParams(false, false, false, REDACTION_TYPE_NONE), false /* forceInflate */, null /* callback */); Assert.assertNull(mRow.getEntry().getRunningTask()); @@ -287,7 +298,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { mBuilder.setCustomContentView(new RemoteViews(getContext().getPackageName(), R.layout.custom_view_dark)); RemoteViews decoratedMediaView = mBuilder.createContentView(); - Assert.assertFalse("The decorated media style doesn't allow a view to be reapplied!", + assertFalse("The decorated media style doesn't allow a view to be reapplied!", NotificationContentInflater.canReapplyRemoteView(mediaView, decoratedMediaView)); } @@ -385,7 +396,8 @@ public class NotificationContentInflaterTest extends SysuiTestCase { mRow.getPrivateLayout().removeAllViews(); mRow.getEntry().getSbn().getNotification().contentView = new RemoteViews(mContext.getPackageName(), R.layout.invalid_notification_height); - inflateAndWait(true, mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); + inflateAndWait(true, mNotificationInflater, FLAG_CONTENT_VIEW_ALL, REDACTION_TYPE_NONE, + mRow); assertEquals(0, mRow.getPrivateLayout().getChildCount()); verify(mRow, times(0)).onNotificationUpdated(); } @@ -395,12 +407,11 @@ public class NotificationContentInflaterTest extends SysuiTestCase { public void testExtractsPromotedContent_notWhenBothFlagsDisabled() throws Exception { final PromotedNotificationContentModel content = new PromotedNotificationContentModel.Builder("key").build(); - when(mPromotedNotificationContentExtractor.extractContent(any(), any())) - .thenReturn(content); + mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content); inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); - verify(mPromotedNotificationContentExtractor, never()).extractContent(any(), any()); + mPromotedNotificationContentExtractor.verifyZeroExtractCalls(); } @Test @@ -410,12 +421,11 @@ public class NotificationContentInflaterTest extends SysuiTestCase { throws Exception { final PromotedNotificationContentModel content = new PromotedNotificationContentModel.Builder("key").build(); - when(mPromotedNotificationContentExtractor.extractContent(any(), any())) - .thenReturn(content); + mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content); inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); - verify(mPromotedNotificationContentExtractor, times(1)).extractContent(any(), any()); + mPromotedNotificationContentExtractor.verifyOneExtractCall(); assertEquals(content, mRow.getEntry().getPromotedNotificationContentModel()); } @@ -425,12 +435,11 @@ public class NotificationContentInflaterTest extends SysuiTestCase { public void testExtractsPromotedContent_whenStatusBarNotifChipsFlagEnabled() throws Exception { final PromotedNotificationContentModel content = new PromotedNotificationContentModel.Builder("key").build(); - when(mPromotedNotificationContentExtractor.extractContent(any(), any())) - .thenReturn(content); + mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content); inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); - verify(mPromotedNotificationContentExtractor, times(1)).extractContent(any(), any()); + mPromotedNotificationContentExtractor.verifyOneExtractCall(); assertEquals(content, mRow.getEntry().getPromotedNotificationContentModel()); } @@ -439,36 +448,107 @@ public class NotificationContentInflaterTest extends SysuiTestCase { public void testExtractsPromotedContent_whenBothFlagsEnabled() throws Exception { final PromotedNotificationContentModel content = new PromotedNotificationContentModel.Builder("key").build(); - when(mPromotedNotificationContentExtractor.extractContent(any(), any())) - .thenReturn(content); + mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), content); inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); - verify(mPromotedNotificationContentExtractor, times(1)).extractContent(any(), any()); + mPromotedNotificationContentExtractor.verifyOneExtractCall(); assertEquals(content, mRow.getEntry().getPromotedNotificationContentModel()); } @Test @EnableFlags({PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME}) public void testExtractsPromotedContent_null() throws Exception { - when(mPromotedNotificationContentExtractor.extractContent(any(), any())).thenReturn(null); + mPromotedNotificationContentExtractor.resetForEntry(mRow.getEntry(), null); inflateAndWait(mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow); - verify(mPromotedNotificationContentExtractor, times(1)).extractContent(any(), any()); + mPromotedNotificationContentExtractor.verifyOneExtractCall(); assertNull(mRow.getEntry().getPromotedNotificationContentModel()); } + @Test + @EnableFlags(LockscreenOtpRedaction.FLAG_NAME) + public void testSensitiveContentPublicView_messageStyle() throws Exception { + String displayName = "Display Name"; + String messageText = "Message Text"; + String contentText = "Content Text"; + Icon personIcon = Icon.createWithResource(mContext, + com.android.systemui.res.R.drawable.ic_person); + Person testPerson = new Person.Builder() + .setName(displayName) + .setIcon(personIcon) + .build(); + Notification.MessagingStyle messagingStyle = new Notification.MessagingStyle(testPerson); + messagingStyle.addMessage(new Notification.MessagingStyle.Message(messageText, + System.currentTimeMillis(), testPerson)); + messagingStyle.setConversationType(Notification.MessagingStyle.CONVERSATION_TYPE_NORMAL); + messagingStyle.setShortcutIcon(personIcon); + Notification messageNotif = new Notification.Builder(mContext).setSmallIcon( + com.android.systemui.res.R.drawable.ic_person).setStyle(messagingStyle).build(); + ExpandableNotificationRow row = mHelper.createRow(messageNotif); + inflateAndWait(false, mNotificationInflater, FLAG_CONTENT_VIEW_PUBLIC, + REDACTION_TYPE_SENSITIVE_CONTENT, row); + NotificationContentView publicView = row.getPublicLayout(); + assertNotNull(publicView); + // The display name should be included, but not the content or message text + assertFalse(hasText(publicView, messageText)); + assertFalse(hasText(publicView, contentText)); + assertTrue(hasText(publicView, displayName)); + } + + @Test + @EnableFlags(LockscreenOtpRedaction.FLAG_NAME) + public void testSensitiveContentPublicView_nonMessageStyle() throws Exception { + String contentTitle = "Content Title"; + String contentText = "Content Text"; + Notification notif = new Notification.Builder(mContext).setSmallIcon( + com.android.systemui.res.R.drawable.ic_person) + .setContentTitle(contentTitle) + .setContentText(contentText) + .build(); + ExpandableNotificationRow row = mHelper.createRow(notif); + inflateAndWait(false, mNotificationInflater, FLAG_CONTENT_VIEW_PUBLIC, + REDACTION_TYPE_SENSITIVE_CONTENT, row); + NotificationContentView publicView = row.getPublicLayout(); + assertNotNull(publicView); + assertFalse(hasText(publicView, contentText)); + assertTrue(hasText(publicView, contentTitle)); + + // The standard public view should not use the content title or text + inflateAndWait(false, mNotificationInflater, FLAG_CONTENT_VIEW_PUBLIC, + REDACTION_TYPE_PUBLIC, row); + publicView = row.getPublicLayout(); + assertFalse(hasText(publicView, contentText)); + assertFalse(hasText(publicView, contentTitle)); + } + + private static boolean hasText(ViewGroup parent, CharSequence text) { + for (int i = 0; i < parent.getChildCount(); i++) { + View child = parent.getChildAt(i); + if (child instanceof ViewGroup) { + if (hasText((ViewGroup) child, text)) { + return true; + } + } else if (child instanceof TextView) { + return ((TextView) child).getText().toString().contains(text); + } + } + return false; + } + private static void inflateAndWait(NotificationContentInflater inflater, @InflationFlag int contentToInflate, ExpandableNotificationRow row) throws Exception { - inflateAndWait(false /* expectingException */, inflater, contentToInflate, row); + inflateAndWait(false /* expectingException */, inflater, contentToInflate, + REDACTION_TYPE_NONE, row); } private static void inflateAndWait(boolean expectingException, NotificationContentInflater inflater, @InflationFlag int contentToInflate, + @RedactionType int redactionType, ExpandableNotificationRow row) throws Exception { CountDownLatch countDownLatch = new CountDownLatch(1); final ExceptionHolder exceptionHolder = new ExceptionHolder(); @@ -496,7 +576,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { row.getEntry(), row, contentToInflate, - new BindParams(), + new BindParams(false, false, false, redactionType), false /* forceInflate */, callback /* callback */); assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS)); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt index 18517998096a..f25ba2c93c65 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.row import android.app.Notification import android.app.Person import android.content.Context +import android.graphics.drawable.Icon import android.os.AsyncTask import android.os.Build import android.os.CancellationSignal @@ -34,10 +35,14 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.res.R +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_PUBLIC +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT +import com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType import com.android.systemui.statusbar.chips.notification.shared.StatusBarNotifChips import com.android.systemui.statusbar.notification.ConversationNotificationProcessor import com.android.systemui.statusbar.notification.collection.NotificationEntry -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor +import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUi import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.BindParams @@ -45,6 +50,7 @@ import com.android.systemui.statusbar.notification.row.NotificationRowContentBin import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP +import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag @@ -67,7 +73,6 @@ import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.eq import org.mockito.kotlin.mock -import org.mockito.kotlin.never import org.mockito.kotlin.spy import org.mockito.kotlin.times import org.mockito.kotlin.verify @@ -110,7 +115,8 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { return inflatedSmartReplyState } } - private val promotedNotificationContentExtractor: PromotedNotificationContentExtractor = mock() + private val promotedNotificationContentExtractor = FakePromotedNotificationContentExtractor() + private val conversationNotificationProcessor: ConversationNotificationProcessor = mock() @Before fun setUp() { @@ -127,7 +133,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { NotificationRowContentBinderImpl( cache, mock(), - mock<ConversationNotificationProcessor>(), + conversationNotificationProcessor, mock(), smartReplyStateInflater, layoutInflaterFactoryProvider, @@ -139,14 +145,14 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { @Test fun testIncreasedHeadsUpBeingUsed() { - val params = BindParams() - params.usesIncreasedHeadsUpHeight = true + val params = + BindParams(false, false, /* usesIncreasedHeadsUpHeight */ true, REDACTION_TYPE_NONE) val builder = spy(builder) notificationInflater.inflateNotificationViews( row.entry, row, params, - true /* inflateSynchronously */, + true, /* inflateSynchronously */ FLAG_CONTENT_VIEW_ALL, builder, mContext, @@ -158,14 +164,13 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { @Test fun testIncreasedHeightBeingUsed() { - val params = BindParams() - params.usesIncreasedHeight = true + val params = BindParams(false, /* usesIncreasedHeight */ true, false, REDACTION_TYPE_NONE) val builder = spy(builder) notificationInflater.inflateNotificationViews( row.entry, row, params, - true /* inflateSynchronously */, + true, /* inflateSynchronously */ FLAG_CONTENT_VIEW_ALL, builder, mContext, @@ -194,15 +199,18 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { row.entry.sbn.notification.contentView = RemoteViews(mContext.packageName, R.layout.status_bar) inflateAndWait( - true /* expectingException */, + true, /* expectingException */ notificationInflater, FLAG_CONTENT_VIEW_ALL, + REDACTION_TYPE_NONE, row, ) Assert.assertTrue(row.privateLayout.childCount == 0) verify(row, times(0)).onNotificationUpdated() } + @Test fun testInflationOfSensitiveContentPublicView() {} + @Test fun testAsyncTaskRemoved() { row.entry.abortTask() @@ -218,8 +226,8 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { row.entry, row, FLAG_CONTENT_VIEW_ALL, - BindParams(), - false /* forceInflate */, + BindParams(false, false, false, REDACTION_TYPE_NONE), + false, /* forceInflate */ null, /* callback */ ) Assert.assertNull(row.entry.runningTask) @@ -234,7 +242,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { packageContext = mContext, remoteViews = NewRemoteViews(), contentModel = NotificationContentModel(headsUpStatusBarModel), - extractedPromotedNotificationContentModel = null, + promotedContent = null, ) val countDownLatch = CountDownLatch(1) NotificationRowContentBinderImpl.applyRemoteView( @@ -432,7 +440,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { mContext.packageName, com.android.systemui.tests.R.layout.invalid_notification_height, ) - inflateAndWait(true, notificationInflater, FLAG_CONTENT_VIEW_ALL, row) + inflateAndWait(true, notificationInflater, FLAG_CONTENT_VIEW_ALL, REDACTION_TYPE_NONE, row) Assert.assertEquals(0, row.privateLayout.childCount.toLong()) verify(row, times(0)).onNotificationUpdated() } @@ -441,7 +449,13 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { @Test fun testInflatePublicSingleLineView() { row.publicLayout.removeAllViews() - inflateAndWait(false, notificationInflater, FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE, row) + inflateAndWait( + false, + notificationInflater, + FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE, + REDACTION_TYPE_NONE, + row, + ) Assert.assertNotNull(row.publicLayout.mSingleLineView) Assert.assertTrue(row.publicLayout.mSingleLineView is HybridNotificationView) } @@ -449,12 +463,15 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { @Test fun testInflatePublicSingleLineConversationView() { val testPerson = Person.Builder().setName("Person").build() + val style = Notification.MessagingStyle(testPerson) val messagingBuilder = Notification.Builder(mContext, "no-id") .setSmallIcon(R.drawable.ic_person) .setContentTitle("Title") .setContentText("Text") - .setStyle(Notification.MessagingStyle(testPerson)) + .setStyle(style) + whenever(conversationNotificationProcessor.processNotification(any(), any(), any())) + .thenReturn(style) val messagingRow = spy(testHelper.createRow(messagingBuilder.build())) messagingRow.publicLayout.removeAllViews() @@ -462,6 +479,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { false, notificationInflater, FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE, + REDACTION_TYPE_NONE, messagingRow, ) Assert.assertNotNull(messagingRow.publicLayout.mSingleLineView) @@ -475,12 +493,11 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { @DisableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) fun testExtractsPromotedContent_notWhenBothFlagsDisabled() { val content = PromotedNotificationContentModel.Builder("key").build() - whenever(promotedNotificationContentExtractor.extractContent(any(), any())) - .thenReturn(content) + promotedNotificationContentExtractor.resetForEntry(row.entry, content) inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) - verify(promotedNotificationContentExtractor, never()).extractContent(any(), any()) + promotedNotificationContentExtractor.verifyZeroExtractCalls() } @Test @@ -488,12 +505,11 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { @DisableFlags(StatusBarNotifChips.FLAG_NAME) fun testExtractsPromotedContent_whenPromotedNotificationUiFlagEnabled() { val content = PromotedNotificationContentModel.Builder("key").build() - whenever(promotedNotificationContentExtractor.extractContent(any(), any())) - .thenReturn(content) + promotedNotificationContentExtractor.resetForEntry(row.entry, content) inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) - verify(promotedNotificationContentExtractor, times(1)).extractContent(any(), any()) + promotedNotificationContentExtractor.verifyOneExtractCall() Assert.assertEquals(content, row.entry.promotedNotificationContentModel) } @@ -502,12 +518,11 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { @DisableFlags(PromotedNotificationUi.FLAG_NAME) fun testExtractsPromotedContent_whenStatusBarNotifChipsFlagEnabled() { val content = PromotedNotificationContentModel.Builder("key").build() - whenever(promotedNotificationContentExtractor.extractContent(any(), any())) - .thenReturn(content) + promotedNotificationContentExtractor.resetForEntry(row.entry, content) inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) - verify(promotedNotificationContentExtractor, times(1)).extractContent(any(), any()) + promotedNotificationContentExtractor.verifyOneExtractCall() Assert.assertEquals(content, row.entry.promotedNotificationContentModel) } @@ -515,15 +530,99 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) fun testExtractsPromotedContent_whenBothFlagsEnabled() { val content = PromotedNotificationContentModel.Builder("key").build() - whenever(promotedNotificationContentExtractor.extractContent(any(), any())) - .thenReturn(content) + promotedNotificationContentExtractor.resetForEntry(row.entry, content) inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) - verify(promotedNotificationContentExtractor, times(1)).extractContent(any(), any()) + promotedNotificationContentExtractor.verifyOneExtractCall() Assert.assertEquals(content, row.entry.promotedNotificationContentModel) } + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME, StatusBarNotifChips.FLAG_NAME) + fun testExtractsPromotedContent_null() { + promotedNotificationContentExtractor.resetForEntry(row.entry, null) + + inflateAndWait(notificationInflater, FLAG_CONTENT_VIEW_ALL, row) + + promotedNotificationContentExtractor.verifyOneExtractCall() + Assert.assertNull(row.entry.promotedNotificationContentModel) + } + + @Test + @Throws(java.lang.Exception::class) + @EnableFlags(LockscreenOtpRedaction.FLAG_NAME) + fun testSensitiveContentPublicView_messageStyle() { + val displayName = "Display Name" + val messageText = "Message Text" + val contentText = "Content Text" + val personIcon = Icon.createWithResource(mContext, R.drawable.ic_person) + val testPerson = Person.Builder().setName(displayName).setIcon(personIcon).build() + val messagingStyle = Notification.MessagingStyle(testPerson) + messagingStyle.addMessage( + Notification.MessagingStyle.Message(messageText, System.currentTimeMillis(), testPerson) + ) + messagingStyle.setConversationType(Notification.MessagingStyle.CONVERSATION_TYPE_NORMAL) + messagingStyle.setShortcutIcon(personIcon) + val messageNotif = + Notification.Builder(mContext) + .setSmallIcon(R.drawable.ic_person) + .setStyle(messagingStyle) + .build() + val newRow: ExpandableNotificationRow = testHelper.createRow(messageNotif) + inflateAndWait( + false, + notificationInflater, + FLAG_CONTENT_VIEW_PUBLIC, + REDACTION_TYPE_SENSITIVE_CONTENT, + newRow, + ) + // The display name should be included, but not the content or message text + val publicView = newRow.publicLayout + Assert.assertNotNull(publicView) + Assert.assertFalse(hasText(publicView, messageText)) + Assert.assertFalse(hasText(publicView, contentText)) + Assert.assertTrue(hasText(publicView, displayName)) + } + + @Test + @Throws(java.lang.Exception::class) + @EnableFlags(LockscreenOtpRedaction.FLAG_NAME) + fun testSensitiveContentPublicView_nonMessageStyle() { + val contentTitle = "Content Title" + val contentText = "Content Text" + val notif = + Notification.Builder(mContext) + .setSmallIcon(R.drawable.ic_person) + .setContentTitle(contentTitle) + .setContentText(contentText) + .build() + val newRow: ExpandableNotificationRow = testHelper.createRow(notif) + inflateAndWait( + false, + notificationInflater, + FLAG_CONTENT_VIEW_PUBLIC, + REDACTION_TYPE_SENSITIVE_CONTENT, + newRow, + ) + var publicView = newRow.publicLayout + Assert.assertNotNull(publicView) + Assert.assertFalse(hasText(publicView, contentText)) + Assert.assertTrue(hasText(publicView, contentTitle)) + + // The standard public view should not use the content title or text + inflateAndWait( + false, + notificationInflater, + FLAG_CONTENT_VIEW_PUBLIC, + REDACTION_TYPE_PUBLIC, + newRow, + ) + publicView = newRow.publicLayout + Assert.assertFalse(hasText(publicView, contentText)) + Assert.assertFalse(hasText(publicView, contentTitle)) + } + private class ExceptionHolder { var exception: Exception? = null } @@ -562,13 +661,20 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { @InflationFlag contentToInflate: Int, row: ExpandableNotificationRow, ) { - inflateAndWait(false /* expectingException */, inflater, contentToInflate, row) + inflateAndWait( + false /* expectingException */, + inflater, + contentToInflate, + REDACTION_TYPE_NONE, + row, + ) } private fun inflateAndWait( expectingException: Boolean, inflater: NotificationRowContentBinderImpl, @InflationFlag contentToInflate: Int, + @RedactionType redactionType: Int, row: ExpandableNotificationRow, ) { val countDownLatch = CountDownLatch(1) @@ -597,12 +703,26 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { row.entry, row, contentToInflate, - BindParams(), - false /* forceInflate */, + BindParams(false, false, false, redactionType), + false, /* forceInflate */ callback, /* callback */ ) Assert.assertTrue(countDownLatch.await(500, TimeUnit.MILLISECONDS)) exceptionHolder.exception?.let { throw it } } + + fun hasText(parent: ViewGroup, text: CharSequence): Boolean { + for (i in 0 until parent.childCount) { + val child = parent.getChildAt(i) + if (child is ViewGroup) { + if (hasText(child, text)) { + return true + } + } else if (child is TextView) { + return child.text.toString().contains(text) + } + } + return false + } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java index b323ef85b370..b8d18757afbb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java @@ -81,7 +81,7 @@ import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.notification.icon.IconBuilder; import com.android.systemui.statusbar.notification.icon.IconManager; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor; +import com.android.systemui.statusbar.notification.promoted.FakePromotedNotificationContentExtractor; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.ExpandableNotificationRowLogger; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.OnExpandClickListener; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; @@ -201,7 +201,7 @@ public class NotificationTestHelper { new MockSmartReplyInflater(), mock(NotifLayoutInflaterFactory.Provider.class), mock(HeadsUpStyleProvider.class), - mock(PromotedNotificationContentExtractor.class), + new FakePromotedNotificationContentExtractor(), mock(NotificationRowContentBinderLogger.class)) : new NotificationContentInflater( mock(NotifRemoteViewCache.class), @@ -212,7 +212,7 @@ public class NotificationTestHelper { new MockSmartReplyInflater(), mock(NotifLayoutInflaterFactory.Provider.class), mock(HeadsUpStyleProvider.class), - mock(PromotedNotificationContentExtractor.class), + new FakePromotedNotificationContentExtractor(), mock(NotificationRowContentBinderLogger.class)); contentBinder.setInflateSynchronously(true); mBindStage = new RowContentBindStage(contentBinder, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index de40abb4d9d8..c6cffa9da13b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -952,11 +952,10 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { @Test @EnableSceneContainer public void onTouchEvent_stopExpandingNotification_sceneContainerEnabled() { - boolean touchHandled = stopExpandingNotification(); + stopExpandingNotification(); - verify(mNotificationStackScrollLayout).startOverscrollAfterExpanding(); + verify(mExpandHelper).finishExpanding(); verify(mNotificationStackScrollLayout, never()).dispatchDownEventToScroller(any()); - assertTrue(touchHandled); } @Test @@ -964,11 +963,11 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { public void onTouchEvent_stopExpandingNotification_sceneContainerDisabled() { stopExpandingNotification(); - verify(mNotificationStackScrollLayout, never()).startOverscrollAfterExpanding(); + verify(mExpandHelper, never()).finishExpanding(); verify(mNotificationStackScrollLayout).dispatchDownEventToScroller(any()); } - private boolean stopExpandingNotification() { + private void stopExpandingNotification() { when(mNotificationStackScrollLayout.getExpandHelper()).thenReturn(mExpandHelper); when(mNotificationStackScrollLayout.getIsExpanded()).thenReturn(true); when(mNotificationStackScrollLayout.getExpandedInThisMotion()).thenReturn(true); @@ -983,13 +982,13 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { NotificationStackScrollLayoutController.TouchHandler touchHandler = mController.getTouchHandler(); - return touchHandler.onTouchEvent(MotionEvent.obtain( - /* downTime= */ 0, - /* eventTime= */ 0, - MotionEvent.ACTION_DOWN, - 0, - 0, - /* metaState= */ 0 + touchHandler.onTouchEvent(MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 0, + MotionEvent.ACTION_DOWN, + 0, + 0, + /* metaState= */ 0 )); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index f48fd3c998b1..6bdd86efa8c0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -241,7 +241,7 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S shadeTestUtil.setSplitShade(true) val horizontalPosition = checkNotNull(dimens).horizontalPosition - assertIs<HorizontalPosition.FloatAtEnd>(horizontalPosition) + assertIs<HorizontalPosition.FloatAtStart>(horizontalPosition) assertThat(horizontalPosition.width).isEqualTo(200) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.kt index 31fbcb984105..baea1a1d2dca 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.phone import android.app.Notification import android.app.Notification.Builder +import android.app.PendingIntent import android.app.StatusBarManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags @@ -27,34 +28,39 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.InitController import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.runTest import com.android.systemui.plugins.activityStarter -import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.settings.FakeDisplayTracker -import com.android.systemui.shade.NotificationShadeWindowView -import com.android.systemui.shade.ShadeController -import com.android.systemui.shade.ShadeViewController import com.android.systemui.shade.domain.interactor.panelExpansionInteractor +import com.android.systemui.shade.notificationShadeWindowView import com.android.systemui.statusbar.CommandQueue -import com.android.systemui.statusbar.NotificationRemoteInputManager -import com.android.systemui.statusbar.NotificationShadeWindowController +import com.android.systemui.statusbar.StatusBarState +import com.android.systemui.statusbar.commandQueue import com.android.systemui.statusbar.lockscreenShadeTransitionController import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder -import com.android.systemui.statusbar.notification.domain.interactor.NotificationAlertsInteractor +import com.android.systemui.statusbar.notification.domain.interactor.notificationAlertsInteractor import com.android.systemui.statusbar.notification.dynamicPrivacyController import com.android.systemui.statusbar.notification.headsup.headsUpManager import com.android.systemui.statusbar.notification.interruption.NotificationInterruptSuppressor import com.android.systemui.statusbar.notification.interruption.VisualInterruptionCondition -import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider import com.android.systemui.statusbar.notification.interruption.VisualInterruptionFilter import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow +import com.android.systemui.statusbar.notification.stack.notificationStackScrollLayoutController +import com.android.systemui.statusbar.notification.visualInterruptionDecisionProvider import com.android.systemui.statusbar.notificationLockscreenUserManager import com.android.systemui.statusbar.notificationRemoteInputManager import com.android.systemui.statusbar.notificationShadeWindowController import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.policy.keyguardStateController import com.android.systemui.statusbar.sysuiStatusBarStateController import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -62,7 +68,9 @@ import com.google.common.truth.Truth.assertWithMessage import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify @@ -72,32 +80,34 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) @RunWithLooper class StatusBarNotificationPresenterTest : SysuiTestCase() { - private val kosmos = testKosmos() - - private val visualInterruptionDecisionProvider: VisualInterruptionDecisionProvider = mock() + private val kosmos: Kosmos = + testKosmos().apply { + whenever(notificationShadeWindowView.resources).thenReturn(mContext.resources) + whenever(notificationStackScrollLayoutController.view).thenReturn(mock()) + whenever(notificationAlertsInteractor.areNotificationAlertsEnabled()).thenReturn(true) + commandQueue = CommandQueue(mContext, FakeDisplayTracker(mContext)) + + // override this controller with a mock, otherwise it would start some animators which + // are not cleaned up after these tests + lockscreenShadeTransitionController = mock() + } + // initiated by argumentCaptors later in the setup step, based on the flag states private var interruptSuppressor: NotificationInterruptSuppressor? = null private var alertsDisabledCondition: VisualInterruptionCondition? = null private var vrModeCondition: VisualInterruptionCondition? = null private var needsRedactionFilter: VisualInterruptionFilter? = null private var panelsDisabledCondition: VisualInterruptionCondition? = null - private val commandQueue: CommandQueue = CommandQueue(mContext, FakeDisplayTracker(mContext)) - private val shadeController: ShadeController = mock() - private val notificationAlertsInteractor: NotificationAlertsInteractor = mock() - private val keyguardStateController: KeyguardStateController = mock() + private val commandQueue: CommandQueue = kosmos.commandQueue + private val keyguardStateController: KeyguardStateController = kosmos.keyguardStateController + private val notificationAlertsInteractor = kosmos.notificationAlertsInteractor + private val visualInterruptionDecisionProvider = kosmos.visualInterruptionDecisionProvider private lateinit var underTest: StatusBarNotificationPresenter @Before fun setup() { - mDependency.injectTestDependency(StatusBarStateController::class.java, mock()) - mDependency.injectTestDependency(ShadeController::class.java, shadeController) - mDependency.injectMockDependency(NotificationRemoteInputManager.Callback::class.java) - mDependency.injectMockDependency(NotificationShadeWindowController::class.java) - - whenever(notificationAlertsInteractor.areNotificationAlertsEnabled()).thenReturn(true) - underTest = createPresenter() if (VisualInterruptionRefactor.isEnabled) { verifyAndCaptureSuppressors() @@ -147,13 +157,12 @@ class StatusBarNotificationPresenterTest : SysuiTestCase() { @DisableFlags(VisualInterruptionRefactor.FLAG_NAME) fun testSuppressHeadsUp_disabledStatusBar_refactorDisabled() { commandQueue.disable( - DEFAULT_DISPLAY, - StatusBarManager.DISABLE_EXPAND, - 0, - false, /* animate */ + /* displayId = */ DEFAULT_DISPLAY, + /* flags = */ StatusBarManager.DISABLE_EXPAND, + /* reason = */ 0, + /* animate = */ false, ) TestableLooper.get(this).processAllMessages() - assertWithMessage("The panel should suppress heads up while disabled") .that(interruptSuppressor!!.suppressAwakeHeadsUp(createNotificationEntry())) .isTrue() @@ -163,13 +172,12 @@ class StatusBarNotificationPresenterTest : SysuiTestCase() { @EnableFlags(VisualInterruptionRefactor.FLAG_NAME) fun testSuppressHeadsUp_disabledStatusBar_refactorEnabled() { commandQueue.disable( - DEFAULT_DISPLAY, - StatusBarManager.DISABLE_EXPAND, - 0, - false, /* animate */ + /* displayId = */ DEFAULT_DISPLAY, + /* flags = */ StatusBarManager.DISABLE_EXPAND, + /* reason = */ 0, + /* animate = */ false, ) TestableLooper.get(this).processAllMessages() - assertWithMessage("The panel should suppress heads up while disabled") .that(panelsDisabledCondition!!.shouldSuppress()) .isTrue() @@ -179,13 +187,12 @@ class StatusBarNotificationPresenterTest : SysuiTestCase() { @DisableFlags(VisualInterruptionRefactor.FLAG_NAME) fun testSuppressHeadsUp_disabledNotificationShade_refactorDisabled() { commandQueue.disable( - DEFAULT_DISPLAY, - 0, - StatusBarManager.DISABLE2_NOTIFICATION_SHADE, - false, /* animate */ + /* displayId = */ DEFAULT_DISPLAY, + /* flags = */ 0, + /* reason = */ StatusBarManager.DISABLE2_NOTIFICATION_SHADE, + /* animate = */ false, ) TestableLooper.get(this).processAllMessages() - assertWithMessage( "The panel should suppress interruptions while notification shade disabled" ) @@ -197,13 +204,12 @@ class StatusBarNotificationPresenterTest : SysuiTestCase() { @EnableFlags(VisualInterruptionRefactor.FLAG_NAME) fun testSuppressHeadsUp_disabledNotificationShade_refactorEnabled() { commandQueue.disable( - DEFAULT_DISPLAY, - 0, - StatusBarManager.DISABLE2_NOTIFICATION_SHADE, - false, /* animate */ + /* displayId = */ DEFAULT_DISPLAY, + /* flags = */ 0, + /* reason = */ StatusBarManager.DISABLE2_NOTIFICATION_SHADE, + /* animate = */ false, ) TestableLooper.get(this).processAllMessages() - assertWithMessage( "The panel should suppress interruptions while notification shade disabled" ) @@ -225,7 +231,6 @@ class StatusBarNotificationPresenterTest : SysuiTestCase() { fun testNoSuppressHeadsUp_FSI_nonOccludedKeyguard_refactorDisabled() { whenever(keyguardStateController.isShowing()).thenReturn(true) whenever(keyguardStateController.isOccluded()).thenReturn(false) - assertThat(interruptSuppressor!!.suppressAwakeHeadsUp(createFsiNotificationEntry())) .isFalse() } @@ -235,9 +240,7 @@ class StatusBarNotificationPresenterTest : SysuiTestCase() { fun testNoSuppressHeadsUp_FSI_nonOccludedKeyguard_refactorEnabled() { whenever(keyguardStateController.isShowing()).thenReturn(true) whenever(keyguardStateController.isOccluded()).thenReturn(false) - assertThat(needsRedactionFilter!!.shouldSuppress(createFsiNotificationEntry())).isFalse() - val types: Set<VisualInterruptionType> = needsRedactionFilter!!.types assertThat(types).contains(VisualInterruptionType.PEEK) assertThat(types) @@ -248,7 +251,6 @@ class StatusBarNotificationPresenterTest : SysuiTestCase() { @DisableFlags(VisualInterruptionRefactor.FLAG_NAME) fun testSuppressInterruptions_vrMode_refactorDisabled() { underTest.mVrMode = true - assertWithMessage("Vr mode should suppress interruptions") .that(interruptSuppressor!!.suppressAwakeInterruptions(createNotificationEntry())) .isTrue() @@ -258,11 +260,9 @@ class StatusBarNotificationPresenterTest : SysuiTestCase() { @EnableFlags(VisualInterruptionRefactor.FLAG_NAME) fun testSuppressInterruptions_vrMode_refactorEnabled() { underTest.mVrMode = true - assertWithMessage("Vr mode should suppress interruptions") .that(vrModeCondition!!.shouldSuppress()) .isTrue() - val types: Set<VisualInterruptionType> = vrModeCondition!!.types assertThat(types).contains(VisualInterruptionType.PEEK) assertThat(types).doesNotContain(VisualInterruptionType.PULSE) @@ -273,7 +273,6 @@ class StatusBarNotificationPresenterTest : SysuiTestCase() { @DisableFlags(VisualInterruptionRefactor.FLAG_NAME) fun testSuppressInterruptions_statusBarAlertsDisabled_refactorDisabled() { whenever(notificationAlertsInteractor.areNotificationAlertsEnabled()).thenReturn(false) - assertWithMessage("When alerts aren't enabled, interruptions are suppressed") .that(interruptSuppressor!!.suppressInterruptions(createNotificationEntry())) .isTrue() @@ -283,55 +282,91 @@ class StatusBarNotificationPresenterTest : SysuiTestCase() { @EnableFlags(VisualInterruptionRefactor.FLAG_NAME) fun testSuppressInterruptions_statusBarAlertsDisabled_refactorEnabled() { whenever(notificationAlertsInteractor.areNotificationAlertsEnabled()).thenReturn(false) - assertWithMessage("When alerts aren't enabled, interruptions are suppressed") .that(alertsDisabledCondition!!.shouldSuppress()) .isTrue() - val types: Set<VisualInterruptionType> = alertsDisabledCondition!!.types assertThat(types).contains(VisualInterruptionType.PEEK) assertThat(types).contains(VisualInterruptionType.PULSE) assertThat(types).contains(VisualInterruptionType.BUBBLE) } - private fun createPresenter(): StatusBarNotificationPresenter { - val shadeViewController: ShadeViewController = mock() + @Test + @EnableSceneContainer + fun testExpandSensitiveNotification_onLockScreen_opensShade() = + kosmos.runTest { + // Given we are on the keyguard + kosmos.sysuiStatusBarStateController.state = StatusBarState.KEYGUARD + // And the device is locked + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) - val notificationShadeWindowView: NotificationShadeWindowView = mock() - whenever(notificationShadeWindowView.resources).thenReturn(mContext.resources) + // When the user expands a sensitive Notification + val row = createRow() + val entry = + row.entry.apply { setSensitive(/* sensitive= */ true, /* deviceSensitive= */ true) } - val stackScrollLayoutController: NotificationStackScrollLayoutController = mock() - whenever(stackScrollLayoutController.view).thenReturn(mock()) + underTest.onExpandClicked(entry, mock(), /* nowExpanded= */ true) - val initController: InitController = InitController() + // Then we open the locked shade + verify(kosmos.lockscreenShadeTransitionController) + // Explicit parameters to avoid issues with Kotlin default arguments in Mockito + .goToLockedShade(row, true) + } + + @Test + @EnableSceneContainer + fun testExpandSensitiveNotification_onLockedShade_showsBouncer() = + kosmos.runTest { + // Given we are on the locked shade + kosmos.sysuiStatusBarStateController.state = StatusBarState.SHADE_LOCKED + // And the device is locked + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + + // When the user expands a sensitive Notification + val entry = + createRow().entry.apply { + setSensitive(/* sensitive= */ true, /* deviceSensitive= */ true) + } + underTest.onExpandClicked(entry, mock(), /* nowExpanded= */ true) + + // Then we show the bouncer + verify(kosmos.activityStarter).dismissKeyguardThenExecute(any(), eq(null), eq(false)) + } + private fun createPresenter(): StatusBarNotificationPresenter { + val initController: InitController = InitController() return StatusBarNotificationPresenter( - mContext, - shadeViewController, + /* context = */ mContext, + /* panel = */ mock(), kosmos.panelExpansionInteractor, /* quickSettingsController = */ mock(), kosmos.headsUpManager, - notificationShadeWindowView, + kosmos.notificationShadeWindowView, kosmos.activityStarter, - stackScrollLayoutController, + kosmos.notificationStackScrollLayoutController, kosmos.dozeScrimController, kosmos.notificationShadeWindowController, kosmos.dynamicPrivacyController, - keyguardStateController, - notificationAlertsInteractor, + kosmos.keyguardStateController, + kosmos.notificationAlertsInteractor, kosmos.lockscreenShadeTransitionController, kosmos.powerInteractor, - commandQueue, + kosmos.commandQueue, kosmos.notificationLockscreenUserManager, kosmos.sysuiStatusBarStateController, /* notifShadeEventSource = */ mock(), /* notificationMediaManager = */ mock(), /* notificationGutsManager = */ mock(), - initController, - visualInterruptionDecisionProvider, + /* initController = */ initController, + kosmos.visualInterruptionDecisionProvider, kosmos.notificationRemoteInputManager, /* remoteInputManagerCallback = */ mock(), /* notificationListContainer = */ mock(), + kosmos.deviceUnlockedInteractor, ) .also { initController.executePostInitTasks() } } @@ -341,6 +376,7 @@ class StatusBarNotificationPresenterTest : SysuiTestCase() { val conditionCaptor = argumentCaptor<VisualInterruptionCondition>() verify(visualInterruptionDecisionProvider, times(3)).addCondition(conditionCaptor.capture()) + val conditions: List<VisualInterruptionCondition> = conditionCaptor.allValues alertsDisabledCondition = conditions[0] vrModeCondition = conditions[1] @@ -362,19 +398,27 @@ class StatusBarNotificationPresenterTest : SysuiTestCase() { interruptSuppressor = suppressorCaptor.lastValue } - private fun createNotificationEntry(): NotificationEntry { - return NotificationEntryBuilder() + private fun createRow(): ExpandableNotificationRow { + val row: ExpandableNotificationRow = mock() + val entry: NotificationEntry = createNotificationEntry() + whenever(row.entry).thenReturn(entry) + entry.row = row + return row + } + + private fun createNotificationEntry(): NotificationEntry = + NotificationEntryBuilder() .setPkg("a") .setOpPkg("a") .setTag("a") .setNotification(Builder(mContext, "a").build()) .build() - } private fun createFsiNotificationEntry(): NotificationEntry { val notification: Notification = - Builder(mContext, "a").setFullScreenIntent(mock(), true).build() - + Builder(mContext, "a") + .setFullScreenIntent(mock<PendingIntent>(), /* highPriority= */ true) + .build() return NotificationEntryBuilder() .setPkg("a") .setOpPkg("a") diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt index b560c591af1e..1ee8005fb7ab 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt @@ -20,7 +20,9 @@ import android.view.WindowManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any @@ -31,7 +33,6 @@ import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.runner.RunWith @@ -43,7 +44,7 @@ import org.mockito.Mockito.verify @RunWithLooper(setAsMainLooper = true) class SystemUIBottomSheetDialogTest : SysuiTestCase() { - private val kosmos = testKosmos() + private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val configurationController = mock<ConfigurationController>() private val config = mock<Configuration>() private val delegate = mock<DialogDelegate<Dialog>>() @@ -67,21 +68,17 @@ class SystemUIBottomSheetDialogTest : SysuiTestCase() { @Test fun onStart_registersConfigCallback() { - kosmos.testScope.runTest { + kosmos.runTest { dialog.show() - runCurrent() - verify(configurationController).addCallback(any()) } } @Test fun onStop_unregisterConfigCallback() { - kosmos.testScope.runTest { + kosmos.runTest { dialog.show() - runCurrent() dialog.dismiss() - runCurrent() verify(configurationController).removeCallback(any()) } @@ -89,14 +86,12 @@ class SystemUIBottomSheetDialogTest : SysuiTestCase() { @Test fun onConfigurationChanged_calledInDelegate() { - kosmos.testScope.runTest { + kosmos.runTest { dialog.show() - runCurrent() val captor = argumentCaptor<ConfigurationController.ConfigurationListener>() verify(configurationController).addCallback(capture(captor)) captor.value.onConfigChanged(config) - runCurrent() verify(delegate).onConfigurationChanged(any(), any()) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt index 06b3b57bd133..b2378d2c3aae 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.kosmos.mainCoroutineContext import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.activityStarter import com.android.systemui.runOnMainThreadAndWaitForIdleSync +import com.android.systemui.shade.data.repository.shadeDialogContextInteractor import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.systemUIDialogFactory import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.modesDialogViewModel @@ -78,6 +79,7 @@ class ModesDialogDelegateTest : SysuiTestCase() { { kosmos.modesDialogViewModel }, mockDialogEventLogger, kosmos.mainCoroutineContext, + kosmos.shadeDialogContextInteractor, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java index 7b52dd836b51..5cd0846ded7e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java @@ -501,6 +501,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { } @Test + @DisableFlags(com.android.systemui.shared.Flags.FLAG_NEW_CUSTOMIZATION_PICKER_UI) public void onWallpaperColorsChanged_changeLockWallpaper() { // Should ask for a new theme when wallpaper colors change WallpaperColors mainColors = new WallpaperColors(Color.valueOf(Color.RED), diff --git a/packages/SystemUI/res/color/slider_active_track_color.xml b/packages/SystemUI/res/color/slider_active_track_color.xml deleted file mode 100644 index 8ba5e4901a7a..000000000000 --- a/packages/SystemUI/res/color/slider_active_track_color.xml +++ /dev/null @@ -1,19 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ 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. - --> -<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> - <item android:color="@androidprv:color/materialColorPrimary" android:state_enabled="true" /> - <item android:color="@androidprv:color/materialColorSurfaceContainerHighest" /> -</selector>
\ No newline at end of file diff --git a/packages/SystemUI/res/color/slider_inactive_track_color.xml b/packages/SystemUI/res/color/slider_inactive_track_color.xml deleted file mode 100644 index 7980f804a516..000000000000 --- a/packages/SystemUI/res/color/slider_inactive_track_color.xml +++ /dev/null @@ -1,19 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ 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. - --> -<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> - <item android:color="@androidprv:color/materialColorSurfaceContainerHighest" android:state_enabled="true" /> - <item android:color="@androidprv:color/materialColorPrimary" /> -</selector>
\ No newline at end of file diff --git a/packages/SystemUI/res/color/slider_thumb_color.xml b/packages/SystemUI/res/color/slider_thumb_color.xml deleted file mode 100644 index 8a98902426f8..000000000000 --- a/packages/SystemUI/res/color/slider_thumb_color.xml +++ /dev/null @@ -1,20 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><!-- - ~ 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. - --> - -<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> - <item android:color="@androidprv:color/materialColorSurfaceContainerHighest" android:state_enabled="false" /> - <item android:color="@androidprv:color/materialColorPrimary" /> -</selector> diff --git a/packages/SystemUI/res/drawable/audio_bars_idle.xml b/packages/SystemUI/res/drawable/audio_bars_idle.xml new file mode 100644 index 000000000000..92a24755fece --- /dev/null +++ b/packages/SystemUI/res/drawable/audio_bars_idle.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2021 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="168dp" + android:height="168dp" + android:viewportWidth="168" + android:viewportHeight="168"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_2_G" + android:translateX="121.161" + android:translateY="83.911"> + <path + android:name="_R_G_L_2_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M-37.16 -5.87 C-33.94,-5.87 -31.32,-3.32 -31.2,-0.13 C-31.2,-0.06 -31.2,0.02 -31.2,0.09 C-31.2,0.16 -31.2,0.23 -31.2,0.29 C-31.31,3.49 -33.94,6.05 -37.16,6.05 C-40.39,6.05 -43.01,3.49 -43.12,0.29 C-43.12,0.23 -43.12,0.16 -43.12,0.09 C-43.12,0.01 -43.12,-0.07 -43.12,-0.15 C-42.99,-3.33 -40.37,-5.87 -37.16,-5.87c " /> + </group> + <group + android:name="_R_G_L_1_G" + android:translateX="102.911" + android:translateY="83.911"> + <path + android:name="_R_G_L_1_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M-37.16 -5.87 C-33.94,-5.87 -31.32,-3.32 -31.2,-0.13 C-31.2,-0.06 -31.2,0.02 -31.2,0.09 C-31.2,0.16 -31.2,0.23 -31.2,0.29 C-31.31,3.49 -33.94,6.05 -37.16,6.05 C-40.39,6.05 -43.01,3.49 -43.12,0.29 C-43.12,0.23 -43.12,0.16 -43.12,0.09 C-43.12,0.01 -43.12,-0.07 -43.12,-0.15 C-42.99,-3.33 -40.37,-5.87 -37.16,-5.87c " /> + </group> + <group + android:name="_R_G_L_0_G" + android:translateX="139.661" + android:translateY="83.911"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="1" + android:fillColor="#ffffff" + android:fillType="nonZero" + android:pathData=" M-37.16 -5.87 C-33.94,-5.87 -31.32,-3.32 -31.2,-0.13 C-31.2,-0.06 -31.2,0.02 -31.2,0.09 C-31.2,0.16 -31.2,0.23 -31.2,0.29 C-31.31,3.49 -33.94,6.05 -37.16,6.05 C-40.39,6.05 -43.01,3.49 -43.12,0.29 C-43.12,0.23 -43.12,0.16 -43.12,0.09 C-43.12,0.01 -43.12,-0.07 -43.12,-0.15 C-42.99,-3.33 -40.37,-5.87 -37.16,-5.87c " /> + </group> + </group> + <group android:name="time_group" /> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/hearing_devices_spinner_background.xml b/packages/SystemUI/res/drawable/hearing_devices_spinner_background.xml index dfefb9d166af..4b7be3512f53 100644 --- a/packages/SystemUI/res/drawable/hearing_devices_spinner_background.xml +++ b/packages/SystemUI/res/drawable/hearing_devices_spinner_background.xml @@ -27,18 +27,7 @@ <solid android:color="@android:color/transparent"/> </shape> </item> - <item - android:end="20dp" - android:gravity="end|center_vertical"> - <vector - android:width="@dimen/hearing_devices_preset_spinner_icon_size" - android:height="@dimen/hearing_devices_preset_spinner_icon_size" - android:viewportWidth="24" - android:viewportHeight="24" - android:tint="?androidprv:attr/colorControlNormal"> - <path - android:fillColor="#FF000000" - android:pathData="M7.41 7.84L12 12.42l4.59-4.58L18 9.25l-6 6-6-6z" /> - </vector> - </item> + <item android:end="20dp" + android:gravity="end|center_vertical" + android:drawable="@drawable/ic_hearing_device_expand" /> </layer-list>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_hearing_device_expand.xml b/packages/SystemUI/res/drawable/ic_hearing_device_expand.xml new file mode 100644 index 000000000000..fdfe7134a748 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_hearing_device_expand.xml @@ -0,0 +1,27 @@ +<!-- + 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. +--> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="@androidprv:color/materialColorOnSurface"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M7.41 7.84L12 12.42l4.59-4.58L18 9.25l-6 6-6-6z" /> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/hearing_device_ambient_volume_layout.xml b/packages/SystemUI/res/layout/hearing_device_ambient_volume_layout.xml new file mode 100644 index 000000000000..fd409a5a8bb1 --- /dev/null +++ b/packages/SystemUI/res/layout/hearing_device_ambient_volume_layout.xml @@ -0,0 +1,67 @@ +<!-- + 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. +--> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <LinearLayout + android:id="@+id/ambient_header" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/bluetooth_dialog_layout_margin" + android:layout_marginEnd="@dimen/bluetooth_dialog_layout_margin" + android:gravity="center_vertical" + android:orientation="horizontal"> + <ImageView + android:id="@+id/ambient_volume_icon" + android:layout_width="48dp" + android:layout_height="48dp" + android:padding="12dp" + android:contentDescription="@string/hearing_devices_ambient_unmute" + android:src="@drawable/ic_ambient_volume" + android:tint="@androidprv:color/materialColorOnSurface" /> + <TextView + android:id="@+id/ambient_title" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:paddingStart="10dp" + android:text="@string/hearing_devices_ambient_label" + android:textAppearance="@style/TextAppearance.Dialog.Title" + android:textDirection="locale" + android:textSize="16sp" + android:gravity="center_vertical" + android:fontFamily="@*android:string/config_headlineFontFamilyMedium" /> + <ImageView + android:id="@+id/ambient_expand_icon" + android:layout_width="48dp" + android:layout_height="48dp" + android:padding="10dp" + android:contentDescription="@string/hearing_devices_ambient_expand_controls" + android:src="@drawable/ic_hearing_device_expand" + android:tint="@androidprv:color/materialColorOnSurface" /> + </LinearLayout> + <LinearLayout + android:id="@+id/ambient_control_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" /> + +</LinearLayout> diff --git a/packages/SystemUI/res/layout/hearing_device_ambient_volume_slider.xml b/packages/SystemUI/res/layout/hearing_device_ambient_volume_slider.xml new file mode 100644 index 000000000000..44ada8943b12 --- /dev/null +++ b/packages/SystemUI/res/layout/hearing_device_ambient_volume_slider.xml @@ -0,0 +1,46 @@ +<!-- + 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. +--> + +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/bluetooth_dialog_layout_margin" + android:layout_marginEnd="@dimen/bluetooth_dialog_layout_margin" + android:orientation="vertical"> + + <TextView + android:id="@+id/ambient_volume_slider_title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:paddingStart="@dimen/hearing_devices_small_title_padding_horizontal" + android:textAppearance="@style/TextAppearance.Dialog.Title" + android:textDirection="locale" + android:textSize="14sp" + android:labelFor="@+id/ambient_volume_slider" + android:gravity="center_vertical" /> + <com.google.android.material.slider.Slider + style="@style/SystemUI.Material3.Slider" + android:id="@+id/ambient_volume_slider" + android:layout_width="match_parent" + android:layout_height="@dimen/bluetooth_dialog_device_height" + android:layout_gravity="center_vertical" + android:theme="@style/Theme.Material3.DayNight" + app:labelBehavior="gone" /> + +</LinearLayout> diff --git a/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml b/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml index bf04a6f64d6a..949a6abb9b9d 100644 --- a/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml +++ b/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml @@ -85,13 +85,22 @@ android:longClickable="false"/> </LinearLayout> + <com.android.systemui.accessibility.hearingaid.AmbientVolumeLayout + android:id="@+id/ambient_layout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/preset_layout" + android:layout_marginTop="@dimen/hearing_devices_layout_margin" /> + <LinearLayout android:id="@+id/tools_layout" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@id/preset_layout" + app:layout_constraintTop_toBottomOf="@id/ambient_layout" android:layout_marginTop="@dimen/hearing_devices_layout_margin" android:orientation="vertical"> <TextView diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml index a3bad8f012ac..5ccedeafcb59 100644 --- a/packages/SystemUI/res/layout/volume_dialog.xml +++ b/packages/SystemUI/res/layout/volume_dialog.xml @@ -56,8 +56,8 @@ android:layout_marginTop="@dimen/volume_dialog_components_spacing" android:background="@drawable/ripple_drawable_20dp" android:contentDescription="@string/accessibility_volume_settings" + android:scaleType="centerInside" android:soundEffectsEnabled="false" - android:src="@drawable/horizontal_ellipsis" android:tint="@androidprv:color/materialColorPrimary" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@id/volume_dialog_main_slider_container" diff --git a/packages/SystemUI/res/layout/volume_dialog_slider.xml b/packages/SystemUI/res/layout/volume_dialog_slider.xml index 9ac456c17084..0acf4109bbb5 100644 --- a/packages/SystemUI/res/layout/volume_dialog_slider.xml +++ b/packages/SystemUI/res/layout/volume_dialog_slider.xml @@ -14,7 +14,6 @@ limitations under the License. --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="wrap_content" android:layout_height="wrap_content"> @@ -24,11 +23,6 @@ android:layout_width="@dimen/volume_dialog_slider_width" android:layout_height="@dimen/volume_dialog_slider_height" android:layout_gravity="center" - android:theme="@style/Theme.Material3.Light" android:orientation="vertical" - app:thumbHeight="52dp" - app:trackCornerSize="12dp" - app:trackHeight="40dp" - app:trackStopIndicatorSize="6dp" - app:trackInsideCornerSize="2dp" /> + android:theme="@style/Theme.Material3.DayNight" /> </FrameLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/raw/audio_bars_in.json b/packages/SystemUI/res/raw/audio_bars_in.json new file mode 100644 index 000000000000..c90a59c47d64 --- /dev/null +++ b/packages/SystemUI/res/raw/audio_bars_in.json @@ -0,0 +1 @@ +{"v":"5.7.13","fr":60,"ip":0,"op":18,"w":168,"h":168,"nm":"audio_bars_in","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 5","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":5,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[120.75,84,0],"ix":2,"l":2},"a":{"a":0,"k":[-37.161,0.089,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-5.961],[5.957,-0.219],[5.961,0],[5.958,0.203],[0,5.961],[-5.958,0.203],[-5.961,0],[-5.957,-0.235]],"c":true}]},{"t":17,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-22.725],[5.957,-16.983],[5.961,0],[5.958,17.391],[0,23.149],[-5.958,17.39],[-5.961,0],[-5.957,-16.998]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":5,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-37.161,0.089],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[102.5,84,0],"ix":2,"l":2},"a":{"a":0,"k":[-37.161,0.089,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-5.961],[5.957,-0.219],[5.961,0],[5.958,0.203],[0,5.961],[-5.958,0.203],[-5.961,0],[-5.957,-0.235]],"c":true}]},{"t":17,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-38.225],[5.957,-32.483],[5.961,0],[5.958,32.016],[0,37.774],[-5.958,32.015],[-5.961,0],[-5.957,-32.498]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-37.161,0.089],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[65.75,84,0],"ix":2,"l":2},"a":{"a":0,"k":[-37.161,0.089,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-5.961],[5.957,-0.219],[5.961,0],[5.958,0.203],[0,5.961],[-5.958,0.203],[-5.961,0],[-5.957,-0.235]],"c":true}]},{"t":17,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-25.1],[5.957,-19.358],[5.961,0],[5.958,19.516],[0,25.274],[-5.958,19.515],[-5.961,0],[-5.957,-19.373]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-37.161,0.089],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[84,84,0],"ix":2,"l":2},"a":{"a":0,"k":[-37.161,0.089,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-5.961],[5.957,-0.219],[5.961,0],[5.958,0.203],[0,5.961],[-5.958,0.203],[-5.961,0],[-5.957,-0.235]],"c":true}]},{"t":17,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-18.6],[5.957,-12.858],[5.961,0],[5.958,13.141],[0,18.899],[-5.958,13.14],[-5.961,0],[-5.957,-12.873]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-37.161,0.089],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":5,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[47.25,84,0],"ix":2,"l":2},"a":{"a":0,"k":[-37.161,0.089,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-5.961],[5.957,-0.219],[5.961,0],[5.958,0.203],[0,5.961],[-5.958,0.203],[-5.961,0],[-5.957,-0.235]],"c":true}]},{"t":17,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-13.475],[5.957,-7.733],[5.961,0],[5.958,6.766],[0,12.524],[-5.958,6.765],[-5.961,0],[-5.957,-7.748]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"t":5,"s":[100]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-37.161,0.089],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file diff --git a/packages/SystemUI/res/raw/audio_bars_out.json b/packages/SystemUI/res/raw/audio_bars_out.json new file mode 100644 index 000000000000..5eab65e057ab --- /dev/null +++ b/packages/SystemUI/res/raw/audio_bars_out.json @@ -0,0 +1 @@ +{"v":"5.7.13","fr":60,"ip":0,"op":31,"w":168,"h":168,"nm":"audio_bars_out","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 5","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":5,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[120.75,84,0],"ix":2,"l":2},"a":{"a":0,"k":[-37.161,0.089,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-22.725],[5.957,-16.983],[5.961,0],[5.958,17.391],[0,23.149],[-5.958,17.39],[-5.961,0],[-5.957,-16.998]],"c":true}]},{"t":30,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-5.961],[5.957,-0.219],[5.961,0],[5.958,0.203],[0,5.961],[-5.958,0.203],[-5.961,0],[-5.957,-0.235]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":5,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-37.161,0.089],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[102.5,84,0],"ix":2,"l":2},"a":{"a":0,"k":[-37.161,0.089,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-38.225],[5.957,-32.483],[5.961,0],[5.958,32.016],[0,37.774],[-5.958,32.015],[-5.961,0],[-5.957,-32.498]],"c":true}]},{"t":30,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-5.961],[5.957,-0.219],[5.961,0],[5.958,0.203],[0,5.961],[-5.958,0.203],[-5.961,0],[-5.957,-0.235]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-37.161,0.089],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[65.75,84,0],"ix":2,"l":2},"a":{"a":0,"k":[-37.161,0.089,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-25.1],[5.957,-19.358],[5.961,0],[5.958,19.516],[0,25.274],[-5.958,19.515],[-5.961,0],[-5.957,-19.373]],"c":true}]},{"t":30,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-5.961],[5.957,-0.219],[5.961,0],[5.958,0.203],[0,5.961],[-5.958,0.203],[-5.961,0],[-5.957,-0.235]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-37.161,0.089],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[84,84,0],"ix":2,"l":2},"a":{"a":0,"k":[-37.161,0.089,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-18.6],[5.957,-12.858],[5.961,0],[5.958,13.141],[0,18.899],[-5.958,13.14],[-5.961,0],[-5.957,-12.873]],"c":true}]},{"t":30,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-5.961],[5.957,-0.219],[5.961,0],[5.958,0.203],[0,5.961],[-5.958,0.203],[-5.961,0],[-5.957,-0.235]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-37.161,0.089],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":5,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[47.25,84,0],"ix":2,"l":2},"a":{"a":0,"k":[-37.161,0.089,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-13.475],[5.957,-7.733],[5.961,0],[5.958,6.766],[0,12.524],[-5.958,6.765],[-5.961,0],[-5.957,-7.748]],"c":true}]},{"t":30,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-5.961],[5.957,-0.219],[5.961,0],[5.958,0.203],[0,5.961],[-5.958,0.203],[-5.961,0],[-5.957,-0.235]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[100]},{"t":5,"s":[0]}],"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-37.161,0.089],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":600,"st":0,"bm":0}],"markers":[]}
\ No newline at end of file diff --git a/packages/SystemUI/res/raw/audio_bars_playing.json b/packages/SystemUI/res/raw/audio_bars_playing.json new file mode 100644 index 000000000000..6ee8e1915f36 --- /dev/null +++ b/packages/SystemUI/res/raw/audio_bars_playing.json @@ -0,0 +1 @@ +{"v":"5.7.13","fr":60,"ip":0,"op":121,"w":168,"h":168,"nm":"audio_bars_playing","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[120.75,84,0],"ix":2,"l":2},"a":{"a":0,"k":[-37.161,0.089,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-22.725],[5.957,-16.983],[5.961,0],[5.958,17.391],[0,23.149],[-5.958,17.39],[-5.961,0],[-5.957,-16.998]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":38,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[-0.016,-14.1],[5.941,-8.358],[5.961,0],[5.958,8.516],[0,14.274],[-5.958,8.515],[-5.961,0],[-5.972,-8.373]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":70,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-22.725],[5.957,-16.983],[5.961,0],[5.958,17.391],[0,23.149],[-5.958,17.39],[-5.961,0],[-5.957,-16.998]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":102,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[-0.016,-14.1],[5.941,-8.358],[5.961,0],[5.958,8.516],[0,14.274],[-5.958,8.515],[-5.961,0],[-5.972,-8.373]],"c":true}]},{"t":120,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-22.725],[5.957,-16.983],[5.961,0],[5.958,17.391],[0,23.149],[-5.958,17.39],[-5.961,0],[-5.957,-16.998]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-37.161,0.089],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":121,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[102.5,84,0],"ix":2,"l":2},"a":{"a":0,"k":[-37.161,0.089,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-38.225],[5.957,-32.483],[5.961,0],[5.958,32.016],[0,37.774],[-5.958,32.015],[-5.961,0],[-5.957,-32.498]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":32,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-19.1],[5.957,-13.358],[5.961,0],[5.958,13.641],[0,19.399],[-5.958,13.64],[-5.961,0],[-5.957,-13.373]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":65,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-38.225],[5.957,-32.483],[5.961,0],[5.958,32.016],[0,37.774],[-5.958,32.015],[-5.961,0],[-5.957,-32.498]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":97,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-19.1],[5.957,-13.358],[5.961,0],[5.958,13.641],[0,19.399],[-5.958,13.64],[-5.961,0],[-5.957,-13.373]],"c":true}]},{"t":120,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-38.225],[5.957,-32.483],[5.961,0],[5.958,32.016],[0,37.774],[-5.958,32.015],[-5.961,0],[-5.957,-32.498]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-37.161,0.089],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":121,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[65.75,84,0],"ix":2,"l":2},"a":{"a":0,"k":[-37.161,0.089,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-25.1],[5.957,-19.358],[5.961,0],[5.958,19.516],[0,25.274],[-5.958,19.515],[-5.961,0],[-5.957,-19.373]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":29,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-15.85],[5.957,-10.108],[5.961,0],[5.958,10.516],[0,16.274],[-5.958,10.515],[-5.961,0],[-5.957,-10.123]],"c":true}]},{"i":{"x":0.833,"y":1},"o":{"x":0.333,"y":0},"t":59,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-25.1],[5.957,-19.358],[5.961,0],[5.958,19.516],[0,25.274],[-5.958,19.515],[-5.961,0],[-5.957,-19.373]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":91,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-15.85],[5.957,-10.108],[5.961,0],[5.958,10.516],[0,16.274],[-5.958,10.515],[-5.961,0],[-5.957,-10.123]],"c":true}]},{"t":120,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-25.1],[5.957,-19.358],[5.961,0],[5.958,19.516],[0,25.274],[-5.958,19.515],[-5.961,0],[-5.957,-19.373]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-37.161,0.089],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":121,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[84,84,0],"ix":2,"l":2},"a":{"a":0,"k":[-37.161,0.089,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-18.6],[5.957,-12.858],[5.961,0],[5.958,13.141],[0,18.899],[-5.958,13.14],[-5.961,0],[-5.957,-12.873]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":24,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-9.225],[5.957,-3.483],[5.961,0],[5.958,3.766],[0,9.524],[-5.958,3.765],[-5.961,0],[-5.957,-3.498]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":54,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-18.6],[5.957,-12.858],[5.961,0],[5.958,13.141],[0,18.899],[-5.958,13.14],[-5.961,0],[-5.957,-12.873]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":86,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-9.225],[5.957,-3.483],[5.961,0],[5.958,3.766],[0,9.524],[-5.958,3.765],[-5.961,0],[-5.957,-3.498]],"c":true}]},{"t":120,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-18.6],[5.957,-12.858],[5.961,0],[5.958,13.141],[0,18.899],[-5.958,13.14],[-5.961,0],[-5.957,-12.873]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-37.161,0.089],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":121,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[47.25,84,0],"ix":2,"l":2},"a":{"a":0,"k":[-37.161,0.089,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-13.475],[5.957,-7.733],[5.961,0],[5.958,6.766],[0,12.524],[-5.958,6.765],[-5.961,0],[-5.957,-7.748]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":19,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-5.961],[5.957,-0.219],[5.961,0],[5.958,0.203],[0,5.961],[-5.958,0.203],[-5.961,0],[-5.957,-0.235]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":48,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-13.475],[5.957,-7.733],[5.961,0],[5.958,6.766],[0,12.524],[-5.958,6.765],[-5.961,0],[-5.957,-7.748]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":81,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-5.961],[5.957,-0.219],[5.961,0],[5.958,0.203],[0,5.961],[-5.958,0.203],[-5.961,0],[-5.957,-0.235]],"c":true}]},{"t":120,"s":[{"i":[[-3.214,0],[-0.115,-3.191],[0,-0.073],[0.002,-0.067],[3.224,0],[0.107,3.198],[0,0.068],[-0.003,0.078]],"o":[[3.219,0],[0.003,0.073],[0,0.068],[-0.107,3.198],[-3.224,0],[-0.002,-0.067],[0,-0.079],[0.123,-3.184]],"v":[[0,-13.475],[5.957,-7.733],[5.961,0],[5.958,6.766],[0,12.524],[-5.958,6.765],[-5.961,0],[-5.957,-7.748]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-37.161,0.089],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":121,"st":0,"bm":0}],"markers":[{"tm":60,"cm":"1","dr":0}]}
\ No newline at end of file diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml index c1eff5f629b3..a77f5e4629c1 100644 --- a/packages/SystemUI/res/values-night/colors.xml +++ b/packages/SystemUI/res/values-night/colors.xml @@ -108,6 +108,9 @@ <color name="people_tile_background">@color/material_dynamic_secondary20</color> + <!-- Dark Theme colors for notification shade/scrim --> + <color name="shade_panel">@android:color/system_accent1_900</color> + <!-- Keyboard shortcut helper dialog --> <color name="ksh_key_item_color">@*android:color/system_on_surface_variant_dark</color> </resources> diff --git a/packages/SystemUI/res/values-sw600dp/config.xml b/packages/SystemUI/res/values-sw600dp/config.xml index b4383156dc71..ec24c3df36a8 100644 --- a/packages/SystemUI/res/values-sw600dp/config.xml +++ b/packages/SystemUI/res/values-sw600dp/config.xml @@ -19,10 +19,13 @@ <!-- These resources are around just to allow their values to be customized for different hardware and product builds. --> -<resources> +<resources xmlns:android="http://schemas.android.com/apk/res/android"> <!-- The maximum number of rows in the QuickSettings --> <integer name="quick_settings_max_rows">4</integer> + <!-- The number of columns in the Split Shade QuickSettings --> + <integer name="quick_settings_split_shade_num_columns">6</integer> + <!-- Use collapsed layout for media player in landscape QQS --> <bool name="config_quickSettingsMediaLandscapeCollapsed">false</bool> @@ -51,7 +54,9 @@ ignored. --> <string-array name="config_keyguardQuickAffordanceDefaults" translatable="false"> <item>bottom_start:home</item> - <item>bottom_end:create_note</item> + <!-- TODO(b/384119565): revisit decision on defaults --> + <item android:featureFlag="!com.android.systemui.glanceable_hub_v2_resources">bottom_end:create_note</item> + <item android:featureFlag="com.android.systemui.glanceable_hub_v2_resources">bottom_end:glanceable_hub</item> </string-array> <!-- Whether volume panel should use the large screen layout or not --> diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 28df2e2a1b8c..d2b7d0b90c43 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -31,6 +31,9 @@ <!-- The dark background color behind the shade --> <color name="shade_scrim_background_dark">@androidprv:color/system_under_surface_light</color> + <!-- Colors for notification shade/scrim --> + <color name="shade_panel">@android:color/system_accent1_800</color> + <!-- The color of the background in the separated list of the Global Actions menu --> <color name="global_actions_separated_background">#F5F5F5</color> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 56aaf4c0c564..c3d84ff39485 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1004,6 +1004,20 @@ <string name="hearing_devices_preset_label">Preset</string> <!-- QuickSettings: Content description for the icon that indicates the item is selected [CHAR LIMIT=NONE]--> <string name="hearing_devices_spinner_item_selected">Selected</string> + <!-- QuickSettings: Title for ambient controls. [CHAR LIMIT=40]--> + <string name="hearing_devices_ambient_label">Surroundings</string> + <!-- QuickSettings: The text to show the control is for left side device. [CHAR LIMIT=30] --> + <string name="hearing_devices_ambient_control_left">Left</string> + <!-- QuickSettings: The text to show the control is for right side device. [CHAR LIMIT=30] --> + <string name="hearing_devices_ambient_control_right">Right</string> + <!-- QuickSettings: Content description for a button, that expands ambient volume sliders [CHAR_LIMIT=NONE] --> + <string name="hearing_devices_ambient_expand_controls">Expand to left and right separated controls</string> + <!-- QuickSettings: Content description for a button, that collapses ambient volume sliders [CHAR LIMIT=NONE] --> + <string name="hearing_devices_ambient_collapse_controls">Collapse to unified control</string> + <!-- QuickSettings: Content description for a button, that mute ambient volume [CHAR_LIMIT=NONE] --> + <string name="hearing_devices_ambient_mute">Mute surroundings</string> + <!-- QuickSettings: Content description for a button, that unmute ambient volume [CHAR LIMIT=NONE] --> + <string name="hearing_devices_ambient_unmute">Unmute surroundings</string> <!-- QuickSettings: Title for related tools of hearing. [CHAR LIMIT=40]--> <string name="hearing_devices_tools_label">Tools</string> <!-- QuickSettings: Tool name for hearing devices dialog related tools [CHAR LIMIT=40] [BACKUP_MESSAGE_ID=8916875614623730005]--> @@ -2504,14 +2518,14 @@ <!-- Accessibility description of action to remove QS tile on click. It will read as "Double-tap to remove tile" in screen readers [CHAR LIMIT=NONE] --> <string name="accessibility_qs_edit_remove_tile_action">remove tile</string> - <!-- Accessibility action of action to add QS tile to end. It will read as "Double-tap to add tile to end" in screen readers [CHAR LIMIT=NONE] --> - <string name="accessibility_qs_edit_tile_add_action">add tile to end</string> + <!-- Accessibility action of action to add QS tile to end. It will read as "Double-tap to add tile to the last position" in screen readers [CHAR LIMIT=NONE] --> + <string name="accessibility_qs_edit_tile_add_action">add tile to the last position</string> <!-- Accessibility action for context menu to move QS tile [CHAR LIMIT=NONE] --> <string name="accessibility_qs_edit_tile_start_move">Move tile</string> - <!-- Accessibility action for context menu to add QS tile [CHAR LIMIT=NONE] --> - <string name="accessibility_qs_edit_tile_start_add">Add tile</string> + <!-- Accessibility action for context menu to add QS tile to a position [CHAR LIMIT=NONE] --> + <string name="accessibility_qs_edit_tile_start_add">Add tile to desired position</string> <!-- Accessibility description when QS tile is to be moved, indicating the destination position [CHAR LIMIT=NONE] --> <string name="accessibility_qs_edit_tile_move_to_position">Move to <xliff:g id="position" example="5">%1$d</xliff:g></string> @@ -2564,7 +2578,7 @@ <string name="accessibility_quick_settings_open_settings">Open <xliff:g name="page" example="Bluetooth">%s</xliff:g> settings.</string> <!-- accessibility label for button to edit quick settings [CHAR LIMIT=NONE] --> - <string name="accessibility_quick_settings_edit">Edit order of settings.</string> + <string name="accessibility_quick_settings_edit">Edit order of Quick Settings.</string> <!-- accessibility label for button to open power menu [CHAR LIMIT=NONE] --> <string name="accessibility_quick_settings_power_menu">Power menu</string> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 3156a50df96f..f6c1ecea2886 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -559,15 +559,18 @@ <style name="SystemUI.Material3.Slider.Volume"> <item name="trackHeight">40dp</item> <item name="thumbHeight">52dp</item> + <item name="trackCornerSize">12dp</item> + <item name="trackInsideCornerSize">2dp</item> + <item name="trackStopIndicatorSize">6dp</item> </style> <style name="SystemUI.Material3.Slider" parent="@style/Widget.Material3.Slider"> <item name="labelStyle">@style/Widget.Material3.Slider.Label</item> - <item name="thumbColor">@color/slider_thumb_color</item> - <item name="tickColorActive">@color/slider_inactive_track_color</item> - <item name="tickColorInactive">@color/slider_active_track_color</item> - <item name="trackColorActive">@color/slider_active_track_color</item> - <item name="trackColorInactive">@color/slider_inactive_track_color</item> + <item name="thumbColor">@androidprv:color/materialColorPrimary</item> + <item name="tickColorActive">@androidprv:color/materialColorSurfaceContainerHighest</item> + <item name="tickColorInactive">@androidprv:color/materialColorPrimary</item> + <item name="trackColorActive">@androidprv:color/materialColorPrimary</item> + <item name="trackColorInactive">@androidprv:color/materialColorSurfaceContainerHighest</item> </style> <style name="Theme.SystemUI.DayNightDialog" parent="@android:style/Theme.DeviceDefault.Light.Dialog"/> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java index fc536bdb126b..6f13d637d5c5 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java @@ -20,6 +20,7 @@ import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; +import static com.android.systemui.Flags.glanceableHubBackAction; import static com.android.systemui.shared.Flags.shadeAllowBackGesture; import android.annotation.LongDef; @@ -352,6 +353,10 @@ public class QuickStepContract { } // Disable back gesture on the hub, but not when the shade is showing. if ((sysuiStateFlags & SYSUI_STATE_COMMUNAL_HUB_SHOWING) != 0) { + // Allow back gesture on Glanceable Hub with back action support. + if (glanceableHubBackAction()) { + return false; + } // Use QS expanded signal as the notification panel is always considered visible // expanded when on the lock screen and when opening hub over lock screen. This does // mean that back gesture is disabled when opening shade over hub while in portrait diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 5af80cbd4b29..71b622aa0608 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -43,7 +43,6 @@ import com.android.systemui.dagger.qualifiers.DisplaySpecific import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags.REGION_SAMPLING -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.Edge @@ -85,8 +84,8 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge /** - * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by - * [KeyguardClockSwitchController]. Functionality is forked from [AnimatableClockController]. + * Controller for a Clock provided by the registry and used on the keyguard. Functionality is forked + * from [AnimatableClockController]. */ open class ClockEventController @Inject @@ -348,14 +347,6 @@ constructor( object : KeyguardUpdateMonitorCallback() { override fun onKeyguardVisibilityChanged(visible: Boolean) { isKeyguardVisible = visible - if (!MigrateClocksToBlueprint.isEnabled) { - if (!isKeyguardVisible) { - clock?.run { - smallClock.animations.doze(if (isDozing) 1f else 0f) - largeClock.animations.doze(if (isDozing) 1f else 0f) - } - } - } if (visible) { refreshTime() @@ -388,10 +379,6 @@ constructor( } private fun refreshTime() { - if (!MigrateClocksToBlueprint.isEnabled) { - return - } - clock?.smallClock?.events?.onTimeTick() clock?.largeClock?.events?.onTimeTick() } @@ -483,14 +470,10 @@ constructor( if (ModesUi.isEnabled) { listenForDnd(this) } - if (MigrateClocksToBlueprint.isEnabled) { - listenForDozeAmountTransition(this) - listenForAnyStateToAodTransition(this) - listenForAnyStateToLockscreenTransition(this) - listenForAnyStateToDozingTransition(this) - } else { - listenForDozeAmount(this) - } + listenForDozeAmountTransition(this) + listenForAnyStateToAodTransition(this) + listenForAnyStateToLockscreenTransition(this) + listenForAnyStateToDozingTransition(this) } } smallTimeListener?.update(shouldTimeListenerRun) @@ -596,11 +579,6 @@ constructor( } @VisibleForTesting - internal fun listenForDozeAmount(scope: CoroutineScope): Job { - return scope.launch { keyguardInteractor.dozeAmount.collect { handleDoze(it) } } - } - - @VisibleForTesting internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job { return scope.launch { merge( @@ -695,8 +673,7 @@ constructor( isRunning = true when (clockFace.config.tickRate) { ClockTickRate.PER_MINUTE -> { - // Handled by KeyguardClockSwitchController and - // by KeyguardUpdateMonitorCallback#onTimeChanged. + // Handled by KeyguardUpdateMonitorCallback#onTimeChanged. } ClockTickRate.PER_SECOND -> executor.execute(secondsRunnable) ClockTickRate.PER_FRAME -> { diff --git a/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt b/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt index df77a58c3b34..3f332f769c6e 100644 --- a/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt +++ b/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt @@ -23,14 +23,11 @@ import android.graphics.Rect import android.os.Bundle import android.view.Display import android.view.Gravity -import android.view.LayoutInflater import android.view.View import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.view.WindowManager import android.widget.FrameLayout import android.widget.FrameLayout.LayoutParams -import com.android.keyguard.dagger.KeyguardStatusViewComponent -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockFaceController import com.android.systemui.res.R @@ -45,7 +42,6 @@ class ConnectedDisplayKeyguardPresentation constructor( @Assisted display: Display, context: Context, - private val keyguardStatusViewComponentFactory: KeyguardStatusViewComponent.Factory, private val clockRegistry: ClockRegistry, private val clockEventController: ClockEventController, ) : @@ -53,12 +49,11 @@ constructor( context, display, R.style.Theme_SystemUI_KeyguardPresentation, - WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG + WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG, ) { private lateinit var rootView: FrameLayout private var clock: View? = null - private lateinit var keyguardStatusViewController: KeyguardStatusViewController private lateinit var faceController: ClockFaceController private lateinit var clockFrame: FrameLayout @@ -82,7 +77,7 @@ constructor( oldLeft: Int, oldTop: Int, oldRight: Int, - oldBottom: Int + oldBottom: Int, ) { clock?.let { faceController.events.onTargetRegionChanged( @@ -95,11 +90,7 @@ constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (MigrateClocksToBlueprint.isEnabled) { - onCreateV2() - } else { - onCreate() - } + onCreateV2() } fun onCreateV2() { @@ -112,39 +103,15 @@ constructor( setClock(clockRegistry.createCurrentClock()) } - fun onCreate() { - setContentView( - LayoutInflater.from(context) - .inflate(R.layout.keyguard_clock_presentation, /* root= */ null) - ) - - setFullscreen() - - clock = requireViewById(R.id.clock) - keyguardStatusViewController = - keyguardStatusViewComponentFactory - .build(clock as KeyguardStatusView, display) - .keyguardStatusViewController - .apply { - setDisplayedOnSecondaryDisplay() - init() - } - } - override fun onAttachedToWindow() { - if (MigrateClocksToBlueprint.isEnabled) { - clockRegistry.registerClockChangeListener(clockChangedListener) - clockEventController.registerListeners(clock!!) - - faceController.animations.enter() - } + clockRegistry.registerClockChangeListener(clockChangedListener) + clockEventController.registerListeners(clock!!) + faceController.animations.enter() } override fun onDetachedFromWindow() { - if (MigrateClocksToBlueprint.isEnabled) { - clockEventController.unregisterListeners() - clockRegistry.unregisterClockChangeListener(clockChangedListener) - } + clockEventController.unregisterListeners() + clockRegistry.unregisterClockChangeListener(clockChangedListener) super.onDetachedFromWindow() } @@ -166,7 +133,7 @@ constructor( context.resources.getDimensionPixelSize(R.dimen.keyguard_presentation_width), WRAP_CONTENT, Gravity.CENTER, - ) + ), ) clockEventController.clock = clockController @@ -190,8 +157,6 @@ constructor( @AssistedFactory interface Factory { /** Creates a new [Presentation] for the given [display]. */ - fun create( - display: Display, - ): ConnectedDisplayKeyguardPresentation + fun create(display: Display): ConnectedDisplayKeyguardPresentation } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java index 1083136b570a..acfa08643b63 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java @@ -26,11 +26,9 @@ import android.hardware.display.DisplayManager; import android.media.MediaRouter; import android.media.MediaRouter.RouteInfo; import android.os.Trace; -import android.text.TextUtils; import android.util.Log; import android.util.SparseArray; import android.view.Display; -import android.view.DisplayAddress; import android.view.DisplayInfo; import android.view.View; import android.view.WindowManager; @@ -58,6 +56,9 @@ import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Provider; +/** + * Manages Keyguard Presentations for non-primary display(s). + */ @SysUISingleton public class KeyguardDisplayManager { protected static final String TAG = "KeyguardDisplayManager"; @@ -170,14 +171,17 @@ public class KeyguardDisplayManager { } return false; } - if (mKeyguardStateController.isOccluded() - && mDeviceStateHelper.isConcurrentDisplayActive(display)) { + + final boolean deviceStateOccludesKeyguard = + mDeviceStateHelper.isConcurrentDisplayActive(display) + || mDeviceStateHelper.isRearDisplayOuterDefaultActive(display); + if (mKeyguardStateController.isOccluded() && deviceStateOccludesKeyguard) { if (DEBUG) { // When activities with FLAG_SHOW_WHEN_LOCKED are shown on top of Keyguard, the // Keyguard state becomes "occluded". In this case, we should not show the // KeyguardPresentation, since the activity is presenting content onto the // non-default display. - Log.i(TAG, "Do not show KeyguardPresentation when occluded and concurrent" + Log.i(TAG, "Do not show KeyguardPresentation when occluded and concurrent or rear" + " display is active"); } return false; @@ -326,44 +330,45 @@ public class KeyguardDisplayManager { public static class DeviceStateHelper implements DeviceStateManager.DeviceStateCallback { @Nullable - private final DisplayAddress.Physical mRearDisplayPhysicalAddress; - - // TODO(b/271317597): These device states should be defined in DeviceStateManager - private final int mConcurrentState; - private boolean mIsInConcurrentDisplayState; + private DeviceState mDeviceState; @Inject DeviceStateHelper( - @ShadeDisplayAware Context context, DeviceStateManager deviceStateManager, @Main Executor mainExecutor) { - - final String rearDisplayPhysicalAddress = context.getResources().getString( - com.android.internal.R.string.config_rearDisplayPhysicalAddress); - if (TextUtils.isEmpty(rearDisplayPhysicalAddress)) { - mRearDisplayPhysicalAddress = null; - } else { - mRearDisplayPhysicalAddress = DisplayAddress - .fromPhysicalDisplayId(Long.parseLong(rearDisplayPhysicalAddress)); - } - - mConcurrentState = context.getResources().getInteger( - com.android.internal.R.integer.config_deviceStateConcurrentRearDisplay); deviceStateManager.registerCallback(mainExecutor, this); } @Override public void onDeviceStateChanged(@NonNull DeviceState state) { - // When concurrent state ends, the display also turns off. This is enforced in various - // ExtensionRearDisplayPresentationTest CTS tests. So, we don't need to invoke - // hide() since that will happen through the onDisplayRemoved callback. - mIsInConcurrentDisplayState = state.getIdentifier() == mConcurrentState; + // When dual display or rear display mode ends, the display also turns off. This is + // enforced in various ExtensionRearDisplayPresentationTest CTS tests. So, we don't need + // to invoke hide() since that will happen through the onDisplayRemoved callback. + mDeviceState = state; + } + + /** + * @return true if the device is in Dual Display mode, and the specified display is the + * rear facing (outer) display. + */ + boolean isConcurrentDisplayActive(@NonNull Display display) { + return mDeviceState != null + && mDeviceState.hasProperty( + DeviceState.PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT) + && (display.getFlags() & Display.FLAG_REAR) != 0; } - boolean isConcurrentDisplayActive(Display display) { - return mIsInConcurrentDisplayState - && mRearDisplayPhysicalAddress != null - && mRearDisplayPhysicalAddress.equals(display.getAddress()); + /** + * @return true if the device is the updated Rear Display mode, and the specified display is + * the inner display. See {@link DeviceState.PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT}. + * Note that in this state, the outer display is the default display, while the inner + * display is the "rear" display. + */ + boolean isRearDisplayOuterDefaultActive(@NonNull Display display) { + return mDeviceState != null + && mDeviceState.hasProperty( + DeviceState.PROPERTY_FEATURE_REAR_DISPLAY_OUTER_DEFAULT) + && (display.getFlags() & Display.FLAG_REAR) != 0; } } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt index 07bd813c2420..40a86dc3713e 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt @@ -19,13 +19,12 @@ package com.android.keyguard import android.content.Context import android.view.View import com.android.systemui.customization.R as customR -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.ui.view.KeyguardRootView import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.res.R -import com.android.systemui.shared.R as sharedR import com.android.systemui.shade.NotificationShadeWindowView import com.android.systemui.shade.ShadeDisplayAware +import com.android.systemui.shared.R as sharedR import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.END import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.START @@ -55,16 +54,17 @@ constructor( var statusViewCentered = false private val filterKeyguardAndSplitShadeOnly: () -> Boolean = { - statusBarStateController.getState() == KEYGUARD && !statusViewCentered } + statusBarStateController.getState() == KEYGUARD && !statusViewCentered + } private val filterKeyguard: () -> Boolean = { statusBarStateController.getState() == KEYGUARD } private val translateAnimator by lazy { - val smartSpaceViews = if (MigrateClocksToBlueprint.isEnabled) { - // Use scrollX instead of translationX as translation is already set by [AodBurnInLayer] - val scrollXTranslation = { view: View, translation: Float -> - view.scrollX = -translation.toInt() - } + // Use scrollX instead of translationX as translation is already set by [AodBurnInLayer] + val scrollXTranslation = { view: View, translation: Float -> + view.scrollX = -translation.toInt() + } + val smartSpaceViews = setOf( ViewIdToTranslate( viewId = sharedR.id.date_smartspace_view, @@ -83,18 +83,8 @@ constructor( direction = START, shouldBeAnimated = filterKeyguard, translateFunc = scrollXTranslation, - ) + ), ) - } else { - setOf(ViewIdToTranslate( - viewId = R.id.keyguard_status_area, - direction = START, - shouldBeAnimated = filterKeyguard, - translateFunc = { view, value -> - (view as? KeyguardStatusAreaView)?.translateXFromUnfold = value - } - )) - } UnfoldConstantTranslateAnimator( viewsIdToTranslate = @@ -102,39 +92,39 @@ constructor( ViewIdToTranslate( viewId = customR.id.lockscreen_clock_view_large, direction = START, - shouldBeAnimated = filterKeyguardAndSplitShadeOnly + shouldBeAnimated = filterKeyguardAndSplitShadeOnly, ), ViewIdToTranslate( viewId = customR.id.lockscreen_clock_view, direction = START, - shouldBeAnimated = filterKeyguard + shouldBeAnimated = filterKeyguard, ), ViewIdToTranslate( viewId = R.id.notification_stack_scroller, direction = END, - shouldBeAnimated = filterKeyguardAndSplitShadeOnly - ) + shouldBeAnimated = filterKeyguardAndSplitShadeOnly, + ), ) + smartSpaceViews, - progressProvider = unfoldProgressProvider + progressProvider = unfoldProgressProvider, ) } private val shortcutButtonsAnimator by lazy { UnfoldConstantTranslateAnimator( viewsIdToTranslate = - setOf( - ViewIdToTranslate( - viewId = R.id.start_button, - direction = START, - shouldBeAnimated = filterKeyguard + setOf( + ViewIdToTranslate( + viewId = R.id.start_button, + direction = START, + shouldBeAnimated = filterKeyguard, + ), + ViewIdToTranslate( + viewId = R.id.end_button, + direction = END, + shouldBeAnimated = filterKeyguard, + ), ), - ViewIdToTranslate( - viewId = R.id.end_button, - direction = END, - shouldBeAnimated = filterKeyguard - ) - ), - progressProvider = unfoldProgressProvider + progressProvider = unfoldProgressProvider, ) } diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java index 0305b5e5ab63..e76f38c8c75c 100644 --- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java +++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java @@ -26,7 +26,6 @@ import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; -import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.plugins.PluginManager; import com.android.systemui.plugins.clocks.ClockMessageBuffers; import com.android.systemui.res.R; @@ -70,7 +69,7 @@ public abstract class ClockRegistryModule { context, layoutInflater, resources, - MigrateClocksToBlueprint.isEnabled(), + com.android.systemui.Flags.clockReactiveVariants() ), context.getString(R.string.lockscreen_clock_id_fallback), diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeLayout.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeLayout.java new file mode 100644 index 000000000000..7c141c1b561e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeLayout.java @@ -0,0 +1,322 @@ +/* + * 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.accessibility.hearingaid; + +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_LEFT; +import static com.android.settingslib.bluetooth.HearingAidInfo.DeviceSide.SIDE_RIGHT; + +import android.bluetooth.BluetoothDevice; +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import com.android.settingslib.bluetooth.AmbientVolumeUi; +import com.android.systemui.res.R; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.google.common.primitives.Ints; + +import java.util.Map; + +/** + * A view of ambient volume controls. + * + * <p> It consists of a header with an expand icon and volume sliders for unified control and + * separated control for devices in the same set. Toggle the expand icon will make the UI switch + * between unified and separated control. + */ +public class AmbientVolumeLayout extends LinearLayout implements AmbientVolumeUi { + + @Nullable + private AmbientVolumeUiListener mListener; + private ImageView mExpandIcon; + private ImageView mVolumeIcon; + private boolean mExpandable = true; + private boolean mExpanded = false; + private boolean mMutable = false; + private boolean mMuted = false; + private final BiMap<Integer, AmbientVolumeSlider> mSideToSliderMap = HashBiMap.create(); + private int mVolumeLevel = AMBIENT_VOLUME_LEVEL_DEFAULT; + + private final AmbientVolumeSlider.OnChangeListener mSliderOnChangeListener = + (slider, value) -> { + if (mListener != null) { + final int side = mSideToSliderMap.inverse().get(slider); + mListener.onSliderValueChange(side, value); + } + }; + + public AmbientVolumeLayout(@Nullable Context context) { + this(context, /* attrs= */ null); + } + + public AmbientVolumeLayout(@Nullable Context context, @Nullable AttributeSet attrs) { + this(context, attrs, /* defStyleAttr= */ 0); + } + + public AmbientVolumeLayout(@Nullable Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + this(context, attrs, defStyleAttr, /* defStyleRes= */ 0); + } + + public AmbientVolumeLayout(@Nullable Context context, @Nullable AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + inflate(context, R.layout.hearing_device_ambient_volume_layout, /* root= */ this); + init(); + } + + private void init() { + mVolumeIcon = requireViewById(R.id.ambient_volume_icon); + mVolumeIcon.setImageResource(com.android.settingslib.R.drawable.ic_ambient_volume); + mVolumeIcon.setOnClickListener(v -> { + if (!mMutable) { + return; + } + setMuted(!mMuted); + if (mListener != null) { + mListener.onAmbientVolumeIconClick(); + } + }); + updateVolumeIcon(); + + mExpandIcon = requireViewById(R.id.ambient_expand_icon); + mExpandIcon.setOnClickListener(v -> { + setExpanded(!mExpanded); + if (mListener != null) { + mListener.onExpandIconClick(); + } + }); + updateExpandIcon(); + } + + @Override + public void setVisible(boolean visible) { + setVisibility(visible ? VISIBLE : GONE); + } + + @Override + public void setExpandable(boolean expandable) { + mExpandable = expandable; + if (!mExpandable) { + setExpanded(false); + } + updateExpandIcon(); + } + + @Override + public boolean isExpandable() { + return mExpandable; + } + + @Override + public void setExpanded(boolean expanded) { + if (!mExpandable && expanded) { + return; + } + mExpanded = expanded; + updateExpandIcon(); + updateLayout(); + } + + @Override + public boolean isExpanded() { + return mExpanded; + } + + @Override + public void setMutable(boolean mutable) { + mMutable = mutable; + if (!mMutable) { + mVolumeLevel = AMBIENT_VOLUME_LEVEL_DEFAULT; + setMuted(false); + } + updateVolumeIcon(); + } + + @Override + public boolean isMutable() { + return mMutable; + } + + @Override + public void setMuted(boolean muted) { + if (!mMutable && muted) { + return; + } + mMuted = muted; + if (mMutable && mMuted) { + for (AmbientVolumeSlider slider : mSideToSliderMap.values()) { + slider.setValue(slider.getMin()); + } + } + updateVolumeIcon(); + } + + @Override + public boolean isMuted() { + return mMuted; + } + + @Override + public void setListener(@Nullable AmbientVolumeUiListener listener) { + mListener = listener; + } + + @Override + public void setupSliders(@NonNull Map<Integer, BluetoothDevice> sideToDeviceMap) { + sideToDeviceMap.forEach((side, device) -> createSlider(side)); + createSlider(SIDE_UNIFIED); + + LinearLayout controlContainer = requireViewById(R.id.ambient_control_container); + controlContainer.removeAllViews(); + if (!mSideToSliderMap.isEmpty()) { + for (int side : VALID_SIDES) { + final AmbientVolumeSlider slider = mSideToSliderMap.get(side); + if (slider != null) { + controlContainer.addView(slider); + } + } + } + updateLayout(); + } + + @Override + public void setSliderEnabled(int side, boolean enabled) { + AmbientVolumeSlider slider = mSideToSliderMap.get(side); + if (slider != null && slider.isEnabled() != enabled) { + slider.setEnabled(enabled); + updateLayout(); + } + } + + @Override + public void setSliderValue(int side, int value) { + AmbientVolumeSlider slider = mSideToSliderMap.get(side); + if (slider != null && slider.getValue() != value) { + slider.setValue(value); + updateVolumeLevel(); + } + } + + @Override + public void setSliderRange(int side, int min, int max) { + AmbientVolumeSlider slider = mSideToSliderMap.get(side); + if (slider != null) { + slider.setMin(min); + slider.setMax(max); + } + } + + @Override + public void updateLayout() { + mSideToSliderMap.forEach((side, slider) -> { + if (side == SIDE_UNIFIED) { + slider.setVisibility(mExpanded ? GONE : VISIBLE); + } else { + slider.setVisibility(mExpanded ? VISIBLE : GONE); + } + if (!slider.isEnabled()) { + slider.setValue(slider.getMin()); + } + }); + updateVolumeLevel(); + } + + private void updateVolumeLevel() { + int leftLevel, rightLevel; + if (mExpanded) { + leftLevel = getVolumeLevel(SIDE_LEFT); + rightLevel = getVolumeLevel(SIDE_RIGHT); + } else { + final int unifiedLevel = getVolumeLevel(SIDE_UNIFIED); + leftLevel = unifiedLevel; + rightLevel = unifiedLevel; + } + mVolumeLevel = Ints.constrainToRange(leftLevel * 5 + rightLevel, + AMBIENT_VOLUME_LEVEL_MIN, AMBIENT_VOLUME_LEVEL_MAX); + updateVolumeIcon(); + } + + private int getVolumeLevel(int side) { + AmbientVolumeSlider slider = mSideToSliderMap.get(side); + if (slider == null || !slider.isEnabled()) { + return 0; + } + return slider.getVolumeLevel(); + } + + private void updateExpandIcon() { + mExpandIcon.setVisibility(mExpandable ? VISIBLE : GONE); + mExpandIcon.setRotation(mExpanded ? ROTATION_EXPANDED : ROTATION_COLLAPSED); + if (mExpandable) { + final int stringRes = mExpanded ? R.string.hearing_devices_ambient_collapse_controls + : R.string.hearing_devices_ambient_expand_controls; + mExpandIcon.setContentDescription(mContext.getString(stringRes)); + } else { + mExpandIcon.setContentDescription(null); + } + } + + private void updateVolumeIcon() { + mVolumeIcon.setImageLevel(mMuted ? 0 : mVolumeLevel); + if (mMutable) { + final int stringRes = mMuted ? R.string.hearing_devices_ambient_unmute + : R.string.hearing_devices_ambient_mute; + mVolumeIcon.setContentDescription(mContext.getString(stringRes)); + mVolumeIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + } else { + mVolumeIcon.setContentDescription(null); + mVolumeIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + } + } + + private void createSlider(int side) { + if (mSideToSliderMap.containsKey(side)) { + return; + } + AmbientVolumeSlider slider = new AmbientVolumeSlider(mContext); + slider.addOnChangeListener(mSliderOnChangeListener); + if (side == SIDE_LEFT) { + slider.setTitle(mContext.getString(R.string.hearing_devices_ambient_control_left)); + } else if (side == SIDE_RIGHT) { + slider.setTitle(mContext.getString(R.string.hearing_devices_ambient_control_right)); + } + mSideToSliderMap.put(side, slider); + } + + @VisibleForTesting + ImageView getVolumeIcon() { + return mVolumeIcon; + } + + @VisibleForTesting + ImageView getExpandIcon() { + return mExpandIcon; + } + + @VisibleForTesting + Map<Integer, AmbientVolumeSlider> getSliders() { + return mSideToSliderMap; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeSlider.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeSlider.java new file mode 100644 index 000000000000..92338ef3773c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeSlider.java @@ -0,0 +1,170 @@ +/* + * 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.accessibility.hearingaid; + +import android.content.Context; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.systemui.res.R; + +import com.google.android.material.slider.Slider; + +import java.util.ArrayList; +import java.util.List; + +/** + * A view of ambient volume slider. + * <p> It consists by a title {@link TextView} with a volume control {@link Slider}. + */ +public class AmbientVolumeSlider extends LinearLayout { + + private final TextView mTitle; + private final Slider mSlider; + private final List<OnChangeListener> mChangeListeners = new ArrayList<>(); + private final Slider.OnSliderTouchListener mSliderTouchListener = + new Slider.OnSliderTouchListener() { + @Override + public void onStartTrackingTouch(@NonNull Slider slider) { + } + + @Override + public void onStopTrackingTouch(@NonNull Slider slider) { + final int value = Math.round(slider.getValue()); + for (OnChangeListener listener : mChangeListeners) { + listener.onValueChange(AmbientVolumeSlider.this, value); + } + } + }; + public AmbientVolumeSlider(@Nullable Context context) { + this(context, /* attrs= */ null); + } + + public AmbientVolumeSlider(@Nullable Context context, @Nullable AttributeSet attrs) { + this(context, attrs, /* defStyleAttr= */ 0); + } + + public AmbientVolumeSlider(@Nullable Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + this(context, attrs, defStyleAttr, /* defStyleRes= */ 0); + } + + public AmbientVolumeSlider(@Nullable Context context, @Nullable AttributeSet attrs, + int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + + inflate(context, R.layout.hearing_device_ambient_volume_slider, /* root= */ this); + mTitle = requireViewById(R.id.ambient_volume_slider_title); + mSlider = requireViewById(R.id.ambient_volume_slider); + mSlider.addOnSliderTouchListener(mSliderTouchListener); + } + + /** + * Sets title for the ambient volume slider. + * <p> If text is null or empty, then {@link TextView} is hidden. + */ + public void setTitle(@Nullable String text) { + mTitle.setText(text); + mTitle.setVisibility(TextUtils.isEmpty(text) ? GONE : VISIBLE); + } + + /** Gets title for the ambient volume slider. */ + public CharSequence getTitle() { + return mTitle.getText(); + } + + /** + * Adds the callback to the ambient volume slider to get notified when the value is changed by + * user. + * <p> Note: The {@link OnChangeListener#onValueChange(AmbientVolumeSlider, int)} will be + * called when user's finger take off from the slider. + */ + public void addOnChangeListener(@Nullable OnChangeListener listener) { + if (listener == null) { + return; + } + mChangeListeners.add(listener); + } + + /** Sets max value to the ambient volume slider. */ + public void setMax(float max) { + mSlider.setValueTo(max); + } + + /** Gets max value from the ambient volume slider. */ + public float getMax() { + return mSlider.getValueTo(); + } + + /** Sets min value to the ambient volume slider. */ + public void setMin(float min) { + mSlider.setValueFrom(min); + } + + /** Gets min value from the ambient volume slider. */ + public float getMin() { + return mSlider.getValueFrom(); + } + + /** Sets value to the ambient volume slider. */ + public void setValue(float value) { + mSlider.setValue(value); + } + + /** Gets value from the ambient volume slider. */ + public float getValue() { + return mSlider.getValue(); + } + + /** Sets the enable state to the ambient volume slider. */ + public void setEnabled(boolean enabled) { + mSlider.setEnabled(enabled); + } + + /** Gets the enable state of the ambient volume slider. */ + public boolean isEnabled() { + return mSlider.isEnabled(); + } + + /** + * Gets the volume value of the ambient volume slider. + * <p> The volume level is divided into 5 levels: + * Level 0 corresponds to the minimum volume value. The range between the minimum and maximum + * volume is divided into 4 equal intervals, represented by levels 1 to 4. + */ + public int getVolumeLevel() { + if (!mSlider.isEnabled()) { + return 0; + } + final double min = mSlider.getValueFrom(); + final double max = mSlider.getValueTo(); + final double levelGap = (max - min) / 4.0; + final double value = mSlider.getValue(); + return (int) Math.ceil((value - min) / levelGap); + } + + /** Interface definition for a callback invoked when a slider's value is changed. */ + public interface OnChangeListener { + /** Called when the finger is take off from the slider. */ + void onValueChange(@NonNull AmbientVolumeSlider slider, int value); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java index 56435df1ad2c..73aabc3cf95a 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java @@ -52,10 +52,12 @@ import androidx.annotation.VisibleForTesting; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import com.android.settingslib.bluetooth.AmbientVolumeUiController; import com.android.settingslib.bluetooth.BluetoothCallback; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.LocalBluetoothManager; import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; +import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.accessibility.hearingaid.HearingDevicesListAdapter.HearingDeviceItemCallback; import com.android.systemui.animation.DialogTransitionAnimator; import com.android.systemui.bluetooth.qsdialog.ActiveHearingDeviceItemFactory; @@ -108,7 +110,6 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, private SystemUIDialog mDialog; - private RecyclerView mDeviceList; private List<DeviceItem> mHearingDeviceItemList; private HearingDevicesListAdapter mDeviceListAdapter; @@ -134,6 +135,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, } }; + private AmbientVolumeUiController mAmbientController; + private final List<DeviceItemFactory> mHearingDeviceItemFactoryList = List.of( new ActiveHearingDeviceItemFactory(), new AvailableHearingDeviceItemFactory(), @@ -225,13 +228,17 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, public void onActiveDeviceChanged(@Nullable CachedBluetoothDevice activeDevice, int bluetoothProfile) { refreshDeviceUi(); - if (mPresetController != null) { - mPresetController.setDevice(getActiveHearingDevice()); - mMainHandler.post(() -> { + mMainHandler.post(() -> { + CachedBluetoothDevice device = getActiveHearingDevice(); + if (mPresetController != null) { + mPresetController.setDevice(device); mPresetLayout.setVisibility( mPresetController.isPresetControlAvailable() ? VISIBLE : GONE); - }); - } + } + if (mAmbientController != null) { + mAmbientController.loadDevice(device); + } + }); } @Override @@ -272,13 +279,13 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, } mUiEventLogger.log(HearingDevicesUiEvent.HEARING_DEVICES_DIALOG_SHOW, mLaunchSourceId); - mDeviceList = dialog.requireViewById(R.id.device_list); - mPresetLayout = dialog.requireViewById(R.id.preset_layout); - mPresetSpinner = dialog.requireViewById(R.id.preset_spinner); setupDeviceListView(dialog); - setupPresetSpinner(dialog); setupPairNewDeviceButton(dialog); + setupPresetSpinner(dialog); + if (com.android.settingslib.flags.Flags.hearingDevicesAmbientVolumeControl()) { + setupAmbientControls(); + } if (com.android.systemui.Flags.hearingDevicesDialogRelatedTools()) { setupRelatedToolsView(dialog); } @@ -286,41 +293,50 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, @Override public void onStart(@NonNull SystemUIDialog dialog) { - if (mLocalBluetoothManager == null) { - return; - } - mLocalBluetoothManager.getEventManager().registerCallback(this); - if (mPresetController != null) { - mPresetController.registerHapCallback(); - } + ThreadUtils.postOnBackgroundThread(() -> { + if (mLocalBluetoothManager != null) { + mLocalBluetoothManager.getEventManager().registerCallback(this); + } + if (mPresetController != null) { + mPresetController.registerHapCallback(); + } + if (mAmbientController != null) { + mAmbientController.start(); + } + }); } @Override public void onStop(@NonNull SystemUIDialog dialog) { - if (mLocalBluetoothManager == null) { - return; - } - - if (mPresetController != null) { - mPresetController.unregisterHapCallback(); - } - mLocalBluetoothManager.getEventManager().unregisterCallback(this); + ThreadUtils.postOnBackgroundThread(() -> { + if (mLocalBluetoothManager != null) { + mLocalBluetoothManager.getEventManager().unregisterCallback(this); + } + if (mPresetController != null) { + mPresetController.unregisterHapCallback(); + } + if (mAmbientController != null) { + mAmbientController.stop(); + } + }); } private void setupDeviceListView(SystemUIDialog dialog) { - mDeviceList.setLayoutManager(new LinearLayoutManager(dialog.getContext())); + final RecyclerView deviceList = dialog.requireViewById(R.id.device_list); + deviceList.setLayoutManager(new LinearLayoutManager(dialog.getContext())); mHearingDeviceItemList = getHearingDeviceItemList(); mDeviceListAdapter = new HearingDevicesListAdapter(mHearingDeviceItemList, this); - mDeviceList.setAdapter(mDeviceListAdapter); + deviceList.setAdapter(mDeviceListAdapter); } private void setupPresetSpinner(SystemUIDialog dialog) { mPresetController = new HearingDevicesPresetsController(mProfileManager, mPresetCallback); mPresetController.setDevice(getActiveHearingDevice()); + mPresetSpinner = dialog.requireViewById(R.id.preset_spinner); mPresetInfoAdapter = new HearingDevicesSpinnerAdapter(dialog.getContext()); mPresetSpinner.setAdapter(mPresetInfoAdapter); - // disable redundant Touch & Hold accessibility action for Switch Access + // Disable redundant Touch & Hold accessibility action for Switch Access mPresetSpinner.setAccessibilityDelegate(new View.AccessibilityDelegate() { @Override public void onInitializeAccessibilityNodeInfo(@NonNull View host, @@ -349,12 +365,20 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, } }); + mPresetLayout = dialog.requireViewById(R.id.preset_layout); mPresetLayout.setVisibility(mPresetController.isPresetControlAvailable() ? VISIBLE : GONE); } + private void setupAmbientControls() { + final AmbientVolumeLayout ambientLayout = mDialog.requireViewById(R.id.ambient_layout); + mAmbientController = new AmbientVolumeUiController( + mDialog.getContext(), mLocalBluetoothManager, ambientLayout); + mAmbientController.setShowUiWhenLocalDataExist(false); + mAmbientController.loadDevice(getActiveHearingDevice()); + } + private void setupPairNewDeviceButton(SystemUIDialog dialog) { final Button pairButton = dialog.requireViewById(R.id.pair_new_device_button); - pairButton.setVisibility(mShowPairNewDevice ? VISIBLE : GONE); if (mShowPairNewDevice) { pairButton.setOnClickListener(v -> { diff --git a/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt index 232b62985ad0..47910f3d25bc 100644 --- a/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt @@ -21,8 +21,11 @@ import android.window.OnBackAnimationCallback import android.window.OnBackInvokedCallback import android.window.OnBackInvokedDispatcher import android.window.WindowOnBackInvokedDispatcher +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.CoreStartable +import com.android.systemui.Flags.glanceableHubBackAction import com.android.systemui.Flags.predictiveBackAnimateShade +import com.android.systemui.communal.domain.interactor.CommunalBackActionInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -35,7 +38,6 @@ import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -import com.android.app.tracing.coroutines.launchTraced as launch /** Handles requests to go back either from a button or gesture. */ @SysUISingleton @@ -50,6 +52,7 @@ constructor( private val windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor, private val shadeBackActionInteractor: ShadeBackActionInteractor, private val qsController: QuickSettingsController, + private val communalBackActionInteractor: CommunalBackActionInteractor, ) : CoreStartable { private var isCallbackRegistered = false @@ -114,6 +117,12 @@ constructor( if (shadeBackActionInteractor.closeUserSwitcherIfOpen()) { return true } + if (glanceableHubBackAction()) { + if (communalBackActionInteractor.canBeDismissed()) { + communalBackActionInteractor.onBackPressed() + return true + } + } if (shouldBackBeHandled()) { if (shadeBackActionInteractor.canBeCollapsed()) { // this is the Shade dismiss animation, so make sure QQS closes when it ends. diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt index 4c2dc41fb759..d8c628fd680b 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt @@ -155,6 +155,7 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at override fun onAnimationEnd(animation: Animator) { drawDwell = false resetDwellAlpha() + invalidate() } }) start() @@ -191,6 +192,7 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at override fun onAnimationEnd(animation: Animator) { drawDwell = false resetDwellAlpha() + invalidate() } }) start() @@ -248,6 +250,7 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at override fun onAnimationEnd(animation: Animator) { drawDwell = false + invalidate() } }) start() diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt index 9cfb5be478ed..b294dd1b0b71 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt @@ -41,6 +41,7 @@ import com.android.internal.R as InternalR import com.android.internal.logging.UiEventLogger import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.time.SystemClock import dagger.assisted.Assisted @@ -68,6 +69,7 @@ internal constructor( private val uiEventLogger: UiEventLogger, private val logger: BluetoothTileDialogLogger, private val systemuiDialogFactory: SystemUIDialog.Factory, + private val shadeDialogContextInteractor: ShadeDialogContextInteractor, ) : SystemUIDialog.Delegate { private val mutableBluetoothStateToggle: MutableStateFlow<Boolean?> = MutableStateFlow(null) @@ -105,7 +107,7 @@ internal constructor( } override fun createDialog(): SystemUIDialog { - return systemuiDialogFactory.create(this) + return systemuiDialogFactory.create(this, shadeDialogContextInteractor.context) } override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { @@ -405,10 +407,11 @@ internal constructor( } // updating icon colors - val tintColor = context.getColor( - if (item.isActive) InternalR.color.materialColorOnPrimaryContainer - else InternalR.color.materialColorOnSurface - ) + val tintColor = + context.getColor( + if (item.isActive) InternalR.color.materialColorOnPrimaryContainer + else InternalR.color.materialColorOnSurface + ) // update icons iconView.apply { diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt index aef5f1f422d1..e6f02457d320 100644 --- a/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/Icon.kt @@ -21,14 +21,17 @@ import android.graphics.drawable.Drawable /** * Models an icon, that can either be already [loaded][Icon.Loaded] or be a [reference] - * [Icon.Resource] to a resource. + * [Icon.Resource] to a resource. In case of [Loaded], the resource ID [res] is optional. */ sealed class Icon { abstract val contentDescription: ContentDescription? - data class Loaded( + data class Loaded + @JvmOverloads + constructor( val drawable: Drawable, override val contentDescription: ContentDescription?, + @DrawableRes val res: Int? = null, ) : Icon() data class Resource( @@ -37,6 +40,11 @@ sealed class Icon { ) : Icon() } -/** Creates [Icon.Loaded] for a given drawable with an optional [contentDescription]. */ -fun Drawable.asIcon(contentDescription: ContentDescription? = null): Icon.Loaded = - Icon.Loaded(this, contentDescription) +/** + * Creates [Icon.Loaded] for a given drawable with an optional [contentDescription] and an optional + * [res]. + */ +fun Drawable.asIcon( + contentDescription: ContentDescription? = null, + @DrawableRes res: Int? = null, +): Icon.Loaded = Icon.Loaded(this, contentDescription, res) diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt index 26abb48ce7db..73c0179cf8ec 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalSettingsRepository.kt @@ -55,6 +55,8 @@ interface CommunalSettingsRepository { /** A [CommunalEnabledState] for the specified user. */ fun getEnabledState(user: UserInfo): Flow<CommunalEnabledState> + fun getScreensaverEnabledState(user: UserInfo): Flow<Boolean> + /** * Returns true if any glanceable hub functionality should be enabled via configs and flags. * @@ -138,6 +140,20 @@ constructor( .flowOn(bgDispatcher) } + override fun getScreensaverEnabledState(user: UserInfo): Flow<Boolean> = + secureSettings + .observerFlow(userId = user.id, names = arrayOf(Settings.Secure.SCREENSAVER_ENABLED)) + // Force an update + .onStart { emit(Unit) } + .map { + secureSettings.getIntForUser( + Settings.Secure.SCREENSAVER_ENABLED, + SCREENSAVER_ENABLED_SETTING_DEFAULT, + user.id, + ) == 1 + } + .flowOn(bgDispatcher) + override fun getAllowedByDevicePolicy(user: UserInfo): Flow<Boolean> = broadcastDispatcher .broadcastFlow( @@ -182,6 +198,7 @@ constructor( companion object { const val GLANCEABLE_HUB_BACKGROUND_SETTING = "glanceable_hub_background" private const val ENABLED_SETTING_DEFAULT = 1 + private const val SCREENSAVER_ENABLED_SETTING_DEFAULT = 0 } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractor.kt new file mode 100644 index 000000000000..2ccf96abff79 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractor.kt @@ -0,0 +1,56 @@ +/* + * 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.communal.domain.interactor + +import com.android.systemui.communal.shared.model.CommunalScenes +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.scene.shared.model.Scenes +import javax.inject.Inject + +/** + * {@link CommunalBackActionInteractor} is responsible for handling back gestures on the glanceable + * hub. When invoked SystemUI should navigate back to the lockscreen. + */ +@SysUISingleton +class CommunalBackActionInteractor +@Inject +constructor( + private val communalInteractor: CommunalInteractor, + private val communalSceneInteractor: CommunalSceneInteractor, + private val sceneInteractor: SceneInteractor, +) { + fun canBeDismissed(): Boolean { + return communalInteractor.isCommunalShowing.value + } + + fun onBackPressed() { + if (SceneContainerFlag.isEnabled) { + // TODO(b/384610333): Properly determine whether to go to dream or lockscreen on back. + sceneInteractor.changeScene( + toScene = Scenes.Lockscreen, + loggingReason = "CommunalBackActionInteractor", + ) + } else { + communalSceneInteractor.changeScene( + newScene = CommunalScenes.Blank, + loggingReason = "CommunalBackActionInteractor", + ) + } + } +} 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 ea428698e476..947113da0e60 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 @@ -285,7 +285,7 @@ constructor( * use [isIdleOnCommunal]. */ // TODO(b/323215860): rename to something more appropriate after cleaning up usages - val isCommunalShowing: Flow<Boolean> = + val isCommunalShowing: StateFlow<Boolean> = flow { emit(SceneContainerFlag.isEnabled) } .flatMapLatest { sceneContainerEnabled -> if (sceneContainerEnabled) { @@ -304,10 +304,10 @@ constructor( columnName = "isCommunalShowing", initialValue = false, ) - .shareIn( + .stateIn( scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - replay = 1, + started = SharingStarted.Eagerly, + initialValue = false, ) /** diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt index 862b05bc9b5d..c1f21e4046a3 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSettingsInteractor.kt @@ -69,6 +69,12 @@ constructor( // Start this eagerly since the value is accessed synchronously in many places. .stateIn(scope = bgScope, started = SharingStarted.Eagerly, initialValue = false) + /** Whether or not screensaver (dreams) is enabled for the currently selected user. */ + val isScreensaverEnabled: Flow<Boolean> = + userInteractor.selectedUserInfo.flatMapLatest { user -> + repository.getScreensaverEnabledState(user) + } + /** * Returns true if any glanceable hub functionality should be enabled via configs and flags. * diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModel.kt index 7d5b196dfaa8..c6f96e198b91 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModel.kt @@ -18,10 +18,15 @@ package com.android.systemui.communal.ui.viewmodel import android.annotation.SuppressLint import android.app.DreamManager +import android.content.Intent +import android.provider.Settings +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.util.kotlin.isDevicePluggedIn +import com.android.systemui.util.kotlin.sample import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlin.coroutines.CoroutineContext @@ -31,7 +36,6 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -41,6 +45,8 @@ class CommunalToDreamButtonViewModel constructor( @Background private val backgroundContext: CoroutineContext, batteryController: BatteryController, + private val settingsInteractor: CommunalSettingsInteractor, + private val activityStarter: ActivityStarter, private val dreamManager: DreamManager, ) : ExclusiveActivatable() { @@ -49,11 +55,7 @@ constructor( /** Whether we should show a button on hub to switch to dream. */ @SuppressLint("MissingPermission") val shouldShowDreamButtonOnHub = - batteryController - .isDevicePluggedIn() - .distinctUntilChanged() - .map { isPluggedIn -> isPluggedIn && dreamManager.canStartDreaming(true) } - .flowOn(backgroundContext) + batteryController.isDevicePluggedIn().distinctUntilChanged().flowOn(backgroundContext) /** Handle a tap on the "show dream" button. */ fun onShowDreamButtonTap() { @@ -63,9 +65,21 @@ constructor( @SuppressLint("MissingPermission") override suspend fun onActivated(): Nothing = coroutineScope { launch { - _requests.receiveAsFlow().collectLatest { - withContext(backgroundContext) { dreamManager.startDream() } - } + _requests + .receiveAsFlow() + .sample(settingsInteractor.isScreensaverEnabled) + .collectLatest { enabled -> + withContext(backgroundContext) { + if (enabled) { + dreamManager.startDream() + } else { + activityStarter.postStartActivityDismissingKeyguard( + Intent(Settings.ACTION_DREAM_SETTINGS), + 0, + ) + } + } + } } awaitCancellation() diff --git a/packages/SystemUI/src/com/android/systemui/compose/ComposeModule.kt b/packages/SystemUI/src/com/android/systemui/compose/ComposeModule.kt new file mode 100644 index 000000000000..31b6f0ff90fd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/compose/ComposeModule.kt @@ -0,0 +1,31 @@ +/* + * 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.compose + +import com.android.systemui.CoreStartable +import dagger.Binds +import dagger.Module +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +interface ComposeModule { + @Binds + @IntoMap + @ClassKey(ComposeTracingStartable::class) + fun composeTracing(impl: ComposeTracingStartable): CoreStartable +} diff --git a/packages/SystemUI/src/com/android/systemui/compose/ComposeTracingStartable.kt b/packages/SystemUI/src/com/android/systemui/compose/ComposeTracingStartable.kt new file mode 100644 index 000000000000..a015900d0817 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/compose/ComposeTracingStartable.kt @@ -0,0 +1,106 @@ +/* + * 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. + */ + +@file:OptIn(InternalComposeTracingApi::class) + +package com.android.systemui.compose + +import android.os.Trace +import android.util.Log +import androidx.compose.runtime.Composer +import androidx.compose.runtime.CompositionTracer +import androidx.compose.runtime.InternalComposeTracingApi +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.commandline.Command +import com.android.systemui.statusbar.commandline.CommandRegistry +import com.android.systemui.statusbar.commandline.ParseableCommand +import java.io.PrintWriter +import javax.inject.Inject + +private const val TAG = "ComposeTracingStartable" +private const val COMMAND_NAME = "composition-tracing" +private const val SUBCOMMAND_ENABLE = "enable" +private const val SUBCOMMAND_DISABLE = "disable" + +/** + * Sets up a [Command] to enable or disable Composition tracing. + * + * Usage: + * ``` + * adb shell cmd statusbar composition-tracing [enable|disable] + * ${ANDROID_BUILD_TOP}/external/perfetto/tools/record_android_trace -c ${ANDROID_BUILD_TOP}/prebuilts/tools/linux-x86_64/perfetto/configs/trace_config_detailed.textproto + * ``` + */ +@SysUISingleton +class ComposeTracingStartable @Inject constructor(private val commandRegistry: CommandRegistry) : + CoreStartable { + @OptIn(InternalComposeTracingApi::class) + override fun start() { + Log.i(TAG, "Set up Compose tracing command") + commandRegistry.registerCommand(COMMAND_NAME) { CompositionTracingCommand() } + } +} + +private class CompositionTracingCommand : ParseableCommand(COMMAND_NAME) { + val enable by subCommand(EnableCommand()) + val disable by subCommand(DisableCommand()) + + override fun execute(pw: PrintWriter) { + if ((enable != null) xor (disable != null)) { + enable?.execute(pw) + disable?.execute(pw) + } else { + help(pw) + } + } +} + +private class EnableCommand : ParseableCommand(SUBCOMMAND_ENABLE) { + override fun execute(pw: PrintWriter) { + val msg = "Enabled Composition tracing" + Log.i(TAG, msg) + pw.println(msg) + enableCompositionTracing() + } + + private fun enableCompositionTracing() { + Composer.setTracer( + object : CompositionTracer { + override fun traceEventStart(key: Int, dirty1: Int, dirty2: Int, info: String) { + Trace.traceBegin(Trace.TRACE_TAG_APP, info) + } + + override fun traceEventEnd() = Trace.traceEnd(Trace.TRACE_TAG_APP) + + override fun isTraceInProgress(): Boolean = Trace.isEnabled() + } + ) + } +} + +private class DisableCommand : ParseableCommand(SUBCOMMAND_DISABLE) { + override fun execute(pw: PrintWriter) { + val msg = "Disabled Composition tracing" + Log.i(TAG, msg) + pw.println(msg) + disableCompositionTracing() + } + + private fun disableCompositionTracing() { + Composer.setTracer(null) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 9ae106c3ab39..014c0db618e1 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -267,6 +267,7 @@ public class FrameworkServicesModule { } @Provides + @Nullable @Singleton static VirtualDeviceManager provideVirtualDeviceManager(Context context) { return context.getSystemService(VirtualDeviceManager.class); diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index d6f8957ace33..7ebe52f3bd58 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -54,6 +54,7 @@ import com.android.systemui.common.ui.data.repository.ConfigurationRepositoryMod import com.android.systemui.common.usagestats.data.CommonUsageStatsDataLayerModule; import com.android.systemui.communal.dagger.CommunalModule; import com.android.systemui.complication.dagger.ComplicationComponent; +import com.android.systemui.compose.ComposeModule; import com.android.systemui.controls.dagger.ControlsModule; import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.dagger.qualifiers.Main; @@ -133,6 +134,7 @@ import com.android.systemui.statusbar.notification.collection.inflation.Notifica import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl; import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection; import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider; +import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider; import com.android.systemui.statusbar.notification.people.PeopleHubModule; import com.android.systemui.statusbar.notification.row.dagger.ExpandableNotificationRowComponent; @@ -141,7 +143,6 @@ import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.ConfigurationControllerModule; import com.android.systemui.statusbar.phone.LetterboxModule; import com.android.systemui.statusbar.pipeline.dagger.StatusBarPipelineModule; -import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.PolicyModule; import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController; @@ -214,6 +215,7 @@ import javax.inject.Named; ClockRegistryModule.class, CommunalModule.class, CommonDataLayerModule.class, + ComposeModule.class, ConfigurationModule.class, ConfigurationRepositoryModule.class, CommonUsageStatsDataLayerModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index 571b37f43fd4..b272d65a8a11 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -54,6 +54,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.policy.PhoneWindow; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; +import com.android.systemui.Flags; import com.android.systemui.ambient.touch.TouchHandler; import com.android.systemui.ambient.touch.TouchMonitor; import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent; @@ -210,6 +211,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mCommunalVisible = communalVisible; updateLifecycleStateLocked(); + updateGestureBlockingLocked(); }); } }; @@ -585,7 +587,8 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ private void updateGestureBlockingLocked() { final boolean shouldBlock = mStarted && !mShadeExpanded && !mBouncerShowing - && !isDreamInPreviewMode(); + && !isDreamInPreviewMode() + && !(Flags.glanceableHubBackAction() && mCommunalVisible); if (shouldBlock) { mGestureInteractor.addGestureBlockedMatcher(DREAM_TYPE_MATCHER, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt index f549e64ca853..d0065c8b06c6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt @@ -39,7 +39,6 @@ import androidx.annotation.VisibleForTesting import androidx.core.math.MathUtils import com.android.app.animation.Interpolators import com.android.internal.R -import com.android.keyguard.KeyguardClockSwitchController import com.android.keyguard.KeyguardViewController import com.android.systemui.Flags.fasterUnlockTransition import com.android.systemui.dagger.SysUISingleton @@ -206,7 +205,7 @@ constructor( fun onUnlockAnimationFinished() {} } - /** The SmartSpace view on the lockscreen, provided by [KeyguardClockSwitchController]. */ + /** The SmartSpace view on the lockscreen. */ var lockscreenSmartspace: View? = null /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt index 74ee052f12b9..57f06fbd3bb5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfig.kt @@ -21,13 +21,13 @@ import android.app.StatusBarManager import android.app.admin.DevicePolicyManager import android.content.Context import android.content.pm.PackageManager -import com.android.systemui.res.R import com.android.systemui.animation.Expandable import com.android.systemui.camera.CameraGestureHelper import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.res.R import com.android.systemui.settings.UserTracker import com.android.systemui.shade.ShadeDisplayAware import dagger.Lazy @@ -65,7 +65,7 @@ constructor( icon = Icon.Resource( R.drawable.ic_camera, - ContentDescription.Resource(R.string.accessibility_camera_button) + ContentDescription.Resource(R.string.accessibility_camera_button), ) ) } else { @@ -88,7 +88,7 @@ constructor( cameraGestureHelper .get() .launchCamera(StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE) - return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(true) } private suspend fun isLaunchable(): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt index e8d3bfac6361..1b8baf657948 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfig.kt @@ -210,16 +210,16 @@ constructor( ): KeyguardQuickAffordanceConfig.OnTriggeredResult { return if (ModesUi.isEnabled) { if (!isAvailable.value) { - KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } else { val dnd = interactor.dndMode.value if (dnd == null) { Log.wtf(TAG, "Triggered DND but it's null!?") - return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } if (dnd.isActive) { interactor.deactivateMode(dnd) - return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } else { if (interactor.shouldAskForZenDuration(dnd)) { // NOTE: The dialog handles turning on the mode itself. @@ -229,16 +229,16 @@ constructor( ) } else { interactor.activateMode(dnd) - return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } } } } else { when { - !oldIsAvailable -> KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + !oldIsAvailable -> KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) zenMode != ZEN_MODE_OFF -> { controller.setZen(ZEN_MODE_OFF, null, TAG) - KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } settingsValue == ZEN_DURATION_PROMPT -> @@ -249,12 +249,12 @@ constructor( settingsValue == ZEN_DURATION_FOREVER -> { controller.setZen(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG) - KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } else -> { controller.setZen(ZEN_MODE_IMPORTANT_INTERRUPTIONS, conditionUri, TAG) - KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt index 480ef5e19d8e..e2642a0964c1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt @@ -18,15 +18,14 @@ package com.android.systemui.keyguard.data.quickaffordance import android.content.Context -import com.android.systemui.res.R import com.android.systemui.animation.Expandable import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.shared.quickaffordance.ActivationState +import com.android.systemui.res.R import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.policy.FlashlightController import javax.inject.Inject @@ -50,9 +49,9 @@ constructor( KeyguardQuickAffordanceConfig.LockScreenState.Visible( Icon.Resource( R.drawable.qs_flashlight_icon_on, - ContentDescription.Resource(R.string.quick_settings_flashlight_label) + ContentDescription.Resource(R.string.quick_settings_flashlight_label), ), - ActivationState.Active + ActivationState.Active, ) } @@ -61,9 +60,9 @@ constructor( KeyguardQuickAffordanceConfig.LockScreenState.Visible( Icon.Resource( R.drawable.qs_flashlight_icon_off, - ContentDescription.Resource(R.string.quick_settings_flashlight_label) + ContentDescription.Resource(R.string.quick_settings_flashlight_label), ), - ActivationState.Inactive + ActivationState.Inactive, ) } @@ -92,14 +91,14 @@ constructor( } else { FlashlightState.OffAvailable.toLockScreenState() }, - TAG + TAG, ) } override fun onFlashlightError() { trySendWithFailureLogging( FlashlightState.OffAvailable.toLockScreenState(), - TAG + TAG, ) } @@ -114,7 +113,7 @@ constructor( FlashlightState.OffAvailable.toLockScreenState() } }, - TAG + TAG, ) } } @@ -130,7 +129,7 @@ constructor( flashlightController.setFlashlight( flashlightController.isAvailable && !flashlightController.isEnabled ) - return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt index d335a1806a6d..06da281648a7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/GlanceableHubQuickAffordanceConfig.kt @@ -111,7 +111,7 @@ constructor( transitionKey = CommunalTransitionKeys.SimpleFade, ) } - return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(true) } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt index 1cf6183fec6c..ade65c38ff3c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt @@ -21,10 +21,10 @@ import android.app.AlertDialog import android.content.Context import android.content.Intent import android.net.Uri -import com.android.systemui.res.R import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Icon import com.android.systemui.keyguard.shared.quickaffordance.ActivationState +import com.android.systemui.res.R import kotlinx.coroutines.flow.Flow /** Defines interface that can act as data source for a single quick affordance model. */ @@ -71,7 +71,7 @@ interface KeyguardQuickAffordanceConfig { /** The picker shows the item for selecting this affordance as it normally would. */ data class Default( /** Optional [Intent] to use to start an activity to configure this affordance. */ - val configureIntent: Intent? = null, + val configureIntent: Intent? = null ) : PickerScreenState() /** @@ -134,34 +134,39 @@ interface KeyguardQuickAffordanceConfig { ) : LockScreenState() } - sealed class OnTriggeredResult { + sealed class OnTriggeredResult() { /** * Returning this as a result from the [onTriggered] method means that the implementation * has taken care of the action, the system will do nothing. + * + * @param[actionLaunched] Whether the implementation handled the action by launching a + * dialog or an activity. */ - object Handled : OnTriggeredResult() + data class Handled(val actionLaunched: Boolean) : OnTriggeredResult() /** * Returning this as a result from the [onTriggered] method means that the implementation * has _not_ taken care of the action and the system should start an activity using the * given [Intent]. */ - data class StartActivity( - val intent: Intent, - val canShowWhileLocked: Boolean, - ) : OnTriggeredResult() + data class StartActivity(val intent: Intent, val canShowWhileLocked: Boolean) : + OnTriggeredResult() /** * Returning this as a result from the [onTriggered] method means that the implementation * has _not_ taken care of the action and the system should show a Dialog using the given * [AlertDialog] and [Expandable]. */ - data class ShowDialog( - val dialog: AlertDialog, - val expandable: Expandable?, - ) : OnTriggeredResult() + data class ShowDialog(val dialog: AlertDialog, val expandable: Expandable?) : + OnTriggeredResult() } + /** + * Models an [OnTriggeredResult] that did or did not launch a dialog or activity for a given + * config key. + */ + data class LaunchingFromTriggeredResult(val launched: Boolean, val configKey: String) + companion object { /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt index 1358634a55f8..1c9bc9f39663 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceConfig.kt @@ -21,6 +21,7 @@ import android.content.Context import android.media.AudioManager import androidx.lifecycle.LiveData import androidx.lifecycle.Observer +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.animation.Expandable import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.common.shared.model.ContentDescription @@ -45,7 +46,6 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart -import com.android.app.tracing.coroutines.launchTraced as launch import kotlinx.coroutines.withContext @SysUISingleton @@ -118,7 +118,7 @@ constructor( audioManager.ringerModeInternal = newRingerMode } } - return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } override suspend fun getPickerScreenState(): KeyguardQuickAffordanceConfig.PickerScreenState = @@ -140,11 +140,11 @@ constructor( .getSharedPreferences( MUTE_QUICK_AFFORDANCE_PREFS_FILE_NAME, Context.MODE_PRIVATE, - userTracker.userId + userTracker.userId, ) .getInt( LAST_NON_SILENT_RINGER_MODE_KEY, - ringerModeTracker.ringerModeInternal.value ?: DEFAULT_LAST_NON_SILENT_VALUE + ringerModeTracker.ringerModeInternal.value ?: DEFAULT_LAST_NON_SILENT_VALUE, ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt index eafa1cea59f3..cb7702e090d0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt @@ -30,7 +30,6 @@ import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCall import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R @@ -72,21 +71,15 @@ constructor( override fun onWalletCardsRetrieved(response: GetWalletCardsResponse) { val hasCards = getPaymentCards(response.walletCards)?.isNotEmpty() == true - trySendWithFailureLogging( - hasCards, - TAG, - ) + trySendWithFailureLogging(hasCards, TAG) } override fun onWalletCardRetrievalError(error: GetWalletCardsError) { Log.e( TAG, - "Wallet card retrieval error, message: \"${error?.message}\"" - ) - trySendWithFailureLogging( - null, - TAG, + "Wallet card retrieval error, message: \"${error?.message}\"", ) + trySendWithFailureLogging(null, TAG) } } @@ -94,7 +87,7 @@ constructor( callback, QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE, QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE, - QuickAccessWalletController.WalletChangeEvent.DEFAULT_WALLET_APP_CHANGE + QuickAccessWalletController.WalletChangeEvent.DEFAULT_WALLET_APP_CHANGE, ) withContext(backgroundDispatcher) { @@ -107,7 +100,7 @@ constructor( walletController.unregisterWalletChangeObservers( QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE, QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE, - QuickAccessWalletController.WalletChangeEvent.DEFAULT_WALLET_APP_CHANGE + QuickAccessWalletController.WalletChangeEvent.DEFAULT_WALLET_APP_CHANGE, ) } } @@ -117,11 +110,7 @@ constructor( if (hasCards == null) { KeyguardQuickAffordanceConfig.LockScreenState.Hidden } else { - state( - isWalletAvailable(), - hasCards, - walletController.walletClient.tileIcon, - ) + state(isWalletAvailable(), hasCards, walletController.walletClient.tileIcon) } flowOf(state) } @@ -135,28 +124,28 @@ constructor( explanation = context.getString( R.string.wallet_quick_affordance_unavailable_install_the_app - ), + ) ) queryCards().isEmpty() -> KeyguardQuickAffordanceConfig.PickerScreenState.Disabled( explanation = context.getString( R.string.wallet_quick_affordance_unavailable_configure_the_app - ), + ) ) else -> KeyguardQuickAffordanceConfig.PickerScreenState.Default() } } override fun onTriggered( - expandable: Expandable?, + expandable: Expandable? ): KeyguardQuickAffordanceConfig.OnTriggeredResult { walletController.startQuickAccessUiIntent( activityStarter, expandable?.activityTransitionController(), /* hasCard= */ true, ) - return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + return KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(true) } private suspend fun queryCards(): List<WalletCard> { @@ -199,10 +188,8 @@ constructor( Icon.Loaded( drawable = tileIcon, contentDescription = - ContentDescription.Resource( - res = R.string.accessibility_wallet_button, - ), - ), + ContentDescription.Resource(res = R.string.accessibility_wallet_button), + ) ) } else { KeyguardQuickAffordanceConfig.LockScreenState.Hidden diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index ae55825c9842..9c2daf52c5df 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -29,7 +29,6 @@ import com.android.keyguard.logging.KeyguardQuickAffordancesLogger import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.devicepolicy.areKeyguardShortcutsDisabled import com.android.systemui.dock.DockManager @@ -62,6 +61,7 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine @@ -101,6 +101,14 @@ constructor( val launchingAffordance: StateFlow<Boolean> = repository.get().launchingAffordance.asStateFlow() /** + * Whether a [KeyguardQuickAffordanceConfig.OnTriggeredResult] indicated that the system + * launched an activity or showed a dialog. + */ + private val _launchingFromTriggeredResult = + MutableStateFlow<KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult?>(null) + val launchingFromTriggeredResult = _launchingFromTriggeredResult.asStateFlow() + + /** * Whether the UI should use the long press gesture to activate quick affordances. * * If `false`, the UI goes back to using single taps. @@ -187,18 +195,45 @@ constructor( metricsLogger.logOnShortcutTriggered(slotId, configKey) when (val result = config.onTriggered(expandable)) { - is KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity -> + is KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity -> { + setLaunchingFromTriggeredResult( + KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult( + launched = true, + configKey, + ) + ) launchQuickAffordance( intent = result.intent, canShowWhileLocked = result.canShowWhileLocked, expandable = expandable, ) - is KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled -> Unit - is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog -> + } + is KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled -> { + setLaunchingFromTriggeredResult( + KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult( + result.actionLaunched, + configKey, + ) + ) + } + is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog -> { + setLaunchingFromTriggeredResult( + KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult( + launched = true, + configKey, + ) + ) showDialog(result.dialog, result.expandable) + } } } + fun setLaunchingFromTriggeredResult( + launchingResult: KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult? + ) { + _launchingFromTriggeredResult.value = launchingResult + } + /** * Selects an affordance with the given ID on the slot with the given ID. * diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt index aa44b6d46289..382436cf9397 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/scenetransition/LockscreenSceneTransitionInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.domain.interactor.scenetransition +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.systemui.CoreStartable @@ -38,7 +39,6 @@ import java.util.UUID import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job -import com.android.app.tracing.coroutines.launchTraced as launch /** * This class listens to scene framework scene transitions and manages keyguard transition framework @@ -111,7 +111,10 @@ constructor( if (currentTransitionId == null) return if (prevTransition !is ObservableTransitionState.Transition) return - if (idle.currentScene == prevTransition.toContent) { + if ( + idle.currentScene == prevTransition.toContent || + idle.currentOverlays.contains(prevTransition.toContent) + ) { finishCurrentTransition() } else { val targetState = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.kt index e7803c5e964c..a4a5ba691965 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.kt @@ -17,12 +17,23 @@ package com.android.systemui.keyguard.ui.binder import android.os.VibrationEffect +import com.android.systemui.Flags import kotlin.time.Duration.Companion.milliseconds object KeyguardBottomAreaVibrations { - val ShakeAnimationDuration = 300.milliseconds - const val ShakeAnimationCycles = 5f + val ShakeAnimationDuration = + if (Flags.msdlFeedback()) { + 285.milliseconds + } else { + 300.milliseconds + } + val ShakeAnimationCycles = + if (Flags.msdlFeedback()) { + 3f + } else { + 5f + } private const val SmallVibrationScale = 0.3f private const val BigVibrationScale = 0.6f @@ -32,7 +43,7 @@ object KeyguardBottomAreaVibrations { .apply { val vibrationDelayMs = (ShakeAnimationDuration.inWholeMilliseconds / (ShakeAnimationCycles * 2)) - .toInt() + .toInt() val vibrationCount = ShakeAnimationCycles.toInt() * 2 repeat(vibrationCount) { @@ -47,29 +58,13 @@ object KeyguardBottomAreaVibrations { val Activated = VibrationEffect.startComposition() - .addPrimitive( - VibrationEffect.Composition.PRIMITIVE_TICK, - BigVibrationScale, - 0, - ) - .addPrimitive( - VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, - 0.1f, - 0, - ) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, BigVibrationScale, 0) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 0.1f, 0) .compose() val Deactivated = VibrationEffect.startComposition() - .addPrimitive( - VibrationEffect.Composition.PRIMITIVE_TICK, - BigVibrationScale, - 0, - ) - .addPrimitive( - VibrationEffect.Composition.PRIMITIVE_QUICK_FALL, - 0.1f, - 0, - ) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, BigVibrationScale, 0) + .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_FALL, 0.1f, 0) .compose() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt index 8725cdd273df..8a2e3dd791c2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt @@ -20,6 +20,7 @@ package com.android.systemui.keyguard.ui.binder import android.annotation.SuppressLint import android.content.res.ColorStateList import android.graphics.drawable.Animatable2 +import android.os.VibrationEffect import android.util.Size import android.view.View import android.view.ViewGroup @@ -33,25 +34,27 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.app.tracing.coroutines.launchTraced as launch import com.android.keyguard.logging.KeyguardQuickAffordancesLogger +import com.android.systemui.Flags import com.android.systemui.animation.Expandable import com.android.systemui.animation.view.LaunchableImageView import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.binder.IconViewBinder import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceHapticViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.util.doOnEnd +import com.google.android.msdl.data.model.MSDLToken +import com.google.android.msdl.domain.MSDLPlayer import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map -import com.android.app.tracing.coroutines.launchTraced as launch /** This is only for a SINGLE Quick affordance */ @SysUISingleton @@ -60,8 +63,9 @@ class KeyguardQuickAffordanceViewBinder constructor( private val falsingManager: FalsingManager?, private val vibratorHelper: VibratorHelper?, + private val msdlPlayer: MSDLPlayer, private val logger: KeyguardQuickAffordancesLogger, - @Main private val mainImmediateDispatcher: CoroutineDispatcher, + private val hapticsViewModelFactory: KeyguardQuickAffordanceHapticViewModel.Factory, ) { private val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L @@ -88,6 +92,12 @@ constructor( ): Binding { val button = view as ImageView val configurationBasedDimensions = MutableStateFlow(loadFromResources(view)) + val hapticsViewModel = + if (Flags.msdlFeedback()) { + hapticsViewModelFactory.create(viewModel) + } else { + null + } val disposableHandle = view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { @@ -98,15 +108,12 @@ constructor( viewModel = buttonModel, messageDisplayer = messageDisplayer, ) + hapticsViewModel?.updateActivatedHistory(buttonModel.isActivated) } } launch { - updateButtonAlpha( - view = button, - viewModel = viewModel, - alphaFlow = alpha, - ) + updateButtonAlpha(view = button, viewModel = viewModel, alphaFlow = alpha) } launch { @@ -117,6 +124,32 @@ constructor( } } } + + if (Flags.msdlFeedback()) { + launch { + hapticsViewModel + ?.quickAffordanceHapticState + ?.filter { + it != + KeyguardQuickAffordanceHapticViewModel.HapticState + .NO_HAPTICS + } + ?.collect { state -> + when (state) { + KeyguardQuickAffordanceHapticViewModel.HapticState + .TOGGLE_ON -> msdlPlayer.playToken(MSDLToken.SWITCH_ON) + KeyguardQuickAffordanceHapticViewModel.HapticState + .TOGGLE_OFF -> + msdlPlayer.playToken(MSDLToken.SWITCH_OFF) + KeyguardQuickAffordanceHapticViewModel.HapticState.LAUNCH -> + msdlPlayer.playToken(MSDLToken.LONG_PRESS) + KeyguardQuickAffordanceHapticViewModel.HapticState + .NO_HAPTICS -> Unit + } + hapticsViewModel.resetLaunchingFromTriggeredResult() + } + } + } } } @@ -178,7 +211,7 @@ constructor( com.android.internal.R.color.materialColorOnPrimaryFixed } else { com.android.internal.R.color.materialColorOnSurface - }, + } ) ) @@ -221,12 +254,7 @@ constructor( .getDimensionPixelSize(R.dimen.keyguard_affordance_shake_amplitude) .toFloat() val shakeAnimator = - ObjectAnimator.ofFloat( - view, - "translationX", - -amplitude / 2, - amplitude / 2, - ) + ObjectAnimator.ofFloat(view, "translationX", -amplitude / 2, amplitude / 2) shakeAnimator.duration = KeyguardBottomAreaVibrations.ShakeAnimationDuration.inWholeMilliseconds shakeAnimator.interpolator = @@ -234,11 +262,17 @@ constructor( shakeAnimator.doOnEnd { view.translationX = 0f } shakeAnimator.start() - vibratorHelper?.vibrate(KeyguardBottomAreaVibrations.Shake) + vibratorHelper?.playFeedback(KeyguardBottomAreaVibrations.Shake, msdlPlayer) logger.logQuickAffordanceTapped(viewModel.configKey) } view.onLongClickListener = - OnLongClickListener(falsingManager, viewModel, vibratorHelper, onTouchListener) + OnLongClickListener( + falsingManager, + viewModel, + vibratorHelper, + onTouchListener, + msdlPlayer, + ) } else { view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager))) } @@ -268,7 +302,7 @@ constructor( Size( view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width), view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height), - ), + ) ) } @@ -297,7 +331,8 @@ constructor( private val falsingManager: FalsingManager?, private val viewModel: KeyguardQuickAffordanceViewModel, private val vibratorHelper: VibratorHelper?, - private val onTouchListener: KeyguardQuickAffordanceOnTouchListener + private val onTouchListener: KeyguardQuickAffordanceOnTouchListener, + private val msdlPlayer: MSDLPlayer, ) : View.OnLongClickListener { override fun onLongClick(view: View): Boolean { if (falsingManager?.isFalseLongTap(FalsingManager.MODERATE_PENALTY) == true) { @@ -312,12 +347,13 @@ constructor( slotId = viewModel.slotId, ) ) - vibratorHelper?.vibrate( + vibratorHelper?.playFeedback( if (viewModel.isActivated) { KeyguardBottomAreaVibrations.Activated } else { KeyguardBottomAreaVibrations.Deactivated - } + }, + msdlPlayer, ) } @@ -328,7 +364,15 @@ constructor( override fun onLongClickUseDefaultHapticFeedback(view: View) = false } - private data class ConfigurationBasedDimensions( - val buttonSizePx: Size, - ) + private data class ConfigurationBasedDimensions(val buttonSizePx: Size) +} + +private fun VibratorHelper.playFeedback(effect: VibrationEffect, msdlPlayer: MSDLPlayer) { + if (!Flags.msdlFeedback()) { + vibrate(effect) + } else { + if (effect == KeyguardBottomAreaVibrations.Shake) { + msdlPlayer.playToken(MSDLToken.FAILURE) + } + } } 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 a2ce4ec5ce9b..6d270b219c81 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 @@ -127,21 +127,18 @@ object KeyguardRootViewBinder { if (Flags.nonTouchscreenDevicesBypassFalsing()) { if ( event.action == MotionEvent.ACTION_DOWN && - event.buttonState == MotionEvent.BUTTON_PRIMARY && - !event.isTouchscreenSource() + event.buttonState == MotionEvent.BUTTON_PRIMARY && + !event.isTouchscreenSource() ) { consumed = true } else if ( - event.action == MotionEvent.ACTION_UP && - !event.isTouchscreenSource() + event.action == MotionEvent.ACTION_UP && !event.isTouchscreenSource() ) { statusBarKeyguardViewManager?.showBouncer(true) consumed = true } } - viewModel.setRootViewLastTapPosition( - Point(event.x.toInt(), event.y.toInt()) - ) + viewModel.setRootViewLastTapPosition(Point(event.x.toInt(), event.y.toInt())) } consumed } @@ -172,7 +169,6 @@ object KeyguardRootViewBinder { launch("$TAG#alpha") { viewModel.alpha(viewState).collect { alpha -> view.alpha = alpha - childViews[statusViewId]?.alpha = alpha childViews[burnInLayerId]?.alpha = alpha } } @@ -253,18 +249,6 @@ object KeyguardRootViewBinder { } launch { - viewModel.burnInLayerAlpha.collect { alpha -> - childViews[statusViewId]?.alpha = alpha - } - } - - launch { - viewModel.lockscreenStateAlpha(viewState).collect { alpha -> - childViews[statusViewId]?.alpha = alpha - } - } - - launch { viewModel.scale.collect { scaleViewModel -> if (scaleViewModel.scaleClockOnly) { // For clocks except weather clock, we have scale transition besides @@ -553,7 +537,6 @@ object KeyguardRootViewBinder { return device?.supportsSource(InputDevice.SOURCE_TOUCHSCREEN) == true } - private val statusViewId = R.id.keyguard_status_view private val burnInLayerId = R.id.burn_in_layer private val aodNotificationIconContainerId = R.id.aod_notification_icon_container private val largeClockId = customR.id.lockscreen_clock_view_large diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index 090b65922d2d..6fb31c0e4191 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -48,19 +48,16 @@ import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.core.view.isInvisible -import com.android.internal.policy.SystemBarUtils import com.android.keyguard.ClockEventController -import com.android.keyguard.KeyguardClockSwitch import com.android.systemui.animation.view.LaunchableImageView import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.communal.ui.binder.CommunalTutorialIndicatorViewBinder import com.android.systemui.communal.ui.viewmodel.CommunalTutorialIndicatorViewModel -import com.android.systemui.coroutines.newTracingContext +import com.android.systemui.customization.R as customR import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.ClockSizeSetting import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardPreviewSmartspaceViewBinder @@ -80,7 +77,6 @@ import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shared.clocks.ClockRegistry -import com.android.systemui.shared.clocks.DefaultClockController import com.android.systemui.shared.clocks.shared.model.ClockPreviewConstants import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.shared.quickaffordance.shared.model.KeyguardPreviewConstants @@ -91,18 +87,13 @@ import com.android.systemui.util.settings.SecureSettings import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.json.JSONException import org.json.JSONObject -import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.systemui.customization.R as customR /** Renders the preview of the lock screen. */ class KeyguardPreviewRenderer @@ -110,7 +101,6 @@ class KeyguardPreviewRenderer @AssistedInject constructor( @Application private val context: Context, - @Application applicationScope: CoroutineScope, @Main private val mainDispatcher: CoroutineDispatcher, @Main private val mainHandler: Handler, @Background private val backgroundDispatcher: CoroutineDispatcher, @@ -157,8 +147,6 @@ constructor( val surfacePackage: SurfaceControlViewHost.SurfacePackage get() = checkNotNull(host.surfacePackage) - private lateinit var largeClockHostView: FrameLayout - private lateinit var smallClockHostView: FrameLayout private var smartSpaceView: View? = null private val disposables = DisposableHandles() @@ -166,29 +154,18 @@ constructor( private val shortcutsBindings = mutableSetOf<KeyguardQuickAffordanceViewBinder.Binding>() - private val coroutineScope: CoroutineScope - @Style.Type private var themeStyle: Int? = null init { - coroutineScope = - CoroutineScope( - applicationScope.coroutineContext + - Job() + - newTracingContext("KeyguardPreviewRenderer") - ) - disposables += DisposableHandle { coroutineScope.cancel() } clockController.setFallbackWeatherData(WeatherData.getPlaceholderWeatherData()) - quickAffordancesCombinedViewModel.enablePreviewMode( initiallySelectedSlotId = - bundle.getString(KeyguardPreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID) - ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, + bundle.getString(KeyguardPreviewConstants.KEY_INITIALLY_SELECTED_SLOT_ID) + ?: KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance, ) - if (MigrateClocksToBlueprint.isEnabled) { - clockViewModel.shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance - } + + clockViewModel.shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance runBlocking(mainDispatcher) { host = SurfaceControlViewHost( @@ -348,6 +325,7 @@ constructor( smartSpaceView?.alpha = if (shouldHighlightSelectedAffordance) DIM_ALPHA else 1.0f } + @OptIn(ExperimentalCoroutinesApi::class) private fun setupKeyguardRootView(previewContext: Context, rootView: FrameLayout) { val keyguardRootView = KeyguardRootView(previewContext, null) rootView.addView( @@ -358,34 +336,23 @@ constructor( ), ) - setUpUdfps( - previewContext, - if (MigrateClocksToBlueprint.isEnabled) keyguardRootView else rootView, - ) + setUpUdfps(previewContext, keyguardRootView) setupShortcuts(keyguardRootView) if (!shouldHideClock) { setUpClock(previewContext, rootView) - if (MigrateClocksToBlueprint.isEnabled) { - KeyguardPreviewClockViewBinder.bind( - keyguardRootView, - clockViewModel, - clockRegistry, - ::updateClockAppearance, - ClockPreviewConfig( - previewContext, - getPreviewShadeLayoutWide(display!!), - SceneContainerFlag.isEnabled, - ), - ) - } else { - KeyguardPreviewClockViewBinder.bind( - largeClockHostView, - smallClockHostView, - clockViewModel, - ) - } + KeyguardPreviewClockViewBinder.bind( + keyguardRootView, + clockViewModel, + clockRegistry, + ::updateClockAppearance, + ClockPreviewConfig( + previewContext, + getPreviewShadeLayoutWide(display!!), + SceneContainerFlag.isEnabled, + ), + ) } setUpSmartspace(previewContext, rootView) @@ -451,82 +418,22 @@ constructor( .inflate(R.layout.udfps_keyguard_preview, parentView, false) as View // Place the UDFPS view in the proper sensor location - if (MigrateClocksToBlueprint.isEnabled) { - val lockId = KeyguardPreviewClockViewBinder.lockId - finger.id = lockId - parentView.addView(finger) - val cs = ConstraintSet() - cs.clone(parentView as ConstraintLayout) - cs.apply { - constrainWidth(lockId, sensorBounds.width()) - constrainHeight(lockId, sensorBounds.height()) - connect(lockId, TOP, PARENT_ID, TOP, sensorBounds.top) - connect(lockId, START, PARENT_ID, START, sensorBounds.left) - } - cs.applyTo(parentView) - } else { - val fingerprintLayoutParams = - FrameLayout.LayoutParams(sensorBounds.width(), sensorBounds.height()) - fingerprintLayoutParams.setMarginsRelative( - sensorBounds.left, - sensorBounds.top, - sensorBounds.right, - sensorBounds.bottom, - ) - parentView.addView(finger, fingerprintLayoutParams) + val lockId = KeyguardPreviewClockViewBinder.lockId + finger.id = lockId + parentView.addView(finger) + val cs = ConstraintSet() + cs.clone(parentView as ConstraintLayout) + cs.apply { + constrainWidth(lockId, sensorBounds.width()) + constrainHeight(lockId, sensorBounds.height()) + connect(lockId, TOP, PARENT_ID, TOP, sensorBounds.top) + connect(lockId, START, PARENT_ID, START, sensorBounds.left) } + cs.applyTo(parentView) } private fun setUpClock(previewContext: Context, parentView: ViewGroup) { val resources = parentView.resources - if (!MigrateClocksToBlueprint.isEnabled) { - largeClockHostView = FrameLayout(previewContext) - largeClockHostView.layoutParams = - FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.MATCH_PARENT, - ) - largeClockHostView.isInvisible = true - parentView.addView(largeClockHostView) - - smallClockHostView = FrameLayout(previewContext) - val layoutParams = - FrameLayout.LayoutParams( - FrameLayout.LayoutParams.WRAP_CONTENT, - resources.getDimensionPixelSize(customR.dimen.small_clock_height), - ) - layoutParams.topMargin = - SystemBarUtils.getStatusBarHeight(previewContext) + - resources.getDimensionPixelSize(customR.dimen.small_clock_padding_top) - smallClockHostView.layoutParams = layoutParams - smallClockHostView.setPaddingRelative( - /* start = */ resources.getDimensionPixelSize(customR.dimen.clock_padding_start), - /* top = */ 0, - /* end = */ 0, - /* bottom = */ 0, - ) - smallClockHostView.clipChildren = false - parentView.addView(smallClockHostView) - smallClockHostView.isInvisible = true - } - - // TODO (b/283465254): Move the listeners to KeyguardClockRepository - if (!MigrateClocksToBlueprint.isEnabled) { - val clockChangeListener = - object : ClockRegistry.ClockChangeListener { - override fun onCurrentClockChanged() { - onClockChanged() - } - } - clockRegistry.registerClockChangeListener(clockChangeListener) - disposables += DisposableHandle { - clockRegistry.unregisterClockChangeListener(clockChangeListener) - } - - clockController.registerListeners(parentView) - disposables += DisposableHandle { clockController.unregisterListeners() } - } - val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { @@ -544,38 +451,9 @@ constructor( }, ) disposables += DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) } - - if (!MigrateClocksToBlueprint.isEnabled) { - val layoutChangeListener = - View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> - if (clockController.clock !is DefaultClockController) { - clockController.clock - ?.largeClock - ?.events - ?.onTargetRegionChanged( - KeyguardClockSwitch.getLargeClockRegion(parentView) - ) - clockController.clock - ?.smallClock - ?.events - ?.onTargetRegionChanged( - KeyguardClockSwitch.getSmallClockRegion(parentView) - ) - } - } - parentView.addOnLayoutChangeListener(layoutChangeListener) - disposables += DisposableHandle { - parentView.removeOnLayoutChangeListener(layoutChangeListener) - } - } - - onClockChanged() } private suspend fun updateClockAppearance(clock: ClockController, resources: Resources) { - if (!MigrateClocksToBlueprint.isEnabled) { - clockController.clock = clock - } val colors = wallpaperColors if (clockRegistry.seedColor == null && colors != null) { // Seed color null means users do not override any color on the clock. The default @@ -601,9 +479,7 @@ constructor( // In clock preview, we should have a seed color for clock // before setting clock to clockEventController to avoid updateColor with seedColor == null // So in update colors, it should already have the correct theme in clockFaceController - if (MigrateClocksToBlueprint.isEnabled) { - clockController.clock = clock - } + clockController.clock = clock // When set clock to clockController,it will reset fontsize based on context.resources // We need to override it with overlaid resources clock.largeClock.events.onFontSettingChanged( @@ -611,19 +487,6 @@ constructor( ) } - private fun onClockChanged() { - if (MigrateClocksToBlueprint.isEnabled) { - return - } - coroutineScope.launch { - val clock = clockRegistry.createCurrentClock() - clockController.clock = clock - updateClockAppearance(clock, context.resources) - updateLargeClock(clock) - updateSmallClock(clock) - } - } - private fun setupCommunalTutorialIndicator(keyguardRootView: ConstraintLayout) { keyguardRootView.findViewById<TextView>(R.id.communal_tutorial_indicator)?.let { indicatorView -> @@ -657,34 +520,6 @@ constructor( } } - private fun updateLargeClock(clock: ClockController) { - if (MigrateClocksToBlueprint.isEnabled) { - return - } - clock.largeClock.events.onTargetRegionChanged( - KeyguardClockSwitch.getLargeClockRegion(largeClockHostView) - ) - if (shouldHighlightSelectedAffordance) { - clock.largeClock.view.alpha = DIM_ALPHA - } - largeClockHostView.removeAllViews() - largeClockHostView.addView(clock.largeClock.view) - } - - private fun updateSmallClock(clock: ClockController) { - if (MigrateClocksToBlueprint.isEnabled) { - return - } - clock.smallClock.events.onTargetRegionChanged( - KeyguardClockSwitch.getSmallClockRegion(smallClockHostView) - ) - if (shouldHighlightSelectedAffordance) { - clock.smallClock.view.alpha = DIM_ALPHA - } - smallClockHostView.removeAllViews() - smallClockHostView.addView(clock.smallClock.view) - } - private fun getPreviewShadeLayoutWide(display: Display): Boolean { return if (display.displayId == 0) { shadeInteractor.isShadeLayoutWide.value diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt index 160380bb09bc..57fe15d4f52c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt @@ -24,7 +24,6 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.view.KeyguardRootView import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel @@ -50,9 +49,6 @@ constructor( } override fun addViews(constraintLayout: ConstraintLayout) { - if (!MigrateClocksToBlueprint.isEnabled) { - return - } if (emptyView.parent != null) { // As emptyView is lazy, it might be already attached. (emptyView.parent as? ViewGroup)?.removeView(emptyView) @@ -68,17 +64,10 @@ constructor( } override fun bindData(constraintLayout: ConstraintLayout) { - if (!MigrateClocksToBlueprint.isEnabled) { - return - } clockViewModel.burnInLayer = burnInLayer } override fun applyConstraints(constraintSet: ConstraintSet) { - if (!MigrateClocksToBlueprint.isEnabled) { - return - } - constraintSet.apply { // The empty view should not occupy any space constrainHeight(R.id.burn_in_layer_empty_view, 1) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index 70bf8bca55b9..738fb73a4918 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -32,7 +32,6 @@ import androidx.constraintlayout.widget.ConstraintSet.VISIBLE import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT import com.android.systemui.customization.R as customR import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.KeyguardSection @@ -82,9 +81,6 @@ constructor( override fun addViews(constraintLayout: ConstraintLayout) {} override fun bindData(constraintLayout: ConstraintLayout) { - if (!MigrateClocksToBlueprint.isEnabled) { - return - } disposableHandle?.dispose() disposableHandle = KeyguardClockViewBinder.bind( @@ -99,20 +95,12 @@ constructor( } override fun applyConstraints(constraintSet: ConstraintSet) { - if (!MigrateClocksToBlueprint.isEnabled) { - return - } - keyguardClockViewModel.currentClock.value?.let { clock -> constraintSet.applyDeltaFrom(buildConstraints(clock, constraintSet)) } } override fun removeViews(constraintLayout: ConstraintLayout) { - if (!MigrateClocksToBlueprint.isEnabled) { - return - } - disposableHandle?.dispose() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt index 3a791fd45528..4bfe5f0458c5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt @@ -24,7 +24,6 @@ import androidx.constraintlayout.widget.ConstraintSet.END import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.res.R import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.shade.NotificationPanelView @@ -54,32 +53,25 @@ constructor( sharedNotificationContainerBinder, ) { override fun applyConstraints(constraintSet: ConstraintSet) { - if (!MigrateClocksToBlueprint.isEnabled) { - return - } constraintSet.apply { val bottomMargin = context.resources.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin) - if (MigrateClocksToBlueprint.isEnabled) { - val useLargeScreenHeader = - context.resources.getBoolean(R.bool.config_use_large_screen_shade_header) - val marginTopLargeScreen = - largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight() - connect( - R.id.nssl_placeholder, - TOP, - R.id.smart_space_barrier_bottom, - BOTTOM, - bottomMargin + - if (useLargeScreenHeader) { - marginTopLargeScreen - } else { - 0 - } - ) - } else { - connect(R.id.nssl_placeholder, TOP, R.id.keyguard_status_view, BOTTOM, bottomMargin) - } + val useLargeScreenHeader = + context.resources.getBoolean(R.bool.config_use_large_screen_shade_header) + val marginTopLargeScreen = + largeScreenHeaderHelperLazy.get().getLargeScreenHeaderHeight() + connect( + R.id.nssl_placeholder, + TOP, + R.id.smart_space_barrier_bottom, + BOTTOM, + bottomMargin + + if (useLargeScreenHeader) { + marginTopLargeScreen + } else { + 0 + }, + ) connect(R.id.nssl_placeholder, START, PARENT_ID, START) connect(R.id.nssl_placeholder, END, PARENT_ID, END) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt index 620cc13a0c3a..fc26d18fde6b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt @@ -25,7 +25,6 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM import androidx.constraintlayout.widget.ConstraintSet.TOP -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.res.R import com.android.systemui.shade.NotificationPanelView @@ -62,9 +61,6 @@ constructor( } override fun addViews(constraintLayout: ConstraintLayout) { - if (!MigrateClocksToBlueprint.isEnabled) { - return - } // This moves the existing NSSL view to a different parent, as the controller is a // singleton and recreating it has other bad side effects. // In the SceneContainer, this is done by the NotificationSection composable. @@ -78,10 +74,6 @@ constructor( } override fun bindData(constraintLayout: ConstraintLayout) { - if (!MigrateClocksToBlueprint.isEnabled) { - return - } - disposableHandle?.dispose() disposableHandle = sharedNotificationContainerBinder.bind( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt index 73e14b1524f3..cd038d799f42 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt @@ -26,7 +26,6 @@ import androidx.constraintlayout.widget.ConstraintSet import com.android.systemui.customization.R as customR import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.KeyguardUnlockAnimationController -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor import com.android.systemui.keyguard.shared.model.KeyguardSection @@ -70,7 +69,6 @@ constructor( } override fun addViews(constraintLayout: ConstraintLayout) { - if (!MigrateClocksToBlueprint.isEnabled) return if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return smartspaceView = smartspaceController.buildAndConnectView(constraintLayout) weatherView = smartspaceController.buildAndConnectWeatherView(constraintLayout) @@ -98,7 +96,6 @@ constructor( } override fun bindData(constraintLayout: ConstraintLayout) { - if (!MigrateClocksToBlueprint.isEnabled) return if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return disposableHandle?.dispose() disposableHandle = @@ -111,13 +108,11 @@ constructor( } override fun applyConstraints(constraintSet: ConstraintSet) { - if (!MigrateClocksToBlueprint.isEnabled) return if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return val dateWeatherPaddingStart = KeyguardSmartspaceViewModel.getDateWeatherStartMargin(context) val smartspaceHorizontalPadding = KeyguardSmartspaceViewModel.getSmartspaceHorizontalMargin(context) constraintSet.apply { - // migrate addDateWeatherView, addWeatherView from KeyguardClockSwitchController constrainHeight(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) constrainWidth(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) connect( @@ -128,7 +123,6 @@ constructor( dateWeatherPaddingStart, ) - // migrate addSmartspaceView from KeyguardClockSwitchController constrainHeight(sharedR.id.bc_smartspace_view, ConstraintSet.WRAP_CONTENT) constrainWidth(sharedR.id.bc_smartspace_view, ConstraintSet.MATCH_CONSTRAINT) connect( @@ -182,7 +176,6 @@ constructor( } override fun removeViews(constraintLayout: ConstraintLayout) { - if (!MigrateClocksToBlueprint.isEnabled) return if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return listOf(smartspaceView, dateWeatherView).forEach { it?.let { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt index 729759a9ad00..5d463f72d8b2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt @@ -23,7 +23,6 @@ import androidx.constraintlayout.widget.ConstraintSet.END import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.res.R import com.android.systemui.shade.NotificationPanelView import com.android.systemui.shade.ShadeDisplayAware @@ -50,16 +49,13 @@ constructor( sharedNotificationContainerBinder, ) { override fun applyConstraints(constraintSet: ConstraintSet) { - if (!MigrateClocksToBlueprint.isEnabled) { - return - } constraintSet.apply { connect( R.id.nssl_placeholder, TOP, PARENT_ID, TOP, - context.resources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin) + context.resources.getDimensionPixelSize(R.dimen.keyguard_split_shade_top_margin), ) connect(R.id.nssl_placeholder, START, PARENT_ID, START) connect(R.id.nssl_placeholder, END, PARENT_ID, END) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt index 1c897237fe89..fb311a533aa2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt @@ -24,7 +24,6 @@ import com.android.app.animation.Interpolators import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.BurnInInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor @@ -194,12 +193,7 @@ constructor( (!useAltAod) && keyguardClockViewModel.clockSize.value == ClockSize.LARGE val burnInY = MathUtils.lerp(0, burnIn.translationY, interpolated).toInt() - val translationY = - if (MigrateClocksToBlueprint.isEnabled) { - max(params.topInset - params.minViewY, burnInY) - } else { - max(params.topInset, params.minViewY + burnInY) - params.minViewY - } + val translationY = max(params.topInset - params.minViewY, burnInY) BurnInModel( translationX = MathUtils.lerp(0, burnIn.translationX, interpolated).toInt(), translationY = translationY, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceHapticViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceHapticViewModel.kt new file mode 100644 index 000000000000..890628c31c55 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceHapticViewModel.kt @@ -0,0 +1,96 @@ +/* + * 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.keyguard.ui.viewmodel + +import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge + +class KeyguardQuickAffordanceHapticViewModel +@AssistedInject +constructor( + @Assisted quickAffordanceViewModel: Flow<KeyguardQuickAffordanceViewModel>, + private val quickAffordanceInteractor: KeyguardQuickAffordanceInteractor, +) { + + private val activatedHistory = MutableStateFlow(ActivatedHistory(false)) + + private val launchingHapticState: Flow<HapticState> = + combine( + quickAffordanceViewModel.map { it.configKey }, + quickAffordanceInteractor.launchingFromTriggeredResult, + ) { key, launchingResult -> + val validKey = key != null && key == launchingResult?.configKey + if (validKey && launchingResult?.launched == true) { + HapticState.LAUNCH + } else { + HapticState.NO_HAPTICS + } + } + .distinctUntilChanged() + + private val toggleHapticState: Flow<HapticState> = + activatedHistory + .map { history -> + when { + history.previousValue == false && history.currentValue -> HapticState.TOGGLE_ON + history.previousValue == true && !history.currentValue -> HapticState.TOGGLE_OFF + else -> HapticState.NO_HAPTICS + } + } + .distinctUntilChanged() + + val quickAffordanceHapticState = + merge(launchingHapticState, toggleHapticState).distinctUntilChanged() + + fun resetLaunchingFromTriggeredResult() = + quickAffordanceInteractor.setLaunchingFromTriggeredResult(null) + + fun updateActivatedHistory(isActivated: Boolean) { + activatedHistory.value = + ActivatedHistory( + currentValue = isActivated, + previousValue = activatedHistory.value.currentValue, + ) + } + + enum class HapticState { + TOGGLE_ON, + TOGGLE_OFF, + LAUNCH, + NO_HAPTICS, + } + + private data class ActivatedHistory( + val currentValue: Boolean, + val previousValue: Boolean? = null, + ) + + @AssistedFactory + interface Factory { + fun create( + quickAffordanceViewModel: Flow<KeyguardQuickAffordanceViewModel> + ): KeyguardQuickAffordanceHapticViewModel + } +} 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 9066d466ceca..eaba5d5a149c 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 @@ -29,7 +29,6 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.PulseExpansionInteractor import com.android.systemui.keyguard.shared.model.Edge -import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING import com.android.systemui.keyguard.shared.model.KeyguardState.GONE @@ -130,7 +129,6 @@ constructor( PrimaryBouncerToLockscreenTransitionViewModel, private val screenOffAnimationController: ScreenOffAnimationController, private val aodBurnInViewModel: AodBurnInViewModel, - private val aodAlphaViewModel: AodAlphaViewModel, private val shadeInteractor: ShadeInteractor, ) { val burnInLayerVisibility: Flow<Int> = @@ -284,15 +282,6 @@ constructor( .distinctUntilChanged() } - /** Specific alpha value for elements visible during [KeyguardState.LOCKSCREEN] */ - @Deprecated("only used for legacy status view") - fun lockscreenStateAlpha(viewState: ViewStateAccessor): Flow<Float> { - return aodToLockscreenTransitionViewModel.lockscreenAlpha(viewState) - } - - /** For elements that appear and move during the animation -> AOD */ - val burnInLayerAlpha: Flow<Float> = aodAlphaViewModel.alpha - val translationY: Flow<Float> = aodBurnInViewModel.movement.map { it.translationY.toFloat() } val translationX: Flow<StateToValue> = diff --git a/packages/SystemUI/src/com/android/systemui/lottie/LottieTaskExt.kt b/packages/SystemUI/src/com/android/systemui/lottie/LottieTaskExt.kt new file mode 100644 index 000000000000..dd2525f5ca45 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lottie/LottieTaskExt.kt @@ -0,0 +1,49 @@ +/* + * 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.lottie + +import com.airbnb.lottie.LottieComposition +import com.airbnb.lottie.LottieListener +import com.airbnb.lottie.LottieTask +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlinx.coroutines.suspendCancellableCoroutine + +/** + * Suspends until [LottieTask] is finished with a result or a failure. + * + * @return result of the [LottieTask] when it's successful + */ +suspend fun LottieTask<LottieComposition>.await() = + suspendCancellableCoroutine<LottieComposition> { continuation -> + val resultListener = + LottieListener<LottieComposition> { result -> + with(continuation) { if (!isCancelled && !isCompleted) resume(result) } + } + val failureListener = + LottieListener<Throwable> { throwable -> + with(continuation) { + if (!isCancelled && !isCompleted) resumeWithException(throwable) + } + } + addListener(resultListener) + addFailureListener(failureListener) + continuation.invokeOnCancellation { + removeListener(resultListener) + removeFailureListener(failureListener) + } + } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt index c32bd403d2e8..b4dabbe036e9 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManager.kt @@ -34,13 +34,14 @@ import android.view.ViewGroup import android.view.ViewGroupOverlay import androidx.annotation.VisibleForTesting import com.android.app.animation.Interpolators +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.app.tracing.traceSection import com.android.keyguard.KeyguardViewController import com.android.systemui.Flags.mediaControlsLockscreenShadeBugFix import com.android.systemui.communal.ui.viewmodel.CommunalTransitionViewModel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dreams.DreamOverlayStateController import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor @@ -68,7 +69,6 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.mapLatest -import com.android.app.tracing.coroutines.launchTraced as launch private val TAG: String = MediaHierarchyManager::class.java.simpleName @@ -115,7 +115,7 @@ constructor( wakefulnessLifecycle: WakefulnessLifecycle, shadeInteractor: ShadeInteractor, private val secureSettings: SecureSettings, - @Main private val handler: Handler, + @Background private val handler: Handler, @Application private val coroutineScope: CoroutineScope, private val splitShadeStateController: SplitShadeStateController, private val logger: MediaViewLogger, @@ -631,7 +631,7 @@ constructor( } } } - secureSettings.registerContentObserverForUserSync( + secureSettings.registerContentObserverForUserAsync( Settings.Secure.MEDIA_CONTROLS_LOCK_SCREEN, settingsObserver, UserHandle.USER_ALL, diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java index 574ccee28faa..ab998d10287f 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseAdapter.java @@ -366,7 +366,6 @@ public abstract class MediaOutputBaseAdapter extends / (double) seekBar.getMax()); mVolumeValueText.setText(mContext.getResources().getString( R.string.media_output_dialog_volume_percentage, percentage)); - mVolumeValueText.setVisibility(View.VISIBLE); if (mStartFromMute) { updateUnmutedVolumeIcon(device); mStartFromMute = false; diff --git a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt index 311cbfb7e632..b2696aeaabfc 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfig.kt @@ -132,7 +132,7 @@ constructor( val isDefaultNotesAppSet = noteTaskInfoResolver.resolveInfo( QUICK_AFFORDANCE, - user = controller.getUserForHandlingNotesTaking(QUICK_AFFORDANCE) + user = controller.getUserForHandlingNotesTaking(QUICK_AFFORDANCE), ) != null return when { isEnabled && isDefaultNotesAppSet -> PickerScreenState.Default() @@ -158,7 +158,7 @@ constructor( override fun onTriggered(expandable: Expandable?): OnTriggeredResult { controller.showNoteTask(entryPoint = QUICK_AFFORDANCE) - return OnTriggeredResult.Handled + return OnTriggeredResult.Handled(true) } } @@ -194,7 +194,7 @@ private fun RoleManager.createNotesRoleFlow( fun isDefaultNotesAppSetForUser() = noteTaskInfoResolver.resolveInfo( QUICK_AFFORDANCE, - user = noteTaskController.getUserForHandlingNotesTaking(QUICK_AFFORDANCE) + user = noteTaskController.getUserForHandlingNotesTaking(QUICK_AFFORDANCE), ) != null trySendBlocking(isDefaultNotesAppSetForUser()) diff --git a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt index 91a3120ec770..1e608af14568 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FgsManagerController.kt @@ -67,6 +67,7 @@ import com.android.systemui.dump.DumpManager import com.android.systemui.res.R import com.android.systemui.settings.UserTracker import com.android.systemui.shade.ShadeDisplayAware +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor import com.android.systemui.shared.system.SysUiStatsLog import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.util.DeviceConfigProxy @@ -141,7 +142,6 @@ interface FgsManagerController { class FgsManagerControllerImpl @Inject constructor( - @ShadeDisplayAware private val context: Context, @ShadeDisplayAware private val resources: Resources, @Main private val mainExecutor: Executor, @Background private val backgroundExecutor: Executor, @@ -155,6 +155,7 @@ constructor( private val broadcastDispatcher: BroadcastDispatcher, private val dumpManager: DumpManager, private val systemUIDialogFactory: SystemUIDialog.Factory, + private val shadeDialogContextRepository: ShadeDialogContextInteractor, ) : Dumpable, FgsManagerController { companion object { @@ -388,7 +389,7 @@ constructor( override fun showDialog(expandable: Expandable?) { synchronized(lock) { if (dialog == null) { - val dialog = systemUIDialogFactory.create(context) + val dialog = systemUIDialogFactory.create(shadeDialogContextRepository.context) dialog.setTitle(R.string.fgs_manager_dialog_title) dialog.setMessage(R.string.fgs_manager_dialog_message) diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt b/packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt index ef7e7eb59898..84b995e1cd28 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileIcon.kt @@ -22,16 +22,21 @@ import com.android.systemui.qs.tileimpl.QSTileImpl /** * Creates a [QSTile.Icon] from an [Icon]. - * * [Icon.Loaded] -> [QSTileImpl.DrawableIcon] + * * [Icon.Loaded] with null [res] -> [QSTileImpl.DrawableIcon] + * * [Icon.Loaded] & with non null [res] -> [QSTileImpl.DrawableIconWithRes] * * [Icon.Resource] -> [QSTileImpl.ResourceIcon] */ fun Icon.asQSTileIcon(): QSTile.Icon { return when (this) { is Icon.Loaded -> { - QSTileImpl.DrawableIcon(this.drawable) + if (res == null) { + QSTileImpl.DrawableIcon(drawable) + } else { + QSTileImpl.DrawableIconWithRes(drawable, res) + } } is Icon.Resource -> { - QSTileImpl.ResourceIcon.get(this.res) + QSTileImpl.ResourceIcon.get(res) } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt index 790793eab258..3049a40f18c4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt @@ -17,16 +17,16 @@ package com.android.systemui.qs.composefragment.ui import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.ClipOp import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy import androidx.compose.ui.graphics.drawscope.clipRect -import androidx.compose.ui.graphics.layer.CompositingStrategy -import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.graphics.graphicsLayer /** * Clipping modifier for clipping out the notification scrim as it slides over QS. It will clip out @@ -34,16 +34,16 @@ import androidx.compose.ui.graphics.layer.drawLayer * from the QS container. */ fun Modifier.notificationScrimClip(clipParams: () -> NotificationScrimClipParams): Modifier { - return this.drawWithCache { + return this.graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } + .drawWithContent { + drawContent() val params = clipParams() val left = -params.leftInset.toFloat() val right = size.width + params.rightInset.toFloat() val top = params.top.toFloat() val bottom = params.bottom.toFloat() - val graphicsLayer = obtainGraphicsLayer() - graphicsLayer.compositingStrategy = CompositingStrategy.Offscreen - graphicsLayer.record { - drawContent() + val clipSize = Size(right - left, bottom - top) + if (!clipSize.isEmpty()) { clipRect { drawRoundRect( color = Color.Black, @@ -54,9 +54,6 @@ fun Modifier.notificationScrimClip(clipParams: () -> NotificationScrimClipParams ) } } - onDrawWithContent { - drawLayer(graphicsLayer) - } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java index 873059ee08db..e7fa27159e9e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileAdapter.java @@ -675,17 +675,11 @@ public class TileAdapter extends RecyclerView.Adapter<Holder> implements TileSta } private void add() { - if (addFromPosition(getLayoutPosition())) { - itemView.announceForAccessibility( - itemView.getContext().getText(R.string.accessibility_qs_edit_tile_added)); - } + addFromPosition(getLayoutPosition()); } private void remove() { - if (removeFromPosition(getLayoutPosition())) { - itemView.announceForAccessibility( - itemView.getContext().getText(R.string.accessibility_qs_edit_tile_removed)); - } + removeFromPosition(getLayoutPosition()); } boolean isCurrentTile() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/EditModeButton.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/EditModeButton.kt index 85db95203b45..f3c06a481fc2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/EditModeButton.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/EditModeButton.kt @@ -54,7 +54,7 @@ fun EditModeButton( ) { Icon( imageVector = Icons.Default.Edit, - contentDescription = stringResource(id = R.string.qs_edit), + contentDescription = stringResource(id = R.string.accessibility_quick_settings_edit), ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java index 42a0cb1004f4..b7ff63cdc1fb 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/DataSaverTile.java @@ -41,6 +41,7 @@ import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.res.R; +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.DataSaverController; @@ -56,6 +57,7 @@ public class DataSaverTile extends QSTileImpl<BooleanState> implements private final DataSaverController mDataSaverController; private final DialogTransitionAnimator mDialogTransitionAnimator; private final SystemUIDialog.Factory mSystemUIDialogFactory; + private final ShadeDialogContextInteractor mShadeDialogContextInteractor; @Inject public DataSaverTile( @@ -70,13 +72,15 @@ public class DataSaverTile extends QSTileImpl<BooleanState> implements QSLogger qsLogger, DataSaverController dataSaverController, DialogTransitionAnimator dialogTransitionAnimator, - SystemUIDialog.Factory systemUIDialogFactory + SystemUIDialog.Factory systemUIDialogFactory, + ShadeDialogContextInteractor shadeDialogContextInteractor ) { super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, statusBarStateController, activityStarter, qsLogger); mDataSaverController = dataSaverController; mDialogTransitionAnimator = dialogTransitionAnimator; mSystemUIDialogFactory = systemUIDialogFactory; + mShadeDialogContextInteractor = shadeDialogContextInteractor; mDataSaverController.observe(getLifecycle(), this); } @@ -102,7 +106,8 @@ public class DataSaverTile extends QSTileImpl<BooleanState> implements // Show a dialog to confirm first. Dialogs shown by the DialogTransitionAnimator must be // created and shown on the main thread, so we post it to the UI handler. mUiHandler.post(() -> { - SystemUIDialog dialog = mSystemUIDialogFactory.create(mContext); + SystemUIDialog dialog = mSystemUIDialogFactory.create( + mShadeDialogContextInteractor.getContext()); dialog.setTitle(com.android.internal.R.string.data_saver_enable_title); dialog.setMessage(com.android.internal.R.string.data_saver_description); dialog.setPositiveButton(com.android.internal.R.string.data_saver_enable_button, diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/NotesTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/NotesTile.kt index 989fc0fd6f44..5ba1527dbf69 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/NotesTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/NotesTile.kt @@ -22,6 +22,7 @@ import android.os.Looper import android.service.quicksettings.Tile import com.android.internal.logging.MetricsLogger import com.android.systemui.animation.Expandable +import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter @@ -92,7 +93,8 @@ constructor( state?.apply { this.state = tileState.activationState.legacyState - icon = maybeLoadResourceIcon(tileState.iconRes ?: R.drawable.ic_qs_notes) + icon = + maybeLoadResourceIcon((tileState.icon as Icon.Loaded).res ?: R.drawable.ic_qs_notes) label = tileState.label contentDescription = tileState.contentDescription expandedAccessibilityClassName = tileState.expandedAccessibilityClassName diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetAdapter.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetAdapter.java index 19b45d50c594..7516ca030d4b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetAdapter.java @@ -193,7 +193,7 @@ public class InternetAdapter extends RecyclerView.Adapter<InternetAdapter.Intern if (mJob == null) { mJob = WifiUtils.checkWepAllowed(mContext, mCoroutineScope, wifiEntry.getSsid(), WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG, intent -> { - mInternetDialogController.startActivity(intent, view); + mInternetDialogController.startActivityForDialog(intent); return null; }, () -> { wifiConnect(wifiEntry, view); @@ -211,7 +211,7 @@ public class InternetAdapter extends RecyclerView.Adapter<InternetAdapter.Intern true /* connectForCaller */); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - mContext.startActivity(intent); + mInternetDialogController.startActivityForDialog(intent); return; } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java index dbe1ae90b3f6..7036ef914a1c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java @@ -781,6 +781,10 @@ public class InternetDialogController implements AccessPointController.AccessPoi mActivityStarter.postStartActivityDismissingKeyguard(intent, 0, controller); } + void startActivityForDialog(Intent intent) { + mActivityStarter.startActivity(intent, false /* dismissShade */); + } + void launchNetworkSetting(View view) { startActivity(getSettingsIntent(), view); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java index 70c2a2a0d55a..5e9deec58c58 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java @@ -72,6 +72,7 @@ import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.res.R; import com.android.systemui.shade.ShadeDisplayAware; +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.wifitrackerlib.WifiEntry; @@ -104,9 +105,9 @@ public class InternetDialogDelegate implements private final Handler mHandler; private final Executor mBackgroundExecutor; private final DialogTransitionAnimator mDialogTransitionAnimator; - private final Context mContext; private final boolean mAboveStatusBar; private final SystemUIDialog.Factory mSystemUIDialogFactory; + private final ShadeDialogContextInteractor mShadeDialogContextInteractor; @VisibleForTesting protected InternetAdapter mAdapter; @@ -204,10 +205,11 @@ public class InternetDialogDelegate implements @Main Handler handler, @Background Executor executor, KeyguardStateController keyguardStateController, - SystemUIDialog.Factory systemUIDialogFactory) { - mContext = context; + SystemUIDialog.Factory systemUIDialogFactory, + ShadeDialogContextInteractor shadeDialogContextInteractor) { mAboveStatusBar = aboveStatusBar; mSystemUIDialogFactory = systemUIDialogFactory; + mShadeDialogContextInteractor = shadeDialogContextInteractor; if (DEBUG) { Log.d(TAG, "Init InternetDialog"); } @@ -230,7 +232,8 @@ public class InternetDialogDelegate implements @Override public SystemUIDialog createDialog() { - SystemUIDialog dialog = mSystemUIDialogFactory.create(this, mContext); + SystemUIDialog dialog = mSystemUIDialogFactory.create(this, + mShadeDialogContextInteractor.getContext()); if (!mAboveStatusBar) { dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY); } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/AirplaneModeMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/AirplaneModeMapper.kt index 34c2ec90f1e8..80d429ce2716 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/AirplaneModeMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/AirplaneModeMapper.kt @@ -35,13 +35,13 @@ constructor(@ShadeDisplayAware private val resources: Resources, val theme: Them override fun map(config: QSTileConfig, data: AirplaneModeTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { - iconRes = + val iconRes = if (data.isEnabled) { R.drawable.qs_airplane_icon_on } else { R.drawable.qs_airplane_icon_off } - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) if (data.isEnabled) { activationState = QSTileState.ActivationState.ACTIVE secondaryLabel = resources.getStringArray(R.array.tile_states_airplane)[2] diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt index a72992db4496..d56d9944dbb8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt @@ -84,8 +84,8 @@ constructor( secondaryLabel = resources.getString(R.string.qs_alarm_tile_no_alarm) } } - iconRes = R.drawable.ic_alarm - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + val iconRes = R.drawable.ic_alarm + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) sideViewIcon = QSTileState.SideViewIcon.Chevron contentDescription = label supportedActions = setOf(QSTileState.UserAction.CLICK) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapper.kt index e116d8cef2ee..72759c5bb066 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/battery/ui/BatterySaverTileMapper.kt @@ -38,10 +38,10 @@ constructor( QSTileState.build(resources, theme, config.uiConfig) { label = resources.getString(R.string.battery_detail_switch_title) contentDescription = label - iconRes = + val iconRes = if (data.isPowerSaving) R.drawable.qs_battery_saver_icon_on else R.drawable.qs_battery_saver_icon_off - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) sideViewIcon = QSTileState.SideViewIcon.None if (data.isPluggedIn) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapper.kt index 21b9f659dde4..e5a0fe8ed048 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/colorcorrection/domain/ColorCorrectionTileMapper.kt @@ -37,8 +37,8 @@ constructor( override fun map(config: QSTileConfig, data: ColorCorrectionTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { val subtitleArray = resources.getStringArray(R.array.tile_states_color_correction) - iconRes = R.drawable.ic_qs_color_correction - icon = Icon.Loaded(resources.getDrawable(R.drawable.ic_qs_color_correction)!!, null) + val iconRes = R.drawable.ic_qs_color_correction + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) if (data.isEnabled) { activationState = QSTileState.ActivationState.ACTIVE secondaryLabel = subtitleArray[2] diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapper.kt index 2dfb1fc4fe98..32ccba6f1fa5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/FlashlightMapper.kt @@ -35,14 +35,14 @@ constructor(@ShadeDisplayAware private val resources: Resources, private val the override fun map(config: QSTileConfig, data: FlashlightTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { - iconRes = + val iconRes = if (data is FlashlightTileModel.FlashlightAvailable && data.isEnabled) { R.drawable.qs_flashlight_icon_on } else { R.drawable.qs_flashlight_icon_off } - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) contentDescription = label diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapper.kt index 7f41cbd322dd..c571b136e18b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/fontscaling/domain/FontScalingTileMapper.kt @@ -36,8 +36,8 @@ constructor( override fun map(config: QSTileConfig, data: FontScalingTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { - iconRes = R.drawable.ic_qs_font_scaling - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + val iconRes = R.drawable.ic_qs_font_scaling + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) contentDescription = label activationState = QSTileState.ActivationState.ACTIVE sideViewIcon = QSTileState.SideViewIcon.Chevron diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/HearingDevicesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/HearingDevicesTileMapper.kt index 4c302b363c3b..12f71491c7b4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/HearingDevicesTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/hearingdevices/domain/HearingDevicesTileMapper.kt @@ -37,8 +37,8 @@ constructor( override fun map(config: QSTileConfig, data: HearingDevicesTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { label = resources.getString(R.string.quick_settings_hearing_devices_label) - iconRes = R.drawable.qs_hearing_devices_icon - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + val iconRes = R.drawable.qs_hearing_devices_icon + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) sideViewIcon = QSTileState.SideViewIcon.Chevron contentDescription = label if (data.isAnyActiveHearingDevice) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt index 1a6876d0b765..7ad01e463399 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt @@ -61,11 +61,11 @@ constructor( when (val dataIcon = data.icon) { is InternetTileIconModel.ResourceId -> { - iconRes = dataIcon.resId icon = Icon.Loaded( resources.getDrawable(dataIcon.resId, theme), contentDescription = null, + dataIcon.resId, ) } @@ -76,11 +76,11 @@ constructor( } is InternetTileIconModel.Satellite -> { - iconRes = dataIcon.resourceIcon.res // level is inferred from res icon = Icon.Loaded( resources.getDrawable(dataIcon.resourceIcon.res, theme), contentDescription = null, + dataIcon.resourceIcon.res, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapper.kt index 8d35b2413bad..05590e803ffa 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/inversion/domain/ColorInversionTileMapper.kt @@ -35,7 +35,7 @@ constructor(@ShadeDisplayAware private val resources: Resources, private val the override fun map(config: QSTileConfig, data: ColorInversionTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { val subtitleArray = resources.getStringArray(R.array.tile_states_inversion) - + val iconRes: Int if (data.isEnabled) { activationState = QSTileState.ActivationState.ACTIVE secondaryLabel = subtitleArray[2] @@ -45,7 +45,7 @@ constructor(@ShadeDisplayAware private val resources: Resources, private val the secondaryLabel = subtitleArray[1] iconRes = R.drawable.qs_invert_colors_icon_off } - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) contentDescription = label supportedActions = setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapper.kt index 3557c1a4ac9d..afb137e1e92f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/irecording/IssueRecordingMapper.kt @@ -39,6 +39,7 @@ constructor(@ShadeDisplayAware private val resources: Resources, private val the Icon.Loaded( resources.getDrawable(R.drawable.qs_record_issue_icon_on, theme), null, + R.drawable.qs_record_issue_icon_on, ) } else { activationState = QSTileState.ActivationState.INACTIVE @@ -46,6 +47,7 @@ constructor(@ShadeDisplayAware private val resources: Resources, private val the Icon.Loaded( resources.getDrawable(R.drawable.qs_record_issue_icon_off, theme), null, + R.drawable.qs_record_issue_icon_off, ) } supportedActions = setOf(QSTileState.UserAction.CLICK) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/LocationTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/LocationTileMapper.kt index dfc24a10c491..ced5a4f099a2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/LocationTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/location/domain/LocationTileMapper.kt @@ -35,13 +35,13 @@ constructor(@ShadeDisplayAware private val resources: Resources, private val the override fun map(config: QSTileConfig, data: LocationTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { - iconRes = + val iconRes = if (data.isEnabled) { R.drawable.qs_location_icon_on } else { R.drawable.qs_location_icon_off } - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), contentDescription = null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) label = resources.getString(R.string.quick_settings_location_label) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt index 9b2880b6d47f..479f61823912 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt @@ -17,10 +17,10 @@ package com.android.systemui.qs.tiles.impl.modes.domain.interactor import android.content.Context +import android.graphics.drawable.Drawable import android.os.UserHandle import com.android.app.tracing.coroutines.flow.flowName import com.android.systemui.common.shared.model.Icon -import com.android.systemui.common.shared.model.asIcon import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.modes.shared.ModesUi import com.android.systemui.modes.shared.ModesUiIcons @@ -31,7 +31,6 @@ import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes -import com.android.systemui.statusbar.policy.domain.model.ZenModeInfo import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow @@ -68,37 +67,29 @@ constructor( suspend fun getCurrentTileModel() = buildTileData(zenModeInteractor.getActiveModes()) private fun buildTileData(activeModes: ActiveZenModes): ModesTileModel { - if (ModesUiIcons.isEnabled) { - val tileIcon = getTileIcon(activeModes.mainMode) - return ModesTileModel( - isActivated = activeModes.isAnyActive(), - icon = tileIcon.icon, - iconResId = tileIcon.resId, - activeModes = activeModes.modeNames, - ) - } else { - return ModesTileModel( - isActivated = activeModes.isAnyActive(), - icon = context.getDrawable(ModesTile.ICON_RES_ID)!!.asIcon(), - iconResId = ModesTile.ICON_RES_ID, - activeModes = activeModes.modeNames, - ) - } - } + val drawable: Drawable + val iconRes: Int? + val activeMode = activeModes.mainMode - private data class TileIcon(val icon: Icon.Loaded, val resId: Int?) - - private fun getTileIcon(activeMode: ZenModeInfo?): TileIcon { - return if (activeMode != null) { + if (ModesUiIcons.isEnabled && activeMode != null) { // ZenIconKey.resPackage is null if its resId is a system icon. - if (activeMode.icon.key.resPackage == null) { - TileIcon(activeMode.icon.drawable.asIcon(), activeMode.icon.key.resId) - } else { - TileIcon(activeMode.icon.drawable.asIcon(), null) - } + iconRes = + if (activeMode.icon.key.resPackage == null) { + activeMode.icon.key.resId + } else { + null + } + drawable = activeMode.icon.drawable } else { - TileIcon(context.getDrawable(ModesTile.ICON_RES_ID)!!.asIcon(), ModesTile.ICON_RES_ID) + iconRes = ModesTile.ICON_RES_ID + drawable = context.getDrawable(iconRes)!! } + + return ModesTileModel( + isActivated = activeModes.isAnyActive(), + icon = Icon.Loaded(drawable, null, iconRes), + activeModes = activeModes.modeNames, + ) } override fun availability(user: UserHandle): Flow<Boolean> = flowOf(ModesUi.isEnabled) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt index db4812342050..d0eacbc9a957 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/model/ModesTileModel.kt @@ -21,12 +21,10 @@ import com.android.systemui.common.shared.model.Icon data class ModesTileModel( val isActivated: Boolean, val activeModes: List<String>, - val icon: Icon.Loaded, - /** - * Resource id corresponding to [icon]. Will only be present if it's know to correspond to a - * resource with a known id in SystemUI (such as resources from `android.R`, - * `com.android.internal.R`, or `com.android.systemui.res` itself). + * icon.res will only be present if it is known to correspond to a resource with a known id in + * SystemUI (such as resources from `android.R`, `com.android.internal.R`, or + * `com.android.systemui.res` itself). */ - val iconResId: Int? = null + val icon: Icon.Loaded, ) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt index 1507ef4b3b58..99ae3b8db709 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt @@ -34,7 +34,6 @@ constructor(@ShadeDisplayAware private val resources: Resources, val theme: Reso QSTileDataToStateMapper<ModesTileModel> { override fun map(config: QSTileConfig, data: ModesTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { - iconRes = data.iconResId icon = data.icon activationState = if (data.isActivated) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt index 3569e4d0b42c..16b36289ad95 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/night/ui/NightDisplayTileMapper.kt @@ -49,7 +49,7 @@ constructor( supportedActions = setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) sideViewIcon = QSTileState.SideViewIcon.None - + val iconRes: Int if (data.isActivated) { activationState = QSTileState.ActivationState.ACTIVE iconRes = R.drawable.qs_nightlight_icon_on @@ -58,7 +58,7 @@ constructor( iconRes = R.drawable.qs_nightlight_icon_off } - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), contentDescription = null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) secondaryLabel = getSecondaryLabel(data, resources) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/notes/domain/NotesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/notes/domain/NotesTileMapper.kt index a5436192af39..ecdd71170cda 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/notes/domain/NotesTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/notes/domain/NotesTileMapper.kt @@ -35,8 +35,8 @@ constructor( ) : QSTileDataToStateMapper<NotesTileModel> { override fun map(config: QSTileConfig, data: NotesTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { - iconRes = R.drawable.ic_qs_notes - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), contentDescription = null) + val iconRes = R.drawable.ic_qs_notes + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) contentDescription = label activationState = QSTileState.ActivationState.INACTIVE sideViewIcon = QSTileState.SideViewIcon.Chevron diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapper.kt index 76f1e8b8760c..5b3ea93ab1ae 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/onehanded/ui/OneHandedModeTileMapper.kt @@ -38,8 +38,8 @@ constructor( QSTileState.build(resources, theme, config.uiConfig) { val subtitleArray = resources.getStringArray(R.array.tile_states_onehanded) label = resources.getString(R.string.quick_settings_onehanded_label) - iconRes = com.android.internal.R.drawable.ic_qs_one_handed_mode - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + val iconRes = com.android.internal.R.drawable.ic_qs_one_handed_mode + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) if (data.isEnabled) { activationState = QSTileState.ActivationState.ACTIVE secondaryLabel = subtitleArray[2] diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapper.kt index c546250e73d2..21e92d3a1972 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/qr/ui/QRCodeScannerTileMapper.kt @@ -38,8 +38,8 @@ constructor( QSTileState.build(resources, theme, config.uiConfig) { label = resources.getString(R.string.qr_code_scanner_title) contentDescription = label - iconRes = R.drawable.ic_qr_code_scanner - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + val iconRes = R.drawable.ic_qr_code_scanner + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) sideViewIcon = QSTileState.SideViewIcon.Chevron supportedActions = setOf(QSTileState.UserAction.CLICK) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapper.kt index 66d0f96fdcde..66759cdfd1a6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/reducebrightness/ui/ReduceBrightColorsTileMapper.kt @@ -37,6 +37,7 @@ constructor( override fun map(config: QSTileConfig, data: ReduceBrightColorsTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { + val iconRes: Int if (data.isEnabled) { activationState = QSTileState.ActivationState.ACTIVE iconRes = R.drawable.qs_extra_dim_icon_on @@ -50,7 +51,7 @@ constructor( resources .getStringArray(R.array.tile_states_reduce_brightness)[Tile.STATE_INACTIVE] } - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) label = resources.getString(com.android.internal.R.string.reduce_bright_colors_feature_name) contentDescription = label diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapper.kt index a0144221577d..000c7025e32b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/rotation/ui/mapper/RotationLockTileMapper.kt @@ -42,7 +42,7 @@ constructor( QSTileState.build(resources, theme, config.uiConfig) { label = resources.getString(R.string.quick_settings_rotation_unlocked_label) contentDescription = resources.getString(R.string.accessibility_quick_settings_rotation) - + val iconRes: Int if (data.isRotationLocked) { activationState = QSTileState.ActivationState.INACTIVE secondaryLabel = EMPTY_SECONDARY_STRING @@ -57,7 +57,7 @@ constructor( } iconRes = R.drawable.qs_auto_rotate_icon_on } - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) if (isDeviceFoldable(resources, deviceStateManager)) { secondaryLabel = getSecondaryLabelWithPosture(activationState) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapper.kt index aea4967c546c..1d5cf29f2462 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverTileMapper.kt @@ -36,6 +36,7 @@ constructor( override fun map(config: QSTileConfig, data: DataSaverTileModel): QSTileState = QSTileState.build(resources, theme, config.uiConfig) { with(data) { + val iconRes: Int if (isEnabled) { activationState = QSTileState.ActivationState.ACTIVE iconRes = R.drawable.qs_data_saver_icon_on @@ -45,7 +46,7 @@ constructor( iconRes = R.drawable.qs_data_saver_icon_off secondaryLabel = resources.getStringArray(R.array.tile_states_saver)[1] } - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) contentDescription = label supportedActions = setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt index f3136e015acf..0a61e3cbe616 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt @@ -38,7 +38,7 @@ constructor( QSTileState.build(resources, theme, config.uiConfig) { label = resources.getString(R.string.quick_settings_screen_record_label) supportedActions = setOf(QSTileState.UserAction.CLICK) - + val iconRes: Int when (data) { is ScreenRecordModel.Recording -> { activationState = QSTileState.ActivationState.ACTIVE @@ -61,7 +61,7 @@ constructor( resources.getString(R.string.quick_settings_screen_record_start) } } - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) contentDescription = if (TextUtils.isEmpty(secondaryLabel)) label diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapper.kt index 73e61b7d178e..f54f46c01dee 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/sensorprivacy/ui/SensorPrivacyToggleTileMapper.kt @@ -50,8 +50,8 @@ constructor( contentDescription = label supportedActions = setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) - iconRes = sensorPrivacyTileResources.getIconRes(data.isBlocked) - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + val iconRes = sensorPrivacyTileResources.getIconRes(data.isBlocked) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) sideViewIcon = QSTileState.SideViewIcon.None if (data.isBlocked) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapper.kt index e9aa46c5f253..5933d65bc61f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/UiModeNightTileMapper.kt @@ -116,11 +116,11 @@ constructor(@ShadeDisplayAware private val resources: Resources, private val the } } - iconRes = + val iconRes = if (activationState == QSTileState.ActivationState.ACTIVE) R.drawable.qs_light_dark_theme_icon_on else R.drawable.qs_light_dark_theme_icon_off - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), null) + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) supportedActions = if (activationState == QSTileState.ActivationState.UNAVAILABLE) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt index 6a3195a493c8..5b462ba074ec 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt @@ -41,8 +41,8 @@ constructor( QSTileState.build(resources, theme, config.uiConfig) { label = getTileLabel()!! contentDescription = label - iconRes = com.android.internal.R.drawable.stat_sys_managed_profile_status - icon = Icon.Loaded(resources.getDrawable(iconRes!!, theme), contentDescription = null) + val iconRes = com.android.internal.R.drawable.stat_sys_managed_profile_status + icon = Icon.Loaded(resources.getDrawable(iconRes, theme), null, iconRes) when (data) { is WorkModeTileModel.HasActiveProfile -> { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt index 8394be5e0a38..c6af729cd4a7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileState.kt @@ -36,7 +36,6 @@ import kotlin.reflect.KClass */ data class QSTileState( val icon: Icon?, - val iconRes: Int?, val label: CharSequence, val activationState: ActivationState, val secondaryLabel: CharSequence?, @@ -58,7 +57,7 @@ data class QSTileState( ): QSTileState { val iconDrawable = resources.getDrawable(config.iconRes, theme) return build( - Icon.Loaded(iconDrawable, null), + Icon.Loaded(iconDrawable, null, config.iconRes), resources.getString(config.labelRes), builder, ) @@ -115,7 +114,6 @@ data class QSTileState( } class Builder(var icon: Icon?, var label: CharSequence) { - var iconRes: Int? = null var activationState: ActivationState = ActivationState.INACTIVE var secondaryLabel: CharSequence? = null var supportedActions: Set<UserAction> = setOf(UserAction.CLICK) @@ -128,7 +126,6 @@ data class QSTileState( fun build(): QSTileState = QSTileState( icon, - iconRes, label, activationState, secondaryLabel, diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt index 632eeefcb462..c34edc81bfe7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt @@ -260,8 +260,8 @@ constructor( icon = when (val stateIcon = viewModelState.icon) { is Icon.Loaded -> - if (viewModelState.iconRes == null) DrawableIcon(stateIcon.drawable) - else DrawableIconWithRes(stateIcon.drawable, viewModelState.iconRes) + if (stateIcon.res == null) DrawableIcon(stateIcon.drawable) + else DrawableIconWithRes(stateIcon.drawable, stateIcon.res) is Icon.Resource -> ResourceIcon.get(stateIcon.res) null -> null } diff --git a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt index 8c54ab40c680..862dba1e7294 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/user/UserSwitchDialogController.kt @@ -17,7 +17,6 @@ package com.android.systemui.qs.user import android.app.Dialog -import android.content.Context import android.content.DialogInterface import android.content.DialogInterface.BUTTON_NEUTRAL import android.content.Intent @@ -34,6 +33,7 @@ import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.QSUserSwitcherEvent import com.android.systemui.qs.tiles.UserDetailView import com.android.systemui.res.R +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.user.ui.dialog.DialogShowerImpl import javax.inject.Inject @@ -50,6 +50,7 @@ constructor( private val dialogTransitionAnimator: DialogTransitionAnimator, private val uiEventLogger: UiEventLogger, private val dialogFactory: SystemUIDialog.Factory, + private val shadeDialogContextInteractor: ShadeDialogContextInteractor, ) { companion object { @@ -63,7 +64,8 @@ constructor( * Populate the dialog with information from and adapter obtained from * [userDetailViewAdapterProvider] and show it as launched from [expandable]. */ - fun showDialog(context: Context, expandable: Expandable) { + fun showDialog(expandable: Expandable) { + val context = shadeDialogContextInteractor.context with(dialogFactory.create(context)) { setShowForAllUsers(true) setCanceledOnTouchOutside(true) diff --git a/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java b/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java index a7b51faaed57..10ac2cf76763 100644 --- a/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java @@ -16,6 +16,8 @@ package com.android.systemui.scrim; +import static com.android.systemui.Flags.notificationShadeBlur; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; @@ -214,8 +216,7 @@ public class ScrimDrawable extends Drawable { public void draw(@NonNull Canvas canvas) { mPaint.setColor(mMainColor); mPaint.setAlpha(mAlpha); - if (WindowBlurFlag.isEnabled()) { - // TODO(b/370555223): Match the alpha to the visual spec when it is finalized. + if (notificationShadeBlur() || WindowBlurFlag.isEnabled()) { // TODO (b/381263600), wire this at ScrimController, move it to PrimaryBouncerTransition mPaint.setAlpha((int) (0.5f * mAlpha)); } diff --git a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java index 4bfa61e9dcd4..0f80e7432a54 100644 --- a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java +++ b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java @@ -16,6 +16,8 @@ package com.android.systemui.scrim; +import static com.android.systemui.Flags.notificationShadeBlur; + import static java.lang.Float.isNaN; import android.annotation.NonNull; @@ -39,13 +41,12 @@ import androidx.core.graphics.ColorUtils; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.colorextraction.ColorExtractor; +import com.android.systemui.res.R; import com.android.systemui.shade.TouchLogger; import com.android.systemui.util.LargeScreenUtils; import java.util.concurrent.Executor; -import static com.android.systemui.Flags.notificationShadeBlur; - /** * A view which can draw a scrim. This view maybe be used in multiple windows running on different * threads, but is controlled by {@link com.android.systemui.statusbar.phone.ScrimController} so we @@ -253,8 +254,11 @@ public class ScrimView extends View { mainTinted = ColorUtils.blendARGB(mColors.getMainColor(), mTintColor, tintAmount); } if (notificationShadeBlur()) { - // TODO(b/370555223): Fix color and transparency to match visual spec exactly - mainTinted = ColorUtils.blendARGB(mColors.getMainColor(), Color.GRAY, 0.5f); + int layerAbove = ColorUtils.setAlphaComponent( + getResources().getColor(R.color.shade_panel, null), + (int) (0.4f * 255)); + int layerBelow = ColorUtils.setAlphaComponent(Color.WHITE, (int) (0.1f * 255)); + mainTinted = ColorUtils.compositeColors(layerAbove, layerBelow); } drawable.setColor(mainTinted, animated); } else { diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt index e1631ccdcb06..bbb13d5c1dfe 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserTracker.kt @@ -61,9 +61,18 @@ interface UserTracker : UserContentResolverProvider, UserContextProvider { /** Callback for notifying of changes. */ @WeaklyReferencedCallback interface Callback { - /** Notifies that the current user will be changed. */ + /** + * Same as {@link onBeforeUserSwitching(Int, Runnable)} but the callback will be called + * automatically after the completion of this method. + */ fun onBeforeUserSwitching(newUser: Int) {} + /** Notifies that the current user will be changed. */ + fun onBeforeUserSwitching(newUser: Int, resultCallback: Runnable) { + onBeforeUserSwitching(newUser) + resultCallback.run() + } + /** * Same as {@link onUserChanging(Int, Context, Runnable)} but the callback will be called * automatically after the completion of this method. diff --git a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt index b7a3aedc565e..42d83637ec1a 100644 --- a/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/settings/UserTrackerImpl.kt @@ -196,8 +196,9 @@ internal constructor( private fun registerUserSwitchObserver() { iActivityManager.registerUserSwitchObserver( object : UserSwitchObserver() { - override fun onBeforeUserSwitching(newUserId: Int) { + override fun onBeforeUserSwitching(newUserId: Int, reply: IRemoteCallback?) { handleBeforeUserSwitching(newUserId) + reply?.sendResult(null) } override fun onUserSwitching(newUserId: Int, reply: IRemoteCallback?) { @@ -236,8 +237,7 @@ internal constructor( setUserIdInternal(newUserId) notifySubscribers { callback, resultCallback -> - callback.onBeforeUserSwitching(newUserId) - resultCallback.run() + callback.onBeforeUserSwitching(newUserId, resultCallback) } .await() } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index f2c39063c867..839d4596bb7c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -48,7 +48,6 @@ import com.android.systemui.flags.FeatureFlagsClassic; import com.android.systemui.flags.Flags; import com.android.systemui.keyevent.domain.interactor.SysUIKeyEventHandler; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; -import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.Edge; import com.android.systemui.keyguard.shared.model.KeyguardState; @@ -367,9 +366,7 @@ public class NotificationShadeWindowViewController implements Dumpable { mTouchActive = true; mTouchCancelled = false; mDownEvent = ev; - if (MigrateClocksToBlueprint.isEnabled()) { - mService.userActivity(); - } + mService.userActivity(); } else if (ev.getActionMasked() == MotionEvent.ACTION_UP || ev.getActionMasked() == MotionEvent.ACTION_CANCEL) { mTouchActive = false; @@ -443,8 +440,7 @@ public class NotificationShadeWindowViewController implements Dumpable { float x = ev.getRawX(); float y = ev.getRawY(); if (mStatusBarViewController.touchIsWithinView(x, y)) { - if (!(MigrateClocksToBlueprint.isEnabled() - && mPrimaryBouncerInteractor.isBouncerShowing())) { + if (!mPrimaryBouncerInteractor.isBouncerShowing()) { if (mStatusBarWindowStateController.windowIsShowing()) { mIsTrackingBarGesture = true; return logDownDispatch(ev, "sending touch to status bar", @@ -453,7 +449,7 @@ public class NotificationShadeWindowViewController implements Dumpable { return logDownDispatch(ev, "hidden or hiding", true); } } else { - mShadeLogger.d("NSWVC: bouncer not showing"); + mShadeLogger.d("NSWVC: bouncer showing"); } } else { mShadeLogger.d("NSWVC: touch not within view"); @@ -511,34 +507,24 @@ public class NotificationShadeWindowViewController implements Dumpable { && !bouncerShowing && !mStatusBarStateController.isDozing()) { if (mDragDownHelper.isDragDownEnabled()) { - if (MigrateClocksToBlueprint.isEnabled()) { - // When on lockscreen, if the touch originates at the top of the screen - // go directly to QS and not the shade - if (mStatusBarStateController.getState() == KEYGUARD - && mQuickSettingsController.shouldQuickSettingsIntercept( - ev.getX(), ev.getY(), 0)) { - mShadeLogger.d("NSWVC: QS intercepted"); - return true; - } + // When on lockscreen, if the touch originates at the top of the screen go + // directly to QS and not the shade + if (mStatusBarStateController.getState() == KEYGUARD + && mQuickSettingsController.shouldQuickSettingsIntercept( + ev.getX(), ev.getY(), 0)) { + mShadeLogger.d("NSWVC: QS intercepted"); + return true; } // This handles drag down over lockscreen boolean result = mDragDownHelper.onInterceptTouchEvent(ev); - if (MigrateClocksToBlueprint.isEnabled()) { - if (result) { - mLastInterceptWasDragDownHelper = true; - if (ev.getAction() == MotionEvent.ACTION_DOWN) { - mShadeLogger.d("NSWVC: drag down helper intercepted"); - } - } else if (didNotificationPanelInterceptEvent(ev)) { - return true; - } - } else { - if (result) { - if (ev.getAction() == MotionEvent.ACTION_DOWN) { - mShadeLogger.d("NSWVC: drag down helper intercepted"); - } + if (result) { + mLastInterceptWasDragDownHelper = true; + if (ev.getAction() == MotionEvent.ACTION_DOWN) { + mShadeLogger.d("NSWVC: drag down helper intercepted"); } + } else if (didNotificationPanelInterceptEvent(ev)) { + return true; } return result; } else { @@ -547,12 +533,10 @@ public class NotificationShadeWindowViewController implements Dumpable { return true; } } - } else if (MigrateClocksToBlueprint.isEnabled()) { + } else if (!bouncerShowing && didNotificationPanelInterceptEvent(ev)) { // This final check handles swipes on HUNs and when Pulsing - if (!bouncerShowing && didNotificationPanelInterceptEvent(ev)) { - mShadeLogger.d("NSWVC: intercepted for HUN/PULSING"); - return true; - } + mShadeLogger.d("NSWVC: intercepted for HUN/PULSING"); + return true; } return false; } @@ -562,9 +546,6 @@ public class NotificationShadeWindowViewController implements Dumpable { MotionEvent cancellation = MotionEvent.obtain(ev); cancellation.setAction(MotionEvent.ACTION_CANCEL); mStackScrollLayout.onInterceptTouchEvent(cancellation); - if (!MigrateClocksToBlueprint.isEnabled()) { - mShadeViewController.handleExternalInterceptTouch(cancellation); - } cancellation.recycle(); } @@ -574,22 +555,12 @@ public class NotificationShadeWindowViewController implements Dumpable { if (mStatusBarStateController.isDozing()) { handled = !mDozeServiceHost.isPulsing(); } - if (MigrateClocksToBlueprint.isEnabled()) { - if (mLastInterceptWasDragDownHelper && (mDragDownHelper.isDraggingDown())) { - // we still want to finish our drag down gesture when locking the screen - handled |= mDragDownHelper.onTouchEvent(ev) || handled; - } - if (!handled && mShadeViewController.handleExternalTouch(ev)) { - return true; - } - } else { - if (mDragDownHelper.isDragDownEnabled() - || mDragDownHelper.isDraggingDown()) { - // we still want to finish our drag down gesture when locking the screen - return mDragDownHelper.onTouchEvent(ev) || handled; - } else { - return handled; - } + if (mLastInterceptWasDragDownHelper && (mDragDownHelper.isDraggingDown())) { + // we still want to finish our drag down gesture when locking the screen + handled |= mDragDownHelper.onTouchEvent(ev) || handled; + } + if (!handled && mShadeViewController.handleExternalTouch(ev)) { + return true; } return handled; } @@ -673,14 +644,12 @@ public class NotificationShadeWindowViewController implements Dumpable { } private boolean didNotificationPanelInterceptEvent(MotionEvent ev) { - if (MigrateClocksToBlueprint.isEnabled()) { - // Since NotificationStackScrollLayout is now a sibling of notification_panel, we need - // to also ask NotificationPanelViewController directly, in order to process swipe up - // events originating from notifications - if (mShadeViewController.handleExternalInterceptTouch(ev)) { - mShadeLogger.d("NSWVC: NPVC intercepted"); - return true; - } + // Since NotificationStackScrollLayout is now a sibling of notification_panel, we need to + // also ask NotificationPanelViewController directly, in order to process swipe up events + // originating from notifications + if (mShadeViewController.handleExternalInterceptTouch(ev)) { + mShadeLogger.d("NSWVC: NPVC intercepted"); + return true; } return false; @@ -707,9 +676,7 @@ public class NotificationShadeWindowViewController implements Dumpable { if (!SceneContainerFlag.isEnabled()) { mAmbientState.setSwipingUp(false); } - if (MigrateClocksToBlueprint.isEnabled()) { - mDragDownHelper.stopDragging(); - } + mDragDownHelper.stopDragging(); } private void setBrightnessMirrorShowingForDepth(boolean showing) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt index 207439e1f374..58111576574e 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt @@ -21,7 +21,6 @@ import android.view.ViewGroup import android.view.WindowInsets import androidx.annotation.VisibleForTesting import androidx.constraintlayout.widget.ConstraintSet -import androidx.constraintlayout.widget.ConstraintSet.BOTTOM import androidx.constraintlayout.widget.ConstraintSet.END import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START @@ -32,7 +31,6 @@ import com.android.systemui.customization.R as customR import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.fragments.FragmentService -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.navigationbar.NavigationModeController import com.android.systemui.plugins.qs.QS @@ -275,7 +273,6 @@ constructor( constraintSet.clone(mView) setKeyguardStatusViewConstraints(constraintSet) setQsConstraints(constraintSet) - setNotificationsConstraints(constraintSet) setLargeScreenShadeHeaderConstraints(constraintSet) mView.applyConstraints(constraintSet) } @@ -288,21 +285,6 @@ constructor( } } - private fun setNotificationsConstraints(constraintSet: ConstraintSet) { - if (MigrateClocksToBlueprint.isEnabled) { - return - } - val startConstraintId = if (splitShadeEnabled) R.id.qs_edge_guideline else PARENT_ID - val nsslId = R.id.notification_stack_scroller - constraintSet.apply { - connect(nsslId, START, startConstraintId, START) - setMargin(nsslId, START, if (splitShadeEnabled) 0 else panelMarginHorizontal) - setMargin(nsslId, END, panelMarginHorizontal) - setMargin(nsslId, TOP, topMargin) - setMargin(nsslId, BOTTOM, notificationsBottomMargin) - } - } - private fun setQsConstraints(constraintSet: ConstraintSet) { val endConstraintId = if (splitShadeEnabled) R.id.qs_edge_guideline else PARENT_ID constraintSet.apply { diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java index 13330553b2de..000a666bac0d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java @@ -21,7 +21,6 @@ import static androidx.constraintlayout.core.widgets.Optimizer.OPTIMIZATION_GRAP import android.app.Fragment; import android.content.Context; import android.content.res.Configuration; -import android.graphics.Canvas; import android.graphics.Rect; import android.util.AttributeSet; import android.view.MotionEvent; @@ -33,13 +32,10 @@ import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintSet; import com.android.systemui.fragments.FragmentHostManager.FragmentListener; -import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.plugins.qs.QS; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.AboveShelfObserver; -import java.util.ArrayList; -import java.util.Comparator; import java.util.function.Consumer; /** @@ -50,11 +46,7 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout private View mQsFrame; private View mStackScroller; - private View mKeyguardStatusBar; - private final ArrayList<View> mDrawingOrderedChildren = new ArrayList<>(); - private final ArrayList<View> mLayoutDrawingOrder = new ArrayList<>(); - private final Comparator<View> mIndexComparator = Comparator.comparingInt(this::indexOfChild); private Consumer<WindowInsets> mInsetsChangedListener = insets -> {}; private Consumer<QS> mQSFragmentAttachedListener = qs -> {}; private QS mQs; @@ -80,7 +72,6 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout protected void onFinishInflate() { super.onFinishInflate(); mQsFrame = findViewById(R.id.qs_frame); - mKeyguardStatusBar = findViewById(R.id.keyguard_header); } void setStackScroller(View stackScroller) { @@ -160,46 +151,11 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout } @Override - protected void dispatchDraw(Canvas canvas) { - mDrawingOrderedChildren.clear(); - mLayoutDrawingOrder.clear(); - if (mKeyguardStatusBar.getVisibility() == View.VISIBLE) { - mDrawingOrderedChildren.add(mKeyguardStatusBar); - mLayoutDrawingOrder.add(mKeyguardStatusBar); - } - if (mQsFrame.getVisibility() == View.VISIBLE) { - mDrawingOrderedChildren.add(mQsFrame); - mLayoutDrawingOrder.add(mQsFrame); - } - if (mStackScroller.getVisibility() == View.VISIBLE) { - mDrawingOrderedChildren.add(mStackScroller); - mLayoutDrawingOrder.add(mStackScroller); - } - - // Let's now find the order that the view has when drawing regularly by sorting - mLayoutDrawingOrder.sort(mIndexComparator); - super.dispatchDraw(canvas); - } - - @Override public boolean dispatchTouchEvent(MotionEvent ev) { return TouchLogger.logDispatchTouch("NotificationsQuickSettingsContainer", ev, super.dispatchTouchEvent(ev)); } - @Override - protected boolean drawChild(Canvas canvas, View child, long drawingTime) { - if (MigrateClocksToBlueprint.isEnabled()) { - return super.drawChild(canvas, child, drawingTime); - } - int layoutIndex = mLayoutDrawingOrder.indexOf(child); - if (layoutIndex >= 0) { - return super.drawChild(canvas, mDrawingOrderedChildren.get(layoutIndex), drawingTime); - } else { - return super.drawChild(canvas, child, drawingTime); - } - } - public void applyConstraints(ConstraintSet constraintSet) { constraintSet.applyTo(this); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java index 0df2299eb8dd..4fb43fdcfdae 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java @@ -68,7 +68,6 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.fragments.FragmentHostManager; -import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.media.controls.domain.pipeline.MediaDataManager; import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager; import com.android.systemui.plugins.FalsingManager; @@ -1828,16 +1827,6 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum "onQsIntercept: down action, QS partially expanded/collapsed"); return true; } - // TODO (b/265193930): remove dependency on NPVC - if (mPanelViewControllerLazy.get().isKeyguardShowing() - && shouldQuickSettingsIntercept(mInitialTouchX, mInitialTouchY, 0)) { - // Dragging down on the lockscreen statusbar should prohibit other interactions - // immediately, otherwise we'll wait on the touchslop. This is to allow - // dragging down to expanded quick settings directly on the lockscreen. - if (!MigrateClocksToBlueprint.isEnabled()) { - mPanelView.getParent().requestDisallowInterceptTouchEvent(true); - } - } if (mExpansionAnimator != null) { mInitialHeightOnTouch = mExpansionHeight; mShadeLog.logMotionEvent(event, @@ -1879,9 +1868,6 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum && Math.abs(h) > Math.abs(x - mInitialTouchX) && shouldQuickSettingsIntercept( mInitialTouchX, mInitialTouchY, h)) { - if (!MigrateClocksToBlueprint.isEnabled()) { - mPanelView.getParent().requestDisallowInterceptTouchEvent(true); - } mShadeLog.onQsInterceptMoveQsTrackingEnabled(h); setTracking(true); traceQsJank(true, false); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt index d31868ca0217..61b9f0819f56 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt @@ -35,6 +35,8 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.android.systemui.scene.ui.view.WindowRootView import com.android.systemui.shade.data.repository.MutableShadeDisplaysRepository +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractorImpl import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.shade.data.repository.ShadeDisplaysRepositoryImpl import com.android.systemui.shade.display.ShadeDisplayPolicyModule @@ -216,6 +218,25 @@ object ShadeDisplayAwareModule { } @Provides + @SysUISingleton + fun provideShadeDialogContextInteractor( + impl: ShadeDialogContextInteractorImpl + ): ShadeDialogContextInteractor = impl + + @Provides + @IntoMap + @ClassKey(ShadeDialogContextInteractor::class) + fun provideShadeDialogContextInteractorCoreStartable( + impl: Provider<ShadeDialogContextInteractorImpl> + ): CoreStartable { + return if (ShadeWindowGoesAround.isEnabled) { + impl.get() + } else { + CoreStartable.NOP + } + } + + @Provides @IntoMap @ClassKey(ShadePrimaryDisplayCommand::class) fun provideShadePrimaryDisplayCommand( diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/FakeShadeDialogContextInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/FakeShadeDialogContextInteractor.kt new file mode 100644 index 000000000000..455370c726a2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/FakeShadeDialogContextInteractor.kt @@ -0,0 +1,23 @@ +/* + * 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.shade.domain.interactor + +import android.content.Context + +/** Fake context repository that always returns the same context. */ +class FakeShadeDialogContextInteractor(override val context: Context) : + ShadeDialogContextInteractor diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDialogContextInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDialogContextInteractor.kt new file mode 100644 index 000000000000..201dc0339a0a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDialogContextInteractor.kt @@ -0,0 +1,97 @@ +/* + * 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.shade.domain.interactor + +import android.content.Context +import android.util.Log +import android.view.Display +import android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL +import com.android.app.tracing.coroutines.launchTraced +import com.android.app.tracing.traceSection +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository +import com.android.systemui.shade.data.repository.ShadeDisplaysRepository +import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround +import javax.inject.Inject +import javax.inject.Provider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.filter + +/** Provides the correct context to show dialogs on the shade window, whenever it moves. */ +interface ShadeDialogContextInteractor { + /** Context usable to create dialogs on the notification shade display. */ + val context: Context +} + +@SysUISingleton +class ShadeDialogContextInteractorImpl +@Inject +constructor( + @Main private val defaultContext: Context, + private val displayWindowPropertyRepository: Provider<DisplayWindowPropertiesRepository>, + private val shadeDisplaysRepository: ShadeDisplaysRepository, + @Background private val bgScope: CoroutineScope, +) : CoreStartable, ShadeDialogContextInteractor { + + override fun start() { + if (ShadeWindowGoesAround.isUnexpectedlyInLegacyMode()) return + bgScope.launchTraced(TAG) { + shadeDisplaysRepository.displayId + // No need for default display pre-warming. + .filter { it != Display.DEFAULT_DISPLAY } + .collectLatest { displayId -> + // Prewarms the context in the background every time the display changes. + // In this way, there will be no main thread delays when a dialog is shown. + getContextOrDefault(displayId) + } + } + } + + override val context: Context + get() { + if (!ShadeWindowGoesAround.isEnabled) { + return defaultContext + } + val displayId = shadeDisplaysRepository.displayId.value + return getContextOrDefault(displayId) + } + + private fun getContextOrDefault(displayId: Int): Context { + return try { + traceSection({ "Getting dialog context for displayId=$displayId" }) { + displayWindowPropertyRepository.get().get(displayId, DIALOG_WINDOW_TYPE).context + } + } catch (e: Exception) { + // This can happen if the display was disconnected in the meantime. + Log.e( + TAG, + "Couldn't get dialog context for displayId=$displayId. Returning default one", + e, + ) + defaultContext + } + } + + private companion object { + const val TAG = "ShadeDialogContextRepo" + const val DIALOG_WINDOW_TYPE = TYPE_STATUS_BAR_SUB_PANEL + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java index b2ca33a4aecf..a7ad46296e08 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java @@ -44,13 +44,11 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.jank.InteractionJankMonitor.Configuration; import com.android.internal.logging.UiEventLogger; -import com.android.keyguard.KeyguardClockSwitch; import com.android.systemui.DejankUtils; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor; import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus; -import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; @@ -136,7 +134,6 @@ public class StatusBarStateControllerImpl implements private HistoricalState[] mHistoricalRecords = new HistoricalState[HISTORY_SIZE]; // These views are used by InteractionJankMonitor to get callback from HWUI. private View mView; - private KeyguardClockSwitch mClockSwitchView; /** * If any of the system bars is hidden. @@ -426,7 +423,6 @@ public class StatusBarStateControllerImpl implements if ((mView == null || !mView.isAttachedToWindow()) && (view != null && view.isAttachedToWindow())) { mView = view; - mClockSwitchView = view.findViewById(R.id.keyguard_clock_container); } mDozeAmountTarget = dozeAmount; if (animated) { @@ -511,16 +507,7 @@ public class StatusBarStateControllerImpl implements /** Returns the id of the currently rendering clock */ public String getClockId() { - if (MigrateClocksToBlueprint.isEnabled()) { - return mKeyguardClockInteractorLazy.get().getRenderedClockId(); - } - - if (mClockSwitchView == null) { - Log.e(TAG, "Clock container was missing"); - return KeyguardClockSwitch.MISSING_CLOCK_ID; - } - - return mClockSwitchView.getClockId(); + return mKeyguardClockInteractorLazy.get().getRenderedClockId(); } private void beginInteractionJankMonitor() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt index 66af275bc702..a7dbb47bc609 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/notification/ui/viewmodel/NotifChipsViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.chips.notification.ui.viewmodel import android.view.View +import com.android.systemui.Flags import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.statusbar.chips.notification.domain.interactor.StatusBarNotificationChipsInteractor @@ -99,6 +100,17 @@ constructor( ) } + if (Flags.promoteNotificationsAutomatically()) { + // When we're promoting notifications automatically, the `when` time set on the + // notification will likely just be set to the current time, which would cause the chip + // to always show "now". We don't want early testers to get that experience since it's + // not what will happen at launch, so just don't show any time. + // TODO(b/364653005): Only ignore the `when` time if the notification was + // *automatically* promoted (as opposed to being legitimately promoted by the + // criteria). We'll need to track that status somehow. + return OngoingActivityChipModel.Shown.IconOnly(icon, colors, onClickListener) + } + if (this.promotedContent.time == null) { return OngoingActivityChipModel.Shown.IconOnly(icon, colors, onClickListener) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/StatusBarPopupChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/StatusBarPopupChips.kt new file mode 100644 index 000000000000..9f523fc845ab --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/StatusBarPopupChips.kt @@ -0,0 +1,61 @@ +/* + * 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.statusbar.chips.notification.shared + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the status bar popup chips flag state. */ +@Suppress("NOTHING_TO_INLINE") +object StatusBarPopupChips { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_STATUS_BAR_POPUP_CHIPS + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.statusBarPopupChips() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is not enabled to ensure that the refactor author catches issues in testing. + * Caution!! Using this check incorrectly will cause crashes in nextfood builds! + */ + @JvmStatic + inline fun assertInNewMode() = RefactorFlagUtils.assertInNewMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt new file mode 100644 index 000000000000..1663aebd7287 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/shared/model/PopupChipModel.kt @@ -0,0 +1,49 @@ +/* + * 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.statusbar.featurepods.popups.shared.model + +import com.android.systemui.common.shared.model.Icon + +/** + * Ids used to track different types of popup chips. Will be used to ensure only one chip is + * displaying its popup at a time. + */ +sealed class PopupChipId(val value: String) { + data object MediaControls : PopupChipId("MediaControls") +} + +/** Model for individual status bar popup chips. */ +sealed class PopupChipModel { + abstract val logName: String + abstract val chipId: PopupChipId + + data class Hidden(override val chipId: PopupChipId, val shouldAnimate: Boolean = true) : + PopupChipModel() { + override val logName = "Hidden(id=$chipId, anim=$shouldAnimate)" + } + + data class Shown( + override val chipId: PopupChipId, + val icon: Icon, + val chipText: String, + val isToggled: Boolean = false, + val onToggle: () -> Unit, + val onIconPressed: () -> Unit, + ) : PopupChipModel() { + override val logName = "Shown(id=$chipId, toggled=$isToggled)" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipViewModel.kt new file mode 100644 index 000000000000..5712be30ccd6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipViewModel.kt @@ -0,0 +1,29 @@ +/* + * 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.statusbar.featurepods.popups.ui.viewmodel + +import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel +import kotlinx.coroutines.flow.StateFlow + +/** + * Interface for a view model that knows the display requirements for a single type of status bar + * popup chip. + */ +interface StatusBarPopupChipViewModel { + /** A flow modeling the popup chip that should be shown (or not shown). */ + val chip: StateFlow<PopupChipModel> +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt new file mode 100644 index 000000000000..b390f29b166c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModel.kt @@ -0,0 +1,48 @@ +/* + * 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.statusbar.featurepods.popups.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipId +import com.android.systemui.statusbar.featurepods.popups.shared.model.PopupChipModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** + * View model deciding which system process chips to show in the status bar. Emits a list of + * PopupChipModels. + */ +@SysUISingleton +class StatusBarPopupChipsViewModel @Inject constructor(@Background scope: CoroutineScope) { + private data class PopupChipBundle( + val media: PopupChipModel = PopupChipModel.Hidden(chipId = PopupChipId.MediaControls) + ) + + private val incomingPopupChipBundle: Flow<PopupChipBundle?> = + flowOf(null).stateIn(scope, SharingStarted.Lazily, PopupChipBundle()) + + val popupChips: Flow<List<PopupChipModel>> = + incomingPopupChipBundle + .map { _ -> listOf(null).filterIsInstance<PopupChipModel.Shown>() } + .stateIn(scope, SharingStarted.Lazily, emptyList()) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java index 80e8f55b897a..d83acf34ca99 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.collection.inflation; import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC; @@ -186,6 +187,9 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC); if (AsyncHybridViewInflation.isEnabled()) { params.markContentViewsFreeable(FLAG_CONTENT_VIEW_SINGLE_LINE); + if (LockscreenOtpRedaction.isSingleLineViewEnabled()) { + params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE); + } } mRowContentBindStage.requestRebind(entry, null); } @@ -256,10 +260,10 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { params.requireContentViews(FLAG_CONTENT_VIEW_EXPANDED); params.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight); params.setUseMinimized(isMinimized); - // TODO b/358403414: use the different types of redaction - boolean needsRedaction = inflaterParams.getRedactionType() != REDACTION_TYPE_NONE; + int redactionType = inflaterParams.getRedactionType(); - if (needsRedaction) { + params.setRedactionType(redactionType); + if (redactionType != REDACTION_TYPE_NONE) { params.requireContentViews(FLAG_CONTENT_VIEW_PUBLIC); } else { params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC); @@ -276,8 +280,8 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { } if (LockscreenOtpRedaction.isSingleLineViewEnabled()) { - - if (inflaterParams.isChildInGroup() && needsRedaction) { + if (inflaterParams.isChildInGroup() + && redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) { params.requireContentViews(FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE); } else { params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index 8a1371f1c415..aa010cf63d5b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -62,6 +62,7 @@ import com.android.systemui.statusbar.notification.data.NotificationDataLayerMod import com.android.systemui.statusbar.notification.domain.NotificationDomainLayerModule; import com.android.systemui.statusbar.notification.domain.interactor.NotificationLaunchAnimationInteractor; import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModelModule; +import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.notification.icon.ConversationIconManager; import com.android.systemui.statusbar.notification.icon.IconManager; import com.android.systemui.statusbar.notification.init.NotificationsController; @@ -78,8 +79,7 @@ import com.android.systemui.statusbar.notification.logging.NotificationPanelLogg import com.android.systemui.statusbar.notification.logging.NotificationPanelLoggerImpl; import com.android.systemui.statusbar.notification.logging.dagger.NotificationsLogModule; import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor; -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationLogger; -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationsProvider; +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractorImpl; import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel; import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactory; import com.android.systemui.statusbar.notification.row.NotificationEntryProcessorFactoryLooperImpl; @@ -92,7 +92,6 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.StatusBarNotificationActivityStarter; -import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.policy.ZenModesCleanupStartable; import dagger.Binds; @@ -105,8 +104,6 @@ import kotlin.coroutines.CoroutineContext; import kotlinx.coroutines.CoroutineScope; -import java.util.Optional; - import javax.inject.Provider; /** @@ -315,21 +312,17 @@ public interface NotificationsModule { @ClassKey(ZenModesCleanupStartable.class) CoreStartable bindsZenModesCleanup(ZenModesCleanupStartable zenModesCleanup); - /** - * Provides {@link - * com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor} if - * one of the relevant feature flags is enabled. - */ + /** Provides the default implementation of {@link PromotedNotificationContentExtractor} if at + * least one of the relevant feature flags is enabled, or an implementation that always returns + * null if none are enabled. */ @Provides @SysUISingleton - static Optional<PromotedNotificationContentExtractor> - providePromotedNotificationContentExtractor( - PromotedNotificationsProvider provider, Context context, - PromotedNotificationLogger logger) { + static PromotedNotificationContentExtractor providesPromotedNotificationContentExtractor( + Provider<PromotedNotificationContentExtractorImpl> implProvider) { if (PromotedNotificationContentModel.featureFlagEnabled()) { - return Optional.of(new PromotedNotificationContentExtractor(provider, context, logger)); + return implProvider.get(); } else { - return Optional.empty(); + return (entry, recoveredBuilder) -> null; } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java index 6756077e5444..d02e17cab534 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImpl.java @@ -1299,7 +1299,6 @@ public class HeadsUpManagerImpl } private NotificationEntry requireEntry() { - /* check if */ SceneContainerFlag.isUnexpectedlyInLegacyMode(); return Objects.requireNonNull(mEntry); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt index 863c665eb4f5..4e9e3336b86f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractor.kt @@ -34,15 +34,22 @@ import com.android.systemui.statusbar.notification.promoted.shared.model.Promote import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When import javax.inject.Inject +interface PromotedNotificationContentExtractor { + fun extractContent( + entry: NotificationEntry, + recoveredBuilder: Notification.Builder, + ): PromotedNotificationContentModel? +} + @SysUISingleton -class PromotedNotificationContentExtractor +class PromotedNotificationContentExtractorImpl @Inject constructor( private val promotedNotificationsProvider: PromotedNotificationsProvider, @ShadeDisplayAware private val context: Context, private val logger: PromotedNotificationLogger, -) { - fun extractContent( +) : PromotedNotificationContentExtractor { + override fun extractContent( entry: NotificationEntry, recoveredBuilder: Notification.Builder, ): PromotedNotificationContentModel? { @@ -169,5 +176,5 @@ private fun CallStyle.extractContent(contentBuilder: PromotedNotificationContent private fun ProgressStyle.extractContent(contentBuilder: PromotedNotificationContentModel.Builder) { // TODO: Create NotificationProgressModel.toSkeleton, or something similar. - contentBuilder.progress = createProgressModel(0xffffffff.toInt(), 0x00000000) + contentBuilder.progress = createProgressModel(0xffffffff.toInt(), 0xff000000.toInt()) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt index 13ad1413e89d..a43f8dbc1b5d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationLogger.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.notification.promoted import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel.ERROR import com.android.systemui.log.core.LogLevel.INFO -import com.android.systemui.log.dagger.NotificationLog import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.logKey import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel @@ -27,7 +26,7 @@ import javax.inject.Inject class PromotedNotificationLogger @Inject -constructor(@NotificationLog private val buffer: LogBuffer) { +constructor(@PromotedNotificationLog private val buffer: LogBuffer) { fun logExtractionSkipped(entry: NotificationEntry, reason: String) { buffer.log( EXTRACTION_TAG, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt new file mode 100644 index 000000000000..0f21514fcc94 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractor.kt @@ -0,0 +1,34 @@ +/* + * 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.statusbar.notification.promoted.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +@SysUISingleton +class AODPromotedNotificationInteractor +@Inject +constructor(activeNotificationsInteractor: ActiveNotificationsInteractor) { + val content: Flow<PromotedNotificationContentModel?> = + activeNotificationsInteractor.topLevelRepresentativeNotifications.map { notifs -> + notifs.firstNotNullOfOrNull { it.promotedContent } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt index fe2dabe1ba8a..74809fd8622f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/shared/model/PromotedNotificationContentModel.kt @@ -28,7 +28,7 @@ import com.android.systemui.statusbar.notification.promoted.PromotedNotification * like the skeleton view on AOD or the status bar chip. */ data class PromotedNotificationContentModel( - val key: String, + val identity: Identity, // for all styles: val skeletonSmallIcon: Icon?, // TODO(b/377568176): Make into an IconModel. @@ -82,7 +82,7 @@ data class PromotedNotificationContentModel( fun build() = PromotedNotificationContentModel( - key = key, + identity = Identity(key, style), skeletonSmallIcon = skeletonSmallIcon, appName = appName, subText = subText, @@ -103,6 +103,8 @@ data class PromotedNotificationContentModel( ) } + data class Identity(val key: String, val style: Style) + /** The timestamp associated with a notification, along with the mode used to display it. */ data class When(val time: Long, val mode: Mode) { /** The mode used to display a notification's `when` value. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/AODPromotedNotificationViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/AODPromotedNotificationViewModel.kt new file mode 100644 index 000000000000..adfa6a10814d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/AODPromotedNotificationViewModel.kt @@ -0,0 +1,46 @@ +/* + * 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.statusbar.notification.promoted.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.notification.promoted.domain.interactor.AODPromotedNotificationInteractor +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Identity +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map + +@SysUISingleton +class AODPromotedNotificationViewModel +@Inject +constructor(interactor: AODPromotedNotificationInteractor) { + private val content: Flow<PromotedNotificationContentModel?> = interactor.content + private val identity: Flow<Identity?> = content.mapNonNullsKeepingNulls { it.identity } + + val notification: Flow<PromotedNotificationViewModel?> = + identity.distinctUntilChanged().mapNonNullsKeepingNulls { identity -> + val updates = interactor.content.filterNotNull().filter { it.identity == identity } + PromotedNotificationViewModel(identity, updates) + } +} + +private fun <T, R> Flow<T?>.mapNonNullsKeepingNulls(block: (T) -> R): Flow<R?> = map { + it?.let(block) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt new file mode 100644 index 000000000000..f265e0ff33f8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/ui/viewmodel/PromotedNotificationViewModel.kt @@ -0,0 +1,58 @@ +/* + * 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.statusbar.notification.promoted.ui.viewmodel + +import android.graphics.drawable.Icon +import com.android.internal.widget.NotificationProgressModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.Style +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel.When +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class PromotedNotificationViewModel( + identity: PromotedNotificationContentModel.Identity, + content: Flow<PromotedNotificationContentModel>, +) { + // for all styles: + + val key: String = identity.key + val style: Style = identity.style + + val skeletonSmallIcon: Flow<Icon?> = content.map { it.skeletonSmallIcon } + val appName: Flow<CharSequence?> = content.map { it.appName } + val subText: Flow<CharSequence?> = content.map { it.subText } + + private val time: Flow<When?> = content.map { it.time } + val whenTime: Flow<Long?> = time.map { it?.time } + val whenMode: Flow<When.Mode?> = time.map { it?.mode } + + val lastAudiblyAlertedMs: Flow<Long> = content.map { it.lastAudiblyAlertedMs } + val profileBadgeResId: Flow<Int?> = content.map { it.profileBadgeResId } + val title: Flow<CharSequence?> = content.map { it.title } + val text: Flow<CharSequence?> = content.map { it.text } + val skeletonLargeIcon: Flow<Icon?> = content.map { it.skeletonLargeIcon } + + // for CallStyle: + val personIcon: Flow<Icon?> = content.map { it.personIcon } + val personName: Flow<CharSequence?> = content.map { it.personName } + val verificationIcon: Flow<Icon?> = content.map { it.verificationIcon } + val verificationText: Flow<CharSequence?> = content.map { it.verificationText } + + // for ProgressStyle: + val progress: Flow<NotificationProgressModel?> = content.map { it.progress } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index 6e05e8e8b80e..70e27a981b49 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.row; import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP; @@ -25,6 +26,7 @@ import static com.android.systemui.statusbar.notification.row.NotificationConten import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Notification; +import android.app.Notification.MessagingStyle; import android.content.Context; import android.content.ContextWrapper; import android.content.pm.ApplicationInfo; @@ -161,9 +163,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder entry, mConversationProcessor, row, - bindParams.isMinimized, - bindParams.usesIncreasedHeight, - bindParams.usesIncreasedHeadsUpHeight, + bindParams, callback, mRemoteInputManager.getRemoteViewsOnClickHandler(), /* isMediaFlagEnabled = */ mIsMediaInQS, @@ -187,13 +187,13 @@ public class NotificationContentInflater implements NotificationRowContentBinder boolean inflateSynchronously, @InflationFlag int reInflateFlags, Notification.Builder builder, + Context systemUiContext, Context packageContext, SmartReplyStateInflater smartRepliesInflater) { InflationProgress result = createRemoteViews(reInflateFlags, builder, - bindParams.isMinimized, - bindParams.usesIncreasedHeight, - bindParams.usesIncreasedHeadsUpHeight, + bindParams, + systemUiContext, packageContext, row, mNotifLayoutInflaterFactoryProvider, @@ -203,18 +203,20 @@ public class NotificationContentInflater implements NotificationRowContentBinder result = inflateSmartReplyViews(result, reInflateFlags, entry, row.getContext(), packageContext, row.getExistingSmartReplyState(), smartRepliesInflater, mLogger); boolean isConversation = entry.getRanking().isConversation(); + Notification.MessagingStyle messagingStyle = null; + if (isConversation && (AsyncHybridViewInflation.isEnabled() + || LockscreenOtpRedaction.isSingleLineViewEnabled())) { + messagingStyle = mConversationProcessor + .processNotification(entry, builder, mLogger); + } if (AsyncHybridViewInflation.isEnabled()) { - Notification.MessagingStyle messagingStyle = null; - if (isConversation) { - messagingStyle = mConversationProcessor - .processNotification(entry, builder, mLogger); - } SingleLineViewModel viewModel = SingleLineViewInflater .inflateSingleLineViewModel( entry.getSbn().getNotification(), messagingStyle, builder, - row.getContext() + row.getContext(), + false ); // If the messagingStyle is null, we want to inflate the normal view isConversation = viewModel.isConversation(); @@ -228,11 +230,22 @@ public class NotificationContentInflater implements NotificationRowContentBinder mLogger ); } - if (LockscreenOtpRedaction.isSingleLineViewEnabled()) { - result.mPublicInflatedSingleLineViewModel = - SingleLineViewInflater.inflateRedactedSingleLineViewModel(row.getContext(), - isConversation); + if (bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) { + result.mPublicInflatedSingleLineViewModel = + SingleLineViewInflater.inflateSingleLineViewModel( + entry.getSbn().getNotification(), + messagingStyle, + builder, + row.getContext(), + true); + } else { + result.mPublicInflatedSingleLineViewModel = + SingleLineViewInflater.inflateRedactedSingleLineViewModel( + row.getContext(), + isConversation + ); + } result.mPublicInflatedSingleLineView = SingleLineViewInflater.inflatePublicSingleLineView( isConversation, @@ -411,8 +424,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder } private static InflationProgress createRemoteViews(@InflationFlag int reInflateFlags, - Notification.Builder builder, boolean isMinimized, boolean usesIncreasedHeight, - boolean usesIncreasedHeadsUpHeight, Context packageContext, + Notification.Builder builder, BindParams bindParams, Context systemUiContext, + Context packageContext, ExpandableNotificationRow row, NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider, HeadsUpStyleProvider headsUpStyleProvider, @@ -423,13 +436,13 @@ public class NotificationContentInflater implements NotificationRowContentBinder if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { logger.logAsyncTaskProgress(entryForLogging, "creating contracted remote view"); - result.newContentView = createContentView(builder, isMinimized, - usesIncreasedHeight); + result.newContentView = createContentView(builder, bindParams.isMinimized, + bindParams.usesIncreasedHeight); } if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) { logger.logAsyncTaskProgress(entryForLogging, "creating expanded remote view"); - result.newExpandedView = createExpandedView(builder, isMinimized); + result.newExpandedView = createExpandedView(builder, bindParams.isMinimized); } if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { @@ -439,13 +452,20 @@ public class NotificationContentInflater implements NotificationRowContentBinder result.newHeadsUpView = builder.createCompactHeadsUpContentView(); } else { result.newHeadsUpView = builder.createHeadsUpContentView( - usesIncreasedHeadsUpHeight); + bindParams.usesIncreasedHeadsUpHeight); } } if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) { logger.logAsyncTaskProgress(entryForLogging, "creating public remote view"); - result.newPublicView = builder.makePublicContentView(isMinimized); + if (LockscreenOtpRedaction.isEnabled() + && bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) { + result.newPublicView = createSensitiveContentMessageNotification( + row.getEntry().getSbn().getNotification(), builder.getStyle(), + systemUiContext, packageContext).createContentView(true); + } else { + result.newPublicView = builder.makePublicContentView(bindParams.isMinimized); + } } if (AsyncGroupHeaderViewInflation.isEnabled()) { @@ -473,6 +493,42 @@ public class NotificationContentInflater implements NotificationRowContentBinder }); } + private static Notification.Builder createSensitiveContentMessageNotification( + Notification original, + Notification.Style originalStyle, + Context systemUiContext, + Context packageContext) { + Notification.Builder redacted = + new Notification.Builder(packageContext, original.getChannelId()); + redacted.setContentTitle(original.extras.getCharSequence(Notification.EXTRA_TITLE)); + CharSequence redactedMessage = systemUiContext.getString( + R.string.redacted_notification_single_line_text + ); + + if (originalStyle instanceof MessagingStyle oldStyle) { + MessagingStyle newStyle = new MessagingStyle(oldStyle.getUser()); + newStyle.setConversationTitle(oldStyle.getConversationTitle()); + newStyle.setGroupConversation(false); + newStyle.setConversationType(oldStyle.getConversationType()); + newStyle.setShortcutIcon(oldStyle.getShortcutIcon()); + newStyle.setBuilder(redacted); + MessagingStyle.Message latestMessage = + MessagingStyle.findLatestIncomingMessage(oldStyle.getMessages()); + if (latestMessage != null) { + MessagingStyle.Message newMessage = new MessagingStyle.Message(redactedMessage, + latestMessage.getTimestamp(), latestMessage.getSenderPerson()); + newStyle.addMessage(newMessage); + } + redacted.setStyle(newStyle); + } else { + redacted.setContentText(redactedMessage); + } + redacted.setLargeIcon(original.getLargeIcon()); + redacted.setSmallIcon(original.getSmallIcon()); + return redacted; + } + + private static void setNotifsViewsInflaterFactory(InflationProgress result, ExpandableNotificationRow row, NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider) { @@ -921,7 +977,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder logger.logAsyncTaskProgress(entry, "finishing"); if (PromotedNotificationContentModel.featureFlagEnabled()) { - entry.setPromotedNotificationContentModel(result.mExtractedPromotedNotificationContent); + entry.setPromotedNotificationContentModel(result.mPromotedContent); } boolean setRepliesAndActions = true; @@ -1118,10 +1174,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder private final NotificationEntry mEntry; private final Context mContext; private final boolean mInflateSynchronously; - private final boolean mIsMinimized; - private final boolean mUsesIncreasedHeight; + private final BindParams mBindParams; private final InflationCallback mCallback; - private final boolean mUsesIncreasedHeadsUpHeight; private final @InflationFlag int mReInflateFlags; private final NotifRemoteViewCache mRemoteViewCache; private final Executor mInflationExecutor; @@ -1145,9 +1199,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder NotificationEntry entry, ConversationNotificationProcessor conversationProcessor, ExpandableNotificationRow row, - boolean isMinimized, - boolean usesIncreasedHeight, - boolean usesIncreasedHeadsUpHeight, + BindParams bindParams, InflationCallback callback, RemoteViews.InteractionHandler remoteViewClickHandler, boolean isMediaFlagEnabled, @@ -1164,9 +1216,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mRemoteViewCache = cache; mSmartRepliesInflater = smartRepliesInflater; mContext = mRow.getContext(); - mIsMinimized = isMinimized; - mUsesIncreasedHeight = usesIncreasedHeight; - mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight; + mBindParams = bindParams; mRemoteViewClickHandler = remoteViewClickHandler; mCallback = callback; mConversationProcessor = conversationProcessor; @@ -1236,8 +1286,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mEntry, recoveredBuilder, mLogger); } InflationProgress inflationProgress = createRemoteViews(mReInflateFlags, - recoveredBuilder, mIsMinimized, mUsesIncreasedHeight, - mUsesIncreasedHeadsUpHeight, packageContext, mRow, + recoveredBuilder, mBindParams, mContext, packageContext, mRow, mNotifLayoutInflaterFactoryProvider, mHeadsUpStyleProvider, mLogger); mLogger.logAsyncTaskProgress(mEntry, @@ -1264,7 +1313,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder mEntry.getSbn().getNotification(), messagingStyle, recoveredBuilder, - mContext + mContext, + false ); result.mInflatedSingleLineView = SingleLineViewInflater.inflatePrivateSingleLineView( @@ -1277,9 +1327,22 @@ public class NotificationContentInflater implements NotificationRowContentBinder } if (LockscreenOtpRedaction.isSingleLineViewEnabled()) { - result.mPublicInflatedSingleLineViewModel = - SingleLineViewInflater.inflateRedactedSingleLineViewModel(mContext, - isConversation); + if (mBindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) { + result.mPublicInflatedSingleLineViewModel = + SingleLineViewInflater.inflateSingleLineViewModel( + mEntry.getSbn().getNotification(), + messagingStyle, + recoveredBuilder, + mContext, + true + ); + } else { + result.mPublicInflatedSingleLineViewModel = + SingleLineViewInflater.inflateRedactedSingleLineViewModel( + mContext, + isConversation + ); + } result.mPublicInflatedSingleLineView = SingleLineViewInflater.inflatePublicSingleLineView( isConversation, @@ -1292,10 +1355,13 @@ public class NotificationContentInflater implements NotificationRowContentBinder if (PromotedNotificationContentModel.featureFlagEnabled()) { mLogger.logAsyncTaskProgress(mEntry, "extracting promoted notification content"); - result.mExtractedPromotedNotificationContent = mPromotedNotificationContentExtractor - .extractContent(mEntry, recoveredBuilder); + final PromotedNotificationContentModel promotedContent = + mPromotedNotificationContentExtractor.extractContent(mEntry, + recoveredBuilder); mLogger.logAsyncTaskProgress(mEntry, "extracted promoted notification content: " - + result.mExtractedPromotedNotificationContent); + + promotedContent); + + result.mPromotedContent = promotedContent; } mLogger.logAsyncTaskProgress(mEntry, @@ -1317,7 +1383,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mCancellationSignal = apply( mInflationExecutor, mInflateSynchronously, - mIsMinimized, + mBindParams.isMinimized, result, mReInflateFlags, mRemoteViewCache, @@ -1399,7 +1465,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder @VisibleForTesting static class InflationProgress { - PromotedNotificationContentModel mExtractedPromotedNotificationContent; + PromotedNotificationContentModel mPromotedContent; private RemoteViews newContentView; private RemoteViews newHeadsUpView; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java index 07384afe2d2e..1cef8791e0ea 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.row; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType; + import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -141,20 +143,33 @@ public interface NotificationRowContentBinder { */ class BindParams { + public BindParams(boolean minimized, boolean increasedHeight, + boolean increasedHeadsUpHeight, int redaction) { + isMinimized = minimized; + usesIncreasedHeight = increasedHeight; + usesIncreasedHeadsUpHeight = increasedHeadsUpHeight; + redactionType = redaction; + } + /** * Bind a minimized version of the content views. */ - public boolean isMinimized; + public final boolean isMinimized; /** * Use increased height when binding contracted view. */ - public boolean usesIncreasedHeight; + public final boolean usesIncreasedHeight; /** * Use increased height when binding heads up views. */ - public boolean usesIncreasedHeadsUpHeight; + public final boolean usesIncreasedHeadsUpHeight; + + /** + * Controls the type of public view to show, if a public view is requested + */ + public final @RedactionType int redactionType; } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt index c7d80e9d03ce..c619b17f1ad8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt @@ -16,8 +16,8 @@ package com.android.systemui.statusbar.notification.row import android.annotation.SuppressLint -import android.app.Flags import android.app.Notification +import android.app.Notification.MessagingStyle import android.content.Context import android.content.ContextWrapper import android.content.pm.ApplicationInfo @@ -43,6 +43,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.NotifInflation import com.android.systemui.res.R import com.android.systemui.statusbar.InflationTask +import com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_SENSITIVE_CONTENT import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.notification.ConversationNotificationProcessor import com.android.systemui.statusbar.notification.InflationException @@ -143,9 +144,7 @@ constructor( entry, conversationProcessor, row, - bindParams.isMinimized, - bindParams.usesIncreasedHeight, - bindParams.usesIncreasedHeadsUpHeight, + bindParams, callback, remoteInputManager.remoteViewsOnClickHandler, /* isMediaFlagEnabled = */ smartReplyStateInflater, @@ -179,10 +178,8 @@ constructor( reInflateFlags = reInflateFlags, entry = entry, builder = builder, - isMinimized = bindParams.isMinimized, - usesIncreasedHeight = bindParams.usesIncreasedHeight, - usesIncreasedHeadsUpHeight = bindParams.usesIncreasedHeadsUpHeight, - systemUIContext = systemUIContext, + bindParams, + systemUiContext = systemUIContext, packageContext = packageContext, row = row, notifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider, @@ -371,9 +368,7 @@ constructor( private val entry: NotificationEntry, private val conversationProcessor: ConversationNotificationProcessor, private val row: ExpandableNotificationRow, - private val isMinimized: Boolean, - private val usesIncreasedHeight: Boolean, - private val usesIncreasedHeadsUpHeight: Boolean, + private val bindParams: BindParams, private val callback: InflationCallback?, private val remoteViewClickHandler: InteractionHandler?, private val smartRepliesInflater: SmartReplyStateInflater, @@ -441,10 +436,8 @@ constructor( reInflateFlags = reInflateFlags, entry = entry, builder = recoveredBuilder, - isMinimized = isMinimized, - usesIncreasedHeight = usesIncreasedHeight, - usesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight, - systemUIContext = context, + bindParams = bindParams, + systemUiContext = context, packageContext = packageContext, row = row, notifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider, @@ -514,7 +507,7 @@ constructor( apply( inflationExecutor, inflateSynchronously, - isMinimized, + bindParams.isMinimized, progress, reInflateFlags, remoteViewCache, @@ -591,7 +584,7 @@ constructor( @VisibleForTesting val packageContext: Context, val remoteViews: NewRemoteViews, val contentModel: NotificationContentModel, - val extractedPromotedNotificationContentModel: PromotedNotificationContentModel?, + val promotedContent: PromotedNotificationContentModel?, ) { var inflatedContentView: View? = null @@ -671,10 +664,8 @@ constructor( @InflationFlag reInflateFlags: Int, entry: NotificationEntry, builder: Notification.Builder, - isMinimized: Boolean, - usesIncreasedHeight: Boolean, - usesIncreasedHeadsUpHeight: Boolean, - systemUIContext: Context, + bindParams: BindParams, + systemUiContext: Context, packageContext: Context, row: ExpandableNotificationRow, notifLayoutInflaterFactoryProvider: NotifLayoutInflaterFactory.Provider, @@ -683,16 +674,15 @@ constructor( promotedNotificationContentExtractor: PromotedNotificationContentExtractor, logger: NotificationRowContentBinderLogger, ): InflationProgress { - val promoted = + val promotedContent = if (PromotedNotificationContentModel.featureFlagEnabled()) { logger.logAsyncTaskProgress(entry, "extracting promoted notification content") - val extracted = - promotedNotificationContentExtractor.extractContent(entry, builder) - logger.logAsyncTaskProgress( - entry, - "extracted promoted notification content: {extracted}", - ) - extracted + promotedNotificationContentExtractor.extractContent(entry, builder).also { + logger.logAsyncTaskProgress( + entry, + "extracted promoted notification content: $it", + ) + } } else { null } @@ -707,9 +697,10 @@ constructor( createRemoteViews( reInflateFlags = reInflateFlags, builder = builder, - isMinimized = isMinimized, - usesIncreasedHeight = usesIncreasedHeight, - usesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight, + bindParams = bindParams, + entry = entry, + systemUiContext = systemUiContext, + packageContext = packageContext, row = row, notifLayoutInflaterFactoryProvider = notifLayoutInflaterFactoryProvider, headsUpStyleProvider = headsUpStyleProvider, @@ -726,7 +717,8 @@ constructor( notification = entry.sbn.notification, messagingStyle = messagingStyle, builder = builder, - systemUiContext = systemUIContext, + systemUiContext = systemUiContext, + redactText = false, ) } else null @@ -736,10 +728,20 @@ constructor( reInflateFlags and FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE != 0 ) { logger.logAsyncTaskProgress(entry, "inflating public single line view model") - SingleLineViewInflater.inflateRedactedSingleLineViewModel( - systemUIContext, - entry.ranking.isConversation, - ) + if (bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT) { + SingleLineViewInflater.inflateSingleLineViewModel( + notification = entry.sbn.notification, + messagingStyle = messagingStyle, + builder = builder, + systemUiContext = systemUiContext, + redactText = true, + ) + } else { + SingleLineViewInflater.inflateRedactedSingleLineViewModel( + systemUiContext, + entry.ranking.isConversation, + ) + } } else null val headsUpStatusBarModel = @@ -759,16 +761,54 @@ constructor( packageContext = packageContext, remoteViews = remoteViews, contentModel = contentModel, - extractedPromotedNotificationContentModel = promoted, + promotedContent = promotedContent, ) } + private fun createSensitiveContentMessageNotification( + original: Notification, + originalStyle: Notification.Style?, + sysUiContext: Context, + packageContext: Context, + ): Notification.Builder { + val redacted = Notification.Builder(packageContext, original.channelId) + redacted.setContentTitle(original.extras.getCharSequence(Notification.EXTRA_TITLE)) + val redactedMessage = + sysUiContext.getString(R.string.redacted_notification_single_line_text) + + if (originalStyle is MessagingStyle) { + val newStyle = MessagingStyle(originalStyle.user) + newStyle.conversationTitle = originalStyle.conversationTitle + newStyle.isGroupConversation = false + newStyle.conversationType = originalStyle.conversationType + newStyle.shortcutIcon = originalStyle.shortcutIcon + newStyle.setBuilder(redacted) + val latestMessage = MessagingStyle.findLatestIncomingMessage(originalStyle.messages) + if (latestMessage != null) { + val newMessage = + MessagingStyle.Message( + redactedMessage, + latestMessage.timestamp, + latestMessage.senderPerson, + ) + newStyle.addMessage(newMessage) + } + redacted.style = newStyle + } else { + redacted.setContentText(redactedMessage) + } + redacted.setLargeIcon(original.getLargeIcon()) + redacted.setSmallIcon(original.smallIcon) + return redacted + } + private fun createRemoteViews( @InflationFlag reInflateFlags: Int, builder: Notification.Builder, - isMinimized: Boolean, - usesIncreasedHeight: Boolean, - usesIncreasedHeadsUpHeight: Boolean, + bindParams: BindParams, + entry: NotificationEntry, + systemUiContext: Context, + packageContext: Context, row: ExpandableNotificationRow, notifLayoutInflaterFactoryProvider: NotifLayoutInflaterFactory.Provider, headsUpStyleProvider: HeadsUpStyleProvider, @@ -782,7 +822,11 @@ constructor( entryForLogging, "creating contracted remote view", ) - createContentView(builder, isMinimized, usesIncreasedHeight) + createContentView( + builder, + bindParams.isMinimized, + bindParams.usesIncreasedHeight, + ) } else null val expanded = if (reInflateFlags and FLAG_CONTENT_VIEW_EXPANDED != 0) { @@ -790,7 +834,7 @@ constructor( entryForLogging, "creating expanded remote view", ) - createExpandedView(builder, isMinimized) + createExpandedView(builder, bindParams.isMinimized) } else null val headsUp = if (reInflateFlags and FLAG_CONTENT_VIEW_HEADS_UP != 0) { @@ -802,13 +846,26 @@ constructor( if (isHeadsUpCompact) { builder.createCompactHeadsUpContentView() } else { - builder.createHeadsUpContentView(usesIncreasedHeadsUpHeight) + builder.createHeadsUpContentView(bindParams.usesIncreasedHeadsUpHeight) } } else null val public = if (reInflateFlags and FLAG_CONTENT_VIEW_PUBLIC != 0) { logger.logAsyncTaskProgress(entryForLogging, "creating public remote view") - builder.makePublicContentView(isMinimized) + if ( + LockscreenOtpRedaction.isEnabled && + bindParams.redactionType == REDACTION_TYPE_SENSITIVE_CONTENT + ) { + createSensitiveContentMessageNotification( + entry.sbn.notification, + builder.style, + systemUiContext, + packageContext, + ) + .createContentView(bindParams.usesIncreasedHeight) + } else { + builder.makePublicContentView(bindParams.isMinimized) + } } else null val normalGroupHeader = if ( @@ -1420,8 +1477,7 @@ constructor( entry.setContentModel(result.contentModel) if (PromotedNotificationContentModel.featureFlagEnabled()) { - entry.promotedNotificationContentModel = - result.extractedPromotedNotificationContentModel + entry.promotedNotificationContentModel = result.promotedContent } result.inflatedSmartReplyState?.let { row.privateLayout.setInflatedSmartReplyState(it) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java index 427fb66ca2d0..bc44cb0e1074 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification.row; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.REDACTION_TYPE_NONE; +import static com.android.systemui.statusbar.NotificationLockscreenUserManager.RedactionType; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP; @@ -31,6 +33,7 @@ public final class RowContentBindParams { private boolean mUseIncreasedHeadsUpHeight; private boolean mViewsNeedReinflation; private @InflationFlag int mContentViews = DEFAULT_INFLATION_FLAGS; + private @RedactionType int mRedactionType = REDACTION_TYPE_NONE; /** * Content views that are out of date and need to be rebound. @@ -58,6 +61,20 @@ public final class RowContentBindParams { } /** + * @return What type of redaction should be used by the public view (if requested) + */ + public @RedactionType int getRedactionType() { + return mRedactionType; + } + + /** + * Set the redaction type, which controls what sort of public view is shown. + */ + public void setRedactionType(@RedactionType int redactionType) { + mRedactionType = redactionType; + } + + /** * Set whether content should use an increased height version of its contracted view. */ public void setUseIncreasedCollapsedHeight(boolean useIncreasedHeight) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java index 89fcda949b5b..53f74161e7fc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java @@ -72,10 +72,8 @@ public class RowContentBindStage extends BindStage<RowContentBindParams> { // Bind/unbind with parameters mBinder.unbindContent(entry, row, contentToUnbind); - BindParams bindParams = new BindParams(); - bindParams.isMinimized = params.useMinimized(); - bindParams.usesIncreasedHeight = params.useIncreasedHeight(); - bindParams.usesIncreasedHeadsUpHeight = params.useIncreasedHeadsUpHeight(); + BindParams bindParams = new BindParams(params.useMinimized(), params.useIncreasedHeight(), + params.useIncreasedHeadsUpHeight(), params.getRedactionType()); boolean forceInflate = params.needsReinflation(); InflationCallback inflationCallback = new InflationCallback() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt index e702f10d7f50..fe2803bfc5d6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt @@ -51,6 +51,7 @@ internal object SingleLineViewInflater { * notification, not for legacy messaging notifications * @param builder the recovered Notification Builder * @param systemUiContext the context of Android System UI + * @param redactText indicates if the text needs to be redacted * @return the inflated SingleLineViewModel */ @JvmStatic @@ -59,13 +60,21 @@ internal object SingleLineViewInflater { messagingStyle: MessagingStyle?, builder: Notification.Builder, systemUiContext: Context, + redactText: Boolean, ): SingleLineViewModel { if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) { return SingleLineViewModel(null, null, null) } peopleHelper.init(systemUiContext) var titleText = HybridGroupManager.resolveTitle(notification) - var contentText = HybridGroupManager.resolveText(notification) + var contentText = + if (redactText) { + systemUiContext.getString( + com.android.systemui.res.R.string.redacted_notification_single_line_text + ) + } else { + HybridGroupManager.resolveText(notification) + } if (messagingStyle == null) { return SingleLineViewModel( @@ -81,7 +90,7 @@ internal object SingleLineViewInflater { if (conversationTextData?.conversationTitle?.isNotEmpty() == true) { titleText = conversationTextData.conversationTitle } - if (conversationTextData?.conversationText?.isNotEmpty() == true) { + if (!redactText && conversationTextData?.conversationText?.isNotEmpty() == true) { contentText = conversationTextData.conversationText } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 7c9d850eaf07..38a70359e816 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -3729,14 +3729,6 @@ public class NotificationStackScrollLayout // Only when scene container is enabled, mark that we are being dragged so that we start // dispatching the rest of the gesture to scene container. - void startOverscrollAfterExpanding() { - if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; - getExpandHelper().finishExpanding(); - setIsBeingDragged(true); - } - - // Only when scene container is enabled, mark that we are being dragged so that we start - // dispatching the rest of the gesture to scene container. void startDraggingOnHun() { if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; setIsBeingDragged(true); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 245b1d29fb79..a33a9ed2df75 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -2203,11 +2203,10 @@ public class NotificationStackScrollLayoutController implements Dumpable { expandingNotification = mView.isExpandingNotification(); if (mView.getExpandedInThisMotion() && !expandingNotification && wasExpandingBefore && !mView.getDisallowScrollingInThisMotion()) { - // We need to dispatch the overscroll differently when Scene Container is on, - // since NSSL no longer controls its own scroll. + // Finish expansion here, as this gesture will be marked to be sent to + // scene container if (SceneContainerFlag.isEnabled() && !isCancelOrUp) { - mView.startOverscrollAfterExpanding(); - return true; + expandHelper.finishExpanding(); } else { mView.dispatchDownEventToScroller(ev); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt index 42acd7bcdc8a..705845ff984c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt @@ -75,7 +75,7 @@ class SharedNotificationContainer(context: Context, attrs: AttributeSet?) : constraintSet.apply { if (SceneContainerFlag.isEnabled) { when (horizontalPosition) { - is HorizontalPosition.FloatAtEnd -> + is HorizontalPosition.FloatAtStart -> constrainWidth(nsslId, horizontalPosition.width) is HorizontalPosition.MiddleToEdge -> setGuidelinePercent(R.id.nssl_guideline, horizontalPosition.ratio) @@ -83,13 +83,13 @@ class SharedNotificationContainer(context: Context, attrs: AttributeSet?) : } } + connect(nsslId, START, startConstraintId, START, marginStart) if ( !SceneContainerFlag.isEnabled || - horizontalPosition !is HorizontalPosition.FloatAtEnd + horizontalPosition !is HorizontalPosition.FloatAtStart ) { - connect(nsslId, START, startConstraintId, START, marginStart) + connect(nsslId, END, PARENT_ID, END, marginEnd) } - connect(nsslId, END, PARENT_ID, END, marginEnd) connect(nsslId, BOTTOM, PARENT_ID, BOTTOM, marginBottom) connect(nsslId, TOP, PARENT_ID, TOP, marginTop) } 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 b81c71ebe19b..fc8c70fb8e9a 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 @@ -247,7 +247,7 @@ constructor( Split -> HorizontalPosition.MiddleToEdge(ratio = 0.5f) Dual -> if (isShadeLayoutWide) { - HorizontalPosition.FloatAtEnd( + HorizontalPosition.FloatAtStart( width = getDimensionPixelSize(R.dimen.shade_panel_width) ) } else { @@ -830,10 +830,10 @@ constructor( data class MiddleToEdge(val ratio: Float = 0.5f) : HorizontalPosition /** - * The container has a fixed [width] and is aligned to the end of the screen. In this - * layout, the start edge of the container is floating, i.e. unconstrained. + * The container has a fixed [width] and is aligned to the start of the screen. In this + * layout, the end edge of the container is floating, i.e. unconstrained. */ - data class FloatAtEnd(val width: Int) : HorizontalPosition + data class FloatAtStart(val width: Int) : HorizontalPosition } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 7bea4800f7fc..6dc25aa5144f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -21,9 +21,10 @@ import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN; import static android.app.StatusBarManager.WINDOW_STATE_SHOWING; import static android.app.StatusBarManager.WindowVisibleState; import static android.app.StatusBarManager.windowStateToString; +import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO; +import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO; +import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS; -import static androidx.core.view.ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO; -import static androidx.core.view.ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS; import static androidx.lifecycle.Lifecycle.State.RESUMED; import static com.android.systemui.Dependency.TIME_TICK_HANDLER_NAME; @@ -207,6 +208,7 @@ import com.android.systemui.statusbar.data.repository.StatusBarModeRepositorySto import com.android.systemui.statusbar.notification.NotificationActivityStarter; import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider; import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator; +import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.notification.init.NotificationsController; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; @@ -221,7 +223,6 @@ import com.android.systemui.statusbar.policy.ConfigurationController.Configurati import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceProvisionedListener; import com.android.systemui.statusbar.policy.ExtensionController; -import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.UserInfoControllerImpl; import com.android.systemui.statusbar.window.StatusBarWindowControllerStore; @@ -2514,12 +2515,15 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { * should update only the status bar components. */ private void setBouncerShowingForStatusBarComponents(boolean bouncerShowing) { - int importance = bouncerShowing - ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS - : IMPORTANT_FOR_ACCESSIBILITY_AUTO; if (!StatusBarConnectedDisplays.isEnabled() && mPhoneStatusBarViewController != null) { + int importance = bouncerShowing + ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + : IMPORTANT_FOR_ACCESSIBILITY_AUTO; mPhoneStatusBarViewController.setImportantForAccessibility(importance); } + int importance = bouncerShowing + ? IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + : IMPORTANT_FOR_ACCESSIBILITY_NO; mShadeSurface.setImportantForAccessibility(importance); mShadeSurface.setBouncerShowing(bouncerShowing); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java index f37bc6b2d4fb..4d1d64ea24c7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java @@ -37,10 +37,12 @@ import androidx.annotation.NonNull; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.InitController; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.ActivityStarter.OnDismissAction; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.res.R; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.QuickSettingsController; import com.android.systemui.shade.ShadeViewController; @@ -59,6 +61,7 @@ import com.android.systemui.statusbar.notification.DynamicPrivacyController; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource; import com.android.systemui.statusbar.notification.domain.interactor.NotificationAlertsInteractor; +import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptSuppressor; import com.android.systemui.statusbar.notification.interruption.VisualInterruptionCondition; import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider; @@ -69,7 +72,6 @@ import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.row.NotificationGutsManager.OnSettingsClickListener; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; -import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; import com.android.systemui.statusbar.policy.KeyguardStateController; import java.util.Set; @@ -102,6 +104,7 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu private final IStatusBarService mBarService; private final DynamicPrivacyController mDynamicPrivacyController; private final NotificationListContainer mNotifListContainer; + private final DeviceUnlockedInteractor mDeviceUnlockedInteractor; private final QuickSettingsController mQsController; protected boolean mVrMode; @@ -133,7 +136,8 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu VisualInterruptionDecisionProvider visualInterruptionDecisionProvider, NotificationRemoteInputManager remoteInputManager, NotificationRemoteInputManager.Callback remoteInputManagerCallback, - NotificationListContainer notificationListContainer) { + NotificationListContainer notificationListContainer, + DeviceUnlockedInteractor deviceUnlockedInteractor) { mActivityStarter = activityStarter; mKeyguardStateController = keyguardStateController; mNotificationPanel = panel; @@ -160,6 +164,7 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu mBarService = IStatusBarService.Stub.asInterface( ServiceManager.getService(Context.STATUS_BAR_SERVICE)); mNotifListContainer = notificationListContainer; + mDeviceUnlockedInteractor = deviceUnlockedInteractor; IVrManager vrManager = IVrManager.Stub.asInterface(ServiceManager.getService( Context.VR_SERVICE)); @@ -246,16 +251,27 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu mPowerInteractor.wakeUpIfDozing("NOTIFICATION_CLICK", PowerManager.WAKE_REASON_GESTURE); if (nowExpanded) { if (mStatusBarStateController.getState() == StatusBarState.KEYGUARD) { - mShadeTransitionController.goToLockedShade(clickedEntry.getRow()); - } else if (clickedEntry.isSensitive().getValue() - && mDynamicPrivacyController.isInLockedDownShade()) { + mShadeTransitionController.goToLockedShade( + clickedEntry.getRow(), /* needsQSAnimation = */ true); + } else if (clickedEntry.isSensitive().getValue() && isInLockedDownShade()) { mStatusBarStateController.setLeaveOpenOnKeyguardHide(true); + // launch the bouncer if the device is locked mActivityStarter.dismissKeyguardThenExecute(() -> false /* dismissAction */ , null /* cancelRunnable */, false /* afterKeyguardGone */); } } } + /** @return true if the Shade is shown over the Lockscreen, and the device is locked */ + private boolean isInLockedDownShade() { + if (SceneContainerFlag.isEnabled()) { + return mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED + && !mDeviceUnlockedInteractor.getDeviceUnlockStatus().getValue().isUnlocked(); + } else { + return mDynamicPrivacyController.isInLockedDownShade(); + } + } + @Override public boolean isDeviceInVrMode() { return mVrMode; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java index 78954dea27ba..8b60ee56d5f8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java @@ -192,7 +192,7 @@ public class KeyguardQsUserSwitchController extends ViewController<FrameLayout> mUiEventLogger.log( LockscreenGestureLogger.LockscreenUiEvent.LOCKSCREEN_SWITCH_USER_TAP); - mUserSwitchDialogController.showDialog(mUserAvatarViewWithBackground.getContext(), + mUserSwitchDialogController.showDialog( Expandable.fromView(mUserAvatarViewWithBackground)); }); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt index a1d5cbea62f9..9ff0d18f0e2b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegate.kt @@ -44,6 +44,7 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dialog.ui.composable.AlertDialogContent import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor import com.android.systemui.statusbar.phone.ComponentSystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialogFactory @@ -67,6 +68,7 @@ constructor( private val viewModel: Provider<ModesDialogViewModel>, private val dialogEventLogger: ModesDialogEventLogger, @Main private val mainCoroutineContext: CoroutineContext, + private val shadeDisplayContextRepository: ShadeDialogContextInteractor, ) : SystemUIDialog.Delegate { // NOTE: This should only be accessed/written from the main thread. @VisibleForTesting var currentDialog: ComponentSystemUIDialog? = null @@ -78,7 +80,10 @@ constructor( currentDialog?.dismiss() } - currentDialog = sysuiDialogFactory.create { ModesDialogContent(it) } + currentDialog = + sysuiDialogFactory.create(context = shadeDisplayContextRepository.context) { + ModesDialogContent(it) + } currentDialog ?.lifecycle ?.addObserver( @@ -106,9 +111,8 @@ constructor( modifier = Modifier.semantics { testTagsAsResourceId = true - paneTitle = dialog.context.getString( - R.string.accessibility_desc_quick_settings - ) + paneTitle = + dialog.context.getString(R.string.accessibility_desc_quick_settings) }, title = { Text( diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt index a382cf921152..e08114f6c3cd 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt @@ -21,10 +21,8 @@ import android.content.Context import android.hardware.devicestate.DeviceStateManager import android.os.PowerManager import android.provider.Settings -import androidx.core.view.OneShotPreDrawListener import com.android.internal.util.LatencyTracker import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.ToAodFoldTransitionInteractor @@ -125,11 +123,7 @@ constructor( private val shadeFoldAnimator: ShadeFoldAnimator get() { - return if (MigrateClocksToBlueprint.isEnabled) { - foldTransitionInteractor.get().foldAnimator - } else { - shadeViewController.shadeFoldAnimator - } + return foldTransitionInteractor.get().foldAnimator } private fun setAnimationState(playing: Boolean) { @@ -164,15 +158,7 @@ constructor( setAnimationState(playing = true) shadeFoldAnimator.prepareFoldToAodAnimation() - // We don't need to wait for the scrim as it is already displayed - // but we should wait for the initial animation preparations to be drawn - // (setting initial alpha/translation) - // TODO(b/254878364): remove this call to NPVC.getView() - if (!MigrateClocksToBlueprint.isEnabled) { - shadeFoldAnimator.view?.let { OneShotPreDrawListener.add(it, onReady) } - } else { - onReady.run() - } + onReady.run() } else { // No animation, call ready callback immediately onReady.run() @@ -252,7 +238,7 @@ constructor( if (isFolded) { foldToAodLatencyTracker.onFolded() } - } + }, ) /** @@ -272,6 +258,7 @@ constructor( latencyTracker.onActionStart(LatencyTracker.ACTION_FOLD_TO_AOD) } } + /** * Called once the Fold -> AOD animation is started. * diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt index 102fcc0c59f2..e4b2dc25e411 100644 --- a/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/user/ui/dialog/UserSwitcherDialogCoordinator.kt @@ -18,7 +18,6 @@ package com.android.systemui.user.ui.dialog import android.app.Dialog -import android.content.Context import com.android.internal.jank.InteractionJankMonitor import com.android.internal.logging.UiEventLogger import com.android.settingslib.users.UserCreatingDialog @@ -32,6 +31,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.tiles.UserDetailView +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor import com.android.systemui.user.UserSwitchFullscreenDialog import com.android.systemui.user.domain.interactor.UserSwitcherInteractor import com.android.systemui.user.domain.model.ShowDialogRequestModel @@ -48,7 +48,6 @@ import com.android.app.tracing.coroutines.launchTraced as launch class UserSwitcherDialogCoordinator @Inject constructor( - @Application private val context: Lazy<Context>, @Application private val applicationScope: Lazy<CoroutineScope>, private val falsingManager: Lazy<FalsingManager>, private val broadcastSender: Lazy<BroadcastSender>, @@ -59,6 +58,7 @@ constructor( private val activityStarter: Lazy<ActivityStarter>, private val falsingCollector: Lazy<FalsingCollector>, private val userSwitcherViewModel: Lazy<UserSwitcherViewModel>, + private val shadeDialogContextInteractor: Lazy<ShadeDialogContextInteractor>, ) : CoreStartable { private var currentDialog: Dialog? = null @@ -71,12 +71,13 @@ constructor( private fun startHandlingDialogShowRequests() { applicationScope.get().launch { interactor.get().dialogShowRequests.filterNotNull().collect { request -> + val context = shadeDialogContextInteractor.get().context val (dialog, dialogCuj) = when (request) { is ShowDialogRequestModel.ShowAddUserDialog -> Pair( AddUserDialog( - context = context.get(), + context = context, userHandle = request.userHandle, isKeyguardShowing = request.isKeyguardShowing, showEphemeralMessage = request.showEphemeralMessage, @@ -92,7 +93,7 @@ constructor( is ShowDialogRequestModel.ShowUserCreationDialog -> Pair( UserCreatingDialog( - context.get(), + context, request.isGuest, ), null, @@ -100,7 +101,7 @@ constructor( is ShowDialogRequestModel.ShowExitGuestDialog -> Pair( ExitGuestDialog( - context = context.get(), + context = context, guestUserId = request.guestUserId, isGuestEphemeral = request.isGuestEphemeral, targetUserId = request.targetUserId, @@ -117,7 +118,7 @@ constructor( is ShowDialogRequestModel.ShowUserSwitcherDialog -> Pair( UserSwitchDialog( - context = context.get(), + context = context, adapter = userDetailAdapterProvider.get(), uiEventLogger = eventLogger.get(), falsingManager = falsingManager.get(), @@ -132,7 +133,7 @@ constructor( is ShowDialogRequestModel.ShowUserSwitcherFullscreenDialog -> Pair( UserSwitchFullscreenDialog( - context = context.get(), + context = context, falsingCollector = falsingCollector.get(), userSwitcherViewModel = userSwitcherViewModel.get(), ), diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt index 2e1f82d56fc4..70e342f3eefb 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt @@ -17,45 +17,28 @@ package com.android.systemui.volume.dialog.settings.ui.binder import android.view.View -import com.android.systemui.lifecycle.WindowLifecycleState -import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.lifecycle.setSnapshotBinding -import com.android.systemui.lifecycle.viewModel +import android.widget.ImageButton import com.android.systemui.res.R import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope import com.android.systemui.volume.dialog.settings.ui.viewmodel.VolumeDialogSettingsButtonViewModel import javax.inject.Inject -import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @VolumeDialogScope class VolumeDialogSettingsButtonViewBinder @Inject -constructor(private val viewModelFactory: VolumeDialogSettingsButtonViewModel.Factory) { +constructor(private val viewModel: VolumeDialogSettingsButtonViewModel) { - fun bind(view: View) { - with(view) { - val button = requireViewById<View>(R.id.volume_dialog_settings) - repeatWhenAttached { - viewModel( - traceName = "VolumeDialogViewBinder", - minWindowLifecycleState = WindowLifecycleState.ATTACHED, - factory = { viewModelFactory.create() }, - ) { viewModel -> - setSnapshotBinding { - viewModel.isVisible - .onEach { isVisible -> - visibility = if (isVisible) View.VISIBLE else View.GONE - } - .launchIn(this) + fun CoroutineScope.bind(view: View) { + val button = view.requireViewById<ImageButton>(R.id.volume_dialog_settings) + viewModel.isVisible + .onEach { isVisible -> button.visibility = if (isVisible) View.VISIBLE else View.GONE } + .launchIn(this) - button.setOnClickListener { viewModel.onButtonClicked() } - } + viewModel.icon.onEach { button.setImageDrawable(it) }.launchIn(this) - awaitCancellation() - } - } - } + button.setOnClickListener { viewModel.onButtonClicked() } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModel.kt index 015d773b2c02..03442dbcde66 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/viewmodel/VolumeDialogSettingsButtonViewModel.kt @@ -14,27 +14,206 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.volume.dialog.settings.ui.viewmodel -import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.ColorFilter +import android.graphics.drawable.Drawable +import android.media.session.PlaybackState +import androidx.annotation.ColorInt +import com.airbnb.lottie.LottieComposition +import com.airbnb.lottie.LottieCompositionFactory +import com.airbnb.lottie.LottieDrawable +import com.airbnb.lottie.LottieProperty +import com.airbnb.lottie.SimpleColorFilter +import com.airbnb.lottie.model.KeyPath +import com.airbnb.lottie.value.LottieValueCallback +import com.android.internal.R as internalR +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.UiBackground +import com.android.systemui.lottie.await +import com.android.systemui.res.R +import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog import com.android.systemui.volume.dialog.settings.domain.VolumeDialogSettingsButtonInteractor -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor +import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor +import com.android.systemui.volume.panel.shared.model.filterData +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.resume +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.runningFold +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.transform +import kotlinx.coroutines.suspendCancellableCoroutine class VolumeDialogSettingsButtonViewModel -@AssistedInject -constructor(private val interactor: VolumeDialogSettingsButtonInteractor) { +@Inject +constructor( + @Application private val context: Context, + @UiBackground private val uiBgCoroutineContext: CoroutineContext, + @VolumeDialog private val coroutineScope: CoroutineScope, + mediaOutputInteractor: MediaOutputInteractor, + private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor, + private val interactor: VolumeDialogSettingsButtonInteractor, +) { + + @SuppressLint("UseCompatLoadingForDrawables") + private val drawables: Flow<Drawables> = + flow { + val color = context.getColor(internalR.color.materialColorPrimary) + emit( + Drawables( + start = + LottieCompositionFactory.fromRawRes(context, R.raw.audio_bars_in) + .await() + .toDrawable { setColor(color) }, + playing = + LottieCompositionFactory.fromRawRes(context, R.raw.audio_bars_playing) + .await() + .toDrawable { + repeatCount = LottieDrawable.INFINITE + repeatMode = LottieDrawable.RESTART + setColor(color) + }, + stop = + LottieCompositionFactory.fromRawRes(context, R.raw.audio_bars_out) + .await() + .toDrawable { setColor(color) }, + idle = context.getDrawable(R.drawable.audio_bars_idle)!!, + ) + ) + } + .buffer() + .flowOn(uiBgCoroutineContext) + .stateIn(coroutineScope, SharingStarted.Eagerly, null) + .filterNotNull() val isVisible = interactor.isVisible + val icon: Flow<Drawable> = + mediaOutputInteractor.defaultActiveMediaSession + .filterData() + .flatMapLatest { session -> + if (session == null) { + flowOf(null) + } else { + mediaDeviceSessionInteractor.playbackState(session) + } + } + .runningFold(null) { playbackStates: PlaybackStates?, playbackState: PlaybackState? -> + val isCurrentActive = playbackState?.isActive ?: false + if (playbackStates != null && isCurrentActive == playbackState?.isActive) { + return@runningFold playbackStates + } + playbackStates?.copy( + isPreviousActive = playbackStates.isCurrentActive, + isCurrentActive = isCurrentActive, + ) ?: PlaybackStates(isPreviousActive = null, isCurrentActive = isCurrentActive) + } + .filterNotNull() + // only apply the most recent state if we wait for the animation. + .buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + // distinct again because the changed state might've been dropped by the buffer + .distinctUntilChangedBy { it.isCurrentActive } + .transform { emitDrawables(it) } + .runningFold(null) { previous: Drawable?, current: Drawable -> + // wait for the previous animation to finish before starting the new one + // this also waits for the current loop of the playing animation to finish + (previous as? LottieDrawable)?.awaitFinish() + (current as? LottieDrawable)?.start() + current + } + .filterNotNull() + + private suspend fun FlowCollector<Drawable>.emitDrawables(playbackStates: PlaybackStates) { + val animations = drawables.first() + val stateChanged = + playbackStates.isPreviousActive != null && + playbackStates.isPreviousActive != playbackStates.isCurrentActive + if (playbackStates.isCurrentActive) { + if (stateChanged) { + emit(animations.start) + } + emit(animations.playing) + } else { + if (stateChanged) { + emit(animations.stop) + } + emit(animations.idle) + } + } fun onButtonClicked() { interactor.onButtonClicked() } - @VolumeDialogScope - @AssistedFactory - interface Factory { + private data class PlaybackStates(val isPreviousActive: Boolean?, val isCurrentActive: Boolean) + + private data class Drawables( + val start: LottieDrawable, + val playing: LottieDrawable, + val stop: LottieDrawable, + val idle: Drawable, + ) +} + +private fun LottieComposition.toDrawable(setup: LottieDrawable.() -> Unit = {}): LottieDrawable = + LottieDrawable().also { drawable -> + drawable.composition = this + drawable.setup() + } - fun create(): VolumeDialogSettingsButtonViewModel +/** Suspends until current loop of the repeating animation is finished */ +private suspend fun LottieDrawable.awaitFinish() = suspendCancellableCoroutine { continuation -> + if (!isRunning) { + continuation.resume(Unit) + return@suspendCancellableCoroutine } + val listener = + object : AnimatorListenerAdapter() { + override fun onAnimationRepeat(animation: Animator) { + continuation.resume(Unit) + removeAnimatorListener(this) + } + + override fun onAnimationEnd(animation: Animator) { + continuation.resume(Unit) + removeAnimatorListener(this) + } + + override fun onAnimationCancel(animation: Animator) { + continuation.resume(Unit) + removeAnimatorListener(this) + } + } + addAnimatorListener(listener) + continuation.invokeOnCancellation { removeAnimatorListener(listener) } +} + +/** + * Overrides colors of the [LottieDrawable] to a specified [color] + * + * @see com.airbnb.lottie.LottieAnimationView + */ +private fun LottieDrawable.setColor(@ColorInt color: Int) { + val callback = LottieValueCallback<ColorFilter>(SimpleColorFilter(color)) + addValueCallback(KeyPath("**"), LottieProperty.COLOR_FILTER, callback) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt index f30524638150..faf06b942cab 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt @@ -26,7 +26,7 @@ import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderStateModel import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel import com.android.systemui.volume.dialog.ui.utils.JankListenerFactory -import com.android.systemui.volume.dialog.ui.utils.awaitAnimation +import com.android.systemui.volume.dialog.ui.utils.suspendAnimate import com.google.android.material.slider.LabelFormatter import com.google.android.material.slider.Slider import javax.inject.Inject @@ -84,5 +84,5 @@ private suspend fun Slider.setValueAnimated( interpolator = DecelerateInterpolator() addListener(jankListener) } - .awaitAnimation<Float> { value = it } + .suspendAnimate<Float> { value = it } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt index 10cf615ce0ce..5f124806dac7 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt @@ -22,6 +22,7 @@ import android.animation.ValueAnimator import android.view.ViewPropertyAnimator import androidx.dynamicanimation.animation.DynamicAnimation import androidx.dynamicanimation.animation.SpringAnimation +import com.airbnb.lottie.LottieDrawable import kotlin.coroutines.resume import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.suspendCancellableCoroutine @@ -66,7 +67,7 @@ suspend fun ViewPropertyAnimator.suspendAnimate( * is cancelled. */ @Suppress("UNCHECKED_CAST") -suspend fun <T> ValueAnimator.awaitAnimation(onValueChanged: (T) -> Unit) { +suspend fun <T> ValueAnimator.suspendAnimate(onValueChanged: (T) -> Unit) { suspendCancellableCoroutine { continuation -> addListener( object : AnimatorListenerAdapter() { @@ -103,6 +104,29 @@ suspend fun SpringAnimation.suspendAnimate( } } +/** + * Starts the animation and suspends until it's finished. Cancels the animation if the running + * coroutine is cancelled. + */ +suspend fun LottieDrawable.suspendAnimate() = suspendCancellableCoroutine { continuation -> + val listener = + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + continuation.resumeIfCan(Unit) + } + + override fun onAnimationCancel(animation: Animator) { + continuation.resumeIfCan(Unit) + } + } + addAnimatorListener(listener) + start() + continuation.invokeOnCancellation { + removeAnimatorListener(listener) + stop() + } +} + private fun <T> CancellableContinuation<T>.resumeIfCan(value: T) { if (!isCancelled && !isCompleted) { resume(value) diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt index 6e1ebc820b08..12e624cae4d4 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt @@ -19,10 +19,10 @@ package com.android.systemui.volume.panel.component.mediaoutput.domain.interacto import android.media.session.MediaController import android.media.session.PlaybackState import com.android.settingslib.volume.data.repository.MediaControllerRepository +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaControllerChangeModel import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession -import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -38,7 +38,7 @@ import kotlinx.coroutines.withContext /** Allows to observe and change [MediaDeviceSession] state. */ @OptIn(ExperimentalCoroutinesApi::class) -@VolumePanelScope +@SysUISingleton class MediaDeviceSessionInteractor @Inject constructor( diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt index b3848a6d7817..2973e11c365d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt @@ -24,12 +24,13 @@ import androidx.annotation.WorkerThread import com.android.settingslib.media.MediaDevice import com.android.settingslib.volume.data.repository.LocalMediaRepository import com.android.settingslib.volume.data.repository.MediaControllerRepository +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.util.concurrency.Execution import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSessions import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession -import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import com.android.systemui.volume.panel.shared.model.Result import com.android.systemui.volume.panel.shared.model.filterData import com.android.systemui.volume.panel.shared.model.wrapInResult @@ -54,13 +55,13 @@ import kotlinx.coroutines.withContext /** Provides observable models about the current media session state. */ @OptIn(ExperimentalCoroutinesApi::class) -@VolumePanelScope +@SysUISingleton class MediaOutputInteractor @Inject constructor( private val localMediaRepositoryFactory: LocalMediaRepositoryFactory, private val packageManager: PackageManager, - @VolumePanelScope private val coroutineScope: CoroutineScope, + @Application private val coroutineScope: CoroutineScope, @Background private val backgroundCoroutineContext: CoroutineContext, mediaControllerRepository: MediaControllerRepository, private val mediaControllerInteractor: MediaControllerInteractor, @@ -77,7 +78,7 @@ constructor( .onStart { emit(activeSessions) } } .map { getMediaControllers(it) } - .stateIn(coroutineScope, SharingStarted.Eagerly, MediaControllers(null, null)) + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), MediaControllers(null, null)) /** [MediaDeviceSessions] that contains currently active sessions. */ val activeMediaDeviceSessions: Flow<MediaDeviceSessions> = @@ -89,7 +90,11 @@ constructor( ) } .flowOn(backgroundCoroutineContext) - .stateIn(coroutineScope, SharingStarted.Eagerly, MediaDeviceSessions(null, null)) + .stateIn( + coroutineScope, + SharingStarted.WhileSubscribed(), + MediaDeviceSessions(null, null), + ) /** Returns the default [MediaDeviceSession] from [activeMediaDeviceSessions] */ val defaultActiveMediaSession: StateFlow<Result<MediaDeviceSession?>> = @@ -104,7 +109,7 @@ constructor( } .wrapInResult() .flowOn(backgroundCoroutineContext) - .stateIn(coroutineScope, SharingStarted.Eagerly, Result.Loading()) + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), Result.Loading()) private val localMediaRepository: Flow<LocalMediaRepository> = defaultActiveMediaSession diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt index a41725f754df..4abbbacd800b 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt @@ -25,7 +25,6 @@ import android.widget.FrameLayout import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND_INACTIVE -import com.android.systemui.Flags as AConfigFlags import com.android.systemui.SysuiTestCase import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.flags.Flags @@ -299,20 +298,6 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - @DisableFlags(AConfigFlags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - fun keyguardCallback_visibilityChanged_clockDozeCalled() = - runBlocking(IMMEDIATE) { - val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() - verify(keyguardUpdateMonitor).registerCallback(capture(captor)) - - captor.value.onKeyguardVisibilityChanged(true) - verify(animations, never()).doze(0f) - - captor.value.onKeyguardVisibilityChanged(false) - verify(animations, times(2)).doze(0f) - } - - @Test fun keyguardCallback_timeFormat_clockNotified() = runBlocking(IMMEDIATE) { val captor = argumentCaptor<KeyguardUpdateMonitorCallback>() @@ -344,19 +329,6 @@ class ClockEventControllerTest : SysuiTestCase() { } @Test - fun keyguardCallback_verifyKeyguardChanged() = - runBlocking(IMMEDIATE) { - val job = underTest.listenForDozeAmount(this) - repository.setDozeAmount(0.4f) - - yield() - - verify(animations, times(2)).doze(0.4f) - - job.cancel() - } - - @Test fun listenForDozeAmountTransition_updatesClockDozeAmount() = runBlocking(IMMEDIATE) { val transitionStep = MutableStateFlow(TransitionStep()) diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt index fb0fd23fab24..6bfd08025833 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt @@ -34,8 +34,10 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.model.SysUiState import com.android.systemui.res.R +import com.android.systemui.shade.data.repository.shadeDialogContextInteractor import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialogManager +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock @@ -82,7 +84,7 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { private val uiProperties = BluetoothTileDialogViewModel.UiProperties.build( isBluetoothEnabled = ENABLED, - isAutoOnToggleFeatureAvailable = ENABLED + isAutoOnToggleFeatureAvailable = ENABLED, ) @Mock private lateinit var sysuiDialogFactory: SystemUIDialog.Factory @Mock private lateinit var dialogManager: SystemUIDialogManager @@ -98,6 +100,8 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { private lateinit var mBluetoothTileDialogDelegate: BluetoothTileDialogDelegate private lateinit var deviceItem: DeviceItem + private val kosmos = testKosmos() + @Before fun setUp() { scheduler = TestCoroutineScheduler() @@ -116,10 +120,16 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { fakeSystemClock, uiEventLogger, logger, - sysuiDialogFactory + sysuiDialogFactory, + kosmos.shadeDialogContextInteractor, ) - whenever(sysuiDialogFactory.create(any(SystemUIDialog.Delegate::class.java))).thenAnswer { + whenever( + sysuiDialogFactory.create( + any(SystemUIDialog.Delegate::class.java), + any() + ) + ).thenAnswer { SystemUIDialog( mContext, 0, @@ -128,7 +138,7 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { sysuiState, fakeBroadcastDispatcher, dialogTransitionAnimator, - it.getArgument(0) + it.getArgument(0), ) } @@ -140,7 +150,7 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { deviceName = DEVICE_NAME, connectionSummary = DEVICE_CONNECTION_SUMMARY, iconWithDescription = icon, - background = null + background = null, ) `when`(cachedBluetoothDevice.isBusy).thenReturn(false) } @@ -169,7 +179,7 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { dialog, listOf(deviceItem), showSeeAll = false, - showPairNewDevice = false + showPairNewDevice = false, ) val recyclerView = dialog.requireViewById<RecyclerView>(R.id.device_list) @@ -217,6 +227,7 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { uiEventLogger, logger, sysuiDialogFactory, + kosmos.shadeDialogContextInteractor, ) .Adapter(bluetoothTileDialogCallback) .DeviceItemViewHolder(view) @@ -238,7 +249,7 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { dialog, listOf(deviceItem), showSeeAll = false, - showPairNewDevice = true + showPairNewDevice = true, ) val seeAllButton = dialog.requireViewById<View>(R.id.see_all_button) @@ -272,6 +283,7 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { uiEventLogger, logger, sysuiDialogFactory, + kosmos.shadeDialogContextInteractor, ) .createDialog() dialog.show() @@ -295,6 +307,7 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { uiEventLogger, logger, sysuiDialogFactory, + kosmos.shadeDialogContextInteractor, ) .createDialog() dialog.show() @@ -318,6 +331,7 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { uiEventLogger, logger, sysuiDialogFactory, + kosmos.shadeDialogContextInteractor, ) .createDialog() dialog.show() @@ -339,7 +353,7 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { dialog, visibility = VISIBLE, label = null, - isActive = true + isActive = true, ) val audioSharingButton = dialog.requireViewById<View>(R.id.audio_sharing_button) @@ -361,7 +375,7 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { dialog, visibility = VISIBLE, label = null, - isActive = false + isActive = false, ) val audioSharingButton = dialog.requireViewById<View>(R.id.audio_sharing_button) @@ -383,7 +397,7 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { dialog, visibility = GONE, label = null, - isActive = false + isActive = false, ) val audioSharingButton = dialog.requireViewById<View>(R.id.audio_sharing_button) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java index c2c94a88603a..1cabf202463e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java @@ -77,6 +77,8 @@ public class CommunalTouchHandlerTest extends SysuiTestCase { INITIATION_WIDTH, mKosmos.getCommunalInteractor(), mKosmos.getConfigurationInteractor(), + mKosmos.getSceneInteractor(), + Optional.of(mKosmos.getMockWindowRootViewProvider()), mLifecycle ); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt index 32fa160fc29f..21dd5bc068f5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt @@ -344,7 +344,7 @@ class KeyguardQuickAffordanceInteractorParameterizedTest : SysuiTestCase() { canShowWhileLocked = canShowWhileLocked, ) } else { - KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } underTest.onQuickAffordanceTriggered( diff --git a/packages/SystemUI/tests/src/com/android/systemui/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt index 1184a76d54ff..1ce128c2403a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt @@ -81,7 +81,7 @@ import platform.test.runner.parameterized.Parameters @OptIn(ExperimentalCoroutinesApi::class) @FlakyTest( bugId = 292574995, - detail = "on certain architectures all permutations with startActivity=true is causing failures" + detail = "on certain architectures all permutations with startActivity=true is causing failures", ) @SmallTest @RunWith(ParameterizedAndroidJunit4::class) @@ -93,11 +93,7 @@ class KeyguardQuickAffordanceInteractorSceneContainerTest : SysuiTestCase() { private val DRAWABLE = mock<Icon> { whenever(this.contentDescription) - .thenReturn( - ContentDescription.Resource( - res = CONTENT_DESCRIPTION_RESOURCE_ID, - ) - ) + .thenReturn(ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID)) } private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337 @@ -273,13 +269,7 @@ class KeyguardQuickAffordanceInteractorSceneContainerTest : SysuiTestCase() { context = context, userFileManager = mock<UserFileManager>().apply { - whenever( - getSharedPreferences( - anyString(), - anyInt(), - anyInt(), - ) - ) + whenever(getSharedPreferences(anyString(), anyInt(), anyInt())) .thenReturn(FakeSharedPreferences()) }, userTracker = userTracker, @@ -316,9 +306,7 @@ class KeyguardQuickAffordanceInteractorSceneContainerTest : SysuiTestCase() { underTest = KeyguardQuickAffordanceInteractor( keyguardInteractor = - KeyguardInteractorFactory.create( - featureFlags = featureFlags, - ) + KeyguardInteractorFactory.create(featureFlags = featureFlags) .keyguardInteractor, shadeInteractor = kosmos.shadeInteractor, lockPatternUtils = lockPatternUtils, @@ -350,9 +338,7 @@ class KeyguardQuickAffordanceInteractorSceneContainerTest : SysuiTestCase() { homeControls.setState( lockScreenState = - KeyguardQuickAffordanceConfig.LockScreenState.Visible( - icon = DRAWABLE, - ) + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = DRAWABLE) ) homeControls.onTriggeredResult = if (startActivity) { @@ -361,7 +347,7 @@ class KeyguardQuickAffordanceInteractorSceneContainerTest : SysuiTestCase() { canShowWhileLocked = canShowWhileLocked, ) } else { - KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled + KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled(false) } underTest.onQuickAffordanceTriggered( diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java index b26f0a6e71a3..782b24825bcf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateControllerTest.java @@ -557,6 +557,13 @@ public class InternetDialogDelegateControllerTest extends SysuiTestCase { } @Test + public void startActivityForDialog_always_startActivityWithoutDismissShade() { + mInternetDialogController.startActivityForDialog(mock(Intent.class)); + + verify(mActivityStarter).startActivity(any(Intent.class), eq(false) /* dismissShade */); + } + + @Test public void launchWifiDetailsSetting_withNoWifiEntryKey_doNothing() { mInternetDialogController.launchWifiDetailsSetting(null /* key */, mDialogLaunchView); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateTest.java index 300c9b843d1c..8560b67dee33 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegateTest.java @@ -35,6 +35,7 @@ import com.android.settingslib.wifi.WifiEnterpriseRestrictionUtils; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.DialogTransitionAnimator; import com.android.systemui.res.R; +import com.android.systemui.shade.domain.interactor.FakeShadeDialogContextInteractor; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.util.concurrency.FakeExecutor; @@ -149,7 +150,8 @@ public class InternetDialogDelegateTest extends SysuiTestCase { mHandler, mBgExecutor, mKeyguard, - mSystemUIDialogFactory); + mSystemUIDialogFactory, + new FakeShadeDialogContextInteractor(mContext)); mInternetDialogDelegate.createDialog(); mInternetDialogDelegate.onCreate(mSystemUIDialog, null); mInternetDialogDelegate.mAdapter = mInternetAdapter; diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt index a0ecb802dd61..f695c13a9e62 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/settings/UserTrackerImplTest.kt @@ -76,6 +76,8 @@ class UserTrackerImplTest : SysuiTestCase() { @Mock private lateinit var iActivityManager: IActivityManager + @Mock private lateinit var beforeUserSwitchingReply: IRemoteCallback + @Mock private lateinit var userSwitchingReply: IRemoteCallback @Mock(stubOnly = true) private lateinit var dumpManager: DumpManager @@ -199,9 +201,10 @@ class UserTrackerImplTest : SysuiTestCase() { val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) - captor.value.onBeforeUserSwitching(newID) + captor.value.onBeforeUserSwitching(newID, beforeUserSwitchingReply) captor.value.onUserSwitching(newID, userSwitchingReply) runCurrent() + verify(beforeUserSwitchingReply).sendResult(any()) verify(userSwitchingReply).sendResult(any()) verify(userManager).getProfiles(newID) @@ -341,10 +344,11 @@ class UserTrackerImplTest : SysuiTestCase() { val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) - captor.value.onBeforeUserSwitching(newID) + captor.value.onBeforeUserSwitching(newID, beforeUserSwitchingReply) captor.value.onUserSwitching(newID, userSwitchingReply) runCurrent() + verify(beforeUserSwitchingReply).sendResult(any()) verify(userSwitchingReply).sendResult(any()) assertThat(callback.calledOnUserChanging).isEqualTo(1) assertThat(callback.lastUser).isEqualTo(newID) @@ -395,7 +399,7 @@ class UserTrackerImplTest : SysuiTestCase() { val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) - captor.value.onBeforeUserSwitching(newID) + captor.value.onBeforeUserSwitching(newID, any()) captor.value.onUserSwitchComplete(newID) runCurrent() @@ -453,8 +457,10 @@ class UserTrackerImplTest : SysuiTestCase() { val captor = ArgumentCaptor.forClass(IUserSwitchObserver::class.java) verify(iActivityManager).registerUserSwitchObserver(capture(captor), anyString()) + captor.value.onBeforeUserSwitching(newID, beforeUserSwitchingReply) captor.value.onUserSwitching(newID, userSwitchingReply) runCurrent() + verify(beforeUserSwitchingReply).sendResult(any()) verify(userSwitchingReply).sendResult(any()) captor.value.onUserSwitchComplete(newID) @@ -488,6 +494,7 @@ class UserTrackerImplTest : SysuiTestCase() { } private class TestCallback : UserTracker.Callback { + var calledOnBeforeUserChanging = 0 var calledOnUserChanging = 0 var calledOnUserChanged = 0 var calledOnProfilesChanged = 0 @@ -495,6 +502,11 @@ class UserTrackerImplTest : SysuiTestCase() { var lastUserContext: Context? = null var lastUserProfiles = emptyList<UserInfo>() + override fun onBeforeUserSwitching(newUser: Int) { + calledOnBeforeUserChanging++ + lastUser = newUser + } + override fun onUserChanging(newUser: Int, userContext: Context) { calledOnUserChanging++ lastUser = newUser diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 041d1a611b55..4b11e2c24722 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -17,7 +17,6 @@ package com.android.systemui.shade import android.content.Context -import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.annotations.RequiresFlagsDisabled import android.platform.test.flag.junit.FlagsParameterization @@ -32,7 +31,6 @@ import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardSecurityContainerController import com.android.keyguard.dagger.KeyguardBouncerComponent import com.android.systemui.Flags -import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor @@ -406,18 +404,6 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : } @Test - @DisableSceneContainer - @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - fun handleDispatchTouchEvent_nsslMigrationOff_userActivity_not_called() { - underTest.setStatusBarViewController(phoneStatusBarViewController) - - interactionEventHandler.handleDispatchTouchEvent(DOWN_EVENT) - - verify(centralSurfaces, times(0)).userActivity() - } - - @Test - @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun handleDispatchTouchEvent_nsslMigrationOn_userActivity() { underTest.setStatusBarViewController(phoneStatusBarViewController) @@ -438,7 +424,6 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : } @Test - @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun shouldInterceptTouchEvent_dozing_touchNotInLockIconArea_touchIntercepted() { // GIVEN dozing whenever(sysuiStatusBarStateController.isDozing).thenReturn(true) @@ -451,7 +436,6 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : } @Test - @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun shouldInterceptTouchEvent_dozing_touchInStatusBar_touchIntercepted() { // GIVEN dozing whenever(sysuiStatusBarStateController.isDozing).thenReturn(true) @@ -464,7 +448,6 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : } @Test - @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun shouldInterceptTouchEvent_dozingAndPulsing_touchIntercepted() { // GIVEN dozing whenever(sysuiStatusBarStateController.isDozing).thenReturn(true) @@ -609,7 +592,6 @@ class NotificationShadeWindowViewControllerTest(flags: FlagsParameterization) : } @Test - @EnableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) fun cancelCurrentTouch_callsDragDownHelper() { underTest.cancelCurrentTouch() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt index 13bc82fa2c70..a04ca038021e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationsQSContainerControllerLegacyTest.kt @@ -16,7 +16,6 @@ package com.android.systemui.shade -import android.platform.test.annotations.DisableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.View @@ -27,7 +26,6 @@ import androidx.annotation.IdRes import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT import com.android.systemui.SysuiTestCase import com.android.systemui.fragments.FragmentHostManager import com.android.systemui.fragments.FragmentService @@ -65,10 +63,7 @@ import org.mockito.Mockito.reset import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -/** - * Uses Flags.KEYGUARD_STATUS_VIEW_MIGRATE_NSSL set to false. If all goes well, this set of tests - * will be deleted. - */ +/** NotificationsQSContainerController tests */ @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @SmallTest @@ -122,7 +117,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { delayableExecutor, notificationStackScrollLayoutController, ResourcesSplitShadeStateController(), - largeScreenHeaderHelperLazy = { largeScreenHeaderHelper } + largeScreenHeaderHelperLazy = { largeScreenHeaderHelper }, ) overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, SCRIM_MARGIN) @@ -209,23 +204,23 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { given( taskbarVisible = true, navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withStableBottom() + insets = windowInsets().withStableBottom(), ) then( expectedContainerPadding = 0, // taskbar should disappear when shade is expanded expectedNotificationsMargin = NOTIFICATIONS_MARGIN, - expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET, ) given( taskbarVisible = true, navigationMode = BUTTONS_NAVIGATION, - insets = windowInsets().withStableBottom() + insets = windowInsets().withStableBottom(), ) then( expectedContainerPadding = STABLE_INSET_BOTTOM, expectedNotificationsMargin = NOTIFICATIONS_MARGIN, - expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET, ) } @@ -237,22 +232,22 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { given( taskbarVisible = false, navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withStableBottom() + insets = windowInsets().withStableBottom(), ) then( expectedContainerPadding = 0, - expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET, ) given( taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, - insets = windowInsets().withStableBottom() + insets = windowInsets().withStableBottom(), ) then( expectedContainerPadding = 0, // qs goes full height as it's not obscuring nav buttons expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, - expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET, ) } @@ -263,22 +258,22 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { given( taskbarVisible = false, navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withCutout() + insets = windowInsets().withCutout(), ) then( expectedContainerPadding = CUTOUT_HEIGHT, - expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + expectedQsPadding = NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET, ) given( taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, - insets = windowInsets().withCutout().withStableBottom() + insets = windowInsets().withCutout().withStableBottom(), ) then( expectedContainerPadding = 0, expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, - expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET + expectedQsPadding = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN - QS_PADDING_OFFSET, ) } @@ -289,18 +284,18 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { given( taskbarVisible = true, navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withStableBottom() + insets = windowInsets().withStableBottom(), ) then(expectedContainerPadding = 0, expectedQsPadding = STABLE_INSET_BOTTOM) given( taskbarVisible = true, navigationMode = BUTTONS_NAVIGATION, - insets = windowInsets().withStableBottom() + insets = windowInsets().withStableBottom(), ) then( expectedContainerPadding = STABLE_INSET_BOTTOM, - expectedQsPadding = STABLE_INSET_BOTTOM + expectedQsPadding = STABLE_INSET_BOTTOM, ) } @@ -314,19 +309,19 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { given( taskbarVisible = false, navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withCutout().withStableBottom() + insets = windowInsets().withCutout().withStableBottom(), ) then(expectedContainerPadding = CUTOUT_HEIGHT, expectedQsPadding = STABLE_INSET_BOTTOM) given( taskbarVisible = false, navigationMode = BUTTONS_NAVIGATION, - insets = windowInsets().withStableBottom() + insets = windowInsets().withStableBottom(), ) then( expectedContainerPadding = 0, expectedNotificationsMargin = STABLE_INSET_BOTTOM + NOTIFICATIONS_MARGIN, - expectedQsPadding = STABLE_INSET_BOTTOM + expectedQsPadding = STABLE_INSET_BOTTOM, ) } @@ -339,7 +334,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { given( taskbarVisible = false, navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withStableBottom() + insets = windowInsets().withStableBottom(), ) then(expectedContainerPadding = 0, expectedNotificationsMargin = 0) @@ -355,7 +350,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { given( taskbarVisible = false, navigationMode = GESTURES_NAVIGATION, - insets = windowInsets().withStableBottom() + insets = windowInsets().withStableBottom(), ) then(expectedContainerPadding = 0) @@ -376,43 +371,6 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - fun testSplitShadeLayout_isAlignedToGuideline() { - enableSplitShade() - underTest.updateResources() - assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd).isEqualTo(R.id.qs_edge_guideline) - assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startToStart) - .isEqualTo(R.id.qs_edge_guideline) - } - - @Test - @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - fun testSinglePaneLayout_childrenHaveEqualMargins() { - disableSplitShade() - underTest.updateResources() - val qsStartMargin = getConstraintSetLayout(R.id.qs_frame).startMargin - val qsEndMargin = getConstraintSetLayout(R.id.qs_frame).endMargin - val notifStartMargin = getConstraintSetLayout(R.id.notification_stack_scroller).startMargin - val notifEndMargin = getConstraintSetLayout(R.id.notification_stack_scroller).endMargin - assertThat( - qsStartMargin == qsEndMargin && - notifStartMargin == notifEndMargin && - qsStartMargin == notifStartMargin - ) - .isTrue() - } - - @Test - @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - fun testSplitShadeLayout_childrenHaveInsideMarginsOfZero() { - enableSplitShade() - underTest.updateResources() - assertThat(getConstraintSetLayout(R.id.qs_frame).endMargin).isEqualTo(0) - assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startMargin) - .isEqualTo(0) - } - - @Test fun testSplitShadeLayout_qsFrameHasHorizontalMarginsOfZero() { enableSplitShade() underTest.updateResources() @@ -421,37 +379,6 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - fun testLargeScreenLayout_qsAndNotifsTopMarginIsOfHeaderHeightHelper() { - setLargeScreen() - val largeScreenHeaderResourceHeight = 100 - val largeScreenHeaderHelperHeight = 200 - whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()) - .thenReturn(largeScreenHeaderHelperHeight) - overrideResource(R.dimen.large_screen_shade_header_height, largeScreenHeaderResourceHeight) - - // ensure the estimated height (would be 30 here) wouldn't impact this test case - overrideResource(R.dimen.large_screen_shade_header_min_height, 10) - overrideResource(R.dimen.new_qs_header_non_clickable_element_height, 10) - - underTest.updateResources() - - assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin) - .isEqualTo(largeScreenHeaderHelperHeight) - assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin) - .isEqualTo(largeScreenHeaderHelperHeight) - } - - @Test - @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - fun testSmallScreenLayout_qsAndNotifsTopMarginIsZero() { - setSmallScreen() - underTest.updateResources() - assertThat(getConstraintSetLayout(R.id.qs_frame).topMargin).isEqualTo(0) - assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).topMargin).isEqualTo(0) - } - - @Test fun testSinglePaneShadeLayout_qsFrameHasHorizontalMarginsSetToCorrectValue() { disableSplitShade() underTest.updateResources() @@ -464,17 +391,6 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { } @Test - @DisableFlags(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) - fun testSinglePaneShadeLayout_isAlignedToParent() { - disableSplitShade() - underTest.updateResources() - assertThat(getConstraintSetLayout(R.id.qs_frame).endToEnd) - .isEqualTo(ConstraintSet.PARENT_ID) - assertThat(getConstraintSetLayout(R.id.notification_stack_scroller).startToStart) - .isEqualTo(ConstraintSet.PARENT_ID) - } - - @Test fun testAllChildrenOfNotificationContainer_haveIds() { // set dimen to 0 to avoid triggering updating bottom spacing overrideResource(R.dimen.split_shade_notifications_scrim_margin_bottom, 0) @@ -493,7 +409,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { delayableExecutor, notificationStackScrollLayoutController, ResourcesSplitShadeStateController(), - largeScreenHeaderHelperLazy = { largeScreenHeaderHelper } + largeScreenHeaderHelperLazy = { largeScreenHeaderHelper }, ) controller.updateConstraints() @@ -509,7 +425,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { taskbarVisible = false, navigationMode = GESTURES_NAVIGATION, insets = emptyInsets(), - applyImmediately = false + applyImmediately = false, ) fakeSystemClock.advanceTime(INSET_DEBOUNCE_MILLIS / 2) windowInsetsCallback.accept(windowInsets().withStableBottom()) @@ -576,7 +492,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { taskbarVisible: Boolean, navigationMode: Int, insets: WindowInsets, - applyImmediately: Boolean = true + applyImmediately: Boolean = true, ) { Mockito.clearInvocations(view) taskbarVisibilityCallback.onTaskbarStatusUpdated(taskbarVisible, false) @@ -591,7 +507,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { fun then( expectedContainerPadding: Int, expectedNotificationsMargin: Int = NOTIFICATIONS_MARGIN, - expectedQsPadding: Int = 0 + expectedQsPadding: Int = 0, ) { verify(view).setPadding(anyInt(), anyInt(), anyInt(), eq(expectedContainerPadding)) verify(view).setNotificationsMarginBottom(expectedNotificationsMargin) @@ -623,7 +539,7 @@ class NotificationsQSContainerControllerLegacyTest : SysuiTestCase() { val layoutParams = ConstraintLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT + ViewGroup.LayoutParams.WRAP_CONTENT, ) // required as cloning ConstraintSet fails if view doesn't have layout params view.layoutParams = layoutParams diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt index 503fa789cb80..1eb88c5a5616 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt @@ -89,6 +89,7 @@ class SingleLineViewBinderTest : SysuiTestCase() { messagingStyle = null, builder = notificationBuilder, systemUiContext = context, + redactText = false, ) // WHEN: binds the viewHolder @@ -149,6 +150,7 @@ class SingleLineViewBinderTest : SysuiTestCase() { messagingStyle = style, builder = notificationBuilder, systemUiContext = context, + redactText = false, ) // WHEN: binds the view SingleLineViewBinder.bind(viewModel, view) @@ -197,6 +199,7 @@ class SingleLineViewBinderTest : SysuiTestCase() { messagingStyle = null, builder = notificationBuilder, systemUiContext = context, + redactText = false, ) // WHEN: binds the view with the view model SingleLineViewBinder.bind(viewModel, view) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt index d3666321c8e4..ef70e277832e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt @@ -379,7 +379,8 @@ class SingleLineViewInflaterTest : SysuiTestCase() { this, if (isConversation) messagingStyle else null, builder, - context + context, + false ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index a91fb45c9c80..e1a891662889 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -1544,14 +1544,6 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { assertFalse(mStackScroller.mHeadsUpAnimatingAway); } - @Test - @EnableSceneContainer - public void finishExpanding_sceneContainerEnabled() { - mStackScroller.startOverscrollAfterExpanding(); - verify(mStackScroller.getExpandHelper()).finishExpanding(); - assertTrue(mStackScroller.getIsBeingDragged()); - } - private MotionEvent captureTouchSentToSceneFramework() { ArgumentCaptor<MotionEvent> captor = ArgumentCaptor.forClass(MotionEvent.class); verify(mStackScrollLayoutController).sendTouchToSceneFramework(captor.capture()); diff --git a/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt b/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt index 76fc61185b49..25d1c377ecbd 100644 --- a/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt +++ b/packages/SystemUI/tests/utils/src/android/internal/statusbar/FakeStatusBarService.kt @@ -433,6 +433,8 @@ class FakeStatusBarService : IStatusBarService.Stub() { override fun showRearDisplayDialog(currentBaseState: Int) {} + override fun unbundleNotification(key: String) {} + companion object { const val DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY const val SECONDARY_DISPLAY_ID = 2 diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorKosmos.kt new file mode 100644 index 000000000000..57c8fd066ea8 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorKosmos.kt @@ -0,0 +1,29 @@ +/* + * 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.communal.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.scene.domain.interactor.sceneInteractor + +val Kosmos.communalBackActionInteractor by + Kosmos.Fixture { + CommunalBackActionInteractor( + communalInteractor = communalInteractor, + communalSceneInteractor = communalSceneInteractor, + sceneInteractor = sceneInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt index ad92b318b0b9..bfc424848900 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt @@ -86,12 +86,8 @@ suspend fun Kosmos.setCommunalEnabled(enabled: Boolean) { } suspend fun Kosmos.setCommunalV2Enabled(enabled: Boolean) { - setCommunalV2ConfigEnabled(true) - if (enabled) { - fakeUserRepository.asMainUser() - } else { - fakeUserRepository.asDefaultUser() - } + setCommunalV2ConfigEnabled(enabled) + setCommunalEnabled(enabled) } suspend fun Kosmos.setCommunalAvailable(available: Boolean) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelKosmos.kt index b407b1ba227a..e3cfb80489e3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelKosmos.kt @@ -17,8 +17,10 @@ package com.android.systemui.communal.ui.viewmodel import android.service.dream.dreamManager +import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.plugins.activityStarter import com.android.systemui.statusbar.policy.batteryController val Kosmos.communalToDreamButtonViewModel by @@ -26,6 +28,8 @@ val Kosmos.communalToDreamButtonViewModel by CommunalToDreamButtonViewModel( backgroundContext = testDispatcher, batteryController = batteryController, + settingsInteractor = communalSettingsInteractor, + activityStarter = activityStarter, dreamManager = dreamManager, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.kt index 534ded57eb85..9012393badd6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.kt @@ -58,4 +58,26 @@ class FakeDisplayWindowPropertiesRepository(private val context: Context) : fun insert(instance: DisplayWindowProperties) { properties.put(instance.displayId, instance.windowType, instance) } + + /** inserts an entry, mocking everything except the context. */ + fun insertForContext(displayId: Int, windowType: Int, context: Context) { + properties.put( + displayId, + windowType, + DisplayWindowProperties( + displayId = displayId, + windowType = windowType, + context = context, + windowManager = mock(), + layoutInflater = mock(), + ), + ) + } + + /** Whether the repository contains an entry already. */ + fun contains(displayId: Int, windowType: Int): Boolean = + properties.contains(displayId, windowType) + + /** Removes all the entries. */ + fun clear() = properties.clear() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt index 548b5646f5d4..5d206691b520 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt @@ -30,7 +30,7 @@ class FakeKeyguardQuickAffordanceConfig( override val pickerIconResourceId: Int = 0, ) : KeyguardQuickAffordanceConfig { - var onTriggeredResult: OnTriggeredResult = OnTriggeredResult.Handled + var onTriggeredResult: OnTriggeredResult = OnTriggeredResult.Handled(false) private val _lockScreenState = MutableStateFlow<KeyguardQuickAffordanceConfig.LockScreenState>( @@ -41,9 +41,7 @@ class FakeKeyguardQuickAffordanceConfig( override fun pickerName(): String = pickerName - override fun onTriggered( - expandable: Expandable?, - ): OnTriggeredResult { + override fun onTriggered(expandable: Expandable?): OnTriggeredResult { return onTriggeredResult } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceHapticViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceHapticViewModelKosmos.kt new file mode 100644 index 000000000000..d857157137b6 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceHapticViewModelKosmos.kt @@ -0,0 +1,35 @@ +/* + * 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.keyguard.domain.interactor + +import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceHapticViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel +import com.android.systemui.kosmos.Kosmos +import kotlinx.coroutines.flow.Flow + +val Kosmos.keyguardQuickAffordanceHapticViewModelFactory by + Kosmos.Fixture { + object : KeyguardQuickAffordanceHapticViewModel.Factory { + override fun create( + quickAffordanceViewModel: Flow<KeyguardQuickAffordanceViewModel> + ): KeyguardQuickAffordanceHapticViewModel = + KeyguardQuickAffordanceHapticViewModel( + quickAffordanceViewModel, + keyguardQuickAffordanceInteractor, + ) + } + } 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 e47310727905..abbfa93edd17 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 @@ -87,7 +87,6 @@ val Kosmos.keyguardRootViewModel by Fixture { primaryBouncerToLockscreenTransitionViewModel, screenOffAnimationController = screenOffAnimationController, aodBurnInViewModel = aodBurnInViewModel, - aodAlphaViewModel = aodAlphaViewModel, shadeInteractor = shadeInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt index d941fb049835..43835607c77d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.mockito.kotlin.verify var Kosmos.testDispatcher by Fixture { StandardTestDispatcher() } @@ -82,6 +83,32 @@ fun <T> TestScope.currentValue(stateFlow: StateFlow<T>): T { } /** Retrieve the current value of this [StateFlow] safely. See `currentValue(TestScope)`. */ +fun <T> Kosmos.currentValue(fn: () -> T) = testScope.currentValue(fn) + +/** + * Retrieve the result of [fn] after running all pending tasks. Do not use to retrieve the value of + * a flow directly; for that, use either `currentValue(StateFlow)` or [collectLastValue] + */ +@OptIn(ExperimentalCoroutinesApi::class) +fun <T> TestScope.currentValue(fn: () -> T): T { + runCurrent() + return fn() +} + +/** Retrieve the result of [fn] after running all pending tasks. See `TestScope.currentValue(fn)` */ fun <T> Kosmos.currentValue(stateFlow: StateFlow<T>): T { return testScope.currentValue(stateFlow) } + +/** Safely verify that a mock has been called after the test scope has caught up */ +@OptIn(ExperimentalCoroutinesApi::class) +fun <T> TestScope.verifyCurrent(mock: T): T { + runCurrent() + return verify(mock) +} + +/** + * Safely verify that a mock has been called after the test scope has caught up. See + * `TestScope.verifyCurrent` + */ +fun <T> Kosmos.verifyCurrent(mock: T) = testScope.verifyCurrent(mock) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index 41cfceaa5e38..39f1ad42797b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -63,6 +63,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.domain.startable.scrimStartable import com.android.systemui.scene.sceneContainerConfig import com.android.systemui.scene.shared.model.sceneDataSource +import com.android.systemui.scene.ui.view.mockWindowRootViewProvider import com.android.systemui.settings.brightness.domain.interactor.brightnessMirrorShowingInteractor import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor @@ -191,4 +192,5 @@ class KosmosJavaAdapter() { } val disableFlagsInteractor by lazy { kosmos.disableFlagsInteractor } val fakeDisableFlagsRepository by lazy { kosmos.fakeDisableFlagsRepository } + val mockWindowRootViewProvider by lazy { kosmos.mockWindowRootViewProvider } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/ActivityStarterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/ActivityStarterKosmos.kt index 0ec8d49ec29b..49bbbef6ccc0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/ActivityStarterKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/ActivityStarterKosmos.kt @@ -17,6 +17,6 @@ package com.android.systemui.plugins import com.android.systemui.kosmos.Kosmos -import com.android.systemui.util.mockito.mock +import org.mockito.kotlin.mock var Kosmos.activityStarter by Kosmos.Fixture { mock<ActivityStarter>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt index ab1c1818bf80..aa29808bd9ee 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt @@ -45,7 +45,6 @@ private constructor(failureMetadata: FailureMetadata, subject: QSTileState?) : other ?: return } check("icon").that(actual.icon).isEqualTo(other.icon) - check("iconRes").that(actual.iconRes).isEqualTo(other.iconRes) check("label").that(actual.label).isEqualTo(other.label) check("activationState").that(actual.activationState).isEqualTo(other.activationState) check("secondaryLabel").that(actual.secondaryLabel).isEqualTo(other.secondaryLabel) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt index f52572a9e42d..f5eebb46c2ec 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneDataSource.kt @@ -19,13 +19,13 @@ package com.android.systemui.scene.shared.model 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.kosmos.currentValue import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.test.TestScope -class FakeSceneDataSource( - initialSceneKey: SceneKey, -) : SceneDataSource { +class FakeSceneDataSource(initialSceneKey: SceneKey, val testScope: TestScope) : SceneDataSource { private val _currentScene = MutableStateFlow(initialSceneKey) override val currentScene: StateFlow<SceneKey> = _currentScene.asStateFlow() @@ -33,18 +33,20 @@ class FakeSceneDataSource( private val _currentOverlays = MutableStateFlow<Set<OverlayKey>>(emptySet()) override val currentOverlays: StateFlow<Set<OverlayKey>> = _currentOverlays.asStateFlow() - var isPaused = false - private set + private var _isPaused = false + val isPaused + get() = testScope.currentValue { _isPaused } - var pendingScene: SceneKey? = null - private set + private var _pendingScene: SceneKey? = null + val pendingScene + get() = testScope.currentValue { _pendingScene } var pendingOverlays: Set<OverlayKey>? = null private set override fun changeScene(toScene: SceneKey, transitionKey: TransitionKey?) { - if (isPaused) { - pendingScene = toScene + if (_isPaused) { + _pendingScene = toScene } else { _currentScene.value = toScene } @@ -55,7 +57,7 @@ class FakeSceneDataSource( } override fun showOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) { - if (isPaused) { + if (_isPaused) { pendingOverlays = (pendingOverlays ?: currentOverlays.value) + overlay } else { _currentOverlays.value += overlay @@ -63,7 +65,7 @@ class FakeSceneDataSource( } override fun hideOverlay(overlay: OverlayKey, transitionKey: TransitionKey?) { - if (isPaused) { + if (_isPaused) { pendingOverlays = (pendingOverlays ?: currentOverlays.value) - overlay } else { _currentOverlays.value -= overlay @@ -82,9 +84,9 @@ class FakeSceneDataSource( * last one will be remembered. */ fun pause() { - check(!isPaused) { "Can't pause what's already paused!" } + check(!_isPaused) { "Can't pause what's already paused!" } - isPaused = true + _isPaused = true } /** @@ -100,15 +102,12 @@ class FakeSceneDataSource( * * If [expectedScene] is provided, will assert that it's indeed the latest called. */ - fun unpause( - force: Boolean = false, - expectedScene: SceneKey? = null, - ) { - check(force || isPaused) { "Can't unpause what's already not paused!" } - - isPaused = false - pendingScene?.let { _currentScene.value = it } - pendingScene = null + fun unpause(force: Boolean = false, expectedScene: SceneKey? = null) { + check(force || _isPaused) { "Can't unpause what's already not paused!" } + + _isPaused = false + _pendingScene?.let { _currentScene.value = it } + _pendingScene = null pendingOverlays?.let { _currentOverlays.value = it } pendingOverlays = null diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneDataSourceKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneDataSourceKosmos.kt index f5196866ae6f..7eebfc305682 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneDataSourceKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneDataSourceKosmos.kt @@ -19,13 +19,12 @@ package com.android.systemui.scene.shared.model import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope import com.android.systemui.scene.initialSceneKey import com.android.systemui.scene.sceneContainerConfig val Kosmos.fakeSceneDataSource by Fixture { - FakeSceneDataSource( - initialSceneKey = initialSceneKey, - ) + FakeSceneDataSource(initialSceneKey = initialSceneKey, testScope = testScope) } val Kosmos.sceneDataSourceDelegator by Fixture { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/view/WindowRootViewKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/view/WindowRootViewKosmos.kt index 5c91dc80b3d4..e6ba9a581836 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/view/WindowRootViewKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/view/WindowRootViewKosmos.kt @@ -17,6 +17,9 @@ package com.android.systemui.scene.ui.view import com.android.systemui.kosmos.Kosmos +import javax.inject.Provider import org.mockito.kotlin.mock val Kosmos.mockShadeRootView by Kosmos.Fixture { mock<WindowRootView>() } +val Kosmos.mockWindowRootViewProvider by + Kosmos.Fixture { Provider<WindowRootView> { mock<WindowRootView>() } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/NotificationShadeWindowViewKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/NotificationShadeWindowViewKosmos.kt new file mode 100644 index 000000000000..18c44bac3ff1 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/NotificationShadeWindowViewKosmos.kt @@ -0,0 +1,22 @@ +/* + * 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.shade + +import com.android.systemui.kosmos.Kosmos +import org.mockito.kotlin.mock + +var Kosmos.notificationShadeWindowView by Kosmos.Fixture { mock<NotificationShadeWindowView>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeAnimationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeAnimationRepositoryKosmos.kt index 4dcd2208b152..3ed730271bc3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeAnimationRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeAnimationRepositoryKosmos.kt @@ -16,6 +16,14 @@ package com.android.systemui.shade.data.repository +import android.content.applicationContext import com.android.systemui.kosmos.Kosmos +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock val Kosmos.shadeAnimationRepository by Kosmos.Fixture { ShadeAnimationRepository() } +val Kosmos.shadeDialogContextInteractor by + Kosmos.Fixture { + mock<ShadeDialogContextInteractor> { on { context } doReturn applicationContext } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionControllerKosmos.kt index e5a75d59468d..9f4091ccfc26 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeKeyguardTransitionControllerKosmos.kt @@ -18,8 +18,14 @@ package com.android.systemui.statusbar import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.util.mockito.mock +import org.mockito.kotlin.mock + +var Kosmos.lockscreenShadeKeyguardTransitionController by Fixture { + mock<LockscreenShadeKeyguardTransitionController>() +} var Kosmos.lockscreenShadeKeyguardTransitionControllerFactory by Fixture { - mock<LockscreenShadeKeyguardTransitionController.Factory>() + LockscreenShadeKeyguardTransitionController.Factory { + lockscreenShadeKeyguardTransitionController + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeQsTransitionControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeQsTransitionControllerKosmos.kt index 27679804d11f..fc52e454a1c6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeQsTransitionControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeQsTransitionControllerKosmos.kt @@ -18,8 +18,12 @@ package com.android.systemui.statusbar import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.util.mockito.mock +import org.mockito.kotlin.mock + +var Kosmos.lockscreenShadeQsTransitionController by Fixture { + mock<LockscreenShadeQsTransitionController>() +} var Kosmos.lockscreenShadeQsTransitionControllerFactory by Fixture { - mock<LockscreenShadeQsTransitionController.Factory>() + LockscreenShadeQsTransitionController.Factory { lockscreenShadeQsTransitionController } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerKosmos.kt index e4a3896378f6..1a451ce01525 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerKosmos.kt @@ -36,7 +36,7 @@ import com.android.systemui.statusbar.phone.lsShadeTransitionLogger import com.android.systemui.statusbar.policy.configurationController import com.android.systemui.statusbar.policy.splitShadeStateController -val Kosmos.lockscreenShadeTransitionController by Fixture { +var Kosmos.lockscreenShadeTransitionController by Fixture { LockscreenShadeTransitionController( statusBarStateController = sysuiStatusBarStateController, logger = lsShadeTransitionLogger, @@ -62,6 +62,6 @@ val Kosmos.lockscreenShadeTransitionController by Fixture { splitShadeStateController = splitShadeStateController, shadeLockscreenInteractorLazy = { shadeLockscreenInteractor }, naturalScrollingSettingObserver = naturalScrollingSettingObserver, - lazyQSSceneAdapter = { qsSceneAdapter } + lazyQSSceneAdapter = { qsSceneAdapter }, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/SingleShadeLockScreenOverScrollerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/SingleShadeLockScreenOverScrollerKosmos.kt index 43e39c05f6e9..8a68eef2f371 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/SingleShadeLockScreenOverScrollerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/SingleShadeLockScreenOverScrollerKosmos.kt @@ -18,7 +18,7 @@ package com.android.systemui.statusbar import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.util.mockito.mock +import org.mockito.kotlin.mock var Kosmos.singleShadeLockScreenOverScrollerFactory by Fixture { mock<SingleShadeLockScreenOverScroller.Factory>() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/SplitShadeLockScreenOverScrollerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/SplitShadeLockScreenOverScrollerKosmos.kt index 017371a6cba8..e491dffb0ed5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/SplitShadeLockScreenOverScrollerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/SplitShadeLockScreenOverScrollerKosmos.kt @@ -18,8 +18,10 @@ package com.android.systemui.statusbar import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.util.mockito.mock +import org.mockito.kotlin.mock + +var Kosmos.splitShadeLockScreenOverScroller by Fixture { mock<SplitShadeLockScreenOverScroller>() } var Kosmos.splitShadeLockScreenOverScrollerFactory by Fixture { - mock<SplitShadeLockScreenOverScroller.Factory>() + SplitShadeLockScreenOverScroller.Factory { _, _ -> splitShadeLockScreenOverScroller } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt new file mode 100644 index 000000000000..62cdc87f980f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/featurepods/popups/ui/viewmodel/StatusBarPopupChipsViewModelKosmos.kt @@ -0,0 +1,23 @@ +/* + * 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.statusbar.featurepods.popups.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope + +val Kosmos.statusBarPopupChipsViewModel: StatusBarPopupChipsViewModel by + Kosmos.Fixture { StatusBarPopupChipsViewModel(testScope.backgroundScope) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/VisualInterruptionDecisionProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/VisualInterruptionDecisionProviderKosmos.kt new file mode 100644 index 000000000000..360e9e93f18d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/VisualInterruptionDecisionProviderKosmos.kt @@ -0,0 +1,24 @@ +/* + * 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.statusbar.notification + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider +import org.mockito.kotlin.mock + +val Kosmos.visualInterruptionDecisionProvider by + Kosmos.Fixture { mock<VisualInterruptionDecisionProvider>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorKosmos.kt new file mode 100644 index 000000000000..768952d1ee77 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationAlertsInteractorKosmos.kt @@ -0,0 +1,22 @@ +/* + * 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.statusbar.notification.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import org.mockito.kotlin.mock + +val Kosmos.notificationAlertsInteractor by Kosmos.Fixture { mock<NotificationAlertsInteractor>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt new file mode 100644 index 000000000000..680e0de22794 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationContentExtractor.kt @@ -0,0 +1,58 @@ +/* + * 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.statusbar.notification.promoted + +import android.app.Notification +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel +import org.junit.Assert + +class FakePromotedNotificationContentExtractor : PromotedNotificationContentExtractor { + @JvmField + val contentForEntry = mutableMapOf<NotificationEntry, PromotedNotificationContentModel?>() + @JvmField val extractCalls = mutableListOf<Pair<NotificationEntry, Notification.Builder>>() + + override fun extractContent( + entry: NotificationEntry, + recoveredBuilder: Notification.Builder, + ): PromotedNotificationContentModel? { + extractCalls.add(entry to recoveredBuilder) + + if (contentForEntry.isEmpty()) { + // If *no* entries are set, just return null for everything. + return null + } else { + // If entries *are* set, fail on unexpected ones. + Assert.assertTrue(contentForEntry.containsKey(entry)) + return contentForEntry.get(entry) + } + } + + fun resetForEntry(entry: NotificationEntry, content: PromotedNotificationContentModel?) { + contentForEntry.clear() + contentForEntry.put(entry, content) + extractCalls.clear() + } + + fun verifyZeroExtractCalls() { + Assert.assertTrue(extractCalls.isEmpty()) + } + + fun verifyOneExtractCall() { + Assert.assertEquals(1, extractCalls.size) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationsProvider.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationsProvider.kt index 88caf6e2ba30..ea7b41d43871 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationsProvider.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/FakePromotedNotificationsProvider.kt @@ -17,11 +17,20 @@ package com.android.systemui.statusbar.notification.promoted import com.android.systemui.statusbar.notification.collection.NotificationEntry +import org.junit.Assert class FakePromotedNotificationsProvider : PromotedNotificationsProvider { val promotedEntries = mutableSetOf<NotificationEntry>() + val shouldPromoteForEntry = mutableMapOf<NotificationEntry, Boolean>() override fun shouldPromote(entry: NotificationEntry): Boolean { - return promotedEntries.contains(entry) + if (shouldPromoteForEntry.isEmpty()) { + // If *no* entries are set, just return false for everything. + return false + } else { + // If entries *are* set, fail on unexpected ones. + Assert.assertTrue(shouldPromoteForEntry.containsKey(entry)) + return shouldPromoteForEntry[entry] ?: false + } } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt index 5e9f12b4b1cc..52c17c82fb12 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/PromotedNotificationContentExtractorKosmos.kt @@ -21,7 +21,7 @@ import com.android.systemui.kosmos.Kosmos var Kosmos.promotedNotificationContentExtractor by Kosmos.Fixture { - PromotedNotificationContentExtractor( + PromotedNotificationContentExtractorImpl( promotedNotificationsProvider, applicationContext, promotedNotificationLogger, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt index 7126933154df..e739e82aa8a8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowBuilder.kt @@ -65,7 +65,7 @@ import com.android.systemui.statusbar.notification.headsup.HeadsUpManager import com.android.systemui.statusbar.notification.icon.IconBuilder import com.android.systemui.statusbar.notification.icon.IconManager import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier -import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractor +import com.android.systemui.statusbar.notification.promoted.PromotedNotificationContentExtractorImpl import com.android.systemui.statusbar.notification.promoted.PromotedNotificationLogger import com.android.systemui.statusbar.notification.promoted.PromotedNotificationsProviderImpl import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.CoordinateOnClickListener @@ -222,16 +222,11 @@ class ExpandableNotificationRowBuilder( Mockito.mock(LauncherApps::class.java, STUB_ONLY), Mockito.mock(ConversationNotificationManager::class.java, STUB_ONLY), ) - - val promotedNotificationsProvider = PromotedNotificationsProviderImpl() - val promotedNotificationLog = logcatLogBuffer("PromotedNotifLog") - val promotedNotificationLogger = PromotedNotificationLogger(promotedNotificationLog) - val promotedNotificationContentExtractor = - PromotedNotificationContentExtractor( - promotedNotificationsProvider, + PromotedNotificationContentExtractorImpl( + PromotedNotificationsProviderImpl(), context, - promotedNotificationLogger, + PromotedNotificationLogger(logcatLogBuffer("PromotedNotifLog")), ) mContentBinder = diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/KeyguardStateControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/KeyguardStateControllerKosmos.kt index f19ac1e5a58d..26642d4f7534 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/KeyguardStateControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/KeyguardStateControllerKosmos.kt @@ -17,7 +17,7 @@ package com.android.systemui.statusbar.policy import com.android.systemui.kosmos.Kosmos -import org.mockito.Mockito.mock +import org.mockito.kotlin.mock var Kosmos.keyguardStateController: KeyguardStateController by - Kosmos.Fixture { mock(KeyguardStateController::class.java) } + Kosmos.Fixture { mock<KeyguardStateController>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateKosmos.kt index 932e768676cb..6c98d19db5d7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateKosmos.kt @@ -20,6 +20,7 @@ import com.android.systemui.animation.dialogTransitionAnimator import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.mainCoroutineContext import com.android.systemui.plugins.activityStarter +import com.android.systemui.shade.data.repository.shadeDialogContextInteractor import com.android.systemui.statusbar.phone.systemUIDialogFactory import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.modesDialogViewModel import com.android.systemui.util.mockito.mock @@ -35,5 +36,6 @@ var Kosmos.modesDialogDelegate: ModesDialogDelegate by { modesDialogViewModel }, modesDialogEventLogger, mainCoroutineContext, + shadeDialogContextInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.kt index a3572754ab19..f31697e82a45 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.kt @@ -70,6 +70,14 @@ class FakeSettings : SecureSettings, SystemSettings, UserSettingsProxy { } } + fun getContentObservers(uri: Uri, userHandle: Int): List<ContentObserver> { + if (userHandle == UserHandle.USER_ALL) { + return contentObserversAllUsers[uri.toString()] ?: listOf() + } else { + return contentObservers[SettingsKey(userHandle, uri.toString())] ?: listOf() + } + } + override fun getContentResolver(): ContentResolver { throw UnsupportedOperationException("FakeSettings.getContentResolver is not implemented") } diff --git a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java index 3441d94facda..9ceca5d1dbfe 100644 --- a/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java +++ b/services/accessibility/java/com/android/server/accessibility/AbstractAccessibilityServiceConnection.java @@ -1589,7 +1589,13 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ * lock because this calls out to WindowManagerService. */ void addWindowTokensForAllDisplays() { - final Display[] displays = mDisplayManager.getDisplays(); + Display[] displays = {}; + final long identity = Binder.clearCallingIdentity(); + try { + displays = mDisplayManager.getDisplays(); + } finally { + Binder.restoreCallingIdentity(identity); + } for (int i = 0; i < displays.length; i++) { final int displayId = displays[i].getDisplayId(); addWindowTokenForDisplay(displayId); @@ -1625,7 +1631,13 @@ abstract class AbstractAccessibilityServiceConnection extends IAccessibilityServ } public void onRemoved() { - final Display[] displays = mDisplayManager.getDisplays(); + Display[] displays = {}; + final long identity = Binder.clearCallingIdentity(); + try { + displays = mDisplayManager.getDisplays(); + } finally { + Binder.restoreCallingIdentity(identity); + } for (int i = 0; i < displays.length; i++) { final int displayId = displays[i].getDisplayId(); onDisplayRemoved(displayId); diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java index 3025e2eaede0..549f8fa77b53 100644 --- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java +++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java @@ -217,6 +217,13 @@ public class UserBackupManagerService { + mPowerManagerWakeLock.getTag())); return; } + + if (!mPowerManagerWakeLock.isHeld()) { + Slog.w(TAG, addUserIdToLogMessage(mUserId, + "Wakelock not held: " + mPowerManagerWakeLock.getTag())); + return; + } + mPowerManagerWakeLock.release(); Slog.v( TAG, diff --git a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java index fd18fa856916..abfb8268bd9a 100644 --- a/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java +++ b/services/contextualsearch/java/com/android/server/contextualsearch/ContextualSearchManagerService.java @@ -55,6 +55,7 @@ import android.content.Intent; import android.content.pm.PackageManagerInternal; import android.content.pm.ResolveInfo; import android.graphics.Bitmap; +import android.media.AudioManager; import android.os.Binder; import android.os.Bundle; import android.os.Handler; @@ -102,6 +103,7 @@ public class ContextualSearchManagerService extends SystemService { private final PackageManagerInternal mPackageManager; private final WindowManagerInternal mWmInternal; private final DevicePolicyManagerInternal mDpmInternal; + private final AudioManager mAudioManager; private final Object mLock = new Object(); private final AssistDataRequester mAssistDataRequester; @@ -163,6 +165,8 @@ public class ContextualSearchManagerService extends SystemService { mAtmInternal = Objects.requireNonNull( LocalServices.getService(ActivityTaskManagerInternal.class)); mPackageManager = LocalServices.getService(PackageManagerInternal.class); + mAudioManager = context.getSystemService(AudioManager.class); + mWmInternal = Objects.requireNonNull(LocalServices.getService(WindowManagerInternal.class)); mDpmInternal = LocalServices.getService(DevicePolicyManagerInternal.class); mAssistDataRequester = new AssistDataRequester( @@ -306,6 +310,10 @@ public class ContextualSearchManagerService extends SystemService { SystemClock.uptimeMillis()); launchIntent.putExtra(ContextualSearchManager.EXTRA_ENTRYPOINT, entrypoint); launchIntent.putExtra(ContextualSearchManager.EXTRA_TOKEN, mToken); + if (Flags.includeAudioPlayingStatus()) { + launchIntent.putExtra(ContextualSearchManager.EXTRA_IS_AUDIO_PLAYING, + mAudioManager.isMusicActive()); + } boolean isAssistDataAllowed = mAtmInternal.isAssistDataAllowed(); final List<ActivityAssistInfo> records = mAtmInternal.getTopVisibleActivities(); final List<IBinder> activityTokens = new ArrayList<>(records.size()); diff --git a/services/core/java/com/android/server/am/ActivityManagerConstants.java b/services/core/java/com/android/server/am/ActivityManagerConstants.java index 4944cafeb83d..4b8770b3cd35 100644 --- a/services/core/java/com/android/server/am/ActivityManagerConstants.java +++ b/services/core/java/com/android/server/am/ActivityManagerConstants.java @@ -16,6 +16,7 @@ package com.android.server.am; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_RECONFIGURATION; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH; import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION; @@ -31,6 +32,7 @@ import static com.android.server.am.BroadcastConstants.DEFER_BOOT_COMPLETED_BROA import static com.android.server.am.BroadcastConstants.getDeviceConfigBoolean; import android.annotation.NonNull; +import android.app.ActivityManagerInternal; import android.app.ActivityThread; import android.app.ForegroundServiceTypePolicy; import android.content.ComponentName; @@ -55,6 +57,7 @@ import android.util.SparseBooleanArray; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; +import com.android.server.LocalServices; import dalvik.annotation.optimization.NeverCompile; @@ -181,6 +184,12 @@ final class ActivityManagerConstants extends ContentObserver { static final String KEY_FOLLOW_UP_OOMADJ_UPDATE_WAIT_DURATION = "follow_up_oomadj_update_wait_duration"; + /* + * Oom score cutoff beyond which any process that does not have the CPU_TIME capability will be + * frozen. + */ + static final String KEY_FREEZER_CUTOFF_ADJ = "freezer_cutoff_adj"; + private static final int DEFAULT_MAX_CACHED_PROCESSES = 1024; private static final boolean DEFAULT_PRIORITIZE_ALARM_BROADCASTS = true; private static final long DEFAULT_FGSERVICE_MIN_SHOWN_TIME = 2*1000; @@ -267,6 +276,9 @@ final class ActivityManagerConstants extends ContentObserver { */ private static final long DEFAULT_FOLLOW_UP_OOMADJ_UPDATE_WAIT_DURATION = 1000L; + /** The default value to {@link #KEY_FREEZER_CUTOFF_ADJ} */ + private static final int DEFAULT_FREEZER_CUTOFF_ADJ = ProcessList.CACHED_APP_MIN_ADJ; + /** * Same as {@link TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED} */ @@ -1171,6 +1183,14 @@ final class ActivityManagerConstants extends ContentObserver { DEFAULT_FOLLOW_UP_OOMADJ_UPDATE_WAIT_DURATION; /** + * The cutoff adj for the freezer, app processes with adj greater than this value will be + * eligible for the freezer. + * + * @see #KEY_FREEZER_CUTOFF_ADJ + */ + public int FREEZER_CUTOFF_ADJ = DEFAULT_FREEZER_CUTOFF_ADJ; + + /** * Indicates whether PSS profiling in AppProfiler is disabled or not. */ static final String KEY_DISABLE_APP_PROFILER_PSS_PROFILING = @@ -1194,6 +1214,7 @@ final class ActivityManagerConstants extends ContentObserver { new OnPropertiesChangedListener() { @Override public void onPropertiesChanged(Properties properties) { + boolean oomAdjusterConfigUpdated = false; for (String name : properties.getKeyset()) { if (name == null) { return; @@ -1372,6 +1393,11 @@ final class ActivityManagerConstants extends ContentObserver { case KEY_TIERED_CACHED_ADJ_UI_TIER_SIZE: updateUseTieredCachedAdj(); break; + case KEY_FREEZER_CUTOFF_ADJ: + FREEZER_CUTOFF_ADJ = properties.getInt(KEY_FREEZER_CUTOFF_ADJ, + DEFAULT_FREEZER_CUTOFF_ADJ); + oomAdjusterConfigUpdated = true; + break; case KEY_DISABLE_APP_PROFILER_PSS_PROFILING: updateDisableAppProfilerPssProfiling(); break; @@ -1389,6 +1415,13 @@ final class ActivityManagerConstants extends ContentObserver { break; } } + if (oomAdjusterConfigUpdated) { + final ActivityManagerInternal ami = LocalServices.getService( + ActivityManagerInternal.class); + if (ami != null) { + ami.updateOomAdj(OOM_ADJ_REASON_RECONFIGURATION); + } + } } }; @@ -2534,6 +2567,9 @@ final class ActivityManagerConstants extends ContentObserver { pw.print(" "); pw.print(KEY_ENABLE_NEW_OOMADJ); pw.print("="); pw.println(ENABLE_NEW_OOMADJ); + pw.print(" "); pw.print(KEY_FREEZER_CUTOFF_ADJ); + pw.print("="); pw.println(FREEZER_CUTOFF_ADJ); + pw.print(" "); pw.print(KEY_DISABLE_APP_PROFILER_PSS_PROFILING); pw.print("="); pw.println(APP_PROFILER_PSS_PROFILING_DISABLED); diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index a184e905d0fa..50b6990c0c1c 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -19396,9 +19396,6 @@ public class ActivityManagerService extends IActivityManager.Stub creatorPackage); if (creatorToken != null) { extraIntent.setCreatorToken(creatorToken); - // TODO remove Slog.wtf once proven FrameworkStatsLog works. b/375396329 - Slog.wtf(TAG, "A creator token is added to an intent. creatorPackage: " - + creatorPackage + "; intent: " + extraIntent); FrameworkStatsLog.write(INTENT_CREATOR_TOKEN_ADDED, creatorUid, false); } }); diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index 9a63546bf5a7..cbebc905796c 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -449,6 +449,8 @@ final class ActivityManagerShellCommand extends ShellCommand { return runSetAppZygotePreloadTimeout(pw); case "set-media-foreground-service": return runSetMediaForegroundService(pw); + case "clear-bad-process": + return runClearBadProcess(pw); default: return handleDefaultCommands(cmd); } @@ -4276,6 +4278,27 @@ final class ActivityManagerShellCommand extends ShellCommand { return 0; } + int runClearBadProcess(PrintWriter pw) throws RemoteException { + final String processName = getNextArgRequired(); + int userId = UserHandle.USER_CURRENT; + String opt; + while ((opt = getNextOption()) != null) { + if ("--user".equals(opt)) { + userId = UserHandle.parseUserArg(getNextArgRequired()); + } else { + getErrPrintWriter().println("Error: unknown option " + opt); + return -1; + } + } + if (userId == UserHandle.USER_CURRENT) { + userId = mInternal.getCurrentUserId(); + } + + pw.println("Clearing '" + processName + "' in u" + userId + " from bad processes list"); + mInternal.mAppErrors.clearBadProcessForUser(processName, userId); + return 0; + } + private Resources getResources(PrintWriter pw) throws RemoteException { // system resources does not contain all the device configuration, construct it manually. Configuration config = mInterface.getConfiguration(); @@ -4717,6 +4740,8 @@ final class ActivityManagerShellCommand extends ShellCommand { pw.println(" set-media-foreground-service inactive|active [--user USER_ID] <PACKAGE>" + " <NOTIFICATION_ID>"); pw.println(" Set an app's media service inactive or active."); + pw.println(" clear-bad-process [--user USER_ID] <PROCESS_NAME>"); + pw.println(" Clears a process from the bad processes list."); Intent.printIntentArgsHelp(pw, ""); } } diff --git a/services/core/java/com/android/server/am/AppErrors.java b/services/core/java/com/android/server/am/AppErrors.java index b7a5f3e4099a..2fbf05eb0061 100644 --- a/services/core/java/com/android/server/am/AppErrors.java +++ b/services/core/java/com/android/server/am/AppErrors.java @@ -373,6 +373,24 @@ class AppErrors { } } + void clearBadProcessForUser(final String processName, final int userId) { + synchronized (mBadProcessLock) { + final ProcessMap<BadProcessInfo> badProcesses = new ProcessMap<>(); + badProcesses.putAll(mBadProcesses); + final SparseArray<BadProcessInfo> uids = badProcesses.get(processName); + if (uids == null) { + return; + } + for (int i = uids.size() - 1; i >= 0; --i) { + final int uid = uids.keyAt(i); + if (userId == UserHandle.USER_ALL || userId == UserHandle.getUserId(uid)) { + badProcesses.remove(processName, uid); + } + } + mBadProcesses = badProcesses; + } + } + void markBadProcess(final String processName, final int uid, BadProcessInfo info) { synchronized (mBadProcessLock) { final ProcessMap<BadProcessInfo> badProcesses = new ProcessMap<>(); diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index 2f5362f53361..d335529a006a 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -25,9 +25,11 @@ import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_BIND_SERVICE; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_COMPONENT_DISABLED; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_EXECUTING_SERVICE; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_FINISH_RECEIVER; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_FOLLOW_UP; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_GET_PROVIDER; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_PROCESS_BEGIN; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_PROCESS_END; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_RECONFIGURATION; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_REMOVE_PROVIDER; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_REMOVE_TASK; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_RESTRICTION_CHANGE; @@ -203,6 +205,10 @@ public class CachedAppOptimizer { FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_RESTRICTION_CHANGE; static final int UNFREEZE_REASON_COMPONENT_DISABLED = FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_COMPONENT_DISABLED; + static final int UNFREEZE_REASON_OOM_ADJ_FOLLOW_UP = + FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_OOM_ADJ_FOLLOW_UP; + static final int UNFREEZE_REASON_OOM_ADJ_RECONFIGURATION = + FrameworkStatsLog.APP_FREEZE_CHANGED__UNFREEZE_REASON_V2__UFR_OOM_ADJ_RECONFIGURATION; @IntDef(prefix = {"UNFREEZE_REASON_"}, value = { UNFREEZE_REASON_NONE, @@ -234,6 +240,8 @@ public class CachedAppOptimizer { UNFREEZE_REASON_EXECUTING_SERVICE, UNFREEZE_REASON_RESTRICTION_CHANGE, UNFREEZE_REASON_COMPONENT_DISABLED, + UNFREEZE_REASON_OOM_ADJ_FOLLOW_UP, + UNFREEZE_REASON_OOM_ADJ_RECONFIGURATION, }) @Retention(RetentionPolicy.SOURCE) public @interface UnfreezeReason {} @@ -2451,8 +2459,8 @@ public class CachedAppOptimizer { synchronized (mAm.mPidsSelfLocked) { pr = mAm.mPidsSelfLocked.get(blocked); } - if (pr != null - && pr.mState.getCurAdj() < ProcessList.FREEZER_CUTOFF_ADJ) { + if (pr != null && pr.mState.getCurAdj() + < mAm.mConstants.FREEZER_CUTOFF_ADJ) { Slog.d(TAG_AM, app.processName + " (" + pid + ") blocks " + pr.processName + " (" + blocked + ")"); // Found at least one blocked non-cached process @@ -2539,6 +2547,10 @@ public class CachedAppOptimizer { return UNFREEZE_REASON_RESTRICTION_CHANGE; case OOM_ADJ_REASON_COMPONENT_DISABLED: return UNFREEZE_REASON_COMPONENT_DISABLED; + case OOM_ADJ_REASON_FOLLOW_UP: + return UNFREEZE_REASON_OOM_ADJ_FOLLOW_UP; + case OOM_ADJ_REASON_RECONFIGURATION: + return UNFREEZE_REASON_OOM_ADJ_RECONFIGURATION; default: return UNFREEZE_REASON_NONE; } diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index aadf6f61956c..9c569db99797 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -55,6 +55,7 @@ import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_GET_PROVIDER; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_NONE; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_PROCESS_BEGIN; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_PROCESS_END; +import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_RECONFIGURATION; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_REMOVE_PROVIDER; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_REMOVE_TASK; import static android.app.ActivityManagerInternal.OOM_ADJ_REASON_RESTRICTION_CHANGE; @@ -105,7 +106,6 @@ import static com.android.server.am.ProcessList.CACHED_APP_IMPORTANCE_LEVELS; import static com.android.server.am.ProcessList.CACHED_APP_MAX_ADJ; import static com.android.server.am.ProcessList.CACHED_APP_MIN_ADJ; import static com.android.server.am.ProcessList.FOREGROUND_APP_ADJ; -import static com.android.server.am.ProcessList.FREEZER_CUTOFF_ADJ; import static com.android.server.am.ProcessList.HEAVY_WEIGHT_APP_ADJ; import static com.android.server.am.ProcessList.HOME_APP_ADJ; import static com.android.server.am.ProcessList.INVALID_ADJ; @@ -232,6 +232,8 @@ public class OomAdjuster { return AppProtoEnums.OOM_ADJ_REASON_COMPONENT_DISABLED; case OOM_ADJ_REASON_FOLLOW_UP: return AppProtoEnums.OOM_ADJ_REASON_FOLLOW_UP; + case OOM_ADJ_REASON_RECONFIGURATION: + return AppProtoEnums.OOM_ADJ_REASON_RECONFIGURATION; default: return AppProtoEnums.OOM_ADJ_REASON_UNKNOWN_TO_PROTO; } @@ -288,6 +290,8 @@ public class OomAdjuster { return OOM_ADJ_REASON_METHOD + "_componentDisabled"; case OOM_ADJ_REASON_FOLLOW_UP: return OOM_ADJ_REASON_METHOD + "_followUp"; + case OOM_ADJ_REASON_RECONFIGURATION: + return OOM_ADJ_REASON_METHOD + "_reconfiguration"; default: return "_unknown"; } @@ -4079,7 +4083,7 @@ public class OomAdjuster { } // Reasons to freeze: - if (proc.mState.getCurAdj() >= FREEZER_CUTOFF_ADJ) { + if (proc.mState.getCurAdj() >= mConstants.FREEZER_CUTOFF_ADJ) { // Oomscore is in a high enough state, it is safe to freeze. return true; } @@ -4098,9 +4102,8 @@ public class OomAdjuster { final ProcessCachedOptimizerRecord opt = app.mOptRecord; final ProcessStateRecord state = app.mState; if (Flags.traceUpdateAppFreezeStateLsp()) { - final boolean oomAdjChanged = - (state.getCurAdj() >= FREEZER_CUTOFF_ADJ ^ oldOomAdj >= FREEZER_CUTOFF_ADJ) - || oldOomAdj == UNKNOWN_ADJ; + final boolean oomAdjChanged = (state.getCurAdj() >= mConstants.FREEZER_CUTOFF_ADJ + ^ oldOomAdj >= mConstants.FREEZER_CUTOFF_ADJ) || oldOomAdj == UNKNOWN_ADJ; final boolean shouldNotFreezeChanged = opt.shouldNotFreezeAdjSeq() == mAdjSeq; final boolean hasCpuCapability = (PROCESS_CAPABILITY_CPU_TIME & app.mState.getCurCapability()) diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index 70febcd63455..bddde9d589f3 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -383,12 +383,6 @@ public final class ProcessList { private static final long LMKD_RECONNECT_DELAY_MS = 1000; /** - * The cuttoff adj for the freezer, app processes with adj greater than this value will be - * eligible for the freezer. - */ - static final int FREEZER_CUTOFF_ADJ = CACHED_APP_MIN_ADJ; - - /** * Apps have no access to the private data directories of any other app, even if the other * app has made them world-readable. */ diff --git a/services/core/java/com/android/server/am/ProcessRecord.java b/services/core/java/com/android/server/am/ProcessRecord.java index 98f738c38d63..49149e1fa415 100644 --- a/services/core/java/com/android/server/am/ProcessRecord.java +++ b/services/core/java/com/android/server/am/ProcessRecord.java @@ -1699,7 +1699,7 @@ class ProcessRecord implements WindowProcessListener { return mService.mOomAdjuster.mCachedAppOptimizer.useFreezer() && !mOptRecord.isFreezeExempt() && !mOptRecord.shouldNotFreeze() - && mState.getCurAdj() >= ProcessList.FREEZER_CUTOFF_ADJ; + && mState.getCurAdj() >= mService.mConstants.FREEZER_CUTOFF_ADJ; } public void forEachConnectionHost(Consumer<ProcessRecord> consumer) { diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 87f87c76725e..c82933c5069e 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -158,6 +158,7 @@ public class SettingsToPropertiesMapper { "aoc", "app_widgets", "arc_next", + "art_cloud", "art_mainline", "art_performance", "attack_tools", diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index c31b9ef60bd2..70f2a8e1dd1b 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -160,6 +160,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiConsumer; import java.util.function.Consumer; /** @@ -176,6 +177,9 @@ import java.util.function.Consumer; class UserController implements Handler.Callback { private static final String TAG = TAG_WITH_CLASS_NAME ? "UserController" : TAG_AM; + // Amount of time we wait for observers to handle onBeforeUserSwitching, before crashing system. + static final int DEFAULT_BEFORE_USER_SWITCH_TIMEOUT_MS = 20 * 1000; + // Amount of time we wait for observers to handle a user switch before // giving up on them and dismissing the user switching dialog. static final int DEFAULT_USER_SWITCH_TIMEOUT_MS = 3 * 1000; @@ -1920,8 +1924,14 @@ class UserController implements Handler.Callback { return false; } - mHandler.post(() -> startUserInternalOnHandler(userId, oldUserId, userStartMode, - unlockListener, callingUid, callingPid)); + final Runnable continueStartUserInternal = () -> continueStartUserInternal(userInfo, + oldUserId, userStartMode, unlockListener, callingUid, callingPid); + if (foreground) { + mHandler.post(() -> dispatchOnBeforeUserSwitching(userId, () -> + mHandler.post(continueStartUserInternal))); + } else { + continueStartUserInternal.run(); + } } finally { Binder.restoreCallingIdentity(ident); } @@ -1929,11 +1939,11 @@ class UserController implements Handler.Callback { return true; } - private void startUserInternalOnHandler(int userId, int oldUserId, int userStartMode, + private void continueStartUserInternal(UserInfo userInfo, int oldUserId, int userStartMode, IProgressListener unlockListener, int callingUid, int callingPid) { final TimingsTraceAndSlog t = new TimingsTraceAndSlog(); final boolean foreground = userStartMode == USER_START_MODE_FOREGROUND; - final UserInfo userInfo = getUserInfo(userId); + final int userId = userInfo.id; boolean needStart = false; boolean updateUmState = false; @@ -1995,7 +2005,6 @@ class UserController implements Handler.Callback { // it should be moved outside, but for now it's not as there are many calls to // external components here afterwards updateProfileRelatedCaches(); - dispatchOnBeforeUserSwitching(userId); mInjector.getWindowManager().setCurrentUser(userId); mInjector.reportCurWakefulnessUsageEvent(); // Once the internal notion of the active user has switched, we lock the device @@ -2296,25 +2305,40 @@ class UserController implements Handler.Callback { mUserSwitchObservers.finishBroadcast(); } - private void dispatchOnBeforeUserSwitching(@UserIdInt int newUserId) { + private void dispatchOnBeforeUserSwitching(@UserIdInt int newUserId, Runnable onComplete) { final TimingsTraceAndSlog t = new TimingsTraceAndSlog(); t.traceBegin("dispatchOnBeforeUserSwitching-" + newUserId); - final int observerCount = mUserSwitchObservers.beginBroadcast(); - for (int i = 0; i < observerCount; i++) { - final String name = "#" + i + " " + mUserSwitchObservers.getBroadcastCookie(i); - t.traceBegin("onBeforeUserSwitching-" + name); + final AtomicBoolean isSuccessful = new AtomicBoolean(false); + startTimeoutForOnBeforeUserSwitching(isSuccessful); + informUserSwitchObservers((observer, callback) -> { try { - mUserSwitchObservers.getBroadcastItem(i).onBeforeUserSwitching(newUserId); + observer.onBeforeUserSwitching(newUserId, callback); } catch (RemoteException e) { - // Ignore - } finally { - t.traceEnd(); + // ignore } - } - mUserSwitchObservers.finishBroadcast(); + }, () -> { + isSuccessful.set(true); + onComplete.run(); + }, "onBeforeUserSwitching"); t.traceEnd(); } + private void startTimeoutForOnBeforeUserSwitching(AtomicBoolean isSuccessful) { + mHandler.postDelayed(() -> { + if (isSuccessful.get()) { + return; + } + String unresponsiveObservers; + synchronized (mLock) { + unresponsiveObservers = String.join(", ", mCurWaitingUserSwitchCallbacks); + } + throw new RuntimeException("Timeout on dispatchOnBeforeUserSwitching. " + + "These UserSwitchObservers did not respond in " + + DEFAULT_BEFORE_USER_SWITCH_TIMEOUT_MS + "ms: " + unresponsiveObservers + "."); + }, DEFAULT_BEFORE_USER_SWITCH_TIMEOUT_MS); + } + + /** Called on handler thread */ @VisibleForTesting void dispatchUserSwitchComplete(@UserIdInt int oldUserId, @UserIdInt int newUserId) { @@ -2527,70 +2551,76 @@ class UserController implements Handler.Callback { t.traceBegin("dispatchUserSwitch-" + oldUserId + "-to-" + newUserId); EventLog.writeEvent(EventLogTags.UC_DISPATCH_USER_SWITCH, oldUserId, newUserId); + uss.switching = true; + informUserSwitchObservers((observer, callback) -> { + try { + observer.onUserSwitching(newUserId, callback); + } catch (RemoteException e) { + // ignore + } + }, () -> { + synchronized (mLock) { + sendContinueUserSwitchLU(uss, oldUserId, newUserId); + } + }, "onUserSwitching"); + t.traceEnd(); + } + void informUserSwitchObservers(BiConsumer<IUserSwitchObserver, IRemoteCallback> consumer, + final Runnable onComplete, String trace) { final int observerCount = mUserSwitchObservers.beginBroadcast(); - if (observerCount > 0) { - final ArraySet<String> curWaitingUserSwitchCallbacks = new ArraySet<>(); + if (observerCount == 0) { + onComplete.run(); + mUserSwitchObservers.finishBroadcast(); + return; + } + final ArraySet<String> curWaitingUserSwitchCallbacks = new ArraySet<>(); + synchronized (mLock) { + mCurWaitingUserSwitchCallbacks = curWaitingUserSwitchCallbacks; + } + final AtomicInteger waitingCallbacksCount = new AtomicInteger(observerCount); + final long userSwitchTimeoutMs = getUserSwitchTimeoutMs(); + final long dispatchStartedTime = SystemClock.elapsedRealtime(); + for (int i = 0; i < observerCount; i++) { + final long dispatchStartedTimeForObserver = SystemClock.elapsedRealtime(); + // Prepend with unique prefix to guarantee that keys are unique + final String name = "#" + i + " " + mUserSwitchObservers.getBroadcastCookie(i); synchronized (mLock) { - uss.switching = true; - mCurWaitingUserSwitchCallbacks = curWaitingUserSwitchCallbacks; - } - final AtomicInteger waitingCallbacksCount = new AtomicInteger(observerCount); - final long userSwitchTimeoutMs = getUserSwitchTimeoutMs(); - final long dispatchStartedTime = SystemClock.elapsedRealtime(); - for (int i = 0; i < observerCount; i++) { - final long dispatchStartedTimeForObserver = SystemClock.elapsedRealtime(); - try { - // Prepend with unique prefix to guarantee that keys are unique - final String name = "#" + i + " " + mUserSwitchObservers.getBroadcastCookie(i); + curWaitingUserSwitchCallbacks.add(name); + } + final IRemoteCallback callback = new IRemoteCallback.Stub() { + @Override + public void sendResult(Bundle data) throws RemoteException { + asyncTraceEnd(trace + "-" + name, 0); synchronized (mLock) { - curWaitingUserSwitchCallbacks.add(name); - } - final IRemoteCallback callback = new IRemoteCallback.Stub() { - @Override - public void sendResult(Bundle data) throws RemoteException { - asyncTraceEnd("onUserSwitching-" + name, newUserId); - synchronized (mLock) { - long delayForObserver = SystemClock.elapsedRealtime() - - dispatchStartedTimeForObserver; - if (delayForObserver > LONG_USER_SWITCH_OBSERVER_WARNING_TIME_MS) { - Slogf.w(TAG, "User switch slowed down by observer " + name - + ": result took " + delayForObserver - + " ms to process."); - } - - long totalDelay = SystemClock.elapsedRealtime() - - dispatchStartedTime; - if (totalDelay > userSwitchTimeoutMs) { - Slogf.e(TAG, "User switch timeout: observer " + name - + "'s result was received " + totalDelay - + " ms after dispatchUserSwitch."); - } - - curWaitingUserSwitchCallbacks.remove(name); - // Continue switching if all callbacks have been notified and - // user switching session is still valid - if (waitingCallbacksCount.decrementAndGet() == 0 - && (curWaitingUserSwitchCallbacks - == mCurWaitingUserSwitchCallbacks)) { - sendContinueUserSwitchLU(uss, oldUserId, newUserId); - } - } + long delayForObserver = SystemClock.elapsedRealtime() + - dispatchStartedTimeForObserver; + if (delayForObserver > LONG_USER_SWITCH_OBSERVER_WARNING_TIME_MS) { + Slogf.w(TAG, "User switch slowed down by observer " + name + + ": result took " + delayForObserver + + " ms to process. " + trace); } - }; - asyncTraceBegin("onUserSwitching-" + name, newUserId); - mUserSwitchObservers.getBroadcastItem(i).onUserSwitching(newUserId, callback); - } catch (RemoteException e) { - // Ignore + long totalDelay = SystemClock.elapsedRealtime() - dispatchStartedTime; + if (totalDelay > userSwitchTimeoutMs) { + Slogf.e(TAG, "User switch timeout: observer " + name + + "'s result was received " + totalDelay + + " ms after dispatchUserSwitch. " + trace); + } + curWaitingUserSwitchCallbacks.remove(name); + // Continue switching if all callbacks have been notified and + // user switching session is still valid + if (waitingCallbacksCount.decrementAndGet() == 0 + && (curWaitingUserSwitchCallbacks + == mCurWaitingUserSwitchCallbacks)) { + onComplete.run(); + } + } } - } - } else { - synchronized (mLock) { - sendContinueUserSwitchLU(uss, oldUserId, newUserId); - } + }; + asyncTraceBegin(trace + "-" + name, 0); + consumer.accept(mUserSwitchObservers.getBroadcastItem(i), callback); } mUserSwitchObservers.finishBroadcast(); - t.traceEnd(); // end dispatchUserSwitch- } @GuardedBy("mLock") diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index 06c586f5e9c2..295e0443371d 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -3191,7 +3191,7 @@ public class AppOpsService extends IAppOpsService.Stub { resolveProxyPackageName, proxyAttributionTag, proxyVirtualDeviceId, Process.INVALID_UID, null, null, Context.DEVICE_ID_DEFAULT, proxyFlags, !isProxyTrusted, - "proxy " + message, shouldCollectMessage); + "proxy " + message, shouldCollectMessage, 1); if (proxyReturn.getOpMode() != AppOpsManager.MODE_ALLOWED) { return new SyncNotedAppOp(proxyReturn.getOpMode(), code, proxiedAttributionTag, proxiedPackageName); @@ -3210,7 +3210,20 @@ public class AppOpsService extends IAppOpsService.Stub { return noteOperationUnchecked(code, proxiedUid, resolveProxiedPackageName, proxiedAttributionTag, proxiedVirtualDeviceId, proxyUid, resolveProxyPackageName, proxyAttributionTag, proxyVirtualDeviceId, proxiedFlags, shouldCollectAsyncNotedOp, - message, shouldCollectMessage); + message, shouldCollectMessage, 1); + } + + @Override + public void noteOperationsInBatch(Map batchedNoteOps) { + for (var entry : ((Map<AppOpsManager.NotedOp, Integer>) batchedNoteOps).entrySet()) { + AppOpsManager.NotedOp notedOp = entry.getKey(); + int notedCount = entry.getValue(); + mCheckOpsDelegateDispatcher.noteOperation( + notedOp.getOp(), notedOp.getUid(), notedOp.getPackageName(), + notedOp.getAttributionTag(), notedOp.getVirtualDeviceId(), + notedOp.getShouldCollectAsyncNotedOp(), notedOp.getMessage(), + notedOp.getShouldCollectMessage(), notedCount); + } } @Override @@ -3228,7 +3241,7 @@ public class AppOpsService extends IAppOpsService.Stub { } return mCheckOpsDelegateDispatcher.noteOperation(code, uid, packageName, attributionTag, Context.DEVICE_ID_DEFAULT, shouldCollectAsyncNotedOp, message, - shouldCollectMessage); + shouldCollectMessage, 1); } @Override @@ -3237,13 +3250,12 @@ public class AppOpsService extends IAppOpsService.Stub { String message, boolean shouldCollectMessage) { return mCheckOpsDelegateDispatcher.noteOperation(code, uid, packageName, attributionTag, virtualDeviceId, shouldCollectAsyncNotedOp, message, - shouldCollectMessage); + shouldCollectMessage, 1); } private SyncNotedAppOp noteOperationImpl(int code, int uid, @Nullable String packageName, - @Nullable String attributionTag, int virtualDeviceId, - boolean shouldCollectAsyncNotedOp, @Nullable String message, - boolean shouldCollectMessage) { + @Nullable String attributionTag, int virtualDeviceId, boolean shouldCollectAsyncNotedOp, + @Nullable String message, boolean shouldCollectMessage, int notedCount) { String resolvedPackageName; if (!shouldUseNewCheckOp()) { verifyIncomingUid(uid); @@ -3278,14 +3290,14 @@ public class AppOpsService extends IAppOpsService.Stub { return noteOperationUnchecked(code, uid, resolvedPackageName, attributionTag, virtualDeviceId, Process.INVALID_UID, null, null, Context.DEVICE_ID_DEFAULT, AppOpsManager.OP_FLAG_SELF, shouldCollectAsyncNotedOp, - message, shouldCollectMessage); + message, shouldCollectMessage, notedCount); } private SyncNotedAppOp noteOperationUnchecked(int code, int uid, @NonNull String packageName, @Nullable String attributionTag, int virtualDeviceId, int proxyUid, String proxyPackageName, @Nullable String proxyAttributionTag, int proxyVirtualDeviceId, @OpFlags int flags, boolean shouldCollectAsyncNotedOp, @Nullable String message, - boolean shouldCollectMessage) { + boolean shouldCollectMessage, int notedCount) { PackageVerificationResult pvr; try { pvr = verifyAndGetBypass(uid, packageName, attributionTag, proxyPackageName); @@ -3388,11 +3400,11 @@ public class AppOpsService extends IAppOpsService.Stub { virtualDeviceId, flags, AppOpsManager.MODE_ALLOWED); attributedOp.accessed(proxyUid, proxyPackageName, proxyAttributionTag, - getPersistentId(proxyVirtualDeviceId), uidState.getState(), flags); + getPersistentId(proxyVirtualDeviceId), uidState.getState(), flags, notedCount); if (shouldCollectAsyncNotedOp) { collectAsyncNotedOp(uid, packageName, code, attributionTag, flags, message, - shouldCollectMessage); + shouldCollectMessage, notedCount); } return new SyncNotedAppOp(AppOpsManager.MODE_ALLOWED, code, attributionTag, @@ -3551,7 +3563,7 @@ public class AppOpsService extends IAppOpsService.Stub { */ private void collectAsyncNotedOp(int uid, @NonNull String packageName, int opCode, @Nullable String attributionTag, @OpFlags int flags, @NonNull String message, - boolean shouldCollectMessage) { + boolean shouldCollectMessage, int notedCount) { Objects.requireNonNull(message); int callingUid = Binder.getCallingUid(); @@ -3559,42 +3571,51 @@ public class AppOpsService extends IAppOpsService.Stub { final long token = Binder.clearCallingIdentity(); try { synchronized (this) { - Pair<String, Integer> key = getAsyncNotedOpsKey(packageName, uid); - - RemoteCallbackList<IAppOpsAsyncNotedCallback> callbacks = mAsyncOpWatchers.get(key); - AsyncNotedAppOp asyncNotedOp = new AsyncNotedAppOp(opCode, callingUid, - attributionTag, message, System.currentTimeMillis()); - final boolean[] wasNoteForwarded = {false}; - if ((flags & (OP_FLAG_SELF | OP_FLAG_TRUSTED_PROXIED)) != 0 && shouldCollectMessage) { reportRuntimeAppOpAccessMessageAsyncLocked(uid, packageName, opCode, attributionTag, message); } - if (callbacks != null) { + Pair<String, Integer> key = getAsyncNotedOpsKey(packageName, uid); + RemoteCallbackList<IAppOpsAsyncNotedCallback> callbacks = mAsyncOpWatchers.get(key); + if (callbacks == null) { + return; + } + + final boolean[] wasNoteForwarded = {false}; + if (Flags.rateLimitBatchedNoteOpAsyncCallbacksEnabled()) { + notedCount = 1; + } + + for (int i = 0; i < notedCount; i++) { + AsyncNotedAppOp asyncNotedOp = new AsyncNotedAppOp(opCode, callingUid, + attributionTag, message, System.currentTimeMillis()); + wasNoteForwarded[0] = false; callbacks.broadcast((cb) -> { try { cb.opNoted(asyncNotedOp); wasNoteForwarded[0] = true; } catch (RemoteException e) { Slog.e(TAG, - "Could not forward noteOp of " + opCode + " to " + packageName + "Could not forward noteOp of " + opCode + " to " + + packageName + "/" + uid + "(" + attributionTag + ")", e); } }); - } - if (!wasNoteForwarded[0]) { - ArrayList<AsyncNotedAppOp> unforwardedOps = mUnforwardedAsyncNotedOps.get(key); - if (unforwardedOps == null) { - unforwardedOps = new ArrayList<>(1); - mUnforwardedAsyncNotedOps.put(key, unforwardedOps); - } + if (!wasNoteForwarded[0]) { + ArrayList<AsyncNotedAppOp> unforwardedOps = mUnforwardedAsyncNotedOps.get( + key); + if (unforwardedOps == null) { + unforwardedOps = new ArrayList<>(1); + mUnforwardedAsyncNotedOps.put(key, unforwardedOps); + } - unforwardedOps.add(asyncNotedOp); - if (unforwardedOps.size() > MAX_UNFORWARDED_OPS) { - unforwardedOps.remove(0); + unforwardedOps.add(asyncNotedOp); + if (unforwardedOps.size() > MAX_UNFORWARDED_OPS) { + unforwardedOps.remove(0); + } } } } @@ -4026,7 +4047,7 @@ public class AppOpsService extends IAppOpsService.Stub { if (shouldCollectAsyncNotedOp && !isRestricted) { collectAsyncNotedOp(uid, packageName, code, attributionTag, AppOpsManager.OP_FLAG_SELF, - message, shouldCollectMessage); + message, shouldCollectMessage, 1); } return new SyncNotedAppOp(isRestricted ? MODE_IGNORED : MODE_ALLOWED, code, attributionTag, @@ -7574,34 +7595,36 @@ public class AppOpsService extends IAppOpsService.Stub { public SyncNotedAppOp noteOperation(int code, int uid, String packageName, String attributionTag, int virtualDeviceId, boolean shouldCollectAsyncNotedOp, - String message, boolean shouldCollectMessage) { + String message, boolean shouldCollectMessage, int notedCount) { if (mPolicy != null) { if (mCheckOpsDelegate != null) { return mPolicy.noteOperation(code, uid, packageName, attributionTag, virtualDeviceId, shouldCollectAsyncNotedOp, message, - shouldCollectMessage, this::noteDelegateOperationImpl + shouldCollectMessage, notedCount, this::noteDelegateOperationImpl ); } else { return mPolicy.noteOperation(code, uid, packageName, attributionTag, virtualDeviceId, shouldCollectAsyncNotedOp, message, - shouldCollectMessage, AppOpsService.this::noteOperationImpl + shouldCollectMessage, notedCount, AppOpsService.this::noteOperationImpl ); } } else if (mCheckOpsDelegate != null) { return noteDelegateOperationImpl(code, uid, packageName, attributionTag, - virtualDeviceId, shouldCollectAsyncNotedOp, message, shouldCollectMessage); + virtualDeviceId, shouldCollectAsyncNotedOp, message, shouldCollectMessage, + notedCount); } return noteOperationImpl(code, uid, packageName, attributionTag, - virtualDeviceId, shouldCollectAsyncNotedOp, message, shouldCollectMessage); + virtualDeviceId, shouldCollectAsyncNotedOp, message, shouldCollectMessage, + notedCount); } private SyncNotedAppOp noteDelegateOperationImpl(int code, int uid, @Nullable String packageName, @Nullable String featureId, int virtualDeviceId, boolean shouldCollectAsyncNotedOp, @Nullable String message, - boolean shouldCollectMessage) { + boolean shouldCollectMessage, int notedCount) { return mCheckOpsDelegate.noteOperation(code, uid, packageName, featureId, virtualDeviceId, shouldCollectAsyncNotedOp, message, shouldCollectMessage, - AppOpsService.this::noteOperationImpl + notedCount, AppOpsService.this::noteOperationImpl ); } diff --git a/services/core/java/com/android/server/appop/AttributedOp.java b/services/core/java/com/android/server/appop/AttributedOp.java index 314664b0a79d..4d114b4ad4ac 100644 --- a/services/core/java/com/android/server/appop/AttributedOp.java +++ b/services/core/java/com/android/server/appop/AttributedOp.java @@ -100,10 +100,12 @@ final class AttributedOp { * @param proxyDeviceId The device Id of the proxy * @param uidState UID state of the app noteOp/startOp was called for * @param flags OpFlags of the call + * @param accessCount The number of times the op is accessed */ public void accessed(int proxyUid, @Nullable String proxyPackageName, @Nullable String proxyAttributionTag, @Nullable String proxyDeviceId, - @AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags) { + @AppOpsManager.UidState int uidState, @AppOpsManager.OpFlags int flags, + int accessCount) { long accessTime = System.currentTimeMillis(); accessed(accessTime, -1, proxyUid, proxyPackageName, proxyAttributionTag, proxyDeviceId, uidState, flags); @@ -111,7 +113,7 @@ final class AttributedOp { mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, parent.packageName, persistentDeviceId, tag, uidState, flags, accessTime, AppOpsManager.ATTRIBUTION_FLAGS_NONE, AppOpsManager.ATTRIBUTION_CHAIN_ID_NONE, - DiscreteRegistry.ACCESS_TYPE_NOTE_OP); + DiscreteRegistry.ACCESS_TYPE_NOTE_OP, accessCount); } /** @@ -255,7 +257,7 @@ final class AttributedOp { if (isStarted) { mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, parent.packageName, persistentDeviceId, tag, uidState, flags, startTime, - attributionFlags, attributionChainId, DiscreteRegistry.ACCESS_TYPE_START_OP); + attributionFlags, attributionChainId, DiscreteRegistry.ACCESS_TYPE_START_OP, 1); } } @@ -451,7 +453,7 @@ final class AttributedOp { mAppOpsService.mHistoricalRegistry.incrementOpAccessedCount(parent.op, parent.uid, parent.packageName, persistentDeviceId, tag, event.getUidState(), event.getFlags(), startTime, event.getAttributionFlags(), - event.getAttributionChainId(), DiscreteRegistry.ACCESS_TYPE_RESUME_OP); + event.getAttributionChainId(), DiscreteRegistry.ACCESS_TYPE_RESUME_OP, 1); if (shouldSendActive) { mAppOpsService.scheduleOpActiveChangedIfNeededLocked(parent.op, parent.uid, parent.packageName, tag, event.getVirtualDeviceId(), true, diff --git a/services/core/java/com/android/server/appop/HistoricalRegistry.java b/services/core/java/com/android/server/appop/HistoricalRegistry.java index 6b0253864e2b..5e67f26ba1f6 100644 --- a/services/core/java/com/android/server/appop/HistoricalRegistry.java +++ b/services/core/java/com/android/server/appop/HistoricalRegistry.java @@ -475,7 +475,7 @@ final class HistoricalRegistry { @NonNull String deviceId, @Nullable String attributionTag, @UidState int uidState, @OpFlags int flags, long accessTime, @AppOpsManager.AttributionFlags int attributionFlags, int attributionChainId, - @DiscreteRegistry.AccessType int accessType) { + @DiscreteRegistry.AccessType int accessType, int accessCount) { synchronized (mInMemoryLock) { if (mMode == AppOpsManager.HISTORICAL_MODE_ENABLED_ACTIVE) { if (!isPersistenceInitializedMLocked()) { @@ -484,7 +484,7 @@ final class HistoricalRegistry { } getUpdatedPendingHistoricalOpsMLocked( System.currentTimeMillis()).increaseAccessCount(op, uid, packageName, - attributionTag, uidState, flags, 1); + attributionTag, uidState, flags, accessCount); mDiscreteRegistry.recordDiscreteAccess(uid, packageName, deviceId, op, attributionTag, flags, uidState, accessTime, -1, attributionFlags, diff --git a/services/core/java/com/android/server/audio/FadeOutManager.java b/services/core/java/com/android/server/audio/FadeOutManager.java index 4d5bce559a91..fedfe511b747 100644 --- a/services/core/java/com/android/server/audio/FadeOutManager.java +++ b/services/core/java/com/android/server/audio/FadeOutManager.java @@ -199,7 +199,9 @@ public final class FadeOutManager { for (AudioPlaybackConfiguration apc : players) { final VolumeShaper.Configuration volShaper = mFadeConfigurations.getFadeOutVolumeShaperConfig(apc.getAudioAttributes()); - fa.addFade(apc, /* skipRamp= */ false, volShaper); + if (volShaper != null) { + fa.addFade(apc, /* skipRamp= */ false, volShaper); + } } } } @@ -249,7 +251,7 @@ public final class FadeOutManager { final VolumeShaper.Configuration volShaper = mFadeConfigurations.getFadeOutVolumeShaperConfig(apc.getAudioAttributes()); final FadedOutApp fa = mUidToFadedAppsMap.get(apc.getClientUid()); - if (fa == null) { + if (fa == null || volShaper == null) { return; } fa.addFade(apc, /* skipRamp= */ true, volShaper); diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index bad5b8b9567a..737820b4a788 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -119,6 +119,7 @@ import android.os.Handler; import android.os.HandlerExecutor; import android.os.IBinder; import android.os.IBinder.DeathRecipient; +import android.os.IBinder.FrozenStateChangeCallback; import android.os.IThermalService; import android.os.Looper; import android.os.Message; @@ -272,6 +273,7 @@ public final class DisplayManagerService extends SystemService { private static final int MSG_DELIVER_DISPLAY_EVENT_FRAME_RATE_OVERRIDE = 7; private static final int MSG_DELIVER_DISPLAY_GROUP_EVENT = 8; private static final int MSG_RECEIVED_DEVICE_STATE = 9; + private static final int MSG_DISPATCH_PENDING_PROCESS_EVENTS = 10; private static final int[] EMPTY_ARRAY = new int[0]; private static final HdrConversionMode HDR_CONVERSION_MODE_UNSUPPORTED = new HdrConversionMode( HDR_CONVERSION_UNSUPPORTED); @@ -286,7 +288,6 @@ public final class DisplayManagerService extends SystemService { private InputManagerInternal mInputManagerInternal; private ActivityManagerInternal mActivityManagerInternal; private final UidImportanceListener mUidImportanceListener = new UidImportanceListener(); - private final DisplayFrozenProcessListener mDisplayFrozenProcessListener; @Nullable private IMediaProjectionManager mProjectionService; @@ -630,7 +631,6 @@ public final class DisplayManagerService extends SystemService { mFlags = injector.getFlags(); mHandler = new DisplayManagerHandler(displayThreadLooper); mHandlerExecutor = new HandlerExecutor(mHandler); - mDisplayFrozenProcessListener = new DisplayFrozenProcessListener(); mUiHandler = UiThread.getHandler(); mDisplayDeviceRepo = new DisplayDeviceRepository(mSyncRoot, mPersistentDataStore); mLogicalDisplayMapper = new LogicalDisplayMapper(mContext, @@ -1165,31 +1165,11 @@ public final class DisplayManagerService extends SystemService { } } - private class DisplayFrozenProcessListener - implements ActivityManagerInternal.FrozenProcessListener { - public void onProcessFrozen(int pid) { - synchronized (mSyncRoot) { - CallbackRecord callback = mCallbacks.get(pid); - if (callback == null) { - return; - } - callback.setFrozen(true); - } - } - - public void onProcessUnfrozen(int pid) { - // First, see if there is a callback associated with this pid. If there's no - // callback, then there is nothing to do. - CallbackRecord callback; - synchronized (mSyncRoot) { - callback = mCallbacks.get(pid); - if (callback == null) { - return; - } - callback.setFrozen(false); - } - // Attempt to dispatch pending events if the process is coming out of frozen. + private void dispatchPendingProcessEvents(@NonNull Object cb) { + if (cb instanceof CallbackRecord callback) { callback.dispatchPending(); + } else { + Slog.wtf(TAG, "not a callback: " + cb); } } @@ -2397,9 +2377,13 @@ public final class DisplayManagerService extends SystemService { // We don't bother invalidating the display info caches here because any changes to the // display info will trigger a cache invalidation inside of LogicalDisplay before we hit // this point. - sendDisplayEventIfEnabledLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_CHANGED); + sendDisplayEventIfEnabledLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_BASIC_CHANGED); applyDisplayChangedLocked(display); + + if (mDisplayTopologyCoordinator != null) { + mDisplayTopologyCoordinator.onDisplayChanged(display.getDisplayInfoLocked()); + } } private void applyDisplayChangedLocked(@NonNull LogicalDisplay display) { @@ -2643,7 +2627,8 @@ public final class DisplayManagerService extends SystemService { private void updateCanHostTasksIfNeededLocked(LogicalDisplay display) { if (display.setCanHostTasksLocked(!mMirrorBuiltInDisplay)) { - sendDisplayEventIfEnabledLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_CHANGED); + sendDisplayEventIfEnabledLocked(display, + DisplayManagerGlobal.EVENT_DISPLAY_BASIC_CHANGED); } } @@ -3474,7 +3459,7 @@ public final class DisplayManagerService extends SystemService { private void sendDisplayEventFrameRateOverrideLocked(int displayId) { Message msg = mHandler.obtainMessage(MSG_DELIVER_DISPLAY_EVENT_FRAME_RATE_OVERRIDE, - displayId, DisplayManagerGlobal.EVENT_DISPLAY_CHANGED); + displayId, DisplayManagerGlobal.EVENT_DISPLAY_BASIC_CHANGED); mHandler.sendMessage(msg); } @@ -4047,6 +4032,9 @@ public final class DisplayManagerService extends SystemService { deliverDisplayGroupEvent(msg.arg1, msg.arg2); break; + case MSG_DISPATCH_PENDING_PROCESS_EVENTS: + dispatchPendingProcessEvents(msg.obj); + break; } } } @@ -4061,7 +4049,7 @@ public final class DisplayManagerService extends SystemService { handleLogicalDisplayAddedLocked(display); break; - case LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_CHANGED: + case LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_BASIC_CHANGED: handleLogicalDisplayChangedLocked(display); break; @@ -4114,7 +4102,7 @@ public final class DisplayManagerService extends SystemService { } } - private final class CallbackRecord implements DeathRecipient { + private final class CallbackRecord implements DeathRecipient, FrozenStateChangeCallback { public final int mPid; public final int mUid; private final IDisplayManagerCallback mCallback; @@ -4137,6 +4125,8 @@ public final class DisplayManagerService extends SystemService { private boolean mCached; @GuardedBy("mCallback") private boolean mFrozen; + @GuardedBy("mCallback") + private boolean mAlive; CallbackRecord(int pid, int uid, @NonNull IDisplayManagerCallback callback, @InternalEventFlag long internalEventFlagsMask) { @@ -4146,18 +4136,20 @@ public final class DisplayManagerService extends SystemService { mInternalEventFlagsMask = new AtomicLong(internalEventFlagsMask); mCached = false; mFrozen = false; + mAlive = true; if (deferDisplayEventsWhenFrozen()) { - // Some CallbackRecords are registered very early in system boot, before - // mActivityManagerInternal is initialized. If mActivityManagerInternal is null, - // do not register the frozen process listener. However, do verify that all such - // registrations are for the self pid (which can never be frozen, so the frozen - // process listener does not matter). - if (mActivityManagerInternal != null) { - mActivityManagerInternal.addFrozenProcessListener(pid, mHandlerExecutor, - mDisplayFrozenProcessListener); - } else if (Process.myPid() != pid) { - Slog.e(TAG, "DisplayListener registered too early"); + try { + callback.asBinder().addFrozenStateChangeCallback(this); + } catch (UnsupportedOperationException e) { + // Ignore the exception. The callback is not supported on this platform or on + // this binder. The callback is never supported for local binders. There is + // no error: the UID importance listener will still operate. A log message is + // provided for debug. + Slog.v(TAG, "FrozenStateChangeCallback not supported for pid " + mPid); + } catch (RemoteException e) { + // This is unexpected. Just give up. + throw new RuntimeException(e); } } @@ -4182,7 +4174,7 @@ public final class DisplayManagerService extends SystemService { */ @GuardedBy("mCallback") private boolean hasPendingAndIsReadyLocked() { - return isReadyLocked() && mPendingEvents != null && !mPendingEvents.isEmpty(); + return isReadyLocked() && mPendingEvents != null && !mPendingEvents.isEmpty() && mAlive; } /** @@ -4190,7 +4182,7 @@ public final class DisplayManagerService extends SystemService { * receive events and there are pending events to be delivered. * This is only used if {@link deferDisplayEventsWhenFrozen()} is true. */ - public boolean setFrozen(boolean frozen) { + private boolean setFrozen(boolean frozen) { synchronized (mCallback) { mFrozen = frozen; return hasPendingAndIsReadyLocked(); @@ -4211,6 +4203,9 @@ public final class DisplayManagerService extends SystemService { @Override public void binderDied() { + synchronized (mCallback) { + mAlive = false; + } if (DEBUG || extraLogging(mPackageName)) { Slog.d(TAG, "Display listener for pid " + mPid + " died."); } @@ -4221,6 +4216,14 @@ public final class DisplayManagerService extends SystemService { onCallbackDied(this); } + @Override + public void onFrozenStateChanged(@NonNull IBinder who, int state) { + if (setFrozen(state == FrozenStateChangeCallback.STATE_FROZEN)) { + Message msg = mHandler.obtainMessage(MSG_DISPATCH_PENDING_PROCESS_EVENTS, this); + mHandler.sendMessage(msg); + } + } + /** * @return {@code false} if RemoteException happens; otherwise {@code true} for * success. This returns true even if the event was deferred because the remote client is @@ -4286,8 +4289,9 @@ public final class DisplayManagerService extends SystemService { switch (event) { case DisplayManagerGlobal.EVENT_DISPLAY_ADDED: return (mask & DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED) != 0; - case DisplayManagerGlobal.EVENT_DISPLAY_CHANGED: - return (mask & DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_CHANGED) != 0; + case DisplayManagerGlobal.EVENT_DISPLAY_BASIC_CHANGED: + return (mask & DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED) + != 0; case DisplayManagerGlobal.EVENT_DISPLAY_BRIGHTNESS_CHANGED: return (mask & DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BRIGHTNESS_CHANGED) @@ -4386,7 +4390,7 @@ public final class DisplayManagerService extends SystemService { // This is only used if {@link deferDisplayEventsWhenFrozen()} is true. public boolean dispatchPending() { synchronized (mCallback) { - if (mPendingEvents == null || mPendingEvents.isEmpty()) { + if (mPendingEvents == null || mPendingEvents.isEmpty() || !mAlive) { return true; } if (!isReadyLocked()) { @@ -4542,7 +4546,8 @@ public final class DisplayManagerService extends SystemService { public void registerCallback(IDisplayManagerCallback callback) { registerCallbackWithEventMask(callback, DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED - | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_CHANGED + | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED + | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REMOVED); } @@ -6057,6 +6062,7 @@ public final class DisplayManagerService extends SystemService { * Return the value of the pause */ private static boolean deferDisplayEventsWhenFrozen() { - return com.android.server.am.Flags.deferDisplayEventsWhenFrozen(); + return android.os.Flags.binderFrozenStateChangeCallback() + && com.android.server.am.Flags.deferDisplayEventsWhenFrozen(); } } diff --git a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java index 5b78726cc421..461a9f3f2a0d 100644 --- a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java +++ b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java @@ -85,13 +85,26 @@ class DisplayTopologyCoordinator { } /** + * Update the topology with display changes. + * @param info The new display info + */ + void onDisplayChanged(DisplayInfo info) { + synchronized (mSyncRoot) { + if (mTopology.updateDisplay(info.displayId, getWidth(info), getHeight(info))) { + sendTopologyUpdateLocked(); + } + } + } + + /** * Remove a display from the topology. * @param displayId The logical display ID */ void onDisplayRemoved(int displayId) { synchronized (mSyncRoot) { - mTopology.removeDisplay(displayId); - sendTopologyUpdateLocked(); + if (mTopology.removeDisplay(displayId)) { + sendTopologyUpdateLocked(); + } } } diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index 79592a656409..006921572977 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -81,7 +81,7 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { public static final int LOGICAL_DISPLAY_EVENT_BASE = 0; public static final int LOGICAL_DISPLAY_EVENT_ADDED = 1 << 0; - public static final int LOGICAL_DISPLAY_EVENT_CHANGED = 1 << 1; + public static final int LOGICAL_DISPLAY_EVENT_BASIC_CHANGED = 1 << 1; public static final int LOGICAL_DISPLAY_EVENT_REMOVED = 1 << 2; public static final int LOGICAL_DISPLAY_EVENT_SWAPPED = 1 << 3; public static final int LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED = 1 << 4; @@ -172,9 +172,7 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { /** * Has an entry for every logical display that the rest of the system has been notified about. - * Any entry in here requires us to send a {@link LOGICAL_DISPLAY_EVENT_REMOVED} event when it - * is deleted or {@link LOGICAL_DISPLAY_EVENT_CHANGED} when it is changed. The values are any - * of the {@code UPDATE_STATE_*} constant types. + * The values are any of the {@code UPDATE_STATE_*} constant types. */ private final SparseIntArray mUpdatedLogicalDisplays = new SparseIntArray(); @@ -811,7 +809,8 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { final boolean isCurrentlyEnabled = display.isEnabledLocked(); int logicalDisplayEventMask = mLogicalDisplaysToUpdate .get(displayId, LOGICAL_DISPLAY_EVENT_BASE); - + boolean hasBasicInfoChanged = + !mTempDisplayInfo.equals(newDisplayInfo, /* compareRefreshRate */ false); // The display is no longer valid and needs to be removed. if (!display.isValidLocked()) { // Remove from group @@ -863,19 +862,28 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { int event = isCurrentlyEnabled ? LOGICAL_DISPLAY_EVENT_ADDED : LOGICAL_DISPLAY_EVENT_REMOVED; logicalDisplayEventMask |= event; - } else if (wasDirty || !mTempDisplayInfo.equals(newDisplayInfo)) { + } else if (wasDirty) { // If only the hdr/sdr ratio changed, then send just the event for that case if ((diff == DisplayDeviceInfo.DIFF_HDR_SDR_RATIO)) { logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_HDR_SDR_RATIO_CHANGED; } else { - logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_CHANGED; + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_BASIC_CHANGED + | LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED + | LOGICAL_DISPLAY_EVENT_STATE_CHANGED; } + } else if (hasBasicInfoChanged + || mTempDisplayInfo.getRefreshRate() != newDisplayInfo.getRefreshRate()) { + // If only the hdr/sdr ratio changed, then send just the event for that case + if ((diff == DisplayDeviceInfo.DIFF_HDR_SDR_RATIO)) { + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_HDR_SDR_RATIO_CHANGED; + } else { - if (mFlags.isDisplayListenerPerformanceImprovementsEnabled()) { + if (hasBasicInfoChanged) { + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_BASIC_CHANGED; + } logicalDisplayEventMask |= updateAndGetMaskForDisplayPropertyChanges(newDisplayInfo); } - // The display is involved in a display layout transition } else if (updateState == UPDATE_STATE_TRANSITION) { logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_DEVICE_STATE_TRANSITION; @@ -891,7 +899,8 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { // things like display cutouts. display.getNonOverrideDisplayInfoLocked(mTempDisplayInfo); if (!mTempNonOverrideDisplayInfo.equals(mTempDisplayInfo)) { - logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_CHANGED; + logicalDisplayEventMask |= LOGICAL_DISPLAY_EVENT_BASIC_CHANGED + | LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED; } } mLogicalDisplaysToUpdate.put(displayId, logicalDisplayEventMask); @@ -930,7 +939,7 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { if (mFlags.isConnectedDisplayManagementEnabled()) { sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_DISCONNECTED); } - sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_CHANGED); + sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_BASIC_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_STATE_CHANGED); sendUpdatesForDisplaysLocked(LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED); @@ -962,7 +971,8 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { mask |= LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED; } - if (mTempDisplayInfo.state != newDisplayInfo.state) { + if (mFlags.isDisplayListenerPerformanceImprovementsEnabled() + && mTempDisplayInfo.state != newDisplayInfo.state) { mask |= LOGICAL_DISPLAY_EVENT_STATE_CHANGED; } return mask; @@ -1357,8 +1367,6 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { return "added"; case LOGICAL_DISPLAY_EVENT_DEVICE_STATE_TRANSITION: return "transition"; - case LOGICAL_DISPLAY_EVENT_CHANGED: - return "changed"; case LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED: return "framerate_override"; case LOGICAL_DISPLAY_EVENT_SWAPPED: @@ -1375,6 +1383,8 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { return "state_changed"; case LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED: return "refresh_rate_changed"; + case LOGICAL_DISPLAY_EVENT_BASIC_CHANGED: + return "basic_changed"; } return null; } diff --git a/services/core/java/com/android/server/location/provider/proxy/ProxyGnssAssistanceProvider.java b/services/core/java/com/android/server/location/provider/proxy/ProxyGnssAssistanceProvider.java new file mode 100644 index 000000000000..6cab60c05b8e --- /dev/null +++ b/services/core/java/com/android/server/location/provider/proxy/ProxyGnssAssistanceProvider.java @@ -0,0 +1,97 @@ +/* + * 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.server.location.provider.proxy; + +import android.annotation.Nullable; +import android.content.Context; +import android.location.provider.GnssAssistanceProviderBase; +import android.location.provider.IGnssAssistanceCallback; +import android.location.provider.IGnssAssistanceProvider; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; + +import com.android.server.servicewatcher.CurrentUserServiceSupplier; +import com.android.server.servicewatcher.ServiceWatcher; + +/** + * Proxy for IGnssAssitanceProvider implementations. + */ +public class ProxyGnssAssistanceProvider { + + private static final String TAG = "GnssAssistanceProxy"; + /** + * Creates and registers this proxy. If no suitable service is available for the proxy, returns + * null. + */ + @Nullable + public static ProxyGnssAssistanceProvider createAndRegister(Context context) { + ProxyGnssAssistanceProvider proxy = new ProxyGnssAssistanceProvider(context); + if (proxy.register()) { + return proxy; + } else { + return null; + } + } + + private final ServiceWatcher mServiceWatcher; + + private ProxyGnssAssistanceProvider(Context context) { + mServiceWatcher = + ServiceWatcher.create( + context, + TAG, + CurrentUserServiceSupplier.createFromConfig( + context, + GnssAssistanceProviderBase.ACTION_GNSS_ASSISTANCE_PROVIDER, + com.android.internal.R.bool.config_enableGnssAssistanceOverlay, + com.android.internal.R.string + .config_gnssAssistanceProviderPackageName), + /* serviceListener= */ null); + } + + private boolean register() { + boolean resolves = mServiceWatcher.checkServiceResolves(); + if (resolves) { + mServiceWatcher.register(); + } + return resolves; + } + + /** + * Request GNSS assistance. + */ + public void request(IGnssAssistanceCallback callback) { + mServiceWatcher.runOnBinder( + new ServiceWatcher.BinderOperation() { + @Override + public void run(IBinder binder) throws RemoteException { + IGnssAssistanceProvider.Stub.asInterface(binder).request(callback); + } + + @Override + public void onError(Throwable t) { + try { + Log.w(TAG, "Error on requesting GnssAssistance: " + t); + callback.onError(); + } catch (RemoteException e) { + // ignore + } + } + }); + } +} diff --git a/services/core/java/com/android/server/media/MediaRoute2Provider.java b/services/core/java/com/android/server/media/MediaRoute2Provider.java index 58c8450d714d..0d6e502cf965 100644 --- a/services/core/java/com/android/server/media/MediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/MediaRoute2Provider.java @@ -21,6 +21,7 @@ import android.annotation.Nullable; import android.content.ComponentName; import android.media.MediaRoute2Info; import android.media.MediaRoute2ProviderInfo; +import android.media.MediaRoute2ProviderService.Reason; import android.media.MediaRouter2; import android.media.MediaRouter2Utils; import android.media.RouteDiscoveryPreference; @@ -123,6 +124,13 @@ abstract class MediaRoute2Provider { } } + /** Calls {@link Callback#onRequestFailed} with the given id and reason. */ + protected void notifyRequestFailed(long requestId, @Reason int reason) { + if (mCallback != null) { + mCallback.onRequestFailed(/* provider= */ this, requestId, reason); + } + } + void setAndNotifyProviderState(MediaRoute2ProviderInfo providerInfo) { setProviderState(providerInfo); notifyProviderState(); @@ -171,11 +179,34 @@ abstract class MediaRoute2Provider { void onProviderStateChanged(@Nullable MediaRoute2Provider provider); void onSessionCreated(@NonNull MediaRoute2Provider provider, long requestId, @Nullable RoutingSessionInfo sessionInfo); - void onSessionUpdated(@NonNull MediaRoute2Provider provider, - @NonNull RoutingSessionInfo sessionInfo); + + /** + * Called when there's a session info change. + * + * <p>If the provided {@code sessionInfo} has a null {@link + * RoutingSessionInfo#getClientPackageName()}, that means that it's applicable to all + * packages. We call this type of routing session "global". This is typically used for + * system provided {@link RoutingSessionInfo}. However, some applications may be exempted + * from the global routing sessions, because their media is being routed using a session + * different from the global routing session. + * + * @param provider The provider that owns the session that changed. + * @param sessionInfo The new {@link RoutingSessionInfo}. + * @param packageNamesWithRoutingSessionOverrides The names of packages that are not + * affected by global session changes. This set may only be non-empty when the {@code + * sessionInfo} is for the global session, and therefore has no {@link + * RoutingSessionInfo#getClientPackageName()}. + */ + void onSessionUpdated( + @NonNull MediaRoute2Provider provider, + @NonNull RoutingSessionInfo sessionInfo, + Set<String> packageNamesWithRoutingSessionOverrides); + void onSessionReleased(@NonNull MediaRoute2Provider provider, @NonNull RoutingSessionInfo sessionInfo); - void onRequestFailed(@NonNull MediaRoute2Provider provider, long requestId, int reason); + + void onRequestFailed( + @NonNull MediaRoute2Provider provider, long requestId, @Reason int reason); } /** diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java index f09be2c15ee0..d6f7d3bdd4a4 100644 --- a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java +++ b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java @@ -16,6 +16,7 @@ package com.android.server.media; +import static android.media.MediaRoute2ProviderService.REASON_REJECTED; import static android.media.MediaRoute2ProviderService.REQUEST_ID_NONE; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; @@ -31,6 +32,7 @@ import android.media.IMediaRoute2ProviderServiceCallback; import android.media.MediaRoute2Info; import android.media.MediaRoute2ProviderInfo; import android.media.MediaRoute2ProviderService; +import android.media.MediaRoute2ProviderService.Reason; import android.media.RouteDiscoveryPreference; import android.media.RoutingSessionInfo; import android.os.Bundle; @@ -41,6 +43,7 @@ import android.os.Looper; import android.os.RemoteException; import android.os.UserHandle; import android.text.TextUtils; +import android.util.ArrayMap; import android.util.Log; import android.util.LongSparseArray; import android.util.Slog; @@ -89,6 +92,12 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider { mRequestIdToSessionCreationRequest; @GuardedBy("mLock") + private final Map<String, SystemMediaSessionCallback> mSystemSessionCallbacks; + + @GuardedBy("mLock") + private final LongSparseArray<SystemMediaSessionCallback> mRequestIdToSystemSessionRequest; + + @GuardedBy("mLock") private final Map<String, SessionCreationOrTransferRequest> mSessionOriginalIdToTransferRequest; MediaRoute2ProviderServiceProxy( @@ -102,6 +111,8 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider { mContext = Objects.requireNonNull(context, "Context must not be null."); mRequestIdToSessionCreationRequest = new LongSparseArray<>(); mSessionOriginalIdToTransferRequest = new HashMap<>(); + mRequestIdToSystemSessionRequest = new LongSparseArray<>(); + mSystemSessionCallbacks = new ArrayMap<>(); mIsSelfScanOnlyProvider = isSelfScanOnlyProvider; mSupportsSystemMediaRouting = supportsSystemMediaRouting; mUserId = userId; @@ -236,6 +247,48 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider { } } + /** + * Requests the creation of a system media routing session. + * + * @param requestId The id of the request. + * @param uid The uid of the package whose media to route, or {@link + * android.os.Process#INVALID_UID} if not applicable (for example, if all the system's media + * must be routed). + * @param packageName The package name to populate {@link + * RoutingSessionInfo#getClientPackageName()}. + * @param routeId The id of the route to be initially {@link + * RoutingSessionInfo#getSelectedRoutes()}. + * @param sessionHints An optional bundle with paramets. + * @param callback A {@link SystemMediaSessionCallback} to notify of session events. + * @see MediaRoute2ProviderService#onCreateSystemRoutingSession + */ + public void requestCreateSystemMediaSession( + long requestId, + int uid, + String packageName, + String routeId, + @Nullable Bundle sessionHints, + @NonNull SystemMediaSessionCallback callback) { + if (!Flags.enableMirroringInMediaRouter2()) { + throw new IllegalStateException( + "Unexpected call to requestCreateSystemMediaSession. Governing flag is" + + " disabled."); + } + if (mConnectionReady) { + boolean binderRequestSucceeded = + mActiveConnection.requestCreateSystemMediaSession( + requestId, uid, packageName, routeId, sessionHints); + if (!binderRequestSucceeded) { + // notify failure. + return; + } + updateBinding(); + synchronized (mLock) { + mRequestIdToSystemSessionRequest.put(requestId, callback); + } + } + } + public boolean hasComponentName(String packageName, String className) { return mComponentName.getPackageName().equals(packageName) && mComponentName.getClassName().equals(className); @@ -292,7 +345,14 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider { mLastDiscoveryPreference != null && mLastDiscoveryPreference.shouldPerformActiveScan() && mSupportsSystemMediaRouting; + boolean bindDueToOngoingSystemMediaRoutingSessions = false; + if (Flags.enableMirroringInMediaRouter2()) { + synchronized (mLock) { + bindDueToOngoingSystemMediaRoutingSessions = !mSystemSessionCallbacks.isEmpty(); + } + } if (!getSessionInfos().isEmpty() + || bindDueToOngoingSystemMediaRoutingSessions || bindDueToManagerScan || bindDueToSystemMediaRoutingSupport) { return true; @@ -438,6 +498,14 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider { String newSessionId = newSession.getId(); synchronized (mLock) { + var systemMediaSessionCallback = mRequestIdToSystemSessionRequest.get(requestId); + if (systemMediaSessionCallback != null) { + mRequestIdToSystemSessionRequest.remove(requestId); + mSystemSessionCallbacks.put(newSession.getOriginalId(), systemMediaSessionCallback); + systemMediaSessionCallback.onSessionUpdate(newSession); + return; + } + if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()) { newSession = createSessionWithPopulatedTransferInitiationDataLocked( @@ -569,6 +637,12 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider { boolean found = false; synchronized (mLock) { + var sessionCallback = mSystemSessionCallbacks.get(releasedSession.getOriginalId()); + if (sessionCallback != null) { + sessionCallback.onSessionReleased(); + return; + } + mSessionOriginalIdToTransferRequest.remove(releasedSession.getId()); for (RoutingSessionInfo session : mSessionInfos) { if (TextUtils.equals(session.getId(), releasedSession.getId())) { @@ -602,7 +676,11 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider { private void dispatchSessionUpdated(RoutingSessionInfo session) { mHandler.sendMessage( - obtainMessage(mCallback::onSessionUpdated, this, session)); + obtainMessage( + mCallback::onSessionUpdated, + this, + session, + /* packageNamesWithRoutingSessionOverrides= */ Set.of())); } private void dispatchSessionReleased(RoutingSessionInfo session) { @@ -645,6 +723,19 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider { for (RoutingSessionInfo sessionInfo : mSessionInfos) { mCallback.onSessionReleased(this, sessionInfo); } + if (Flags.enableMirroringInMediaRouter2()) { + for (var callback : mSystemSessionCallbacks.values()) { + callback.onSessionReleased(); + } + mSystemSessionCallbacks.clear(); + int requestsSize = mRequestIdToSystemSessionRequest.size(); + for (int i = 0; i < requestsSize; i++) { + var callback = mRequestIdToSystemSessionRequest.valueAt(i); + var requestId = mRequestIdToSystemSessionRequest.keyAt(i); + callback.onRequestFailed(requestId, REASON_REJECTED); + } + mSystemSessionCallbacks.clear(); + } mSessionInfos.clear(); mReleasingSessions.clear(); mRequestIdToSessionCreationRequest.clear(); @@ -673,6 +764,26 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider { pendingTransferCount); } + /** + * Callback for events related to system media sessions. + * + * @see MediaRoute2ProviderService#onCreateSystemRoutingSession + */ + public interface SystemMediaSessionCallback { + + /** + * Called when the corresponding session's {@link RoutingSessionInfo}, or upon the creation + * of the given session info. + */ + void onSessionUpdate(@NonNull RoutingSessionInfo sessionInfo); + + /** Called when the request with the given id fails for the given reason. */ + void onRequestFailed(long requestId, @Reason int reason); + + /** Called when the corresponding session is released. */ + void onSessionReleased(); + } + // All methods in this class are called on the main thread. private final class ServiceConnectionImpl implements ServiceConnection { @@ -739,6 +850,28 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider { } } + /** + * Sends a system media session creation request to the provider service, and returns + * whether the request transaction succeeded. + * + * <p>The transaction might fail, for example, if the recipient process has died. + */ + public boolean requestCreateSystemMediaSession( + long requestId, + int uid, + String packageName, + String routeId, + @Nullable Bundle sessionHints) { + try { + mService.requestCreateSystemMediaSession( + requestId, uid, packageName, routeId, sessionHints); + return true; + } catch (RemoteException ex) { + Slog.e(TAG, "requestCreateSystemMediaSession: Failed to deliver request."); + } + return false; + } + public void releaseSession(long requestId, String sessionId) { try { mService.releaseSession(requestId, sessionId); diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index 58deffcbd4ba..5e6737a485af 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -846,33 +846,29 @@ class MediaRouter2ServiceImpl { try { synchronized (mLock) { UserRecord userRecord = getOrCreateUserRecordLocked(userId); - List<RoutingSessionInfo> sessionInfos; + SystemMediaRoute2Provider systemProvider = userRecord.mHandler.getSystemProvider(); if (hasSystemRoutingPermissions) { - if (setDeviceRouteSelected && !Flags.enableMirroringInMediaRouter2()) { + if (!Flags.enableMirroringInMediaRouter2() && setDeviceRouteSelected) { // Return a fake system session that shows the device route as selected and // available bluetooth routes as transferable. - return userRecord.mHandler.getSystemProvider() - .generateDeviceRouteSelectedSessionInfo(targetPackageName); + return systemProvider.generateDeviceRouteSelectedSessionInfo( + targetPackageName); } else { - sessionInfos = userRecord.mHandler.getSystemProvider().getSessionInfos(); - if (!sessionInfos.isEmpty()) { - // Return a copy of the current system session with no modification, - // except setting the client package name. - return new RoutingSessionInfo.Builder(sessionInfos.get(0)) - .setClientPackageName(targetPackageName) - .build(); + RoutingSessionInfo session = + systemProvider.getSessionForPackage(targetPackageName); + if (session != null) { + return session; } else { Slog.w(TAG, "System provider does not have any session info."); + return null; } } } else { - return new RoutingSessionInfo.Builder( - userRecord.mHandler.getSystemProvider().getDefaultSessionInfo()) + return new RoutingSessionInfo.Builder(systemProvider.getDefaultSessionInfo()) .setClientPackageName(targetPackageName) .build(); } } - return null; } finally { Binder.restoreCallingIdentity(token); } @@ -2638,10 +2634,17 @@ class MediaRouter2ServiceImpl { } @Override - public void onSessionUpdated(@NonNull MediaRoute2Provider provider, - @NonNull RoutingSessionInfo sessionInfo) { - sendMessage(PooledLambda.obtainMessage(UserHandler::onSessionInfoChangedOnHandler, - this, provider, sessionInfo)); + public void onSessionUpdated( + @NonNull MediaRoute2Provider provider, + @NonNull RoutingSessionInfo sessionInfo, + Set<String> packageNamesWithRoutingSessionOverrides) { + sendMessage( + PooledLambda.obtainMessage( + UserHandler::onSessionInfoChangedOnHandler, + this, + provider, + sessionInfo, + packageNamesWithRoutingSessionOverrides)); } @Override @@ -3152,10 +3155,31 @@ class MediaRouter2ServiceImpl { toOriginalRequestId(uniqueRequestId), sessionInfo); } - private void onSessionInfoChangedOnHandler(@NonNull MediaRoute2Provider provider, - @NonNull RoutingSessionInfo sessionInfo) { + /** + * Implementation of {@link MediaRoute2Provider.Callback#onSessionUpdated}. + * + * <p>Must run on the thread that corresponds to this {@link UserHandler}. + */ + private void onSessionInfoChangedOnHandler( + @NonNull MediaRoute2Provider provider, + @NonNull RoutingSessionInfo sessionInfo, + Set<String> packageNamesWithRoutingSessionOverrides) { List<ManagerRecord> managers = getManagerRecords(); for (ManagerRecord manager : managers) { + if (Flags.enableMirroringInMediaRouter2()) { + String targetPackageName = manager.mTargetPackageName; + boolean skipDueToOverride = + targetPackageName != null + && packageNamesWithRoutingSessionOverrides.contains( + targetPackageName); + boolean sessionIsForTargetPackage = + TextUtils.isEmpty(sessionInfo.getClientPackageName()) // is global. + || TextUtils.equals( + targetPackageName, sessionInfo.getClientPackageName()); + if (skipDueToOverride || !sessionIsForTargetPackage) { + continue; + } + } manager.notifySessionUpdated(sessionInfo); } diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java index b93846bf9ee7..60fced1e0c51 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java @@ -62,7 +62,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { static final String SYSTEM_SESSION_ID = "SYSTEM_SESSION"; private final AudioManager mAudioManager; - private final Handler mHandler; + protected final Handler mHandler; private final Context mContext; private final UserHandle mUser; @@ -116,7 +116,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { () -> { publishProviderState(); if (updateSessionInfosIfNeeded()) { - notifySessionInfoUpdated(); + notifyGlobalSessionInfoUpdated(); } }); @@ -129,7 +129,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { () -> { publishProviderState(); if (updateSessionInfosIfNeeded()) { - notifySessionInfoUpdated(); + notifyGlobalSessionInfoUpdated(); } })); } @@ -161,7 +161,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { public void setCallback(Callback callback) { super.setCallback(callback); notifyProviderState(); - notifySessionInfoUpdated(); + notifyGlobalSessionInfoUpdated(); } @Override @@ -296,7 +296,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { if (Flags.enableBuiltInSpeakerRouteSuitabilityStatuses() && updateSessionInfosIfNeeded()) { - notifySessionInfoUpdated(); + notifyGlobalSessionInfoUpdated(); } } @@ -327,6 +327,23 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { } /** + * Returns the {@link RoutingSessionInfo} that corresponds to the package with the given name. + */ + public RoutingSessionInfo getSessionForPackage(String targetPackageName) { + synchronized (mLock) { + if (!mSessionInfos.isEmpty()) { + // Return a copy of the current system session with no modification, + // except setting the client package name. + return new RoutingSessionInfo.Builder(mSessionInfos.get(0)) + .setClientPackageName(targetPackageName) + .build(); + } else { + return null; + } + } + } + + /** * Builds a system {@link RoutingSessionInfo} with the selected route set to the currently * selected <b>device</b> route (wired or built-in, but not bluetooth) and transferable routes * set to the currently available (connected) bluetooth routes. @@ -626,20 +643,21 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { notifyProviderState(); } - void notifySessionInfoUpdated() { + void notifyGlobalSessionInfoUpdated() { if (mCallback == null) { return; } RoutingSessionInfo sessionInfo; synchronized (mLock) { - sessionInfo = mSessionInfos.get(0); - if (sessionInfo == null) { + if (mSessionInfos.isEmpty()) { return; } + sessionInfo = mSessionInfos.get(0); } - mCallback.onSessionUpdated(this, sessionInfo); + mCallback.onSessionUpdated( + this, sessionInfo, /* packageNamesWithRoutingSessionOverrides= */ Set.of()); } @Override diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java index 7dc30ab66fd2..8931e3a1426e 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java @@ -18,23 +18,33 @@ package com.android.server.media; import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO; +import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.content.ComponentName; import android.content.Context; +import android.content.pm.PackageManager; import android.media.MediaRoute2Info; import android.media.MediaRoute2ProviderInfo; import android.media.MediaRoute2ProviderService; +import android.media.MediaRoute2ProviderService.Reason; +import android.media.MediaRouter2Utils; import android.media.RoutingSessionInfo; +import android.os.Binder; import android.os.Looper; +import android.os.Process; import android.os.UserHandle; -import android.util.ArraySet; +import android.text.TextUtils; +import android.util.ArrayMap; +import android.util.Log; +import android.util.LongSparseArray; import com.android.internal.annotations.GuardedBy; +import com.android.server.media.MediaRoute2ProviderServiceProxy.SystemMediaSessionCallback; -import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.stream.Stream; /** @@ -48,11 +58,33 @@ import java.util.stream.Stream; private static final String ROUTE_ID_PREFIX_SYSTEM = "SYSTEM"; private static final String ROUTE_ID_SYSTEM_SEPARATOR = "."; + private final PackageManager mPackageManager; + @GuardedBy("mLock") private MediaRoute2ProviderInfo mLastSystemProviderInfo; @GuardedBy("mLock") - private final Map<String, ProviderProxyRecord> mProxyRecords = new HashMap<>(); + private final Map<String, ProviderProxyRecord> mProxyRecords = new ArrayMap<>(); + + /** + * Maps package names to corresponding sessions maintained by {@link MediaRoute2ProviderService + * provider services}. + */ + @GuardedBy("mLock") + private final Map<String, SystemMediaSessionRecord> mPackageNameToSessionRecord = + new ArrayMap<>(); + + /** + * Maps route {@link MediaRoute2Info#getOriginalId original ids} to the id of the {@link + * MediaRoute2ProviderService provider service} that manages the corresponding route. + */ + @GuardedBy("mLock") + private final Map<String, String> mOriginalRouteIdToProviderId = new ArrayMap<>(); + + /** Maps request ids to pending session creation callbacks. */ + @GuardedBy("mLock") + private final LongSparseArray<SystemMediaSessionCallbackImpl> mPendingSessionCreations = + new LongSparseArray<>(); private static final ComponentName COMPONENT_NAME = new ComponentName( @@ -69,6 +101,128 @@ import java.util.stream.Stream; private SystemMediaRoute2Provider2(Context context, UserHandle user, Looper looper) { super(context, COMPONENT_NAME, user, looper); + mPackageManager = context.getPackageManager(); + } + + @Override + public void transferToRoute( + long requestId, + @NonNull UserHandle clientUserHandle, + @NonNull String clientPackageName, + String sessionOriginalId, + String routeOriginalId, + int transferReason) { + synchronized (mLock) { + var targetProviderProxyId = mOriginalRouteIdToProviderId.get(routeOriginalId); + var targetProviderProxyRecord = mProxyRecords.get(targetProviderProxyId); + // Holds the target route, if it's managed by a provider service. Holds null otherwise. + var serviceTargetRoute = + targetProviderProxyRecord != null + ? targetProviderProxyRecord.getRouteByOriginalId(routeOriginalId) + : null; + var existingSessionRecord = mPackageNameToSessionRecord.get(clientPackageName); + if (existingSessionRecord != null) { + var existingSession = existingSessionRecord.mSourceSessionInfo; + if (targetProviderProxyId != null + && TextUtils.equals( + targetProviderProxyId, existingSession.getProviderId())) { + // The currently selected route and target route both belong to the same + // provider. We tell the provider to handle the transfer. + targetProviderProxyRecord.requestTransfer( + existingSession.getOriginalId(), serviceTargetRoute); + } else { + // The target route is handled by a provider other than the target one. We need + // to release the existing session. + var currentProxyRecord = existingSessionRecord.getProxyRecord(); + if (currentProxyRecord != null) { + currentProxyRecord.releaseSession( + requestId, existingSession.getOriginalId()); + existingSessionRecord.removeSelfFromSessionMap(); + } + } + } + + if (serviceTargetRoute != null) { + boolean isGlobalSession = TextUtils.isEmpty(clientPackageName); + int uid; + if (isGlobalSession) { + uid = Process.INVALID_UID; + } else { + uid = fetchUid(clientPackageName, clientUserHandle); + if (uid == Process.INVALID_UID) { + throw new IllegalArgumentException( + "Cannot resolve transfer for " + + clientPackageName + + " and " + + clientUserHandle); + } + } + var pendingCreationCallback = + new SystemMediaSessionCallbackImpl( + targetProviderProxyId, requestId, clientPackageName); + mPendingSessionCreations.put(requestId, pendingCreationCallback); + targetProviderProxyRecord.requestCreateSystemMediaSession( + requestId, + uid, + clientPackageName, + routeOriginalId, + pendingCreationCallback); + } else { + // The target route is not provided by any of the services. Assume it's a system + // provided route. + super.transferToRoute( + requestId, + clientUserHandle, + clientPackageName, + sessionOriginalId, + routeOriginalId, + transferReason); + } + } + } + + @Nullable + @Override + public RoutingSessionInfo getSessionForPackage(String packageName) { + synchronized (mLock) { + var systemSession = super.getSessionForPackage(packageName); + if (systemSession == null) { + return null; + } + var overridingSession = mPackageNameToSessionRecord.get(packageName); + if (overridingSession != null) { + var builder = + new RoutingSessionInfo.Builder(overridingSession.mTranslatedSessionInfo) + .setProviderId(mUniqueId) + .setSystemSession(true); + for (var systemRoute : mLastSystemProviderInfo.getRoutes()) { + builder.addTransferableRoute(systemRoute.getOriginalId()); + } + return builder.build(); + } else { + return systemSession; + } + } + } + + /** + * Returns the uid that corresponds to the given name and user handle, or {@link + * Process#INVALID_UID} if a uid couldn't be found. + */ + @SuppressLint("MissingPermission") + // We clear the calling identity before calling the package manager, and we are running on the + // system_server. + private int fetchUid(String clientPackageName, UserHandle clientUserHandle) { + final long token = Binder.clearCallingIdentity(); + try { + return mPackageManager.getApplicationInfoAsUser( + clientPackageName, /* flags= */ 0, clientUserHandle) + .uid; + } catch (PackageManager.NameNotFoundException e) { + return Process.INVALID_UID; + } finally { + Binder.restoreCallingIdentity(token); + } } @Override @@ -85,21 +239,21 @@ import java.util.stream.Stream; } else { mProxyRecords.put(serviceProxy.mUniqueId, proxyRecord); } - setProviderState(buildProviderInfo()); + updateProviderInfo(); } updateSessionInfo(); notifyProviderState(); - notifySessionInfoUpdated(); + notifyGlobalSessionInfoUpdated(); } @Override public void onSystemProviderRoutesChanged(MediaRoute2ProviderInfo providerInfo) { synchronized (mLock) { mLastSystemProviderInfo = providerInfo; - setProviderState(buildProviderInfo()); + updateProviderInfo(); } updateSessionInfo(); - notifySessionInfoUpdated(); + notifyGlobalSessionInfoUpdated(); } /** @@ -116,10 +270,13 @@ import java.util.stream.Stream; var builder = new RoutingSessionInfo.Builder(systemSessionInfo); mProxyRecords.values().stream() .flatMap(ProviderProxyRecord::getRoutesStream) - .map(MediaRoute2Info::getId) + .map(MediaRoute2Info::getOriginalId) .forEach(builder::addTransferableRoute); mSessionInfos.clear(); mSessionInfos.add(builder.build()); + for (var sessionRecords : mPackageNameToSessionRecord.values()) { + mSessionInfos.add(sessionRecords.mTranslatedSessionInfo); + } } } @@ -129,13 +286,84 @@ import java.util.stream.Stream; * provider services}. */ @GuardedBy("mLock") - private MediaRoute2ProviderInfo buildProviderInfo() { + private void updateProviderInfo() { MediaRoute2ProviderInfo.Builder builder = new MediaRoute2ProviderInfo.Builder(mLastSystemProviderInfo); - mProxyRecords.values().stream() - .flatMap(ProviderProxyRecord::getRoutesStream) - .forEach(builder::addRoute); - return builder.build(); + mOriginalRouteIdToProviderId.clear(); + for (var proxyRecord : mProxyRecords.values()) { + String proxyId = proxyRecord.mProxy.mUniqueId; + proxyRecord + .getRoutesStream() + .forEach( + route -> { + builder.addRoute(route); + mOriginalRouteIdToProviderId.put(route.getOriginalId(), proxyId); + }); + } + setProviderState(builder.build()); + } + + @Override + /* package */ void notifyGlobalSessionInfoUpdated() { + if (mCallback == null) { + return; + } + + RoutingSessionInfo sessionInfo; + Set<String> packageNamesWithRoutingSessionOverrides; + synchronized (mLock) { + if (mSessionInfos.isEmpty()) { + return; + } + packageNamesWithRoutingSessionOverrides = mPackageNameToSessionRecord.keySet(); + sessionInfo = mSessionInfos.getFirst(); + } + + mCallback.onSessionUpdated(this, sessionInfo, packageNamesWithRoutingSessionOverrides); + } + + private void onSessionOverrideUpdated(RoutingSessionInfo sessionInfo) { + // TODO: b/362507305 - Consider adding routes from other provider services. This is not a + // trivial change because a provider1-route to provider2-route transfer has seemingly two + // possible approachies. Either we first release the current session and then create the new + // one, in which case the audio is briefly going to leak through the system route. On the + // other hand, if we first create the provider2 session, then there will be a period during + // which there will be two overlapping routing policies asking for the exact same media + // stream. + var builder = new RoutingSessionInfo.Builder(sessionInfo); + mLastSystemProviderInfo.getRoutes().stream() + .map(MediaRoute2Info::getOriginalId) + .forEach(builder::addTransferableRoute); + mCallback.onSessionUpdated( + /* provider= */ this, + builder.build(), + /* packageNamesWithRoutingSessionOverrides= */ Set.of()); + } + + /** + * Equivalent to {@link #asSystemRouteId}, except it takes a unique route id instead of a + * original id. + */ + private static String uniqueIdAsSystemRouteId(String providerId, String uniqueRouteId) { + return asSystemRouteId(providerId, MediaRouter2Utils.getOriginalId(uniqueRouteId)); + } + + /** + * Returns a unique {@link MediaRoute2Info#getOriginalId() original id} for this provider to + * publish system media routes from {@link MediaRoute2ProviderService provider services}. + * + * <p>This provider will publish system media routes as part of the system routing session. + * However, said routes may also support {@link MediaRoute2Info#FLAG_ROUTING_TYPE_REMOTE remote + * routing}, meaning we cannot use the same id, or there would be an id collision. As a result, + * we derive a {@link MediaRoute2Info#getOriginalId original id} that is unique among all + * original route ids used by this provider. + */ + private static String asSystemRouteId(String providerId, String originalRouteId) { + return ROUTE_ID_PREFIX_SYSTEM + + ROUTE_ID_SYSTEM_SEPARATOR + + providerId + + ROUTE_ID_SYSTEM_SEPARATOR + + originalRouteId; } /** @@ -145,14 +373,69 @@ import java.util.stream.Stream; * @param mProxy The corresponding {@link MediaRoute2ProviderServiceProxy}. * @param mSystemMediaRoutes The last snapshot of routes from the service that support system * media routing, as defined by {@link MediaRoute2Info#supportsSystemMediaRouting()}. + * @param mNewOriginalIdToSourceOriginalIdMap Maps the {@link #mSystemMediaRoutes} ids to the + * original ids of corresponding {@link MediaRoute2ProviderService service} route. */ private record ProviderProxyRecord( MediaRoute2ProviderServiceProxy mProxy, - Collection<MediaRoute2Info> mSystemMediaRoutes) { + Map<String, MediaRoute2Info> mSystemMediaRoutes, + Map<String, String> mNewOriginalIdToSourceOriginalIdMap) { /** Returns a stream representation of the {@link #mSystemMediaRoutes}. */ public Stream<MediaRoute2Info> getRoutesStream() { - return mSystemMediaRoutes.stream(); + return mSystemMediaRoutes.values().stream(); + } + + @Nullable + public MediaRoute2Info getRouteByOriginalId(String routeOriginalId) { + return mSystemMediaRoutes.get(routeOriginalId); + } + + /** + * Requests the creation of a system media routing session. + * + * @param requestId The request id. + * @param uid The uid of the package whose media to route, or {@link Process#INVALID_UID} if + * not applicable. + * @param packageName The name of the package whose media to route. + * @param originalRouteId The {@link MediaRoute2Info#getOriginalId() original route id} of + * the route that should be initially selected. + * @param callback A {@link MediaRoute2ProviderServiceProxy.SystemMediaSessionCallback} for + * events. + * @see MediaRoute2ProviderService#onCreateSystemRoutingSession + */ + public void requestCreateSystemMediaSession( + long requestId, + int uid, + String packageName, + String originalRouteId, + SystemMediaSessionCallback callback) { + var targetRouteId = mNewOriginalIdToSourceOriginalIdMap.get(originalRouteId); + if (targetRouteId == null) { + Log.w( + TAG, + "Failed system media session creation due to lack of mapping for id: " + + originalRouteId); + callback.onRequestFailed( + requestId, MediaRoute2ProviderService.REASON_ROUTE_NOT_AVAILABLE); + } else { + mProxy.requestCreateSystemMediaSession( + requestId, + uid, + packageName, + targetRouteId, + /* sessionHints= */ null, + callback); + } + } + + public void requestTransfer(String sessionId, MediaRoute2Info targetRoute) { + // TODO: Map the target route to the source route original id. + throw new UnsupportedOperationException("TODO Implement"); + } + + public void releaseSession(long requestId, String originalSessionId) { + mProxy.releaseSession(requestId, originalSessionId); } /** @@ -165,22 +448,177 @@ import java.util.stream.Stream; if (providerInfo == null) { return null; } - ArraySet<MediaRoute2Info> routes = new ArraySet<>(); - providerInfo.getRoutes().stream() - .filter(MediaRoute2Info::supportsSystemMediaRouting) - .forEach( - route -> { - String id = - ROUTE_ID_PREFIX_SYSTEM - + route.getProviderId() - + ROUTE_ID_SYSTEM_SEPARATOR - + route.getOriginalId(); - routes.add( - new MediaRoute2Info.Builder(id, route.getName()) - .addFeature(FEATURE_LIVE_AUDIO) - .build()); - }); - return new ProviderProxyRecord(serviceProxy, Collections.unmodifiableSet(routes)); + Map<String, MediaRoute2Info> routesMap = new ArrayMap<>(); + Map<String, String> idMap = new ArrayMap<>(); + for (MediaRoute2Info sourceRoute : providerInfo.getRoutes()) { + if (!sourceRoute.supportsSystemMediaRouting()) { + continue; + } + String id = + asSystemRouteId(providerInfo.getUniqueId(), sourceRoute.getOriginalId()); + var newRoute = + new MediaRoute2Info.Builder(id, sourceRoute.getName()) + .addFeature(FEATURE_LIVE_AUDIO) + .build(); + routesMap.put(id, newRoute); + idMap.put(id, sourceRoute.getOriginalId()); + } + return new ProviderProxyRecord( + serviceProxy, + Collections.unmodifiableMap(routesMap), + Collections.unmodifiableMap(idMap)); + } + } + + private class SystemMediaSessionCallbackImpl implements SystemMediaSessionCallback { + + private final String mProviderId; + private final long mRequestId; + private final String mClientPackageName; + // Accessed only on mHandler. + @Nullable private SystemMediaSessionRecord mSessionRecord; + + private SystemMediaSessionCallbackImpl( + String providerId, long requestId, String clientPackageName) { + mProviderId = providerId; + mRequestId = requestId; + mClientPackageName = clientPackageName; + } + + @Override + public void onSessionUpdate(@NonNull RoutingSessionInfo sessionInfo) { + mHandler.post( + () -> { + if (mSessionRecord != null) { + mSessionRecord.onSessionUpdate(sessionInfo); + } + SystemMediaSessionRecord systemMediaSessionRecord = + new SystemMediaSessionRecord(mProviderId, sessionInfo); + RoutingSessionInfo translatedSession; + synchronized (mLock) { + mSessionRecord = systemMediaSessionRecord; + mPackageNameToSessionRecord.put( + mClientPackageName, systemMediaSessionRecord); + mPendingSessionCreations.remove(mRequestId); + translatedSession = systemMediaSessionRecord.mTranslatedSessionInfo; + } + onSessionOverrideUpdated(translatedSession); + }); + } + + @Override + public void onRequestFailed(long requestId, @Reason int reason) { + mHandler.post( + () -> { + if (mSessionRecord != null) { + mSessionRecord.onRequestFailed(requestId, reason); + } + synchronized (mLock) { + mPendingSessionCreations.remove(mRequestId); + } + notifyRequestFailed(requestId, reason); + }); + } + + @Override + public void onSessionReleased() { + mHandler.post( + () -> { + if (mSessionRecord != null) { + mSessionRecord.onSessionReleased(); + } else { + // Should never happen. The session hasn't yet been created. + throw new IllegalStateException(); + } + }); + } + } + + private class SystemMediaSessionRecord implements SystemMediaSessionCallback { + + private final String mProviderId; + + @GuardedBy("SystemMediaRoute2Provider2.this.mLock") + @NonNull + private RoutingSessionInfo mSourceSessionInfo; + + /** + * The same as {@link #mSourceSessionInfo}, except ids are {@link #asSystemRouteId system + * provider ids}. + */ + @GuardedBy("SystemMediaRoute2Provider2.this.mLock") + @NonNull + private RoutingSessionInfo mTranslatedSessionInfo; + + SystemMediaSessionRecord( + @NonNull String providerId, @NonNull RoutingSessionInfo sessionInfo) { + mProviderId = providerId; + mSourceSessionInfo = sessionInfo; + mTranslatedSessionInfo = asSystemProviderSession(sessionInfo); + } + + @Override + public void onSessionUpdate(@NonNull RoutingSessionInfo sessionInfo) { + RoutingSessionInfo translatedSessionInfo = mTranslatedSessionInfo; + synchronized (mLock) { + mSourceSessionInfo = sessionInfo; + mTranslatedSessionInfo = asSystemProviderSession(sessionInfo); + } + onSessionOverrideUpdated(translatedSessionInfo); + } + + @Override + public void onRequestFailed(long requestId, @Reason int reason) { + notifyRequestFailed(requestId, reason); + } + + @Override + public void onSessionReleased() { + synchronized (mLock) { + removeSelfFromSessionMap(); + } + notifyGlobalSessionInfoUpdated(); + } + + @GuardedBy("SystemMediaRoute2Provider2.this.mLock") + @Nullable + public ProviderProxyRecord getProxyRecord() { + ProviderProxyRecord provider = mProxyRecords.get(mProviderId); + if (provider == null) { + // Unexpected condition where the proxy is no longer available while there's an + // ongoing session. Could happen due to a crash in the provider process. + removeSelfFromSessionMap(); + } + return provider; + } + + @GuardedBy("SystemMediaRoute2Provider2.this.mLock") + private void removeSelfFromSessionMap() { + mPackageNameToSessionRecord.remove(mSourceSessionInfo.getClientPackageName()); + } + + private RoutingSessionInfo asSystemProviderSession(RoutingSessionInfo session) { + var builder = + new RoutingSessionInfo.Builder(session) + .setProviderId(mUniqueId) + .setSystemSession(true) + .clearSelectedRoutes() + .clearSelectableRoutes() + .clearDeselectableRoutes() + .clearTransferableRoutes(); + session.getSelectedRoutes().stream() + .map(it -> uniqueIdAsSystemRouteId(session.getProviderId(), it)) + .forEach(builder::addSelectedRoute); + session.getSelectableRoutes().stream() + .map(it -> uniqueIdAsSystemRouteId(session.getProviderId(), it)) + .forEach(builder::addSelectableRoute); + session.getDeselectableRoutes().stream() + .map(it -> uniqueIdAsSystemRouteId(session.getProviderId(), it)) + .forEach(builder::addDeselectableRoute); + session.getTransferableRoutes().stream() + .map(it -> uniqueIdAsSystemRouteId(session.getProviderId(), it)) + .forEach(builder::addTransferableRoute); + return builder.build(); } } } diff --git a/services/core/java/com/android/server/notification/NotificationDelegate.java b/services/core/java/com/android/server/notification/NotificationDelegate.java index 89902f7f8321..7cbbe2938fd5 100644 --- a/services/core/java/com/android/server/notification/NotificationDelegate.java +++ b/services/core/java/com/android/server/notification/NotificationDelegate.java @@ -101,4 +101,10 @@ public interface NotificationDelegate { void onNotificationFeedbackReceived(String key, Bundle feedback); void prepareForPossibleShutdown(); + + /** + * Called when the notification should be unbundled. + * @param key the notification key + */ + void unbundleNotification(String key); } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index c6d7fc7508da..7375a68c547b 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -112,6 +112,7 @@ import static android.service.notification.Flags.FLAG_NOTIFICATION_CONVERSATION_ import static android.service.notification.Flags.callstyleCallbackApi; import static android.service.notification.Flags.notificationClassification; import static android.service.notification.Flags.notificationForceGrouping; +import static android.service.notification.Flags.notificationRegroupOnClassification; import static android.service.notification.Flags.redactSensitiveNotificationsBigTextStyle; import static android.service.notification.Flags.redactSensitiveNotificationsFromUntrustedListeners; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; @@ -1851,6 +1852,42 @@ public class NotificationManagerService extends SystemService { } } + @Override + public void unbundleNotification(String key) { + if (!(notificationClassification() && notificationRegroupOnClassification())) { + return; + } + synchronized (mNotificationLock) { + NotificationRecord r = mNotificationsByKey.get(key); + if (r == null) { + return; + } + + if (DBG) { + Slog.v(TAG, "unbundleNotification: " + r); + } + + boolean hasOriginalSummary = false; + if (r.getSbn().isAppGroup() && r.getNotification().isGroupChild()) { + final String oldGroupKey = GroupHelper.getFullAggregateGroupKey( + r.getSbn().getPackageName(), r.getOriginalGroupKey(), r.getUserId()); + NotificationRecord groupSummary = mSummaryByGroupKey.get(oldGroupKey); + // We only care about app-provided valid groups + hasOriginalSummary = (groupSummary != null + && !GroupHelper.isAggregatedGroup(groupSummary)); + } + + // Only NotificationRecord's mChannel is updated when bundled, the Notification + // mChannelId will always be the original channel. + String origChannelId = r.getNotification().getChannelId(); + NotificationChannel originalChannel = mPreferencesHelper.getNotificationChannel( + r.getSbn().getPackageName(), r.getUid(), origChannelId, false); + if (originalChannel != null && !origChannelId.equals(r.getChannel().getId())) { + r.updateNotificationChannel(originalChannel); + mGroupHelper.onNotificationUnbundled(r, hasOriginalSummary); + } + } + } }; NotificationManagerPrivate mNotificationManagerPrivate = new NotificationManagerPrivate() { diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 749952e3336f..15377d6b269a 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -559,7 +559,7 @@ public class PreferencesHelper implements RankingConfig { if (r.uid == UNKNOWN_UID) { if (Flags.persistIncompleteRestoreData()) { - r.userId = userId; + r.userIdWhenUidUnknown = userId; } mRestoredWithoutUids.put(unrestoredPackageKey(pkg, userId), r); } else { @@ -756,7 +756,7 @@ public class PreferencesHelper implements RankingConfig { if (Flags.persistIncompleteRestoreData() && r.uid == UNKNOWN_UID) { out.attributeLong(null, ATT_CREATION_TIME, r.creationTime); - out.attributeInt(null, ATT_USERID, r.userId); + out.attributeInt(null, ATT_USERID, r.userIdWhenUidUnknown); } if (!forBackup) { @@ -1959,7 +1959,7 @@ public class PreferencesHelper implements RankingConfig { ArrayList<ZenBypassingApp> bypassing = new ArrayList<>(); synchronized (mLock) { for (PackagePreferences p : mPackagePreferences.values()) { - if (p.userId != userId) { + if (UserHandle.getUserId(p.uid) != userId) { continue; } int totalChannelCount = p.channels.size(); @@ -3189,7 +3189,7 @@ public class PreferencesHelper implements RankingConfig { // Until we enable the UI, we should return false. boolean canHavePromotedNotifs = android.app.Flags.uiRichOngoing(); - @UserIdInt int userId; + @UserIdInt int userIdWhenUidUnknown; Delegate delegate = null; ArrayMap<String, NotificationChannel> channels = new ArrayMap<>(); diff --git a/services/core/java/com/android/server/pm/Computer.java b/services/core/java/com/android/server/pm/Computer.java index 3528d3d96c2b..8a35006e0f6a 100644 --- a/services/core/java/com/android/server/pm/Computer.java +++ b/services/core/java/com/android/server/pm/Computer.java @@ -487,6 +487,20 @@ public interface Computer extends PackageDataSnapshot { ProviderInfo resolveContentProvider(@NonNull String name, @PackageManager.ResolveInfoFlagsBits long flags, @UserIdInt int userId, int callingUid); + /** + * Resolves a ContentProvider on behalf of a UID + * @param name Authority of the content provider + * @param flags option flags to modify the data returned. + * @param userId Current user ID + * @param filterCallingUid UID of the caller who's access to the content provider + * is to be checked + * @return + */ + @Nullable + ProviderInfo resolveContentProviderForUid(@NonNull String name, + @PackageManager.ResolveInfoFlagsBits long flags, @UserIdInt int userId, + int filterCallingUid); + @Nullable ProviderInfo getGrantImplicitAccessProviderInfo(int recipientUid, @NonNull String visibleAuthority); diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index be2f58dc276c..38617621bf89 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -4749,6 +4749,38 @@ public class ComputerEngine implements Computer { @Nullable @Override + public ProviderInfo resolveContentProviderForUid(@NonNull String name, + @PackageManager.ResolveInfoFlagsBits long flags, @UserIdInt int userId, + int filterCallingUid) { + mContext.enforceCallingOrSelfPermission(Manifest.permission.RESOLVE_COMPONENT_FOR_UID, + "resolveContentProviderForUid"); + + int callingUid = Binder.getCallingUid(); + int filterUserId = UserHandle.getUserId(filterCallingUid); + enforceCrossUserPermission(callingUid, filterUserId, false, false, + "resolveContentProviderForUid"); + + // Real callingUid should be able to see filterCallingUid + if (filterAppAccess(filterCallingUid, callingUid)) { + return null; + } + + ProviderInfo pInfo = resolveContentProvider(name, flags, userId, filterCallingUid); + if (pInfo == null) { + return null; + } + // Real callingUid should be able to see the ContentProvider accessible to filterCallingUid + ProviderInfo pInfo2 = resolveContentProvider(name, flags, userId, callingUid); + if (pInfo2 != null + && Objects.equals(pInfo.name, pInfo2.name) + && Objects.equals(pInfo.authority, pInfo2.authority)) { + return pInfo; + } + return null; + } + + @Nullable + @Override public ProviderInfo resolveContentProvider(@NonNull String name, @PackageManager.ResolveInfoFlagsBits long flags, @UserIdInt int userId, int callingUid) { diff --git a/services/core/java/com/android/server/pm/IPackageManagerBase.java b/services/core/java/com/android/server/pm/IPackageManagerBase.java index f05c54d666df..b11d3499d391 100644 --- a/services/core/java/com/android/server/pm/IPackageManagerBase.java +++ b/services/core/java/com/android/server/pm/IPackageManagerBase.java @@ -1129,6 +1129,12 @@ public abstract class IPackageManagerBase extends IPackageManager.Stub { } @Override + public final ProviderInfo resolveContentProviderForUid(String name, + @PackageManager.ResolveInfoFlagsBits long flags, int userId, int filterCallingUid) { + return snapshot().resolveContentProviderForUid(name, flags, userId, filterCallingUid); + } + + @Override @Deprecated public final void resetApplicationPreferences(int userId) { mPreferredActivityHelper.resetApplicationPreferences(userId); diff --git a/services/core/java/com/android/server/pm/permission/AccessCheckDelegate.java b/services/core/java/com/android/server/pm/permission/AccessCheckDelegate.java index e9cb279439a6..e989d6875d15 100644 --- a/services/core/java/com/android/server/pm/permission/AccessCheckDelegate.java +++ b/services/core/java/com/android/server/pm/permission/AccessCheckDelegate.java @@ -40,7 +40,7 @@ import com.android.internal.util.ArrayUtils; import com.android.internal.util.function.DodecFunction; import com.android.internal.util.function.HexConsumer; import com.android.internal.util.function.HexFunction; -import com.android.internal.util.function.OctFunction; +import com.android.internal.util.function.NonaFunction; import com.android.internal.util.function.QuadFunction; import com.android.internal.util.function.TriFunction; import com.android.internal.util.function.UndecFunction; @@ -351,22 +351,22 @@ public interface AccessCheckDelegate extends CheckPermissionDelegate, CheckOpsDe @Override public SyncNotedAppOp noteOperation(int code, int uid, @Nullable String packageName, @Nullable String featureId, int virtualDeviceId, boolean shouldCollectAsyncNotedOp, - @Nullable String message, boolean shouldCollectMessage, - @NonNull OctFunction<Integer, Integer, String, String, Integer, Boolean, String, - Boolean, SyncNotedAppOp> superImpl) { + @Nullable String message, boolean shouldCollectMessage, int notedCount, + @NonNull NonaFunction<Integer, Integer, String, String, Integer, Boolean, String, + Boolean, Integer, SyncNotedAppOp> superImpl) { if (uid == mDelegateAndOwnerUid && isDelegateOp(code)) { final int shellUid = UserHandle.getUid(UserHandle.getUserId(uid), Process.SHELL_UID); final long identity = Binder.clearCallingIdentity(); try { return superImpl.apply(code, shellUid, SHELL_PKG, featureId, virtualDeviceId, - shouldCollectAsyncNotedOp, message, shouldCollectMessage); + shouldCollectAsyncNotedOp, message, shouldCollectMessage, notedCount); } finally { Binder.restoreCallingIdentity(identity); } } return superImpl.apply(code, uid, packageName, featureId, virtualDeviceId, - shouldCollectAsyncNotedOp, message, shouldCollectMessage); + shouldCollectAsyncNotedOp, message, shouldCollectMessage, notedCount); } @Override diff --git a/services/core/java/com/android/server/policy/AppOpsPolicy.java b/services/core/java/com/android/server/policy/AppOpsPolicy.java index 3f9144f0d980..dea52fd7cd99 100644 --- a/services/core/java/com/android/server/policy/AppOpsPolicy.java +++ b/services/core/java/com/android/server/policy/AppOpsPolicy.java @@ -53,7 +53,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.util.function.DodecFunction; import com.android.internal.util.function.HexConsumer; import com.android.internal.util.function.HexFunction; -import com.android.internal.util.function.OctFunction; +import com.android.internal.util.function.NonaFunction; import com.android.internal.util.function.QuadFunction; import com.android.internal.util.function.UndecFunction; import com.android.server.LocalServices; @@ -248,11 +248,12 @@ public final class AppOpsPolicy implements AppOpsManagerInternal.CheckOpsDelegat public SyncNotedAppOp noteOperation(int code, int uid, @Nullable String packageName, @Nullable String attributionTag, int virtualDeviceId, boolean shouldCollectAsyncNotedOp, @Nullable String message, - boolean shouldCollectMessage, @NonNull OctFunction<Integer, Integer, String, String, - Integer, Boolean, String, Boolean, SyncNotedAppOp> superImpl) { + boolean shouldCollectMessage, int notedCount, + @NonNull NonaFunction<Integer, Integer, String, String, + Integer, Boolean, String, Boolean, Integer, SyncNotedAppOp> superImpl) { return superImpl.apply(resolveDatasourceOp(code, uid, packageName, attributionTag), resolveUid(code, uid), packageName, attributionTag, virtualDeviceId, - shouldCollectAsyncNotedOp, message, shouldCollectMessage); + shouldCollectAsyncNotedOp, message, shouldCollectMessage, notedCount); } @Override diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index c4d1cc723804..ec0f25169d75 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -4068,7 +4068,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { @Nullable IBinder focusedToken) { boolean handled = PhoneWindowManager.this.handleKeyGestureEvent(event, focusedToken); - if (handled && Arrays.stream(event.getKeycodes()).anyMatch( + if (handled && !event.isCancelled() && Arrays.stream(event.getKeycodes()).anyMatch( (keycode) -> keycode == KeyEvent.KEYCODE_POWER)) { mPowerKeyHandled = true; } diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java index 0c3c46c75eee..7f88e7463208 100644 --- a/services/core/java/com/android/server/power/Notifier.java +++ b/services/core/java/com/android/server/power/Notifier.java @@ -479,6 +479,7 @@ public class Notifier { case PowerManager.PARTIAL_WAKE_LOCK: return BatteryStats.WAKE_TYPE_PARTIAL; + case PowerManager.FULL_WAKE_LOCK: case PowerManager.SCREEN_DIM_WAKE_LOCK: case PowerManager.SCREEN_BRIGHT_WAKE_LOCK: return BatteryStats.WAKE_TYPE_FULL; @@ -503,6 +504,31 @@ public class Notifier { } } + @VisibleForTesting + int getWakelockMonitorTypeForLogging(int flags) { + switch (flags & PowerManager.WAKE_LOCK_LEVEL_MASK) { + case PowerManager.FULL_WAKE_LOCK, PowerManager.SCREEN_DIM_WAKE_LOCK, + PowerManager.SCREEN_BRIGHT_WAKE_LOCK: + return PowerManager.FULL_WAKE_LOCK; + case PowerManager.DRAW_WAKE_LOCK: + return PowerManager.DRAW_WAKE_LOCK; + case PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK: + if (mSuspendWhenScreenOffDueToProximityConfig) { + return -1; + } + return PowerManager.PARTIAL_WAKE_LOCK; + case PowerManager.PARTIAL_WAKE_LOCK: + return PowerManager.PARTIAL_WAKE_LOCK; + case PowerManager.DOZE_WAKE_LOCK: + // Doze wake locks are an internal implementation detail of the + // communication between dream manager service and power manager + // service. They have no additive battery impact. + return -1; + default: + return -1; + } + } + /** * Notifies that the device is changing wakefulness. * This function may be called even if the previous change hasn't finished in @@ -1288,7 +1314,7 @@ public class Notifier { if (mBatteryStatsInternal == null) { return; } - final int type = flags & PowerManager.WAKE_LOCK_LEVEL_MASK; + final int type = getWakelockMonitorTypeForLogging(flags); if (workSource == null || workSource.isEmpty()) { final int mappedUid = mBatteryStatsInternal.getOwnerUid(ownerUid); mFrameworkStatsLogger.wakelockStateChanged(mappedUid, tag, type, eventType); diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java index 83461125b404..a0bc77e939d1 100644 --- a/services/core/java/com/android/server/power/hint/HintManagerService.java +++ b/services/core/java/com/android/server/power/hint/HintManagerService.java @@ -24,6 +24,7 @@ import static com.android.server.power.hint.Flags.cpuHeadroomAffinityCheck; import static com.android.server.power.hint.Flags.powerhintThreadCleanup; import static com.android.server.power.hint.Flags.resetOnForkEnabled; +import android.Manifest; import android.adpf.ISessionManager; import android.annotation.NonNull; import android.annotation.Nullable; @@ -59,6 +60,9 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.SessionCreationConfig; import android.os.SystemProperties; +import android.os.UserHandle; +import android.system.Os; +import android.system.OsConstants; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -79,7 +83,10 @@ import com.android.server.SystemService; import com.android.server.power.hint.HintManagerService.AppHintSession.SessionModes; import com.android.server.utils.Slogf; +import java.io.BufferedReader; import java.io.FileDescriptor; +import java.io.FileReader; +import java.io.IOException; import java.io.PrintWriter; import java.lang.reflect.Field; import java.util.ArrayList; @@ -95,6 +102,8 @@ import java.util.Set; import java.util.TreeMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** An hint service implementation that runs in System Server process. */ public final class HintManagerService extends SystemService { @@ -103,10 +112,10 @@ public final class HintManagerService extends SystemService { private static final int EVENT_CLEAN_UP_UID = 3; @VisibleForTesting static final int CLEAN_UP_UID_DELAY_MILLIS = 1000; - // The minimum interval between the headroom calls as rate limiting. - private static final int DEFAULT_GPU_HEADROOM_INTERVAL_MILLIS = 1000; - private static final int DEFAULT_CPU_HEADROOM_INTERVAL_MILLIS = 1000; + // example: cpu 2255 34 2290 22625563 6290 127 456 + private static final Pattern PROC_STAT_CPU_TIME_TOTAL_PATTERN = + Pattern.compile("cpu\\s+(?<user>[0-9]+)\\s(?<nice>[0-9]+).+"); @VisibleForTesting final long mHintSessionPreferredRate; @@ -192,10 +201,26 @@ public final class HintManagerService extends SystemService { private static final String PROPERTY_HWUI_ENABLE_HINT_MANAGER = "debug.hwui.use_hint_manager"; private static final String PROPERTY_USE_HAL_HEADROOMS = "persist.hms.use_hal_headrooms"; private static final String PROPERTY_CHECK_HEADROOM_TID = "persist.hms.check_headroom_tid"; - private static final String PROPERTY_CHECK_HEADROOM_AFFINITY = "persist.hms.check_affinity"; + private static final String PROPERTY_CHECK_HEADROOM_AFFINITY = + "persist.hms.check_headroom_affinity"; + private static final String PROPERTY_CHECK_HEADROOM_PROC_STAT_MIN_MILLIS = + "persist.hms.check_headroom_proc_stat_min_millis"; private Boolean mFMQUsesIntegratedEventFlag = false; private final Object mCpuHeadroomLock = new Object(); + @VisibleForTesting + final float mJiffyMillis; + private final int mCheckHeadroomProcStatMinMillis; + @GuardedBy("mCpuHeadroomLock") + private long mLastCpuUserModeTimeCheckedMillis = 0; + @GuardedBy("mCpuHeadroomLock") + private long mLastCpuUserModeJiffies = 0; + @GuardedBy("mCpuHeadroomLock") + private final Map<Integer, Long> mUidToLastUserModeJiffies; + @VisibleForTesting + private String mProcStatFilePathOverride = null; + @VisibleForTesting + private boolean mEnforceCpuHeadroomUserModeCpuTimeCheck = false; private ISessionManager mSessionManager; @@ -310,8 +335,16 @@ public final class HintManagerService extends SystemService { new GpuHeadroomParamsInternal().calculationWindowMillis; if (mSupportInfo.headroom.isCpuSupported) { mCpuHeadroomCache = new HeadroomCache<>(2, mSupportInfo.headroom.cpuMinIntervalMillis); + mUidToLastUserModeJiffies = new ArrayMap<>(); + long jiffyHz = Os.sysconf(OsConstants._SC_CLK_TCK); + mJiffyMillis = 1000.0f / jiffyHz; + mCheckHeadroomProcStatMinMillis = SystemProperties.getInt( + PROPERTY_CHECK_HEADROOM_PROC_STAT_MIN_MILLIS, 50); } else { mCpuHeadroomCache = null; + mUidToLastUserModeJiffies = null; + mJiffyMillis = 0.0f; + mCheckHeadroomProcStatMinMillis = 0; } if (mSupportInfo.headroom.isGpuSupported) { mGpuHeadroomCache = new HeadroomCache<>(2, mSupportInfo.headroom.gpuMinIntervalMillis); @@ -370,6 +403,12 @@ public final class HintManagerService extends SystemService { return supportInfo; } + @VisibleForTesting + void setProcStatPathOverride(String override) { + mProcStatFilePathOverride = override; + mEnforceCpuHeadroomUserModeCpuTimeCheck = true; + } + private ServiceThread createCleanUpThread() { final ServiceThread handlerThread = new ServiceThread(TAG, Process.THREAD_PRIORITY_LOWEST, true /*allowIo*/); @@ -851,6 +890,11 @@ public final class HintManagerService extends SystemService { mChannelMap.remove(uid); } } + synchronized (mCpuHeadroomLock) { + if (mSupportInfo.headroom.isCpuSupported && mUidToLastUserModeJiffies != null) { + mUidToLastUserModeJiffies.remove(uid); + } + } }); } @@ -1230,7 +1274,7 @@ public final class HintManagerService extends SystemService { // Only call into AM if the tid is either isolated or invalid if (isolatedPids == null) { // To avoid deadlock, do not call into AMS if the call is from system. - if (uid == Process.SYSTEM_UID) { + if (UserHandle.getAppId(uid) == Process.SYSTEM_UID) { return tid; } isolatedPids = mAmInternal.getIsolatedProcesses(uid); @@ -1485,14 +1529,17 @@ public final class HintManagerService extends SystemService { throw new UnsupportedOperationException(); } checkCpuHeadroomParams(params); + final int uid = Binder.getCallingUid(); + final int pid = Binder.getCallingPid(); final CpuHeadroomParams halParams = new CpuHeadroomParams(); - halParams.tids = new int[]{Binder.getCallingPid()}; + halParams.tids = new int[]{pid}; halParams.calculationType = params.calculationType; halParams.calculationWindowMillis = params.calculationWindowMillis; if (params.usesDeviceHeadroom) { halParams.tids = new int[]{}; } else if (params.tids != null && params.tids.length > 0) { - if (SystemProperties.getBoolean(PROPERTY_CHECK_HEADROOM_TID, true)) { + if (UserHandle.getAppId(uid) != Process.SYSTEM_UID && SystemProperties.getBoolean( + PROPERTY_CHECK_HEADROOM_TID, true)) { final int tgid = Process.getThreadGroupLeader(Binder.getCallingPid()); for (int tid : params.tids) { if (Process.getThreadGroupLeader(tid) != tgid) { @@ -1515,6 +1562,20 @@ public final class HintManagerService extends SystemService { if (res != null) return res; } } + final boolean shouldCheckUserModeCpuTime = + mEnforceCpuHeadroomUserModeCpuTimeCheck + || (UserHandle.getAppId(uid) != Process.SYSTEM_UID + && mContext.checkCallingPermission( + Manifest.permission.DEVICE_POWER) + == PackageManager.PERMISSION_DENIED); + + if (shouldCheckUserModeCpuTime) { + synchronized (mCpuHeadroomLock) { + if (!checkPerUidUserModeCpuTimeElapsedLocked(uid)) { + return null; + } + } + } // return from HAL directly try { final CpuHeadroomResult result = mPowerHal.getCpuHeadroom(halParams); @@ -1528,6 +1589,11 @@ public final class HintManagerService extends SystemService { mCpuHeadroomCache.add(halParams, result); } } + if (shouldCheckUserModeCpuTime) { + synchronized (mCpuHeadroomLock) { + mUidToLastUserModeJiffies.put(uid, mLastCpuUserModeJiffies); + } + } return result; } catch (RemoteException e) { Slog.e(TAG, "Failed to get CPU headroom from Power HAL", e); @@ -1556,6 +1622,40 @@ public final class HintManagerService extends SystemService { } } + // check if there has been sufficient user mode cpu time elapsed since last call + // from the same uid + @GuardedBy("mCpuHeadroomLock") + private boolean checkPerUidUserModeCpuTimeElapsedLocked(int uid) { + // skip checking proc stat if it's within mCheckHeadroomProcStatMinMillis + if (System.currentTimeMillis() - mLastCpuUserModeTimeCheckedMillis + > mCheckHeadroomProcStatMinMillis) { + try { + mLastCpuUserModeJiffies = getUserModeJiffies(); + } catch (Exception e) { + Slog.e(TAG, "Failed to get user mode CPU time", e); + return false; + } + mLastCpuUserModeTimeCheckedMillis = System.currentTimeMillis(); + } + if (mUidToLastUserModeJiffies.containsKey(uid)) { + long uidLastUserModeJiffies = mUidToLastUserModeJiffies.get(uid); + if ((mLastCpuUserModeJiffies - uidLastUserModeJiffies) * mJiffyMillis + < mSupportInfo.headroom.cpuMinIntervalMillis) { + Slog.w(TAG, "UID " + uid + " is requesting CPU headroom too soon"); + Slog.d(TAG, "UID " + uid + " last request at " + + uidLastUserModeJiffies * mJiffyMillis + + "ms with device currently at " + + mLastCpuUserModeJiffies * mJiffyMillis + + "ms, the interval: " + + (mLastCpuUserModeJiffies - uidLastUserModeJiffies) + * mJiffyMillis + "ms is less than require minimum interval " + + mSupportInfo.headroom.cpuMinIntervalMillis + "ms"); + return false; + } + } + return true; + } + private void checkCpuHeadroomParams(CpuHeadroomParamsInternal params) { boolean calculationTypeMatched = false; try { @@ -1731,6 +1831,27 @@ public final class HintManagerService extends SystemService { } } + private long getUserModeJiffies() throws IOException { + String filePath = + mProcStatFilePathOverride == null ? "/proc/stat" : mProcStatFilePathOverride; + try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { + String line; + while ((line = reader.readLine()) != null) { + Matcher matcher = PROC_STAT_CPU_TIME_TOTAL_PATTERN.matcher(line.trim()); + if (matcher.find()) { + long userJiffies = Long.parseLong(matcher.group("user")); + long niceJiffies = Long.parseLong(matcher.group("nice")); + Slog.d(TAG, + "user: " + userJiffies + " nice: " + niceJiffies + + " total " + (userJiffies + niceJiffies)); + reader.close(); + return userJiffies + niceJiffies; + } + } + } + throw new IllegalStateException("Can't find cpu line in " + filePath); + } + private boolean checkGraphicsPipelineValid(SessionCreationConfig creationConfig, int uid) { if (creationConfig.modesToEnable == null) { return true; diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index f8877ad912b5..c18918fd9f8b 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -2175,6 +2175,19 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } } + /** + * Called when the notification should be unbundled. + * @param key the notification key + */ + @Override + public void unbundleNotification(@Nullable String key) { + enforceStatusBarService(); + enforceValidCallingUser(); + Binder.withCleanCallingIdentity(() -> { + mNotificationDelegate.unbundleNotification(key); + }); + } + @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, diff --git a/services/core/java/com/android/server/storage/CacheQuotaStrategy.java b/services/core/java/com/android/server/storage/CacheQuotaStrategy.java index dad3a784cfb9..bb163ef4c1d5 100644 --- a/services/core/java/com/android/server/storage/CacheQuotaStrategy.java +++ b/services/core/java/com/android/server/storage/CacheQuotaStrategy.java @@ -369,10 +369,9 @@ public class CacheQuotaStrategy implements RemoteCallback.OnResultListener { tagName = parser.getName(); if (TAG_QUOTA.equals(tagName)) { CacheQuotaHint request = getRequestFromXml(parser); - if (request == null) { - continue; + if (request != null) { + quotas.add(request); } - quotas.add(request); } } eventType = parser.next(); diff --git a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java index 19eba5fe5755..90c2216f6b22 100644 --- a/services/core/java/com/android/server/wm/AbsAppSnapshotController.java +++ b/services/core/java/com/android/server/wm/AbsAppSnapshotController.java @@ -51,8 +51,11 @@ import android.window.TaskSnapshot; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.ColorUtils; import com.android.server.wm.utils.InsetUtils; +import com.android.window.flags.Flags; import java.io.PrintWriter; +import java.util.function.Consumer; +import java.util.function.Supplier; /** * Base class for a Snapshot controller @@ -148,43 +151,60 @@ abstract class AbsAppSnapshotController<TYPE extends WindowContainer, protected abstract Rect getLetterboxInsets(ActivityRecord topActivity); /** - * This is different than {@link #recordSnapshotInner(TYPE)} because it doesn't store - * the snapshot to the cache and returns the TaskSnapshot immediately. - * - * This is only used for testing so the snapshot content can be verified. + * This is different than {@link #recordSnapshotInner(TYPE, boolean, Consumer)} because it + * doesn't store the snapshot to the cache and returns the TaskSnapshot immediately. */ @VisibleForTesting - TaskSnapshot captureSnapshot(TYPE source) { - final TaskSnapshot snapshot; + SnapshotSupplier captureSnapshot(TYPE source, boolean allowAppTheme) { + final SnapshotSupplier supplier = new SnapshotSupplier(); switch (getSnapshotMode(source)) { - case SNAPSHOT_MODE_NONE: - return null; case SNAPSHOT_MODE_APP_THEME: - snapshot = drawAppThemeSnapshot(source); + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "drawAppThemeSnapshot"); + if (Flags.excludeDrawingAppThemeSnapshotFromLock()) { + if (allowAppTheme) { + supplier.setSupplier(drawAppThemeSnapshot(source)); + } + } else { + final Supplier<TaskSnapshot> original = drawAppThemeSnapshot(source); + final TaskSnapshot snapshot = original != null ? original.get() : null; + supplier.setSnapshot(snapshot); + } + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); break; case SNAPSHOT_MODE_REAL: - snapshot = snapshot(source); + supplier.setSnapshot(snapshot(source)); break; default: - snapshot = null; break; } - return snapshot; + return supplier; } - final TaskSnapshot recordSnapshotInner(TYPE source) { + /** + * @param allowAppTheme If true, allows to draw app theme snapshot when it's not allowed to take + * a real screenshot, but create a fake representation of the app. + * @param inLockConsumer Extra task to do in WM lock when first get the snapshot object. + */ + final SnapshotSupplier recordSnapshotInner(TYPE source, boolean allowAppTheme, + @Nullable Consumer<TaskSnapshot> inLockConsumer) { if (shouldDisableSnapshots()) { return null; } - final TaskSnapshot snapshot = captureSnapshot(source); - if (snapshot == null) { - return null; - } - mCache.putSnapshot(source, snapshot); - return snapshot; + final SnapshotSupplier supplier = captureSnapshot(source, allowAppTheme); + supplier.setConsumer(t -> { + synchronized (mService.mGlobalLock) { + if (!source.isAttached()) { + return; + } + mCache.putSnapshot(source, t); + if (inLockConsumer != null) { + inLockConsumer.accept(t); + } + } + }); + return supplier; } - @VisibleForTesting int getSnapshotMode(TYPE source) { final int type = source.getActivityType(); if (type == ACTIVITY_TYPE_RECENTS || type == ACTIVITY_TYPE_DREAM) { @@ -400,7 +420,7 @@ abstract class AbsAppSnapshotController<TYPE extends WindowContainer, * If we are not allowed to take a real screenshot, this attempts to represent the app as best * as possible by using the theme's window background. */ - private TaskSnapshot drawAppThemeSnapshot(TYPE source) { + private Supplier<TaskSnapshot> drawAppThemeSnapshot(TYPE source) { final ActivityRecord topActivity = getTopActivity(source); if (topActivity == null) { return null; @@ -432,26 +452,46 @@ abstract class AbsAppSnapshotController<TYPE extends WindowContainer, decorPainter.setInsets(systemBarInsets); decorPainter.drawDecors(c /* statusBarExcludeFrame */, null /* alreadyDrawFrame */); node.end(c); - final Bitmap hwBitmap = ThreadedRenderer.createHardwareBitmap(node, width, height); - if (hwBitmap == null) { - return null; - } + final Rect contentInsets = new Rect(systemBarInsets); final Rect letterboxInsets = getLetterboxInsets(topActivity); InsetUtils.addInsets(contentInsets, letterboxInsets); - // Note, the app theme snapshot is never translucent because we enforce a non-translucent - // color above - final TaskSnapshot taskSnapshot = new TaskSnapshot( - System.currentTimeMillis() /* id */, - SystemClock.elapsedRealtimeNanos() /* captureTime */, - topActivity.mActivityComponent, hwBitmap.getHardwareBuffer(), - hwBitmap.getColorSpace(), mainWindow.getConfiguration().orientation, - mainWindow.getWindowConfiguration().getRotation(), new Point(taskWidth, taskHeight), - contentInsets, letterboxInsets, false /* isLowResolution */, - false /* isRealSnapshot */, source.getWindowingMode(), - attrs.insetsFlags.appearance, false /* isTranslucent */, false /* hasImeSurface */, - topActivity.getConfiguration().uiMode /* uiMode */); - return validateSnapshot(taskSnapshot); + + final TaskSnapshot.Builder builder = new TaskSnapshot.Builder(); + builder.setIsRealSnapshot(false); + builder.setId(System.currentTimeMillis()); + builder.setContentInsets(contentInsets); + builder.setLetterboxInsets(letterboxInsets); + + builder.setTopActivityComponent(topActivity.mActivityComponent); + // Note, the app theme snapshot is never translucent because we enforce a + // non-translucent color above. + builder.setIsTranslucent(false); + builder.setWindowingMode(source.getWindowingMode()); + builder.setAppearance(attrs.insetsFlags.appearance); + builder.setUiMode(topActivity.getConfiguration().uiMode); + + builder.setRotation(mainWindow.getWindowConfiguration().getRotation()); + builder.setOrientation(mainWindow.getConfiguration().orientation); + builder.setTaskSize(new Point(taskWidth, taskHeight)); + builder.setCaptureTime(SystemClock.elapsedRealtimeNanos()); + + return () -> { + try { + Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "drawAppThemeSnapshot_acquire"); + // Do not hold WM lock when calling to render thread. + final Bitmap hwBitmap = ThreadedRenderer.createHardwareBitmap(node, width, + height); + if (hwBitmap == null) { + return null; + } + builder.setSnapshot(hwBitmap.getHardwareBuffer()); + builder.setColorSpace(hwBitmap.getColorSpace()); + return validateSnapshot(builder.build()); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER); + } + }; } static Rect getSystemBarInsets(Rect frame, InsetsState state) { @@ -482,4 +522,45 @@ abstract class AbsAppSnapshotController<TYPE extends WindowContainer, pw.println(prefix + "mSnapshotEnabled=" + mSnapshotEnabled); mCache.dump(pw, prefix); } + + static class SnapshotSupplier implements Supplier<TaskSnapshot> { + + private TaskSnapshot mSnapshot; + private boolean mHasSet; + private Consumer<TaskSnapshot> mConsumer; + private Supplier<TaskSnapshot> mSupplier; + + /** Callback when the snapshot is get for the first time. */ + void setConsumer(@NonNull Consumer<TaskSnapshot> consumer) { + mConsumer = consumer; + } + + void setSupplier(@NonNull Supplier<TaskSnapshot> createSupplier) { + mSupplier = createSupplier; + } + + void setSnapshot(TaskSnapshot snapshot) { + mSnapshot = snapshot; + } + + void handleSnapshot() { + if (mHasSet) { + return; + } + mHasSet = true; + if (mSnapshot == null) { + mSnapshot = mSupplier != null ? mSupplier.get() : null; + } + if (mConsumer != null && mSnapshot != null) { + mConsumer.accept(mSnapshot); + } + } + + @Override + @Nullable + public TaskSnapshot get() { + handleSnapshot(); + return mSnapshot; + } + } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 83b273c04648..745d41dd0d3d 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -3898,11 +3898,18 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A final TaskFragment taskFragment = getTaskFragment(); if (next != null && taskFragment != null && taskFragment.isEmbedded()) { final TaskFragment organized = taskFragment.getOrganizedTaskFragment(); - final TaskFragment adjacent = - organized != null ? organized.getAdjacentTaskFragment() : null; - if (adjacent != null && next.isDescendantOf(adjacent) - && organized.topRunningActivity() == null) { - delayRemoval = organized.isDelayLastActivityRemoval(); + if (Flags.allowMultipleAdjacentTaskFragments()) { + delayRemoval = organized != null + && organized.topRunningActivity() == null + && organized.isDelayLastActivityRemoval() + && organized.forOtherAdjacentTaskFragments(next::isDescendantOf); + } else { + final TaskFragment adjacent = + organized != null ? organized.getAdjacentTaskFragment() : null; + if (adjacent != null && next.isDescendantOf(adjacent) + && organized.topRunningActivity() == null) { + delayRemoval = organized.isDelayLastActivityRemoval(); + } } } @@ -4880,15 +4887,25 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * @see #canShowWhenLocked(ActivityRecord) */ boolean canShowWhenLocked() { + if (!canShowWhenLocked(this)) { + return false; + } final TaskFragment taskFragment = getTaskFragment(); - if (taskFragment != null && taskFragment.getAdjacentTaskFragment() != null - && taskFragment.isEmbedded()) { - final TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment(); - final ActivityRecord r = adjacentTaskFragment.getTopNonFinishingActivity(); - return canShowWhenLocked(this) && canShowWhenLocked(r); - } else { - return canShowWhenLocked(this); + if (taskFragment == null || !taskFragment.hasAdjacentTaskFragment() + || !taskFragment.isEmbedded()) { + // No embedded adjacent that need to be checked. + return true; + } + + // Make sure the embedded adjacent can also be shown. + if (!Flags.allowMultipleAdjacentTaskFragments()) { + final ActivityRecord adjacentActivity = taskFragment.getAdjacentTaskFragment() + .getTopNonFinishingActivity(); + return canShowWhenLocked(adjacentActivity); } + final boolean hasAdjacentNotAllowToShow = taskFragment.forOtherAdjacentTaskFragments( + adjacentTF -> !canShowWhenLocked(adjacentTF.getTopNonFinishingActivity())); + return !hasAdjacentNotAllowToShow; } /** @@ -8528,8 +8545,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // If activity in fullscreen mode is letterboxed because of fixed orientation then bounds // are already calculated in resolveFixedOrientationConfiguration. // Don't apply aspect ratio if app is overridden to fullscreen by device user/manufacturer. - if (Flags.immersiveAppRepositioning() - && !mAppCompatController.getAppCompatAspectRatioPolicy() + if (!mAppCompatController.getAppCompatAspectRatioPolicy() .isLetterboxedForFixedOrientationAndAspectRatio() && !mAppCompatController.getAppCompatAspectRatioOverrides() .hasFullscreenOverride()) { @@ -8551,18 +8567,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A computeConfigByResolveHint(resolvedConfig, newParentConfiguration); } } - // If activity in fullscreen mode is letterboxed because of fixed orientation then bounds - // are already calculated in resolveFixedOrientationConfiguration, or if in size compat - // mode, it should already be calculated in resolveSizeCompatModeConfiguration. - // Don't apply aspect ratio if app is overridden to fullscreen by device user/manufacturer. - if (!Flags.immersiveAppRepositioning() - && !mAppCompatController.getAppCompatAspectRatioPolicy() - .isLetterboxedForFixedOrientationAndAspectRatio() - && !scmPolicy.isInSizeCompatModeForBounds() - && !mAppCompatController.getAppCompatAspectRatioOverrides() - .hasFullscreenOverride()) { - resolveAspectRatioRestriction(newParentConfiguration); - } if (isFixedOrientationLetterboxAllowed || scmPolicy.hasAppCompatDisplayInsetsWithoutInheritance() @@ -8819,9 +8823,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } boolean isImmersiveMode(@NonNull Rect parentBounds) { - if (!Flags.immersiveAppRepositioning()) { - return false; - } if (!mResolveConfigHint.mUseOverrideInsetsForConfig && mWmService.mFlags.mInsetsDecoupledConfiguration) { return false; @@ -10355,7 +10356,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } if (!isVisibleRequested()) { // TODO(b/294925498): Remove this finishing check once we have accurate ready tracking. - if (task != null && task.getPausingActivity() == this) { + if (task != null && task.getPausingActivity() == this + // Display is asleep, so nothing will be visible anyways. + && !mDisplayContent.isSleeping()) { // Visibility of starting activities isn't calculated until pause-complete, so if // this is not paused yet, don't consider it ready. return false; diff --git a/services/core/java/com/android/server/wm/ActivitySnapshotController.java b/services/core/java/com/android/server/wm/ActivitySnapshotController.java index 9aaa0e1cfd6b..cfd324830db5 100644 --- a/services/core/java/com/android/server/wm/ActivitySnapshotController.java +++ b/services/core/java/com/android/server/wm/ActivitySnapshotController.java @@ -38,6 +38,7 @@ import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider; import java.io.File; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.function.Supplier; /** * When an app token becomes invisible, we take a snapshot (bitmap) and put it into our cache. @@ -355,7 +356,9 @@ class ActivitySnapshotController extends AbsAppSnapshotController<ActivityRecord final int[] mixedCode = new int[size]; if (size == 1) { final ActivityRecord singleActivity = activity.get(0); - final TaskSnapshot snapshot = recordSnapshotInner(singleActivity); + final Supplier<TaskSnapshot> supplier = recordSnapshotInner(singleActivity, + false /* allowAppTheme */, null /* inLockConsumer */); + final TaskSnapshot snapshot = supplier != null ? supplier.get() : null; if (snapshot != null) { mixedCode[0] = getSystemHashCode(singleActivity); addUserSavedFile(singleActivity.mUserId, snapshot, mixedCode); diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 5eee8ece6a67..290f155bb4cd 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -314,6 +314,7 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.Supplier; /** * System service for managing activities and their containers (task, displays,... ). @@ -4038,6 +4039,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mAmInternal.enforceCallingPermission(READ_FRAME_BUFFER, "takeTaskSnapshot()"); final long ident = Binder.clearCallingIdentity(); try { + final Supplier<TaskSnapshot> supplier; synchronized (mGlobalLock) { final Task task = mRootWindowContainer.anyTaskForId(taskId, MATCH_ATTACHED_TASK_OR_RECENT_TASKS); @@ -4050,11 +4052,13 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { // be retrieved by recents. While if updateCache is false, the real snapshot will // always be taken and the snapshot won't be put into SnapshotPersister. if (updateCache) { - return mWindowManager.mTaskSnapshotController.recordSnapshot(task); + supplier = mWindowManager.mTaskSnapshotController + .getRecordSnapshotSupplier(task); } else { return mWindowManager.mTaskSnapshotController.snapshot(task); } } + return supplier != null ? supplier.get() : null; } finally { Binder.restoreCallingIdentity(ident); } @@ -6403,6 +6407,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { @Override public boolean shuttingDown(boolean booted, int timeout) { mShuttingDown = true; + mWindowManager.mSnapshotController.mTaskSnapshotController.prepareShutdown(); synchronized (mGlobalLock) { mRootWindowContainer.prepareForShutdown(); updateEventDispatchingLocked(booted); diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java index e8eae4f96a04..6a0de98c0ffa 100644 --- a/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioPolicy.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_EXCLUDE_PORTRAIT_FULLSCREEN; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_LARGE; @@ -36,6 +37,8 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Rect; +import com.android.window.flags.Flags; + /** * Encapsulate app compat policy logic related to aspect ratio. */ @@ -239,7 +242,14 @@ class AppCompatAspectRatioPolicy { || AppCompatUtils.isInVrUiMode(mActivityRecord.getConfiguration()) // TODO(b/232898850): Always respect aspect ratio requests. // Don't set aspect ratio for activity in ActivityEmbedding split. - || (organizedTf != null && !organizedTf.fillsParent())) { + || (organizedTf != null && !organizedTf.fillsParent()) + // Don't set aspect ratio for resizeable activities in freeform. + // {@link ActivityRecord#shouldCreateAppCompatDisplayInsets()} will be false for + // both activities that are naturally resizeable and activities that have been + // forced resizeable. + || (Flags.ignoreAspectRatioRestrictionsForResizeableFreeformActivities() + && task.getWindowingMode() == WINDOWING_MODE_FREEFORM + && !mActivityRecord.shouldCreateAppCompatDisplayInsets())) { return false; } diff --git a/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java b/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java index caff96ba4a9f..4fac81b06680 100644 --- a/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatReachabilityOverrides.java @@ -35,8 +35,6 @@ import android.annotation.NonNull; import android.content.res.Configuration; import android.graphics.Rect; -import com.android.window.flags.Flags; - /** * Encapsulate overrides and configurations about app compat reachability. */ @@ -157,33 +155,27 @@ class AppCompatReachabilityOverrides { } /** - * @return {@value true} if the vertical reachability should be allowed in case of + * @return {@code true} if the vertical reachability should be allowed in case of * thin letterboxing. */ boolean allowVerticalReachabilityForThinLetterbox() { - if (!Flags.disableThinLetterboxingPolicy()) { - return true; - } // When the flag is enabled we allow vertical reachability only if the // app is not thin letterboxed vertically. return !isVerticalThinLetterboxed(); } /** - * @return {@value true} if the horizontal reachability should be enabled in case of + * @return {@code true} if the horizontal reachability should be enabled in case of * thin letterboxing. */ boolean allowHorizontalReachabilityForThinLetterbox() { - if (!Flags.disableThinLetterboxingPolicy()) { - return true; - } // When the flag is enabled we allow horizontal reachability only if the // app is not thin pillarboxed. return !isHorizontalThinLetterboxed(); } /** - * @return {@value true} if the resulting app is letterboxed in a way defined as thin. + * @return {@code true} if the resulting app is letterboxed in a way defined as thin. */ boolean isVerticalThinLetterboxed() { final int thinHeight = mAppCompatConfiguration.getThinLetterboxHeightPx(); @@ -200,7 +192,7 @@ class AppCompatReachabilityOverrides { } /** - * @return {@value true} if the resulting app is pillarboxed in a way defined as thin. + * @return {@code true} if the resulting app is pillarboxed in a way defined as thin. */ boolean isHorizontalThinLetterboxed() { final int thinWidth = mAppCompatConfiguration.getThinLetterboxWidthPx(); diff --git a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java index f3b043bb51dd..d278dc3d1be7 100644 --- a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java @@ -28,8 +28,6 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.graphics.Rect; -import com.android.window.flags.Flags; - import java.io.PrintWriter; import java.util.function.DoubleSupplier; @@ -202,9 +200,7 @@ class AppCompatSizeCompatModePolicy { // saved here before resolved bounds are overridden below. final AppCompatAspectRatioPolicy aspectRatioPolicy = mActivityRecord.mAppCompatController .getAppCompatAspectRatioPolicy(); - final boolean useResolvedBounds = Flags.immersiveAppRepositioning() - ? aspectRatioPolicy.isAspectRatioApplied() - : aspectRatioPolicy.isLetterboxedForFixedOrientationAndAspectRatio(); + final boolean useResolvedBounds = aspectRatioPolicy.isAspectRatioApplied(); final Rect containerBounds = useResolvedBounds ? new Rect(resolvedBounds) : newParentConfiguration.windowConfiguration.getBounds(); diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java index 0369a0ff4c76..9f88bc952351 100644 --- a/services/core/java/com/android/server/wm/AppCompatUtils.java +++ b/services/core/java/com/android/server/wm/AppCompatUtils.java @@ -164,15 +164,13 @@ final class AppCompatUtils { appCompatTaskInfo.setIsFromLetterboxDoubleTap(reachabilityOverrides.isFromDoubleTap()); + appCompatTaskInfo.topActivityAppBounds.set(getAppBounds(top)); final boolean isTopActivityLetterboxed = top.areBoundsLetterboxed(); appCompatTaskInfo.setTopActivityLetterboxed(isTopActivityLetterboxed); if (isTopActivityLetterboxed) { final Rect bounds = top.getBounds(); - final Rect appBounds = getAppBounds(top); appCompatTaskInfo.topActivityLetterboxWidth = bounds.width(); appCompatTaskInfo.topActivityLetterboxHeight = bounds.height(); - appCompatTaskInfo.topActivityLetterboxAppWidth = appBounds.width(); - appCompatTaskInfo.topActivityLetterboxAppHeight = appBounds.height(); // TODO(b/379824541) Remove duplicate information. appCompatTaskInfo.topActivityLetterboxBounds = bounds; // We need to consider if letterboxed or pillarboxed. @@ -281,8 +279,7 @@ final class AppCompatUtils { info.topActivityLetterboxHorizontalPosition = TaskInfo.PROPERTY_VALUE_UNSET; info.topActivityLetterboxWidth = TaskInfo.PROPERTY_VALUE_UNSET; info.topActivityLetterboxHeight = TaskInfo.PROPERTY_VALUE_UNSET; - info.topActivityLetterboxAppHeight = TaskInfo.PROPERTY_VALUE_UNSET; - info.topActivityLetterboxAppWidth = TaskInfo.PROPERTY_VALUE_UNSET; + info.topActivityAppBounds.setEmpty(); info.topActivityLetterboxBounds = null; info.cameraCompatTaskInfo.freeformCameraCompatMode = CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_UNSPECIFIED; diff --git a/services/core/java/com/android/server/wm/AsyncRotationController.java b/services/core/java/com/android/server/wm/AsyncRotationController.java index dd1af0a497ca..6b6f0111305c 100644 --- a/services/core/java/com/android/server/wm/AsyncRotationController.java +++ b/services/core/java/com/android/server/wm/AsyncRotationController.java @@ -29,9 +29,6 @@ import android.view.SurfaceControl; import android.view.WindowManager; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; -import android.view.animation.AnimationUtils; - -import com.android.internal.R; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -687,11 +684,12 @@ class AsyncRotationController extends FadeAnimationController implements Consume @Override public Animation getFadeInAnimation() { + final Animation anim = super.getFadeInAnimation(); if (mHasScreenRotationAnimation) { // Use a shorter animation so it is easier to align with screen rotation animation. - return AnimationUtils.loadAnimation(mContext, R.anim.screen_rotate_0_enter); + anim.setDuration(getScaledDuration(SHORT_DURATION_MS)); } - return super.getFadeInAnimation(); + return anim; } @Override diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 1a7c6b70f007..fc0df645a2db 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -111,9 +111,7 @@ class BackNavigationController { } void onEmbeddedWindowGestureTransferred(@NonNull WindowState host) { - if (Flags.disallowAppProgressEmbeddedWindow()) { - mNavigationMonitor.onEmbeddedWindowGestureTransferred(host); - } + mNavigationMonitor.onEmbeddedWindowGestureTransferred(host); } /** @@ -215,7 +213,7 @@ class BackNavigationController { infoBuilder.setFocusedTaskId(currentTask.mTaskId); } boolean transferGestureToEmbedded = false; - if (Flags.disallowAppProgressEmbeddedWindow() && embeddedWindows != null) { + if (embeddedWindows != null) { for (int i = embeddedWindows.size() - 1; i >= 0; --i) { if (embeddedWindows.get(i).mGestureToEmbedded) { transferGestureToEmbedded = true; @@ -997,11 +995,9 @@ class BackNavigationController { /** * Handle the pending animation when the running transition finished, all the visibility change * has applied so ready to start pending predictive back animation. - * @param targets The final animation targets derived in transition. * @param finishedTransition The finished transition target. */ - void onTransitionFinish(ArrayList<Transition.ChangeInfo> targets, - @NonNull Transition finishedTransition) { + void onTransitionFinish(@NonNull Transition finishedTransition) { if (isMonitoringPrepareTransition(finishedTransition)) { if (mAnimationHandler.mPrepareCloseTransition == null) { clearBackAnimations(true /* cancel */); @@ -1049,14 +1045,6 @@ class BackNavigationController { return; } - // Ensure the final animation targets which hidden by transition could be visible. - for (int i = 0; i < targets.size(); i++) { - final WindowContainer wc = targets.get(i).mContainer; - if (wc.mSurfaceControl != null) { - wc.prepareSurfaces(); - } - } - // The pending builder could be cleared due to prepareSurfaces // => updateNonSystemOverlayWindowsVisibilityIfNeeded // => setForceHideNonSystemOverlayWindowIfNeeded diff --git a/services/core/java/com/android/server/wm/CameraStateMonitor.java b/services/core/java/com/android/server/wm/CameraStateMonitor.java index 3aa355869d85..00279921953d 100644 --- a/services/core/java/com/android/server/wm/CameraStateMonitor.java +++ b/services/core/java/com/android/server/wm/CameraStateMonitor.java @@ -110,8 +110,10 @@ class CameraStateMonitor { } void startListeningToCameraState() { - mCameraManager.registerAvailabilityCallback( - mWmService.mContext.getMainExecutor(), mAvailabilityCallback); + if (mCameraManager != null) { + mCameraManager.registerAvailabilityCallback( + mWmService.mContext.getMainExecutor(), mAvailabilityCallback); + } mIsRunning = true; } diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java index 258a87eae196..3c60d8296577 100644 --- a/services/core/java/com/android/server/wm/DragDropController.java +++ b/services/core/java/com/android/server/wm/DragDropController.java @@ -289,7 +289,8 @@ class DragDropController { transaction.setAlpha(surfaceControl, mDragState.mOriginalAlpha); transaction.show(surfaceControl); displayContent.reparentToOverlay(transaction, surfaceControl); - mDragState.updateDragSurfaceLocked(true, touchX, touchY); + mDragState.updateDragSurfaceLocked(true /* keepHandling */, + displayContent.getDisplayId(), touchX, touchY); if (SHOW_LIGHT_TRANSACTIONS) { Slog.i(TAG_WM, "<<< CLOSE TRANSACTION performDrag"); } @@ -483,10 +484,11 @@ class DragDropController { * Handles motion events. * @param keepHandling Whether if the drag operation is continuing or this is the last motion * event. + * @param displayId id of the display the X,Y coordinate is n. * @param newX X coordinate value in dp in the screen coordinate * @param newY Y coordinate value in dp in the screen coordinate */ - void handleMotionEvent(boolean keepHandling, float newX, float newY) { + void handleMotionEvent(boolean keepHandling, int displayId, float newX, float newY) { synchronized (mService.mGlobalLock) { if (!dragDropActiveLocked()) { // The drag has ended but the clean-up message has not been processed by @@ -495,7 +497,7 @@ class DragDropController { return; } - mDragState.updateDragSurfaceLocked(keepHandling, newX, newY); + mDragState.updateDragSurfaceLocked(keepHandling, displayId, newX, newY); } } diff --git a/services/core/java/com/android/server/wm/DragInputEventReceiver.java b/services/core/java/com/android/server/wm/DragInputEventReceiver.java index 5372d8b6e796..8f4548fa4fcb 100644 --- a/services/core/java/com/android/server/wm/DragInputEventReceiver.java +++ b/services/core/java/com/android/server/wm/DragInputEventReceiver.java @@ -22,13 +22,13 @@ import static android.view.MotionEvent.ACTION_DOWN; import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_UP; import static android.view.MotionEvent.BUTTON_STYLUS_PRIMARY; + import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DRAG; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import android.os.Looper; import android.util.Slog; import android.view.InputChannel; -import android.view.InputDevice; import android.view.InputEvent; import android.view.InputEventReceiver; import android.view.MotionEvent; @@ -63,6 +63,7 @@ class DragInputEventReceiver extends InputEventReceiver { return; } final MotionEvent motionEvent = (MotionEvent) event; + final int displayId = motionEvent.getDisplayId(); final float newX = motionEvent.getRawX(); final float newY = motionEvent.getRawY(); final boolean isStylusButtonDown = @@ -102,7 +103,8 @@ class DragInputEventReceiver extends InputEventReceiver { return; } - mDragDropController.handleMotionEvent(!mMuteInput /* keepHandling */, newX, newY); + mDragDropController.handleMotionEvent(!mMuteInput /* keepHandling */, displayId, newX, + newY); handled = true; } catch (Exception e) { Slog.e(TAG_WM, "Exception caught by drag handleMotion", e); diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java index 1c4e487d2e7e..3a0e41a5f9f8 100644 --- a/services/core/java/com/android/server/wm/DragState.java +++ b/services/core/java/com/android/server/wm/DragState.java @@ -113,8 +113,8 @@ class DragState { boolean mRelinquishDragSurfaceToDropTarget; float mAnimatedScale = 1.0f; float mOriginalAlpha; - float mOriginalX, mOriginalY; - float mCurrentX, mCurrentY; + float mOriginalDisplayX, mOriginalDisplayY; + float mCurrentDisplayX, mCurrentDisplayY; float mThumbOffsetX, mThumbOffsetY; InputInterceptor mInputInterceptor; ArrayList<WindowState> mNotifiedWindows; @@ -230,22 +230,22 @@ class DragState { if (mDragInProgress) { if (DEBUG_DRAG) Slog.d(TAG_WM, "Broadcasting DRAG_ENDED"); for (WindowState ws : mNotifiedWindows) { - float x = 0; - float y = 0; + float inWindowX = 0; + float inWindowY = 0; SurfaceControl dragSurface = null; if (!mDragResult && (ws.mSession.mPid == mPid)) { // Report unconsumed drop location back to the app that started the drag. - x = ws.translateToWindowX(mCurrentX); - y = ws.translateToWindowY(mCurrentY); + inWindowX = ws.translateToWindowX(mCurrentDisplayX); + inWindowY = ws.translateToWindowY(mCurrentDisplayY); if (relinquishDragSurfaceToDragSource()) { // If requested (and allowed), report the drag surface back to the app // starting the drag to handle the return animation dragSurface = mSurfaceControl; } } - DragEvent event = DragEvent.obtain(DragEvent.ACTION_DRAG_ENDED, x, y, - mThumbOffsetX, mThumbOffsetY, mFlags, null, null, null, dragSurface, null, - mDragResult); + DragEvent event = DragEvent.obtain(DragEvent.ACTION_DRAG_ENDED, inWindowX, + inWindowY, mThumbOffsetX, mThumbOffsetY, mFlags, null, null, null, + dragSurface, null, mDragResult); try { if (DEBUG_DRAG) Slog.d(TAG_WM, "Sending DRAG_ENDED to " + ws); ws.mClient.dispatchDragEvent(event); @@ -297,70 +297,71 @@ class DragState { } /** - * Creates the drop event for this drag gesture. If `touchedWin` is null, then the drop event - * will be created for dispatching to the unhandled drag and the drag surface will be provided - * as a part of the dispatched event. + * Creates the drop event for dispatching to the unhandled drag. + * TODO(b/384841906): Update `inWindowX` and `inWindowY` to be display-coordinate. */ - private DragEvent createDropEvent(float x, float y, @Nullable WindowState touchedWin, - boolean includePrivateInfo) { - if (touchedWin != null) { - final int targetUserId = UserHandle.getUserId(touchedWin.getOwningUid()); - final DragAndDropPermissionsHandler dragAndDropPermissions; - if ((mFlags & View.DRAG_FLAG_GLOBAL) != 0 && (mFlags & DRAG_FLAGS_URI_ACCESS) != 0 - && mData != null) { - dragAndDropPermissions = new DragAndDropPermissionsHandler(mService.mGlobalLock, - mData, - mUid, - touchedWin.getOwningPackage(), - mFlags & DRAG_FLAGS_URI_PERMISSIONS, - mSourceUserId, - targetUserId); - } else { - dragAndDropPermissions = null; - } - if (mSourceUserId != targetUserId) { - if (mData != null) { - mData.fixUris(mSourceUserId); - } - } - final boolean targetInterceptsGlobalDrag = targetInterceptsGlobalDrag(touchedWin); - return obtainDragEvent(DragEvent.ACTION_DROP, x, y, mDataDescription, mData, - /* includeDragSurface= */ targetInterceptsGlobalDrag, - /* includeDragFlags= */ targetInterceptsGlobalDrag, - dragAndDropPermissions); + private DragEvent createUnhandledDropEvent(float inWindowX, float inWindowY) { + return obtainDragEvent(DragEvent.ACTION_DROP, inWindowX, inWindowY, mDataDescription, mData, + /* includeDragSurface= */ true, + /* includeDragFlags= */ true, null /* dragAndDropPermissions */); + } + + /** + * Creates the drop event for this drag gesture. + */ + private DragEvent createDropEvent(float inWindowX, float inWindowY, WindowState touchedWin) { + final int targetUserId = UserHandle.getUserId(touchedWin.getOwningUid()); + final DragAndDropPermissionsHandler dragAndDropPermissions; + if ((mFlags & View.DRAG_FLAG_GLOBAL) != 0 && (mFlags & DRAG_FLAGS_URI_ACCESS) != 0 + && mData != null) { + dragAndDropPermissions = new DragAndDropPermissionsHandler(mService.mGlobalLock, mData, + mUid, touchedWin.getOwningPackage(), mFlags & DRAG_FLAGS_URI_PERMISSIONS, + mSourceUserId, targetUserId); } else { - return obtainDragEvent(DragEvent.ACTION_DROP, x, y, mDataDescription, mData, - /* includeDragSurface= */ includePrivateInfo, - /* includeDragFlags= */ includePrivateInfo, - null /* dragAndDropPermissions */); + dragAndDropPermissions = null; } + if (mSourceUserId != targetUserId) { + if (mData != null) { + mData.fixUris(mSourceUserId); + } + } + final boolean targetInterceptsGlobalDrag = targetInterceptsGlobalDrag(touchedWin); + return obtainDragEvent(DragEvent.ACTION_DROP, inWindowX, inWindowY, mDataDescription, mData, + /* includeDragSurface= */ targetInterceptsGlobalDrag, + /* includeDragFlags= */ targetInterceptsGlobalDrag, dragAndDropPermissions); } /** * Notify the drop target and tells it about the data. If the drop event is not sent to the * target, invokes {@code endDragLocked} after the unhandled drag listener gets a chance to * handle the drop. + * @param inWindowX if `token` refers to a dragEvent-accepting window, `inWindowX` will be + * inside the window, else values might be invalid (0, 0). + * @param inWindowY if `token` refers to a dragEvent-accepting window, `inWindowY` will be + * inside the window, else values might be invalid (0, 0). */ - boolean reportDropWindowLock(IBinder token, float x, float y) { + boolean reportDropWindowLock(IBinder token, float inWindowX, float inWindowY) { if (mAnimator != null) { return false; } try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "DragDropController#DROP"); - return reportDropWindowLockInner(token, x, y); + return reportDropWindowLockInner(token, inWindowX, inWindowY); } finally { Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); } } - private boolean reportDropWindowLockInner(IBinder token, float x, float y) { + private boolean reportDropWindowLockInner(IBinder token, float inWindowX, float inWindowY) { if (mAnimator != null) { return false; } final WindowState touchedWin = mService.mInputToWindowMap.get(token); - final DragEvent unhandledDropEvent = createDropEvent(x, y, null /* touchedWin */, - true /* includePrivateInfo */); + // TODO(b/384841906): The x, y here when sent to a window and unhandled, will still be + // relative to the window it was originally sent to. Need to update this to actually be + // display-coordinate. + final DragEvent unhandledDropEvent = createUnhandledDropEvent(inWindowX, inWindowY); if (!isWindowNotified(touchedWin)) { // Delegate to the unhandled drag listener as a first pass if (mDragDropController.notifyUnhandledDrop(unhandledDropEvent, "unhandled-drop")) { @@ -381,7 +382,7 @@ class DragState { if (DEBUG_DRAG) Slog.d(TAG_WM, "Sending DROP to " + touchedWin); final IBinder clientToken = touchedWin.mClient.asBinder(); - final DragEvent event = createDropEvent(x, y, touchedWin, false /* includePrivateInfo */); + final DragEvent event = createDropEvent(inWindowX, inWindowY, touchedWin); try { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "DragDropController#dispatchDrop"); touchedWin.mClient.dispatchDragEvent(event); @@ -486,8 +487,8 @@ class DragState { */ void broadcastDragStartedLocked(final float touchX, final float touchY) { Trace.instant(TRACE_TAG_WINDOW_MANAGER, "DragDropController#DRAG_STARTED"); - mOriginalX = mCurrentX = touchX; - mOriginalY = mCurrentY = touchY; + mOriginalDisplayX = mCurrentDisplayX = touchX; + mOriginalDisplayY = mCurrentDisplayY = touchY; // Cache a base-class instance of the clip metadata so that parceling // works correctly in calling out to the apps. @@ -636,7 +637,7 @@ class DragState { if (isWindowNotified(newWin)) { return; } - sendDragStartedLocked(newWin, mCurrentX, mCurrentY, + sendDragStartedLocked(newWin, mCurrentDisplayX, mCurrentDisplayY, containsApplicationExtras(mDataDescription)); } } @@ -685,12 +686,21 @@ class DragState { mAnimator = createCancelAnimationLocked(); } - void updateDragSurfaceLocked(boolean keepHandling, float x, float y) { + /** + * Updates the position of the drag surface. + * + * @param keepHandling whether to keep handling the drag. + * @param displayId the display ID of the drag surface. + * @param displayX the x-coordinate of the drag surface in the display's coordinate frame. + * @param displayY the y-coordinate of the drag surface in the display's coordinate frame. + */ + void updateDragSurfaceLocked(boolean keepHandling, int displayId, float displayX, + float displayY) { if (mAnimator != null) { return; } - mCurrentX = x; - mCurrentY = y; + mCurrentDisplayX = displayX; + mCurrentDisplayY = displayY; if (!keepHandling) { return; @@ -700,9 +710,10 @@ class DragState { if (SHOW_LIGHT_TRANSACTIONS) { Slog.i(TAG_WM, ">>> OPEN TRANSACTION notifyMoveLocked"); } - mTransaction.setPosition(mSurfaceControl, x - mThumbOffsetX, y - mThumbOffsetY).apply(); - ProtoLog.i(WM_SHOW_TRANSACTIONS, "DRAG %s: pos=(%d,%d)", mSurfaceControl, - (int) (x - mThumbOffsetX), (int) (y - mThumbOffsetY)); + mTransaction.setPosition(mSurfaceControl, displayX - mThumbOffsetX, + displayY - mThumbOffsetY).apply(); + ProtoLog.i(WM_SHOW_TRANSACTIONS, "DRAG %s: displayId=%d, pos=(%d,%d)", mSurfaceControl, + displayId, (int) (displayX - mThumbOffsetX), (int) (displayY - mThumbOffsetY)); } /** @@ -713,6 +724,12 @@ class DragState { return mDragInProgress; } + /** + * `x` and `y` here varies between local window coordinate, relative coordinate to another + * window and local display coordinate, all depending on the `action`. Please take a look + * at the callers to determine the type. + * TODO(b/384845022): Properly document the events sent based on the event type. + */ private DragEvent obtainDragEvent(int action, float x, float y, ClipDescription description, ClipData data, boolean includeDragSurface, boolean includeDragFlags, IDragAndDropPermissions dragAndDropPermissions) { @@ -728,34 +745,34 @@ class DragState { final long duration; if (mCallingTaskIdToHide != -1) { animator = ValueAnimator.ofPropertyValuesHolder( - PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_X, mCurrentX, mCurrentX), - PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_Y, mCurrentY, mCurrentY), + PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_X, mCurrentDisplayX, + mCurrentDisplayX), + PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_Y, mCurrentDisplayY, + mCurrentDisplayY), PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_SCALE, mAnimatedScale, mAnimatedScale), PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_ALPHA, mOriginalAlpha, 0f)); duration = MIN_ANIMATION_DURATION_MS; } else { animator = ValueAnimator.ofPropertyValuesHolder( - PropertyValuesHolder.ofFloat( - ANIMATED_PROPERTY_X, mCurrentX - mThumbOffsetX, - mOriginalX - mThumbOffsetX), - PropertyValuesHolder.ofFloat( - ANIMATED_PROPERTY_Y, mCurrentY - mThumbOffsetY, - mOriginalY - mThumbOffsetY), + PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_X, + mCurrentDisplayX - mThumbOffsetX, mOriginalDisplayX - mThumbOffsetX), + PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_Y, + mCurrentDisplayY - mThumbOffsetY, mOriginalDisplayY - mThumbOffsetY), PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_SCALE, mAnimatedScale, mAnimatedScale), - PropertyValuesHolder.ofFloat( - ANIMATED_PROPERTY_ALPHA, mOriginalAlpha, mOriginalAlpha / 2)); + PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_ALPHA, mOriginalAlpha, + mOriginalAlpha / 2)); - final float translateX = mOriginalX - mCurrentX; - final float translateY = mOriginalY - mCurrentY; + final float translateX = mOriginalDisplayX - mCurrentDisplayX; + final float translateY = mOriginalDisplayY - mCurrentDisplayY; // Adjust the duration to the travel distance. final double travelDistance = Math.sqrt( translateX * translateX + translateY * translateY); - final double displayDiagonal = - Math.sqrt(mDisplaySize.x * mDisplaySize.x + mDisplaySize.y * mDisplaySize.y); - duration = MIN_ANIMATION_DURATION_MS + (long) (travelDistance / displayDiagonal - * (MAX_ANIMATION_DURATION_MS - MIN_ANIMATION_DURATION_MS)); + final double displayDiagonal = Math.sqrt( + mDisplaySize.x * mDisplaySize.x + mDisplaySize.y * mDisplaySize.y); + duration = MIN_ANIMATION_DURATION_MS + (long) (travelDistance / displayDiagonal * ( + MAX_ANIMATION_DURATION_MS - MIN_ANIMATION_DURATION_MS)); } final AnimationListener listener = new AnimationListener(); @@ -771,18 +788,20 @@ class DragState { private ValueAnimator createCancelAnimationLocked() { final ValueAnimator animator; if (mCallingTaskIdToHide != -1) { - animator = ValueAnimator.ofPropertyValuesHolder( - PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_X, mCurrentX, mCurrentX), - PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_Y, mCurrentY, mCurrentY), + animator = ValueAnimator.ofPropertyValuesHolder( + PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_X, mCurrentDisplayX, + mCurrentDisplayX), + PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_Y, mCurrentDisplayY, + mCurrentDisplayY), PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_SCALE, mAnimatedScale, mAnimatedScale), PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_ALPHA, mOriginalAlpha, 0f)); } else { animator = ValueAnimator.ofPropertyValuesHolder( - PropertyValuesHolder.ofFloat( - ANIMATED_PROPERTY_X, mCurrentX - mThumbOffsetX, mCurrentX), - PropertyValuesHolder.ofFloat( - ANIMATED_PROPERTY_Y, mCurrentY - mThumbOffsetY, mCurrentY), + PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_X, + mCurrentDisplayX - mThumbOffsetX, mCurrentDisplayX), + PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_Y, + mCurrentDisplayY - mThumbOffsetY, mCurrentDisplayY), PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_SCALE, mAnimatedScale, 0), PropertyValuesHolder.ofFloat(ANIMATED_PROPERTY_ALPHA, mOriginalAlpha, 0)); } diff --git a/services/core/java/com/android/server/wm/FadeAnimationController.java b/services/core/java/com/android/server/wm/FadeAnimationController.java index 7af67e63f469..c60d3677319a 100644 --- a/services/core/java/com/android/server/wm/FadeAnimationController.java +++ b/services/core/java/com/android/server/wm/FadeAnimationController.java @@ -20,41 +20,51 @@ import static com.android.server.wm.AnimationSpecProto.WINDOW; import static com.android.server.wm.WindowAnimationSpecProto.ANIMATION; import android.annotation.NonNull; -import android.content.Context; import android.util.proto.ProtoOutputStream; import android.view.SurfaceControl; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.AlphaAnimation; import android.view.animation.Animation; -import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; import android.view.animation.Transformation; -import com.android.internal.R; - import java.io.PrintWriter; /** * An animation controller to fade-in/out for a window token. */ public class FadeAnimationController { + static final int SHORT_DURATION_MS = 200; + static final int MEDIUM_DURATION_MS = 350; + protected final DisplayContent mDisplayContent; - protected final Context mContext; public FadeAnimationController(DisplayContent displayContent) { mDisplayContent = displayContent; - mContext = displayContent.mWmService.mContext; } /** * @return a fade-in Animation. */ public Animation getFadeInAnimation() { - return AnimationUtils.loadAnimation(mContext, R.anim.fade_in); + final AlphaAnimation anim = new AlphaAnimation(0f, 1f); + anim.setDuration(getScaledDuration(MEDIUM_DURATION_MS)); + anim.setInterpolator(new DecelerateInterpolator()); + return anim; } /** * @return a fade-out Animation. */ public Animation getFadeOutAnimation() { - return AnimationUtils.loadAnimation(mContext, R.anim.fade_out); + final AlphaAnimation anim = new AlphaAnimation(1f, 0f); + anim.setDuration(getScaledDuration(SHORT_DURATION_MS)); + anim.setInterpolator(new AccelerateInterpolator()); + return anim; + } + + long getScaledDuration(int durationMs) { + return (long) (durationMs * mDisplayContent.mWmService.getWindowAnimationScaleLocked()); } /** Run the fade in/out animation for a window token. */ diff --git a/services/core/java/com/android/server/wm/InsetsStateController.java b/services/core/java/com/android/server/wm/InsetsStateController.java index cf145f94f787..ce8518449230 100644 --- a/services/core/java/com/android/server/wm/InsetsStateController.java +++ b/services/core/java/com/android/server/wm/InsetsStateController.java @@ -374,12 +374,6 @@ class InsetsStateController { void notifyControlChanged(InsetsControlTarget target, InsetsSourceProvider provider) { addToPendingControlMaps(target, provider); notifyPendingInsetsControlChanged(); - - if (android.view.inputmethod.Flags.refactorInsetsController()) { - notifyInsetsChanged(); - mDisplayContent.updateSystemGestureExclusion(); - mDisplayContent.getDisplayPolicy().updateSystemBarAttributes(); - } } void notifySurfaceTransactionReady(InsetsSourceProvider provider, long id, boolean ready) { diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 3d2868540334..865d5facc4a4 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -2926,7 +2926,6 @@ class RootWindowContainer extends WindowContainer<DisplayContent> } void prepareForShutdown() { - mWindowManager.mSnapshotController.mTaskSnapshotController.prepareShutdown(); for (int i = 0; i < getChildCount(); i++) { createSleepToken("shutdown", getChildAt(i).mDisplayId); } diff --git a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java index 38e011509885..efc68aac0323 100644 --- a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java +++ b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java @@ -95,8 +95,9 @@ public class ScreenRecordingCallbackController { if (mediaProjectionInfo.getLaunchCookie() == null) { mRecordedWC = (WindowContainer) mWms.mRoot.getDefaultDisplay(); } else { - mRecordedWC = mWms.mRoot.getActivity(activity -> activity.mLaunchCookie - == mediaProjectionInfo.getLaunchCookie().binder).getTask(); + final ActivityRecord matchingActivity = mWms.mRoot.getActivity(activity -> + activity.mLaunchCookie == mediaProjectionInfo.getLaunchCookie().binder); + mRecordedWC = matchingActivity != null ? matchingActivity.getTask() : null; } } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 9062afb50acb..d92301ba4f6f 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3707,10 +3707,21 @@ class Task extends TaskFragment { // Boost the adjacent TaskFragment for dimmer if needed. final TaskFragment taskFragment = wc.asTaskFragment(); - if (taskFragment != null && taskFragment.isEmbedded()) { - final TaskFragment adjacentTf = taskFragment.getAdjacentTaskFragment(); - if (adjacentTf != null && adjacentTf.shouldBoostDimmer()) { - adjacentTf.assignLayer(t, layer++); + if (taskFragment != null && taskFragment.isEmbedded() + && taskFragment.hasAdjacentTaskFragment()) { + if (Flags.allowMultipleAdjacentTaskFragments()) { + final int[] nextLayer = { layer }; + taskFragment.forOtherAdjacentTaskFragments(adjacentTf -> { + if (adjacentTf.shouldBoostDimmer()) { + adjacentTf.assignLayer(t, nextLayer[0]++); + } + }); + layer = nextLayer[0]; + } else { + final TaskFragment adjacentTf = taskFragment.getAdjacentTaskFragment(); + if (adjacentTf.shouldBoostDimmer()) { + adjacentTf.assignLayer(t, layer++); + } } } diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index 9564c5959d98..3d0b41ba3a0f 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -1045,7 +1045,7 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { + adjacentFlagRootTask); } - if (adjacentFlagRootTask.getAdjacentTaskFragment() == null) { + if (!adjacentFlagRootTask.hasAdjacentTaskFragment()) { throw new UnsupportedOperationException( "Can't set non-adjacent root as launch adjacent flag root tr=" + adjacentFlagRootTask); diff --git a/services/core/java/com/android/server/wm/TaskSnapshotController.java b/services/core/java/com/android/server/wm/TaskSnapshotController.java index 38a2ebeba332..7d300e98f44b 100644 --- a/services/core/java/com/android/server/wm/TaskSnapshotController.java +++ b/services/core/java/com/android/server/wm/TaskSnapshotController.java @@ -36,7 +36,9 @@ import android.window.TaskSnapshot; import com.android.internal.annotations.VisibleForTesting; import com.android.server.policy.WindowManagerPolicy.ScreenOffListener; import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider; +import com.android.window.flags.Flags; +import java.util.ArrayList; import java.util.Set; /** @@ -154,6 +156,8 @@ class TaskSnapshotController extends AbsAppSnapshotController<Task, TaskSnapshot * The attributes of task snapshot are based on task configuration. But sometimes the * configuration may have been changed during a transition, so supply the ChangeInfo that * stored the previous appearance of the closing task. + * + * The snapshot won't be created immediately if it should be captured as fake snapshot. */ void recordSnapshot(Task task, Transition.ChangeInfo changeInfo) { mCurrentChangeInfo = changeInfo; @@ -164,13 +168,35 @@ class TaskSnapshotController extends AbsAppSnapshotController<Task, TaskSnapshot } } - TaskSnapshot recordSnapshot(Task task) { - final TaskSnapshot snapshot = recordSnapshotInner(task); - if (snapshot != null && !task.isActivityTypeHome()) { - mPersister.persistSnapshot(task.mTaskId, task.mUserId, snapshot); - task.onSnapshotChanged(snapshot); + void recordSnapshot(Task task) { + if (shouldDisableSnapshots()) { + return; + } + final SnapshotSupplier supplier = getRecordSnapshotSupplier(task); + if (supplier == null) { + return; } - return snapshot; + final int mode = getSnapshotMode(task); + if (Flags.excludeDrawingAppThemeSnapshotFromLock() && mode == SNAPSHOT_MODE_APP_THEME) { + mService.mH.post(supplier::handleSnapshot); + } else { + supplier.handleSnapshot(); + } + } + + /** + * Note that the snapshot is not created immediately, if the returned supplier is non-null, the + * caller must call {@link AbsAppSnapshotController.SnapshotSupplier#get} or + * {@link AbsAppSnapshotController.SnapshotSupplier#handleSnapshot} to complete the entire + * record request. + */ + SnapshotSupplier getRecordSnapshotSupplier(Task task) { + return recordSnapshotInner(task, true /* allowAppTheme */, snapshot -> { + if (!task.isActivityTypeHome()) { + mPersister.persistSnapshot(task.mTaskId, task.mUserId, snapshot); + task.onSnapshotChanged(snapshot); + } + }); } /** @@ -328,27 +354,38 @@ class TaskSnapshotController extends AbsAppSnapshotController<Task, TaskSnapshot * Record task snapshots before shutdown. */ void prepareShutdown() { - if (!com.android.window.flags.Flags.recordTaskSnapshotsBeforeShutdown()) { + if (!Flags.recordTaskSnapshotsBeforeShutdown()) { return; } - // Make write items run in a batch. - mPersister.mSnapshotPersistQueue.setPaused(true); - mPersister.mSnapshotPersistQueue.prepareShutdown(); - for (int i = 0; i < mService.mRoot.getChildCount(); i++) { - mService.mRoot.getChildAt(i).forAllLeafTasks(task -> { - if (task.isVisible() && !task.isActivityTypeHome()) { - final TaskSnapshot snapshot = captureSnapshot(task); - if (snapshot != null) { - mPersister.persistSnapshot(task.mTaskId, task.mUserId, snapshot); + final ArrayList<SnapshotSupplier> supplierArrayList = new ArrayList<>(); + synchronized (mService.mGlobalLock) { + // Make write items run in a batch. + mPersister.mSnapshotPersistQueue.setPaused(true); + mPersister.mSnapshotPersistQueue.prepareShutdown(); + for (int i = 0; i < mService.mRoot.getChildCount(); i++) { + mService.mRoot.getChildAt(i).forAllLeafTasks(task -> { + if (task.isVisible() && !task.isActivityTypeHome()) { + final SnapshotSupplier supplier = captureSnapshot(task, + true /* allowAppTheme */); + if (supplier != null) { + supplier.setConsumer(t -> + mPersister.persistSnapshot(task.mTaskId, task.mUserId, t)); + supplierArrayList.add(supplier); + } } - } - }, true /* traverseTopToBottom */); + }, true /* traverseTopToBottom */); + } + } + for (int i = supplierArrayList.size() - 1; i >= 0; --i) { + supplierArrayList.get(i).handleSnapshot(); + } + synchronized (mService.mGlobalLock) { + mPersister.mSnapshotPersistQueue.setPaused(false); } - mPersister.mSnapshotPersistQueue.setPaused(false); } void waitFlush(long timeout) { - if (!com.android.window.flags.Flags.recordTaskSnapshotsBeforeShutdown()) { + if (!Flags.recordTaskSnapshotsBeforeShutdown()) { return; } mPersister.mSnapshotPersistQueue.waitFlush(timeout); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index 1f539a129e7d..a3d71dbc5ed1 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -1589,7 +1589,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { cleanUpInternal(); // Handle back animation if it's already started. - mController.mAtm.mBackNavigationController.onTransitionFinish(mTargets, this); + mController.mAtm.mBackNavigationController.onTransitionFinish(this); mController.mFinishingTransition = null; mController.mSnapshotController.onTransitionFinish(mType, mTargets); // Resume snapshot persist thread after snapshot controller analysis this transition. @@ -2542,15 +2542,16 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // TaskFragment doesn't contain occluded ActivityRecord. return true; } - final TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment(); - if (adjacentTaskFragment != null) { - // When the TaskFragment has an adjacent TaskFragment, sibling behind them should be - // hidden unless any of them are translucent. - return adjacentTaskFragment.isTranslucentForTransition(); - } else { + if (!taskFragment.hasAdjacentTaskFragment()) { // Non-filling without adjacent is considered as translucent. return !wc.fillsParent(); } + // When the TaskFragment has an adjacent TaskFragment, sibling behind them should be + // hidden unless any of them are translucent. + if (!Flags.allowMultipleAdjacentTaskFragments()) { + return taskFragment.getAdjacentTaskFragment().isTranslucentForTransition(); + } + return taskFragment.forOtherAdjacentTaskFragments(TaskFragment::isTranslucentForTransition); } private void updatePriorVisibility() { diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 9d9c53dfe0f4..04650b9e0150 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -2945,7 +2945,7 @@ public class WindowManagerService extends IWindowManager.Stub final DisplayContent dc = mRoot.getDisplayContent(displayId); if (dc == null) { if (callingPid != MY_PID) { - throw new WindowManager.InvalidDisplayException( + throw new IllegalArgumentException( "attachWindowContextToDisplayContent: trying to attach to a" + " non-existing display:" + displayId); } @@ -10084,14 +10084,16 @@ public class WindowManagerService extends IWindowManager.Stub TaskSnapshot taskSnapshot; final long token = Binder.clearCallingIdentity(); try { + final Supplier<TaskSnapshot> supplier; synchronized (mGlobalLock) { Task task = mRoot.anyTaskForId(taskId, MATCH_ATTACHED_TASK_OR_RECENT_TASKS); if (task == null) { throw new IllegalArgumentException( "Failed to find matching task for taskId=" + taskId); } - taskSnapshot = mTaskSnapshotController.captureSnapshot(task); + supplier = mTaskSnapshotController.captureSnapshot(task, true /* allowAppTheme */); } + taskSnapshot = supplier != null ? supplier.get() : null; } finally { Binder.restoreCallingIdentity(token); } diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 60130d1f97be..f10b7b9a95a4 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -91,6 +91,7 @@ import android.server.ServerProtoEnums; import android.system.ErrnoException; import android.system.Os; import android.text.TextUtils; +import android.tracing.perfetto.InitArguments; import android.util.ArrayMap; import android.util.DisplayMetrics; import android.util.Dumpable; @@ -792,6 +793,12 @@ public final class SystemServer implements Dumpable { private void run() { TimingsTraceAndSlog t = new TimingsTraceAndSlog(); try { + if (android.tracing.Flags.systemServerLargePerfettoShmemBuffer()) { + // Explicitly initialize a 4 MB shmem buffer for Perfetto producers (b/382369925) + android.tracing.perfetto.Producer.init(new InitArguments( + InitArguments.PERFETTO_BACKEND_SYSTEM, 4 * 1024)); + } + t.traceBegin("InitBeforeStartServices"); // Record the process start information in sys props. @@ -3114,10 +3121,10 @@ public final class SystemServer implements Dumpable { if (com.android.ranging.flags.Flags.rangingStackEnabled()) { if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_UWB) || context.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_WIFI_RTT) + PackageManager.FEATURE_WIFI_AWARE) || (com.android.ranging.flags.Flags.rangingCsEnabled() && context.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_BLUETOOTH_LE_CHANNEL_SOUNDING))) { + PackageManager.FEATURE_BLUETOOTH_LE))) { t.traceBegin("RangingService"); // TODO: b/375264320 - Remove after RELEASE_RANGING_STACK is ramped to next. try { 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 d2c91ff2ef60..232bb83fdf9f 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 @@ -286,14 +286,21 @@ class AppIdPermissionPolicy : SchemePolicy() { return@forEach } var newFlags = oldFlags + val isSystemOrInstalled = + packageState.isSystem || packageState.getUserStateOrDefault(userId).isInstalled newFlags = if ( - newFlags.hasBits(PermissionFlags.ROLE) || - newFlags.hasBits(PermissionFlags.PREGRANT) + isSystemOrInstalled && ( + newFlags.hasBits(PermissionFlags.ROLE) || + newFlags.hasBits(PermissionFlags.PREGRANT) + ) ) { newFlags or PermissionFlags.RUNTIME_GRANTED } else { - newFlags andInv PermissionFlags.RUNTIME_GRANTED + newFlags andInv ( + PermissionFlags.RUNTIME_GRANTED or PermissionFlags.ROLE or + PermissionFlags.PREGRANT + ) } newFlags = newFlags andInv USER_SETTABLE_MASK if (newFlags.hasBits(PermissionFlags.LEGACY_GRANTED)) { diff --git a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionResetTest.kt b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionResetTest.kt index 12370954e9a5..8b357862dcbc 100644 --- a/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionResetTest.kt +++ b/services/tests/PermissionServiceMockingTests/src/com/android/server/permission/test/AppIdPermissionPolicyPermissionResetTest.kt @@ -72,7 +72,8 @@ class AppIdPermissionPolicyPermissionResetTest : BasePermissionPolicyTest() { } else { mockPackageState( APP_ID_1, - mockAndroidPackage(PACKAGE_NAME_1, requestedPermissions = setOf(PERMISSION_NAME_0)) + mockAndroidPackage(PACKAGE_NAME_1, requestedPermissions = setOf(PERMISSION_NAME_0)), + true ) } setPermissionFlags(APP_ID_1, USER_ID_0, PERMISSION_NAME_0, oldFlags) diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java index d00e2c677930..1f45792e5097 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayEventDeliveryTest.java @@ -33,6 +33,7 @@ import android.content.Context; import android.content.Intent; import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; +import android.os.BinderProxy; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; @@ -290,11 +291,15 @@ public class DisplayEventDeliveryTest { } /** - * Return true if the freezer is enabled on this platform. + * Return true if the freezer is enabled on this platform and if freezer notifications are + * supported. It is not enough to test that the freezer notification feature is enabled + * because some devices do not have the necessary kernel support. */ private boolean isAppFreezerEnabled() { try { - return mActivityManager.getService().isAppFreezerEnabled(); + return mActivityManager.getService().isAppFreezerEnabled() + && android.os.Flags.binderFrozenStateChangeCallback() + && BinderProxy.isFrozenStateChangeCallbackSupported(); } catch (Exception e) { Log.e(TAG, "isAppFreezerEnabled() failed: " + e); return false; diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index f96294ed4ca8..b7b4f0424165 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -214,7 +214,8 @@ public class DisplayManagerServiceTest { private static final String PACKAGE_NAME = "com.android.frameworks.displayservicetests"; private static final long STANDARD_DISPLAY_EVENTS = DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_ADDED - | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_CHANGED + | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_BASIC_CHANGED + | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REFRESH_RATE | DisplayManagerGlobal.INTERNAL_EVENT_FLAG_DISPLAY_REMOVED; private static final long STANDARD_AND_CONNECTION_DISPLAY_EVENTS = STANDARD_DISPLAY_EVENTS @@ -222,7 +223,7 @@ public class DisplayManagerServiceTest { private static final String EVENT_DISPLAY_ADDED = "EVENT_DISPLAY_ADDED"; private static final String EVENT_DISPLAY_REMOVED = "EVENT_DISPLAY_REMOVED"; - private static final String EVENT_DISPLAY_CHANGED = "EVENT_DISPLAY_CHANGED"; + private static final String EVENT_DISPLAY_BASIC_CHANGED = "EVENT_DISPLAY_BASIC_CHANGED"; private static final String EVENT_DISPLAY_BRIGHTNESS_CHANGED = "EVENT_DISPLAY_BRIGHTNESS_CHANGED"; private static final String EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED = @@ -889,7 +890,6 @@ public class DisplayManagerServiceTest { FakeDisplayManagerCallback callback = registerDisplayListenerCallback( displayManager, bs, displayDevice); - // Simulate DisplayDevice change DisplayDeviceInfo displayDeviceInfo2 = new DisplayDeviceInfo(); displayDeviceInfo2.copyFrom(displayDeviceInfo); @@ -900,7 +900,8 @@ public class DisplayManagerServiceTest { Handler handler = displayManager.getDisplayHandler(); waitForIdleHandler(handler); - assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_CHANGED); + assertThat(callback.receivedEvents()).containsExactly(EVENT_DISPLAY_BASIC_CHANGED, + EVENT_DISPLAY_REFRESH_RATE_CHANGED); } /** @@ -2145,7 +2146,7 @@ public class DisplayManagerServiceTest { new DisplayEventReceiver.FrameRateOverride(myUid, 30f), }); waitForIdleHandler(displayManager.getDisplayHandler()); - assertThat(callback.receivedEvents()).contains(EVENT_DISPLAY_CHANGED); + assertThat(callback.receivedEvents()).contains(EVENT_DISPLAY_BASIC_CHANGED); callback.clear(); updateFrameRateOverride(displayManager, displayDevice, @@ -2154,7 +2155,7 @@ public class DisplayManagerServiceTest { new DisplayEventReceiver.FrameRateOverride(1234, 30f), }); waitForIdleHandler(displayManager.getDisplayHandler()); - assertThat(callback.receivedEvents()).doesNotContain(EVENT_DISPLAY_CHANGED); + assertThat(callback.receivedEvents()).doesNotContain(EVENT_DISPLAY_BASIC_CHANGED); updateFrameRateOverride(displayManager, displayDevice, new DisplayEventReceiver.FrameRateOverride[]{ @@ -2163,7 +2164,7 @@ public class DisplayManagerServiceTest { new DisplayEventReceiver.FrameRateOverride(5678, 30f), }); waitForIdleHandler(displayManager.getDisplayHandler()); - assertThat(callback.receivedEvents()).contains(EVENT_DISPLAY_CHANGED); + assertThat(callback.receivedEvents()).contains(EVENT_DISPLAY_BASIC_CHANGED); callback.clear(); updateFrameRateOverride(displayManager, displayDevice, @@ -2172,7 +2173,7 @@ public class DisplayManagerServiceTest { new DisplayEventReceiver.FrameRateOverride(5678, 30f), }); waitForIdleHandler(displayManager.getDisplayHandler()); - assertThat(callback.receivedEvents()).contains(EVENT_DISPLAY_CHANGED); + assertThat(callback.receivedEvents()).contains(EVENT_DISPLAY_BASIC_CHANGED); callback.clear(); updateFrameRateOverride(displayManager, displayDevice, @@ -2180,7 +2181,7 @@ public class DisplayManagerServiceTest { new DisplayEventReceiver.FrameRateOverride(5678, 30f), }); waitForIdleHandler(displayManager.getDisplayHandler()); - assertThat(callback.receivedEvents()).doesNotContain(EVENT_DISPLAY_CHANGED); + assertThat(callback.receivedEvents()).doesNotContain(EVENT_DISPLAY_BASIC_CHANGED); } /** @@ -2303,16 +2304,16 @@ public class DisplayManagerServiceTest { updateRenderFrameRate(displayManager, displayDevice, 30f); waitForIdleHandler(displayManager.getDisplayHandler()); - assertThat(callback.receivedEvents()).contains(EVENT_DISPLAY_CHANGED); + assertThat(callback.receivedEvents()).contains(EVENT_DISPLAY_REFRESH_RATE_CHANGED); callback.clear(); updateRenderFrameRate(displayManager, displayDevice, 30f); waitForIdleHandler(displayManager.getDisplayHandler()); - assertThat(callback.receivedEvents()).doesNotContain(EVENT_DISPLAY_CHANGED); + assertThat(callback.receivedEvents()).doesNotContain(EVENT_DISPLAY_REFRESH_RATE_CHANGED); updateRenderFrameRate(displayManager, displayDevice, 20f); waitForIdleHandler(displayManager.getDisplayHandler()); - assertThat(callback.receivedEvents()).contains(EVENT_DISPLAY_CHANGED); + assertThat(callback.receivedEvents()).contains(EVENT_DISPLAY_REFRESH_RATE_CHANGED); callback.clear(); } @@ -3888,7 +3889,7 @@ public class DisplayManagerServiceTest { observer.onChange(false, Settings.Secure.getUriFor(MIRROR_BUILT_IN_DISPLAY)); waitForIdleHandler(handler); - assertThat(callback.receivedEvents()).doesNotContain(EVENT_DISPLAY_CHANGED); + assertThat(callback.receivedEvents()).doesNotContain(EVENT_DISPLAY_BASIC_CHANGED); } @Test @@ -3919,7 +3920,7 @@ public class DisplayManagerServiceTest { observer.onChange(false, Settings.Secure.getUriFor(MIRROR_BUILT_IN_DISPLAY)); waitForIdleHandler(handler); - assertThat(callback.receivedEvents()).contains(EVENT_DISPLAY_CHANGED); + assertThat(callback.receivedEvents()).contains(EVENT_DISPLAY_BASIC_CHANGED); } private void initDisplayPowerController(DisplayManagerInternal localService) { @@ -4389,8 +4390,8 @@ public class DisplayManagerServiceTest { return EVENT_DISPLAY_ADDED; case DisplayManagerGlobal.EVENT_DISPLAY_REMOVED: return EVENT_DISPLAY_REMOVED; - case DisplayManagerGlobal.EVENT_DISPLAY_CHANGED: - return EVENT_DISPLAY_CHANGED; + case DisplayManagerGlobal.EVENT_DISPLAY_BASIC_CHANGED: + return EVENT_DISPLAY_BASIC_CHANGED; case DisplayManagerGlobal.EVENT_DISPLAY_BRIGHTNESS_CHANGED: return EVENT_DISPLAY_BRIGHTNESS_CHANGED; case DisplayManagerGlobal.EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED: diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt index 5d427139a857..c65024f8f9d5 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt @@ -26,6 +26,7 @@ import org.junit.Test import org.mockito.ArgumentMatchers.anyFloat import org.mockito.ArgumentMatchers.anyInt import org.mockito.kotlin.any +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify @@ -43,7 +44,7 @@ class DisplayTopologyCoordinatorTest { @Before fun setUp() { - displayInfo.displayId = 2 + displayInfo.displayId = Display.DEFAULT_DISPLAY displayInfo.logicalWidth = 300 displayInfo.logicalHeight = 200 displayInfo.logicalDensityDpi = 100 @@ -90,6 +91,44 @@ class DisplayTopologyCoordinatorTest { } @Test + fun updateDisplay() { + whenever(mockTopology.updateDisplay(eq(Display.DEFAULT_DISPLAY), anyFloat(), anyFloat())) + .thenReturn(true) + + coordinator.onDisplayChanged(displayInfo) + + verify(mockTopologyChangedCallback).invoke(mockTopologyCopy) + } + + @Test + fun updateDisplay_notChanged() { + whenever(mockTopology.updateDisplay(eq(Display.DEFAULT_DISPLAY), anyFloat(), anyFloat())) + .thenReturn(false) + + coordinator.onDisplayChanged(displayInfo) + + verify(mockTopologyChangedCallback, never()).invoke(any()) + } + + @Test + fun removeDisplay() { + whenever(mockTopology.removeDisplay(Display.DEFAULT_DISPLAY)).thenReturn(true) + + coordinator.onDisplayRemoved(Display.DEFAULT_DISPLAY) + + verify(mockTopologyChangedCallback).invoke(mockTopologyCopy) + } + + @Test + fun removeDisplay_notChanged() { + whenever(mockTopology.removeDisplay(Display.DEFAULT_DISPLAY)).thenReturn(false) + + coordinator.onDisplayRemoved(Display.DEFAULT_DISPLAY) + + verify(mockTopologyChangedCallback, never()).invoke(any()) + } + + @Test fun getTopology_copy() { assertThat(coordinator.topology).isEqualTo(mockTopologyCopy) } diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java index ad30f22fe060..0dbb6ba58b3c 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java @@ -36,9 +36,9 @@ import static com.android.server.display.DisplayDeviceInfo.FLAG_ALLOWED_TO_BE_DE import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_ADDED; import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_CONNECTED; import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_DISCONNECTED; -import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED; import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_REMOVED; import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_STATE_CHANGED; +import static com.android.server.display.LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED; import static com.android.server.display.layout.Layout.Display.POSITION_REAR; import static com.android.server.display.layout.Layout.Display.POSITION_UNKNOWN; import static com.android.server.utils.FoldSettingProvider.SETTING_VALUE_SELECTIVE_STAY_AWAKE; @@ -1170,18 +1170,20 @@ public class LogicalDisplayMapperTest { @Test public void updateAndGetMaskForDisplayPropertyChanges_getsPropertyChangedFlags() { - // Change the display state + // Change the refresh rate override DisplayInfo newDisplayInfo = new DisplayInfo(); - newDisplayInfo.state = STATE_OFF; - assertEquals(LOGICAL_DISPLAY_EVENT_STATE_CHANGED, + newDisplayInfo.refreshRateOverride = 30; + assertEquals(LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED, mLogicalDisplayMapper.updateAndGetMaskForDisplayPropertyChanges(newDisplayInfo)); - // Change the refresh rate override + // Change the display state + when(mFlagsMock.isDisplayListenerPerformanceImprovementsEnabled()).thenReturn(true); newDisplayInfo = new DisplayInfo(); - newDisplayInfo.refreshRateOverride = 30; - assertEquals(LOGICAL_DISPLAY_EVENT_REFRESH_RATE_CHANGED, + newDisplayInfo.state = STATE_OFF; + assertEquals(LOGICAL_DISPLAY_EVENT_STATE_CHANGED, mLogicalDisplayMapper.updateAndGetMaskForDisplayPropertyChanges(newDisplayInfo)); + // Change multiple properties newDisplayInfo = new DisplayInfo(); newDisplayInfo.refreshRateOverride = 30; diff --git a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java index 35f421e582d8..de6f9bd7527a 100644 --- a/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java +++ b/services/tests/performancehinttests/src/com/android/server/power/hint/HintManagerServiceTest.java @@ -78,12 +78,15 @@ import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.platform.test.flag.junit.SetFlagsRule; import android.util.Log; +import androidx.test.InstrumentationRegistry; + import com.android.server.FgThread; import com.android.server.LocalServices; import com.android.server.power.hint.HintManagerService.AppHintSession; import com.android.server.power.hint.HintManagerService.Injector; import com.android.server.power.hint.HintManagerService.NativeWrapper; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -93,6 +96,8 @@ import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collections; @@ -111,6 +116,7 @@ import java.util.concurrent.locks.LockSupport; */ public class HintManagerServiceTest { private static final String TAG = "HintManagerServiceTest"; + private List<File> mFilesCreated = new ArrayList<>(); private static WorkDuration makeWorkDuration( long timestamp, long duration, long workPeriodStartTime, @@ -192,9 +198,9 @@ public class HintManagerServiceTest { mSupportInfo.sessionTags = -1; mSupportInfo.headroom = new SupportInfo.HeadroomSupportInfo(); mSupportInfo.headroom.isCpuSupported = true; - mSupportInfo.headroom.cpuMinIntervalMillis = 2000; + mSupportInfo.headroom.cpuMinIntervalMillis = 1000; mSupportInfo.headroom.isGpuSupported = true; - mSupportInfo.headroom.gpuMinIntervalMillis = 2000; + mSupportInfo.headroom.gpuMinIntervalMillis = 1000; mSupportInfo.compositionData = new SupportInfo.CompositionDataSupportInfo(); return mSupportInfo; } @@ -243,6 +249,13 @@ public class HintManagerServiceTest { LocalServices.addService(ActivityManagerInternal.class, mAmInternalMock); } + @After + public void tearDown() { + for (File file : mFilesCreated) { + file.delete(); + } + } + /** * Mocks the creation calls, but without support for new createHintSessionWithConfig method */ @@ -1327,6 +1340,58 @@ public class HintManagerServiceTest { }); } + @Test + public void testCpuHeadroomCpuProcStatPath() throws Exception { + File dir = InstrumentationRegistry.getTargetContext().getFilesDir(); + dir.mkdir(); + String procStatFileStr = "mock_proc_stat"; + File file = new File(dir, procStatFileStr); + mFilesCreated.add(file); + try (FileOutputStream output = new FileOutputStream(file)) { + output.write("cpu 2000 3000 4000 0 0 0 0 0 0 0".getBytes()); + } + HintManagerService service = createService(); + service.setProcStatPathOverride(file.getPath()); + + CpuHeadroomParamsInternal params1 = new CpuHeadroomParamsInternal(); + CpuHeadroomParams halParams1 = new CpuHeadroomParams(); + halParams1.calculationType = CpuHeadroomParams.CalculationType.MIN; + halParams1.tids = new int[]{Process.myPid()}; + + float headroom1 = 0.1f; + CpuHeadroomResult halRet1 = CpuHeadroomResult.globalHeadroom(headroom1); + when(mIPowerMock.getCpuHeadroom(eq(halParams1))).thenReturn(halRet1); + clearInvocations(mIPowerMock); + assertEquals(halRet1, service.getBinderServiceInstance().getCpuHeadroom(params1)); + verify(mIPowerMock, times(1)).getCpuHeadroom(eq(halParams1)); + // expire the cache but cpu proc hasn't changed so we expect no value return + Thread.sleep(1100); + clearInvocations(mIPowerMock); + assertEquals(null, service.getBinderServiceInstance().getCpuHeadroom(params1)); + verify(mIPowerMock, times(0)).getCpuHeadroom(eq(halParams1)); + + // update user jiffies with 500 equivalent jiffies, which is not sufficient cpu time + Thread.sleep(1100); + try (FileOutputStream output = new FileOutputStream(file)) { + output.write(("cpu " + (2000 + (int) (500 / service.mJiffyMillis)) + + " 3000 4000 0 0 0 0 0 0 0").getBytes()); + } + clearInvocations(mIPowerMock); + assertEquals(null, service.getBinderServiceInstance().getCpuHeadroom(params1)); + verify(mIPowerMock, times(0)).getCpuHeadroom(eq(halParams1)); + + // update nice jiffies with 600 equivalent jiffies, now it exceeds 1000ms requirement + Thread.sleep(1100); + try (FileOutputStream output = new FileOutputStream(file)) { + output.write(("cpu " + (2000 + (int) (500 / service.mJiffyMillis)) + + " " + +(3000 + (int) (600 / service.mJiffyMillis)) + + " 4000 0 0 0 0 0 0 0").getBytes()); + } + clearInvocations(mIPowerMock); + assertEquals(halRet1, service.getBinderServiceInstance().getCpuHeadroom(params1)); + verify(mIPowerMock, times(1)).getCpuHeadroom(eq(halParams1)); + } + @Test @EnableFlags({Flags.FLAG_CPU_HEADROOM_AFFINITY_CHECK}) @@ -1397,8 +1462,8 @@ public class HintManagerServiceTest { verify(mIPowerMock, times(0)).getCpuHeadroom(eq(halParams3)); verify(mIPowerMock, times(1)).getCpuHeadroom(eq(halParams4)); - // after 1 more second it should be served with cache still - Thread.sleep(1000); + // after 500ms more it should be served with cache + Thread.sleep(500); clearInvocations(mIPowerMock); assertEquals(halRet1, service.getBinderServiceInstance().getCpuHeadroom(params1)); assertEquals(halRet2, service.getBinderServiceInstance().getCpuHeadroom(params2)); @@ -1410,8 +1475,8 @@ public class HintManagerServiceTest { verify(mIPowerMock, times(0)).getCpuHeadroom(eq(halParams3)); verify(mIPowerMock, times(1)).getCpuHeadroom(eq(halParams4)); - // after 2+ seconds it should be served from HAL as it exceeds 2000 millis interval - Thread.sleep(1100); + // after 1+ seconds it should be served from HAL as it exceeds 1000 millis interval + Thread.sleep(600); clearInvocations(mIPowerMock); assertEquals(halRet1, service.getBinderServiceInstance().getCpuHeadroom(params1)); assertEquals(halRet2, service.getBinderServiceInstance().getCpuHeadroom(params2)); @@ -1519,8 +1584,8 @@ public class HintManagerServiceTest { verify(mIPowerMock, times(0)).getGpuHeadroom(eq(halParams1)); verify(mIPowerMock, times(1)).getGpuHeadroom(eq(halParams2)); - // after 1 more second it should be served with cache still - Thread.sleep(1000); + // after 500ms it should be served with cache + Thread.sleep(500); clearInvocations(mIPowerMock); assertEquals(halRet1, service.getBinderServiceInstance().getGpuHeadroom(params1)); assertEquals(halRet2, service.getBinderServiceInstance().getGpuHeadroom(params2)); @@ -1528,8 +1593,8 @@ public class HintManagerServiceTest { verify(mIPowerMock, times(0)).getGpuHeadroom(eq(halParams1)); verify(mIPowerMock, times(1)).getGpuHeadroom(eq(halParams2)); - // after 2+ seconds it should be served from HAL as it exceeds 2000 millis interval - Thread.sleep(1100); + // after 1+ seconds it should be served from HAL as it exceeds 1000 millis interval + Thread.sleep(600); clearInvocations(mIPowerMock); assertEquals(halRet1, service.getBinderServiceInstance().getGpuHeadroom(params1)); assertEquals(halRet2, service.getBinderServiceInstance().getGpuHeadroom(params2)); diff --git a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java index 96741e0b1e87..469bd66b7e7b 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/NotifierTest.java @@ -21,6 +21,7 @@ import static android.os.PowerManagerInternal.WAKEFULNESS_AWAKE; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; @@ -889,6 +890,32 @@ public class NotifierTest { "my.package.name", false, null, null); } + @Test + public void getWakelockMonitorTypeForLogging_evaluatesWakelockLevel() { + createNotifier(); + assertEquals(mNotifier.getWakelockMonitorTypeForLogging(PowerManager.SCREEN_DIM_WAKE_LOCK), + PowerManager.FULL_WAKE_LOCK); + assertEquals(mNotifier.getWakelockMonitorTypeForLogging( + PowerManager.SCREEN_BRIGHT_WAKE_LOCK), PowerManager.FULL_WAKE_LOCK); + assertEquals(mNotifier.getWakelockMonitorTypeForLogging(PowerManager.DRAW_WAKE_LOCK), + PowerManager.DRAW_WAKE_LOCK); + assertEquals(mNotifier.getWakelockMonitorTypeForLogging(PowerManager.PARTIAL_WAKE_LOCK), + PowerManager.PARTIAL_WAKE_LOCK); + assertEquals(mNotifier.getWakelockMonitorTypeForLogging( + PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK), + PowerManager.PARTIAL_WAKE_LOCK); + assertEquals(mNotifier.getWakelockMonitorTypeForLogging( + PowerManager.DOZE_WAKE_LOCK), -1); + + when(mResourcesSpy.getBoolean( + com.android.internal.R.bool.config_suspendWhenScreenOffDueToProximity)) + .thenReturn(true); + + createNotifier(); + assertEquals(mNotifier.getWakelockMonitorTypeForLogging( + PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK), -1); + } + private final PowerManagerService.Injector mInjector = new PowerManagerService.Injector() { @Override Notifier createNotifier(Looper looper, Context context, IBatteryStats batteryStats, diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index 2fe6918630f6..7dbbff22a537 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -33,6 +33,7 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat import static com.android.server.am.UserController.CLEAR_USER_JOURNEY_SESSION_MSG; import static com.android.server.am.UserController.COMPLETE_USER_SWITCH_MSG; import static com.android.server.am.UserController.CONTINUE_USER_SWITCH_MSG; +import static com.android.server.am.UserController.DEFAULT_BEFORE_USER_SWITCH_TIMEOUT_MS; import static com.android.server.am.UserController.REPORT_LOCKED_BOOT_COMPLETE_MSG; import static com.android.server.am.UserController.REPORT_USER_SWITCH_COMPLETE_MSG; import static com.android.server.am.UserController.REPORT_USER_SWITCH_MSG; @@ -94,6 +95,7 @@ import android.os.Looper; import android.os.Message; import android.os.PowerManagerInternal; import android.os.RemoteException; +import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; import android.os.storage.IStorageManager; @@ -181,14 +183,12 @@ public class UserControllerTest { Intent.ACTION_USER_STARTING); private static final Set<Integer> START_FOREGROUND_USER_MESSAGE_CODES = newHashSet( - 0, // for startUserInternalOnHandler REPORT_USER_SWITCH_MSG, USER_SWITCH_TIMEOUT_MSG, USER_START_MSG, USER_CURRENT_MSG); private static final Set<Integer> START_BACKGROUND_USER_MESSAGE_CODES = newHashSet( - 0, // for startUserInternalOnHandler USER_START_MSG, REPORT_LOCKED_BOOT_COMPLETE_MSG); @@ -376,7 +376,7 @@ public class UserControllerTest { // and the cascade effect goes on...). In fact, a better approach would to not assert the // binder calls, but their side effects (in this case, that the user is stopped right away) assertWithMessage("wrong binder message calls").that(mInjector.mHandler.getMessageCodes()) - .containsExactly(/* for startUserInternalOnHandler */ 0, USER_START_MSG); + .containsExactly(USER_START_MSG); } private void startUserAssertions( @@ -419,17 +419,12 @@ public class UserControllerTest { @Test public void testDispatchUserSwitch() throws RemoteException { // Prepare mock observer and register it - IUserSwitchObserver observer = mock(IUserSwitchObserver.class); - when(observer.asBinder()).thenReturn(new Binder()); - doAnswer(invocation -> { - IRemoteCallback callback = (IRemoteCallback) invocation.getArguments()[1]; - callback.sendResult(null); - return null; - }).when(observer).onUserSwitching(anyInt(), any()); - mUserController.registerUserSwitchObserver(observer, "mock"); + IUserSwitchObserver observer = registerUserSwitchObserver( + /* replyToOnBeforeUserSwitchingCallback= */ true, + /* replyToOnUserSwitchingCallback= */ true); // Start user -- this will update state of mUserController mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND); - verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID)); + verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID), any()); Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG); assertNotNull(reportMsg); UserState userState = (UserState) reportMsg.obj; @@ -453,14 +448,26 @@ public class UserControllerTest { } @Test + public void testShouldCrashWhenOnBeforeUserSwitchingTimeouts() throws RemoteException { + IUserSwitchObserver observer = registerUserSwitchObserver( + /* replyToOnBeforeUserSwitchingCallback= */ false, + /* replyToOnUserSwitchingCallback= */ true); + mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND); + verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID), any()); + assertThrows("Should have crashed when observers don't reply to onBeforeUserSwitching in " + + DEFAULT_BEFORE_USER_SWITCH_TIMEOUT_MS + " ms", RuntimeException.class, + mInjector.mHandler::runPendingCallbacks); + } + + @Test public void testDispatchUserSwitchBadReceiver() throws RemoteException { - // Prepare mock observer which doesn't notify the callback and register it - IUserSwitchObserver observer = mock(IUserSwitchObserver.class); - when(observer.asBinder()).thenReturn(new Binder()); - mUserController.registerUserSwitchObserver(observer, "mock"); + // Prepare mock observer which doesn't notify the onUserSwitching callback and register it + IUserSwitchObserver observer = registerUserSwitchObserver( + /* replyToOnBeforeUserSwitchingCallback= */ true, + /* replyToOnUserSwitchingCallback= */ false); // Start user -- this will update state of mUserController mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND); - verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID)); + verify(observer, times(1)).onBeforeUserSwitching(eq(TEST_USER_ID), any()); Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG); assertNotNull(reportMsg); UserState userState = (UserState) reportMsg.obj; @@ -551,7 +558,6 @@ public class UserControllerTest { expectedCodes.add(REPORT_USER_SWITCH_COMPLETE_MSG); if (backgroundUserStopping) { expectedCodes.add(CLEAR_USER_JOURNEY_SESSION_MSG); - expectedCodes.add(0); // this is for directly posting in stopping. } if (expectScheduleBackgroundUserStopping) { expectedCodes.add(SCHEDULED_STOP_BACKGROUND_USER_MSG); @@ -567,9 +573,9 @@ public class UserControllerTest { @Test public void testDispatchUserSwitchComplete() throws RemoteException { // Prepare mock observer and register it - IUserSwitchObserver observer = mock(IUserSwitchObserver.class); - when(observer.asBinder()).thenReturn(new Binder()); - mUserController.registerUserSwitchObserver(observer, "mock"); + IUserSwitchObserver observer = registerUserSwitchObserver( + /* replyToOnBeforeUserSwitchingCallback= */ true, + /* replyToOnUserSwitchingCallback= */ true); // Start user -- this will update state of mUserController mUserController.startUser(TEST_USER_ID, USER_START_MODE_FOREGROUND); Message reportMsg = mInjector.mHandler.getMessageForCode(REPORT_USER_SWITCH_MSG); @@ -1752,6 +1758,29 @@ public class UserControllerTest { verify(mInjector, never()).onSystemUserVisibilityChanged(anyBoolean()); } + private IUserSwitchObserver registerUserSwitchObserver( + boolean replyToOnBeforeUserSwitchingCallback, boolean replyToOnUserSwitchingCallback) + throws RemoteException { + IUserSwitchObserver observer = mock(IUserSwitchObserver.class); + when(observer.asBinder()).thenReturn(new Binder()); + if (replyToOnBeforeUserSwitchingCallback) { + doAnswer(invocation -> { + IRemoteCallback callback = (IRemoteCallback) invocation.getArguments()[1]; + callback.sendResult(null); + return null; + }).when(observer).onBeforeUserSwitching(anyInt(), any()); + } + if (replyToOnUserSwitchingCallback) { + doAnswer(invocation -> { + IRemoteCallback callback = (IRemoteCallback) invocation.getArguments()[1]; + callback.sendResult(null); + return null; + }).when(observer).onUserSwitching(anyInt(), any()); + } + mUserController.registerUserSwitchObserver(observer, "mock"); + return observer; + } + // Should be public to allow mocking private static class TestInjector extends UserController.Injector { public final TestHandler mHandler; @@ -1957,6 +1986,7 @@ public class UserControllerTest { * fix this, but in the meantime, this is your warning. */ private final List<Message> mMessages = new ArrayList<>(); + private final List<Runnable> mPendingCallbacks = new ArrayList<>(); TestHandler(Looper looper) { super(looper); @@ -1989,14 +2019,24 @@ public class UserControllerTest { @Override public boolean sendMessageAtTime(Message msg, long uptimeMillis) { - Message copy = new Message(); - copy.copyFrom(msg); - mMessages.add(copy); - if (msg.getCallback() != null) { - msg.getCallback().run(); + if (msg.getCallback() == null) { + Message copy = new Message(); + copy.copyFrom(msg); + mMessages.add(copy); + } else { + if (SystemClock.uptimeMillis() >= uptimeMillis) { + msg.getCallback().run(); + } else { + mPendingCallbacks.add(msg.getCallback()); + } msg.setCallback(null); } return super.sendMessageAtTime(msg, uptimeMillis); } + + private void runPendingCallbacks() { + mPendingCallbacks.forEach(Runnable::run); + mPendingCallbacks.clear(); + } } } diff --git a/services/tests/servicestests/src/com/android/server/storage/CacheQuotaStrategyTest.java b/services/tests/servicestests/src/com/android/server/storage/CacheQuotaStrategyTest.java index 9c61d95bc5e5..9528a05d38a0 100644 --- a/services/tests/servicestests/src/com/android/server/storage/CacheQuotaStrategyTest.java +++ b/services/tests/servicestests/src/com/android/server/storage/CacheQuotaStrategyTest.java @@ -23,13 +23,13 @@ import android.test.AndroidTestCase; import android.util.Pair; import android.util.Xml; -import com.android.internal.util.FastXmlSerializer; import com.android.modules.utils.TypedXmlSerializer; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import org.xmlpull.v1.XmlPullParserException; import java.io.ByteArrayInputStream; import java.io.StringWriter; @@ -123,8 +123,24 @@ public class CacheQuotaStrategyTest extends AndroidTestCase { buildCacheQuotaHint("uuid2", 10, 250)); } + @Test + public void testReadInvalidInput() throws Exception { + String input = "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n" + + "<cache-info previousBytes=\"1000\">\n" + + "<quota/>\n" + + "</cache-info>\n"; + + try { + CacheQuotaStrategy.readFromXml(new ByteArrayInputStream( + input.getBytes("UTF-8"))); + fail("Expected XML parsing exception"); + } catch (XmlPullParserException e) { + // Expected XmlPullParserException exception + } + } + private CacheQuotaHint buildCacheQuotaHint(String volumeUuid, int uid, long quota) { return new CacheQuotaHint.Builder() .setVolumeUuid(volumeUuid).setUid(uid).setQuota(quota).build(); } -}
\ No newline at end of file +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 20f4bb65d27b..601023f89656 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -17683,4 +17683,145 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { assertThat(mService.mNotificationList).isEmpty(); } + + @Test + @EnableFlags({FLAG_NOTIFICATION_CLASSIFICATION, + FLAG_NOTIFICATION_FORCE_GROUPING, + FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION}) + public void testUnbundleNotification_ungrouped_restoresOriginalChannel() throws Exception { + NotificationManagerService.WorkerHandler handler = mock( + NotificationManagerService.WorkerHandler.class); + mService.setHandler(handler); + when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true); + when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(true); + when(mAssistants.isAdjustmentKeyTypeAllowed(anyInt())).thenReturn(true); + when(mAssistants.isTypeAdjustmentAllowedForPackage(anyString())).thenReturn(true); + + // Post a single notification + final boolean hasOriginalSummary = false; + final NotificationRecord r = generateNotificationRecord(mTestNotificationChannel); + final String keyToUnbundle = r.getKey(); + mService.addNotification(r); + + // Classify notification into the NEWS bundle + Bundle signals = new Bundle(); + signals.putInt(Adjustment.KEY_TYPE, Adjustment.TYPE_NEWS); + Adjustment adjustment = new Adjustment( + r.getSbn().getPackageName(), r.getKey(), signals, "", r.getUser().getIdentifier()); + mBinderService.applyAdjustmentFromAssistant(null, adjustment); + waitForIdle(); + r.applyAdjustments(); + // Check that the NotificationRecord channel is updated + assertThat(r.getChannel().getId()).isEqualTo(NEWS_ID); + // Check that the Notification mChannelId is not updated + assertThat(r.getNotification().getChannelId()).isEqualTo(TEST_CHANNEL_ID); + + // Unbundle the notification + mService.mNotificationDelegate.unbundleNotification(keyToUnbundle); + + // Check that the original channel was restored + assertThat(r.getChannel().getId()).isEqualTo(TEST_CHANNEL_ID); + verify(mGroupHelper, times(1)).onNotificationUnbundled(eq(r), eq(hasOriginalSummary)); + } + + @Test + @EnableFlags({FLAG_NOTIFICATION_CLASSIFICATION, + FLAG_NOTIFICATION_FORCE_GROUPING, + FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION}) + public void testUnbundleNotification_grouped_restoresOriginalChannel() throws Exception { + NotificationManagerService.WorkerHandler handler = mock( + NotificationManagerService.WorkerHandler.class); + mService.setHandler(handler); + when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true); + when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(true); + when(mAssistants.isAdjustmentKeyTypeAllowed(anyInt())).thenReturn(true); + when(mAssistants.isTypeAdjustmentAllowedForPackage(anyString())).thenReturn(true); + + // Post grouped notifications + final String originalGroupName = "originalGroup"; + final int summaryId = 0; + final NotificationRecord r1 = generateNotificationRecord(mTestNotificationChannel, + summaryId + 1, originalGroupName, false); + mService.addNotification(r1); + final NotificationRecord r2 = generateNotificationRecord(mTestNotificationChannel, + summaryId + 2, originalGroupName, false); + mService.addNotification(r2); + final NotificationRecord summary = generateNotificationRecord(mTestNotificationChannel, + summaryId, originalGroupName, true); + mService.addNotification(summary); + final String originalGroupKey = summary.getGroupKey(); + assertThat(mService.mSummaryByGroupKey).containsEntry(originalGroupKey, summary); + + // Classify a child notification into the NEWS bundle + final String keyToUnbundle = r1.getKey(); + final boolean hasOriginalSummary = true; + Bundle signals = new Bundle(); + signals.putInt(Adjustment.KEY_TYPE, Adjustment.TYPE_NEWS); + Adjustment adjustment = new Adjustment(r1.getSbn().getPackageName(), r1.getKey(), signals, + "", r1.getUser().getIdentifier()); + mBinderService.applyAdjustmentFromAssistant(null, adjustment); + waitForIdle(); + r1.applyAdjustments(); + assertThat(r1.getChannel().getId()).isEqualTo(NEWS_ID); + + // Unbundle the notification + mService.mNotificationDelegate.unbundleNotification(keyToUnbundle); + + // Check that the original channel was restored + assertThat(r1.getChannel().getId()).isEqualTo(TEST_CHANNEL_ID); + verify(mGroupHelper, times(1)).onNotificationUnbundled(eq(r1), eq(hasOriginalSummary)); + } + + @Test + @EnableFlags({FLAG_NOTIFICATION_CLASSIFICATION, + FLAG_NOTIFICATION_FORCE_GROUPING, + FLAG_NOTIFICATION_REGROUP_ON_CLASSIFICATION}) + public void testUnbundleNotification_groupedSummaryCanceled_restoresOriginalChannel() + throws Exception { + NotificationManagerService.WorkerHandler handler = mock( + NotificationManagerService.WorkerHandler.class); + mService.setHandler(handler); + when(mAssistants.isSameUser(any(), anyInt())).thenReturn(true); + when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(true); + when(mAssistants.isAdjustmentKeyTypeAllowed(anyInt())).thenReturn(true); + when(mAssistants.isTypeAdjustmentAllowedForPackage(anyString())).thenReturn(true); + + // Post grouped notifications + final String originalGroupName = "originalGroup"; + final int summaryId = 0; + final NotificationRecord r1 = generateNotificationRecord(mTestNotificationChannel, + summaryId + 1, originalGroupName, false); + mService.addNotification(r1); + final NotificationRecord r2 = generateNotificationRecord(mTestNotificationChannel, + summaryId + 2, originalGroupName, false); + mService.addNotification(r2); + final NotificationRecord summary = generateNotificationRecord(mTestNotificationChannel, + summaryId, originalGroupName, true); + mService.addNotification(summary); + final String originalGroupKey = summary.getGroupKey(); + assertThat(mService.mSummaryByGroupKey).containsEntry(originalGroupKey, summary); + + // Classify a child notification into the NEWS bundle + final String keyToUnbundle = r1.getKey(); + Bundle signals = new Bundle(); + signals.putInt(Adjustment.KEY_TYPE, Adjustment.TYPE_NEWS); + Adjustment adjustment = new Adjustment(r1.getSbn().getPackageName(), r1.getKey(), signals, + "", r1.getUser().getIdentifier()); + mBinderService.applyAdjustmentFromAssistant(null, adjustment); + waitForIdle(); + r1.applyAdjustments(); + assertThat(r1.getChannel().getId()).isEqualTo(NEWS_ID); + + // Cancel original summary + final boolean hasOriginalSummary = false; + mService.mSummaryByGroupKey.remove(summary.getGroupKey()); + + // Unbundle the notification + mService.mNotificationDelegate.unbundleNotification(keyToUnbundle); + + // Check that the original channel was restored + assertThat(r1.getChannel().getId()).isEqualTo(TEST_CHANNEL_ID); + verify(mGroupHelper, times(1)).onNotificationUnbundled(eq(r1), eq(hasOriginalSummary)); + } + } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index fbd53f714dbf..8e79514c875e 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -66,7 +66,6 @@ import static com.android.internal.config.sysui.SystemUiSystemPropertiesFlags.No import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__DENIED; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__GRANTED; import static com.android.internal.util.FrameworkStatsLog.PACKAGE_NOTIFICATION_PREFERENCES__FSI_STATE__NOT_REQUESTED; -import static com.android.server.notification.Flags.FLAG_ALL_NOTIFS_NEED_TTL; import static com.android.server.notification.Flags.FLAG_NOTIFICATION_VERIFY_CHANNEL_SOUND_URI; import static com.android.server.notification.Flags.FLAG_PERSIST_INCOMPLETE_RESTORE_DATA; import static com.android.server.notification.NotificationChannelLogger.NotificationChannelEvent.NOTIFICATION_CHANNEL_UPDATED_BY_USER; @@ -155,7 +154,6 @@ import android.util.proto.ProtoOutputStream; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import com.android.internal.config.sysui.SystemUiSystemPropertiesFlags; import com.android.internal.config.sysui.TestableFlagResolver; @@ -167,9 +165,6 @@ import com.android.os.AtomsProto.PackageNotificationPreferences; import com.android.server.UiServiceTestCase; import com.android.server.notification.PermissionHelper.PackagePermission; -import platform.test.runner.parameterized.ParameterizedAndroidJunit4; -import platform.test.runner.parameterized.Parameters; - import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.protobuf.InvalidProtocolBufferException; @@ -204,6 +199,9 @@ import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ThreadLocalRandom; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + @SmallTest @RunWith(ParameterizedAndroidJunit4.class) @EnableFlags(FLAG_PERSIST_INCOMPLETE_RESTORE_DATA) @@ -2640,6 +2638,35 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test + public void getPackagesBypassingDnd_multipleUsers() { + int uidUser1 = UserHandle.getUid(1, UID_P); + NotificationChannel channelUser1Bypass = new NotificationChannel("id11", "name1", + NotificationManager.IMPORTANCE_MAX); + channelUser1Bypass.setBypassDnd(true); + NotificationChannel channelUser1NoBypass = new NotificationChannel("id12", "name2", + NotificationManager.IMPORTANCE_MAX); + channelUser1NoBypass.setBypassDnd(false); + + int uidUser2 = UserHandle.getUid(2, UID_P); + NotificationChannel channelUser2Bypass = new NotificationChannel("id21", "name1", + NotificationManager.IMPORTANCE_MAX); + channelUser2Bypass.setBypassDnd(true); + + mHelper.createNotificationChannel(PKG_P, uidUser1, channelUser1Bypass, true, + /* hasDndAccess= */ true, uidUser1, false); + mHelper.createNotificationChannel(PKG_P, uidUser1, channelUser1NoBypass, true, + /* hasDndAccess= */ true, uidUser1, false); + mHelper.createNotificationChannel(PKG_P, uidUser2, channelUser2Bypass, true, + /* hasDndAccess= */ true, uidUser2, false); + + assertThat(mHelper.getPackagesBypassingDnd(0)).isEmpty(); + assertThat(mHelper.getPackagesBypassingDnd(1)) + .containsExactly(new ZenBypassingApp(PKG_P, false)); + assertThat(mHelper.getPackagesBypassingDnd(2)) + .containsExactly(new ZenBypassingApp(PKG_P, true)); + } + + @Test public void getPackagesBypassingDnd_oneChannelBypassing_groupBlocked() { int uid = UID_N_MR1; NotificationChannelGroup ncg = new NotificationChannelGroup("group1", "name1"); diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java index a7fc10f2fcc5..948371f74a9c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivitySnapshotControllerTests.java @@ -29,6 +29,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.never; @@ -253,7 +254,11 @@ public class ActivitySnapshotControllerTests extends TaskSnapshotPersisterTestBa */ @Test public void testSkipRecordActivity() { - doReturn(createSnapshot()).when(mActivitySnapshotController).recordSnapshotInner(any()); + final AbsAppSnapshotController.SnapshotSupplier supplier = + new AbsAppSnapshotController.SnapshotSupplier(); + supplier.setSupplier(this::createSnapshot); + doReturn(supplier).when(mActivitySnapshotController).recordSnapshotInner( + any(), anyBoolean(), any()); final Task task = createTask(mDisplayContent); mSnapshotPersistQueue.setPaused(true); diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java index 1edbcd527bf4..463254caa845 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatReachabilityOverridesTest.java @@ -23,14 +23,10 @@ import static org.mockito.Mockito.spy; import android.compat.testing.PlatformCompatChangeRule; import android.graphics.Rect; -import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import androidx.annotation.NonNull; -import com.android.window.flags.Flags; - import junit.framework.Assert; import org.junit.Rule; @@ -125,8 +121,7 @@ public class AppCompatReachabilityOverridesTest extends WindowTestsBase { } @Test - @EnableFlags(Flags.FLAG_DISABLE_THIN_LETTERBOXING_POLICY) - public void testAllowReachabilityForThinLetterboxWithFlagEnabled() { + public void testAllowReachabilityForThinLetterbox_disableForThinLetterboxing() { runTestScenario((robot) -> { robot.activity().createActivityWithComponent(); @@ -142,24 +137,6 @@ public class AppCompatReachabilityOverridesTest extends WindowTestsBase { }); } - @Test - @DisableFlags(Flags.FLAG_DISABLE_THIN_LETTERBOXING_POLICY) - public void testAllowReachabilityForThinLetterboxWithFlagDisabled() { - runTestScenario((robot) -> { - robot.activity().createActivityWithComponent(); - - robot.configureIsVerticalThinLetterboxed(/* isThin */ true); - robot.checkAllowVerticalReachabilityForThinLetterbox(/* expected */ true); - robot.configureIsHorizontalThinLetterboxed(/* isThin */ true); - robot.checkAllowHorizontalReachabilityForThinLetterbox(/* expected */ true); - - robot.configureIsVerticalThinLetterboxed(/* isThin */ false); - robot.checkAllowVerticalReachabilityForThinLetterbox(/* expected */ true); - robot.configureIsHorizontalThinLetterboxed(/* isThin */ false); - robot.checkAllowHorizontalReachabilityForThinLetterbox(/* expected */ true); - }); - } - /** * Runs a test scenario providing a Robot. */ diff --git a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java index 429a396ad997..de4b6fac7abf 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DragDropControllerTests.java @@ -150,8 +150,8 @@ public class DragDropControllerTests extends WindowTestsBase { mProcess).build(); // Use a new TestIWindow so we don't collect events for other windows - final WindowState window = createWindow(null, TYPE_BASE_APPLICATION, activity, name, - ownerId, false, new TestIWindow()); + final WindowState window = newWindowBuilder(name, TYPE_BASE_APPLICATION).setWindowToken( + activity).setOwnerId(ownerId).setClientWindow(new TestIWindow()).build(); InputChannel channel = new InputChannel(); window.openInputChannel(channel); window.mHasSurface = true; @@ -249,7 +249,7 @@ public class DragDropControllerTests extends WindowTestsBase { mTarget.mDeferDragStateClosed = true; mTarget.reportDropWindow(mWindow.mInputChannelToken, 0, 0); // Verify the drop event includes the drag surface - mTarget.handleMotionEvent(false, 0, 0); + mTarget.handleMotionEvent(false, mWindow.getDisplayId(), 0, 0); final DragEvent dropEvent = dragEvents.get(dragEvents.size() - 1); assertTrue(dropEvent.getDragSurface() != null); @@ -296,7 +296,7 @@ public class DragDropControllerTests extends WindowTestsBase { 0).getClipData().willParcelWithActivityInfo()); mTarget.reportDropWindow(globalInterceptWindow.mInputChannelToken, 0, 0); - mTarget.handleMotionEvent(false, 0, 0); + mTarget.handleMotionEvent(false, globalInterceptWindow.getDisplayId(), 0, 0); mToken = globalInterceptWindow.mClient.asBinder(); // Verify the drop event is only sent for the global intercept window @@ -334,8 +334,8 @@ public class DragDropControllerTests extends WindowTestsBase { try { mTarget.mDeferDragStateClosed = true; mTarget.reportDropWindow(mWindow.mInputChannelToken, 0, 0); - // // Verify the drop event does not have the drag flags - mTarget.handleMotionEvent(false, 0, 0); + // Verify the drop event does not have the drag flags + mTarget.handleMotionEvent(false, mWindow.getDisplayId(), 0, 0); final DragEvent dropEvent = dragEvents.get(dragEvents.size() - 1); assertTrue(dropEvent.getDragFlags() == (View.DRAG_FLAG_GLOBAL | View.DRAG_FLAG_START_INTENT_SENDER_ON_UNHANDLED_DRAG)); @@ -520,7 +520,7 @@ public class DragDropControllerTests extends WindowTestsBase { // Verify after consuming that the drag surface is relinquished mTarget.reportDropWindow(otherWindow.mInputChannelToken, 0, 0); - mTarget.handleMotionEvent(false, 0, 0); + mTarget.handleMotionEvent(false, otherWindow.getDisplayId(), 0, 0); mToken = otherWindow.mClient.asBinder(); mTarget.reportDropResult(otherIWindow, true); @@ -551,7 +551,7 @@ public class DragDropControllerTests extends WindowTestsBase { // Verify after consuming that the drag surface is relinquished mTarget.reportDropWindow(otherWindow.mInputChannelToken, 0, 0); - mTarget.handleMotionEvent(false, 0, 0); + mTarget.handleMotionEvent(false, otherWindow.getDisplayId(), 0, 0); mToken = otherWindow.mClient.asBinder(); mTarget.reportDropResult(otherIWindow, false); @@ -586,7 +586,8 @@ public class DragDropControllerTests extends WindowTestsBase { ClipData.newPlainText("label", "Test"), () -> { // Trigger an unhandled drop and verify the global drag listener was called mTarget.reportDropWindow(mWindow.mInputChannelToken, invalidXY, invalidXY); - mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY); + mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), + invalidXY, invalidXY); mTarget.reportDropResult(mWindow.mClient, false); mTarget.onUnhandledDropCallback(true); mToken = null; @@ -610,7 +611,8 @@ public class DragDropControllerTests extends WindowTestsBase { ClipData.newPlainText("label", "Test"), () -> { // Trigger an unhandled drop and verify the global drag listener was called mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); - mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY); + mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), + invalidXY, invalidXY); mTarget.onUnhandledDropCallback(true); mToken = null; try { @@ -632,7 +634,8 @@ public class DragDropControllerTests extends WindowTestsBase { startDrag(View.DRAG_FLAG_GLOBAL, ClipData.newPlainText("label", "Test"), () -> { // Trigger an unhandled drop and verify the global drag listener was not called mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); - mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY); + mTarget.handleMotionEvent(false /* keepHandling */, mDisplayContent.getDisplayId(), + invalidXY, invalidXY); mToken = null; try { verify(listener, never()).onUnhandledDrop(any(), any()); @@ -654,7 +657,8 @@ public class DragDropControllerTests extends WindowTestsBase { ClipData.newPlainText("label", "Test"), () -> { // Trigger an unhandled drop and verify the global drag listener was called mTarget.reportDropWindow(mock(IBinder.class), invalidXY, invalidXY); - mTarget.handleMotionEvent(false /* keepHandling */, invalidXY, invalidXY); + mTarget.handleMotionEvent(false /* keepHandling */, + mDisplayContent.getDisplayId(), invalidXY, invalidXY); // Verify that the unhandled drop listener callback timeout has been scheduled final Handler handler = mTarget.getHandler(); @@ -673,7 +677,8 @@ public class DragDropControllerTests extends WindowTestsBase { private void doDragAndDrop(int flags, ClipData data, float dropX, float dropY) { startDrag(flags, data, () -> { mTarget.reportDropWindow(mWindow.mInputChannelToken, dropX, dropY); - mTarget.handleMotionEvent(false /* keepHandling */, dropX, dropY); + mTarget.handleMotionEvent(false /* keepHandling */, mWindow.getDisplayId(), dropX, + dropY); mToken = mWindow.mClient.asBinder(); }); } diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 201ff51f1495..6a738ae54dcd 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -391,7 +391,6 @@ public class SizeCompatTests extends WindowTestsBase { } @Test - @EnableFlags(Flags.FLAG_IMMERSIVE_APP_REPOSITIONING) @DisableCompatChanges({ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED}) public void testRepositionLandscapeImmersiveAppWithDisplayCutout() { final int dw = 2100; @@ -3783,7 +3782,6 @@ public class SizeCompatTests extends WindowTestsBase { } @Test - @EnableFlags(Flags.FLAG_IMMERSIVE_APP_REPOSITIONING) public void testImmersiveLetterboxAlignedToBottom_OverlappingNavbar() { assertLandscapeActivityAlignedToBottomWithNavbar(true /* immersive */); } diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index 0cd036f0c61c..19c1ce2616af 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -741,16 +741,16 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // Not allowed because TaskFragments are not organized by the caller organizer. assertApplyTransactionDisallowed(mTransaction); - assertNull(mTaskFragment.getAdjacentTaskFragment()); - assertNull(taskFragment2.getAdjacentTaskFragment()); + assertFalse(mTaskFragment.hasAdjacentTaskFragment()); + assertFalse(taskFragment2.hasAdjacentTaskFragment()); mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, "Test:TaskFragmentOrganizer" /* processName */); // Not allowed because TaskFragment2 is not organized by the caller organizer. assertApplyTransactionDisallowed(mTransaction); - assertNull(mTaskFragment.getAdjacentTaskFragment()); - assertNull(taskFragment2.getAdjacentTaskFragment()); + assertFalse(mTaskFragment.hasAdjacentTaskFragment()); + assertFalse(taskFragment2.hasAdjacentTaskFragment()); mTaskFragment.onTaskFragmentOrganizerRemoved(); taskFragment2.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, @@ -758,14 +758,14 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // Not allowed because mTaskFragment is not organized by the caller organizer. assertApplyTransactionDisallowed(mTransaction); - assertNull(mTaskFragment.getAdjacentTaskFragment()); - assertNull(taskFragment2.getAdjacentTaskFragment()); + assertFalse(mTaskFragment.hasAdjacentTaskFragment()); + assertFalse(taskFragment2.hasAdjacentTaskFragment()); mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, "Test:TaskFragmentOrganizer" /* processName */); assertApplyTransactionAllowed(mTransaction); - assertEquals(taskFragment2, mTaskFragment.getAdjacentTaskFragment()); + assertTrue(mTaskFragment.isAdjacentTo(taskFragment2)); } @Test @@ -790,14 +790,14 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // Not allowed because TaskFragment is not organized by the caller organizer. assertApplyTransactionDisallowed(mTransaction); - assertEquals(taskFragment2, mTaskFragment.getAdjacentTaskFragment()); + assertTrue(mTaskFragment.isAdjacentTo(taskFragment2)); mTaskFragment.setTaskFragmentOrganizer(mOrganizerToken, 10 /* uid */, "Test:TaskFragmentOrganizer" /* processName */); assertApplyTransactionAllowed(mTransaction); - assertNull(mTaskFragment.getAdjacentTaskFragment()); - assertNull(taskFragment2.getAdjacentTaskFragment()); + assertFalse(mTaskFragment.hasAdjacentTaskFragment()); + assertFalse(taskFragment2.hasAdjacentTaskFragment()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index dafa96f91812..35a2546fca1a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -365,7 +365,7 @@ public class TaskFragmentTest extends WindowTestsBase { assertEquals(taskFragmentBounds, activity.getBounds()); assertEquals(WINDOWING_MODE_MULTI_WINDOW, activity.getWindowingMode()); - assertEquals(taskFragment1, taskFragment0.getAdjacentTaskFragment()); + assertTrue(taskFragment0.isAdjacentTo(taskFragment1)); assertEquals(taskFragment1, taskFragment0.getCompanionTaskFragment()); assertNotEquals(TaskFragmentAnimationParams.DEFAULT, taskFragment0.getAnimationParams()); @@ -381,7 +381,7 @@ public class TaskFragmentTest extends WindowTestsBase { assertEquals(taskBounds, taskFragment0.getBounds()); assertEquals(taskBounds, activity.getBounds()); assertEquals(Configuration.EMPTY, taskFragment0.getRequestedOverrideConfiguration()); - assertNull(taskFragment0.getAdjacentTaskFragment()); + assertFalse(taskFragment0.hasAdjacentTaskFragment()); assertNull(taskFragment0.getCompanionTaskFragment()); assertEquals(TaskFragmentAnimationParams.DEFAULT, taskFragment0.getAnimationParams()); // Because the whole Task is entering PiP, no need to record for future reparent. diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java index 6655932b060b..c6b2a6b8d42f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskSnapshotControllerTest.java @@ -33,6 +33,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -45,12 +46,15 @@ import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; import android.hardware.HardwareBuffer; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.util.ArraySet; import android.window.TaskSnapshot; import androidx.test.filters.SmallTest; +import com.android.window.flags.Flags; + import com.google.android.collect.Sets; import org.junit.Test; @@ -285,4 +289,27 @@ public class TaskSnapshotControllerTest extends WindowTestsBase { assertFalse(success); } + + @Test + @EnableFlags(Flags.FLAG_EXCLUDE_DRAWING_APP_THEME_SNAPSHOT_FROM_LOCK) + public void testRecordTaskSnapshot() { + spyOn(mWm.mTaskSnapshotController.mCache); + spyOn(mWm.mTaskSnapshotController); + doReturn(false).when(mWm.mTaskSnapshotController).shouldDisableSnapshots(); + + final WindowState normalWindow = createWindow(null, + FIRST_APPLICATION_WINDOW, mDisplayContent, "normalWindow"); + final TaskSnapshot snapshot = new TaskSnapshotPersisterTestBase.TaskSnapshotBuilder() + .setTopActivityComponent(normalWindow.mActivityRecord.mActivityComponent).build(); + doReturn(snapshot).when(mWm.mTaskSnapshotController).snapshot(any()); + final Task task = normalWindow.mActivityRecord.getTask(); + mWm.mTaskSnapshotController.recordSnapshot(task); + verify(mWm.mTaskSnapshotController.mCache).putSnapshot(eq(task), any()); + clearInvocations(mWm.mTaskSnapshotController.mCache); + + normalWindow.mAttrs.flags |= FLAG_SECURE; + mWm.mTaskSnapshotController.recordSnapshot(task); + waitHandlerIdle(mWm.mH); + verify(mWm.mTaskSnapshotController.mCache).putSnapshot(eq(task), any()); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index da4c522834a6..1281be5132d3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -909,8 +909,8 @@ public class WindowOrganizerTests extends WindowTestsBase { WindowContainerTransaction wct = new WindowContainerTransaction(); wct.setAdjacentRoots(info1.token, info2.token); mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct); - assertEquals(task1.getAdjacentTaskFragment(), task2); - assertEquals(task2.getAdjacentTaskFragment(), task1); + assertTrue(task1.isAdjacentTo(task2)); + assertTrue(task2.isAdjacentTo(task1)); wct = new WindowContainerTransaction(); wct.setLaunchAdjacentFlagRoot(info1.token); @@ -921,8 +921,8 @@ public class WindowOrganizerTests extends WindowTestsBase { wct.clearAdjacentRoots(info1.token); wct.clearLaunchAdjacentFlagRoot(info1.token); mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct); - assertEquals(task1.getAdjacentTaskFragment(), null); - assertEquals(task2.getAdjacentTaskFragment(), null); + assertFalse(task1.hasAdjacentTaskFragment()); + assertFalse(task2.hasAdjacentTaskFragment()); assertEquals(dc.getDefaultTaskDisplayArea().mLaunchAdjacentFlagRootTask, null); } diff --git a/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java index 34f0c191ecf5..fe9f63615757 100644 --- a/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java +++ b/tests/AppJankTest/src/android/app/jank/tests/IntegrationTests.java @@ -76,6 +76,7 @@ public class IntegrationTests { private ActivityTestRule<EmptyActivity> mEmptyActivityRule = new ActivityTestRule<>(EmptyActivity.class, false , true); + @Before public void setUp() { mInstrumentation = InstrumentationRegistry.getInstrumentation(); @@ -163,7 +164,7 @@ public class IntegrationTests { // of that state. for (int i = 0; i < uiStates.size(); i++) { StateTracker.StateData stateData = uiStates.get(i); - if (stateData.mWidgetCategory.equals(AppJankStats.ANIMATION)) { + if (stateData.mWidgetCategory.equals(AppJankStats.WIDGET_CATEGORY_ANIMATION)) { assertNotEquals(Long.MAX_VALUE, stateData.mVsyncIdEnd); } } diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java b/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java index 30c568be7716..c90595782cd1 100644 --- a/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java +++ b/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java @@ -215,7 +215,8 @@ public class JankDataProcessorTest { assertEquals(jankStats.getJankyFrameCount() * 2, pendingStat.getJankyFrames()); assertEquals(jankStats.getTotalFrameCount() * 2, pendingStat.getTotalFrames()); - int[] originalHistogramBuckets = jankStats.getFrameOverrunHistogram().getBucketCounters(); + int[] originalHistogramBuckets = + jankStats.getRelativeFrameTimeHistogram().getBucketCounters(); int[] frameOverrunBuckets = pendingStat.getFrameOverrunBuckets(); for (int i = 0; i < frameOverrunBuckets.length; i++) { diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java b/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java index 0b4d97ed20d6..df92898d76b1 100644 --- a/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java +++ b/tests/AppJankTest/src/android/app/jank/tests/JankUtils.java @@ -17,7 +17,7 @@ package android.app.jank.tests; import android.app.jank.AppJankStats; -import android.app.jank.FrameOverrunHistogram; +import android.app.jank.RelativeFrameTimeHistogram; public class JankUtils { private static final int APP_ID = 25; @@ -29,8 +29,8 @@ public class JankUtils { AppJankStats jankStats = new AppJankStats( /*App Uid*/APP_ID, /*Widget Id*/"test widget id", - /*Widget Category*/AppJankStats.SCROLL, - /*Widget State*/AppJankStats.SCROLLING, + /*Widget Category*/AppJankStats.WIDGET_CATEGORY_SCROLL, + /*Widget State*/AppJankStats.WIDGET_STATE_SCROLLING, /*Total Frames*/100, /*Janky Frames*/25, getOverrunHistogram() @@ -41,12 +41,12 @@ public class JankUtils { /** * Returns a mock histogram to be used with an AppJankStats object. */ - public static FrameOverrunHistogram getOverrunHistogram() { - FrameOverrunHistogram overrunHistogram = new FrameOverrunHistogram(); - overrunHistogram.addFrameOverrunMillis(-2); - overrunHistogram.addFrameOverrunMillis(1); - overrunHistogram.addFrameOverrunMillis(5); - overrunHistogram.addFrameOverrunMillis(25); + public static RelativeFrameTimeHistogram getOverrunHistogram() { + RelativeFrameTimeHistogram overrunHistogram = new RelativeFrameTimeHistogram(); + overrunHistogram.addRelativeFrameTimeMillis(-2); + overrunHistogram.addRelativeFrameTimeMillis(1); + overrunHistogram.addRelativeFrameTimeMillis(5); + overrunHistogram.addRelativeFrameTimeMillis(25); return overrunHistogram; } } diff --git a/tests/AppJankTest/src/android/app/jank/tests/TestWidget.java b/tests/AppJankTest/src/android/app/jank/tests/TestWidget.java index 5fff46038ead..71796d64ddee 100644 --- a/tests/AppJankTest/src/android/app/jank/tests/TestWidget.java +++ b/tests/AppJankTest/src/android/app/jank/tests/TestWidget.java @@ -45,8 +45,8 @@ public class TestWidget extends View { */ public void simulateAnimationStarting() { if (jankTrackerCreated()) { - mJankTracker.addUiState(AppJankStats.ANIMATION, - Integer.toString(this.getId()), AppJankStats.ANIMATING); + mJankTracker.addUiState(AppJankStats.WIDGET_CATEGORY_ANIMATION, + Integer.toString(this.getId()), AppJankStats.WIDGET_STATE_ANIMATING); } } @@ -55,8 +55,8 @@ public class TestWidget extends View { */ public void simulateAnimationEnding() { if (jankTrackerCreated()) { - mJankTracker.removeUiState(AppJankStats.ANIMATION, - Integer.toString(this.getId()), AppJankStats.ANIMATING); + mJankTracker.removeUiState(AppJankStats.WIDGET_CATEGORY_ANIMATION, + Integer.toString(this.getId()), AppJankStats.WIDGET_STATE_ANIMATING); } } diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java index 700856c50bae..14c8de8db5fc 100644 --- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java +++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java @@ -819,7 +819,7 @@ public class GraphicsActivity extends Activity { private List<Float> getExpectedFrameRateForCompatibility(int compatibility) { assumeTrue("**** testSurfaceControlFrameRateCompatibility SKIPPED for compatibility " + compatibility, - compatibility == Surface.FRAME_RATE_COMPATIBILITY_GTE); + compatibility == Surface.FRAME_RATE_COMPATIBILITY_AT_LEAST); Display display = getDisplay(); List<Float> expectedFrameRates = getRefreshRates(display.getMode(), display) diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java index 4d4827676c74..f1d4dc6b8faf 100644 --- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java +++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java @@ -85,7 +85,8 @@ public class SurfaceControlTest { @Test public void testSurfaceControlFrameRateCompatibilityGte() throws InterruptedException { GraphicsActivity activity = mActivityRule.getActivity(); - activity.testSurfaceControlFrameRateCompatibility(Surface.FRAME_RATE_COMPATIBILITY_GTE); + activity.testSurfaceControlFrameRateCompatibility( + Surface.FRAME_RATE_COMPATIBILITY_AT_LEAST); } @Test diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt index 2e7b20763b9e..2db8b1e18ec8 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt @@ -29,6 +29,7 @@ import android.tools.helpers.SYSTEMUI_PACKAGE import android.tools.traces.parsers.WindowManagerStateHelper import android.tools.traces.wm.WindowingMode import android.view.KeyEvent.KEYCODE_LEFT_BRACKET +import android.view.KeyEvent.KEYCODE_MINUS import android.view.KeyEvent.KEYCODE_RIGHT_BRACKET import android.view.KeyEvent.META_META_ON import android.view.WindowInsets @@ -160,10 +161,21 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : ?: error("Unable to find resource $MINIMIZE_BUTTON_VIEW\n") } - fun minimizeDesktopApp(wmHelper: WindowManagerStateHelper, device: UiDevice, isPip: Boolean = false) { - val caption = getCaptionForTheApp(wmHelper, device) - val minimizeButton = getMinimizeButtonForTheApp(caption) - minimizeButton.click() + fun minimizeDesktopApp( + wmHelper: WindowManagerStateHelper, + device: UiDevice, + isPip: Boolean = false, + usingKeyboard: Boolean = false, + ) { + if (usingKeyboard) { + val keyEventHelper = KeyEventHelper(getInstrumentation()) + keyEventHelper.press(KEYCODE_MINUS, META_META_ON) + } else { + val caption = getCaptionForTheApp(wmHelper, device) + val minimizeButton = getMinimizeButtonForTheApp(caption) + minimizeButton.click() + } + wmHelper .StateSyncBuilder() .withAppTransitionIdle() @@ -226,8 +238,7 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : toLeft: Boolean, ) { val bracketKey = if (toLeft) KEYCODE_LEFT_BRACKET else KEYCODE_RIGHT_BRACKET - keyEventHelper.actionDown(bracketKey, META_META_ON) - keyEventHelper.actionUp(bracketKey, META_META_ON) + keyEventHelper.press(bracketKey, META_META_ON) waitAndVerifySnapResize(wmHelper, context, toLeft) } diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/KeyEventHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/KeyEventHelper.kt index ebd8cc3ce1b4..55ed09154aee 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/KeyEventHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/KeyEventHelper.kt @@ -29,6 +29,10 @@ import android.view.KeyEvent class KeyEventHelper( private val instr: Instrumentation, ) { + fun press(keyCode: Int, metaState: Int = 0) { + actionDown(keyCode, metaState) + actionUp(keyCode, metaState) + } fun actionDown(keyCode: Int, metaState: Int = 0, time: Long = SystemClock.uptimeMillis()) { injectKeyEvent(ACTION_DOWN, keyCode, metaState, downTime = time, eventTime = time) diff --git a/tests/Input/src/com/android/test/input/KeyCharacterMapTest.kt b/tests/Input/src/com/android/test/input/KeyCharacterMapTest.kt index 281837920548..860d9f680c4c 100644 --- a/tests/Input/src/com/android/test/input/KeyCharacterMapTest.kt +++ b/tests/Input/src/com/android/test/input/KeyCharacterMapTest.kt @@ -16,10 +16,17 @@ package com.android.test.input +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule + import android.view.KeyCharacterMap import android.view.KeyEvent +import com.android.hardware.input.Flags + import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Rule import org.junit.Test /** @@ -30,26 +37,38 @@ import org.junit.Test * */ class KeyCharacterMapTest { + @get:Rule + val setFlagsRule = SetFlagsRule() + @Test + @EnableFlags(Flags.FLAG_REMOVE_FALLBACK_MODIFIERS) fun testGetFallback() { // Based off of VIRTUAL kcm fallbacks. val keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD) // One modifier fallback. - assertEquals( - keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_SPACE, - KeyEvent.META_CTRL_ON).keyCode, - KeyEvent.KEYCODE_LANGUAGE_SWITCH) + val oneModifierFallback = keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_SPACE, + KeyEvent.META_CTRL_ON) + assertEquals(KeyEvent.KEYCODE_LANGUAGE_SWITCH, oneModifierFallback.keyCode) + assertEquals(0, oneModifierFallback.metaState) // Multiple modifier fallback. - assertEquals( - keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_DEL, - KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON).keyCode, - KeyEvent.KEYCODE_BACK) + val twoModifierFallback = keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_DEL, + KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON) + assertEquals(KeyEvent.KEYCODE_BACK, twoModifierFallback.keyCode) + assertEquals(0, twoModifierFallback.metaState) // No default button, fallback only. - assertEquals( - keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_BUTTON_A, 0).keyCode, - KeyEvent.KEYCODE_DPAD_CENTER) + val keyOnlyFallback = + keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_BUTTON_A, 0) + assertEquals(KeyEvent.KEYCODE_DPAD_CENTER, keyOnlyFallback.keyCode) + assertEquals(0, keyOnlyFallback.metaState) + + // A key event that is not an exact match for a fallback. Expect a null return. + // E.g. Ctrl + Space -> LanguageSwitch + // Ctrl + Alt + Space -> Ctrl + Alt + Space (No fallback). + val noMatchFallback = keyCharacterMap.getFallbackAction(KeyEvent.KEYCODE_SPACE, + KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON) + assertNull(noMatchFallback) } } diff --git a/tools/aapt/Package.cpp b/tools/aapt/Package.cpp index 5e0f87f0dcaf..60c4bf5c4131 100644 --- a/tools/aapt/Package.cpp +++ b/tools/aapt/Package.cpp @@ -292,13 +292,12 @@ bool processFile(Bundle* bundle, ZipFile* zip, } if (!hasData) { const String8& srcName = file->getSourceFile(); - time_t fileModWhen; - fileModWhen = getFileModDate(srcName.c_str()); - if (fileModWhen == (time_t) -1) { // file existence tested earlier, - return false; // not expecting an error here + auto fileModWhen = getFileModDate(srcName.c_str()); + if (fileModWhen == kInvalidModDate) { // file existence tested earlier, + return false; // not expecting an error here } - - if (fileModWhen > entry->getModWhen()) { + + if (toTimeT(fileModWhen) > entry->getModWhen()) { // mark as deleted so add() will succeed if (bundle->getVerbose()) { printf(" (removing old '%s')\n", storageName.c_str()); diff --git a/tools/aapt2/Debug.cpp b/tools/aapt2/Debug.cpp index 661df4d0fe33..e24fe07f959b 100644 --- a/tools/aapt2/Debug.cpp +++ b/tools/aapt2/Debug.cpp @@ -683,8 +683,6 @@ class ChunkPrinter { item->PrettyPrint(printer_); printer_->Print(")"); } - - printer_->Print("\n"); } void PrintQualifiers(uint32_t qualifiers) const { @@ -763,11 +761,13 @@ class ChunkPrinter { bool PrintTableType(const ResTable_type* chunk) { printer_->Print(StringPrintf(" id: 0x%02x", android::util::DeviceToHost32(chunk->id))); - printer_->Print(StringPrintf( - " name: %s", - android::util::GetString(type_pool_, android::util::DeviceToHost32(chunk->id) - 1) - .c_str())); + const auto name = + android::util::GetString(type_pool_, android::util::DeviceToHost32(chunk->id) - 1); + printer_->Print(StringPrintf(" name: %s", name.c_str())); printer_->Print(StringPrintf(" flags: 0x%02x", android::util::DeviceToHost32(chunk->flags))); + printer_->Print(android::util::DeviceToHost32(chunk->flags) & ResTable_type::FLAG_SPARSE + ? " (SPARSE)" + : " (DENSE)"); printer_->Print( StringPrintf(" entryCount: %u", android::util::DeviceToHost32(chunk->entryCount))); printer_->Print( @@ -777,8 +777,7 @@ class ChunkPrinter { config.copyFromDtoH(chunk->config); printer_->Print(StringPrintf(" config: %s\n", config.to_string().c_str())); - const ResourceType* type = ParseResourceType( - android::util::GetString(type_pool_, android::util::DeviceToHost32(chunk->id) - 1)); + const ResourceType* type = ParseResourceType(name); printer_->Indent(); @@ -817,11 +816,8 @@ class ChunkPrinter { for (size_t i = 0; i < map_entry_count; i++) { PrintResValue(&(maps[i].value), config, type); - printer_->Print(StringPrintf( - " name: %s name-id:%d\n", - android::util::GetString(key_pool_, android::util::DeviceToHost32(maps[i].name.ident)) - .c_str(), - android::util::DeviceToHost32(maps[i].name.ident))); + printer_->Print(StringPrintf(" name-id: 0x%08x\n", + android::util::DeviceToHost32(maps[i].name.ident))); } } else { printer_->Print("\n"); @@ -829,6 +825,8 @@ class ChunkPrinter { // Print the value of the entry Res_value value = entry->value(); PrintResValue(&value, config, type); + + printer_->Print("\n"); } printer_->Undent(); |