diff options
421 files changed, 14349 insertions, 5132 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 1b494c51a1f4..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 diff --git a/core/api/system-current.txt b/core/api/system-current.txt index d95a17f52080..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 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/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/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/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/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/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/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/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 ec1650947aec..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; @@ -13272,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 @@ -13283,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/flags/content_capture_flags.aconfig b/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig index 20d193ea2351..f709ed7f57cd 100644 --- a/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig +++ b/core/java/android/view/contentcapture/flags/content_capture_flags.aconfig @@ -12,5 +12,5 @@ flag { name: "ccapi_baklava_enabled" namespace: "machine_learning" description: "Feature flag for baklava content capture API" - bug: "309411951" + 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..a9674716eb40 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,16 +2,6 @@ package: "com.android.window.flags" container: "system" flag { - name: "disable_thin_letterboxing_policy" - namespace: "large_screen_experiences_app_compat" - description: "Whether reachability is disabled in case of thin letterboxing" - bug: "341027847" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "allows_screen_size_decoupled_from_status_bar_and_cutout" namespace: "large_screen_experiences_app_compat" description: "When necessary, configuration decoupled from status bar and display cutout" @@ -58,13 +48,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" diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 9d11d149b0ed..80f42e587c50 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -430,4 +430,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/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/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/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/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/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/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/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/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/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/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 60584d9c6f72..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()); } @@ -419,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); } @@ -526,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; @@ -544,20 +554,26 @@ public abstract class MediaRoute2ProviderService extends Service { } } - /** Releases any system media routing resources associated with the given {@code sessionId}. */ - private boolean 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 false; + 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 true; + return streams.mSessionInfo; } } - return false; + return null; } // We cannot reach the code that requires MODIFY_AUDIO_ROUTING without holding it. @@ -1026,11 +1042,15 @@ public abstract class MediaRoute2ProviderService extends Service { if (!checkCallerIsSystem()) { return; } - // We proactively release the system media routing once the system requests it, to - // ensure it happens immediately. - if (!maybeReleaseMediaStreams(sessionId) - && !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; + } } addRequestId(requestId); @@ -1054,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; } @@ -1077,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/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/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/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/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/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/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/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/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/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/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/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/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/data/repository/CommunalSettingsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalSettingsRepositoryImplTest.kt index 8b43f14fd1d3..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 @@ -304,6 +304,34 @@ class CommunalSettingsRepositoryImplTest(flags: FlagsParameterization?) : SysuiT } } + @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) 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/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/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/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/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/row/NotificationContentInflaterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java index cdc8bc1e6cbb..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; @@ -36,7 +42,9 @@ 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; @@ -66,6 +74,7 @@ import com.android.systemui.statusbar.notification.promoted.shared.model.Promote 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; @@ -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(); } @@ -455,16 +467,88 @@ public class NotificationContentInflaterTest extends SysuiTestCase { 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(); @@ -492,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 9fb72fba4d71..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,6 +35,10 @@ 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 @@ -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 @@ -110,6 +116,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { } } private val promotedNotificationContentExtractor = FakePromotedNotificationContentExtractor() + private val conversationNotificationProcessor: ConversationNotificationProcessor = mock() @Before fun setUp() { @@ -126,7 +133,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { NotificationRowContentBinderImpl( cache, mock(), - mock<ConversationNotificationProcessor>(), + conversationNotificationProcessor, mock(), smartReplyStateInflater, layoutInflaterFactoryProvider, @@ -138,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, @@ -157,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, @@ -193,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() @@ -217,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) @@ -431,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() } @@ -440,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) } @@ -448,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() @@ -461,6 +479,7 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { false, notificationInflater, FLAG_CONTENT_VIEW_PUBLIC_SINGLE_LINE, + REDACTION_TYPE_NONE, messagingRow, ) Assert.assertNotNull(messagingRow.publicLayout.mSingleLineView) @@ -530,6 +549,80 @@ class NotificationRowContentBinderImplTest : SysuiTestCase() { 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 } @@ -568,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) @@ -603,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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 6eeb80d45211..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) { @@ -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, @@ -1320,7 +1383,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mCancellationSignal = apply( mInflationExecutor, mInflateSynchronously, - mIsMinimized, + mBindParams.isMinimized, result, mReInflateFlags, mRemoteViewCache, 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 7dcb2de57e56..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 @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.notification.row import android.annotation.SuppressLint import android.app.Notification +import android.app.Notification.MessagingStyle import android.content.Context import android.content.ContextWrapper import android.content.pm.ApplicationInfo @@ -42,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 @@ -142,9 +144,7 @@ constructor( entry, conversationProcessor, row, - bindParams.isMinimized, - bindParams.usesIncreasedHeight, - bindParams.usesIncreasedHeadsUpHeight, + bindParams, callback, remoteInputManager.remoteViewsOnClickHandler, /* isMediaFlagEnabled = */ smartReplyStateInflater, @@ -178,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, @@ -370,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, @@ -440,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, @@ -513,7 +507,7 @@ constructor( apply( inflationExecutor, inflateSynchronously, - isMinimized, + bindParams.isMinimized, progress, reInflateFlags, remoteViewCache, @@ -670,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, @@ -705,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, @@ -724,7 +717,8 @@ constructor( notification = entry.sbn.notification, messagingStyle = messagingStyle, builder = builder, - systemUiContext = systemUIContext, + systemUiContext = systemUiContext, + redactText = false, ) } else null @@ -734,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 = @@ -761,12 +765,50 @@ constructor( ) } + 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, @@ -780,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) { @@ -788,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) { @@ -800,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 ( 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/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/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/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/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/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt index eb19a9c2528d..1ce128c2403a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorSceneContainerTest.kt @@ -347,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/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/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/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/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/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/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/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/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/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/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/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 2985ad330bc6..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); } } @@ -2400,6 +2380,10 @@ public final class DisplayManagerService extends SystemService { sendDisplayEventIfEnabledLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_BASIC_CHANGED); applyDisplayChangedLocked(display); + + if (mDisplayTopologyCoordinator != null) { + mDisplayTopologyCoordinator.onDisplayChanged(display.getDisplayInfoLocked()); + } } private void applyDisplayChangedLocked(@NonNull LogicalDisplay display) { @@ -4048,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; } } } @@ -4115,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; @@ -4138,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) { @@ -4147,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); } } @@ -4183,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; } /** @@ -4191,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(); @@ -4212,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."); } @@ -4222,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 @@ -4388,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()) { @@ -6060,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/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 0438a1bac662..0d6e502cf965 100644 --- a/services/core/java/com/android/server/media/MediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/MediaRoute2Provider.java @@ -179,8 +179,29 @@ 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); diff --git a/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java b/services/core/java/com/android/server/media/MediaRoute2ProviderServiceProxy.java index 80d3c5c5c5ec..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; @@ -499,6 +500,7 @@ final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider { synchronized (mLock) { var systemMediaSessionCallback = mRequestIdToSystemSessionRequest.get(requestId); if (systemMediaSessionCallback != null) { + mRequestIdToSystemSessionRequest.remove(requestId); mSystemSessionCallbacks.put(newSession.getOriginalId(), systemMediaSessionCallback); systemMediaSessionCallback.onSessionUpdate(newSession); return; @@ -674,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) { @@ -717,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(); diff --git a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java index 83ac05d9d4c3..5e6737a485af 100644 --- a/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java +++ b/services/core/java/com/android/server/media/MediaRouter2ServiceImpl.java @@ -2634,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 @@ -3148,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 4aec3678af8b..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(); } } @@ -643,7 +643,7 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { notifyProviderState(); } - void notifySessionInfoUpdated() { + void notifyGlobalSessionInfoUpdated() { if (mCallback == null) { return; } @@ -656,7 +656,8 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { 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 a27a14b87d53..8931e3a1426e 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider2.java @@ -44,6 +44,7 @@ import com.android.server.media.MediaRoute2ProviderServiceProxy.SystemMediaSessi import java.util.Collections; import java.util.Map; +import java.util.Set; import java.util.stream.Stream; /** @@ -82,7 +83,7 @@ import java.util.stream.Stream; /** Maps request ids to pending session creation callbacks. */ @GuardedBy("mLock") - private final LongSparseArray<PendingSessionCreationCallbackImpl> mPendingSessionCreations = + private final LongSparseArray<SystemMediaSessionCallbackImpl> mPendingSessionCreations = new LongSparseArray<>(); private static final ComponentName COMPONENT_NAME = @@ -157,7 +158,7 @@ import java.util.stream.Stream; } } var pendingCreationCallback = - new PendingSessionCreationCallbackImpl( + new SystemMediaSessionCallbackImpl( targetProviderProxyId, requestId, clientPackageName); mPendingSessionCreations.put(requestId, pendingCreationCallback); targetProviderProxyRecord.requestCreateSystemMediaSession( @@ -242,7 +243,7 @@ import java.util.stream.Stream; } updateSessionInfo(); notifyProviderState(); - notifySessionInfoUpdated(); + notifyGlobalSessionInfoUpdated(); } @Override @@ -252,7 +253,7 @@ import java.util.stream.Stream; updateProviderInfo(); } updateSessionInfo(); - notifySessionInfoUpdated(); + notifyGlobalSessionInfoUpdated(); } /** @@ -302,6 +303,43 @@ import java.util.stream.Stream; 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. @@ -432,13 +470,15 @@ import java.util.stream.Stream; } } - private class PendingSessionCreationCallbackImpl implements SystemMediaSessionCallback { + 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 PendingSessionCreationCallbackImpl( + private SystemMediaSessionCallbackImpl( String providerId, long requestId, String clientPackageName) { mProviderId = providerId; mRequestId = requestId; @@ -446,27 +486,51 @@ import java.util.stream.Stream; } @Override - public void onSessionUpdate(RoutingSessionInfo sessionInfo) { - SystemMediaSessionRecord systemMediaSessionRecord = - new SystemMediaSessionRecord(mProviderId, sessionInfo); - synchronized (mLock) { - mPackageNameToSessionRecord.put(mClientPackageName, systemMediaSessionRecord); - mPendingSessionCreations.remove(mRequestId); - } + 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) { - synchronized (mLock) { - mPendingSessionCreations.remove(mRequestId); - } - notifyRequestFailed(requestId, reason); + mHandler.post( + () -> { + if (mSessionRecord != null) { + mSessionRecord.onRequestFailed(requestId, reason); + } + synchronized (mLock) { + mPendingSessionCreations.remove(mRequestId); + } + notifyRequestFailed(requestId, reason); + }); } @Override public void onSessionReleased() { - // Unexpected. The session hasn't yet been created. - throw new IllegalStateException(); + mHandler.post( + () -> { + if (mSessionRecord != null) { + mSessionRecord.onSessionReleased(); + } else { + // Should never happen. The session hasn't yet been created. + throw new IllegalStateException(); + } + }); } } @@ -494,12 +558,13 @@ import java.util.stream.Stream; } @Override - public void onSessionUpdate(RoutingSessionInfo sessionInfo) { + public void onSessionUpdate(@NonNull RoutingSessionInfo sessionInfo) { + RoutingSessionInfo translatedSessionInfo = mTranslatedSessionInfo; synchronized (mLock) { mSourceSessionInfo = sessionInfo; mTranslatedSessionInfo = asSystemProviderSession(sessionInfo); } - notifySessionInfoUpdated(); + onSessionOverrideUpdated(translatedSessionInfo); } @Override @@ -512,7 +577,7 @@ import java.util.stream.Stream; synchronized (mLock) { removeSelfFromSessionMap(); } - notifySessionInfoUpdated(); + notifyGlobalSessionInfoUpdated(); } @GuardedBy("SystemMediaRoute2Provider2.this.mLock") @@ -536,6 +601,7 @@ import java.util.stream.Stream; var builder = new RoutingSessionInfo.Builder(session) .setProviderId(mUniqueId) + .setSystemSession(true) .clearSelectedRoutes() .clearSelectableRoutes() .clearDeselectableRoutes() 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/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/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..a01fa48f2af2 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -10355,7 +10355,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/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/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..e2499bc0f3ee 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -997,11 +997,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 +1047,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/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/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..d08d6f22feef 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. 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/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/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/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/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/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/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/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/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(); |