diff options
447 files changed, 20026 insertions, 4335 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 6ecd38f054aa..3391698ee15a 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -335,6 +335,11 @@ java_aconfig_library { aconfig_declarations: "android.os.flags-aconfig", defaults: ["framework-minus-apex-aconfig-java-defaults"], mode: "exported", + min_sdk_version: "30", + apex_available: [ + "//apex_available:platform", + "com.android.mediaprovider", + ], } cc_aconfig_library { @@ -716,6 +721,7 @@ aconfig_declarations { name: "android.credentials.flags-aconfig", package: "android.credentials.flags", srcs: ["core/java/android/credentials/flags.aconfig"], + exportable: true, } java_aconfig_library { @@ -724,6 +730,13 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +java_aconfig_library { + name: "android.credentials.flags-aconfig-java-export", + aconfig_declarations: "android.credentials.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], + mode: "exported", +} + // Content Protection aconfig_declarations { name: "android.view.contentprotection.flags-aconfig", diff --git a/TEST_MAPPING b/TEST_MAPPING index c904eb46d88e..49384cde5803 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -232,30 +232,5 @@ } ] } - ], - "auto-features-postsubmit": [ - // Test tag for automotive feature targets. These are only running in postsubmit. - // This tag is used in targeted test features testing to limit resource use. - // TODO(b/256932212): this tag to be removed once the above is no longer in use. - { - "name": "FrameworksMockingServicesTests", - "options": [ - { - "include-filter": "com.android.server.pm.UserVisibilityMediatorSUSDTest" - }, - { - "include-filter": "com.android.server.pm.UserVisibilityMediatorMUMDTest" - }, - { - "include-filter": "com.android.server.pm.UserVisibilityMediatorMUPANDTest" - }, - { - "exclude-annotation": "androidx.test.filters.FlakyTest" - }, - { - "exclude-annotation": "org.junit.Ignore" - } - ] - } ] } diff --git a/core/api/current.txt b/core/api/current.txt index 4d3ca1335416..8a61f4a14b50 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -10764,6 +10764,7 @@ package android.content { field public static final String OVERLAY_SERVICE = "overlay"; field public static final String PEOPLE_SERVICE = "people"; field public static final String PERFORMANCE_HINT_SERVICE = "performance_hint"; + field @FlaggedApi("android.security.frp_enforcement") public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block"; field public static final String POWER_SERVICE = "power"; field public static final String PRINT_SERVICE = "print"; field @FlaggedApi("android.os.telemetry_apis_framework_initialization") public static final String PROFILING_SERVICE = "profiling"; @@ -20235,10 +20236,10 @@ package android.hardware.camera2.params { method public android.hardware.camera2.CaptureRequest getSessionParameters(); method public int getSessionType(); method public android.hardware.camera2.CameraCaptureSession.StateCallback getStateCallback(); - method @FlaggedApi("com.android.internal.camera.flags.camera_device_setup") public void setCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraCaptureSession.StateCallback); method public void setColorSpace(@NonNull android.graphics.ColorSpace.Named); method public void setInputConfiguration(@NonNull android.hardware.camera2.params.InputConfiguration); method public void setSessionParameters(android.hardware.camera2.CaptureRequest); + method @FlaggedApi("com.android.internal.camera.flags.camera_device_setup") public void setStateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.camera2.CameraCaptureSession.StateCallback); method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.hardware.camera2.params.SessionConfiguration> CREATOR; field public static final int SESSION_HIGH_SPEED = 1; // 0x1 diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 8ceda62e0e02..5ead3e11b387 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -598,7 +598,6 @@ package android.app { field public static final int FOREGROUND_SERVICE_API_TYPE_MICROPHONE = 6; // 0x6 field public static final int FOREGROUND_SERVICE_API_TYPE_PHONE_CALL = 7; // 0x7 field public static final int FOREGROUND_SERVICE_API_TYPE_USB = 8; // 0x8 - field @FlaggedApi("android.media.audio.foreground_audio_control") public static final int PROCESS_CAPABILITY_FOREGROUND_AUDIO_CONTROL = 64; // 0x40 field public static final int PROCESS_CAPABILITY_FOREGROUND_CAMERA = 2; // 0x2 field public static final int PROCESS_CAPABILITY_FOREGROUND_LOCATION = 1; // 0x1 field public static final int PROCESS_CAPABILITY_FOREGROUND_MICROPHONE = 4; // 0x4 @@ -3797,7 +3796,6 @@ package android.content { field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence") public static final String ON_DEVICE_INTELLIGENCE_SERVICE = "on_device_intelligence"; field public static final String PERMISSION_CONTROLLER_SERVICE = "permission_controller"; field public static final String PERMISSION_SERVICE = "permission"; - field public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block"; field public static final String REBOOT_READINESS_SERVICE = "reboot_readiness"; field public static final String ROLLBACK_SERVICE = "rollback"; field public static final String SAFETY_CENTER_SERVICE = "safety_center"; @@ -4356,7 +4354,7 @@ package android.content.pm { field @Deprecated public static final int INTENT_FILTER_VERIFICATION_SUCCESS = 1; // 0x1 field @Deprecated public static final int MASK_PERMISSION_FLAGS = 255; // 0xff field public static final int MATCH_ANY_USER = 4194304; // 0x400000 - field public static final int MATCH_CLONE_PROFILE = 536870912; // 0x20000000 + field @Deprecated public static final int MATCH_CLONE_PROFILE = 536870912; // 0x20000000 field @FlaggedApi("android.content.pm.fix_duplicated_flags") public static final long MATCH_CLONE_PROFILE_LONG = 17179869184L; // 0x400000000L field public static final int MATCH_FACTORY_ONLY = 2097152; // 0x200000 field public static final int MATCH_HIDDEN_UNTIL_INSTALLED_COMPONENTS = 536870912; // 0x20000000 diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 0a26490b772f..a2b847e0fb5f 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -2460,6 +2460,7 @@ package android.os { } public class UserManager { + method @FlaggedApi("android.os.allow_private_profile") @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}, conditional=true) public boolean canAddPrivateProfile(); method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo createProfileForUser(@Nullable String, @NonNull String, int, int, @Nullable String[]); method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo createRestrictedProfile(@Nullable String); method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_USERS, android.Manifest.permission.CREATE_USERS}) public android.content.pm.UserInfo createUser(@Nullable String, @NonNull String, int); diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index fae434828222..0c543515f4cf 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -20,7 +20,6 @@ import static android.app.WindowConfiguration.activityTypeToString; import static android.app.WindowConfiguration.windowingModeToString; import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE; -import static android.media.audio.Flags.FLAG_FOREGROUND_AUDIO_CONTROL; import android.Manifest; import android.annotation.ColorInt; @@ -948,8 +947,6 @@ public class ActivityManager { * @hide * Process can access volume APIs and can request audio focus with GAIN. */ - @FlaggedApi(FLAG_FOREGROUND_AUDIO_CONTROL) - @SystemApi public static final int PROCESS_CAPABILITY_FOREGROUND_AUDIO_CONTROL = 1 << 6; /** diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index a8352fad8a90..ff713d071a05 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1581,6 +1581,10 @@ public class AppOpsManager { * Allows an app to access location without the traditional location permissions and while the * user location setting is off, but only during pre-defined emergency sessions. * + * <p>This op is only used for tracking, not for permissions, so it is still the client's + * responsibility to check the {@link Manifest.permission.LOCATION_BYPASS} permission + * appropriately. + * * @hide */ public static final int OP_EMERGENCY_LOCATION = AppProtoEnums.APP_OP_EMERGENCY_LOCATION; @@ -2459,6 +2463,10 @@ public class AppOpsManager { * Allows an app to access location without the traditional location permissions and while the * user location setting is off, but only during pre-defined emergency sessions. * + * <p>This op is only used for tracking, not for permissions, so it is still the client's + * responsibility to check the {@link Manifest.permission.LOCATION_BYPASS} permission + * appropriately. + * * @hide */ @SystemApi @@ -3047,8 +3055,10 @@ public class AppOpsManager { new AppOpInfo.Builder(OP_UNARCHIVAL_CONFIRMATION, OPSTR_UNARCHIVAL_CONFIRMATION, "UNARCHIVAL_CONFIRMATION") .setDefaultMode(MODE_ALLOWED).build(), - // TODO(b/301150056): STOPSHIP determine how this appop should work with the permission new AppOpInfo.Builder(OP_EMERGENCY_LOCATION, OPSTR_EMERGENCY_LOCATION, "EMERGENCY_LOCATION") + .setDefaultMode(MODE_ALLOWED) + // even though this has a permission associated, this op is only used for tracking, + // and the client is responsible for checking the LOCATION_BYPASS permission. .setPermission(Manifest.permission.LOCATION_BYPASS).build(), }; diff --git a/core/java/android/app/ApplicationExitInfo.java b/core/java/android/app/ApplicationExitInfo.java index 24cb9ea87a12..cac10f588aa8 100644 --- a/core/java/android/app/ApplicationExitInfo.java +++ b/core/java/android/app/ApplicationExitInfo.java @@ -487,6 +487,15 @@ public final class ApplicationExitInfo implements Parcelable { */ public static final int SUBREASON_FREEZER_BINDER_ASYNC_FULL = 31; + /** + * The process was killed because it was sending too many broadcasts while it is in the + * Cached state. This would be set only when the reason is {@link #REASON_OTHER}. + * + * For internal use only. + * @hide + */ + public static final int SUBREASON_EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED = 32; + // If there is any OEM code which involves additional app kill reasons, it should // be categorized in {@link #REASON_OTHER}, with subreason code starting from 1000. @@ -665,6 +674,7 @@ public final class ApplicationExitInfo implements Parcelable { SUBREASON_EXCESSIVE_BINDER_OBJECTS, SUBREASON_OOM_KILL, SUBREASON_FREEZER_BINDER_ASYNC_FULL, + SUBREASON_EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED, }) @Retention(RetentionPolicy.SOURCE) public @interface SubReason {} @@ -1396,6 +1406,8 @@ public final class ApplicationExitInfo implements Parcelable { return "OOM KILL"; case SUBREASON_FREEZER_BINDER_ASYNC_FULL: return "FREEZER BINDER ASYNC FULL"; + case SUBREASON_EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED: + return "EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED"; default: return "UNKNOWN"; } diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 6f6e0911fa4b..716dee4dc082 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -344,23 +344,37 @@ class ContextImpl extends Context { */ private boolean mOwnsToken = false; - private final Object mDirsLock = new Object(); - @GuardedBy("mDirsLock") + private final Object mDatabasesDirLock = new Object(); + @GuardedBy("mDatabasesDirLock") private File mDatabasesDir; - @GuardedBy("mDirsLock") + + private final Object mPreferencesDirLock = new Object(); @UnsupportedAppUsage + @GuardedBy("mPreferencesDirLock") private File mPreferencesDir; - @GuardedBy("mDirsLock") + + private final Object mFilesDirLock = new Object(); + @GuardedBy("mFilesDirLock") private File mFilesDir; - @GuardedBy("mDirsLock") + + private final Object mCratesDirLock = new Object(); + @GuardedBy("mCratesDirLock") private File mCratesDir; - @GuardedBy("mDirsLock") + + private final Object mNoBackupFilesDirLock = new Object(); + @GuardedBy("mNoBackupFilesDirLock") private File mNoBackupFilesDir; - @GuardedBy("mDirsLock") + + private final Object mCacheDirLock = new Object(); + @GuardedBy("mCacheDirLock") private File mCacheDir; - @GuardedBy("mDirsLock") + + private final Object mCodeCacheDirLock = new Object(); + @GuardedBy("mCodeCacheDirLock") private File mCodeCacheDir; + private final Object mMiscDirsLock = new Object(); + // The system service cache for the system services that are cached per-ContextImpl. @UnsupportedAppUsage final Object[] mServiceCache = SystemServiceRegistry.createServiceCache(); @@ -742,7 +756,7 @@ class ContextImpl extends Context { @UnsupportedAppUsage private File getPreferencesDir() { - synchronized (mDirsLock) { + synchronized (mPreferencesDirLock) { if (mPreferencesDir == null) { mPreferencesDir = new File(getDataDir(), "shared_prefs"); } @@ -831,7 +845,7 @@ class ContextImpl extends Context { @Override public File getFilesDir() { - synchronized (mDirsLock) { + synchronized (mFilesDirLock) { if (mFilesDir == null) { mFilesDir = new File(getDataDir(), "files"); } @@ -846,7 +860,7 @@ class ContextImpl extends Context { final Path absoluteNormalizedCratePath = cratesRootPath.resolve(crateId) .toAbsolutePath().normalize(); - synchronized (mDirsLock) { + synchronized (mCratesDirLock) { if (mCratesDir == null) { mCratesDir = cratesRootPath.toFile(); } @@ -859,7 +873,7 @@ class ContextImpl extends Context { @Override public File getNoBackupFilesDir() { - synchronized (mDirsLock) { + synchronized (mNoBackupFilesDirLock) { if (mNoBackupFilesDir == null) { mNoBackupFilesDir = new File(getDataDir(), "no_backup"); } @@ -876,7 +890,7 @@ class ContextImpl extends Context { @Override public File[] getExternalFilesDirs(String type) { - synchronized (mDirsLock) { + synchronized (mMiscDirsLock) { File[] dirs = Environment.buildExternalStorageAppFilesDirs(getPackageName()); if (type != null) { dirs = Environment.buildPaths(dirs, type); @@ -894,7 +908,7 @@ class ContextImpl extends Context { @Override public File[] getObbDirs() { - synchronized (mDirsLock) { + synchronized (mMiscDirsLock) { File[] dirs = Environment.buildExternalStorageAppObbDirs(getPackageName()); return ensureExternalDirsExistOrFilter(dirs, true /* tryCreateInProcess */); } @@ -902,7 +916,7 @@ class ContextImpl extends Context { @Override public File getCacheDir() { - synchronized (mDirsLock) { + synchronized (mCacheDirLock) { if (mCacheDir == null) { mCacheDir = new File(getDataDir(), "cache"); } @@ -912,7 +926,7 @@ class ContextImpl extends Context { @Override public File getCodeCacheDir() { - synchronized (mDirsLock) { + synchronized (mCodeCacheDirLock) { if (mCodeCacheDir == null) { mCodeCacheDir = getCodeCacheDirBeforeBind(getDataDir()); } @@ -938,7 +952,7 @@ class ContextImpl extends Context { @Override public File[] getExternalCacheDirs() { - synchronized (mDirsLock) { + synchronized (mMiscDirsLock) { File[] dirs = Environment.buildExternalStorageAppCacheDirs(getPackageName()); // We don't try to create cache directories in-process, because they need special // setup for accurate quota tracking. This ensures the cache dirs are always @@ -949,7 +963,7 @@ class ContextImpl extends Context { @Override public File[] getExternalMediaDirs() { - synchronized (mDirsLock) { + synchronized (mMiscDirsLock) { File[] dirs = Environment.buildExternalStorageAppMediaDirs(getPackageName()); return ensureExternalDirsExistOrFilter(dirs, true /* tryCreateInProcess */); } @@ -1051,7 +1065,7 @@ class ContextImpl extends Context { } private File getDatabasesDir() { - synchronized (mDirsLock) { + synchronized (mDatabasesDirLock) { if (mDatabasesDir == null) { if ("android".equals(getPackageName())) { mDatabasesDir = new File("/data/system"); diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 1cbec3126aac..66ec865092f7 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -450,6 +450,11 @@ public final class SystemServiceRegistry { new CachedServiceFetcher<VcnManager>() { @Override public VcnManager createService(ContextImpl ctx) throws ServiceNotFoundException { + if (!ctx.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) { + return null; + } + IBinder b = ServiceManager.getService(Context.VCN_MANAGEMENT_SERVICE); IVcnManagementService service = IVcnManagementService.Stub.asInterface(b); return new VcnManager(ctx, service); @@ -1736,6 +1741,13 @@ public final class SystemServiceRegistry { return fetcher; } + private static boolean hasSystemFeatureOpportunistic(@NonNull ContextImpl ctx, + @NonNull String featureName) { + PackageManager manager = ctx.getPackageManager(); + if (manager == null) return true; + return manager.hasSystemFeature(featureName); + } + /** * Gets a system service from a given context. * @hide @@ -1758,12 +1770,18 @@ public final class SystemServiceRegistry { case Context.VIRTUALIZATION_SERVICE: case Context.VIRTUAL_DEVICE_SERVICE: return null; + case Context.VCN_MANAGEMENT_SERVICE: + if (!hasSystemFeatureOpportunistic(ctx, + PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) { + return null; + } + break; case Context.SEARCH_SERVICE: // Wear device does not support SEARCH_SERVICE so we do not print WTF here - PackageManager manager = ctx.getPackageManager(); - if (manager != null && manager.hasSystemFeature(PackageManager.FEATURE_WATCH)) { + if (hasSystemFeatureOpportunistic(ctx, PackageManager.FEATURE_WATCH)) { return null; } + break; } Slog.wtf(TAG, "Manager wrapper not available: " + name); return null; diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index a075ac51e1ed..60dffbd0e421 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -6545,8 +6545,10 @@ public class DevicePolicyManager { } /** - * Flag for {@link #wipeData(int)}: also erase the device's external - * storage (such as SD cards). + * Flag for {@link #wipeData(int)}: also erase the device's adopted external storage (such as + * adopted SD cards). + * @see <a href="{@docRoot}about/versions/marshmallow/android-6.0.html#adoptable-storage"> + * Adoptable Storage Devices</a> */ public static final int WIPE_EXTERNAL_STORAGE = 0x0001; diff --git a/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl b/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl index 0dbe18156904..8bf288abb0f9 100644 --- a/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl +++ b/core/java/android/app/ondeviceintelligence/IOnDeviceIntelligenceManager.aidl @@ -53,19 +53,22 @@ void getFeatureDetails(in Feature feature, in IFeatureDetailsCallback featureDetailsCallback) = 4; @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") - void requestFeatureDownload(in Feature feature, in ICancellationSignal signal, in IDownloadCallback callback) = 5; + void requestFeatureDownload(in Feature feature, in AndroidFuture cancellationSignalFuture, in IDownloadCallback callback) = 5; @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") - void requestTokenInfo(in Feature feature, in Bundle requestBundle, in ICancellationSignal signal, + void requestTokenInfo(in Feature feature, in Bundle requestBundle, in AndroidFuture cancellationSignalFuture, in ITokenInfoCallback tokenInfocallback) = 6; @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") - void processRequest(in Feature feature, in Bundle requestBundle, int requestType, in ICancellationSignal cancellationSignal, - in IProcessingSignal signal, in IResponseCallback responseCallback) = 7; + void processRequest(in Feature feature, in Bundle requestBundle, int requestType, + in AndroidFuture cancellationSignalFuture, + in AndroidFuture processingSignalFuture, + in IResponseCallback responseCallback) = 7; @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.USE_ON_DEVICE_INTELLIGENCE)") void processRequestStreaming(in Feature feature, - in Bundle requestBundle, int requestType, in ICancellationSignal cancellationSignal, in IProcessingSignal signal, + in Bundle requestBundle, int requestType, in AndroidFuture cancellationSignalFuture, + in AndroidFuture processingSignalFuture, in IStreamingResponseCallback streamingCallback) = 8; String getRemoteServicePackageName() = 9; diff --git a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java index a465e3cbb6ec..bc50d2e492ae 100644 --- a/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java +++ b/core/java/android/app/ondeviceintelligence/OnDeviceIntelligenceManager.java @@ -26,22 +26,23 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; -import android.content.ComponentName; import android.content.Context; import android.graphics.Bitmap; import android.os.Binder; import android.os.Bundle; import android.os.CancellationSignal; +import android.os.IBinder; import android.os.ICancellationSignal; import android.os.OutcomeReceiver; import android.os.PersistableBundle; import android.os.RemoteCallback; import android.os.RemoteException; import android.system.OsConstants; +import android.util.Log; import androidx.annotation.IntDef; -import com.android.internal.R; +import com.android.internal.infra.AndroidFuture; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -76,6 +77,8 @@ public final class OnDeviceIntelligenceManager { */ public static final String AUGMENT_REQUEST_CONTENT_BUNDLE_KEY = "AugmentRequestContentBundleKey"; + + private static final String TAG = "OnDeviceIntelligence"; private final Context mContext; private final IOnDeviceIntelligenceManager mService; @@ -121,9 +124,9 @@ public final class OnDeviceIntelligenceManager { @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) public String getRemoteServicePackageName() { String result; - try{ - result = mService.getRemoteServicePackageName(); - } catch (RemoteException e){ + try { + result = mService.getRemoteServicePackageName(); + } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } return result; @@ -288,18 +291,15 @@ public final class OnDeviceIntelligenceManager { } }; - ICancellationSignal transport = null; - if (cancellationSignal != null) { - transport = CancellationSignal.createTransport(); - cancellationSignal.setRemote(transport); - } - - mService.requestFeatureDownload(feature, transport, downloadCallback); + mService.requestFeatureDownload(feature, + configureRemoteCancellationFuture(cancellationSignal, callbackExecutor), + downloadCallback); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } + /** * The methods computes the token related information for a given request payload using the * provided {@link Feature}. @@ -337,13 +337,9 @@ public final class OnDeviceIntelligenceManager { } }; - ICancellationSignal transport = null; - if (cancellationSignal != null) { - transport = CancellationSignal.createTransport(); - cancellationSignal.setRemote(transport); - } - - mService.requestTokenInfo(feature, request, transport, callback); + mService.requestTokenInfo(feature, request, + configureRemoteCancellationFuture(cancellationSignal, callbackExecutor), + callback); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -407,19 +403,9 @@ public final class OnDeviceIntelligenceManager { }; - IProcessingSignal transport = null; - if (processingSignal != null) { - transport = ProcessingSignal.createTransport(); - processingSignal.setRemote(transport); - } - - ICancellationSignal cancellationTransport = null; - if (cancellationSignal != null) { - cancellationTransport = CancellationSignal.createTransport(); - cancellationSignal.setRemote(cancellationTransport); - } - - mService.processRequest(feature, request, requestType, cancellationTransport, transport, + mService.processRequest(feature, request, requestType, + configureRemoteCancellationFuture(cancellationSignal, callbackExecutor), + configureRemoteProcessingSignalFuture(processingSignal, callbackExecutor), callback); } catch (RemoteException e) { @@ -449,7 +435,8 @@ public final class OnDeviceIntelligenceManager { * @param callbackExecutor executor to run the callback on. */ @RequiresPermission(Manifest.permission.USE_ON_DEVICE_INTELLIGENCE) - public void processRequestStreaming(@NonNull Feature feature, @NonNull @InferenceParams Bundle request, + public void processRequestStreaming(@NonNull Feature feature, + @NonNull @InferenceParams Bundle request, @RequestType int requestType, @Nullable CancellationSignal cancellationSignal, @Nullable ProcessingSignal processingSignal, @@ -500,20 +487,11 @@ public final class OnDeviceIntelligenceManager { } }; - IProcessingSignal transport = null; - if (processingSignal != null) { - transport = ProcessingSignal.createTransport(); - processingSignal.setRemote(transport); - } - - ICancellationSignal cancellationTransport = null; - if (cancellationSignal != null) { - cancellationTransport = CancellationSignal.createTransport(); - cancellationSignal.setRemote(cancellationTransport); - } - mService.processRequestStreaming( - feature, request, requestType, cancellationTransport, transport, callback); + feature, request, requestType, + configureRemoteCancellationFuture(cancellationSignal, callbackExecutor), + configureRemoteProcessingSignalFuture(processingSignal, callbackExecutor), + callback); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -574,4 +552,45 @@ public final class OnDeviceIntelligenceManager { @Target({ElementType.PARAMETER, ElementType.FIELD}) public @interface InferenceParams { } + + + @Nullable + private static AndroidFuture<IBinder> configureRemoteCancellationFuture( + @Nullable CancellationSignal cancellationSignal, + @NonNull Executor callbackExecutor) { + if (cancellationSignal == null) { + return null; + } + AndroidFuture<IBinder> cancellationFuture = new AndroidFuture<>(); + cancellationFuture.whenCompleteAsync( + (cancellationTransport, error) -> { + if (error != null || cancellationTransport == null) { + Log.e(TAG, "Unable to receive the remote cancellation signal.", error); + } else { + cancellationSignal.setRemote( + ICancellationSignal.Stub.asInterface(cancellationTransport)); + } + }, callbackExecutor); + return cancellationFuture; + } + + @Nullable + private static AndroidFuture<IBinder> configureRemoteProcessingSignalFuture( + ProcessingSignal processingSignal, Executor executor) { + if (processingSignal == null) { + return null; + } + AndroidFuture<IBinder> processingSignalFuture = new AndroidFuture<>(); + processingSignalFuture.whenCompleteAsync( + (transport, error) -> { + if (error != null || transport == null) { + Log.e(TAG, "Unable to receive the remote processing signal.", error); + } else { + processingSignal.setRemote(IProcessingSignal.Stub.asInterface(transport)); + } + }, executor); + return processingSignalFuture; + } + + } diff --git a/core/java/android/app/ondeviceintelligence/ProcessingSignal.java b/core/java/android/app/ondeviceintelligence/ProcessingSignal.java index c275cc786007..733f4fad96f4 100644 --- a/core/java/android/app/ondeviceintelligence/ProcessingSignal.java +++ b/core/java/android/app/ondeviceintelligence/ProcessingSignal.java @@ -123,10 +123,10 @@ public final class ProcessingSignal { * Sets the processing signal callback to be called when signals are received. * * This method is intended to be used by the recipient of a processing signal - * such as the remote implementation for {@link OnDeviceIntelligenceManager} to handle - * cancellation requests while performing a long-running operation. This method is not - * intended - * to be used by applications themselves. + * such as the remote implementation in + * {@link android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService} to handle + * processing signals while performing a long-running operation. This method is not + * intended to be used by the caller themselves. * * If {@link ProcessingSignal#sendSignal} has already been called, then the provided callback * is invoked immediately and all previously queued actions are passed to remote signal. @@ -200,7 +200,7 @@ public final class ProcessingSignal { } /** - * Given a locally created transport, returns its associated cancellation signal. + * Given a locally created transport, returns its associated processing signal. * * @param transport The locally created transport, or null if none. * @return The associated processing signal, or null if none. diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl index 6eab363c4eb1..30a1135d6be4 100644 --- a/core/java/android/companion/virtual/IVirtualDevice.aidl +++ b/core/java/android/companion/virtual/IVirtualDevice.aidl @@ -79,6 +79,11 @@ interface IVirtualDevice { int getDevicePolicy(int policyType); /** + * Returns whether the device has a valid microphone. + */ + boolean hasCustomAudioInputSupport(); + + /** * Closes the virtual device and frees all associated resources. */ @EnforcePermission("CREATE_VIRTUAL_DEVICE") diff --git a/core/java/android/companion/virtual/VirtualDevice.java b/core/java/android/companion/virtual/VirtualDevice.java index 97fa2ba2638d..b9e9afea8893 100644 --- a/core/java/android/companion/virtual/VirtualDevice.java +++ b/core/java/android/companion/virtual/VirtualDevice.java @@ -17,7 +17,6 @@ package android.companion.virtual; import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM; -import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_SENSORS; @@ -176,8 +175,7 @@ public final class VirtualDevice implements Parcelable { @FlaggedApi(Flags.FLAG_VDM_PUBLIC_APIS) public boolean hasCustomAudioInputSupport() { try { - return mVirtualDevice.getDevicePolicy(POLICY_TYPE_AUDIO) == DEVICE_POLICY_CUSTOM; - // TODO(b/291735254): also check for a custom audio injection mix for this device id. + return mVirtualDevice.hasCustomAudioInputSupport(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index 3304475df89f..ec59cf61097b 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -972,6 +972,7 @@ public final class VirtualDeviceManager { * * @param config camera configuration. * @return newly created camera. + * @throws UnsupportedOperationException if virtual camera isn't supported on this device. * @see VirtualDeviceParams#POLICY_TYPE_CAMERA */ @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 89300e3a15f1..284e3184d436 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -4208,7 +4208,7 @@ public abstract class Context { MEDIA_COMMUNICATION_SERVICE, BATTERY_SERVICE, JOB_SCHEDULER_SERVICE, - //@hide: PERSISTENT_DATA_BLOCK_SERVICE, + PERSISTENT_DATA_BLOCK_SERVICE, //@hide: OEM_LOCK_SERVICE, MEDIA_PROJECTION_SERVICE, MIDI_SERVICE, @@ -5930,9 +5930,8 @@ public abstract class Context { * * @see #getSystemService(String) * @see android.service.persistentdata.PersistentDataBlockManager - * @hide */ - @SystemApi + @FlaggedApi(android.security.Flags.FLAG_FRP_ENFORCEMENT) public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block"; /** diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 9f2f74b66eb3..b5809cfb9170 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -895,7 +895,7 @@ public abstract class PackageManager { GET_DISABLED_COMPONENTS, GET_DISABLED_UNTIL_USED_COMPONENTS, GET_UNINSTALLED_PACKAGES, - MATCH_CLONE_PROFILE, + MATCH_CLONE_PROFILE_LONG, MATCH_QUARANTINED_COMPONENTS, }) @Retention(RetentionPolicy.SOURCE) @@ -1235,10 +1235,11 @@ public abstract class PackageManager { public static final int MATCH_DEBUG_TRIAGED_MISSING = MATCH_DIRECT_BOOT_AUTO; /** - * Use {@link #MATCH_CLONE_PROFILE_LONG} instead. + * @deprecated Use {@link #MATCH_CLONE_PROFILE_LONG} instead. * * @hide */ + @Deprecated @SystemApi public static final int MATCH_CLONE_PROFILE = 0x20000000; diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig index 47edba6a9e56..16ca31f27028 100644 --- a/core/java/android/credentials/flags.aconfig +++ b/core/java/android/credentials/flags.aconfig @@ -47,6 +47,7 @@ flag { name: "configurable_selector_ui_enabled" description: "Enables OEM configurable Credential Selector UI" bug: "319448437" + is_exported: true } flag { diff --git a/core/java/android/credentials/selection/IntentCreationResult.java b/core/java/android/credentials/selection/IntentCreationResult.java new file mode 100644 index 000000000000..189ff7bbcb6e --- /dev/null +++ b/core/java/android/credentials/selection/IntentCreationResult.java @@ -0,0 +1,155 @@ +/* + * 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.credentials.selection; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Intent; + +/** + * Result of creating a Credential Manager UI intent. + * + * @hide + */ +public final class IntentCreationResult { + @NonNull + private final Intent mIntent; + @Nullable + private final String mFallbackUiPackageName; + @Nullable + private final String mOemUiPackageName; + @NonNull + private final OemUiUsageStatus mOemUiUsageStatus; + + private IntentCreationResult(@NonNull Intent intent, @Nullable String fallbackUiPackageName, + @Nullable String oemUiPackageName, OemUiUsageStatus oemUiUsageStatus) { + mIntent = intent; + mFallbackUiPackageName = fallbackUiPackageName; + mOemUiPackageName = oemUiPackageName; + mOemUiUsageStatus = oemUiUsageStatus; + } + + /** Returns the UI intent. */ + @NonNull + public Intent getIntent() { + return mIntent; + } + + /** + * Returns the result of attempting to use the config_oemCredentialManagerDialogComponent + * as the Credential Manager UI. + */ + @NonNull + public OemUiUsageStatus getOemUiUsageStatus() { + return mOemUiUsageStatus; + } + + /** + * Returns the package name of the ui component specified in + * config_fallbackCredentialManagerDialogComponent, or null if unspecified / not parsable + * successfully. + */ + @Nullable + public String getFallbackUiPackageName() { + return mFallbackUiPackageName; + } + + /** + * Returns the package name of the oem ui component specified in + * config_oemCredentialManagerDialogComponent, or null if unspecified / not parsable. + */ + @Nullable + public String getOemUiPackageName() { + return mOemUiPackageName; + } + + /** + * Result of attempting to use the config_oemCredentialManagerDialogComponent as the Credential + * Manager UI. + */ + public enum OemUiUsageStatus { + UNKNOWN, + // Success: the UI specified in config_oemCredentialManagerDialogComponent was used to + // fulfill the request. + SUCCESS, + // The config value was not specified (e.g. left empty). + OEM_UI_CONFIG_NOT_SPECIFIED, + // The config value component was specified but not found (e.g. component doesn't exist or + // component isn't a system app). + OEM_UI_CONFIG_SPECIFIED_BUT_NOT_FOUND, + // The config value component was found but not enabled. + OEM_UI_CONFIG_SPECIFIED_FOUND_BUT_NOT_ENABLED, + } + + /** + * Builder for {@link IntentCreationResult}. + * + * @hide + */ + public static final class Builder { + @NonNull + private Intent mIntent; + @Nullable + private String mFallbackUiPackageName = null; + @Nullable + private String mOemUiPackageName = null; + @NonNull + private OemUiUsageStatus mOemUiUsageStatus = OemUiUsageStatus.UNKNOWN; + + public Builder(Intent intent) { + mIntent = intent; + } + + /** + * Sets the package name of the ui component specified in + * config_fallbackCredentialManagerDialogComponent, or null if unspecified / not parsable + * successfully. + */ + @NonNull + public Builder setFallbackUiPackageName(@Nullable String fallbackUiPackageName) { + mFallbackUiPackageName = fallbackUiPackageName; + return this; + } + + /** + * Sets the package name of the oem ui component specified in + * config_oemCredentialManagerDialogComponent, or null if unspecified / not parsable. + */ + @NonNull + public Builder setOemUiPackageName(@Nullable String oemUiPackageName) { + mOemUiPackageName = oemUiPackageName; + return this; + } + + /** + * Sets the result of attempting to use the config_oemCredentialManagerDialogComponent + * as the Credential Manager UI. + */ + @NonNull + public Builder setOemUiUsageStatus(OemUiUsageStatus oemUiUsageStatus) { + mOemUiUsageStatus = oemUiUsageStatus; + return this; + } + + /** Builds a {@link IntentCreationResult}. */ + @NonNull + public IntentCreationResult build() { + return new IntentCreationResult(mIntent, mFallbackUiPackageName, mOemUiPackageName, + mOemUiUsageStatus); + } + } +} diff --git a/core/java/android/credentials/selection/IntentFactory.java b/core/java/android/credentials/selection/IntentFactory.java index 79fba9b19250..b98a0d825227 100644 --- a/core/java/android/credentials/selection/IntentFactory.java +++ b/core/java/android/credentials/selection/IntentFactory.java @@ -36,6 +36,8 @@ import android.os.ResultReceiver; import android.text.TextUtils; import android.util.Slog; +import com.android.internal.annotations.VisibleForTesting; + import java.util.ArrayList; /** @@ -57,22 +59,104 @@ public class IntentFactory { * @hide */ @NonNull - public static Intent createCredentialSelectorIntentForAutofill( + public static IntentCreationResult createCredentialSelectorIntentForAutofill( + @NonNull Context context, + @NonNull RequestInfo requestInfo, + @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling. + @NonNull + ArrayList<DisabledProviderData> disabledProviderDataList, + @NonNull ResultReceiver resultReceiver) { + return createCredentialSelectorIntentInternal(context, requestInfo, + disabledProviderDataList, resultReceiver); + } + + /** + * Generate a new launch intent to the Credential Selector UI. + * + * @param context the CredentialManager system service (only expected caller) + * context that may be used to query existence of the key UI + * application + * @param disabledProviderDataList the list of disabled provider data that when non-empty the + * UI should accordingly generate an entry suggesting the user + * to navigate to settings and enable them + * @param enabledProviderDataList the list of enabled provider that contain options for this + * request; the UI should render each option to the user for + * selection + * @param requestInfo the display information about the given app request + * @param resultReceiver used by the UI to send the UI selection result back + * @hide + */ + @NonNull + public static IntentCreationResult createCredentialSelectorIntentForCredMan( @NonNull Context context, @NonNull RequestInfo requestInfo, @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling. @NonNull + ArrayList<ProviderData> enabledProviderDataList, + @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling. + @NonNull ArrayList<DisabledProviderData> disabledProviderDataList, @NonNull ResultReceiver resultReceiver) { - return createCredentialSelectorIntent(context, requestInfo, + IntentCreationResult result = createCredentialSelectorIntentInternal(context, requestInfo, disabledProviderDataList, resultReceiver); + result.getIntent().putParcelableArrayListExtra( + ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, enabledProviderDataList); + return result; + } + + /** + * Generate a new launch intent to the Credential Selector UI. + * + * @param context the CredentialManager system service (only expected caller) + * context that may be used to query existence of the key UI + * application + * @param disabledProviderDataList the list of disabled provider data that when non-empty the + * UI should accordingly generate an entry suggesting the user + * to navigate to settings and enable them + * @param enabledProviderDataList the list of enabled provider that contain options for this + * request; the UI should render each option to the user for + * selection + * @param requestInfo the display information about the given app request + * @param resultReceiver used by the UI to send the UI selection result back + */ + @VisibleForTesting + @NonNull + public static Intent createCredentialSelectorIntent( + @NonNull Context context, + @NonNull RequestInfo requestInfo, + @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling. + @NonNull + ArrayList<ProviderData> enabledProviderDataList, + @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling. + @NonNull + ArrayList<DisabledProviderData> disabledProviderDataList, + @NonNull ResultReceiver resultReceiver) { + return createCredentialSelectorIntentForCredMan(context, requestInfo, + enabledProviderDataList, disabledProviderDataList, resultReceiver).getIntent(); + } + + /** + * Creates an Intent that cancels any UI matching the given request token id. + */ + @VisibleForTesting + @NonNull + public static Intent createCancelUiIntent(@NonNull Context context, + @NonNull IBinder requestToken, boolean shouldShowCancellationUi, + @NonNull String appPackageName) { + Intent intent = new Intent(); + IntentCreationResult.Builder intentResultBuilder = new IntentCreationResult.Builder(intent); + setCredentialSelectorUiComponentName(context, intent, intentResultBuilder); + intent.putExtra(CancelSelectionRequest.EXTRA_CANCEL_UI_REQUEST, + new CancelSelectionRequest(new RequestToken(requestToken), shouldShowCancellationUi, + appPackageName)); + return intent; } /** * Generate a new launch intent to the Credential Selector UI. */ @NonNull - private static Intent createCredentialSelectorIntent( + private static IntentCreationResult createCredentialSelectorIntentInternal( @NonNull Context context, @NonNull RequestInfo requestInfo, @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling. @@ -80,25 +164,37 @@ public class IntentFactory { ArrayList<DisabledProviderData> disabledProviderDataList, @NonNull ResultReceiver resultReceiver) { Intent intent = new Intent(); - setCredentialSelectorUiComponentName(context, intent); + IntentCreationResult.Builder intentResultBuilder = new IntentCreationResult.Builder(intent); + setCredentialSelectorUiComponentName(context, intent, intentResultBuilder); intent.putParcelableArrayListExtra( ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST, disabledProviderDataList); intent.putExtra(RequestInfo.EXTRA_REQUEST_INFO, requestInfo); intent.putExtra( Constants.EXTRA_RESULT_RECEIVER, toIpcFriendlyResultReceiver(resultReceiver)); - - return intent; + return intentResultBuilder.build(); } private static void setCredentialSelectorUiComponentName(@NonNull Context context, - @NonNull Intent intent) { + @NonNull Intent intent, @NonNull IntentCreationResult.Builder intentResultBuilder) { if (configurableSelectorUiEnabled()) { - ComponentName componentName = getOemOverrideComponentName(context); + ComponentName componentName = getOemOverrideComponentName(context, intentResultBuilder); + + ComponentName fallbackUiComponentName = null; + try { + fallbackUiComponentName = ComponentName.unflattenFromString( + Resources.getSystem().getString( + com.android.internal.R.string + .config_fallbackCredentialManagerDialogComponent)); + intentResultBuilder.setFallbackUiPackageName( + fallbackUiComponentName.getPackageName()); + } catch (Exception e) { + Slog.w(TAG, "Fallback CredMan IU not found: " + e); + } + if (componentName == null) { - componentName = ComponentName.unflattenFromString(Resources.getSystem().getString( - com.android.internal.R.string - .config_fallbackCredentialManagerDialogComponent)); + componentName = fallbackUiComponentName; } + intent.setComponent(componentName); } else { ComponentName componentName = ComponentName.unflattenFromString(Resources.getSystem() @@ -113,7 +209,8 @@ public class IntentFactory { * default platform UI component name should be used instead. */ @Nullable - private static ComponentName getOemOverrideComponentName(@NonNull Context context) { + private static ComponentName getOemOverrideComponentName(@NonNull Context context, + @NonNull IntentCreationResult.Builder intentResultBuilder) { ComponentName result = null; String oemComponentString = Resources.getSystem() @@ -121,86 +218,54 @@ public class IntentFactory { com.android.internal.R.string .config_oemCredentialManagerDialogComponent); if (!TextUtils.isEmpty(oemComponentString)) { - ComponentName oemComponentName = ComponentName.unflattenFromString( - oemComponentString); + ComponentName oemComponentName = null; + try { + oemComponentName = ComponentName.unflattenFromString( + oemComponentString); + } catch (Exception e) { + Slog.i(TAG, "Failed to parse OEM component name " + oemComponentString + ": " + e); + } if (oemComponentName != null) { try { + intentResultBuilder.setOemUiPackageName(oemComponentName.getPackageName()); ActivityInfo info = context.getPackageManager().getActivityInfo( oemComponentName, PackageManager.ComponentInfoFlags.of( PackageManager.MATCH_SYSTEM_ONLY)); if (info.enabled && info.exported) { + intentResultBuilder.setOemUiUsageStatus(IntentCreationResult + .OemUiUsageStatus.SUCCESS); Slog.i(TAG, "Found enabled oem CredMan UI component." + oemComponentString); result = oemComponentName; } else { + intentResultBuilder.setOemUiUsageStatus(IntentCreationResult + .OemUiUsageStatus.OEM_UI_CONFIG_SPECIFIED_FOUND_BUT_NOT_ENABLED); Slog.i(TAG, "Found enabled oem CredMan UI component but it was not " + "enabled."); } } catch (PackageManager.NameNotFoundException e) { + intentResultBuilder.setOemUiUsageStatus(IntentCreationResult.OemUiUsageStatus + .OEM_UI_CONFIG_SPECIFIED_BUT_NOT_FOUND); Slog.i(TAG, "Unable to find oem CredMan UI component: " + oemComponentString + "."); } } else { + intentResultBuilder.setOemUiUsageStatus(IntentCreationResult.OemUiUsageStatus + .OEM_UI_CONFIG_SPECIFIED_BUT_NOT_FOUND); Slog.i(TAG, "Invalid OEM ComponentName format."); } } else { + intentResultBuilder.setOemUiUsageStatus( + IntentCreationResult.OemUiUsageStatus.OEM_UI_CONFIG_NOT_SPECIFIED); Slog.i(TAG, "Invalid empty OEM component name."); } return result; } /** - * Generate a new launch intent to the Credential Selector UI. - * - * @param context the CredentialManager system service (only expected caller) - * context that may be used to query existence of the key UI - * application - * @param disabledProviderDataList the list of disabled provider data that when non-empty the - * UI should accordingly generate an entry suggesting the user - * to navigate to settings and enable them - * @param enabledProviderDataList the list of enabled provider that contain options for this - * request; the UI should render each option to the user for - * selection - * @param requestInfo the display information about the given app request - * @param resultReceiver used by the UI to send the UI selection result back - */ - @NonNull - public static Intent createCredentialSelectorIntent( - @NonNull Context context, - @NonNull RequestInfo requestInfo, - @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling. - @NonNull - ArrayList<ProviderData> enabledProviderDataList, - @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling. - @NonNull - ArrayList<DisabledProviderData> disabledProviderDataList, - @NonNull ResultReceiver resultReceiver) { - Intent intent = createCredentialSelectorIntent(context, requestInfo, - disabledProviderDataList, resultReceiver); - intent.putParcelableArrayListExtra( - ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, enabledProviderDataList); - return intent; - } - - /** - * Creates an Intent that cancels any UI matching the given request token id. - */ - @NonNull - public static Intent createCancelUiIntent(@NonNull Context context, - @NonNull IBinder requestToken, boolean shouldShowCancellationUi, - @NonNull String appPackageName) { - Intent intent = new Intent(); - setCredentialSelectorUiComponentName(context, intent); - intent.putExtra(CancelSelectionRequest.EXTRA_CANCEL_UI_REQUEST, - new CancelSelectionRequest(new RequestToken(requestToken), shouldShowCancellationUi, - appPackageName)); - return intent; - } - - /** * Convert an instance of a "locally-defined" ResultReceiver to an instance of {@link * android.os.ResultReceiver} itself, which the receiving process will be able to unmarshall. */ diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java index 13d5c7e74e4b..6f901d7ec7d2 100644 --- a/core/java/android/hardware/camera2/CaptureRequest.java +++ b/core/java/android/hardware/camera2/CaptureRequest.java @@ -2800,7 +2800,9 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * upright.</p> * <p>Camera devices may either encode this value into the JPEG EXIF header, or * rotate the image data to match this orientation. When the image data is rotated, - * the thumbnail data will also be rotated.</p> + * the thumbnail data will also be rotated. Additionally, in the case where the image data + * is rotated, {@link android.media.Image#getWidth } and {@link android.media.Image#getHeight } + * will not be updated to reflect the height and width of the rotated image.</p> * <p>Note that this orientation is relative to the orientation of the camera sensor, given * by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p> * <p>To translate from the device orientation given by the Android sensor APIs for camera diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java index 7145501c718d..69b1c34a1da2 100644 --- a/core/java/android/hardware/camera2/CaptureResult.java +++ b/core/java/android/hardware/camera2/CaptureResult.java @@ -3091,7 +3091,9 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * upright.</p> * <p>Camera devices may either encode this value into the JPEG EXIF header, or * rotate the image data to match this orientation. When the image data is rotated, - * the thumbnail data will also be rotated.</p> + * the thumbnail data will also be rotated. Additionally, in the case where the image data + * is rotated, {@link android.media.Image#getWidth } and {@link android.media.Image#getHeight } + * will not be updated to reflect the height and width of the rotated image.</p> * <p>Note that this orientation is relative to the orientation of the camera sensor, given * by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p> * <p>To translate from the device orientation given by the Android sensor APIs for camera diff --git a/core/java/android/hardware/camera2/params/SessionConfiguration.java b/core/java/android/hardware/camera2/params/SessionConfiguration.java index b0f354fac009..3b2913c81d49 100644 --- a/core/java/android/hardware/camera2/params/SessionConfiguration.java +++ b/core/java/android/hardware/camera2/params/SessionConfiguration.java @@ -133,7 +133,7 @@ public final class SessionConfiguration implements Parcelable { * {@link CameraDeviceSetup.isSessionConfigurationSupported} and {@link * CameraDeviceSetup.getSessionCharacteristics} to query a camera device's feature * combination support and session specific characteristics. For the SessionConfiguration - * object to be used to create a capture session, {@link #setCallback} must be called to + * object to be used to create a capture session, {@link #setStateCallback} must be called to * specify the state callback function, and any incomplete OutputConfigurations must be * completed via {@link OutputConfiguration#addSurface} or * {@link OutputConfiguration#setSurfacesForMultiResolutionOutput} as appropriate.</p> @@ -419,7 +419,7 @@ public final class SessionConfiguration implements Parcelable { * @param cb A state callback interface implementation. */ @FlaggedApi(Flags.FLAG_CAMERA_DEVICE_SETUP) - public void setCallback( + public void setStateCallback( @NonNull @CallbackExecutor Executor executor, @NonNull CameraCaptureSession.StateCallback cb) { mStateCallback = cb; diff --git a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java index b067095668b2..978a8f9200ba 100644 --- a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java +++ b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java @@ -1473,6 +1473,11 @@ public final class StreamConfigurationMap { * <li>ImageFormat.DEPTH_JPEG => HAL_DATASPACE_DYNAMIC_DEPTH * <li>ImageFormat.HEIC => HAL_DATASPACE_HEIF * <li>ImageFormat.JPEG_R => HAL_DATASPACE_JPEG_R + * <li>ImageFormat.YUV_420_888 => HAL_DATASPACE_JFIF + * <li>ImageFormat.RAW_SENSOR => HAL_DATASPACE_ARBITRARY + * <li>ImageFormat.RAW_OPAQUE => HAL_DATASPACE_ARBITRARY + * <li>ImageFormat.RAW10 => HAL_DATASPACE_ARBITRARY + * <li>ImageFormat.RAW12 => HAL_DATASPACE_ARBITRARY * <li>others => HAL_DATASPACE_UNKNOWN * </ul> * </p> @@ -1511,6 +1516,11 @@ public final class StreamConfigurationMap { return HAL_DATASPACE_JPEG_R; case ImageFormat.YUV_420_888: return HAL_DATASPACE_JFIF; + case ImageFormat.RAW_SENSOR: + case ImageFormat.RAW_PRIVATE: + case ImageFormat.RAW10: + case ImageFormat.RAW12: + return HAL_DATASPACE_ARBITRARY; default: return HAL_DATASPACE_UNKNOWN; } @@ -2005,6 +2015,12 @@ public final class StreamConfigurationMap { private static final int HAL_DATASPACE_RANGE_SHIFT = 27; private static final int HAL_DATASPACE_UNKNOWN = 0x0; + + /** + * @hide + */ + public static final int HAL_DATASPACE_ARBITRARY = 0x1; + /** @hide */ public static final int HAL_DATASPACE_V0_JFIF = (2 << HAL_DATASPACE_STANDARD_SHIFT) | diff --git a/core/java/android/hardware/devicestate/DeviceState.java b/core/java/android/hardware/devicestate/DeviceState.java index b214da227a2d..689e343bcbc6 100644 --- a/core/java/android/hardware/devicestate/DeviceState.java +++ b/core/java/android/hardware/devicestate/DeviceState.java @@ -173,7 +173,7 @@ public final class DeviceState { public static final int PROPERTY_FEATURE_DUAL_DISPLAY_INTERNAL_DEFAULT = 17; /** @hide */ - @IntDef(prefix = {"PROPERTY_"}, flag = true, value = { + @IntDef(prefix = {"PROPERTY_"}, flag = false, value = { PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED, PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN, PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_OPEN, @@ -197,7 +197,7 @@ public final class DeviceState { public @interface DeviceStateProperties {} /** @hide */ - @IntDef(prefix = {"PROPERTY_"}, flag = true, value = { + @IntDef(prefix = {"PROPERTY_"}, flag = false, value = { PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_CLOSED, PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_HALF_OPEN, PROPERTY_FOLDABLE_HARDWARE_CONFIGURATION_FOLD_IN_OPEN @@ -207,7 +207,7 @@ public final class DeviceState { public @interface PhysicalDeviceStateProperties {} /** @hide */ - @IntDef(prefix = {"PROPERTY_"}, flag = true, value = { + @IntDef(prefix = {"PROPERTY_"}, flag = false, value = { PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS, PROPERTY_POLICY_CANCEL_WHEN_REQUESTER_NOT_ON_TOP, PROPERTY_POLICY_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL, diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java index 387eebe0f376..ed4037c7d246 100644 --- a/core/java/android/os/Bundle.java +++ b/core/java/android/os/Bundle.java @@ -18,6 +18,7 @@ package android.os; import static java.util.Objects.requireNonNull; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; @@ -31,6 +32,8 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; import java.io.Serializable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.List; @@ -53,6 +56,53 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { @VisibleForTesting static final int FLAG_ALLOW_FDS = 1 << 10; + @VisibleForTesting + static final int FLAG_HAS_BINDERS_KNOWN = 1 << 11; + + @VisibleForTesting + static final int FLAG_HAS_BINDERS = 1 << 12; + + + /** + * Status when the Bundle can <b>assert</b> that the underlying Parcel DOES NOT contain + * Binder object(s). + * + * @hide + */ + public static final int STATUS_BINDERS_NOT_PRESENT = 0; + + /** + * Status when the Bundle can <b>assert</b> that there are Binder object(s) in the Parcel. + * + * @hide + */ + public static final int STATUS_BINDERS_PRESENT = 1; + + /** + * Status when the Bundle cannot be checked for Binders and there is no parcelled data + * available to check either. + * <p> This could happen when a Bundle is unparcelled or was never parcelled, and modified such + * that it is not possible to assert if the Bundle has any Binder objects in the current state. + * + * For e.g. calling {@link #putParcelable} or {@link #putBinder} could have added a Binder + * object to the Bundle but it is not possible to assert this fact unless the Bundle is written + * to a Parcel. + * </p> + * + * @hide + */ + public static final int STATUS_BINDERS_UNKNOWN = 2; + + /** @hide */ + @IntDef(flag = true, prefix = {"STATUS_BINDERS_"}, value = { + STATUS_BINDERS_PRESENT, + STATUS_BINDERS_UNKNOWN, + STATUS_BINDERS_NOT_PRESENT + }) + @Retention(RetentionPolicy.SOURCE) + public @interface HasBinderStatus { + } + /** An unmodifiable {@code Bundle} that is always {@link #isEmpty() empty}. */ public static final Bundle EMPTY; @@ -75,7 +125,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { */ public Bundle() { super(); - mFlags = FLAG_HAS_FDS_KNOWN | FLAG_ALLOW_FDS; + mFlags = FLAG_HAS_FDS_KNOWN | FLAG_HAS_BINDERS_KNOWN | FLAG_ALLOW_FDS; } /** @@ -111,7 +161,6 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { * * @param from The bundle to be copied. * @param deep Whether is a deep or shallow copy. - * * @hide */ Bundle(Bundle from, boolean deep) { @@ -143,7 +192,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { */ public Bundle(ClassLoader loader) { super(loader); - mFlags = FLAG_HAS_FDS_KNOWN | FLAG_ALLOW_FDS; + mFlags = FLAG_HAS_FDS_KNOWN | FLAG_HAS_BINDERS_KNOWN | FLAG_ALLOW_FDS; } /** @@ -154,7 +203,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { */ public Bundle(int capacity) { super(capacity); - mFlags = FLAG_HAS_FDS_KNOWN | FLAG_ALLOW_FDS; + mFlags = FLAG_HAS_FDS_KNOWN | FLAG_HAS_BINDERS_KNOWN | FLAG_ALLOW_FDS; } /** @@ -180,7 +229,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { */ public Bundle(PersistableBundle b) { super(b); - mFlags = FLAG_HAS_FDS_KNOWN | FLAG_ALLOW_FDS; + mFlags = FLAG_HAS_FDS_KNOWN | FLAG_HAS_BINDERS_KNOWN | FLAG_ALLOW_FDS; } /** @@ -292,6 +341,9 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { if ((mFlags & FLAG_HAS_FDS) != 0) { mFlags &= ~FLAG_HAS_FDS_KNOWN; } + if ((mFlags & FLAG_HAS_BINDERS) != 0) { + mFlags &= ~FLAG_HAS_BINDERS_KNOWN; + } } /** @@ -306,13 +358,20 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { bundle.mOwnsLazyValues = false; mMap.putAll(bundle.mMap); - // FD state is now known if and only if both bundles already knew + // FD and Binders state is now known if and only if both bundles already knew if ((bundle.mFlags & FLAG_HAS_FDS) != 0) { mFlags |= FLAG_HAS_FDS; } if ((bundle.mFlags & FLAG_HAS_FDS_KNOWN) == 0) { mFlags &= ~FLAG_HAS_FDS_KNOWN; } + + if ((bundle.mFlags & FLAG_HAS_BINDERS) != 0) { + mFlags |= FLAG_HAS_BINDERS; + } + if ((bundle.mFlags & FLAG_HAS_BINDERS_KNOWN) == 0) { + mFlags &= ~FLAG_HAS_BINDERS_KNOWN; + } } /** @@ -343,6 +402,33 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { return (mFlags & FLAG_HAS_FDS) != 0; } + /** + * Returns a status indicating whether the bundle contains any parcelled Binder objects. + * @hide + */ + public @HasBinderStatus int hasBinders() { + if ((mFlags & FLAG_HAS_BINDERS_KNOWN) != 0) { + if ((mFlags & FLAG_HAS_BINDERS) != 0) { + return STATUS_BINDERS_PRESENT; + } else { + return STATUS_BINDERS_NOT_PRESENT; + } + } + + final Parcel p = mParcelledData; + if (p == null) { + return STATUS_BINDERS_UNKNOWN; + } + if (p.hasBinders()) { + mFlags = mFlags | FLAG_HAS_BINDERS | FLAG_HAS_BINDERS_KNOWN; + return STATUS_BINDERS_PRESENT; + } else { + mFlags = mFlags & ~FLAG_HAS_BINDERS; + mFlags |= FLAG_HAS_BINDERS_KNOWN; + return STATUS_BINDERS_NOT_PRESENT; + } + } + /** {@hide} */ @Override public void putObject(@Nullable String key, @Nullable Object value) { @@ -464,6 +550,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { unparcel(); mMap.put(key, value); mFlags &= ~FLAG_HAS_FDS_KNOWN; + mFlags &= ~FLAG_HAS_BINDERS_KNOWN; } /** @@ -502,6 +589,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { unparcel(); mMap.put(key, value); mFlags &= ~FLAG_HAS_FDS_KNOWN; + mFlags &= ~FLAG_HAS_BINDERS_KNOWN; } /** @@ -517,6 +605,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { unparcel(); mMap.put(key, value); mFlags &= ~FLAG_HAS_FDS_KNOWN; + mFlags &= ~FLAG_HAS_BINDERS_KNOWN; } /** {@hide} */ @@ -525,6 +614,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { unparcel(); mMap.put(key, value); mFlags &= ~FLAG_HAS_FDS_KNOWN; + mFlags &= ~FLAG_HAS_BINDERS_KNOWN; } /** @@ -540,6 +630,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { unparcel(); mMap.put(key, value); mFlags &= ~FLAG_HAS_FDS_KNOWN; + mFlags &= ~FLAG_HAS_BINDERS_KNOWN; } /** @@ -680,6 +771,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { public void putBinder(@Nullable String key, @Nullable IBinder value) { unparcel(); mMap.put(key, value); + mFlags &= ~FLAG_HAS_BINDERS_KNOWN; } /** @@ -697,6 +789,7 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { public void putIBinder(@Nullable String key, @Nullable IBinder value) { unparcel(); mMap.put(key, value); + mFlags &= ~FLAG_HAS_BINDERS_KNOWN; } /** diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index ccfb6326d941..bcef8153c691 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -475,6 +475,10 @@ public final class Parcel { private static native boolean nativeHasFileDescriptors(long nativePtr); private static native boolean nativeHasFileDescriptorsInRange( long nativePtr, int offset, int length); + + private static native boolean nativeHasBinders(long nativePtr); + private static native boolean nativeHasBindersInRange( + long nativePtr, int offset, int length); @RavenwoodThrow private static native void nativeWriteInterfaceToken(long nativePtr, String interfaceName); @RavenwoodThrow @@ -970,6 +974,34 @@ public final class Parcel { } /** + * Report whether the parcel contains any marshalled IBinder objects. + * + * @throws UnsupportedOperationException if binder kernel driver was disabled or if method was + * invoked in case of Binder RPC protocol. + * @hide + */ + public boolean hasBinders() { + return nativeHasBinders(mNativePtr); + } + + /** + * Report whether the parcel contains any marshalled {@link IBinder} objects in the range + * defined by {@code offset} and {@code length}. + * + * @param offset The offset from which the range starts. Should be between 0 and + * {@link #dataSize()}. + * @param length The length of the range. Should be between 0 and {@link #dataSize()} - {@code + * offset}. + * @return whether there are binders in the range or not. + * @throws IllegalArgumentException if the parameters are out of the permitted ranges. + * + * @hide + */ + public boolean hasBinders(int offset, int length) { + return nativeHasBindersInRange(mNativePtr, offset, length); + } + + /** * Store or read an IBinder interface token in the parcel at the current * {@link #dataPosition}. This is used to validate that the marshalled * transaction is intended for the target interface. This is typically written diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index 7020a38ed08a..db06a6ba0ef5 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -48,6 +48,7 @@ import libcore.io.IoUtils; import java.io.FileDescriptor; import java.io.IOException; import java.util.Map; +import java.util.NoSuchElementException; import java.util.concurrent.TimeoutException; /** @@ -588,6 +589,8 @@ public class Process { **/ public static final int THREAD_GROUP_RESTRICTED = 7; + /** @hide */ + public static final int SIGNAL_DEFAULT = 0; public static final int SIGNAL_QUIT = 3; public static final int SIGNAL_KILL = 9; public static final int SIGNAL_USR1 = 10; @@ -1437,6 +1440,49 @@ public class Process { sendSignal(pid, SIGNAL_KILL); } + /** + * Check the tgid and tid pair to see if the tid still exists and belong to the tgid. + * + * TOCTOU warning: the status of the tid can change at the time this method returns. This should + * be used in very rare cases such as checking if a (tid, tgid) pair that is known to exist + * recently no longer exists now. As the possibility of the same tid to be reused under the same + * tgid during a short window is rare. And even if it happens the caller logic should be robust + * to handle it without error. + * + * @throws IllegalArgumentException if tgid or tid is not positive. + * @throws SecurityException if the caller doesn't have the permission, this method is expected + * to be used by system process with {@link #SYSTEM_UID} because it + * internally uses tkill(2). + * @throws NoSuchElementException if the Linux process with pid as the tid has exited or it + * doesn't belong to the tgid. + * @hide + */ + public static final void checkTid(int tgid, int tid) + throws IllegalArgumentException, SecurityException, NoSuchElementException { + sendTgSignalThrows(tgid, tid, SIGNAL_DEFAULT); + } + + /** + * Check if the pid still exists. + * + * TOCTOU warning: the status of the pid can change at the time this method returns. This should + * be used in very rare cases such as checking if a pid that belongs to an isolated process of a + * uid known to exist recently no longer exists now. As the possibility of the same pid to be + * reused again under the same uid during a short window is rare. And even if it happens the + * caller logic should be robust to handle it without error. + * + * @throws IllegalArgumentException if pid is not positive. + * @throws SecurityException if the caller doesn't have the permission, this method is expected + * to be used by system process with {@link #SYSTEM_UID} because it + * internally uses kill(2). + * @throws NoSuchElementException if the Linux process with the pid has exited. + * @hide + */ + public static final void checkPid(int pid) + throws IllegalArgumentException, SecurityException, NoSuchElementException { + sendSignalThrows(pid, SIGNAL_DEFAULT); + } + /** @hide */ public static final native int setUid(int uid); @@ -1451,6 +1497,12 @@ public class Process { */ public static final native void sendSignal(int pid, int signal); + private static native void sendSignalThrows(int pid, int signal) + throws IllegalArgumentException, SecurityException, NoSuchElementException; + + private static native void sendTgSignalThrows(int pid, int tgid, int signal) + throws IllegalArgumentException, SecurityException, NoSuchElementException; + /** * @hide * Private impl for avoiding a log message... DO NOT USE without doing diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java index bebb912bd069..edb3a641f107 100644 --- a/core/java/android/os/Trace.java +++ b/core/java/android/os/Trace.java @@ -125,15 +125,15 @@ public final class Trace { @UnsupportedAppUsage @CriticalNative @android.ravenwood.annotation.RavenwoodReplace - private static native long nativeGetEnabledTags(); + private static native boolean nativeIsTagEnabled(long tag); @android.ravenwood.annotation.RavenwoodReplace private static native void nativeSetAppTracingAllowed(boolean allowed); @android.ravenwood.annotation.RavenwoodReplace private static native void nativeSetTracingEnabled(boolean allowed); - private static long nativeGetEnabledTags$ravenwood() { + private static boolean nativeIsTagEnabled$ravenwood(long traceTag) { // Tracing currently completely disabled under Ravenwood - return 0; + return false; } private static void nativeSetAppTracingAllowed$ravenwood(boolean allowed) { @@ -181,8 +181,7 @@ public final class Trace { @UnsupportedAppUsage @SystemApi(client = MODULE_LIBRARIES) public static boolean isTagEnabled(long traceTag) { - long tags = nativeGetEnabledTags(); - return (tags & traceTag) != 0; + return nativeIsTagEnabled(traceTag); } /** diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 84619a0eee2e..f172c3e52415 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -3188,6 +3188,8 @@ public class UserManager { * @return whether the context user can add a private profile. * @hide */ + @TestApi + @FlaggedApi(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE) @RequiresPermission(anyOf = { Manifest.permission.MANAGE_USERS, Manifest.permission.CREATE_USERS}, diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index e26dc73f7172..25c2b0eb80d7 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -11090,21 +11090,12 @@ public final class Settings { "assist_long_press_home_enabled"; /** - * Whether press and hold on nav handle can trigger search. + * Whether all entrypoints can trigger search. Replaces individual settings. * * @hide */ - public static final String SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED = - "search_press_hold_nav_handle_enabled"; - - /** - * Whether long-pressing on the home button can trigger search. - * - * @hide - */ - public static final String SEARCH_LONG_PRESS_HOME_ENABLED = - "search_long_press_home_enabled"; - + public static final String SEARCH_ALL_ENTRYPOINTS_ENABLED = + "search_all_entrypoints_enabled"; /** * Whether or not the accessibility data streaming is enbled for the @@ -12395,6 +12386,13 @@ public final class Settings { */ public static final String HIDE_PRIVATESPACE_ENTRY_POINT = "hide_privatespace_entry_point"; + /** + * Whether or not secure windows should be disabled. This only works on debuggable builds. + * + * @hide + */ + public static final String DISABLE_SECURE_WINDOWS = "disable_secure_windows"; + /** @hide */ public static final int PRIVATE_SPACE_AUTO_LOCK_ON_DEVICE_LOCK = 0; /** @hide */ diff --git a/core/java/android/service/chooser/flags.aconfig b/core/java/android/service/chooser/flags.aconfig index d72441f1e4b7..00236dfa7876 100644 --- a/core/java/android/service/chooser/flags.aconfig +++ b/core/java/android/service/chooser/flags.aconfig @@ -27,14 +27,3 @@ flag { description: "Provides additional callbacks with information about user actions in ChooserResult" bug: "263474465" } - -flag { - name: "legacy_chooser_pinning_removal" - namespace: "intentresolver" - description: "Removing pinning functionality from the legacy chooser (used by partial screenshare)" - bug: "301068735" - metadata { - purpose: PURPOSE_BUGFIX - } -} - diff --git a/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl b/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl index 6dbff7185f6f..908ab5f69775 100644 --- a/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl +++ b/core/java/android/service/ondeviceintelligence/IOnDeviceIntelligenceService.aidl @@ -41,7 +41,9 @@ oneway interface IOnDeviceIntelligenceService { void getFeatureDetails(int callerUid, in Feature feature, in IFeatureDetailsCallback featureDetailsCallback); void getReadOnlyFileDescriptor(in String fileName, in AndroidFuture<ParcelFileDescriptor> future); void getReadOnlyFeatureFileDescriptorMap(in Feature feature, in RemoteCallback remoteCallback); - void requestFeatureDownload(int callerUid, in Feature feature, in ICancellationSignal cancellationSignal, in IDownloadCallback downloadCallback); + void requestFeatureDownload(int callerUid, in Feature feature, + in AndroidFuture<ICancellationSignal> cancellationSignal, + in IDownloadCallback downloadCallback); void registerRemoteServices(in IRemoteProcessingService remoteProcessingService); void notifyInferenceServiceConnected(); void notifyInferenceServiceDisconnected(); diff --git a/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl b/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl index 799c7545968e..4213a0996e4c 100644 --- a/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl +++ b/core/java/android/service/ondeviceintelligence/IOnDeviceSandboxedInferenceService.aidl @@ -24,6 +24,7 @@ import android.app.ondeviceintelligence.Feature; import android.os.ICancellationSignal; import android.os.PersistableBundle; import android.os.Bundle; +import com.android.internal.infra.AndroidFuture; import android.service.ondeviceintelligence.IRemoteStorageService; import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback; @@ -34,13 +35,16 @@ import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback; */ oneway interface IOnDeviceSandboxedInferenceService { void registerRemoteStorageService(in IRemoteStorageService storageService); - void requestTokenInfo(int callerUid, in Feature feature, in Bundle request, in ICancellationSignal cancellationSignal, + void requestTokenInfo(int callerUid, in Feature feature, in Bundle request, + in AndroidFuture<ICancellationSignal> cancellationSignal, in ITokenInfoCallback tokenInfoCallback); void processRequest(int callerUid, in Feature feature, in Bundle request, in int requestType, - in ICancellationSignal cancellationSignal, in IProcessingSignal processingSignal, + in AndroidFuture<ICancellationSignal> cancellationSignal, + in AndroidFuture<IProcessingSignal> processingSignal, in IResponseCallback callback); void processRequestStreaming(int callerUid, in Feature feature, in Bundle request, in int requestType, - in ICancellationSignal cancellationSignal, in IProcessingSignal processingSignal, + in AndroidFuture<ICancellationSignal> cancellationSignal, + in AndroidFuture<IProcessingSignal> processingSignal, in IStreamingResponseCallback callback); void updateProcessingState(in Bundle processingState, in IProcessingUpdateStatusCallback callback); diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java index 93213182d284..86320b801f6c 100644 --- a/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java +++ b/core/java/android/service/ondeviceintelligence/OnDeviceIntelligenceService.java @@ -148,14 +148,18 @@ public abstract class OnDeviceIntelligenceService extends Service { @Override public void requestFeatureDownload(int callerUid, Feature feature, - ICancellationSignal cancellationSignal, + AndroidFuture cancellationSignalFuture, IDownloadCallback downloadCallback) { Objects.requireNonNull(feature); Objects.requireNonNull(downloadCallback); - + ICancellationSignal transport = null; + if (cancellationSignalFuture != null) { + transport = CancellationSignal.createTransport(); + cancellationSignalFuture.complete(transport); + } OnDeviceIntelligenceService.this.onDownloadFeature(callerUid, feature, - CancellationSignal.fromTransport(cancellationSignal), + CancellationSignal.fromTransport(transport), wrapDownloadCallback(downloadCallback)); } diff --git a/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java index fc7a4c83f82c..96c45eef3731 100644 --- a/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java +++ b/core/java/android/service/ondeviceintelligence/OnDeviceSandboxedInferenceService.java @@ -122,46 +122,72 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { @Override public void requestTokenInfo(int callerUid, Feature feature, Bundle request, - ICancellationSignal cancellationSignal, + AndroidFuture cancellationSignalFuture, ITokenInfoCallback tokenInfoCallback) { Objects.requireNonNull(feature); Objects.requireNonNull(tokenInfoCallback); + ICancellationSignal transport = null; + if (cancellationSignalFuture != null) { + transport = CancellationSignal.createTransport(); + cancellationSignalFuture.complete(transport); + } OnDeviceSandboxedInferenceService.this.onTokenInfoRequest(callerUid, feature, request, - CancellationSignal.fromTransport(cancellationSignal), + CancellationSignal.fromTransport(transport), wrapTokenInfoCallback(tokenInfoCallback)); } @Override public void processRequestStreaming(int callerUid, Feature feature, Bundle request, - int requestType, ICancellationSignal cancellationSignal, - IProcessingSignal processingSignal, + int requestType, + AndroidFuture cancellationSignalFuture, + AndroidFuture processingSignalFuture, IStreamingResponseCallback callback) { Objects.requireNonNull(feature); Objects.requireNonNull(callback); + ICancellationSignal transport = null; + if (cancellationSignalFuture != null) { + transport = CancellationSignal.createTransport(); + cancellationSignalFuture.complete(transport); + } + IProcessingSignal processingSignalTransport = null; + if (processingSignalFuture != null) { + processingSignalTransport = ProcessingSignal.createTransport(); + processingSignalFuture.complete(processingSignalTransport); + } OnDeviceSandboxedInferenceService.this.onProcessRequestStreaming(callerUid, feature, request, requestType, - CancellationSignal.fromTransport(cancellationSignal), - ProcessingSignal.fromTransport(processingSignal), + CancellationSignal.fromTransport(transport), + ProcessingSignal.fromTransport(processingSignalTransport), wrapStreamingResponseCallback(callback)); } @Override public void processRequest(int callerUid, Feature feature, Bundle request, - int requestType, ICancellationSignal cancellationSignal, - IProcessingSignal processingSignal, + int requestType, + AndroidFuture cancellationSignalFuture, + AndroidFuture processingSignalFuture, IResponseCallback callback) { Objects.requireNonNull(feature); Objects.requireNonNull(callback); - + ICancellationSignal transport = null; + if (cancellationSignalFuture != null) { + transport = CancellationSignal.createTransport(); + cancellationSignalFuture.complete(transport); + } + IProcessingSignal processingSignalTransport = null; + if (processingSignalFuture != null) { + processingSignalTransport = ProcessingSignal.createTransport(); + processingSignalFuture.complete(processingSignalTransport); + } OnDeviceSandboxedInferenceService.this.onProcessRequest(callerUid, feature, request, requestType, - CancellationSignal.fromTransport(cancellationSignal), - ProcessingSignal.fromTransport(processingSignal), + CancellationSignal.fromTransport(transport), + ProcessingSignal.fromTransport(processingSignalTransport), wrapResponseCallback(callback)); } @@ -206,7 +232,8 @@ public abstract class OnDeviceSandboxedInferenceService extends Service { * Invoked when caller provides a request for a particular feature to be processed in a * streaming manner. The expectation from the implementation is that when processing the * request, - * it periodically populates the {@link StreamingProcessingCallback#onPartialResult} to continuously + * it periodically populates the {@link StreamingProcessingCallback#onPartialResult} to + * continuously * provide partial Bundle results for the caller to utilize. Optionally the implementation can * provide the complete response in the {@link StreamingProcessingCallback#onResult} upon * processing completion. diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig index aff1d4a4ee12..30b1a2ef5849 100644 --- a/core/java/android/text/flags/flags.aconfig +++ b/core/java/android/text/flags/flags.aconfig @@ -121,8 +121,22 @@ flag { } flag { + name: "handwriting_end_of_line_tap" + namespace: "text" + description: "Initiate handwriting when stylus taps at the end of a line in a focused non-empty TextView with the cursor at the end of that line" + bug: "323376217" +} + +flag { name: "handwriting_cursor_position" namespace: "text" description: "When handwriting is initiated in an unfocused TextView, cursor is placed at the end of the closest paragraph." bug: "323376217" } + +flag { + name: "handwriting_unsupported_message" + namespace: "text" + description: "Feature flag for showing error message when user tries stylus handwriting on a text field which doesn't support it" + bug: "297962571" +} diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java index 29c83509dbf2..192b2ec93ce0 100644 --- a/core/java/android/view/HandwritingInitiator.java +++ b/core/java/android/view/HandwritingInitiator.java @@ -34,7 +34,9 @@ import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.Editor; import android.widget.TextView; +import android.widget.Toast; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import java.lang.ref.WeakReference; @@ -223,7 +225,24 @@ public class HandwritingInitiator { View candidateView = findBestCandidateView(mState.mStylusDownX, mState.mStylusDownY, /* isHover */ false); if (candidateView != null && candidateView.isEnabled()) { - if (candidateView == getConnectedOrFocusedView()) { + if (shouldShowHandwritingUnavailableMessageForView(candidateView)) { + int messagesResId = (candidateView instanceof TextView tv + && tv.isAnyPasswordInputType()) + ? R.string.error_handwriting_unsupported_password + : R.string.error_handwriting_unsupported; + Toast.makeText(candidateView.getContext(), messagesResId, + Toast.LENGTH_SHORT).show(); + if (!candidateView.hasFocus()) { + requestFocusWithoutReveal(candidateView); + } + mImm.showSoftInput(candidateView, 0); + mState.mHandled = true; + mState.mShouldInitHandwriting = false; + motionEvent.setAction((motionEvent.getAction() + & MotionEvent.ACTION_POINTER_INDEX_MASK) + | MotionEvent.ACTION_CANCEL); + candidateView.getRootView().dispatchTouchEvent(motionEvent); + } else if (candidateView == getConnectedOrFocusedView()) { if (!mInitiateWithoutConnection && !candidateView.hasFocus()) { requestFocusWithoutReveal(candidateView); } @@ -484,6 +503,15 @@ public class HandwritingInitiator { return view.isStylusHandwritingAvailable(); } + private static boolean shouldShowHandwritingUnavailableMessageForView(@NonNull View view) { + return (view instanceof TextView) && !shouldTriggerStylusHandwritingForView(view); + } + + private static boolean shouldTriggerHandwritingOrShowUnavailableMessageForView( + @NonNull View view) { + return (view instanceof TextView) || shouldTriggerStylusHandwritingForView(view); + } + /** * Returns the pointer icon for the motion event, or null if it doesn't specify the icon. * This gives HandwritingInitiator a chance to show the stylus handwriting icon over a @@ -491,7 +519,7 @@ public class HandwritingInitiator { */ public PointerIcon onResolvePointerIcon(Context context, MotionEvent event) { final View hoverView = findHoverView(event); - if (hoverView == null) { + if (hoverView == null || !shouldTriggerStylusHandwritingForView(hoverView)) { return null; } @@ -594,7 +622,7 @@ public class HandwritingInitiator { /** * Given the location of the stylus event, return the best candidate view to initialize - * handwriting mode. + * handwriting mode or show the handwriting unavailable error message. * * @param x the x coordinates of the stylus event, in the coordinates of the window. * @param y the y coordinates of the stylus event, in the coordinates of the window. @@ -610,7 +638,8 @@ public class HandwritingInitiator { Rect handwritingArea = mTempRect; if (getViewHandwritingArea(connectedOrFocusedView, handwritingArea) && isInHandwritingArea(handwritingArea, x, y, connectedOrFocusedView, isHover) - && shouldTriggerStylusHandwritingForView(connectedOrFocusedView)) { + && shouldTriggerHandwritingOrShowUnavailableMessageForView( + connectedOrFocusedView)) { if (!isHover && mState != null) { mState.mStylusDownWithinEditorBounds = contains(handwritingArea, x, y, 0f, 0f, 0f, 0f); @@ -628,7 +657,7 @@ public class HandwritingInitiator { final View view = viewInfo.getView(); final Rect handwritingArea = viewInfo.getHandwritingArea(); if (!isInHandwritingArea(handwritingArea, x, y, view, isHover) - || !shouldTriggerStylusHandwritingForView(view)) { + || !shouldTriggerHandwritingOrShowUnavailableMessageForView(view)) { continue; } @@ -856,7 +885,7 @@ public class HandwritingInitiator { /** The helper method to check if the given view is still active for handwriting. */ private static boolean isViewActive(@Nullable View view) { return view != null && view.isAttachedToWindow() && view.isAggregatedVisible() - && view.shouldInitiateHandwriting(); + && view.shouldTrackHandwritingArea(); } private CursorAnchorInfo getCursorAnchorInfoForConnectionless(View view) { diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index e126836020b4..3a90841c5327 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -47,6 +47,19 @@ import java.util.List; * {@hide} */ interface IWindowSession { + + /** + * Bundle key to store the latest sync seq id for the relayout configuration. + * @see #relayout + */ + const String KEY_RELAYOUT_BUNDLE_SEQID = "seqid"; + /** + * Bundle key to store the latest ActivityWindowInfo associated with the relayout configuration. + * Will only be set if the relayout window is an activity window. + * @see #relayout + */ + const String KEY_RELAYOUT_BUNDLE_ACTIVITY_WINDOW_INFO = "activity_window_info"; + int addToDisplay(IWindow window, in WindowManager.LayoutParams attrs, in int viewVisibility, in int layerStackId, int requestedVisibleTypes, out InputChannel outInputChannel, out InsetsState insetsState, @@ -92,7 +105,7 @@ interface IWindowSession { * @param outSurfaceControl Object in which is placed the new display surface. * @param insetsState The current insets state in the system. * @param activeControls Objects which allow controlling {@link InsetsSource}s. - * @param bundle A temporary object to obtain the latest SyncSeqId. + * @param bundle A Bundle to contain the latest SyncSeqId and any extra relayout optional infos. * @return int Result flags, defined in {@link WindowManagerGlobal}. */ int relayout(IWindow window, in WindowManager.LayoutParams attrs, diff --git a/core/java/android/view/OWNERS b/core/java/android/view/OWNERS index a2f767d002f4..07d05a4ff1ea 100644 --- a/core/java/android/view/OWNERS +++ b/core/java/android/view/OWNERS @@ -75,12 +75,14 @@ per-file View.java = file:/graphics/java/android/graphics/OWNERS per-file View.java = file:/services/core/java/com/android/server/input/OWNERS per-file View.java = file:/services/core/java/com/android/server/wm/OWNERS per-file View.java = file:/core/java/android/view/inputmethod/OWNERS +per-file View.java = file:/core/java/android/text/OWNERS per-file ViewRootImpl.java = file:/services/accessibility/OWNERS per-file ViewRootImpl.java = file:/core/java/android/service/autofill/OWNERS per-file ViewRootImpl.java = file:/graphics/java/android/graphics/OWNERS per-file ViewRootImpl.java = file:/services/core/java/com/android/server/input/OWNERS per-file ViewRootImpl.java = file:/services/core/java/com/android/server/wm/OWNERS per-file ViewRootImpl.java = file:/core/java/android/view/inputmethod/OWNERS +per-file ViewRootImpl.java = file:/core/java/android/text/OWNERS per-file AccessibilityInteractionController.java = file:/services/accessibility/OWNERS per-file OnReceiveContentListener.java = file:/core/java/android/service/autofill/OWNERS per-file OnReceiveContentListener.java = file:/core/java/android/widget/OWNERS diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 0a75f4e6d731..3db45e09209d 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -8496,8 +8496,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * hierarchy * @param refocus when propagate is true, specifies whether to request the * root view place new focus + * @hide */ - void clearFocusInternal(View focused, boolean propagate, boolean refocus) { + public void clearFocusInternal(View focused, boolean propagate, boolean refocus) { if ((mPrivateFlags & PFLAG_FOCUSED) != 0) { mPrivateFlags &= ~PFLAG_FOCUSED; clearParentsWantFocus(); @@ -12695,7 +12696,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (getSystemGestureExclusionRects().isEmpty() && collectPreferKeepClearRects().isEmpty() && collectUnrestrictedPreferKeepClearRects().isEmpty() - && (info.mHandwritingArea == null || !shouldInitiateHandwriting())) { + && (info.mHandwritingArea == null || !shouldTrackHandwritingArea())) { if (info.mPositionUpdateListener != null) { mRenderNode.removePositionUpdateListener(info.mPositionUpdateListener); info.mPositionUpdateListener = null; @@ -13062,7 +13063,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, void updateHandwritingArea() { // If autoHandwritingArea is not enabled, do nothing. - if (!shouldInitiateHandwriting()) return; + if (!shouldTrackHandwritingArea()) return; final AttachInfo ai = mAttachInfo; if (ai != null) { ai.mViewRootImpl.getHandwritingInitiator().updateHandwritingAreasForView(this); @@ -13080,6 +13081,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Returns whether the handwriting initiator should track the handwriting area for this view, + * either to initiate handwriting mode, or to prepare handwriting delegation, or to show the + * handwriting unsupported message. + * @hide + */ + public boolean shouldTrackHandwritingArea() { + return shouldInitiateHandwriting(); + } + + /** * Sets a callback which should be called when a stylus {@link MotionEvent} occurs within this * view's bounds. The callback will be called from the UI thread. * diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index cae66720e49e..304e43eaf1c1 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -8933,7 +8933,8 @@ public final class ViewRootImpl implements ViewParent, mTempInsets, mTempControls, mRelayoutBundle); mRelayoutRequested = true; - final int maybeSyncSeqId = mRelayoutBundle.getInt("seqid"); + final int maybeSyncSeqId = mRelayoutBundle.getInt( + IWindowSession.KEY_RELAYOUT_BUNDLE_SEQID); if (maybeSyncSeqId > 0) { mSyncSeqId = maybeSyncSeqId; } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 0373539c44ea..fbb5116fb82f 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -9733,7 +9733,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return KEY_EVENT_HANDLED; } if (hasFocus()) { - clearFocus(); + clearFocusInternal(null, /* propagate */ true, /* refocus */ false); InputMethodManager imm = getInputMethodManager(); if (imm != null) { imm.hideSoftInputFromView(this, 0); @@ -13118,6 +13118,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return superResult; } + // At this point, the event is not a long press, otherwise it would be handled above. + if (Flags.handwritingEndOfLineTap() && action == MotionEvent.ACTION_UP + && shouldStartHandwritingForEndOfLineTap(event)) { + InputMethodManager imm = getInputMethodManager(); + if (imm != null) { + imm.startStylusHandwriting(this); + return true; + } + } + final boolean touchIsFinished = (action == MotionEvent.ACTION_UP) && (mEditor == null || !mEditor.mIgnoreActionUpEvent) && isFocused(); @@ -13167,6 +13177,46 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * If handwriting is supported, the TextView is already focused and not empty, and the cursor is + * at the end of a line, a stylus tap after the end of the line will trigger handwriting. + */ + private boolean shouldStartHandwritingForEndOfLineTap(MotionEvent actionUpEvent) { + if (!onCheckIsTextEditor() + || !isEnabled() + || !isAutoHandwritingEnabled() + || TextUtils.isEmpty(mText) + || didTouchFocusSelect() + || mLayout == null + || !actionUpEvent.isStylusPointer()) { + return false; + } + int cursorOffset = getSelectionStart(); + if (cursorOffset < 0 || getSelectionEnd() != cursorOffset) { + return false; + } + int cursorLine = mLayout.getLineForOffset(cursorOffset); + int cursorLineEnd = mLayout.getLineEnd(cursorLine); + if (cursorLine != mLayout.getLineCount() - 1) { + cursorLineEnd--; + } + if (cursorLineEnd != cursorOffset) { + return false; + } + // Check that the stylus down point is within the same line as the cursor. + if (getLineAtCoordinate(actionUpEvent.getY()) != cursorLine) { + return false; + } + // Check that the stylus down point is after the end of the line. + float localX = convertToLocalHorizontalCoordinate(actionUpEvent.getX()); + if (mLayout.getParagraphDirection(cursorLine) == Layout.DIR_RIGHT_TO_LEFT + ? localX >= mLayout.getLineLeft(cursorLine) + : localX <= mLayout.getLineRight(cursorLine)) { + return false; + } + return isStylusHandwritingAvailable(); + } + + /** * Returns true when need to show UIs, e.g. floating toolbar, etc, for finger based interaction. * * @return true if UIs need to show for finger interaciton. false if UIs are not necessary. @@ -13565,6 +13615,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** @hide */ @Override + public boolean shouldTrackHandwritingArea() { + // The handwriting initiator tracks all editable TextViews regardless of whether handwriting + // is supported, so that it can show an error message for unsupported editable TextViews. + return super.shouldTrackHandwritingArea() + || (Flags.handwritingUnsupportedMessage() && onCheckIsTextEditor()); + } + + /** @hide */ + @Override public boolean isStylusHandwritingAvailable() { if (mTextOperationUser == null) { return super.isStylusHandwritingAvailable(); diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java index 7e77f150b63b..43df4f962256 100644 --- a/core/java/android/window/TaskFragmentOperation.java +++ b/core/java/android/window/TaskFragmentOperation.java @@ -112,10 +112,13 @@ public final class TaskFragmentOperation implements Parcelable { /** * Creates a decor surface in the parent Task of the TaskFragment. The created decor surface * will be provided in {@link TaskFragmentTransaction#TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED} - * event callback. The decor surface can be used to draw the divider between TaskFragments or - * other decorations. + * event callback. If a decor surface already exists in the parent Task, the current + * TaskFragment will become the new owner of the decor surface and the decor surface will be + * moved above the TaskFragment. + * + * The decor surface can be used to draw the divider between TaskFragments or other decorations. */ - public static final int OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE = 14; + public static final int OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE = 14; /** * Removes the decor surface in the parent Task of the TaskFragment. @@ -162,7 +165,7 @@ public final class TaskFragmentOperation implements Parcelable { OP_TYPE_SET_ISOLATED_NAVIGATION, OP_TYPE_REORDER_TO_BOTTOM_OF_TASK, OP_TYPE_REORDER_TO_TOP_OF_TASK, - OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE, + OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE, OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE, OP_TYPE_SET_DIM_ON_TASK, OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH, diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java index 7f5331b936e9..4a3aba13fd54 100644 --- a/core/java/android/window/WindowTokenClient.java +++ b/core/java/android/window/WindowTokenClient.java @@ -165,7 +165,8 @@ public class WindowTokenClient extends Binder { Log.d(TAG, "Configuration not dispatch to IME because configuration is up" + " to date. Current config=" + context.getResources().getConfiguration() + ", reported config=" + currentConfig - + ", updated config=" + newConfig); + + ", updated config=" + newConfig + + ", updated display ID=" + newDisplayId); } // Update display first. In case callers want to obtain display information( // ex: DisplayMetrics) in #onConfigurationChanged callback. @@ -190,13 +191,18 @@ public class WindowTokenClient extends Binder { if (mShouldDumpConfigForIme) { if (!shouldReportConfigChange) { Log.d(TAG, "Only apply configuration update to Resources because " - + "shouldReportConfigChange is false.\n" + Debug.getCallers(5)); + + "shouldReportConfigChange is false. " + + "context=" + context + + ", config=" + context.getResources().getConfiguration() + + ", display ID=" + context.getDisplayId() + "\n" + + Debug.getCallers(5)); } else if (diff == 0) { Log.d(TAG, "Configuration not dispatch to IME because configuration has no " + " public difference with updated config. " + " Current config=" + context.getResources().getConfiguration() + ", reported config=" + currentConfig - + ", updated config=" + newConfig); + + ", updated config=" + newConfig + + ", display ID=" + context.getDisplayId()); } } } diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 14fb17c09031..65bf24179bea 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -38,6 +38,17 @@ flag { } flag { + name: "skip_sleeping_when_switching_display" + namespace: "windowing_frontend" + description: "Reduce unnecessary visibility or lifecycle changes when changing fold state" + bug: "303241079" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "introduce_smoother_dimmer" namespace: "windowing_frontend" description: "Refactor dim to fix flickers" diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java index 2e80b7e19516..c70febb3a7bf 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityShortcutChooserActivity.java @@ -20,7 +20,6 @@ import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_SHOR import static android.view.accessibility.AccessibilityManager.ShortcutType; import static com.android.internal.accessibility.common.ShortcutConstants.ShortcutMenuMode; -import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.createEnableDialogContentView; import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getInstalledTargets; import static com.android.internal.accessibility.dialog.AccessibilityTargetHelper.getTargets; import static com.android.internal.accessibility.util.AccessibilityUtils.isUserSetupCompleted; @@ -115,39 +114,22 @@ public class AccessibilityShortcutChooserActivity extends Activity { private void onTargetChecked(AdapterView<?> parent, View view, int position, long id) { final AccessibilityTarget target = mTargets.get(position); - if (Flags.cleanupAccessibilityWarningDialog()) { - if (target instanceof AccessibilityServiceTarget serviceTarget) { - if (sendRestrictedDialogIntentIfNeeded(target)) { - return; - } - final AccessibilityManager am = getSystemService(AccessibilityManager.class); - if (am.isAccessibilityServiceWarningRequired( - serviceTarget.getAccessibilityServiceInfo())) { - showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target, - position, mTargetAdapter); - return; - } + if (target instanceof AccessibilityServiceTarget serviceTarget) { + if (sendRestrictedDialogIntentIfNeeded(target)) { + return; } - if (target instanceof AccessibilityActivityTarget activityTarget) { - if (!activityTarget.isShortcutEnabled() - && sendRestrictedDialogIntentIfNeeded(activityTarget)) { - return; - } + final AccessibilityManager am = getSystemService(AccessibilityManager.class); + if (am.isAccessibilityServiceWarningRequired( + serviceTarget.getAccessibilityServiceInfo())) { + showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target, + position, mTargetAdapter); + return; } - } else { - if (!target.isShortcutEnabled()) { - if (target instanceof AccessibilityServiceTarget - || target instanceof AccessibilityActivityTarget) { - if (sendRestrictedDialogIntentIfNeeded(target)) { - return; - } - } - - if (target instanceof AccessibilityServiceTarget) { - showPermissionDialogIfNeeded(this, (AccessibilityServiceTarget) target, - position, mTargetAdapter); - return; - } + } + if (target instanceof AccessibilityActivityTarget activityTarget) { + if (!activityTarget.isShortcutEnabled() + && sendRestrictedDialogIntentIfNeeded(activityTarget)) { + return; } } @@ -178,37 +160,25 @@ public class AccessibilityShortcutChooserActivity extends Activity { return; } - if (Flags.cleanupAccessibilityWarningDialog()) { - mPermissionDialog = AccessibilityServiceWarning - .createAccessibilityServiceWarningDialog(context, - serviceTarget.getAccessibilityServiceInfo(), - v -> { - serviceTarget.onCheckedChanged(true); - targetAdapter.notifyDataSetChanged(); - mPermissionDialog.dismiss(); - }, v -> { - serviceTarget.onCheckedChanged(false); - mPermissionDialog.dismiss(); - }, - v -> { - mTargets.remove(position); - context.getPackageManager().getPackageInstaller().uninstall( - serviceTarget.getComponentName().getPackageName(), null); - targetAdapter.notifyDataSetChanged(); - mPermissionDialog.dismiss(); - }); - mPermissionDialog.setOnDismissListener(dialog -> mPermissionDialog = null); - } else { - mPermissionDialog = new AlertDialog.Builder(context) - .setView(createEnableDialogContentView(context, serviceTarget, - v -> { - mPermissionDialog.dismiss(); - targetAdapter.notifyDataSetChanged(); - }, - v -> mPermissionDialog.dismiss())) - .setOnDismissListener(dialog -> mPermissionDialog = null) - .create(); - } + mPermissionDialog = AccessibilityServiceWarning + .createAccessibilityServiceWarningDialog(context, + serviceTarget.getAccessibilityServiceInfo(), + v -> { + serviceTarget.onCheckedChanged(true); + targetAdapter.notifyDataSetChanged(); + mPermissionDialog.dismiss(); + }, v -> { + serviceTarget.onCheckedChanged(false); + mPermissionDialog.dismiss(); + }, + v -> { + mTargets.remove(position); + context.getPackageManager().getPackageInstaller().uninstall( + serviceTarget.getComponentName().getPackageName(), null); + targetAdapter.notifyDataSetChanged(); + mPermissionDialog.dismiss(); + }); + mPermissionDialog.setOnDismissListener(dialog -> mPermissionDialog = null); mPermissionDialog.show(); } diff --git a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java index 3d3db47faddb..0d82d63d8450 100644 --- a/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java +++ b/core/java/com/android/internal/accessibility/dialog/AccessibilityTargetHelper.java @@ -37,14 +37,8 @@ import android.content.Context; import android.os.Build; import android.os.UserHandle; import android.provider.Settings; -import android.text.BidiFormatter; -import android.view.LayoutInflater; -import android.view.View; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager.ShortcutType; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; import com.android.internal.R; import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType; @@ -52,7 +46,6 @@ import com.android.internal.accessibility.common.ShortcutConstants.Accessibility import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Locale; /** * Collection of utilities for accessibility target. @@ -298,50 +291,6 @@ public final class AccessibilityTargetHelper { } /** - * @deprecated Use {@link AccessibilityServiceWarning}. - */ - @Deprecated - static View createEnableDialogContentView(Context context, - AccessibilityServiceTarget target, View.OnClickListener allowListener, - View.OnClickListener denyListener) { - final LayoutInflater inflater = (LayoutInflater) context.getSystemService( - Context.LAYOUT_INFLATER_SERVICE); - - final View content = inflater.inflate( - R.layout.accessibility_enable_service_warning, /* root= */ null); - - final ImageView dialogIcon = content.findViewById( - R.id.accessibility_permissionDialog_icon); - dialogIcon.setImageDrawable(target.getIcon()); - - final TextView dialogTitle = content.findViewById( - R.id.accessibility_permissionDialog_title); - dialogTitle.setText(context.getString(R.string.accessibility_enable_service_title, - getServiceName(context, target.getLabel()))); - - final Button allowButton = content.findViewById( - R.id.accessibility_permission_enable_allow_button); - final Button denyButton = content.findViewById( - R.id.accessibility_permission_enable_deny_button); - allowButton.setOnClickListener((view) -> { - target.onCheckedChanged(/* isChecked= */ true); - allowListener.onClick(view); - }); - denyButton.setOnClickListener((view) -> { - target.onCheckedChanged(/* isChecked= */ false); - denyListener.onClick(view); - }); - - return content; - } - - // Gets the service name and bidi wrap it to protect from bidi side effects. - private static CharSequence getServiceName(Context context, CharSequence label) { - final Locale locale = context.getResources().getConfiguration().getLocales().get(0); - return BidiFormatter.getInstance(locale).unicodeWrap(label); - } - - /** * Determines if the{@link AccessibilityTarget} is allowed. */ public static boolean isAccessibilityTargetAllowed(Context context, String packageName, diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index 29669d312b1b..ab456a84d9ad 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -96,7 +96,6 @@ import android.provider.Downloads; import android.provider.OpenableColumns; import android.provider.Settings; import android.service.chooser.ChooserTarget; -import android.service.chooser.Flags; import android.text.TextUtils; import android.util.AttributeSet; import android.util.HashedStringCache; @@ -1801,54 +1800,6 @@ public class ChooserActivity extends ResolverActivity implements return getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true); } - private void showTargetDetails(TargetInfo targetInfo) { - if (targetInfo == null) return; - - ArrayList<DisplayResolveInfo> targetList; - ChooserTargetActionsDialogFragment fragment = new ChooserTargetActionsDialogFragment(); - Bundle bundle = new Bundle(); - - if (targetInfo instanceof SelectableTargetInfo) { - SelectableTargetInfo selectableTargetInfo = (SelectableTargetInfo) targetInfo; - if (selectableTargetInfo.getDisplayResolveInfo() == null - || selectableTargetInfo.getChooserTarget() == null) { - Log.e(TAG, "displayResolveInfo or chooserTarget in selectableTargetInfo are null"); - return; - } - targetList = new ArrayList<>(); - targetList.add(selectableTargetInfo.getDisplayResolveInfo()); - bundle.putString(ChooserTargetActionsDialogFragment.SHORTCUT_ID_KEY, - selectableTargetInfo.getChooserTarget().getIntentExtras().getString( - Intent.EXTRA_SHORTCUT_ID)); - bundle.putBoolean(ChooserTargetActionsDialogFragment.IS_SHORTCUT_PINNED_KEY, - selectableTargetInfo.isPinned()); - bundle.putParcelable(ChooserTargetActionsDialogFragment.INTENT_FILTER_KEY, - getTargetIntentFilter()); - if (selectableTargetInfo.getDisplayLabel() != null) { - bundle.putString(ChooserTargetActionsDialogFragment.SHORTCUT_TITLE_KEY, - selectableTargetInfo.getDisplayLabel().toString()); - } - } else if (targetInfo instanceof MultiDisplayResolveInfo) { - // For multiple targets, include info on all targets - MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo; - targetList = mti.getTargets(); - } else { - targetList = new ArrayList<DisplayResolveInfo>(); - targetList.add((DisplayResolveInfo) targetInfo); - } - // Adding userHandle from ResolveInfo allows the app icon in Dialog Box to be - // resolved correctly. - bundle.putParcelable(ChooserTargetActionsDialogFragment.USER_HANDLE_KEY, - getResolveInfoUserHandle( - targetInfo.getResolveInfo(), - mChooserMultiProfilePagerAdapter.getCurrentUserHandle())); - bundle.putParcelableArrayList(ChooserTargetActionsDialogFragment.TARGET_INFOS_KEY, - targetList); - fragment.setArguments(bundle); - - fragment.show(getFragmentManager(), TARGET_DETAILS_FRAGMENT_TAG); - } - private void modifyTargetIntent(Intent in) { if (isSendAction(in)) { in.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | @@ -2544,10 +2495,7 @@ public class ChooserActivity extends ResolverActivity implements @Override public boolean isComponentPinned(ComponentName name) { - if (Flags.legacyChooserPinningRemoval()) { - return false; - } - return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false); + return false; } @Override @@ -3135,34 +3083,10 @@ public class ChooserActivity extends ResolverActivity implements if (isClickable) { itemView.setOnClickListener(v -> startSelected(mListPosition, false/* always */, true/* filterd */)); - - itemView.setOnLongClickListener(v -> { - final TargetInfo ti = mChooserMultiProfilePagerAdapter.getActiveListAdapter() - .targetInfoForPosition(mListPosition, /* filtered */ true); - - // This should always be the case for ItemViewHolder, check for validity - if (ti instanceof DisplayResolveInfo && shouldShowTargetDetails(ti)) { - showTargetDetails((DisplayResolveInfo) ti); - } - return true; - }); } } } - private boolean shouldShowTargetDetails(TargetInfo ti) { - if (Flags.legacyChooserPinningRemoval()) { - // Never show the long press menu if we've removed pinning. - return false; - } - ComponentName nearbyShare = getNearbySharingComponent(); - // Suppress target details for nearby share to hide pin/unpin action - boolean isNearbyShare = nearbyShare != null && nearbyShare.equals( - ti.getResolvedComponentName()) && shouldNearbyShareBeFirstInRankedRow(); - return ti instanceof SelectableTargetInfo - || (ti instanceof DisplayResolveInfo && !isNearbyShare); - } - /** * Add a footer to the list, to support scrolling behavior below the navbar. */ @@ -3517,16 +3441,6 @@ public class ChooserActivity extends ResolverActivity implements } }); - // Show menu for both direct share and app share targets after long click. - v.setOnLongClickListener(v1 -> { - TargetInfo ti = mChooserListAdapter.targetInfoForPosition( - holder.getItemIndex(column), true); - if (shouldShowTargetDetails(ti)) { - showTargetDetails(ti); - } - return true; - }); - holder.addView(i, v); // Force Direct Share to be 2 lines and auto-wrap to second line via hoz scroll = diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java index 3662d69e1974..d2a533c78da6 100644 --- a/core/java/com/android/internal/jank/Cuj.java +++ b/core/java/com/android/internal/jank/Cuj.java @@ -124,10 +124,13 @@ public class Cuj { public static final int CUJ_BACK_PANEL_ARROW = 88; public static final int CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK = 89; public static final int CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH = 90; + public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE = 91; + public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR = 92; + public static final int CUJ_LAUNCHER_SAVE_APP_PAIR = 93; // When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE. @VisibleForTesting - static final int LAST_CUJ = CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH; + static final int LAST_CUJ = CUJ_LAUNCHER_SAVE_APP_PAIR; /** @hide */ @IntDef({ @@ -212,6 +215,9 @@ public class Cuj { CUJ_BACK_PANEL_ARROW, CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK, CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH, + CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE, + CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR, + CUJ_LAUNCHER_SAVE_APP_PAIR }) @Retention(RetentionPolicy.SOURCE) public @interface CujType { @@ -306,6 +312,9 @@ public class Cuj { CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_BACK_PANEL_ARROW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__BACK_PANEL_ARROW; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_CLOSE_ALL_APPS_BACK] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_CLOSE_ALL_APPS_BACK; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_SEARCH_QSB_WEB_SEARCH; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_SAVE_APP_PAIR] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_SAVE_APP_PAIR; } private Cuj() { @@ -484,6 +493,12 @@ public class Cuj { return "LAUNCHER_CLOSE_ALL_APPS_BACK"; case CUJ_LAUNCHER_SEARCH_QSB_WEB_SEARCH: return "LAUNCHER_SEARCH_QSB_WEB_SEARCH"; + case CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE: + return "LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE"; + case CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR: + return "LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR"; + case CUJ_LAUNCHER_SAVE_APP_PAIR: + return "LAUNCHER_SAVE_APP_PAIR"; } return "UNKNOWN"; } diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java index 0ec8b7461221..a288fb77749e 100644 --- a/core/java/com/android/internal/jank/InteractionJankMonitor.java +++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java @@ -165,6 +165,9 @@ public class InteractionJankMonitor { @Deprecated public static final int CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY = Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY; @Deprecated public static final int CUJ_PREDICTIVE_BACK_CROSS_TASK = Cuj.CUJ_PREDICTIVE_BACK_CROSS_TASK; @Deprecated public static final int CUJ_PREDICTIVE_BACK_HOME = Cuj.CUJ_PREDICTIVE_BACK_HOME; + @Deprecated public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE = Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_WORKSPACE; + @Deprecated public static final int CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR = Cuj.CUJ_LAUNCHER_LAUNCH_APP_PAIR_FROM_TASKBAR; + @Deprecated public static final int CUJ_LAUNCHER_SAVE_APP_PAIR = Cuj.CUJ_LAUNCHER_SAVE_APP_PAIR; private static class InstanceHolder { public static final InteractionJankMonitor INSTANCE = diff --git a/core/java/com/android/internal/net/ConnectivityBlobStore.java b/core/java/com/android/internal/net/ConnectivityBlobStore.java new file mode 100644 index 000000000000..51997cf13546 --- /dev/null +++ b/core/java/com/android/internal/net/ConnectivityBlobStore.java @@ -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.internal.net; + +import android.database.sqlite.SQLiteDatabase; + +import com.android.internal.annotations.VisibleForTesting; + +import java.io.File; + +/** + * Database for storing blobs with a key of name strings. + * @hide + */ +public class ConnectivityBlobStore { + private static final String TAG = ConnectivityBlobStore.class.getSimpleName(); + private static final String TABLENAME = "blob_table"; + private static final String ROOT_DIR = "/data/misc/connectivityblobdb/"; + + private static final String CREATE_TABLE = + "CREATE TABLE IF NOT EXISTS " + TABLENAME + " (" + + "owner INTEGER," + + "name BLOB," + + "blob BLOB," + + "UNIQUE(owner, name));"; + + private final SQLiteDatabase mDb; + + /** + * Construct a ConnectivityBlobStore object. + * + * @param dbName the filename of the database to create/access. + */ + public ConnectivityBlobStore(String dbName) { + this(new File(ROOT_DIR + dbName)); + } + + @VisibleForTesting + public ConnectivityBlobStore(File file) { + final SQLiteDatabase.OpenParams params = new SQLiteDatabase.OpenParams.Builder() + .addOpenFlags(SQLiteDatabase.CREATE_IF_NECESSARY) + .addOpenFlags(SQLiteDatabase.ENABLE_WRITE_AHEAD_LOGGING) + .build(); + mDb = SQLiteDatabase.openDatabase(file, params); + mDb.execSQL(CREATE_TABLE); + } +} diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index a22232ac945e..f5b1a47e917e 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -388,9 +388,9 @@ oneway interface IStatusBar */ void showMediaOutputSwitcher(String packageName); - /** Enters desktop mode. + /** Enters desktop mode from the current focused app. * * @param displayId the id of the current display. */ - void enterDesktop(int displayId); + void moveFocusedTaskToDesktop(int displayId); } diff --git a/core/jni/android_media_AudioSystem.cpp b/core/jni/android_media_AudioSystem.cpp index 52237989f059..d48cdc4645c6 100644 --- a/core/jni/android_media_AudioSystem.cpp +++ b/core/jni/android_media_AudioSystem.cpp @@ -161,6 +161,7 @@ static struct { jfieldID mMixType; jfieldID mCallbackFlags; jfieldID mToken; + jfieldID mVirtualDeviceId; } gAudioMixFields; static jclass gAudioFormatClass; @@ -2312,7 +2313,7 @@ static jint convertAudioMixFromNative(JNIEnv *env, jobject *jAudioMix, const Aud jstring deviceAddress = env->NewStringUTF(nAudioMix.mDeviceAddress.c_str()); *jAudioMix = env->NewObject(gAudioMixClass, gAudioMixCstor, jAudioMixingRule, jAudioFormat, nAudioMix.mRouteFlags, nAudioMix.mCbFlags, nAudioMix.mDeviceType, - deviceAddress, jBinderToken); + deviceAddress, jBinderToken, nAudioMix.mVirtualDeviceId); return AUDIO_JAVA_SUCCESS; } @@ -2347,6 +2348,7 @@ static jint convertAudioMixToNative(JNIEnv *env, AudioMix *nAudioMix, const jobj aiBinder(AIBinder_fromJavaBinder(env, jToken), &AIBinder_decStrong); nAudioMix->mToken = AIBinder_toPlatformBinder(aiBinder.get()); + nAudioMix->mVirtualDeviceId = env->GetIntField(jAudioMix, gAudioMixFields.mVirtualDeviceId); jint status = convertAudioMixingRuleToNative(env, jRule, &(nAudioMix->mCriteria)); env->DeleteLocalRef(jRule); @@ -3676,7 +3678,7 @@ int register_android_media_AudioSystem(JNIEnv *env) gAudioMixCstor = GetMethodIDOrDie(env, audioMixClass, "<init>", "(Landroid/media/audiopolicy/AudioMixingRule;Landroid/" - "media/AudioFormat;IIILjava/lang/String;Landroid/os/IBinder;)V"); + "media/AudioFormat;IIILjava/lang/String;Landroid/os/IBinder;I)V"); } gAudioMixFields.mRule = GetFieldIDOrDie(env, audioMixClass, "mRule", "Landroid/media/audiopolicy/AudioMixingRule;"); @@ -3689,6 +3691,7 @@ int register_android_media_AudioSystem(JNIEnv *env) gAudioMixFields.mMixType = GetFieldIDOrDie(env, audioMixClass, "mMixType", "I"); gAudioMixFields.mCallbackFlags = GetFieldIDOrDie(env, audioMixClass, "mCallbackFlags", "I"); gAudioMixFields.mToken = GetFieldIDOrDie(env, audioMixClass, "mToken", "Landroid/os/IBinder;"); + gAudioMixFields.mVirtualDeviceId = GetFieldIDOrDie(env, audioMixClass, "mVirtualDeviceId", "I"); jclass audioFormatClass = FindClassOrDie(env, "android/media/AudioFormat"); gAudioFormatClass = MakeGlobalRefOrDie(env, audioFormatClass); diff --git a/core/jni/android_os_Parcel.cpp b/core/jni/android_os_Parcel.cpp index 3539476b8ce8..584ebaa221fc 100644 --- a/core/jni/android_os_Parcel.cpp +++ b/core/jni/android_os_Parcel.cpp @@ -661,6 +661,35 @@ static void android_os_Parcel_appendFrom(JNIEnv* env, jclass clazz, jlong thisNa return; } +static jboolean android_os_Parcel_hasBinders(JNIEnv* env, jclass clazz, jlong nativePtr) { + Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); + if (parcel != NULL) { + bool result; + status_t err = parcel->hasBinders(&result); + if (err != NO_ERROR) { + signalExceptionForError(env, clazz, err); + return JNI_FALSE; + } + return result ? JNI_TRUE : JNI_FALSE; + } + return JNI_FALSE; +} + +static jboolean android_os_Parcel_hasBindersInRange(JNIEnv* env, jclass clazz, jlong nativePtr, + jint offset, jint length) { + Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); + if (parcel != NULL) { + bool result; + status_t err = parcel->hasBindersInRange(offset, length, &result); + if (err != NO_ERROR) { + signalExceptionForError(env, clazz, err); + return JNI_FALSE; + } + return result ? JNI_TRUE : JNI_FALSE; + } + return JNI_FALSE; +} + static jboolean android_os_Parcel_hasFileDescriptors(jlong nativePtr) { jboolean ret = JNI_FALSE; @@ -806,7 +835,7 @@ static jboolean android_os_Parcel_replaceCallingWorkSourceUid(jlong nativePtr, j } // ---------------------------------------------------------------------------- - +// clang-format off static const JNINativeMethod gParcelMethods[] = { // @CriticalNative {"nativeMarkSensitive", "(J)V", (void*)android_os_Parcel_markSensitive}, @@ -886,6 +915,9 @@ static const JNINativeMethod gParcelMethods[] = { // @CriticalNative {"nativeHasFileDescriptors", "(J)Z", (void*)android_os_Parcel_hasFileDescriptors}, {"nativeHasFileDescriptorsInRange", "(JII)Z", (void*)android_os_Parcel_hasFileDescriptorsInRange}, + + {"nativeHasBinders", "(J)Z", (void*)android_os_Parcel_hasBinders}, + {"nativeHasBindersInRange", "(JII)Z", (void*)android_os_Parcel_hasBindersInRange}, {"nativeWriteInterfaceToken", "(JLjava/lang/String;)V", (void*)android_os_Parcel_writeInterfaceToken}, {"nativeEnforceInterface", "(JLjava/lang/String;)V", (void*)android_os_Parcel_enforceInterface}, @@ -900,6 +932,7 @@ static const JNINativeMethod gParcelMethods[] = { // @CriticalNative {"nativeReplaceCallingWorkSourceUid", "(JI)Z", (void*)android_os_Parcel_replaceCallingWorkSourceUid}, }; +// clang-format on const char* const kParcelPathName = "android/os/Parcel"; diff --git a/core/jni/android_os_Trace.cpp b/core/jni/android_os_Trace.cpp index b579daf505e7..4387a4c63673 100644 --- a/core/jni/android_os_Trace.cpp +++ b/core/jni/android_os_Trace.cpp @@ -124,8 +124,8 @@ static void android_os_Trace_nativeInstantForTrack(JNIEnv* env, jclass, }); } -static jlong android_os_Trace_nativeGetEnabledTags(JNIEnv* env) { - return tracing_perfetto::getEnabledCategories(); +static jboolean android_os_Trace_nativeIsTagEnabled(jlong tag) { + return tracing_perfetto::isTagEnabled(tag); } static void android_os_Trace_nativeRegisterWithPerfetto(JNIEnv* env) { @@ -157,7 +157,7 @@ static const JNINativeMethod gTraceMethods[] = { {"nativeRegisterWithPerfetto", "()V", (void*)android_os_Trace_nativeRegisterWithPerfetto}, // ----------- @CriticalNative ---------------- - {"nativeGetEnabledTags", "()J", (void*)android_os_Trace_nativeGetEnabledTags}, + {"nativeIsTagEnabled", "(J)Z", (void*)android_os_Trace_nativeIsTagEnabled}, }; int register_android_os_Trace(JNIEnv* env) { diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp index d2e58bb62c46..982189e30beb 100644 --- a/core/jni/android_util_Process.cpp +++ b/core/jni/android_util_Process.cpp @@ -1137,6 +1137,41 @@ void android_os_Process_sendSignalQuiet(JNIEnv* env, jobject clazz, jint pid, ji } } +void android_os_Process_sendSignalThrows(JNIEnv* env, jobject clazz, jint pid, jint sig) { + if (pid <= 0) { + jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", "Invalid argument: pid(%d)", + pid); + return; + } + int ret = kill(pid, sig); + if (ret < 0) { + if (errno == ESRCH) { + jniThrowExceptionFmt(env, "java/util/NoSuchElementException", + "Process with pid %d not found", pid); + } else { + signalExceptionForError(env, errno, pid); + } + } +} + +void android_os_Process_sendTgSignalThrows(JNIEnv* env, jobject clazz, jint tgid, jint tid, + jint sig) { + if (tgid <= 0 || tid <= 0) { + jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", + "Invalid argument: tgid(%d), tid(%d)", tid, tgid); + return; + } + int ret = tgkill(tgid, tid, sig); + if (ret < 0) { + if (errno == ESRCH) { + jniThrowExceptionFmt(env, "java/util/NoSuchElementException", + "Process with tid %d and tgid %d not found", tid, tgid); + } else { + signalExceptionForError(env, errno, tid); + } + } +} + static jlong android_os_Process_getElapsedCpuTime(JNIEnv* env, jobject clazz) { struct timespec ts; @@ -1357,6 +1392,8 @@ static const JNINativeMethod methods[] = { {"setGid", "(I)I", (void*)android_os_Process_setGid}, {"sendSignal", "(II)V", (void*)android_os_Process_sendSignal}, {"sendSignalQuiet", "(II)V", (void*)android_os_Process_sendSignalQuiet}, + {"sendSignalThrows", "(II)V", (void*)android_os_Process_sendSignalThrows}, + {"sendTgSignalThrows", "(III)V", (void*)android_os_Process_sendTgSignalThrows}, {"setProcessFrozen", "(IIZ)V", (void*)android_os_Process_setProcessFrozen}, {"getFreeMemory", "()J", (void*)android_os_Process_getFreeMemory}, {"getTotalMemory", "()J", (void*)android_os_Process_getTotalMemory}, diff --git a/core/proto/android/providers/settings/secure.proto b/core/proto/android/providers/settings/secure.proto index 763d9ce1a053..6b0c2d28b776 100644 --- a/core/proto/android/providers/settings/secure.proto +++ b/core/proto/android/providers/settings/secure.proto @@ -143,9 +143,11 @@ message SecureSettingsProto { optional SettingProto gesture_setup_complete = 9 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto touch_gesture_enabled = 10 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto long_press_home_enabled = 11 [ (android.privacy).dest = DEST_AUTOMATIC ]; - optional SettingProto search_press_hold_nav_handle_enabled = 12 [ (android.privacy).dest = DEST_AUTOMATIC ]; - optional SettingProto search_long_press_home_enabled = 13 [ (android.privacy).dest = DEST_AUTOMATIC ]; + // Deprecated - use search_all_entrypoints_enabled instead + optional SettingProto search_press_hold_nav_handle_enabled = 12 [ (android.privacy).dest = DEST_AUTOMATIC, deprecated = true ]; + optional SettingProto search_long_press_home_enabled = 13 [ (android.privacy).dest = DEST_AUTOMATIC, deprecated = true ]; optional SettingProto visual_query_accessibility_detection_enabled = 14 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto search_all_entrypoints_enabled = 15 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Assist assist = 7; diff --git a/core/res/res/drawable/activity_embedding_divider_handle.xml b/core/res/res/drawable/activity_embedding_divider_handle.xml new file mode 100644 index 000000000000..d9f363cb33a7 --- /dev/null +++ b/core/res/res/drawable/activity_embedding_divider_handle.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_pressed="true" + android:drawable="@drawable/activity_embedding_divider_handle_pressed" /> + <item android:drawable="@drawable/activity_embedding_divider_handle_default" /> +</selector>
\ No newline at end of file diff --git a/core/res/res/drawable/activity_embedding_divider_handle_default.xml b/core/res/res/drawable/activity_embedding_divider_handle_default.xml new file mode 100644 index 000000000000..565f67169ab5 --- /dev/null +++ b/core/res/res/drawable/activity_embedding_divider_handle_default.xml @@ -0,0 +1,23 @@ +<!-- + 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. + --> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners android:radius="@dimen/activity_embedding_divider_handle_radius" /> + <size + android:width="@dimen/activity_embedding_divider_handle_width" + android:height="@dimen/activity_embedding_divider_handle_height" /> + <solid android:color="@color/activity_embedding_divider_color" /> +</shape>
\ No newline at end of file diff --git a/core/res/res/drawable/activity_embedding_divider_handle_pressed.xml b/core/res/res/drawable/activity_embedding_divider_handle_pressed.xml new file mode 100644 index 000000000000..e5cca2397806 --- /dev/null +++ b/core/res/res/drawable/activity_embedding_divider_handle_pressed.xml @@ -0,0 +1,23 @@ +<!-- + 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. + --> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners android:radius="@dimen/activity_embedding_divider_handle_radius_pressed" /> + <size + android:width="@dimen/activity_embedding_divider_handle_width_pressed" + android:height="@dimen/activity_embedding_divider_handle_height_pressed" /> + <solid android:color="@color/activity_embedding_divider_color_pressed" /> +</shape>
\ No newline at end of file diff --git a/core/res/res/drawable/autofill_dataset_picker_background.xml b/core/res/res/drawable/autofill_dataset_picker_background.xml index d57497037616..6c4ef11f3879 100644 --- a/core/res/res/drawable/autofill_dataset_picker_background.xml +++ b/core/res/res/drawable/autofill_dataset_picker_background.xml @@ -16,7 +16,7 @@ <inset xmlns:android="http://schemas.android.com/apk/res/android"> <shape android:shape="rectangle"> - <corners android:radius="@dimen/config_bottomDialogCornerRadius" /> + <corners android:radius="@dimen/config_buttonCornerRadius" /> <solid android:color="?attr/colorBackground" /> </shape> </inset> diff --git a/core/res/res/layout/transient_notification_with_icon.xml b/core/res/res/layout/transient_notification_with_icon.xml index 0dfb3adc8364..04518b2a75a2 100644 --- a/core/res/res/layout/transient_notification_with_icon.xml +++ b/core/res/res/layout/transient_notification_with_icon.xml @@ -22,7 +22,7 @@ android:orientation="horizontal" android:gravity="center_vertical" android:maxWidth="@dimen/toast_width" - android:background="?android:attr/colorBackground" + android:background="@android:drawable/toast_frame" android:elevation="@dimen/toast_elevation" android:layout_marginEnd="16dp" android:layout_marginStart="16dp" @@ -31,8 +31,11 @@ <ImageView android:id="@android:id/icon" - android:layout_width="wrap_content" - android:layout_height="wrap_content" /> + android:layout_width="24dp" + android:layout_height="24dp" + android:layout_marginTop="10dp" + android:layout_marginBottom="10dp" + android:layout_marginEnd="10dp" /> <TextView android:id="@android:id/message" diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml index 417c6df1e30d..e6719195565e 100644 --- a/core/res/res/values/colors.xml +++ b/core/res/res/values/colors.xml @@ -593,6 +593,10 @@ <color name="accessibility_magnification_thumbnail_container_background_color">#99000000</color> <color name="accessibility_magnification_thumbnail_container_stroke_color">#FFFFFF</color> + <!-- Activity Embedding divider --> + <color name="activity_embedding_divider_color">#8e918f</color> + <color name="activity_embedding_divider_color_pressed">#e3e3e3</color> + <!-- Lily Language Picker language item view colors --> <color name="language_picker_item_text_color">#202124</color> <color name="language_picker_item_text_color_secondary">#5F6368</color> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index efba7099d678..89ac81ebce56 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -6419,10 +6419,8 @@ <!-- Default value for Settings.ASSIST_TOUCH_GESTURE_ENABLED --> <bool name="config_assistTouchGestureEnabledDefault">true</bool> - <!-- Default value for Settings.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED --> - <bool name="config_searchPressHoldNavHandleEnabledDefault">true</bool> - <!-- Default value for Settings.ASSIST_LONG_PRESS_HOME_ENABLED for search overlay --> - <bool name="config_searchLongPressHomeEnabledDefault">true</bool> + <!-- Default value for Settings.SEARCH_ALL_ENTRYPOINTS_ENABLED --> + <bool name="config_searchAllEntrypointsEnabledDefault">true</bool> <!-- The maximum byte size of the information contained in the bundle of HotwordDetectedResult. --> diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 291a5936330a..4aa741de80a5 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -1028,6 +1028,16 @@ <dimen name="popup_enter_animation_from_y_delta">20dp</dimen> <dimen name="popup_exit_animation_to_y_delta">-10dp</dimen> + <!-- Dimensions for the activity embedding divider. --> + <dimen name="activity_embedding_divider_handle_width">4dp</dimen> + <dimen name="activity_embedding_divider_handle_height">48dp</dimen> + <dimen name="activity_embedding_divider_handle_radius">2dp</dimen> + <dimen name="activity_embedding_divider_handle_width_pressed">12dp</dimen> + <dimen name="activity_embedding_divider_handle_height_pressed">53dp</dimen> + <dimen name="activity_embedding_divider_handle_radius_pressed">6dp</dimen> + <dimen name="activity_embedding_divider_touch_target_width">24dp</dimen> + <dimen name="activity_embedding_divider_touch_target_height">64dp</dimen> + <!-- Default handwriting bounds offsets for editors. --> <dimen name="handwriting_bounds_offset_left">10dp</dimen> <dimen name="handwriting_bounds_offset_top">40dp</dimen> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 5639a583f296..a3dba48bbb7d 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -3249,6 +3249,12 @@ <!-- Title for EditText context menu [CHAR LIMIT=20] --> <string name="editTextMenuTitle">Text actions</string> + <!-- Error shown when a user uses a stylus to try handwriting on a text field which doesn't support stylus handwriting. [CHAR LIMIT=TOAST] --> + <string name="error_handwriting_unsupported">Handwriting is not supported in this field</string> + + <!-- Error shown when a user uses a stylus to try handwriting on a password text field which doesn't support stylus handwriting. [CHAR LIMIT=TOAST] --> + <string name="error_handwriting_unsupported_password">Handwriting is not supported in password fields</string> + <!-- Content description of the back button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> <string name="input_method_nav_back_button_desc">Back</string> <!-- Content description of the switch input method button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] --> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 668a88c4370a..2e029b23f6af 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3122,6 +3122,8 @@ <!-- TextView --> <java-symbol type="bool" name="config_textShareSupported" /> <java-symbol type="string" name="failed_to_copy_to_clipboard" /> + <java-symbol type="string" name="error_handwriting_unsupported" /> + <java-symbol type="string" name="error_handwriting_unsupported_password" /> <java-symbol type="id" name="notification_material_reply_container" /> <java-symbol type="id" name="notification_material_reply_text_1" /> @@ -5017,8 +5019,7 @@ <java-symbol type="bool" name="config_assistLongPressHomeEnabledDefault" /> <java-symbol type="bool" name="config_assistTouchGestureEnabledDefault" /> - <java-symbol type="bool" name="config_searchPressHoldNavHandleEnabledDefault" /> - <java-symbol type="bool" name="config_searchLongPressHomeEnabledDefault" /> + <java-symbol type="bool" name="config_searchAllEntrypointsEnabledDefault" /> <java-symbol type="integer" name="config_hotwordDetectedResultMaxBundleSize" /> @@ -5336,6 +5337,11 @@ <java-symbol type="raw" name="default_ringtone_vibration_effect" /> + <!-- For activity embedding divider --> + <java-symbol type="drawable" name="activity_embedding_divider_handle" /> + <java-symbol type="dimen" name="activity_embedding_divider_touch_target_width" /> + <java-symbol type="dimen" name="activity_embedding_divider_touch_target_height" /> + <!-- Whether we order unlocking and waking --> <java-symbol type="bool" name="config_orderUnlockAndWake" /> diff --git a/core/tests/coretests/src/android/os/BundleTest.java b/core/tests/coretests/src/android/os/BundleTest.java index 93c2e0e40593..40e79ad8ada3 100644 --- a/core/tests/coretests/src/android/os/BundleTest.java +++ b/core/tests/coretests/src/android/os/BundleTest.java @@ -24,6 +24,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import android.platform.test.annotations.DisabledOnRavenwood; import android.platform.test.annotations.IgnoreUnderRavenwood; import android.platform.test.annotations.Presubmit; import android.platform.test.ravenwood.RavenwoodRule; @@ -445,6 +446,42 @@ public class BundleTest { assertThat(bundle.size()).isEqualTo(0); } + @Test + @DisabledOnRavenwood(blockedBy = Parcel.class) + public void parcelledBundleWithBinder_shouldReturnHasBindersTrue() throws Exception { + Bundle bundle = new Bundle(); + bundle.putParcelable("test", new CustomParcelable(13, "Tiramisu")); + bundle.putBinder("test_binder", + new IBinderWorkSourceNestedService.Stub() { + + public int[] nestedCallWithWorkSourceToSet(int uidToBlame) { + return new int[0]; + } + + public int[] nestedCall() { + return new int[0]; + } + }); + Bundle bundle2 = new Bundle(getParcelledBundle(bundle)); + assertEquals(bundle2.hasBinders(), Bundle.STATUS_BINDERS_PRESENT); + + bundle2.putParcelable("test2", new CustomParcelable(13, "Tiramisu")); + assertEquals(bundle2.hasBinders(), Bundle.STATUS_BINDERS_UNKNOWN); + } + + @Test + @DisabledOnRavenwood(blockedBy = Parcel.class) + public void parcelledBundleWithoutBinder_shouldReturnHasBindersFalse() throws Exception { + Bundle bundle = new Bundle(); + bundle.putParcelable("test", new CustomParcelable(13, "Tiramisu")); + Bundle bundle2 = new Bundle(getParcelledBundle(bundle)); + //Should fail to load with framework classloader. + assertEquals(bundle2.hasBinders(), Bundle.STATUS_BINDERS_NOT_PRESENT); + + bundle2.putParcelable("test2", new CustomParcelable(13, "Tiramisu")); + assertEquals(bundle2.hasBinders(), Bundle.STATUS_BINDERS_UNKNOWN); + } + private Bundle getMalformedBundle() { Parcel p = Parcel.obtain(); p.writeInt(BaseBundle.BUNDLE_MAGIC); @@ -520,6 +557,7 @@ public class BundleTest { public CustomParcelable createFromParcel(Parcel in) { return new CustomParcelable(in); } + @Override public CustomParcelable[] newArray(int size) { return new CustomParcelable[size]; diff --git a/core/tests/coretests/src/android/os/ParcelTest.java b/core/tests/coretests/src/android/os/ParcelTest.java index 26f6d696768a..442394e3428a 100644 --- a/core/tests/coretests/src/android/os/ParcelTest.java +++ b/core/tests/coretests/src/android/os/ParcelTest.java @@ -347,4 +347,30 @@ public class ParcelTest { p.recycle(); Binder.setIsDirectlyHandlingTransactionOverride(false); } + + @Test + @IgnoreUnderRavenwood(blockedBy = Parcel.class) + public void testHasBinders_AfterWritingBinderToParcel() { + Binder binder = new Binder(); + Parcel pA = Parcel.obtain(); + int iA = pA.dataPosition(); + pA.writeInt(13); + assertFalse(pA.hasBinders()); + pA.writeStrongBinder(binder); + assertTrue(pA.hasBinders()); + } + + + @Test + @IgnoreUnderRavenwood(blockedBy = Parcel.class) + public void testHasBindersInRange_AfterWritingBinderToParcel() { + Binder binder = new Binder(); + Parcel pA = Parcel.obtain(); + pA.writeInt(13); + + int binderStartPos = pA.dataPosition(); + pA.writeStrongBinder(binder); + int binderEndPos = pA.dataPosition(); + assertTrue(pA.hasBinders(binderStartPos, binderEndPos - binderStartPos)); + } } diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java index a5c962412024..6c00fd80c5e1 100644 --- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java +++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java @@ -55,6 +55,7 @@ import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.EditText; +import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; @@ -72,6 +73,7 @@ import org.mockito.ArgumentCaptor; */ @Presubmit @SmallTest +@UiThreadTest @RunWith(AndroidJUnit4.class) public class HandwritingInitiatorTest { private static final long TIMEOUT = ViewConfiguration.getLongPressTimeout(); diff --git a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java index 60a436e6b2c2..745390d1648e 100644 --- a/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/accessibility/AccessibilityShortcutChooserActivityTest.java @@ -25,7 +25,6 @@ import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.RootMatchers.isDialog; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; import static androidx.test.espresso.matcher.ViewMatchers.withClassName; -import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static com.google.common.truth.Truth.assertThat; @@ -54,7 +53,6 @@ import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.os.Bundle; import android.os.Handler; -import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; @@ -176,21 +174,6 @@ public class AccessibilityShortcutChooserActivityTest { } @Test - @RequiresFlagsDisabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) - public void selectTestService_oldPermissionDialog_deny_dialogIsHidden() { - launchActivity(); - openShortcutsList(); - - mDevice.findObject(By.text(TEST_LABEL)).clickAndWait(Until.newWindow(), UI_TIMEOUT_MS); - onView(withText(DENY_LABEL)).perform(scrollTo(), click()); - InstrumentationRegistry.getInstrumentation().waitForIdleSync(); - - onView(withId(R.id.accessibility_permissionDialog_title)).inRoot(isDialog()).check( - doesNotExist()); - } - - @Test - @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void selectTestService_permissionDialog_allow_rowChecked() { launchActivity(); openShortcutsList(); @@ -202,7 +185,6 @@ public class AccessibilityShortcutChooserActivityTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void selectTestService_permissionDialog_deny_rowNotChecked() { launchActivity(); openShortcutsList(); @@ -214,7 +196,6 @@ public class AccessibilityShortcutChooserActivityTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void selectTestService_permissionDialog_uninstall_callsUninstaller_rowRemoved() { launchActivity(); openShortcutsList(); @@ -228,7 +209,6 @@ public class AccessibilityShortcutChooserActivityTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void selectTestService_permissionDialog_notShownWhenNotRequired() throws Exception { when(mAccessibilityManagerService.isAccessibilityServiceWarningRequired(any())) .thenReturn(false); @@ -243,7 +223,6 @@ public class AccessibilityShortcutChooserActivityTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void selectTestService_notPermittedByAdmin_blockedEvenIfNoWarningRequired() throws Exception { when(mAccessibilityManagerService.isAccessibilityServiceWarningRequired(any())) @@ -380,11 +359,9 @@ public class AccessibilityShortcutChooserActivityTest { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (Flags.cleanupAccessibilityWarningDialog()) { - // Setting the Theme is necessary here for the dialog to use the proper style - // resources as designated in its layout XML. - setTheme(R.style.Theme_DeviceDefault_DayNight); - } + // Setting the Theme is necessary here for the dialog to use the proper style + // resources as designated in its layout XML. + setTheme(R.style.Theme_DeviceDefault_DayNight); } @Override diff --git a/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java b/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java index 24aab6192c50..362eeeacfc1e 100644 --- a/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java +++ b/core/tests/coretests/src/com/android/internal/accessibility/dialog/AccessibilityServiceWarningTest.java @@ -25,7 +25,6 @@ import android.accessibilityservice.AccessibilityServiceInfo; import android.app.AlertDialog; import android.content.Context; import android.os.RemoteException; -import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; @@ -57,8 +56,6 @@ import java.util.concurrent.atomic.AtomicBoolean; */ @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper -@RequiresFlagsEnabled( - android.view.accessibility.Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public class AccessibilityServiceWarningTest { private static final String A11Y_SERVICE_PACKAGE_LABEL = "TestA11yService"; private static final String A11Y_SERVICE_SUMMARY = "TestA11yService summary"; diff --git a/core/tests/coretests/src/com/android/internal/net/ConnectivityBlobStoreTest.java b/core/tests/coretests/src/com/android/internal/net/ConnectivityBlobStoreTest.java new file mode 100644 index 000000000000..e361ce3fb490 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/net/ConnectivityBlobStoreTest.java @@ -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. + */ + +package com.android.internal.net; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.content.Context; + +import androidx.test.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.File; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ConnectivityBlobStoreTest { + private static final String DATABASE_FILENAME = "ConnectivityBlobStore.db"; + + private Context mContext; + private File mFile; + + private ConnectivityBlobStore createConnectivityBlobStore() { + return new ConnectivityBlobStore(mFile); + } + + @Before + public void setUp() throws Exception { + mContext = InstrumentationRegistry.getContext(); + mFile = mContext.getDatabasePath(DATABASE_FILENAME); + } + + @After + public void tearDown() throws Exception { + mContext.deleteDatabase(DATABASE_FILENAME); + } + + @Test + public void testFileCreateDelete() { + assertFalse(mFile.exists()); + createConnectivityBlobStore(); + assertTrue(mFile.exists()); + + assertTrue(mContext.deleteDatabase(DATABASE_FILENAME)); + assertFalse(mFile.exists()); + } +} diff --git a/core/tests/coretests/src/com/android/internal/net/OWNERS b/core/tests/coretests/src/com/android/internal/net/OWNERS new file mode 100644 index 000000000000..f51ba475ab63 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/net/OWNERS @@ -0,0 +1 @@ +include /core/java/com/android/internal/net/OWNERS diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 9c1c700641f1..ea3235bfff6c 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -588,6 +588,8 @@ applications that come with the platform <permission name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW" /> <!-- Permission required for CTS test - PackageManagerShellCommandInstallTest --> <permission name="android.permission.EMERGENCY_INSTALL_PACKAGES" /> + <!-- Permission required for Cts test - CtsSettingsTestCases --> + <permission name="android.permission.PREPARE_FACTORY_RESET" /> </privapp-permissions> <privapp-permissions package="com.android.statementservice"> diff --git a/data/keyboards/Vendor_054c_Product_05c4.idc b/data/keyboards/Vendor_054c_Product_05c4.idc index 9576e8d042ba..2da622745baf 100644 --- a/data/keyboards/Vendor_054c_Product_05c4.idc +++ b/data/keyboards/Vendor_054c_Product_05c4.idc @@ -45,14 +45,15 @@ sensor.gyroscope.power = 0.8 # This uneven timing causes the apparent speed of a finger (calculated using # time deltas between received reports) to vary dramatically even if it's # actually moving smoothly across the touchpad, triggering the touchpad stack's -# drumroll detection logic, which causes the finger's single smooth movement to -# be treated as many small movements of consecutive touches, which are then -# inhibited by the click wiggle filter. +# drumroll detection logic. For moving fingers, the drumroll detection logic +# splits the finger's single movement into many small movements of consecutive +# touches, which are then inhibited by the click wiggle filter. For tapping +# fingers, it prevents tapping to click because it thinks the finger's moving +# too fast. # -# Since this touchpad does not seem vulnerable to click wiggle, we can safely -# disable drumroll detection due to speed changes (by setting the speed change -# threshold very high, since there's no boolean control property). -gestureProp.Drumroll_Max_Speed_Change_Factor = 1000000000 +# Since this touchpad doesn't seem to have to drumroll issues, we can safely +# disable drumroll detection. +gestureProp.Drumroll_Suppression_Enable = 0 # Because of the way this touchpad is positioned, touches around the edges are # no more likely to be palms than ones in the middle, so remove the edge zones diff --git a/data/keyboards/Vendor_054c_Product_09cc.idc b/data/keyboards/Vendor_054c_Product_09cc.idc index 9576e8d042ba..2a1a4fc62b24 100644 --- a/data/keyboards/Vendor_054c_Product_09cc.idc +++ b/data/keyboards/Vendor_054c_Product_09cc.idc @@ -45,14 +45,15 @@ sensor.gyroscope.power = 0.8 # This uneven timing causes the apparent speed of a finger (calculated using # time deltas between received reports) to vary dramatically even if it's # actually moving smoothly across the touchpad, triggering the touchpad stack's -# drumroll detection logic, which causes the finger's single smooth movement to -# be treated as many small movements of consecutive touches, which are then -# inhibited by the click wiggle filter. +# drumroll detection logic. For moving fingers, the drumroll detection logic +# splits the finger's single movement into many small movements of consecutive +# touches, which are then inhibited by the click wiggle filter. For tapping +# fingers, it prevents tapping to click because it thinks the finger's moving +# too fast. # -# Since this touchpad does not seem vulnerable to click wiggle, we can safely -# disable drumroll detection due to speed changes (by setting the speed change -# threshold very high, since there's no boolean control property). -gestureProp.Drumroll_Max_Speed_Change_Factor = 1000000000 +# Since this touchpad doesn't seem to have drumroll issues, we can safely +# disable drumroll detection. +gestureProp.Drumroll_Suppression_Enable = 0 # Because of the way this touchpad is positioned, touches around the edges are # no more likely to be palms than ones in the middle, so remove the edge zones diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java index 97562783882c..16c77d0c3c81 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java @@ -53,7 +53,7 @@ class WindowExtensionsImpl implements WindowExtensions { * The min version of the WM Extensions that must be supported in the current platform version. */ @VisibleForTesting - static final int EXTENSIONS_VERSION_CURRENT_PLATFORM = 5; + static final int EXTENSIONS_VERSION_CURRENT_PLATFORM = 6; private final Object mLock = new Object(); private volatile DeviceStateManagerFoldingFeatureProducer mFoldingFeatureProducer; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java index 100185b84b77..cae232e54f3c 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java @@ -17,6 +17,12 @@ package androidx.window.extensions.embedding; import static android.util.TypedValue.COMPLEX_UNIT_DIP; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL; +import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_PANEL; +import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE; +import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE; import static androidx.window.extensions.embedding.DividerAttributes.RATIO_UNSET; import static androidx.window.extensions.embedding.DividerAttributes.WIDTH_UNSET; @@ -28,34 +34,253 @@ import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSI import android.annotation.Nullable; import android.app.ActivityThread; import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.RotateDrawable; +import android.hardware.display.DisplayManager; +import android.os.IBinder; import android.util.TypedValue; +import android.view.Gravity; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.WindowManager; +import android.view.WindowlessWindowManager; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.window.InputTransferToken; +import android.window.TaskFragmentOperation; +import android.window.TaskFragmentParentInfo; +import android.window.WindowContainerTransaction; +import androidx.annotation.IdRes; import androidx.annotation.NonNull; +import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import com.android.window.flags.Flags; +import java.util.Objects; + /** * Manages the rendering and interaction of the divider. */ class DividerPresenter { + private static final String WINDOW_NAME = "AE Divider"; + // TODO(b/327067596) Update based on UX guidance. - @VisibleForTesting static final float DEFAULT_MIN_RATIO = 0.35f; - @VisibleForTesting static final float DEFAULT_MAX_RATIO = 0.65f; - @VisibleForTesting static final int DEFAULT_DIVIDER_WIDTH_DP = 24; + private static final Color DEFAULT_DIVIDER_COLOR = Color.valueOf(Color.BLACK); + @VisibleForTesting + static final float DEFAULT_MIN_RATIO = 0.35f; + @VisibleForTesting + static final float DEFAULT_MAX_RATIO = 0.65f; + @VisibleForTesting + static final int DEFAULT_DIVIDER_WIDTH_DP = 24; + + /** + * The {@link Properties} of the divider. This field is {@code null} when no divider should be + * drawn, e.g. when the split doesn't have {@link DividerAttributes} or when the decor surface + * is not available. + */ + @Nullable + @VisibleForTesting + Properties mProperties; + + /** + * The {@link Renderer} of the divider. This field is {@code null} when no divider should be + * drawn, i.e. when {@link #mProperties} is {@code null}. The {@link Renderer} is recreated or + * updated when {@link #mProperties} is changed. + */ + @Nullable + @VisibleForTesting + Renderer mRenderer; + + /** + * The owner TaskFragment token of the decor surface. The decor surface is placed right above + * the owner TaskFragment surface and is removed if the owner TaskFragment is destroyed. + */ + @Nullable + @VisibleForTesting + IBinder mDecorSurfaceOwner; + + /** Updates the divider when external conditions are changed. */ + void updateDivider( + @NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentParentInfo parentInfo, + @Nullable SplitContainer topSplitContainer) { + if (!Flags.activityEmbeddingInteractiveDividerFlag()) { + return; + } + + // Clean up the decor surface if top SplitContainer is null. + if (topSplitContainer == null) { + removeDecorSurfaceAndDivider(wct); + return; + } + + // Clean up the decor surface if DividerAttributes is null. + final DividerAttributes dividerAttributes = + topSplitContainer.getCurrentSplitAttributes().getDividerAttributes(); + if (dividerAttributes == null) { + removeDecorSurfaceAndDivider(wct); + return; + } + + if (topSplitContainer.getCurrentSplitAttributes().getSplitType() + instanceof SplitAttributes.SplitType.ExpandContainersSplitType) { + // No divider is needed for ExpandContainersSplitType. + removeDivider(); + return; + } + + // Skip updating when the TFs have not been updated to match the SplitAttributes. + if (topSplitContainer.getPrimaryContainer().getLastRequestedBounds().isEmpty() + || topSplitContainer.getSecondaryContainer().getLastRequestedBounds().isEmpty()) { + return; + } + + final SurfaceControl decorSurface = parentInfo.getDecorSurface(); + if (decorSurface == null) { + // Clean up when the decor surface is currently unavailable. + removeDivider(); + // Request to create the decor surface + createOrMoveDecorSurface(wct, topSplitContainer.getPrimaryContainer()); + return; + } + + // make the top primary container the owner of the decor surface. + if (!Objects.equals(mDecorSurfaceOwner, + topSplitContainer.getPrimaryContainer().getTaskFragmentToken())) { + createOrMoveDecorSurface(wct, topSplitContainer.getPrimaryContainer()); + } + + updateProperties( + new Properties( + parentInfo.getConfiguration(), + dividerAttributes, + decorSurface, + getInitialDividerPosition(topSplitContainer), + isVerticalSplit(topSplitContainer), + parentInfo.getDisplayId())); + } + + private void updateProperties(@NonNull Properties properties) { + if (Properties.equalsForDivider(mProperties, properties)) { + return; + } + final Properties previousProperties = mProperties; + mProperties = properties; + + if (mRenderer == null) { + // Create a new renderer when a renderer doesn't exist yet. + mRenderer = new Renderer(); + } else if (!Properties.areSameSurfaces( + previousProperties.mDecorSurface, mProperties.mDecorSurface) + || previousProperties.mDisplayId != mProperties.mDisplayId) { + // Release and recreate the renderer if the decor surface or the display has changed. + mRenderer.release(); + mRenderer = new Renderer(); + } else { + // Otherwise, update the renderer for the new properties. + mRenderer.update(); + } + } + + /** + * Creates a decor surface for the TaskFragment if no decor surface exists, or changes the owner + * of the existing decor surface to be the specified TaskFragment. + * + * See {@link TaskFragmentOperation#OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE}. + */ + private void createOrMoveDecorSurface( + @NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container) { + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE) + .build(); + wct.addTaskFragmentOperation(container.getTaskFragmentToken(), operation); + mDecorSurfaceOwner = container.getTaskFragmentToken(); + } + + private void removeDecorSurfaceAndDivider(@NonNull WindowContainerTransaction wct) { + if (mDecorSurfaceOwner != null) { + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE) + .build(); + wct.addTaskFragmentOperation(mDecorSurfaceOwner, operation); + mDecorSurfaceOwner = null; + } + removeDivider(); + } + + private void removeDivider() { + if (mRenderer != null) { + mRenderer.release(); + } + mProperties = null; + mRenderer = null; + } + + @VisibleForTesting + static int getInitialDividerPosition(@NonNull SplitContainer splitContainer) { + final Rect primaryBounds = + splitContainer.getPrimaryContainer().getLastRequestedBounds(); + final Rect secondaryBounds = + splitContainer.getSecondaryContainer().getLastRequestedBounds(); + if (isVerticalSplit(splitContainer)) { + return Math.min(primaryBounds.right, secondaryBounds.right); + } else { + return Math.min(primaryBounds.bottom, secondaryBounds.bottom); + } + } + + private static boolean isVerticalSplit(@NonNull SplitContainer splitContainer) { + final int layoutDirection = splitContainer.getCurrentSplitAttributes().getLayoutDirection(); + switch(layoutDirection) { + case SplitAttributes.LayoutDirection.LEFT_TO_RIGHT: + case SplitAttributes.LayoutDirection.RIGHT_TO_LEFT: + case SplitAttributes.LayoutDirection.LOCALE: + return true; + case SplitAttributes.LayoutDirection.TOP_TO_BOTTOM: + case SplitAttributes.LayoutDirection.BOTTOM_TO_TOP: + return false; + default: + throw new IllegalArgumentException("Invalid layout direction:" + layoutDirection); + } + } - static int getDividerWidthPx(@NonNull DividerAttributes dividerAttributes) { + private static void safeReleaseSurfaceControl(@Nullable SurfaceControl sc) { + if (sc != null) { + sc.release(); + } + } + + private static int getDividerWidthPx(@NonNull DividerAttributes dividerAttributes) { int dividerWidthDp = dividerAttributes.getWidthDp(); + return convertDpToPixel(dividerWidthDp); + } + private static int convertDpToPixel(int dp) { // TODO(b/329193115) support divider on secondary display final Context applicationContext = ActivityThread.currentActivityThread().getApplication(); return (int) TypedValue.applyDimension( COMPLEX_UNIT_DIP, - dividerWidthDp, + dp, applicationContext.getResources().getDisplayMetrics()); } + private static int getDimensionDp(@IdRes int resId) { + final Context context = ActivityThread.currentActivityThread().getApplication(); + final int px = context.getResources().getDimensionPixelSize(resId); + return (int) TypedValue.convertPixelsToDimension( + COMPLEX_UNIT_DIP, + px, + context.getResources().getDisplayMetrics()); + } + /** * Returns the container bound offset that is a result of the presence of a divider. * @@ -140,6 +365,12 @@ class DividerPresenter { widthDp = DEFAULT_DIVIDER_WIDTH_DP; } + if (dividerAttributes.getDividerType() == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { + // Draggable divider width must be larger than the drag handle size. + widthDp = Math.max(widthDp, + getDimensionDp(R.dimen.activity_embedding_divider_touch_target_width)); + } + float minRatio = dividerAttributes.getPrimaryMinRatio(); if (minRatio == RATIO_UNSET) { minRatio = DEFAULT_MIN_RATIO; @@ -156,4 +387,231 @@ class DividerPresenter { .setPrimaryMaxRatio(maxRatio) .build(); } + + /** + * Properties for the {@link DividerPresenter}. The rendering of the divider solely depends on + * these properties. When any value is updated, the divider is re-rendered. The Properties + * instance is created only when all the pre-conditions of drawing a divider are met. + */ + @VisibleForTesting + static class Properties { + private static final int CONFIGURATION_MASK_FOR_DIVIDER = + ActivityInfo.CONFIG_DENSITY | ActivityInfo.CONFIG_WINDOW_CONFIGURATION; + @NonNull + private final Configuration mConfiguration; + @NonNull + private final DividerAttributes mDividerAttributes; + @NonNull + private final SurfaceControl mDecorSurface; + + /** The initial position of the divider calculated based on container bounds. */ + private final int mInitialDividerPosition; + + /** Whether the split is vertical, such as left-to-right or right-to-left split. */ + private final boolean mIsVerticalSplit; + + private final int mDisplayId; + + @VisibleForTesting + Properties( + @NonNull Configuration configuration, + @NonNull DividerAttributes dividerAttributes, + @NonNull SurfaceControl decorSurface, + int initialDividerPosition, + boolean isVerticalSplit, + int displayId) { + mConfiguration = configuration; + mDividerAttributes = dividerAttributes; + mDecorSurface = decorSurface; + mInitialDividerPosition = initialDividerPosition; + mIsVerticalSplit = isVerticalSplit; + mDisplayId = displayId; + } + + /** + * Compares whether two Properties objects are equal for rendering the divider. The + * Configuration is checked for rendering related fields, and other fields are checked for + * regular equality. + */ + private static boolean equalsForDivider(@Nullable Properties a, @Nullable Properties b) { + if (a == b) { + return true; + } + if (a == null || b == null) { + return false; + } + return areSameSurfaces(a.mDecorSurface, b.mDecorSurface) + && Objects.equals(a.mDividerAttributes, b.mDividerAttributes) + && areConfigurationsEqualForDivider(a.mConfiguration, b.mConfiguration) + && a.mInitialDividerPosition == b.mInitialDividerPosition + && a.mIsVerticalSplit == b.mIsVerticalSplit + && a.mDisplayId == b.mDisplayId; + } + + private static boolean areSameSurfaces( + @Nullable SurfaceControl sc1, @Nullable SurfaceControl sc2) { + if (sc1 == sc2) { + // If both are null or both refer to the same object. + return true; + } + if (sc1 == null || sc2 == null) { + return false; + } + return sc1.isSameSurface(sc2); + } + + private static boolean areConfigurationsEqualForDivider( + @NonNull Configuration a, @NonNull Configuration b) { + final int diff = a.diff(b); + return (diff & CONFIGURATION_MASK_FOR_DIVIDER) == 0; + } + } + + /** + * Handles the rendering of the divider. When the decor surface is updated, the renderer is + * recreated. When other fields in the Properties are changed, the renderer is updated. + */ + @VisibleForTesting + class Renderer { + @NonNull + private final SurfaceControl mDividerSurface; + @NonNull + private final WindowlessWindowManager mWindowlessWindowManager; + @NonNull + private final SurfaceControlViewHost mViewHost; + @NonNull + private final FrameLayout mDividerLayout; + private final int mDividerWidthPx; + + private Renderer() { + mDividerWidthPx = getDividerWidthPx(mProperties.mDividerAttributes); + + mDividerSurface = createChildSurface("DividerSurface", true /* visible */); + mWindowlessWindowManager = new WindowlessWindowManager( + mProperties.mConfiguration, + mDividerSurface, + new InputTransferToken()); + + final Context context = ActivityThread.currentActivityThread().getApplication(); + final DisplayManager displayManager = context.getSystemService(DisplayManager.class); + mViewHost = new SurfaceControlViewHost( + context, displayManager.getDisplay(mProperties.mDisplayId), + mWindowlessWindowManager, "DividerContainer"); + mDividerLayout = new FrameLayout(context); + + update(); + } + + /** Updates the divider when properties are changed */ + @VisibleForTesting + void update() { + mWindowlessWindowManager.setConfiguration(mProperties.mConfiguration); + updateSurface(); + updateLayout(); + updateDivider(); + } + + @VisibleForTesting + void release() { + mViewHost.release(); + // TODO handle synchronization between surface transactions and WCT. + new SurfaceControl.Transaction().remove(mDividerSurface).apply(); + safeReleaseSurfaceControl(mDividerSurface); + } + + private void updateSurface() { + final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); + // TODO handle synchronization between surface transactions and WCT. + final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + if (mProperties.mIsVerticalSplit) { + t.setPosition(mDividerSurface, mProperties.mInitialDividerPosition, 0.0f); + t.setWindowCrop(mDividerSurface, mDividerWidthPx, taskBounds.height()); + } else { + t.setPosition(mDividerSurface, 0.0f, mProperties.mInitialDividerPosition); + t.setWindowCrop(mDividerSurface, taskBounds.width(), mDividerWidthPx); + } + t.apply(); + } + + private void updateLayout() { + final Rect taskBounds = mProperties.mConfiguration.windowConfiguration.getBounds(); + final WindowManager.LayoutParams lp = mProperties.mIsVerticalSplit + ? new WindowManager.LayoutParams( + mDividerWidthPx, + taskBounds.height(), + TYPE_APPLICATION_PANEL, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_SLIPPERY, + PixelFormat.TRANSLUCENT) + : new WindowManager.LayoutParams( + taskBounds.width(), + mDividerWidthPx, + TYPE_APPLICATION_PANEL, + FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCH_MODAL | FLAG_SLIPPERY, + PixelFormat.TRANSLUCENT); + lp.setTitle(WINDOW_NAME); + mViewHost.setView(mDividerLayout, lp); + } + + private void updateDivider() { + mDividerLayout.removeAllViews(); + mDividerLayout.setBackgroundColor(DEFAULT_DIVIDER_COLOR.toArgb()); + if (mProperties.mDividerAttributes.getDividerType() + == DividerAttributes.DIVIDER_TYPE_DRAGGABLE) { + drawDragHandle(); + } + mViewHost.getView().invalidate(); + } + + private void drawDragHandle() { + final Context context = mDividerLayout.getContext(); + final ImageButton button = new ImageButton(context); + final FrameLayout.LayoutParams params = mProperties.mIsVerticalSplit + ? new FrameLayout.LayoutParams( + context.getResources().getDimensionPixelSize( + R.dimen.activity_embedding_divider_touch_target_width), + context.getResources().getDimensionPixelSize( + R.dimen.activity_embedding_divider_touch_target_height)) + : new FrameLayout.LayoutParams( + context.getResources().getDimensionPixelSize( + R.dimen.activity_embedding_divider_touch_target_height), + context.getResources().getDimensionPixelSize( + R.dimen.activity_embedding_divider_touch_target_width)); + params.gravity = Gravity.CENTER; + button.setLayoutParams(params); + button.setBackgroundColor(R.color.transparent); + + final Drawable handle = context.getResources().getDrawable( + R.drawable.activity_embedding_divider_handle, context.getTheme()); + if (mProperties.mIsVerticalSplit) { + button.setImageDrawable(handle); + } else { + // Rotate the handle drawable + RotateDrawable rotatedHandle = new RotateDrawable(); + rotatedHandle.setFromDegrees(90f); + rotatedHandle.setToDegrees(90f); + rotatedHandle.setPivotXRelative(true); + rotatedHandle.setPivotYRelative(true); + rotatedHandle.setPivotX(0.5f); + rotatedHandle.setPivotY(0.5f); + rotatedHandle.setLevel(1); + rotatedHandle.setDrawable(handle); + + button.setImageDrawable(rotatedHandle); + } + mDividerLayout.addView(button); + } + + @NonNull + private SurfaceControl createChildSurface(@NonNull String name, boolean visible) { + final Rect bounds = mProperties.mConfiguration.windowConfiguration.getBounds(); + return new SurfaceControl.Builder() + .setParent(mProperties.mDecorSurface) + .setName(name) + .setHidden(!visible) + .setCallsite("DividerManager.createChildSurface") + .setBufferSize(bounds.width(), bounds.height()) + .setColorLayer() + .build(); + } + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java index 80afb16d5832..3f4dddf0cc81 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -168,11 +168,14 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { * @param fragmentToken token of an existing TaskFragment. */ void expandTaskFragment(@NonNull WindowContainerTransaction wct, - @NonNull IBinder fragmentToken) { + @NonNull TaskFragmentContainer container) { + final IBinder fragmentToken = container.getTaskFragmentToken(); resizeTaskFragment(wct, fragmentToken, new Rect()); clearAdjacentTaskFragments(wct, fragmentToken); updateWindowingMode(wct, fragmentToken, WINDOWING_MODE_UNDEFINED); updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT); + + container.getTaskContainer().updateDivider(wct); } /** diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 0cc4b1f367d8..1bc8264d8e7e 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -844,6 +844,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Checks if container should be updated before apply new parentInfo. final boolean shouldUpdateContainer = taskContainer.shouldUpdateContainer(parentInfo); taskContainer.updateTaskFragmentParentInfo(parentInfo); + taskContainer.updateDivider(wct); // If the last direct activity of the host task is dismissed and the overlay container is // the only taskFragment, the overlay container should also be dismissed. @@ -1224,7 +1225,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final TaskFragmentContainer container = getContainerWithActivity(activity); if (shouldContainerBeExpanded(container)) { // Make sure that the existing container is expanded. - mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken()); + mPresenter.expandTaskFragment(wct, container); } else { // Put activity into a new expanded container. final TaskFragmentContainer newContainer = newContainer(activity, getTaskId(activity)); @@ -1928,7 +1929,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } if (shouldContainerBeExpanded(container)) { if (container.getInfo() != null) { - mPresenter.expandTaskFragment(wct, container.getTaskFragmentToken()); + mPresenter.expandTaskFragment(wct, container); } // If the info is not available yet the task fragment will be expanded when it's ready return; diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index f680694c3af9..20bc82002339 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -368,6 +368,7 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { updateTaskFragmentWindowingModeIfRegistered(wct, secondaryContainer, windowingMode); updateAnimationParams(wct, primaryContainer.getTaskFragmentToken(), splitAttributes); updateAnimationParams(wct, secondaryContainer.getTaskFragmentToken(), splitAttributes); + taskContainer.updateDivider(wct); } private void setAdjacentTaskFragments(@NonNull WindowContainerTransaction wct, @@ -686,8 +687,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { splitContainer.getPrimaryContainer().getTaskFragmentToken(); final IBinder secondaryToken = splitContainer.getSecondaryContainer().getTaskFragmentToken(); - expandTaskFragment(wct, primaryToken); - expandTaskFragment(wct, secondaryToken); + expandTaskFragment(wct, splitContainer.getPrimaryContainer()); + expandTaskFragment(wct, splitContainer.getSecondaryContainer()); // Set the companion TaskFragment when the two containers stacked. setCompanionTaskFragment(wct, primaryToken, secondaryToken, splitContainer.getSplitRule(), true /* isStacked */); diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index 73109e266905..e75a317cc3b3 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -77,6 +77,9 @@ class TaskContainer { private boolean mHasDirectActivity; + @Nullable + private TaskFragmentParentInfo mTaskFragmentParentInfo; + /** * TaskFragments that the organizer has requested to be closed. They should be removed when * the organizer receives @@ -85,14 +88,17 @@ class TaskContainer { */ final Set<IBinder> mFinishedContainer = new ArraySet<>(); + // TODO(b/293654166): move DividerPresenter to SplitController. + @NonNull + final DividerPresenter mDividerPresenter; + /** * The {@link TaskContainer} constructor * - * @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with - * {@code activityInTask}. + * @param taskId The ID of the Task, which must match {@link Activity#getTaskId()} with + * {@code activityInTask}. * @param activityInTask The {@link Activity} in the Task with {@code taskId}. It is used to * initialize the {@link TaskContainer} properties. - * */ TaskContainer(int taskId, @NonNull Activity activityInTask) { if (taskId == INVALID_TASK_ID) { @@ -107,6 +113,7 @@ class TaskContainer { // the host task is visible and has an activity in the task. mIsVisible = true; mHasDirectActivity = true; + mDividerPresenter = new DividerPresenter(); } int getTaskId() { @@ -136,10 +143,12 @@ class TaskContainer { } void updateTaskFragmentParentInfo(@NonNull TaskFragmentParentInfo info) { + // TODO(b/293654166): cache the TaskFragmentParentInfo and remove these fields. mConfiguration.setTo(info.getConfiguration()); mDisplayId = info.getDisplayId(); mIsVisible = info.isVisible(); mHasDirectActivity = info.hasDirectActivity(); + mTaskFragmentParentInfo = info; } /** @@ -161,8 +170,8 @@ class TaskContainer { * Returns the windowing mode for the TaskFragments below this Task, which should be split with * other TaskFragments. * - * @param taskFragmentBounds Requested bounds for the TaskFragment. It will be empty when - * the pair of TaskFragments are stacked due to the limited space. + * @param taskFragmentBounds Requested bounds for the TaskFragment. It will be empty when + * the pair of TaskFragments are stacked due to the limited space. */ @WindowingMode int getWindowingModeForTaskFragment(@Nullable Rect taskFragmentBounds) { @@ -228,7 +237,7 @@ class TaskContainer { @Nullable TaskFragmentContainer getTopNonFinishingTaskFragmentContainer(boolean includePin, - boolean includeOverlay) { + boolean includeOverlay) { for (int i = mContainers.size() - 1; i >= 0; i--) { final TaskFragmentContainer container = mContainers.get(i); if (!includePin && isTaskFragmentContainerPinned(container)) { @@ -283,7 +292,7 @@ class TaskContainer { return mContainers.indexOf(child); } - /** Whether the Task is in an intermediate state waiting for the server update.*/ + /** Whether the Task is in an intermediate state waiting for the server update. */ boolean isInIntermediateState() { for (TaskFragmentContainer container : mContainers) { if (container.isInIntermediateState()) { @@ -389,6 +398,26 @@ class TaskContainer { return mContainers; } + void updateDivider(@NonNull WindowContainerTransaction wct) { + if (mTaskFragmentParentInfo != null) { + // Update divider only if TaskFragmentParentInfo is available. + mDividerPresenter.updateDivider( + wct, mTaskFragmentParentInfo, getTopNonFinishingSplitContainer()); + } + } + + @Nullable + private SplitContainer getTopNonFinishingSplitContainer() { + for (int i = mSplitContainers.size() - 1; i >= 0; i--) { + final SplitContainer splitContainer = mSplitContainers.get(i); + if (!splitContainer.getPrimaryContainer().isFinished() + && !splitContainer.getSecondaryContainer().isFinished()) { + return splitContainer; + } + } + return null; + } + private void onTaskFragmentContainerUpdated() { // TODO(b/300211704): Find a better mechanism to handle the z-order in case we introduce // another special container that should also be on top in the future. diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java index a6bf99d4add5..e20a3e02c65d 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java @@ -748,6 +748,10 @@ class TaskFragmentContainer { } } + @NonNull Rect getLastRequestedBounds() { + return mLastRequestedBounds; + } + /** * Checks if last requested windowing mode is equal to the provided value. * @see WindowContainerTransaction#setWindowingMode diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java index 2a277f4c9619..4d1d807038eb 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/DividerPresenterTest.java @@ -16,22 +16,49 @@ package androidx.window.extensions.embedding; +import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE; +import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE; + import static androidx.window.extensions.embedding.DividerPresenter.getBoundsOffsetForDivider; +import static androidx.window.extensions.embedding.DividerPresenter.getInitialDividerPosition; import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_BOTTOM; import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_LEFT; import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_RIGHT; import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSITION_TOP; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.os.Binder; +import android.os.IBinder; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; +import android.view.Display; +import android.view.SurfaceControl; +import android.window.TaskFragmentOperation; +import android.window.TaskFragmentParentInfo; +import android.window.WindowContainerTransaction; import androidx.annotation.NonNull; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.window.flags.Flags; + +import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; /** * Test class for {@link DividerPresenter}. @@ -43,6 +70,167 @@ import org.junit.runner.RunWith; @SmallTest @RunWith(AndroidJUnit4.class) public class DividerPresenterTest { + @Rule + public final SetFlagsRule mSetFlagRule = new SetFlagsRule(); + + @Mock + private DividerPresenter.Renderer mRenderer; + + @Mock + private WindowContainerTransaction mTransaction; + + @Mock + private TaskFragmentParentInfo mParentInfo; + + @Mock + private SplitContainer mSplitContainer; + + @Mock + private SurfaceControl mSurfaceControl; + + private DividerPresenter mDividerPresenter; + + private final IBinder mPrimaryContainerToken = new Binder(); + + private final IBinder mSecondaryContainerToken = new Binder(); + + private final IBinder mAnotherContainerToken = new Binder(); + + private DividerPresenter.Properties mProperties; + + private static final DividerAttributes DEFAULT_DIVIDER_ATTRIBUTES = + new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE).build(); + + private static final DividerAttributes ANOTHER_DIVIDER_ATTRIBUTES = + new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE) + .setWidthDp(10).build(); + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_EMBEDDING_INTERACTIVE_DIVIDER_FLAG); + + when(mParentInfo.getDisplayId()).thenReturn(Display.DEFAULT_DISPLAY); + when(mParentInfo.getConfiguration()).thenReturn(new Configuration()); + when(mParentInfo.getDecorSurface()).thenReturn(mSurfaceControl); + + when(mSplitContainer.getCurrentSplitAttributes()).thenReturn( + new SplitAttributes.Builder() + .setDividerAttributes(DEFAULT_DIVIDER_ATTRIBUTES) + .build()); + final TaskFragmentContainer mockPrimaryContainer = + createMockTaskFragmentContainer( + mPrimaryContainerToken, new Rect(0, 0, 950, 1000)); + final TaskFragmentContainer mockSecondaryContainer = + createMockTaskFragmentContainer( + mSecondaryContainerToken, new Rect(1000, 0, 2000, 1000)); + when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer); + when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer); + + mProperties = new DividerPresenter.Properties( + new Configuration(), + DEFAULT_DIVIDER_ATTRIBUTES, + mSurfaceControl, + getInitialDividerPosition(mSplitContainer), + true /* isVerticalSplit */, + Display.DEFAULT_DISPLAY); + + mDividerPresenter = new DividerPresenter(); + mDividerPresenter.mProperties = mProperties; + mDividerPresenter.mRenderer = mRenderer; + mDividerPresenter.mDecorSurfaceOwner = mPrimaryContainerToken; + } + + @Test + public void testUpdateDivider() { + when(mSplitContainer.getCurrentSplitAttributes()).thenReturn( + new SplitAttributes.Builder() + .setDividerAttributes(ANOTHER_DIVIDER_ATTRIBUTES) + .build()); + mDividerPresenter.updateDivider( + mTransaction, + mParentInfo, + mSplitContainer); + + assertNotEquals(mProperties, mDividerPresenter.mProperties); + verify(mRenderer).update(); + verify(mTransaction, never()).addTaskFragmentOperation(any(), any()); + } + + @Test + public void testUpdateDivider_updateDecorSurfaceOwnerIfPrimaryContainerChanged() { + final TaskFragmentContainer mockPrimaryContainer = + createMockTaskFragmentContainer( + mAnotherContainerToken, new Rect(0, 0, 750, 1000)); + final TaskFragmentContainer mockSecondaryContainer = + createMockTaskFragmentContainer( + mSecondaryContainerToken, new Rect(800, 0, 2000, 1000)); + when(mSplitContainer.getPrimaryContainer()).thenReturn(mockPrimaryContainer); + when(mSplitContainer.getSecondaryContainer()).thenReturn(mockSecondaryContainer); + mDividerPresenter.updateDivider( + mTransaction, + mParentInfo, + mSplitContainer); + + assertNotEquals(mProperties, mDividerPresenter.mProperties); + verify(mRenderer).update(); + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE) + .build(); + assertEquals(mAnotherContainerToken, mDividerPresenter.mDecorSurfaceOwner); + verify(mTransaction).addTaskFragmentOperation(mAnotherContainerToken, operation); + } + + @Test + public void testUpdateDivider_noChangeIfPropertiesIdentical() { + mDividerPresenter.updateDivider( + mTransaction, + mParentInfo, + mSplitContainer); + + assertEquals(mProperties, mDividerPresenter.mProperties); + verify(mRenderer, never()).update(); + verify(mTransaction, never()).addTaskFragmentOperation(any(), any()); + } + + @Test + public void testUpdateDivider_dividerRemovedWhenSplitContainerIsNull() { + mDividerPresenter.updateDivider( + mTransaction, + mParentInfo, + null /* splitContainer */); + final TaskFragmentOperation taskFragmentOperation = new TaskFragmentOperation.Builder( + OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE) + .build(); + + verify(mTransaction).addTaskFragmentOperation( + mPrimaryContainerToken, taskFragmentOperation); + verify(mRenderer).release(); + assertNull(mDividerPresenter.mRenderer); + assertNull(mDividerPresenter.mProperties); + assertNull(mDividerPresenter.mDecorSurfaceOwner); + } + + @Test + public void testUpdateDivider_dividerRemovedWhenDividerAttributesIsNull() { + when(mSplitContainer.getCurrentSplitAttributes()).thenReturn( + new SplitAttributes.Builder().setDividerAttributes(null).build()); + mDividerPresenter.updateDivider( + mTransaction, + mParentInfo, + mSplitContainer); + final TaskFragmentOperation taskFragmentOperation = new TaskFragmentOperation.Builder( + OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE) + .build(); + + verify(mTransaction).addTaskFragmentOperation( + mPrimaryContainerToken, taskFragmentOperation); + verify(mRenderer).release(); + assertNull(mDividerPresenter.mRenderer); + assertNull(mDividerPresenter.mProperties); + assertNull(mDividerPresenter.mDecorSurfaceOwner); + } + @Test public void testSanitizeDividerAttributes_setDefaultValues() { DividerAttributes attributes = @@ -61,7 +249,7 @@ public class DividerPresenterTest { public void testSanitizeDividerAttributes_notChangingValidValues() { DividerAttributes attributes = new DividerAttributes.Builder(DividerAttributes.DIVIDER_TYPE_DRAGGABLE) - .setWidthDp(10) + .setWidthDp(24) .setPrimaryMinRatio(0.3f) .setPrimaryMaxRatio(0.7f) .build(); @@ -123,6 +311,14 @@ public class DividerPresenterTest { dividerWidthPx, splitType, expectedTopLeftOffset, expectedBottomRightOffset); } + private TaskFragmentContainer createMockTaskFragmentContainer( + @NonNull IBinder token, @NonNull Rect bounds) { + final TaskFragmentContainer container = mock(TaskFragmentContainer.class); + when(container.getTaskFragmentToken()).thenReturn(token); + when(container.getLastRequestedBounds()).thenReturn(bounds); + return container; + } + private void assertDividerOffsetEquals( int dividerWidthPx, @NonNull SplitAttributes.SplitType splitType, diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java index dd087e8eb7c9..6f37e9cb794d 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizerTest.java @@ -107,7 +107,7 @@ public class JetpackTaskFragmentOrganizerTest { mOrganizer.mFragmentInfos.put(container.getTaskFragmentToken(), info); container.setInfo(mTransaction, info); - mOrganizer.expandTaskFragment(mTransaction, container.getTaskFragmentToken()); + mOrganizer.expandTaskFragment(mTransaction, container); verify(mTransaction).setWindowingMode(container.getInfo().getToken(), WINDOWING_MODE_UNDEFINED); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index cdb37acfc0c2..c246a19f27e2 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -642,7 +642,7 @@ public class SplitControllerTest { false /* isOnReparent */); assertTrue(result); - verify(mSplitPresenter).expandTaskFragment(mTransaction, container.getTaskFragmentToken()); + verify(mSplitPresenter).expandTaskFragment(mTransaction, container); } @Test diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java index 941b4e1c3e41..62d8aa30a576 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java @@ -665,8 +665,8 @@ public class SplitPresenterTest { assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */)); - verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken()); - verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken()); + verify(mPresenter).expandTaskFragment(mTransaction, primaryTf); + verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf); splitContainer.updateCurrentSplitAttributes(SPLIT_ATTRIBUTES); clearInvocations(mPresenter); @@ -675,8 +675,8 @@ public class SplitPresenterTest { splitContainer, mActivity, null /* secondaryActivity */, new Intent(ApplicationProvider.getApplicationContext(), MinimumDimensionActivity.class))); - verify(mPresenter).expandTaskFragment(mTransaction, primaryTf.getTaskFragmentToken()); - verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf.getTaskFragmentToken()); + verify(mPresenter).expandTaskFragment(mTransaction, primaryTf); + verify(mPresenter).expandTaskFragment(mTransaction, secondaryTf); } @Test diff --git a/libs/WindowManager/Shell/multivalentTests/Android.bp b/libs/WindowManager/Shell/multivalentTests/Android.bp index 1686d0d54dc4..1ad19c9f3033 100644 --- a/libs/WindowManager/Shell/multivalentTests/Android.bp +++ b/libs/WindowManager/Shell/multivalentTests/Android.bp @@ -46,6 +46,7 @@ android_robolectric_test { exclude_srcs: ["src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt"], static_libs: [ "junit", + "androidx.core_core-animation-testing", "androidx.test.runner", "androidx.test.rules", "androidx.test.ext.junit", @@ -64,6 +65,7 @@ android_test { static_libs: [ "WindowManager-Shell", "junit", + "androidx.core_core-animation-testing", "androidx.test.runner", "androidx.test.rules", "androidx.test.ext.junit", diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetControllerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetControllerTest.kt new file mode 100644 index 000000000000..2ac77917a348 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetControllerTest.kt @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.bubbles.bar + +import android.content.Context +import android.graphics.Insets +import android.graphics.Rect +import android.view.View +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 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.internal.protolog.common.ProtoLog +import com.android.wm.shell.R +import com.android.wm.shell.bubbles.BubblePositioner +import com.android.wm.shell.bubbles.DeviceConfig +import com.android.wm.shell.bubbles.bar.BubbleBarDropTargetController.Companion.DROP_TARGET_ALPHA_IN_DURATION +import com.android.wm.shell.bubbles.bar.BubbleBarDropTargetController.Companion.DROP_TARGET_ALPHA_OUT_DURATION +import com.android.wm.shell.bubbles.bar.BubbleBarDropTargetController.Companion.DROP_TARGET_SCALE +import com.android.wm.shell.common.bubbles.BubbleBarLocation +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.ClassRule +import org.junit.Test +import org.junit.runner.RunWith + +/** Tests for [BubbleBarDropTargetController] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleBarDropTargetControllerTest { + + companion object { + @JvmField @ClassRule val animatorTestRule: AnimatorTestRule = AnimatorTestRule() + } + + private val context = ApplicationProvider.getApplicationContext<Context>() + private lateinit var controller: BubbleBarDropTargetController + private lateinit var positioner: BubblePositioner + private lateinit var container: FrameLayout + + @Before + fun setUp() { + ProtoLog.REQUIRE_PROTOLOGTOOL = false + container = FrameLayout(context) + val windowManager = context.getSystemService(WindowManager::class.java) + positioner = BubblePositioner(context, windowManager) + positioner.setShowingInBubbleBar(true) + val deviceConfig = + DeviceConfig( + windowBounds = Rect(0, 0, 2000, 2600), + isLargeScreen = true, + isSmallTablet = false, + isLandscape = true, + isRtl = false, + insets = Insets.of(10, 20, 30, 40) + ) + positioner.update(deviceConfig) + positioner.bubbleBarBounds = Rect(1800, 2400, 1970, 2560) + + controller = BubbleBarDropTargetController(context, container, positioner) + } + + @Test + fun show_moveLeftToRight_isVisibleWithExpectedBounds() { + val expectedBoundsOnLeft = getExpectedDropTargetBounds(onLeft = true) + val expectedBoundsOnRight = getExpectedDropTargetBounds(onLeft = false) + + runOnMainSync { controller.show(BubbleBarLocation.LEFT) } + waitForAnimateIn() + val viewOnLeft = getDropTargetView() + assertThat(viewOnLeft).isNotNull() + assertThat(viewOnLeft!!.alpha).isEqualTo(1f) + assertThat(viewOnLeft.layoutParams.width).isEqualTo(expectedBoundsOnLeft.width()) + assertThat(viewOnLeft.layoutParams.height).isEqualTo(expectedBoundsOnLeft.height()) + assertThat(viewOnLeft.x).isEqualTo(expectedBoundsOnLeft.left) + assertThat(viewOnLeft.y).isEqualTo(expectedBoundsOnLeft.top) + + runOnMainSync { controller.show(BubbleBarLocation.RIGHT) } + waitForAnimateOut() + waitForAnimateIn() + val viewOnRight = getDropTargetView() + assertThat(viewOnRight).isNotNull() + assertThat(viewOnRight!!.alpha).isEqualTo(1f) + assertThat(viewOnRight.layoutParams.width).isEqualTo(expectedBoundsOnRight.width()) + assertThat(viewOnRight.layoutParams.height).isEqualTo(expectedBoundsOnRight.height()) + assertThat(viewOnRight.x).isEqualTo(expectedBoundsOnRight.left) + assertThat(viewOnRight.y).isEqualTo(expectedBoundsOnRight.top) + } + + @Test + fun toggleSetHidden_dropTargetShown_updatesAlpha() { + runOnMainSync { controller.show(BubbleBarLocation.RIGHT) } + waitForAnimateIn() + val view = getDropTargetView() + assertThat(view).isNotNull() + assertThat(view!!.alpha).isEqualTo(1f) + + runOnMainSync { controller.setHidden(true) } + waitForAnimateOut() + val hiddenView = getDropTargetView() + assertThat(hiddenView).isNotNull() + assertThat(hiddenView!!.alpha).isEqualTo(0f) + + runOnMainSync { controller.setHidden(false) } + waitForAnimateIn() + val shownView = getDropTargetView() + assertThat(shownView).isNotNull() + assertThat(shownView!!.alpha).isEqualTo(1f) + } + + @Test + fun toggleSetHidden_dropTargetNotShown_viewNotCreated() { + runOnMainSync { controller.setHidden(true) } + waitForAnimateOut() + assertThat(getDropTargetView()).isNull() + runOnMainSync { controller.setHidden(false) } + waitForAnimateIn() + assertThat(getDropTargetView()).isNull() + } + + @Test + fun dismiss_dropTargetShown_viewRemoved() { + runOnMainSync { controller.show(BubbleBarLocation.LEFT) } + waitForAnimateIn() + assertThat(getDropTargetView()).isNotNull() + runOnMainSync { controller.dismiss() } + waitForAnimateOut() + assertThat(getDropTargetView()).isNull() + } + + @Test + fun dismiss_dropTargetNotShown_doesNothing() { + runOnMainSync { controller.dismiss() } + waitForAnimateOut() + assertThat(getDropTargetView()).isNull() + } + + private fun getDropTargetView(): View? = container.findViewById(R.id.bubble_bar_drop_target) + + private fun getExpectedDropTargetBounds(onLeft: Boolean): Rect { + val rect = Rect() + positioner.getBubbleBarExpandedViewBounds(onLeft, false /* isOveflowExpanded */, rect) + // Scale the rect to expected size, but keep the center point the same + val centerX = rect.centerX() + val centerY = rect.centerY() + rect.scale(DROP_TARGET_SCALE) + rect.offset(centerX - rect.centerX(), centerY - rect.centerY()) + return rect + } + + private fun runOnMainSync(runnable: Runnable) { + InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable) + } + + private fun waitForAnimateIn() { + // Advance animator for on-device test + runOnMainSync { animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_IN_DURATION) } + } + + private fun waitForAnimateOut() { + // Advance animator for on-device test + runOnMainSync { animatorTestRule.advanceTimeBy(DROP_TARGET_ALPHA_OUT_DURATION) } + } +} diff --git a/libs/WindowManager/Shell/res/color/bubble_drop_target_background_color.xml b/libs/WindowManager/Shell/res/color/bubble_drop_target_background_color.xml new file mode 100644 index 000000000000..ab1ab984fd5f --- /dev/null +++ b/libs/WindowManager/Shell/res/color/bubble_drop_target_background_color.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <item android:alpha="0.35" android:color="?androidprv:attr/materialColorPrimaryContainer" /> +</selector>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml b/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml index 468b5c2a712f..9dcde3b54421 100644 --- a/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml +++ b/libs/WindowManager/Shell/res/drawable/bubble_drop_target_background.xml @@ -1,5 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- +<?xml version="1.0" encoding="utf-8"?><!-- ~ Copyright (C) 2024 The Android Open Source Project ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,9 +13,12 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<shape android:shape="rectangle" - xmlns:android="http://schemas.android.com/apk/res/android"> - <solid android:color="#bf309fb5" /> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:shape="rectangle"> <corners android:radius="@dimen/bubble_bar_expanded_view_corner_radius" /> - <stroke android:width="1dp" android:color="#A00080FF"/> + <solid android:color="@color/bubble_drop_target_background_color" /> + <stroke + android:width="1dp" + android:color="?androidprv:attr/materialColorPrimaryContainer" /> </shape> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetController.kt index 55ec6cdfe007..f6b4653b8162 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDropTargetController.kt @@ -21,6 +21,10 @@ import android.view.LayoutInflater import android.view.View import android.widget.FrameLayout import android.widget.FrameLayout.LayoutParams +import androidx.annotation.VisibleForTesting +import androidx.core.animation.Animator +import androidx.core.animation.AnimatorListenerAdapter +import androidx.core.animation.ObjectAnimator import com.android.wm.shell.R import com.android.wm.shell.bubbles.BubblePositioner import com.android.wm.shell.common.bubbles.BubbleBarLocation @@ -33,6 +37,7 @@ class BubbleBarDropTargetController( ) { private var dropTargetView: View? = null + private var animator: ObjectAnimator? = null private val tempRect: Rect by lazy(LazyThreadSafetyMode.NONE) { Rect() } /** @@ -57,7 +62,8 @@ class BubbleBarDropTargetController( /** * Set the view hidden or not * - * Requires the drop target to be first shown by calling [show]. Otherwise does not do anything. + * Requires the drop target to be first shown by calling [animateIn]. Otherwise does not do + * anything. */ fun setHidden(hidden: Boolean) { val targetView = dropTargetView ?: return @@ -106,20 +112,40 @@ class BubbleBarDropTargetController( } private fun View.animateIn() { - animate().alpha(1f).setDuration(DROP_TARGET_ALPHA_IN_DURATION).start() + animator?.cancel() + animator = + ObjectAnimator.ofFloat(this, View.ALPHA, 1f) + .setDuration(DROP_TARGET_ALPHA_IN_DURATION) + .addEndAction { animator = null } + animator?.start() } private fun View.animateOut(endAction: Runnable? = null) { - animate() - .alpha(0f) - .setDuration(DROP_TARGET_ALPHA_OUT_DURATION) - .withEndAction(endAction) - .start() + animator?.cancel() + animator = + ObjectAnimator.ofFloat(this, View.ALPHA, 0f) + .setDuration(DROP_TARGET_ALPHA_OUT_DURATION) + .addEndAction { + endAction?.run() + animator = null + } + animator?.start() + } + + private fun <T : Animator> T.addEndAction(runnable: Runnable): T { + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + runnable.run() + } + } + ) + return this } companion object { - private const val DROP_TARGET_ALPHA_IN_DURATION = 150L - private const val DROP_TARGET_ALPHA_OUT_DURATION = 100L - private const val DROP_TARGET_SCALE = 0.9f + @VisibleForTesting const val DROP_TARGET_ALPHA_IN_DURATION = 150L + @VisibleForTesting const val DROP_TARGET_ALPHA_OUT_DURATION = 100L + @VisibleForTesting const val DROP_TARGET_SCALE = 0.9f } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java index 838603f80cf1..5889da12d6e9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMode.java @@ -49,7 +49,7 @@ public interface DesktopMode { /** Called when requested to go to desktop mode from the current focused app. */ - void enterDesktop(int displayId); + void moveFocusedTaskToDesktop(int displayId); /** Called when requested to go to fullscreen from the current focused desktop app. */ void moveFocusedTaskToFullscreen(int displayId); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 992e5aecdce8..cdef4fddc95b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -263,7 +263,7 @@ class DesktopTasksController( } /** Enter desktop by using the focused task in given `displayId` */ - fun enterDesktop(displayId: Int) { + fun moveFocusedTaskToDesktop(displayId: Int) { val allFocusedTasks = shellTaskOrganizer.getRunningTasks(displayId).filter { taskInfo -> taskInfo.isFocused && @@ -1212,9 +1212,9 @@ class DesktopTasksController( } } - override fun enterDesktop(displayId: Int) { + override fun moveFocusedTaskToDesktop(displayId: Int) { mainExecutor.execute { - this@DesktopTasksController.enterDesktop(displayId) + this@DesktopTasksController.moveFocusedTaskToDesktop(displayId) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index 9130edfa9f26..74e85f8dd468 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -334,6 +334,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { boolean isDisplayRotationAnimationStarted = false; final boolean isDreamTransition = isDreamTransition(info); final boolean isOnlyTranslucent = isOnlyTranslucent(info); + final boolean isActivityLevel = isActivityLevelOnly(info); for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); @@ -502,8 +503,35 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { : new Rect(change.getEndAbsBounds()); clipRect.offsetTo(0, 0); + final TransitionInfo.Root animRoot = TransitionUtil.getRootFor(change, info); + final Point animRelOffset = new Point( + change.getEndAbsBounds().left - animRoot.getOffset().x, + change.getEndAbsBounds().top - animRoot.getOffset().y); + if (change.getActivityComponent() != null && !isActivityLevel) { + // At this point, this is an independent activity change in a non-activity + // transition. This means that an activity transition got erroneously combined + // with another ongoing transition. This then means that the animation root may + // not tightly fit the activities, so we have to put them in a separate crop. + final int layer = Transitions.calculateAnimLayer(change, i, + info.getChanges().size(), info.getType()); + final SurfaceControl leash = new SurfaceControl.Builder() + .setName("Transition ActivityWrap: " + + change.getActivityComponent().toShortString()) + .setParent(animRoot.getLeash()) + .setContainerLayer().build(); + startTransaction.setCrop(leash, clipRect); + startTransaction.setPosition(leash, animRelOffset.x, animRelOffset.y); + startTransaction.setLayer(leash, layer); + startTransaction.show(leash); + startTransaction.reparent(change.getLeash(), leash); + startTransaction.setPosition(change.getLeash(), 0, 0); + animRelOffset.set(0, 0); + finishTransaction.reparent(leash, null); + leash.release(); + } + buildSurfaceAnimation(animations, a, change.getLeash(), onAnimFinish, - mTransactionPool, mMainExecutor, change.getEndRelOffset(), cornerRadius, + mTransactionPool, mMainExecutor, animRelOffset, cornerRadius, clipRect); if (info.getAnimationOptions() != null) { @@ -612,6 +640,18 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { return (translucentOpen + translucentClose) > 0; } + /** + * Does `info` only contain activity-level changes? This kinda assumes that if so, they are + * all in one task. + */ + private static boolean isActivityLevelOnly(@NonNull TransitionInfo info) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (change.getActivityComponent() == null) return false; + } + return true; + } + @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index ccd0b2df8cf1..6a53d33243db 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -31,7 +31,6 @@ import static android.view.WindowManager.fixScale; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; import static android.window.TransitionInfo.FLAG_IS_OCCLUDED; -import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_NO_ANIMATION; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; @@ -530,6 +529,44 @@ public class Transitions implements RemoteCallable<Transitions>, } } + static int calculateAnimLayer(@NonNull TransitionInfo.Change change, int i, + int numChanges, @WindowManager.TransitionType int transitType) { + // Put animating stuff above this line and put static stuff below it. + final int zSplitLine = numChanges + 1; + final boolean isOpening = isOpeningType(transitType); + final boolean isClosing = isClosingType(transitType); + final int mode = change.getMode(); + // Put all the OPEN/SHOW on top + if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { + if (isOpening + // This is for when an activity launches while a different transition is + // collecting. + || change.hasFlags(FLAG_MOVED_TO_TOP)) { + // put on top + return zSplitLine + numChanges - i; + } else { + // put on bottom + return zSplitLine - i; + } + } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) { + if (isOpening) { + // put on bottom and leave visible + return zSplitLine - i; + } else { + // put on top + return zSplitLine + numChanges - i; + } + } else { // CHANGE or other + if (isClosing || TransitionUtil.isOrderOnly(change)) { + // Put below CLOSE mode (in the "static" section). + return zSplitLine - i; + } else { + // Put above CLOSE mode. + return zSplitLine + numChanges - i; + } + } + } + /** * Reparents all participants into a shared parent and orders them based on: the global transit * type, their transit mode, and their destination z-order. @@ -537,19 +574,14 @@ public class Transitions implements RemoteCallable<Transitions>, private static void setupAnimHierarchy(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) { final int type = info.getType(); - final boolean isOpening = isOpeningType(type); - final boolean isClosing = isClosingType(type); for (int i = 0; i < info.getRootCount(); ++i) { t.show(info.getRoot(i).getLeash()); } final int numChanges = info.getChanges().size(); - // Put animating stuff above this line and put static stuff below it. - final int zSplitLine = numChanges + 1; // changes should be ordered top-to-bottom in z for (int i = numChanges - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); final SurfaceControl leash = change.getLeash(); - final int mode = change.getMode(); // Don't reparent anything that isn't independent within its parents if (!TransitionInfo.isIndependent(change, info)) { @@ -558,50 +590,14 @@ public class Transitions implements RemoteCallable<Transitions>, boolean hasParent = change.getParent() != null; - final int rootIdx = TransitionUtil.rootIndexFor(change, info); + final TransitionInfo.Root root = TransitionUtil.getRootFor(change, info); if (!hasParent) { - t.reparent(leash, info.getRoot(rootIdx).getLeash()); + t.reparent(leash, root.getLeash()); t.setPosition(leash, - change.getStartAbsBounds().left - info.getRoot(rootIdx).getOffset().x, - change.getStartAbsBounds().top - info.getRoot(rootIdx).getOffset().y); - } - final int layer; - // Put all the OPEN/SHOW on top - if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { - // Wallpaper is always at the bottom, opening wallpaper on top of closing one. - if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { - layer = -zSplitLine + numChanges - i; - } else { - layer = -zSplitLine - i; - } - } else if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { - if (isOpening - // This is for when an activity launches while a different transition is - // collecting. - || change.hasFlags(FLAG_MOVED_TO_TOP)) { - // put on top - layer = zSplitLine + numChanges - i; - } else { - // put on bottom - layer = zSplitLine - i; - } - } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) { - if (isOpening) { - // put on bottom and leave visible - layer = zSplitLine - i; - } else { - // put on top - layer = zSplitLine + numChanges - i; - } - } else { // CHANGE or other - if (isClosing || TransitionUtil.isOrderOnly(change)) { - // Put below CLOSE mode (in the "static" section). - layer = zSplitLine - i; - } else { - // Put above CLOSE mode. - layer = zSplitLine + numChanges - i; - } + change.getStartAbsBounds().left - root.getOffset().x, + change.getStartAbsBounds().top - root.getOffset().y); } + final int layer = calculateAnimLayer(change, i, numChanges, type); t.setLayer(leash, layer); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java index 6f8b3d5aaaad..76096b0c59f3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositioner.java @@ -18,6 +18,7 @@ package com.android.wm.shell.windowdecor; import static android.view.WindowManager.TRANSIT_CHANGE; +import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.os.IBinder; @@ -178,10 +179,11 @@ class FluidResizeTaskPositioner implements DragPositioningCallback, for (TransitionInfo.Change change: info.getChanges()) { final SurfaceControl sc = change.getLeash(); final Rect endBounds = change.getEndAbsBounds(); + final Point endPosition = change.getEndRelOffset(); startTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height()) - .setPosition(sc, endBounds.left, endBounds.top); + .setPosition(sc, endPosition.x, endPosition.y); finishTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height()) - .setPosition(sc, endBounds.left, endBounds.top); + .setPosition(sc, endPosition.x, endPosition.y); } startTransaction.apply(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java index c12a93edcaf3..5fce5d228d71 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java @@ -18,6 +18,7 @@ package com.android.wm.shell.windowdecor; import static android.view.WindowManager.TRANSIT_CHANGE; +import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.os.IBinder; @@ -179,10 +180,11 @@ public class VeiledResizeTaskPositioner implements DragPositioningCallback, for (TransitionInfo.Change change: info.getChanges()) { final SurfaceControl sc = change.getLeash(); final Rect endBounds = change.getEndAbsBounds(); + final Point endPosition = change.getEndRelOffset(); startTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height()) - .setPosition(sc, endBounds.left, endBounds.top); + .setPosition(sc, endPosition.x, endPosition.y); finishTransaction.setWindowCrop(sc, endBounds.width(), endBounds.height()) - .setPosition(sc, endBounds.left, endBounds.top); + .setPosition(sc, endPosition.x, endPosition.y); } startTransaction.apply(); diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt index 1ccc7d8084a6..5f25d70acf7c 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt @@ -24,6 +24,7 @@ import android.tools.flicker.legacy.LegacyFlickerTest import android.tools.flicker.legacy.LegacyFlickerTestFactory import android.tools.helpers.WindowUtils import android.tools.traces.parsers.toFlickerComponent +import androidx.test.filters.FlakyTest import androidx.test.filters.RequiresDevice import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.server.wm.flicker.testapp.ActivityOptions @@ -181,6 +182,12 @@ class FromSplitScreenEnterPipOnUserLeaveHintTest(flicker: LegacyFlickerTest) : } } + /** {@inheritDoc} */ + @FlakyTest(bugId = 312446524) + @Test + override fun visibleLayersShownMoreThanOneConsecutiveEntry() = + super.visibleLayersShownMoreThanOneConsecutiveEntry() + companion object { @Parameterized.Parameters(name = "{0}") @JvmStatic 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 254bf7da08a6..4fbf2bddb7b2 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 @@ -833,7 +833,7 @@ class DesktopTasksControllerTest : ShellTestCase() { verify(launchAdjacentController).launchAdjacentEnabled = true } @Test - fun enterDesktop_fullscreenTaskIsMovedToDesktop() { + fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop() { val task1 = setUpFullscreenTask() val task2 = setUpFullscreenTask() val task3 = setUpFullscreenTask() @@ -842,7 +842,7 @@ class DesktopTasksControllerTest : ShellTestCase() { task2.isFocused = false task3.isFocused = false - controller.enterDesktop(DEFAULT_DISPLAY) + controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY) val wct = getLatestMoveToDesktopWct() assertThat(wct.changes[task1.token.asBinder()]?.windowingMode) @@ -850,7 +850,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun enterDesktop_splitScreenTaskIsMovedToDesktop() { + fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop() { val task1 = setUpSplitScreenTask() val task2 = setUpFullscreenTask() val task3 = setUpFullscreenTask() @@ -863,7 +863,7 @@ class DesktopTasksControllerTest : ShellTestCase() { task4.parentTaskId = task1.taskId - controller.enterDesktop(DEFAULT_DISPLAY) + controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY) val wct = getLatestMoveToDesktopWct() assertThat(wct.changes[task4.token.asBinder()]?.windowingMode) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt index ce7b63322b4a..9174556d091b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/FluidResizeTaskPositionerTest.kt @@ -2,6 +2,7 @@ package com.android.wm.shell.windowdecor import android.app.ActivityManager import android.app.WindowConfiguration +import android.graphics.Point import android.graphics.Rect import android.os.IBinder import android.testing.AndroidTestingRunner @@ -11,6 +12,7 @@ import android.view.Surface.ROTATION_270 import android.view.Surface.ROTATION_90 import android.view.SurfaceControl import android.view.WindowManager +import android.window.TransitionInfo import android.window.WindowContainerToken import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.Change.CHANGE_DRAG_RESIZING @@ -41,6 +43,8 @@ import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import org.mockito.kotlin.doReturn import java.util.function.Supplier +import org.mockito.Mockito.eq +import org.mockito.Mockito.mock import org.mockito.Mockito.`when` as whenever /** @@ -575,6 +579,32 @@ class FluidResizeTaskPositionerTest : ShellTestCase() { }) } + @Test + fun testStartAnimation_useEndRelOffset() { + val mockTransitionInfo = mock(TransitionInfo::class.java) + val changeMock = mock(TransitionInfo.Change::class.java) + val startTransaction = mock(SurfaceControl.Transaction::class.java) + val finishTransaction = mock(SurfaceControl.Transaction::class.java) + val point = Point(10, 20) + val bounds = Rect(1, 2, 3, 4) + `when`(changeMock.endRelOffset).thenReturn(point) + `when`(changeMock.endAbsBounds).thenReturn(bounds) + `when`(mockTransitionInfo.changes).thenReturn(listOf(changeMock)) + `when`(startTransaction.setWindowCrop(any(), + eq(bounds.width()), + eq(bounds.height()))).thenReturn(startTransaction) + `when`(finishTransaction.setWindowCrop(any(), + eq(bounds.width()), + eq(bounds.height()))).thenReturn(finishTransaction) + + taskPositioner.startAnimation(mockTransitionBinder, mockTransitionInfo, startTransaction, + finishTransaction, { _ -> }) + + verify(startTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat())) + verify(finishTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat())) + verify(changeMock).endRelOffset + } + private fun WindowContainerTransaction.Change.ofBounds(bounds: Rect): Boolean { return ((windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0) && bounds == configuration.windowConfiguration.bounds diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt index 7f6e538f0bbf..a9f44929fc64 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositionerTest.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.windowdecor import android.app.ActivityManager import android.app.WindowConfiguration +import android.graphics.Point import android.graphics.Rect import android.os.IBinder import android.testing.AndroidTestingRunner @@ -25,6 +26,7 @@ import android.view.Surface.ROTATION_0 import android.view.Surface.ROTATION_270 import android.view.Surface.ROTATION_90 import android.view.SurfaceControl +import android.view.SurfaceControl.Transaction import android.view.WindowManager.TRANSIT_CHANGE import android.window.TransitionInfo import android.window.WindowContainerToken @@ -39,6 +41,7 @@ import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED +import java.util.function.Supplier import junit.framework.Assert import org.junit.Before import org.junit.Test @@ -47,13 +50,13 @@ import org.mockito.Mock import org.mockito.Mockito.any import org.mockito.Mockito.argThat import org.mockito.Mockito.eq +import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` -import org.mockito.MockitoAnnotations -import java.util.function.Supplier import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations /** * Tests for [VeiledResizeTaskPositioner]. @@ -439,6 +442,40 @@ class VeiledResizeTaskPositionerTest : ShellTestCase() { Assert.assertFalse(taskPositioner.isResizingOrAnimating) } + @Test + fun testStartAnimation_useEndRelOffset() { + val changeMock = mock(TransitionInfo.Change::class.java) + val startTransaction = mock(Transaction::class.java) + val finishTransaction = mock(Transaction::class.java) + val point = Point(10, 20) + val bounds = Rect(1, 2, 3, 4) + `when`(changeMock.endRelOffset).thenReturn(point) + `when`(changeMock.endAbsBounds).thenReturn(bounds) + `when`(mockTransitionInfo.changes).thenReturn(listOf(changeMock)) + `when`(startTransaction.setWindowCrop( + any(), + eq(bounds.width()), + eq(bounds.height()) + )).thenReturn(startTransaction) + `when`(finishTransaction.setWindowCrop( + any(), + eq(bounds.width()), + eq(bounds.height()) + )).thenReturn(finishTransaction) + + taskPositioner.startAnimation( + mockTransitionBinder, + mockTransitionInfo, + startTransaction, + finishTransaction, + mockFinishCallback + ) + + verify(startTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat())) + verify(finishTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat())) + verify(changeMock).endRelOffset + } + private fun performDrag( startX: Float, startY: Float, diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 6f7024ae76b4..1fe3c2ecec29 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -5453,7 +5453,8 @@ public class AudioManager { String regId = service.registerAudioPolicy(policy.getConfig(), policy.cb(), policy.hasFocusListener(), policy.isFocusPolicy(), policy.isTestFocusPolicy(), policy.isVolumeController(), - projection == null ? null : projection.getProjection()); + projection == null ? null : projection.getProjection(), + policy.getAttributionSource()); if (regId == null) { return ERROR; } else { diff --git a/media/java/android/media/AudioRecord.java b/media/java/android/media/AudioRecord.java index 447d3bbddceb..80e57193d0dc 100644 --- a/media/java/android/media/AudioRecord.java +++ b/media/java/android/media/AudioRecord.java @@ -789,7 +789,7 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection, private @NonNull AudioRecord buildAudioPlaybackCaptureRecord() { AudioMix audioMix = mAudioPlaybackCaptureConfiguration.createAudioMix(mFormat); MediaProjection projection = mAudioPlaybackCaptureConfiguration.getMediaProjection(); - AudioPolicy audioPolicy = new AudioPolicy.Builder(/*context=*/ null) + AudioPolicy audioPolicy = new AudioPolicy.Builder(/*context=*/ mContext) .setMediaProjection(projection) .addMix(audioMix).build(); @@ -853,7 +853,7 @@ public class AudioRecord implements AudioRouting, MicrophoneDirection, .setFormat(mFormat) .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK) .build(); - AudioPolicy audioPolicy = new AudioPolicy.Builder(null).addMix(audioMix).build(); + AudioPolicy audioPolicy = new AudioPolicy.Builder(mContext).addMix(audioMix).build(); if (AudioManager.registerAudioPolicyStatic(audioPolicy) != 0) { throw new UnsupportedOperationException("Error: could not register audio policy"); } diff --git a/media/java/android/media/AudioTrack.java b/media/java/android/media/AudioTrack.java index 194da217a121..73deb17d0055 100644 --- a/media/java/android/media/AudioTrack.java +++ b/media/java/android/media/AudioTrack.java @@ -1353,7 +1353,8 @@ public class AudioTrack extends PlayerBase .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK) .build(); AudioPolicy audioPolicy = - new AudioPolicy.Builder(/*context=*/ null).addMix(audioMix).build(); + new AudioPolicy.Builder(/*context=*/ mContext).addMix(audioMix).build(); + if (AudioManager.registerAudioPolicyStatic(audioPolicy) != 0) { throw new UnsupportedOperationException("Error: could not register audio policy"); } diff --git a/media/java/android/media/IAudioService.aidl b/media/java/android/media/IAudioService.aidl index 98bd3caf3f7d..e612645fb4d7 100644 --- a/media/java/android/media/IAudioService.aidl +++ b/media/java/android/media/IAudioService.aidl @@ -18,6 +18,7 @@ package android.media; import android.bluetooth.BluetoothDevice; import android.content.ComponentName; +import android.content.AttributionSource; import android.media.AudioAttributes; import android.media.AudioDeviceAttributes; import android.media.AudioFormat; @@ -361,7 +362,8 @@ interface IAudioService { String registerAudioPolicy(in AudioPolicyConfig policyConfig, in IAudioPolicyCallback pcb, boolean hasFocusListener, boolean isFocusPolicy, boolean isTestFocusPolicy, - boolean isVolumeController, in IMediaProjection projection); + boolean isVolumeController, in IMediaProjection projection, + in AttributionSource attributionSource); oneway void unregisterAudioPolicyAsync(in IAudioPolicyCallback pcb); diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java index ab7c27f70e05..2d7db5e6ed94 100644 --- a/media/java/android/media/MediaCas.java +++ b/media/java/android/media/MediaCas.java @@ -35,6 +35,7 @@ import android.media.tv.tunerresourcemanager.TunerResourceManager; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; +import android.os.IBinder; import android.os.IHwBinder; import android.os.Looper; import android.os.Message; @@ -43,7 +44,6 @@ import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceSpecificException; import android.util.Log; -import android.util.Singleton; import com.android.internal.util.FrameworkStatsLog; @@ -264,71 +264,107 @@ public final class MediaCas implements AutoCloseable { public static final int PLUGIN_STATUS_SESSION_NUMBER_CHANGED = android.hardware.cas.StatusEvent.PLUGIN_SESSION_NUMBER_CHANGED; - private static final Singleton<IMediaCasService> sService = - new Singleton<IMediaCasService>() { + private static IMediaCasService sService = null; + private static Object sAidlLock = new Object(); + + /** DeathListener for AIDL service */ + private static IBinder.DeathRecipient sDeathListener = + new IBinder.DeathRecipient() { @Override - protected IMediaCasService create() { - try { - Log.d(TAG, "Trying to get AIDL service"); - IMediaCasService serviceAidl = - IMediaCasService.Stub.asInterface( - ServiceManager.waitForDeclaredService( - IMediaCasService.DESCRIPTOR + "/default")); - if (serviceAidl != null) { - return serviceAidl; - } - } catch (Exception eAidl) { - Log.d(TAG, "Failed to get cas AIDL service"); + public void binderDied() { + synchronized (sAidlLock) { + Log.d(TAG, "The service is dead"); + sService.asBinder().unlinkToDeath(sDeathListener, 0); + sService = null; } - return null; } }; - private static final Singleton<android.hardware.cas.V1_0.IMediaCasService> sServiceHidl = - new Singleton<android.hardware.cas.V1_0.IMediaCasService>() { - @Override - protected android.hardware.cas.V1_0.IMediaCasService create() { - try { - Log.d(TAG, "Trying to get cas@1.2 service"); - android.hardware.cas.V1_2.IMediaCasService serviceV12 = - android.hardware.cas.V1_2.IMediaCasService.getService( - true /*wait*/); - if (serviceV12 != null) { - return serviceV12; - } - } catch (Exception eV1_2) { - Log.d(TAG, "Failed to get cas@1.2 service"); + static IMediaCasService getService() { + synchronized (sAidlLock) { + if (sService == null || !sService.asBinder().isBinderAlive()) { + try { + Log.d(TAG, "Trying to get AIDL service"); + sService = + IMediaCasService.Stub.asInterface( + ServiceManager.waitForDeclaredService( + IMediaCasService.DESCRIPTOR + "/default")); + if (sService != null) { + sService.asBinder().linkToDeath(sDeathListener, 0); } + } catch (Exception eAidl) { + Log.d(TAG, "Failed to get cas AIDL service"); + } + } + return sService; + } + } - try { - Log.d(TAG, "Trying to get cas@1.1 service"); - android.hardware.cas.V1_1.IMediaCasService serviceV11 = - android.hardware.cas.V1_1.IMediaCasService.getService( - true /*wait*/); - if (serviceV11 != null) { - return serviceV11; + private static android.hardware.cas.V1_0.IMediaCasService sServiceHidl = null; + private static Object sHidlLock = new Object(); + + /** Used to indicate the right end-point to handle the serviceDied method */ + private static final long MEDIA_CAS_HIDL_COOKIE = 394; + + /** DeathListener for HIDL service */ + private static IHwBinder.DeathRecipient sDeathListenerHidl = + new IHwBinder.DeathRecipient() { + @Override + public void serviceDied(long cookie) { + if (cookie == MEDIA_CAS_HIDL_COOKIE) { + synchronized (sHidlLock) { + sServiceHidl = null; } - } catch (Exception eV1_1) { - Log.d(TAG, "Failed to get cas@1.1 service"); } + } + }; - try { - Log.d(TAG, "Trying to get cas@1.0 service"); - return android.hardware.cas.V1_0.IMediaCasService.getService(true /*wait*/); - } catch (Exception eV1_0) { - Log.d(TAG, "Failed to get cas@1.0 service"); + static android.hardware.cas.V1_0.IMediaCasService getServiceHidl() { + synchronized (sHidlLock) { + if (sServiceHidl != null) { + return sServiceHidl; + } else { + try { + Log.d(TAG, "Trying to get cas@1.2 service"); + android.hardware.cas.V1_2.IMediaCasService serviceV12 = + android.hardware.cas.V1_2.IMediaCasService.getService(true /*wait*/); + if (serviceV12 != null) { + sServiceHidl = serviceV12; + sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE); + return sServiceHidl; } - - return null; + } catch (Exception eV1_2) { + Log.d(TAG, "Failed to get cas@1.2 service"); } - }; - static IMediaCasService getService() { - return sService.get(); - } + try { + Log.d(TAG, "Trying to get cas@1.1 service"); + android.hardware.cas.V1_1.IMediaCasService serviceV11 = + android.hardware.cas.V1_1.IMediaCasService.getService(true /*wait*/); + if (serviceV11 != null) { + sServiceHidl = serviceV11; + sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE); + return sServiceHidl; + } + } catch (Exception eV1_1) { + Log.d(TAG, "Failed to get cas@1.1 service"); + } - static android.hardware.cas.V1_0.IMediaCasService getServiceHidl() { - return sServiceHidl.get(); + try { + Log.d(TAG, "Trying to get cas@1.0 service"); + sServiceHidl = + android.hardware.cas.V1_0.IMediaCasService.getService(true /*wait*/); + if (sServiceHidl != null) { + sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE); + } + return sServiceHidl; + } catch (Exception eV1_0) { + Log.d(TAG, "Failed to get cas@1.0 service"); + } + } + } + // Couldn't find an HIDL service, returning null. + return null; } private void validateInternalStates() { @@ -756,7 +792,7 @@ public final class MediaCas implements AutoCloseable { * @return Whether the specified CA system is supported on this device. */ public static boolean isSystemIdSupported(int CA_system_id) { - IMediaCasService service = sService.get(); + IMediaCasService service = getService(); if (service != null) { try { return service.isSystemIdSupported(CA_system_id); @@ -765,7 +801,7 @@ public final class MediaCas implements AutoCloseable { } } - android.hardware.cas.V1_0.IMediaCasService serviceHidl = sServiceHidl.get(); + android.hardware.cas.V1_0.IMediaCasService serviceHidl = getServiceHidl(); if (serviceHidl != null) { try { return serviceHidl.isSystemIdSupported(CA_system_id); @@ -781,7 +817,7 @@ public final class MediaCas implements AutoCloseable { * @return an array of descriptors for the available CA plugins. */ public static PluginDescriptor[] enumeratePlugins() { - IMediaCasService service = sService.get(); + IMediaCasService service = getService(); if (service != null) { try { AidlCasPluginDescriptor[] descriptors = service.enumeratePlugins(); @@ -794,10 +830,11 @@ public final class MediaCas implements AutoCloseable { } return results; } catch (RemoteException e) { + Log.e(TAG, "Some exception while enumerating plugins"); } } - android.hardware.cas.V1_0.IMediaCasService serviceHidl = sServiceHidl.get(); + android.hardware.cas.V1_0.IMediaCasService serviceHidl = getServiceHidl(); if (serviceHidl != null) { try { ArrayList<HidlCasPluginDescriptor> descriptors = serviceHidl.enumeratePlugins(); diff --git a/media/java/android/media/audiopolicy/AudioMix.java b/media/java/android/media/audiopolicy/AudioMix.java index a53a8ce79354..e4eaaa317b3d 100644 --- a/media/java/android/media/audiopolicy/AudioMix.java +++ b/media/java/android/media/audiopolicy/AudioMix.java @@ -24,6 +24,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; +import android.content.Context; import android.media.AudioDeviceInfo; import android.media.AudioFormat; import android.media.AudioSystem; @@ -67,12 +68,19 @@ public class AudioMix implements Parcelable { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) final int mDeviceSystemType; // an AudioSystem.DEVICE_* value, not AudioDeviceInfo.TYPE_* + // The (virtual) device ID that this AudioMix was registered for. This value is overwritten + // when registering this AudioMix with an AudioPolicy or attaching this AudioMix to an + // AudioPolicy to match the AudioPolicy attribution. Does not imply that it only modifies + // audio routing for this device ID. + private int mVirtualDeviceId; + /** * All parameters are guaranteed valid through the Builder. */ private AudioMix(@NonNull AudioMixingRule rule, @NonNull AudioFormat format, int routeFlags, int callbackFlags, - int deviceType, @Nullable String deviceAddress, IBinder token) { + int deviceType, @Nullable String deviceAddress, IBinder token, + int virtualDeviceId) { mRule = Objects.requireNonNull(rule); mFormat = Objects.requireNonNull(format); mRouteFlags = routeFlags; @@ -81,6 +89,7 @@ public class AudioMix implements Parcelable { mDeviceSystemType = deviceType; mDeviceAddress = (deviceAddress == null) ? new String("") : deviceAddress; mToken = token; + mVirtualDeviceId = virtualDeviceId; } // CALLBACK_FLAG_* values: keep in sync with AudioMix::kCbFlag* values defined @@ -269,6 +278,11 @@ public class AudioMix implements Parcelable { } /** @hide */ + public boolean matchesVirtualDeviceId(int deviceId) { + return mVirtualDeviceId == deviceId; + } + + /** @hide */ @Override public boolean equals(Object o) { if (this == o) return true; @@ -311,6 +325,7 @@ public class AudioMix implements Parcelable { mFormat.writeToParcel(dest, flags); mRule.writeToParcel(dest, flags); dest.writeStrongBinder(mToken); + dest.writeInt(mVirtualDeviceId); } public static final @NonNull Parcelable.Creator<AudioMix> CREATOR = new Parcelable.Creator<>() { @@ -331,6 +346,7 @@ public class AudioMix implements Parcelable { mixBuilder.setFormat(AudioFormat.CREATOR.createFromParcel(p)); mixBuilder.setMixingRule(AudioMixingRule.CREATOR.createFromParcel(p)); mixBuilder.setToken(p.readStrongBinder()); + mixBuilder.setVirtualDeviceId(p.readInt()); return mixBuilder.build(); } @@ -339,6 +355,15 @@ public class AudioMix implements Parcelable { } }; + /** + * Updates the deviceId of the AudioMix to match with the AudioPolicy the mix is registered + * through. + * @hide + */ + public void setVirtualDeviceId(int virtualDeviceId) { + mVirtualDeviceId = virtualDeviceId; + } + /** @hide */ @IntDef(flag = true, value = { ROUTE_FLAG_RENDER, ROUTE_FLAG_LOOP_BACK } ) @@ -354,6 +379,7 @@ public class AudioMix implements Parcelable { private int mRouteFlags = 0; private int mCallbackFlags = 0; private IBinder mToken = null; + private int mVirtualDeviceId = Context.DEVICE_ID_DEFAULT; // an AudioSystem.DEVICE_* value, not AudioDeviceInfo.TYPE_* private int mDeviceSystemType = AudioSystem.DEVICE_NONE; private String mDeviceAddress = null; @@ -404,6 +430,15 @@ public class AudioMix implements Parcelable { /** * @hide + * Only used by AudioMix internally. + */ + Builder setVirtualDeviceId(int virtualDeviceId) { + mVirtualDeviceId = virtualDeviceId; + return this; + } + + /** + * @hide * Only used by AudioPolicyConfig, not a public API. * @param callbackFlags which callbacks are called from native * @return the same Builder instance. @@ -570,7 +605,7 @@ public class AudioMix implements Parcelable { } return new AudioMix(mRule, mFormat, mRouteFlags, mCallbackFlags, mDeviceSystemType, - mDeviceAddress, mToken); + mDeviceAddress, mToken, mVirtualDeviceId); } private int getLoopbackDeviceSystemTypeForAudioMixingRule(AudioMixingRule rule) { diff --git a/media/java/android/media/audiopolicy/AudioPolicy.java b/media/java/android/media/audiopolicy/AudioPolicy.java index 508c0a2b9a21..293a8f89fbca 100644 --- a/media/java/android/media/audiopolicy/AudioPolicy.java +++ b/media/java/android/media/audiopolicy/AudioPolicy.java @@ -27,6 +27,7 @@ import android.annotation.SystemApi; import android.annotation.TestApi; import android.annotation.UserIdInt; import android.app.ActivityManager; +import android.content.AttributionSource; import android.content.Context; import android.content.pm.PackageManager; import android.media.AudioAttributes; @@ -146,6 +147,16 @@ public class AudioPolicy { return mProjection; } + /** @hide */ + public AttributionSource getAttributionSource() { + return getAttributionSource(mContext); + } + + private static AttributionSource getAttributionSource(Context context) { + return context == null + ? AttributionSource.myAttributionSource() : context.getAttributionSource(); + } + /** * The parameters are guaranteed non-null through the Builder */ @@ -208,6 +219,9 @@ public class AudioPolicy { if (mix == null) { throw new IllegalArgumentException("Illegal null AudioMix argument"); } + if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) { + mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId()); + } mMixes.add(mix); return this; } @@ -358,6 +372,9 @@ public class AudioPolicy { if (mix == null) { throw new IllegalArgumentException("Illegal null AudioMix in attachMixes"); } else { + if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) { + mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId()); + } zeMixes.add(mix); } } @@ -400,6 +417,9 @@ public class AudioPolicy { if (mix == null) { throw new IllegalArgumentException("Illegal null AudioMix in detachMixes"); } else { + if (android.permission.flags.Flags.deviceAwarePermissionApisEnabled()) { + mix.setVirtualDeviceId(getAttributionSource(mContext).getDeviceId()); + } zeMixes.add(mix); } } diff --git a/native/android/OWNERS b/native/android/OWNERS index 0b86909929b0..9a3527da9623 100644 --- a/native/android/OWNERS +++ b/native/android/OWNERS @@ -16,6 +16,8 @@ per-file system_fonts.cpp = file:/graphics/java/android/graphics/fonts/OWNERS per-file native_window_jni.cpp = file:/services/core/java/com/android/server/wm/OWNERS per-file native_activity.cpp = file:/services/core/java/com/android/server/wm/OWNERS per-file surface_control.cpp = file:/services/core/java/com/android/server/wm/OWNERS +per-file surface_control_input_receiver.cpp = file:/services/core/java/com/android/server/wm/OWNERS +per-file input_transfer_token.cpp = file:/services/core/java/com/android/server/wm/OWNERS # Graphics per-file choreographer.cpp = file:/graphics/java/android/graphics/OWNERS diff --git a/native/android/surface_control_input_receiver.cpp b/native/android/surface_control_input_receiver.cpp index d178abc2c3d7..a84ec7309a62 100644 --- a/native/android/surface_control_input_receiver.cpp +++ b/native/android/surface_control_input_receiver.cpp @@ -192,7 +192,9 @@ const AInputTransferToken* AInputReceiver_getInputTransferToken(AInputReceiver* void AInputReceiver_release(AInputReceiver* aInputReceiver) { InputReceiver* inputReceiver = AInputReceiver_to_InputReceiver(aInputReceiver); - inputReceiver->remove(); + if (inputReceiver != nullptr) { + inputReceiver->remove(); + } delete inputReceiver; } diff --git a/nfc/Android.bp b/nfc/Android.bp index 7698e2b2d054..ca10949ec77b 100644 --- a/nfc/Android.bp +++ b/nfc/Android.bp @@ -50,7 +50,7 @@ java_sdk_library { ], defaults: ["framework-module-defaults"], sdk_version: "module_current", - min_sdk_version: "34", // should be 35 (making it 34 for compiling for `-next`) + min_sdk_version: "VanillaIceCream", installable: true, optimize: { enabled: false, diff --git a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java index be3c24806c5b..a353df743520 100644 --- a/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java +++ b/nfc/java/android/nfc/cardemulation/ApduServiceInfo.java @@ -723,6 +723,7 @@ public final class ApduServiceInfo implements Parcelable { * delivered to {@link HostApduService#processPollingFrames(List)}. Adding a key with this * multiple times will cause the value to be overwritten each time. * @param pollingLoopFilter the polling loop filter to add, must be a valid hexadecimal string + * @param autoTransact whether Observe Mode should be disabled when this filter matches or not */ @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) public void addPollingLoopFilter(@NonNull String pollingLoopFilter, @@ -747,6 +748,7 @@ public final class ApduServiceInfo implements Parcelable { * multiple times will cause the value to be overwritten each time. * @param pollingLoopPatternFilter the polling loop pattern filter to add, must be a valid * regex to match a hexadecimal string + * @param autoTransact whether Observe Mode should be disabled when this filter matches or not */ @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) public void addPollingLoopPatternFilter(@NonNull String pollingLoopPatternFilter, diff --git a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java index 37b5d408a508..a8d8f9a1a55d 100644 --- a/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java +++ b/packages/CrashRecovery/services/java/com/android/server/PackageWatchdog.java @@ -26,6 +26,7 @@ import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; +import android.crashrecovery.flags.Flags; import android.net.ConnectivityModuleConnector; import android.os.Environment; import android.os.Handler; @@ -57,16 +58,20 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -130,8 +135,25 @@ public class PackageWatchdog { @VisibleForTesting static final int DEFAULT_BOOT_LOOP_TRIGGER_COUNT = 5; - @VisibleForTesting + static final long DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS = TimeUnit.MINUTES.toMillis(10); + // Boot loop at which packageWatchdog starts first mitigation + private static final String BOOT_LOOP_THRESHOLD = + "persist.device_config.configuration.boot_loop_threshold"; + @VisibleForTesting + static final int DEFAULT_BOOT_LOOP_THRESHOLD = 15; + // Once boot_loop_threshold is surpassed next mitigation would be triggered after + // specified number of reboots. + private static final String BOOT_LOOP_MITIGATION_INCREMENT = + "persist.device_config.configuration..boot_loop_mitigation_increment"; + @VisibleForTesting + static final int DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT = 2; + + // Threshold level at which or above user might experience significant disruption. + private static final String MAJOR_USER_IMPACT_LEVEL_THRESHOLD = + "persist.device_config.configuration.major_user_impact_level_threshold"; + private static final int DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD = + PackageHealthObserverImpact.USER_IMPACT_LEVEL_71; private long mNumberOfNativeCrashPollsRemaining; @@ -145,6 +167,7 @@ public class PackageWatchdog { private static final String ATTR_EXPLICIT_HEALTH_CHECK_DURATION = "health-check-duration"; private static final String ATTR_PASSED_HEALTH_CHECK = "passed-health-check"; private static final String ATTR_MITIGATION_CALLS = "mitigation-calls"; + private static final String ATTR_MITIGATION_COUNT = "mitigation-count"; // A file containing information about the current mitigation count in the case of a boot loop. // This allows boot loop information to persist in the case of an fs-checkpoint being @@ -230,8 +253,16 @@ public class PackageWatchdog { mConnectivityModuleConnector = connectivityModuleConnector; mSystemClock = clock; mNumberOfNativeCrashPollsRemaining = NUMBER_OF_NATIVE_CRASH_POLLS; - mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT, - DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS); + if (Flags.recoverabilityDetection()) { + mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS, + SystemProperties.getInt(BOOT_LOOP_MITIGATION_INCREMENT, + DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT)); + } else { + mBootThreshold = new BootThreshold(DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS); + } + loadFromFile(); sPackageWatchdog = this; } @@ -436,8 +467,13 @@ public class PackageWatchdog { mitigationCount = currentMonitoredPackage.getMitigationCountLocked(); } - currentObserverToNotify.execute(versionedPackage, - failureReason, mitigationCount); + if (Flags.recoverabilityDetection()) { + maybeExecute(currentObserverToNotify, versionedPackage, + failureReason, currentObserverImpact, mitigationCount); + } else { + currentObserverToNotify.execute(versionedPackage, + failureReason, mitigationCount); + } } } } @@ -467,37 +503,76 @@ public class PackageWatchdog { } } if (currentObserverToNotify != null) { - currentObserverToNotify.execute(failingPackage, failureReason, 1); + if (Flags.recoverabilityDetection()) { + maybeExecute(currentObserverToNotify, failingPackage, failureReason, + currentObserverImpact, /*mitigationCount=*/ 1); + } else { + currentObserverToNotify.execute(failingPackage, failureReason, 1); + } + } + } + + private void maybeExecute(PackageHealthObserver currentObserverToNotify, + VersionedPackage versionedPackage, + @FailureReasons int failureReason, + int currentObserverImpact, + int mitigationCount) { + if (currentObserverImpact < getUserImpactLevelLimit()) { + currentObserverToNotify.execute(versionedPackage, failureReason, mitigationCount); } } + /** * Called when the system server boots. If the system server is detected to be in a boot loop, * query each observer and perform the mitigation action with the lowest user impact. */ + @SuppressWarnings("GuardedBy") public void noteBoot() { synchronized (mLock) { - if (mBootThreshold.incrementAndTest()) { - mBootThreshold.reset(); + boolean mitigate = mBootThreshold.incrementAndTest(); + if (mitigate) { + if (!Flags.recoverabilityDetection()) { + mBootThreshold.reset(); + } int mitigationCount = mBootThreshold.getMitigationCount() + 1; PackageHealthObserver currentObserverToNotify = null; + ObserverInternal currentObserverInternal = null; int currentObserverImpact = Integer.MAX_VALUE; for (int i = 0; i < mAllObservers.size(); i++) { final ObserverInternal observer = mAllObservers.valueAt(i); PackageHealthObserver registeredObserver = observer.registeredObserver; if (registeredObserver != null) { - int impact = registeredObserver.onBootLoop(mitigationCount); + int impact = Flags.recoverabilityDetection() + ? registeredObserver.onBootLoop( + observer.getBootMitigationCount() + 1) + : registeredObserver.onBootLoop(mitigationCount); if (impact != PackageHealthObserverImpact.USER_IMPACT_LEVEL_0 && impact < currentObserverImpact) { currentObserverToNotify = registeredObserver; + currentObserverInternal = observer; currentObserverImpact = impact; } } } if (currentObserverToNotify != null) { - mBootThreshold.setMitigationCount(mitigationCount); - mBootThreshold.saveMitigationCountToMetadata(); - currentObserverToNotify.executeBootLoopMitigation(mitigationCount); + if (Flags.recoverabilityDetection()) { + if (currentObserverImpact < getUserImpactLevelLimit() + || (currentObserverImpact >= getUserImpactLevelLimit() + && mBootThreshold.getCount() >= getBootLoopThreshold())) { + int currentObserverMitigationCount = + currentObserverInternal.getBootMitigationCount() + 1; + currentObserverInternal.setBootMitigationCount( + currentObserverMitigationCount); + saveAllObserversBootMitigationCountToMetadata(METADATA_FILE); + currentObserverToNotify.executeBootLoopMitigation( + currentObserverMitigationCount); + } + } else { + mBootThreshold.setMitigationCount(mitigationCount); + mBootThreshold.saveMitigationCountToMetadata(); + currentObserverToNotify.executeBootLoopMitigation(mitigationCount); + } } } } @@ -567,13 +642,27 @@ public class PackageWatchdog { mShortTaskHandler.post(()->checkAndMitigateNativeCrashes()); } + private int getUserImpactLevelLimit() { + return SystemProperties.getInt(MAJOR_USER_IMPACT_LEVEL_THRESHOLD, + DEFAULT_MAJOR_USER_IMPACT_LEVEL_THRESHOLD); + } + + private int getBootLoopThreshold() { + return SystemProperties.getInt(BOOT_LOOP_THRESHOLD, + DEFAULT_BOOT_LOOP_THRESHOLD); + } + /** Possible severity values of the user impact of a {@link PackageHealthObserver#execute}. */ @Retention(SOURCE) @IntDef(value = {PackageHealthObserverImpact.USER_IMPACT_LEVEL_0, PackageHealthObserverImpact.USER_IMPACT_LEVEL_10, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_20, PackageHealthObserverImpact.USER_IMPACT_LEVEL_30, PackageHealthObserverImpact.USER_IMPACT_LEVEL_50, PackageHealthObserverImpact.USER_IMPACT_LEVEL_70, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_71, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_75, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_80, PackageHealthObserverImpact.USER_IMPACT_LEVEL_90, PackageHealthObserverImpact.USER_IMPACT_LEVEL_100}) public @interface PackageHealthObserverImpact { @@ -582,11 +671,15 @@ public class PackageWatchdog { /* Action has low user impact, user of a device will barely notice. */ int USER_IMPACT_LEVEL_10 = 10; /* Actions having medium user impact, user of a device will likely notice. */ + int USER_IMPACT_LEVEL_20 = 20; int USER_IMPACT_LEVEL_30 = 30; int USER_IMPACT_LEVEL_50 = 50; int USER_IMPACT_LEVEL_70 = 70; - int USER_IMPACT_LEVEL_90 = 90; /* Action has high user impact, a last resort, user of a device will be very frustrated. */ + int USER_IMPACT_LEVEL_71 = 71; + int USER_IMPACT_LEVEL_75 = 75; + int USER_IMPACT_LEVEL_80 = 80; + int USER_IMPACT_LEVEL_90 = 90; int USER_IMPACT_LEVEL_100 = 100; } @@ -1144,6 +1237,12 @@ public class PackageWatchdog { } } + @VisibleForTesting + @GuardedBy("mLock") + void registerObserverInternal(ObserverInternal observerInternal) { + mAllObservers.put(observerInternal.name, observerInternal); + } + /** * Represents an observer monitoring a set of packages along with the failure thresholds for * each package. @@ -1151,17 +1250,23 @@ public class PackageWatchdog { * <p> Note, the PackageWatchdog#mLock must always be held when reading or writing * instances of this class. */ - private static class ObserverInternal { + static class ObserverInternal { public final String name; @GuardedBy("mLock") private final ArrayMap<String, MonitoredPackage> mPackages = new ArrayMap<>(); @Nullable @GuardedBy("mLock") public PackageHealthObserver registeredObserver; + private int mMitigationCount; ObserverInternal(String name, List<MonitoredPackage> packages) { + this(name, packages, /*mitigationCount=*/ 0); + } + + ObserverInternal(String name, List<MonitoredPackage> packages, int mitigationCount) { this.name = name; updatePackagesLocked(packages); + this.mMitigationCount = mitigationCount; } /** @@ -1173,6 +1278,9 @@ public class PackageWatchdog { try { out.startTag(null, TAG_OBSERVER); out.attribute(null, ATTR_NAME, name); + if (Flags.recoverabilityDetection()) { + out.attributeInt(null, ATTR_MITIGATION_COUNT, mMitigationCount); + } for (int i = 0; i < mPackages.size(); i++) { MonitoredPackage p = mPackages.valueAt(i); p.writeLocked(out); @@ -1185,6 +1293,14 @@ public class PackageWatchdog { } } + public int getBootMitigationCount() { + return mMitigationCount; + } + + public void setBootMitigationCount(int mitigationCount) { + mMitigationCount = mitigationCount; + } + @GuardedBy("mLock") public void updatePackagesLocked(List<MonitoredPackage> packages) { for (int pIndex = 0; pIndex < packages.size(); pIndex++) { @@ -1289,6 +1405,7 @@ public class PackageWatchdog { **/ public static ObserverInternal read(TypedXmlPullParser parser, PackageWatchdog watchdog) { String observerName = null; + int observerMitigationCount = 0; if (TAG_OBSERVER.equals(parser.getName())) { observerName = parser.getAttributeValue(null, ATTR_NAME); if (TextUtils.isEmpty(observerName)) { @@ -1299,6 +1416,9 @@ public class PackageWatchdog { List<MonitoredPackage> packages = new ArrayList<>(); int innerDepth = parser.getDepth(); try { + if (Flags.recoverabilityDetection()) { + observerMitigationCount = parser.getAttributeInt(null, ATTR_MITIGATION_COUNT); + } while (XmlUtils.nextElementWithin(parser, innerDepth)) { if (TAG_PACKAGE.equals(parser.getName())) { try { @@ -1319,7 +1439,7 @@ public class PackageWatchdog { if (packages.isEmpty()) { return null; } - return new ObserverInternal(observerName, packages); + return new ObserverInternal(observerName, packages, observerMitigationCount); } /** Dumps information about this observer and the packages it watches. */ @@ -1679,6 +1799,27 @@ public class PackageWatchdog { } } + @GuardedBy("mLock") + @SuppressWarnings("GuardedBy") + void saveAllObserversBootMitigationCountToMetadata(String filePath) { + HashMap<String, Integer> bootMitigationCounts = new HashMap<>(); + for (int i = 0; i < mAllObservers.size(); i++) { + final ObserverInternal observer = mAllObservers.valueAt(i); + bootMitigationCounts.put(observer.name, observer.getBootMitigationCount()); + } + + try { + FileOutputStream fileStream = new FileOutputStream(new File(filePath)); + ObjectOutputStream 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); + } + } + /** * Handles the thresholding logic for system server boots. */ @@ -1686,10 +1827,16 @@ public class PackageWatchdog { private final int mBootTriggerCount; private final long mTriggerWindow; + private final int mBootMitigationIncrement; BootThreshold(int bootTriggerCount, long triggerWindow) { + this(bootTriggerCount, triggerWindow, /*bootMitigationIncrement=*/ 1); + } + + BootThreshold(int bootTriggerCount, long triggerWindow, int bootMitigationIncrement) { this.mBootTriggerCount = bootTriggerCount; this.mTriggerWindow = triggerWindow; + this.mBootMitigationIncrement = bootMitigationIncrement; } public void reset() { @@ -1761,8 +1908,13 @@ public class PackageWatchdog { /** Increments the boot counter, and returns whether the device is bootlooping. */ + @GuardedBy("mLock") public boolean incrementAndTest() { - readMitigationCountFromMetadataIfNecessary(); + if (Flags.recoverabilityDetection()) { + readAllObserversBootMitigationCountIfNecessary(METADATA_FILE); + } else { + readMitigationCountFromMetadataIfNecessary(); + } final long now = mSystemClock.uptimeMillis(); if (now - getStart() < 0) { Slog.e(TAG, "Window was less than zero. Resetting start to current time."); @@ -1770,8 +1922,12 @@ public class PackageWatchdog { setMitigationStart(now); } if (now - getMitigationStart() > DEFAULT_DEESCALATION_WINDOW_MS) { - setMitigationCount(0); setMitigationStart(now); + if (Flags.recoverabilityDetection()) { + resetAllObserversBootMitigationCount(); + } else { + setMitigationCount(0); + } } final long window = now - getStart(); if (window >= mTriggerWindow) { @@ -1782,9 +1938,48 @@ public class PackageWatchdog { int count = getCount() + 1; setCount(count); EventLogTags.writeRescueNote(Process.ROOT_UID, count, window); + if (Flags.recoverabilityDetection()) { + boolean mitigate = (count >= mBootTriggerCount) + && (count - mBootTriggerCount) % mBootMitigationIncrement == 0; + return mitigate; + } return count >= mBootTriggerCount; } } + @GuardedBy("mLock") + private void resetAllObserversBootMitigationCount() { + for (int i = 0; i < mAllObservers.size(); i++) { + final ObserverInternal observer = mAllObservers.valueAt(i); + observer.setBootMitigationCount(0); + } + } + + @GuardedBy("mLock") + @SuppressWarnings("GuardedBy") + void readAllObserversBootMitigationCountIfNecessary(String filePath) { + File metadataFile = new File(filePath); + if (metadataFile.exists()) { + try { + FileInputStream fileStream = new FileInputStream(metadataFile); + ObjectInputStream objectStream = new ObjectInputStream(fileStream); + HashMap<String, Integer> 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); + } + } + } + } } diff --git a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java index 7bdc1a0e3ac7..7093ba42f40d 100644 --- a/packages/CrashRecovery/services/java/com/android/server/RescueParty.java +++ b/packages/CrashRecovery/services/java/com/android/server/RescueParty.java @@ -20,6 +20,7 @@ import static android.provider.DeviceConfig.Properties; import static com.android.server.pm.PackageManagerServiceUtils.logCriticalInfo; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.ContentResolver; @@ -27,6 +28,7 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; +import android.crashrecovery.flags.Flags; import android.os.Build; import android.os.Environment; import android.os.PowerManager; @@ -53,6 +55,8 @@ import com.android.server.am.SettingsToPropertiesMapper; import com.android.server.crashrecovery.proto.CrashRecoveryStatsLog; import java.io.File; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -89,6 +93,40 @@ public class RescueParty { @VisibleForTesting static final int LEVEL_FACTORY_RESET = 5; @VisibleForTesting + static final int RESCUE_LEVEL_NONE = 0; + @VisibleForTesting + static final int RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET = 1; + @VisibleForTesting + static final int RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET = 2; + @VisibleForTesting + static final int RESCUE_LEVEL_WARM_REBOOT = 3; + @VisibleForTesting + static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS = 4; + @VisibleForTesting + static final int RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES = 5; + @VisibleForTesting + static final int RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS = 6; + @VisibleForTesting + static final int RESCUE_LEVEL_FACTORY_RESET = 7; + + @IntDef(prefix = { "RESCUE_LEVEL_" }, value = { + RESCUE_LEVEL_NONE, + RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET, + RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET, + RESCUE_LEVEL_WARM_REBOOT, + RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS, + RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES, + RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS, + RESCUE_LEVEL_FACTORY_RESET + }) + @Retention(RetentionPolicy.SOURCE) + @interface RescueLevels {} + + @VisibleForTesting + static final String RESCUE_NON_REBOOT_LEVEL_LIMIT = "persist.sys.rescue_non_reboot_level_limit"; + @VisibleForTesting + static final int DEFAULT_RESCUE_NON_REBOOT_LEVEL_LIMIT = RESCUE_LEVEL_WARM_REBOOT - 1; + @VisibleForTesting static final String TAG = "RescueParty"; @VisibleForTesting static final long DEFAULT_OBSERVING_DURATION_MS = TimeUnit.DAYS.toMillis(2); @@ -347,11 +385,20 @@ public class RescueParty { } private static int getMaxRescueLevel(boolean mayPerformReboot) { - if (!mayPerformReboot - || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) { - return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS; + if (Flags.recoverabilityDetection()) { + if (!mayPerformReboot + || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) { + return SystemProperties.getInt(RESCUE_NON_REBOOT_LEVEL_LIMIT, + DEFAULT_RESCUE_NON_REBOOT_LEVEL_LIMIT); + } + return RESCUE_LEVEL_FACTORY_RESET; + } else { + if (!mayPerformReboot + || SystemProperties.getBoolean(PROP_DISABLE_FACTORY_RESET_FLAG, false)) { + return LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS; + } + return LEVEL_FACTORY_RESET; } - return LEVEL_FACTORY_RESET; } /** @@ -379,6 +426,46 @@ public class RescueParty { } } + /** + * Get the rescue level to perform if this is the n-th attempt at mitigating failure. + * When failedPackage is null then 1st and 2nd mitigation counts are redundant (scoped and + * all device config reset). Behaves as if one mitigation attempt was already done. + * + * @param mitigationCount the mitigation attempt number (1 = first attempt etc.). + * @param mayPerformReboot whether or not a reboot and factory reset may be performed + * for the given failure. + * @param failedPackage in case of bootloop this is null. + * @return the rescue level for the n-th mitigation attempt. + */ + private static @RescueLevels int getRescueLevel(int mitigationCount, boolean mayPerformReboot, + @Nullable VersionedPackage failedPackage) { + // Skipping RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET since it's not defined without a failed + // package. + if (failedPackage == null && mitigationCount > 0) { + mitigationCount += 1; + } + if (mitigationCount == 1) { + return RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET; + } else if (mitigationCount == 2) { + return RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET; + } else if (mitigationCount == 3) { + return Math.min(getMaxRescueLevel(mayPerformReboot), RESCUE_LEVEL_WARM_REBOOT); + } else if (mitigationCount == 4) { + return Math.min(getMaxRescueLevel(mayPerformReboot), + RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS); + } else if (mitigationCount == 5) { + return Math.min(getMaxRescueLevel(mayPerformReboot), + RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES); + } else if (mitigationCount == 6) { + return Math.min(getMaxRescueLevel(mayPerformReboot), + RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS); + } else if (mitigationCount >= 7) { + return Math.min(getMaxRescueLevel(mayPerformReboot), RESCUE_LEVEL_FACTORY_RESET); + } else { + return RESCUE_LEVEL_NONE; + } + } + private static void executeRescueLevel(Context context, @Nullable String failedPackage, int level) { Slog.w(TAG, "Attempting rescue level " + levelToString(level)); @@ -397,6 +484,15 @@ public class RescueParty { private static void executeRescueLevelInternal(Context context, int level, @Nullable String failedPackage) throws Exception { + if (Flags.recoverabilityDetection()) { + executeRescueLevelInternalNew(context, level, failedPackage); + } else { + executeRescueLevelInternalOld(context, level, failedPackage); + } + } + + private static void executeRescueLevelInternalOld(Context context, int level, @Nullable + String failedPackage) throws Exception { if (level <= LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS) { // Disabling flag resets on master branch for trunk stable launch. @@ -410,8 +506,6 @@ public class RescueParty { // Try our best to reset all settings possible, and once finished // rethrow any exception that we encountered Exception res = null; - Runnable runnable; - Thread thread; switch (level) { case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: try { @@ -453,21 +547,7 @@ public class RescueParty { } break; case LEVEL_WARM_REBOOT: - // Request the reboot from a separate thread to avoid deadlock on PackageWatchdog - // when device shutting down. - setRebootProperty(true); - runnable = () -> { - try { - PowerManager pm = context.getSystemService(PowerManager.class); - if (pm != null) { - pm.reboot(TAG); - } - } catch (Throwable t) { - logRescueException(level, failedPackage, t); - } - }; - thread = new Thread(runnable); - thread.start(); + executeWarmReboot(context, level, failedPackage); break; case LEVEL_FACTORY_RESET: // Before the completion of Reboot, if any crash happens then PackageWatchdog @@ -475,23 +555,9 @@ public class RescueParty { // Adding a check to prevent factory reset to execute before above reboot completes. // Note: this reboot property is not persistent resets after reboot is completed. if (isRebootPropertySet()) { - break; + return; } - setFactoryResetProperty(true); - long now = System.currentTimeMillis(); - setLastFactoryResetTimeMs(now); - runnable = new Runnable() { - @Override - public void run() { - try { - RecoverySystem.rebootPromptAndWipeUserData(context, TAG); - } catch (Throwable t) { - logRescueException(level, failedPackage, t); - } - } - }; - thread = new Thread(runnable); - thread.start(); + executeFactoryReset(context, level, failedPackage); break; } @@ -500,6 +566,83 @@ public class RescueParty { } } + private static void executeRescueLevelInternalNew(Context context, @RescueLevels int level, + @Nullable String failedPackage) throws Exception { + CrashRecoveryStatsLog.write(CrashRecoveryStatsLog.RESCUE_PARTY_RESET_REPORTED, + level, levelToString(level)); + switch (level) { + case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET: + // Temporary disable deviceConfig reset + // resetDeviceConfig(context, /*isScoped=*/true, failedPackage); + break; + case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET: + // Temporary disable deviceConfig reset + // resetDeviceConfig(context, /*isScoped=*/false, failedPackage); + break; + case RESCUE_LEVEL_WARM_REBOOT: + executeWarmReboot(context, level, failedPackage); + break; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + resetAllSettingsIfNecessary(context, Settings.RESET_MODE_UNTRUSTED_DEFAULTS, level); + break; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + resetAllSettingsIfNecessary(context, Settings.RESET_MODE_UNTRUSTED_CHANGES, level); + break; + case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + resetAllSettingsIfNecessary(context, Settings.RESET_MODE_TRUSTED_DEFAULTS, level); + break; + case RESCUE_LEVEL_FACTORY_RESET: + // Before the completion of Reboot, if any crash happens then PackageWatchdog + // escalates to next level i.e. factory reset, as they happen in separate threads. + // Adding a check to prevent factory reset to execute before above reboot completes. + // Note: this reboot property is not persistent resets after reboot is completed. + if (isRebootPropertySet()) { + return; + } + executeFactoryReset(context, level, failedPackage); + break; + } + } + + private static void executeWarmReboot(Context context, int level, + @Nullable String failedPackage) { + // Request the reboot from a separate thread to avoid deadlock on PackageWatchdog + // when device shutting down. + setRebootProperty(true); + Runnable runnable = () -> { + try { + PowerManager pm = context.getSystemService(PowerManager.class); + if (pm != null) { + pm.reboot(TAG); + } + } catch (Throwable t) { + logRescueException(level, failedPackage, t); + } + }; + Thread thread = new Thread(runnable); + thread.start(); + } + + private static void executeFactoryReset(Context context, int level, + @Nullable String failedPackage) { + setFactoryResetProperty(true); + long now = System.currentTimeMillis(); + setLastFactoryResetTimeMs(now); + Runnable runnable = new Runnable() { + @Override + public void run() { + try { + RecoverySystem.rebootPromptAndWipeUserData(context, TAG); + } catch (Throwable t) { + logRescueException(level, failedPackage, t); + } + } + }; + Thread thread = new Thread(runnable); + thread.start(); + } + + private static String getCompleteMessage(Throwable t) { final StringBuilder builder = new StringBuilder(); builder.append(t.getMessage()); @@ -521,17 +664,38 @@ public class RescueParty { } private static int mapRescueLevelToUserImpact(int rescueLevel) { - switch(rescueLevel) { - case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: - case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10; - case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: - case LEVEL_WARM_REBOOT: - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50; - case LEVEL_FACTORY_RESET: - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100; - default: - return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + if (Flags.recoverabilityDetection()) { + switch (rescueLevel) { + case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10; + case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_20; + case RESCUE_LEVEL_WARM_REBOOT: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_71; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_75; + case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_80; + case RESCUE_LEVEL_FACTORY_RESET: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100; + default: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + } + } else { + switch (rescueLevel) { + case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_10; + case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + case LEVEL_WARM_REBOOT: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_50; + case LEVEL_FACTORY_RESET: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_100; + default: + return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + } } } @@ -548,7 +712,7 @@ public class RescueParty { final ContentResolver resolver = context.getContentResolver(); try { Settings.Global.resetToDefaultsAsUser(resolver, null, mode, - UserHandle.SYSTEM.getIdentifier()); + UserHandle.SYSTEM.getIdentifier()); } catch (Exception e) { res = new RuntimeException("Failed to reset global settings", e); } @@ -667,8 +831,13 @@ public class RescueParty { @FailureReasons int failureReason, int mitigationCount) { if (!isDisabled() && (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH || failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING)) { - return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, + if (Flags.recoverabilityDetection()) { + return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, + mayPerformReboot(failedPackage), failedPackage)); + } else { + return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, mayPerformReboot(failedPackage))); + } } else { return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; } @@ -682,8 +851,10 @@ public class RescueParty { } if (failureReason == PackageWatchdog.FAILURE_REASON_APP_CRASH || failureReason == PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING) { - final int level = getRescueLevel(mitigationCount, - mayPerformReboot(failedPackage)); + final int level = Flags.recoverabilityDetection() ? getRescueLevel(mitigationCount, + mayPerformReboot(failedPackage), failedPackage) + : getRescueLevel(mitigationCount, + mayPerformReboot(failedPackage)); executeRescueLevel(mContext, failedPackage == null ? null : failedPackage.getPackageName(), level); return true; @@ -716,7 +887,12 @@ public class RescueParty { if (isDisabled()) { return PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; } - return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, true)); + if (Flags.recoverabilityDetection()) { + return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, + true, /*failedPackage=*/ null)); + } else { + return mapRescueLevelToUserImpact(getRescueLevel(mitigationCount, true)); + } } @Override @@ -725,8 +901,10 @@ public class RescueParty { return false; } boolean mayPerformReboot = !shouldThrottleReboot(); - executeRescueLevel(mContext, /*failedPackage=*/ null, - getRescueLevel(mitigationCount, mayPerformReboot)); + final int level = Flags.recoverabilityDetection() ? getRescueLevel(mitigationCount, + mayPerformReboot, /*failedPackage=*/ null) + : getRescueLevel(mitigationCount, mayPerformReboot); + executeRescueLevel(mContext, /*failedPackage=*/ null, level); return true; } @@ -843,14 +1021,44 @@ public class RescueParty { } private static String levelToString(int level) { - switch (level) { - case LEVEL_NONE: return "NONE"; - case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: return "RESET_SETTINGS_UNTRUSTED_DEFAULTS"; - case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: return "RESET_SETTINGS_UNTRUSTED_CHANGES"; - case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: return "RESET_SETTINGS_TRUSTED_DEFAULTS"; - case LEVEL_WARM_REBOOT: return "WARM_REBOOT"; - case LEVEL_FACTORY_RESET: return "FACTORY_RESET"; - default: return Integer.toString(level); + if (Flags.recoverabilityDetection()) { + switch (level) { + case RESCUE_LEVEL_NONE: + return "NONE"; + case RESCUE_LEVEL_SCOPED_DEVICE_CONFIG_RESET: + return "SCOPED_DEVICE_CONFIG_RESET"; + case RESCUE_LEVEL_ALL_DEVICE_CONFIG_RESET: + return "ALL_DEVICE_CONFIG_RESET"; + case RESCUE_LEVEL_WARM_REBOOT: + return "WARM_REBOOT"; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + return "RESET_SETTINGS_UNTRUSTED_DEFAULTS"; + case RESCUE_LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + return "RESET_SETTINGS_UNTRUSTED_CHANGES"; + case RESCUE_LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + return "RESET_SETTINGS_TRUSTED_DEFAULTS"; + case RESCUE_LEVEL_FACTORY_RESET: + return "FACTORY_RESET"; + default: + return Integer.toString(level); + } + } else { + switch (level) { + case LEVEL_NONE: + return "NONE"; + case LEVEL_RESET_SETTINGS_UNTRUSTED_DEFAULTS: + return "RESET_SETTINGS_UNTRUSTED_DEFAULTS"; + case LEVEL_RESET_SETTINGS_UNTRUSTED_CHANGES: + return "RESET_SETTINGS_UNTRUSTED_CHANGES"; + case LEVEL_RESET_SETTINGS_TRUSTED_DEFAULTS: + return "RESET_SETTINGS_TRUSTED_DEFAULTS"; + case LEVEL_WARM_REBOOT: + return "WARM_REBOOT"; + case LEVEL_FACTORY_RESET: + return "FACTORY_RESET"; + default: + return Integer.toString(level); + } } } } diff --git a/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java b/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java index 0fb932735ab4..93f26aefb692 100644 --- a/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java +++ b/packages/CrashRecovery/services/java/com/android/server/rollback/RollbackPackageHealthObserver.java @@ -69,7 +69,7 @@ import java.util.function.Consumer; * * @hide */ -final class RollbackPackageHealthObserver implements PackageHealthObserver { +public final class RollbackPackageHealthObserver implements PackageHealthObserver { private static final String TAG = "RollbackPackageHealthObserver"; private static final String NAME = "rollback-observer"; private static final int PERSISTENT_MASK = ApplicationInfo.FLAG_PERSISTENT @@ -89,7 +89,7 @@ final class RollbackPackageHealthObserver implements PackageHealthObserver { private boolean mTwoPhaseRollbackEnabled; @VisibleForTesting - RollbackPackageHealthObserver(Context context, ApexManager apexManager) { + public RollbackPackageHealthObserver(Context context, ApexManager apexManager) { mContext = context; HandlerThread handlerThread = new HandlerThread("RollbackPackageHealthObserver"); handlerThread.start(); diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt index 99a940968cc5..d13d86fccc97 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt @@ -305,10 +305,14 @@ fun CtaButtonRow( modifier = Modifier.fillMaxWidth() ) { if (leftButton != null) { - leftButton() + Box(modifier = Modifier.wrapContentSize().weight(1f, fill = false)) { + leftButton() + } } if (rightButton != null) { - rightButton() + Box(modifier = Modifier.wrapContentSize().weight(1f, fill = false)) { + rightButton() + } } } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt index a46e3586c777..3fb915226963 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt @@ -17,7 +17,6 @@ package com.android.credentialmanager.common.ui import android.content.Context -import android.content.res.Configuration import android.widget.RemoteViews import androidx.core.content.ContextCompat import com.android.credentialmanager.model.get.CredentialEntryInfo @@ -27,10 +26,12 @@ import android.graphics.drawable.Icon class RemoteViewsFactory { companion object { - private const val setAdjustViewBoundsMethodName = "setAdjustViewBounds" - private const val setMaxHeightMethodName = "setMaxHeight" - private const val setBackgroundResourceMethodName = "setBackgroundResource" - private const val bulletPoint = "\u2022" + private const val SET_ADJUST_VIEW_BOUNDS_METHOD_NAME = "setAdjustViewBounds" + private const val SET_MAX_HEIGHT_METHOD_NAME = "setMaxHeight" + private const val SET_BACKGROUND_RESOURCE_METHOD_NAME = "setBackgroundResource" + private const val BULLET_POINT = "\u2022" + // TODO(jbabs): RemoteViews#setViewPadding renders this as 8dp on the display. Debug why. + private const val END_ITEMS_PADDING = 28 fun createDropdownPresentation( context: Context, @@ -50,18 +51,18 @@ class RemoteViewsFactory { val secondaryText = if (credentialEntryInfo.displayName != null && (credentialEntryInfo.displayName != credentialEntryInfo.userName)) - (credentialEntryInfo.userName + " " + bulletPoint + " " + (credentialEntryInfo.userName + " " + BULLET_POINT + " " + credentialEntryInfo.credentialTypeDisplayName - + " " + bulletPoint + " " + credentialEntryInfo.providerDisplayName) - else (credentialEntryInfo.credentialTypeDisplayName + " " + bulletPoint + " " + + " " + BULLET_POINT + " " + credentialEntryInfo.providerDisplayName) + else (credentialEntryInfo.credentialTypeDisplayName + " " + BULLET_POINT + " " + credentialEntryInfo.providerDisplayName) remoteViews.setTextViewText(android.R.id.text2, secondaryText) remoteViews.setImageViewIcon(android.R.id.icon1, icon); remoteViews.setBoolean( - android.R.id.icon1, setAdjustViewBoundsMethodName, true); + android.R.id.icon1, SET_ADJUST_VIEW_BOUNDS_METHOD_NAME, true); remoteViews.setInt( android.R.id.icon1, - setMaxHeightMethodName, + SET_MAX_HEIGHT_METHOD_NAME, context.resources.getDimensionPixelSize( com.android.credentialmanager.R.dimen.autofill_icon_size)); remoteViews.setContentDescription(android.R.id.icon1, credentialEntryInfo @@ -71,11 +72,11 @@ class RemoteViewsFactory { com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_one else com.android.credentialmanager.R.drawable.fill_dialog_dynamic_list_item_middle remoteViews.setInt( - android.R.id.content, setBackgroundResourceMethodName, drawableId); + android.R.id.content, SET_BACKGROUND_RESOURCE_METHOD_NAME, drawableId); if (isFirstEntry) remoteViews.setViewPadding( com.android.credentialmanager.R.id.credential_card, /* left=*/0, - /* top=*/8, + /* top=*/END_ITEMS_PADDING, /* right=*/0, /* bottom=*/0) if (isLastEntry) remoteViews.setViewPadding( @@ -83,7 +84,7 @@ class RemoteViewsFactory { /*left=*/0, /* top=*/0, /* right=*/0, - /* bottom=*/8) + /* bottom=*/END_ITEMS_PADDING) return remoteViews } @@ -95,16 +96,16 @@ class RemoteViewsFactory { com.android.credentialmanager .R.string.dropdown_presentation_more_sign_in_options_text)) remoteViews.setBoolean( - android.R.id.icon1, setAdjustViewBoundsMethodName, true); + android.R.id.icon1, SET_ADJUST_VIEW_BOUNDS_METHOD_NAME, true); remoteViews.setInt( android.R.id.icon1, - setMaxHeightMethodName, + SET_MAX_HEIGHT_METHOD_NAME, context.resources.getDimensionPixelSize( com.android.credentialmanager.R.dimen.autofill_icon_size)); val drawableId = com.android.credentialmanager.R.drawable.more_options_list_item remoteViews.setInt( - android.R.id.content, setBackgroundResourceMethodName, drawableId); + android.R.id.content, SET_BACKGROUND_RESOURCE_METHOD_NAME, drawableId); return remoteViews } } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java index ef418a5c5bde..7c313e8a871d 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java @@ -28,6 +28,7 @@ import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageInstaller; +import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageManager; import android.content.pm.ProviderInfo; import android.net.Uri; @@ -38,7 +39,6 @@ import android.os.UserManager; import android.text.TextUtils; import android.util.EventLog; import android.util.Log; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.packageinstaller.v2.ui.InstallLaunch; import java.util.Arrays; @@ -51,6 +51,7 @@ public class InstallStart extends Activity { private static final String TAG = InstallStart.class.getSimpleName(); private PackageManager mPackageManager; + private PackageInstaller mPackageInstaller; private UserManager mUserManager; private boolean mAbortInstall = false; private boolean mShouldFinish = true; @@ -66,7 +67,7 @@ public class InstallStart extends Activity { Log.i(TAG, "Using Pia V2"); Intent piaV2 = new Intent(getIntent()); - piaV2.putExtra(InstallLaunch.EXTRA_CALLING_PKG_NAME, getCallingPackage()); + piaV2.putExtra(InstallLaunch.EXTRA_CALLING_PKG_NAME, getLaunchedFromPackage()); piaV2.putExtra(InstallLaunch.EXTRA_CALLING_PKG_UID, getLaunchedFromUid()); piaV2.setClass(this, InstallLaunch.class); piaV2.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); @@ -75,6 +76,7 @@ public class InstallStart extends Activity { return; } mPackageManager = getPackageManager(); + mPackageInstaller = mPackageManager.getPackageInstaller(); mUserManager = getSystemService(UserManager.class); Intent intent = getIntent(); @@ -94,12 +96,11 @@ public class InstallStart extends Activity { // If the activity was started via a PackageInstaller session, we retrieve the calling // package from that session final int sessionId = (isSessionInstall - ? intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1) - : -1); + ? intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, SessionInfo.INVALID_ID) + : SessionInfo.INVALID_ID); int originatingUidFromSession = callingUid; - if (callingPackage == null && sessionId != -1) { - PackageInstaller packageInstaller = getPackageManager().getPackageInstaller(); - PackageInstaller.SessionInfo sessionInfo = packageInstaller.getSessionInfo(sessionId); + if (callingPackage == null && sessionId != SessionInfo.INVALID_ID) { + PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(sessionId); if (sessionInfo != null) { callingPackage = sessionInfo.getInstallerPackageName(); callingAttributionTag = sessionInfo.getInstallerAttributionTag(); @@ -188,6 +189,7 @@ public class InstallStart extends Activity { nextActivity.putExtra(Intent.EXTRA_ORIGINATING_UID, originatingUid); nextActivity.putExtra(PackageInstallerActivity.EXTRA_ORIGINATING_UID_FROM_SESSION_INFO, originatingUidFromSession); + nextActivity.putExtra(PackageInstallerActivity.EXTRA_IS_TRUSTED_SOURCE, isTrustedSource); if (isSessionInstall) { nextActivity.setClass(this, PackageInstallerActivity.class); @@ -257,7 +259,7 @@ public class InstallStart extends Activity { private ApplicationInfo getSourceInfo(@Nullable String callingPackage) { if (callingPackage != null) { try { - return getPackageManager().getApplicationInfo(callingPackage, 0); + return mPackageManager.getApplicationInfo(callingPackage, 0); } catch (PackageManager.NameNotFoundException ex) { // ignore } @@ -265,8 +267,6 @@ public class InstallStart extends Activity { return null; } - - @NonNull private boolean canPackageQuery(int callingUid, Uri packageUri) { ProviderInfo info = mPackageManager.resolveContentProvider(packageUri.getAuthority(), PackageManager.ComponentInfoFlags.of(0)); @@ -295,8 +295,7 @@ public class InstallStart extends Activity { if (originatingUid == Process.ROOT_UID) { return true; } - PackageInstaller packageInstaller = getPackageManager().getPackageInstaller(); - PackageInstaller.SessionInfo sessionInfo = packageInstaller.getSessionInfo(sessionId); + PackageInstaller.SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(sessionId); if (sessionInfo == null) { return false; } diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java index 45bfe5469172..1b93c10a8c13 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java @@ -86,6 +86,7 @@ public class PackageInstallerActivity extends Activity { static final String EXTRA_APP_SNIPPET = "EXTRA_APP_SNIPPET"; static final String EXTRA_ORIGINATING_UID_FROM_SESSION_INFO = "EXTRA_ORIGINATING_UID_FROM_SESSION_INFO"; + static final String EXTRA_IS_TRUSTED_SOURCE = "EXTRA_IS_TRUSTED_SOURCE"; private static final String ALLOW_UNKNOWN_SOURCES_KEY = PackageInstallerActivity.class.getName() + "ALLOW_UNKNOWN_SOURCES_KEY"; @@ -304,21 +305,6 @@ public class PackageInstallerActivity extends Activity { return packagesForUid[0]; } - private boolean isInstallRequestFromUnknownSource(Intent intent) { - if (mCallingPackage != null && intent.getBooleanExtra( - Intent.EXTRA_NOT_UNKNOWN_SOURCE, false)) { - if (mSourceInfo != null && mSourceInfo.isPrivilegedApp()) { - // Privileged apps can bypass unknown sources check if they want. - return false; - } - } - if (mSourceInfo != null && checkPermission(Manifest.permission.INSTALL_PACKAGES, - -1 /* pid */, mSourceInfo.uid) == PackageManager.PERMISSION_GRANTED) { - return false; - } - return true; - } - private void initiateInstall() { String pkgName = mPkgInfo.packageName; // Check if there is already a package on the device with this name @@ -557,7 +543,7 @@ public class PackageInstallerActivity extends Activity { * Check if it is allowed to install the package and initiate install if allowed. */ private void checkIfAllowedAndInitiateInstall() { - if (mAllowUnknownSources || !isInstallRequestFromUnknownSource(getIntent())) { + if (mAllowUnknownSources || getIntent().getBooleanExtra(EXTRA_IS_TRUSTED_SOURCE, false)) { if (mLocalLOGV) Log.i(TAG, "install allowed"); initiateInstall(); } else { diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt index 32795e4b2f1c..e48c0f42e62e 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt +++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/model/InstallRepository.kt @@ -96,6 +96,7 @@ class InstallRepository(private val context: Context) { var stagedSessionId = SessionInfo.INVALID_ID private set private var callingUid = Process.INVALID_UID + private var originatingUid = Process.INVALID_UID private var callingPackage: String? = null private var sessionStager: SessionStager? = null private lateinit var intent: Intent @@ -148,7 +149,7 @@ class InstallRepository(private val context: Context) { } val sourceInfo: ApplicationInfo? = getSourceInfo(callingPackage) // Uid of the source package, with a preference to uid from ApplicationInfo - val originatingUid = sourceInfo?.uid ?: callingUid + originatingUid = sourceInfo?.uid ?: callingUid appOpRequestInfo = AppOpRequestInfo( getPackageNameForUid(context, originatingUid, callingPackage), originatingUid, callingAttributionTag @@ -282,7 +283,7 @@ class InstallRepository(private val context: Context) { context.contentResolver.openAssetFileDescriptor(uri, "r").use { afd -> val pfd: ParcelFileDescriptor? = afd?.parcelFileDescriptor val params: SessionParams = - createSessionParams(intent, pfd, uri.toString()) + createSessionParams(originatingUid, intent, pfd, uri.toString()) stagedSessionId = packageInstaller.createSession(params) } } catch (e: Exception) { @@ -338,6 +339,7 @@ class InstallRepository(private val context: Context) { } private fun createSessionParams( + originatingUid: Int, intent: Intent, pfd: ParcelFileDescriptor?, debugPathName: String, @@ -354,9 +356,7 @@ class InstallRepository(private val context: Context) { params.setOriginatingUri( intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI, Uri::class.java) ) - params.setOriginatingUid( - intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID, Process.INVALID_UID) - ) + params.setOriginatingUid(originatingUid) params.setInstallerPackageName(intent.getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME)) params.setInstallReason(PackageManager.INSTALL_REASON_USER) // Disable full screen intent usage by for sideloads. diff --git a/packages/SettingsLib/ProfileSelector/Android.bp b/packages/SettingsLib/ProfileSelector/Android.bp index 6dc07b29a510..4aa67c17ad98 100644 --- a/packages/SettingsLib/ProfileSelector/Android.bp +++ b/packages/SettingsLib/ProfileSelector/Android.bp @@ -20,6 +20,7 @@ android_library { static_libs: [ "com.google.android.material_material", "SettingsLibSettingsTheme", + "android.os.flags-aconfig-java-export", ], sdk_version: "system_current", diff --git a/packages/SettingsLib/ProfileSelector/AndroidManifest.xml b/packages/SettingsLib/ProfileSelector/AndroidManifest.xml index 80f6b7683269..303e20c2497e 100644 --- a/packages/SettingsLib/ProfileSelector/AndroidManifest.xml +++ b/packages/SettingsLib/ProfileSelector/AndroidManifest.xml @@ -18,5 +18,5 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.settingslib.widget.profileselector"> - <uses-sdk android:minSdkVersion="23" /> + <uses-sdk android:minSdkVersion="29" /> </manifest> diff --git a/packages/SettingsLib/ProfileSelector/res/values/strings.xml b/packages/SettingsLib/ProfileSelector/res/values/strings.xml index 68d4047a497c..76ccb651969b 100644 --- a/packages/SettingsLib/ProfileSelector/res/values/strings.xml +++ b/packages/SettingsLib/ProfileSelector/res/values/strings.xml @@ -21,4 +21,6 @@ <string name="settingslib_category_personal">Personal</string> <!-- Header for items under the work user [CHAR LIMIT=30] --> <string name="settingslib_category_work">Work</string> + <!-- Header for items under the private profile user [CHAR LIMIT=30] --> + <string name="settingslib_category_private">Private</string> </resources>
\ No newline at end of file diff --git a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java index be5753beea4e..c52386bef07b 100644 --- a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java +++ b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java @@ -16,31 +16,77 @@ package com.android.settingslib.widget; +import android.annotation.TargetApi; import android.app.Activity; +import android.content.Context; +import android.content.pm.UserProperties; +import android.os.Build; import android.os.Bundle; +import android.os.UserHandle; +import android.os.UserManager; +import android.util.ArrayMap; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.core.os.BuildCompat; import androidx.fragment.app.Fragment; import androidx.viewpager2.widget.ViewPager2; +import com.android.settingslib.widget.profileselector.R; + import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; -import com.android.settingslib.widget.profileselector.R; + +import java.util.ArrayList; +import java.util.List; /** * Base fragment class for profile settings. */ public abstract class ProfileSelectFragment extends Fragment { + private static final String TAG = "ProfileSelectFragment"; + // UserHandle#USER_NULL is a @TestApi so is not accessible. + private static final int USER_NULL = -10000; + private static final int DEFAULT_POSITION = 0; + + /** + * The type of profile tab of {@link ProfileSelectFragment} to show + * <ul> + * <li>0: Personal tab. + * <li>1: Work profile tab. + * </ul> + * + * <p> Please note that this is supported for legacy reasons. Please use + * {@link #EXTRA_SHOW_FRAGMENT_USER_ID} instead. + */ + public static final String EXTRA_SHOW_FRAGMENT_TAB = ":settings:show_fragment_tab"; + + /** + * An {@link ArrayList} of users to show. The supported users are: System user, the managed + * profile user, and the private profile user. A client should pass all the user ids that need + * to be shown in this list. Note that if this list is not provided then, for legacy reasons + * see {@link #EXTRA_SHOW_FRAGMENT_TAB}, an attempt will be made to show two tabs: one for the + * System user and one for the managed profile user. + * + * <p>Please note that this MUST be used in conjunction with + * {@link #EXTRA_SHOW_FRAGMENT_USER_ID} + */ + public static final String EXTRA_LIST_OF_USER_IDS = ":settings:list_user_ids"; /** - * Personal or Work profile tab of {@link ProfileSelectFragment} - * <p>0: Personal tab. - * <p>1: Work profile tab. + * The user id of the user to be show in {@link ProfileSelectFragment}. Only the below user + * types are supported: + * <ul> + * <li> System user. + * <li> Managed profile user. + * <li> Private profile user. + * </ul> + * + * <p>Please note that this MUST be used in conjunction with {@link #EXTRA_LIST_OF_USER_IDS}. */ - public static final String EXTRA_SHOW_FRAGMENT_TAB = - ":settings:show_fragment_tab"; + public static final String EXTRA_SHOW_FRAGMENT_USER_ID = ":settings:show_fragment_user_id"; /** * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB @@ -48,13 +94,23 @@ public abstract class ProfileSelectFragment extends Fragment { public static final int PERSONAL_TAB = 0; /** - * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB + * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB for the managed profile */ public static final int WORK_TAB = 1; + /** + * Please note that private profile is available from API LEVEL + * {@link Build.VERSION_CODES.VANILLA_ICE_CREAM} only, therefore PRIVATE_TAB MUST be + * passed in {@link #EXTRA_SHOW_FRAGMENT_TAB} and {@link #EXTRA_LIST_OF_PROFILE_TABS} for + * {@link Build.VERSION_CODES.VANILLA_ICE_CREAM} or higher API Levels only. + */ + private static final int PRIVATE_TAB = 2; + private ViewGroup mContentView; private ViewPager2 mViewPager; + private final ArrayMap<UserHandle, Integer> mProfileTabsByUsers = new ArrayMap<>(); + private boolean mUsingUserIds = false; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -67,7 +123,7 @@ public abstract class ProfileSelectFragment extends Fragment { if (titleResId > 0) { activity.setTitle(titleResId); } - final int selectedTab = getTabId(activity, getArguments()); + initProfileTabsToShow(); final View tabContainer = mContentView.findViewById(R.id.tab_container); mViewPager = tabContainer.findViewById(R.id.view_pager); @@ -78,16 +134,14 @@ public abstract class ProfileSelectFragment extends Fragment { ).attach(); tabContainer.setVisibility(View.VISIBLE); - final TabLayout.Tab tab = tabs.getTabAt(selectedTab); + final TabLayout.Tab tab = tabs.getTabAt(getSelectedTabPosition(activity, getArguments())); tab.select(); return mContentView; } /** - * create Personal or Work profile fragment - * <p>0: Personal profile. - * <p>1: Work profile. + * Create Personal or Work or Private profile fragment. See {@link #EXTRA_SHOW_FRAGMENT_USER_ID} */ public abstract Fragment createFragment(int position); @@ -99,21 +153,90 @@ public abstract class ProfileSelectFragment extends Fragment { return 0; } - int getTabId(Activity activity, Bundle bundle) { + int getSelectedTabPosition(Activity activity, Bundle bundle) { if (bundle != null) { + final int extraUserId = bundle.getInt(EXTRA_SHOW_FRAGMENT_USER_ID, USER_NULL); + if (extraUserId != USER_NULL) { + return mProfileTabsByUsers.indexOfKey(UserHandle.of(extraUserId)); + } final int extraTab = bundle.getInt(EXTRA_SHOW_FRAGMENT_TAB, -1); if (extraTab != -1) { return extraTab; } } - return PERSONAL_TAB; + return DEFAULT_POSITION; + } + + int getTabCount() { + return mUsingUserIds ? mProfileTabsByUsers.size() : 2; + } + + void initProfileTabsToShow() { + Bundle bundle = getArguments(); + if (bundle != null) { + ArrayList<Integer> userIdsToShow = + bundle.getIntegerArrayList(EXTRA_LIST_OF_USER_IDS); + if (userIdsToShow != null && !userIdsToShow.isEmpty()) { + mUsingUserIds = true; + UserManager userManager = getContext().getSystemService(UserManager.class); + List<UserHandle> userHandles = userManager.getUserProfiles(); + for (UserHandle userHandle : userHandles) { + if (!userIdsToShow.contains(userHandle.getIdentifier())) { + continue; + } + if (userHandle.isSystem()) { + mProfileTabsByUsers.put(userHandle, PERSONAL_TAB); + } else if (userManager.isManagedProfile(userHandle.getIdentifier())) { + mProfileTabsByUsers.put(userHandle, WORK_TAB); + } else if (shouldShowPrivateProfileIfItsOne(userHandle)) { + mProfileTabsByUsers.put(userHandle, PRIVATE_TAB); + } + } + } + } + } + + private int getProfileTabForPosition(int position) { + return mUsingUserIds ? mProfileTabsByUsers.valueAt(position) : position; + } + + int getUserIdForPosition(int position) { + return mUsingUserIds ? mProfileTabsByUsers.keyAt(position).getIdentifier() : position; } private CharSequence getPageTitle(int position) { - if (position == WORK_TAB) { + int tab = getProfileTabForPosition(position); + if (tab == WORK_TAB) { return getContext().getString(R.string.settingslib_category_work); + } else if (tab == PRIVATE_TAB) { + return getContext().getString(R.string.settingslib_category_private); } return getString(R.string.settingslib_category_personal); } + + @TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + private boolean shouldShowUserInQuietMode(UserHandle userHandle, UserManager userManager) { + UserProperties userProperties = userManager.getUserProperties(userHandle); + return !userManager.isQuietModeEnabled(userHandle) + || userProperties.getShowInQuietMode() != UserProperties.SHOW_IN_QUIET_MODE_HIDDEN; + } + + // It's sufficient to have this method marked with the appropriate API level because we expect + // to be here only for this API level - when then private profile was introduced. + @TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + private boolean shouldShowPrivateProfileIfItsOne(UserHandle userHandle) { + if (!BuildCompat.isAtLeastV() || !android.os.Flags.allowPrivateProfile()) { + return false; + } + try { + Context userContext = getContext().createContextAsUser(userHandle, /* flags= */ 0); + UserManager userManager = userContext.getSystemService(UserManager.class); + return userManager.isPrivateProfile() + && shouldShowUserInQuietMode(userHandle, userManager); + } catch (IllegalStateException exception) { + Log.i(TAG, "Ignoring this user as the calling package not available in this user."); + } + return false; + } } diff --git a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java index f5ab64742992..37f4f275cfe7 100644 --- a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java +++ b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java @@ -18,7 +18,6 @@ package com.android.settingslib.widget; import androidx.fragment.app.Fragment; import androidx.viewpager2.adapter.FragmentStateAdapter; -import com.android.settingslib.widget.profileselector.R; /** * ViewPager Adapter to handle between TabLayout and ViewPager2 @@ -34,11 +33,11 @@ public class ProfileViewPagerAdapter extends FragmentStateAdapter { @Override public Fragment createFragment(int position) { - return mParentFragments.createFragment(position); + return mParentFragments.createFragment(mParentFragments.getUserIdForPosition(position)); } @Override public int getItemCount() { - return 2; + return mParentFragments.getTabCount(); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt index 6730aadbdeb3..e7fec692bd63 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt @@ -19,7 +19,6 @@ package com.android.settingslib.volume.data.repository import android.media.AudioDeviceInfo import android.media.AudioManager import android.media.AudioManager.OnCommunicationDeviceChangedListener -import androidx.concurrent.futures.DirectExecutor import com.android.internal.util.ConcurrentUtils import com.android.settingslib.volume.shared.AudioManagerEventsReceiver import com.android.settingslib.volume.shared.model.AudioManagerEvent @@ -109,8 +108,8 @@ class AudioRepositoryImpl( callbackFlow { val listener = OnCommunicationDeviceChangedListener { trySend(Unit) } audioManager.addOnCommunicationDeviceChangedListener( - DirectExecutor.INSTANCE, - listener + ConcurrentUtils.DIRECT_EXECUTOR, + listener, ) awaitClose { audioManager.removeOnCommunicationDeviceChangedListener(listener) } @@ -146,7 +145,7 @@ class AudioRepositoryImpl( maxVolume = audioManager.getStreamMaxVolume(audioStream.value), volume = audioManager.getStreamVolume(audioStream.value), isAffectedByRingerMode = audioManager.isStreamAffectedByRingerMode(audioStream.value), - isMuted = audioManager.isStreamMute(audioStream.value), + isMuted = audioManager.isStreamMute(audioStream.value) ) } diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt index c9ac97dcab7f..778653b9bd44 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/AudioVolumeInteractor.kt @@ -66,6 +66,10 @@ class AudioVolumeInteractor( } } + fun isMutable(audioStream: AudioStream): Boolean = + // Alarm stream doesn't support muting + audioStream.value != AudioManager.STREAM_ALARM + private suspend fun processVolume( audioStreamModel: AudioStreamModel, ringerMode: RingerMode, diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index eaec617cfa70..5629a7bf7b21 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -256,8 +256,7 @@ public class SecureSettings { Settings.Secure.HEARING_AID_MEDIA_ROUTING, Settings.Secure.HEARING_AID_NOTIFICATION_ROUTING, Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED, - Settings.Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED, - Settings.Secure.SEARCH_LONG_PRESS_HOME_ENABLED, + Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED, Settings.Secure.HUB_MODE_TUTORIAL_STATE, Settings.Secure.STYLUS_BUTTONS_ENABLED, Settings.Secure.STYLUS_HANDWRITING_ENABLED, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 046d6e25ff31..b8d95eb5329d 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -208,8 +208,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.ASSIST_TOUCH_GESTURE_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ASSIST_LONG_PRESS_HOME_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED, BOOLEAN_VALIDATOR); - VALIDATORS.put(Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED, BOOLEAN_VALIDATOR); - VALIDATORS.put(Secure.SEARCH_LONG_PRESS_HOME_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.VR_DISPLAY_MODE, new DiscreteValueValidator(new String[] {"0", "1"})); VALIDATORS.put(Secure.NOTIFICATION_BADGING, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.NOTIFICATION_DISMISS_RTL, BOOLEAN_VALIDATOR); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index 02d212cb4996..dba3bac4a4b8 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -1950,11 +1950,8 @@ class SettingsProtoDumpUtil { Settings.Secure.ASSIST_LONG_PRESS_HOME_ENABLED, SecureSettingsProto.Assist.LONG_PRESS_HOME_ENABLED); dumpSetting(s, p, - Settings.Secure.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED, - SecureSettingsProto.Assist.SEARCH_PRESS_HOLD_NAV_HANDLE_ENABLED); - dumpSetting(s, p, - Settings.Secure.SEARCH_LONG_PRESS_HOME_ENABLED, - SecureSettingsProto.Assist.SEARCH_LONG_PRESS_HOME_ENABLED); + Settings.Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED, + SecureSettingsProto.Assist.SEARCH_ALL_ENTRYPOINTS_ENABLED); dumpSetting(s, p, Settings.Secure.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED, SecureSettingsProto.Assist.VISUAL_QUERY_ACCESSIBILITY_DETECTION_ENABLED); diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 6eb2dd043c94..8cafe5faaa09 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -688,6 +688,7 @@ public class SettingsBackupTest { Settings.Secure.DEVICE_PAIRED, Settings.Secure.DIALER_DEFAULT_APPLICATION, Settings.Secure.DISABLED_PRINT_SERVICES, + Settings.Secure.DISABLE_SECURE_WINDOWS, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS, Settings.Secure.DOCKED_CLOCK_FACE, Settings.Secure.DOZE_PULSE_ON_LONG_PRESS, diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 02d19dc84f2e..58040716db3e 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -932,6 +932,9 @@ <uses-permission android:name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW" /> + <!-- Permission required for Cts test - CtsSettingsTestCases --> + <uses-permission android:name="android.permission.PREPARE_FACTORY_RESET" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java index 6546b87c8802..f70ad9ed58b0 100644 --- a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java +++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java @@ -23,10 +23,10 @@ import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_QU import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_RECENTS; import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT; -import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_OPEN_BLOCKED; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_GLOBAL_ACTION; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_GLOBAL_ACTION_EXTRA; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_HIDE_MENU; +import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_OPEN_BLOCKED; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_TOGGLE_MENU; import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.PACKAGE_NAME; @@ -77,6 +77,8 @@ public class AccessibilityMenuServiceTest { private static final int TIMEOUT_SERVICE_STATUS_CHANGE_S = 5; private static final int TIMEOUT_UI_CHANGE_S = 5; private static final int NO_GLOBAL_ACTION = -1; + private static final Intent INTENT_OPEN_MENU = new Intent(INTENT_TOGGLE_MENU) + .setPackage(PACKAGE_NAME); private static Instrumentation sInstrumentation; private static UiAutomation sUiAutomation; @@ -152,9 +154,6 @@ public class AccessibilityMenuServiceTest { @Before public void setup() throws Throwable { sOpenBlocked.set(false); - wakeUpScreen(); - sUiAutomation.executeShellCommand("input keyevent KEYCODE_MENU"); - openMenu(); } @After @@ -188,24 +187,17 @@ public class AccessibilityMenuServiceTest { } private static void openMenu() throws Throwable { - openMenu(false); - } - - private static void openMenu(boolean abandonOnBlock) throws Throwable { - Intent intent = new Intent(INTENT_TOGGLE_MENU); - intent.setPackage(PACKAGE_NAME); - sInstrumentation.getContext().sendBroadcast(intent); + unlockSignal(); + sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU); TestUtils.waitUntil("Timed out before menu could appear.", TIMEOUT_UI_CHANGE_S, () -> { - if (sOpenBlocked.get() && abandonOnBlock) { - throw new IllegalStateException(); - } if (isMenuVisible()) { return true; } else { - sInstrumentation.getContext().sendBroadcast(intent); + unlockSignal(); + sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU); return false; } }); @@ -249,6 +241,7 @@ public class AccessibilityMenuServiceTest { @Test public void testAdjustBrightness() throws Throwable { + openMenu(); Context context = sInstrumentation.getTargetContext(); DisplayManager displayManager = context.getSystemService( DisplayManager.class); @@ -264,22 +257,28 @@ public class AccessibilityMenuServiceTest { context.getDisplayId()).getBrightnessInfo(); try { - displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMinimum); TestUtils.waitUntil("Could not change to minimum brightness", TIMEOUT_UI_CHANGE_S, - () -> displayManager.getBrightness(context.getDisplayId()) - == brightnessInfo.brightnessMinimum); + () -> { + displayManager.setBrightness( + context.getDisplayId(), brightnessInfo.brightnessMinimum); + return displayManager.getBrightness(context.getDisplayId()) + == brightnessInfo.brightnessMinimum; + }); brightnessUpButton.performAction(CLICK_ID); TestUtils.waitUntil("Did not detect an increase in brightness.", TIMEOUT_UI_CHANGE_S, () -> displayManager.getBrightness(context.getDisplayId()) > brightnessInfo.brightnessMinimum); - displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMaximum); TestUtils.waitUntil("Could not change to maximum brightness", TIMEOUT_UI_CHANGE_S, - () -> displayManager.getBrightness(context.getDisplayId()) - == brightnessInfo.brightnessMaximum); + () -> { + displayManager.setBrightness( + context.getDisplayId(), brightnessInfo.brightnessMaximum); + return displayManager.getBrightness(context.getDisplayId()) + == brightnessInfo.brightnessMaximum; + }); brightnessDownButton.performAction(CLICK_ID); TestUtils.waitUntil("Did not detect a decrease in brightness.", TIMEOUT_UI_CHANGE_S, @@ -292,6 +291,7 @@ public class AccessibilityMenuServiceTest { @Test public void testAdjustVolume() throws Throwable { + openMenu(); Context context = sInstrumentation.getTargetContext(); AudioManager audioManager = context.getSystemService(AudioManager.class); int resetVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); @@ -332,6 +332,7 @@ public class AccessibilityMenuServiceTest { @Test public void testAssistantButton_opensVoiceAssistant() throws Throwable { + openMenu(); AccessibilityNodeInfo assistantButton = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_ASSISTANT_VALUE.ordinal())); Intent expectedIntent = new Intent(Intent.ACTION_VOICE_COMMAND); @@ -349,6 +350,7 @@ public class AccessibilityMenuServiceTest { @Test public void testAccessibilitySettingsButton_opensAccessibilitySettings() throws Throwable { + openMenu(); AccessibilityNodeInfo settingsButton = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_A11YSETTING_VALUE.ordinal())); Intent expectedIntent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); @@ -364,6 +366,7 @@ public class AccessibilityMenuServiceTest { @Test public void testPowerButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_POWER_VALUE.ordinal())); @@ -376,6 +379,7 @@ public class AccessibilityMenuServiceTest { @Test public void testRecentButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_RECENT_VALUE.ordinal())); @@ -388,6 +392,7 @@ public class AccessibilityMenuServiceTest { @Test public void testLockButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_LOCKSCREEN_VALUE.ordinal())); @@ -400,6 +405,7 @@ public class AccessibilityMenuServiceTest { @Test public void testQuickSettingsButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_QUICKSETTING_VALUE.ordinal())); @@ -412,6 +418,7 @@ public class AccessibilityMenuServiceTest { @Test public void testNotificationsButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_NOTIFICATION_VALUE.ordinal())); @@ -424,6 +431,7 @@ public class AccessibilityMenuServiceTest { @Test public void testScreenshotButton_performsGlobalAction() throws Throwable { + openMenu(); AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(), String.valueOf(ShortcutId.ID_SCREENSHOT_VALUE.ordinal())); @@ -436,6 +444,7 @@ public class AccessibilityMenuServiceTest { @Test public void testOnScreenLock_closesMenu() throws Throwable { + openMenu(); closeScreen(); wakeUpScreen(); @@ -447,13 +456,18 @@ public class AccessibilityMenuServiceTest { closeScreen(); wakeUpScreen(); - boolean blocked = false; - try { - openMenu(true); - } catch (IllegalStateException e) { - // Expected - blocked = true; - } - assertThat(blocked).isTrue(); + TestUtils.waitUntil("Did not receive signal that menu cannot open", + TIMEOUT_UI_CHANGE_S, + () -> { + sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU); + return sOpenBlocked.get(); + }); + } + + private static void unlockSignal() { + // MENU unlocks screen, + // BACK closes any menu that may appear if the screen wasn't locked. + sUiAutomation.executeShellCommand("input keyevent KEYCODE_MENU"); + sUiAutomation.executeShellCommand("input keyevent KEYCODE_BACK"); } } diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 8da50216f13c..a155dc4d7639 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -104,6 +104,13 @@ flag { } flag { + name: "notifications_heads_up_refactor" + namespace: "systemui" + description: "Use HeadsUpInteractor to feed HUN updates to the NSSL." + bug: "325936094" +} + +flag { name: "pss_app_selector_abrupt_exit_fix" namespace: "systemui" description: "Fixes the app selector abruptly disappearing without an animation, when the" @@ -424,6 +431,13 @@ flag { } flag { + name: "screenshot_shelf_ui" + namespace: "systemui" + description: "Use new shelf UI flow for screenshots" + bug: "329659738" +} + +flag { name: "run_fingerprint_detect_on_dismissible_keyguard" namespace: "systemui" description: "Run fingerprint detect instead of authenticate if the keyguard is dismissible." diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt index 82e19e7c154c..1d86b15dbf4f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt @@ -58,7 +58,6 @@ constructor( if (currentClock?.smallClock?.view == null) { return } - viewModel.clock = currentClock val context = LocalContext.current MovableElement(key = smallClockElementKey, modifier = modifier) { @@ -89,7 +88,6 @@ constructor( @Composable fun SceneScope.LargeClock(modifier: Modifier = Modifier) { val currentClock by viewModel.currentClock.collectAsState() - viewModel.clock = currentClock if (currentClock?.largeClock?.view == null) { return } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt index 31d3fa0be163..9f02201f1d81 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt @@ -32,12 +32,12 @@ import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneScope import com.android.keyguard.LockIconView import com.android.keyguard.LockIconViewController -import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.biometrics.AuthController import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.ui.binder.DeviceEntryIconViewBinder import com.android.systemui.keyguard.ui.composable.blueprint.BlueprintAlignmentLines import com.android.systemui.keyguard.ui.view.DeviceEntryIconView @@ -69,7 +69,7 @@ constructor( ) { @Composable fun SceneScope.LockIcon(modifier: Modifier = Modifier) { - if (!keyguardBottomAreaRefactor() && !DeviceEntryUdfpsRefactor.isEnabled) { + if (!KeyguardBottomAreaRefactor.isEnabled && !DeviceEntryUdfpsRefactor.isEnabled) { return } @@ -96,7 +96,7 @@ constructor( ) } } else { - // keyguardBottomAreaRefactor() + // KeyguardBottomAreaRefactor.isEnabled LockIconView(context, null).apply { id = R.id.lock_icon_view lockIconViewController.get().setLockIconView(this) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt index 5c9b271b342c..6b86a484069b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt @@ -16,50 +16,34 @@ package com.android.systemui.keyguard.ui.composable.section -import android.content.Context import android.view.ViewGroup import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.android.compose.animation.scene.SceneScope -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.notifications.ui.composable.NotificationStack -import com.android.systemui.scene.shared.flag.SceneContainerFlags -import com.android.systemui.statusbar.notification.stack.AmbientState import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher @SysUISingleton class NotificationSection @Inject constructor( - @Application private val context: Context, private val viewModel: NotificationsPlaceholderViewModel, - controller: NotificationStackScrollLayoutController, - sceneContainerFlags: SceneContainerFlags, sharedNotificationContainer: SharedNotificationContainer, sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, stackScrollLayout: NotificationStackScrollLayout, - notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, - ambientState: AmbientState, - notificationStackSizeCalculator: NotificationStackSizeCalculator, - @Main private val mainImmediateDispatcher: CoroutineDispatcher, + sharedNotificationContainerBinder: SharedNotificationContainerBinder, ) { init { - if (!migrateClocksToBlueprint()) { - throw IllegalStateException("this requires migrateClocksToBlueprint()") + if (!MigrateClocksToBlueprint.isEnabled) { + throw IllegalStateException("this requires MigrateClocksToBlueprint.isEnabled") } // This scene container section moves the NSSL to the SharedNotificationContainer. // This also requires that SharedNotificationContainer gets moved to the @@ -73,25 +57,10 @@ constructor( sharedNotificationContainer.addNotificationStackScrollLayout(stackScrollLayout) } - SharedNotificationContainerBinder.bind( + sharedNotificationContainerBinder.bind( sharedNotificationContainer, sharedNotificationContainerViewModel, - sceneContainerFlags, - controller, - notificationStackSizeCalculator, - mainImmediateDispatcher = mainImmediateDispatcher, ) - - if (sceneContainerFlags.isEnabled()) { - NotificationStackAppearanceViewBinder.bind( - context, - sharedNotificationContainer, - notificationStackAppearanceViewModel, - ambientState, - controller, - mainImmediateDispatcher = mainImmediateDispatcher, - ) - } } @Composable diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index 8ad2bb78f5d6..9ba5e3b846ed 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -57,6 +57,7 @@ import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize @@ -70,10 +71,10 @@ import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadi import com.android.systemui.notifications.ui.composable.Notifications.Form import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_CORNER_RADIUS import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA +import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.ui.composable.ShadeHeader import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding -import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder.SCRIM_CORNER_RADIUS import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import kotlin.math.roundToInt @@ -140,6 +141,7 @@ fun SceneScope.NotificationScrollingStack( ) { val density = LocalDensity.current val screenCornerRadius = LocalScreenCornerRadius.current + val scrimCornerRadius = dimensionResource(R.dimen.notification_scrim_corner_radius) val scrollState = rememberScrollState() val syntheticScroll = viewModel.syntheticScroll.collectAsState(0f) val expansionFraction by viewModel.expandFraction.collectAsState(0f) @@ -157,7 +159,7 @@ fun SceneScope.NotificationScrollingStack( val contentHeight = viewModel.intrinsicContentHeight.collectAsState() - val stackRounding = viewModel.stackRounding.collectAsState() + val stackRounding = viewModel.stackRounding.collectAsState(StackRounding()) // the offset for the notifications scrim. Its upper bound is 0, and its lower bound is // calculated in minScrimOffset. The scrim is the same height as the screen minus the @@ -225,6 +227,7 @@ fun SceneScope.NotificationScrollingStack( .graphicsLayer { shape = calculateCornerRadius( + scrimCornerRadius, screenCornerRadius, { expansionFraction }, layoutState.isTransitioningBetween(Scenes.Gone, Scenes.Shade) @@ -357,6 +360,7 @@ private fun SceneScope.NotificationPlaceholder( } private fun calculateCornerRadius( + scrimCornerRadius: Dp, screenCornerRadius: Dp, expansionFraction: () -> Float, transitioning: Boolean, @@ -364,12 +368,12 @@ private fun calculateCornerRadius( return if (transitioning) { lerp( start = screenCornerRadius.value, - stop = SCRIM_CORNER_RADIUS, + stop = scrimCornerRadius.value, fraction = (expansionFraction() / EXPANSION_FOR_MAX_CORNER_RADIUS).coerceIn(0f, 1f), ) .dp } else { - SCRIM_CORNER_RADIUS.dp + scrimCornerRadius } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 2c31f9b6b92d..85798acd0dcd 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -370,7 +370,8 @@ private fun SceneScope.SplitShade( NotificationScrollingStack( viewModel = viewModel.notifications, maxScrimTop = { 0f }, - modifier = Modifier.weight(1f).fillMaxHeight(), + modifier = + Modifier.weight(1f).fillMaxHeight().padding(bottom = navBarBottomHeight), ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt index 24351706cb46..248dfeee2281 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt @@ -18,6 +18,7 @@ package com.android.systemui.volume.panel.component.volume.ui.composable import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.size import androidx.compose.material3.IconButton @@ -27,6 +28,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.semantics.ProgressBarRangeInfo @@ -38,6 +40,7 @@ import androidx.compose.ui.semantics.setProgress import androidx.compose.ui.unit.dp import com.android.compose.PlatformSlider import com.android.compose.PlatformSliderColors +import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.compose.Icon import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState @@ -86,18 +89,11 @@ fun VolumeSlider( Text(text = state.valueText, color = LocalContentColor.current) } else { state.icon?.let { - IconButton( - onClick = onIconTapped, - colors = - IconButtonColors( - contentColor = LocalContentColor.current, - containerColor = Color.Transparent, - disabledContentColor = LocalContentColor.current, - disabledContainerColor = Color.Transparent, - ) - ) { - Icon(modifier = Modifier.size(24.dp), icon = it) - } + SliderIcon( + icon = it, + onIconTapped = onIconTapped, + isTappable = state.isMutable, + ) } } }, @@ -127,3 +123,32 @@ fun VolumeSlider( } ) } + +@Composable +private fun SliderIcon( + icon: Icon, + onIconTapped: () -> Unit, + isTappable: Boolean, + modifier: Modifier = Modifier +) { + if (isTappable) { + IconButton( + modifier = modifier, + onClick = onIconTapped, + colors = + IconButtonColors( + contentColor = LocalContentColor.current, + containerColor = Color.Transparent, + disabledContentColor = LocalContentColor.current, + disabledContainerColor = Color.Transparent, + ), + content = { Icon(modifier = Modifier.size(24.dp), icon = icon) }, + ) + } else { + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + content = { Icon(modifier = Modifier.size(24.dp), icon = icon) }, + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt index 8e2e94716660..a7e98ea34154 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -18,10 +18,16 @@ package com.android.systemui.communal.view.viewmodel import android.app.smartspace.SmartspaceTarget import android.appwidget.AppWidgetProviderInfo +import android.content.ActivityNotFoundException +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo import android.content.pm.UserInfo import android.os.UserHandle import android.provider.Settings import android.widget.RemoteViews +import androidx.activity.result.ActivityResultLauncher import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger @@ -39,6 +45,7 @@ import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.media.controls.ui.view.MediaHost @@ -46,15 +53,19 @@ import com.android.systemui.settings.fakeUserTracker import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository import com.android.systemui.testKosmos -import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito +import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -64,6 +75,8 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { @Mock private lateinit var mediaHost: MediaHost @Mock private lateinit var uiEventLogger: UiEventLogger @Mock private lateinit var providerInfo: AppWidgetProviderInfo + @Mock private lateinit var packageManager: PackageManager + @Mock private lateinit var activityResultLauncher: ActivityResultLauncher<Intent> private val kosmos = testKosmos() private val testScope = kosmos.testScope @@ -73,6 +86,8 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { private lateinit var smartspaceRepository: FakeSmartspaceRepository private lateinit var mediaRepository: FakeCommunalMediaRepository + private val testableResources = context.orCreateTestableResources + private lateinit var underTest: CommunalEditModeViewModel @Before @@ -96,6 +111,7 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { mediaHost, uiEventLogger, logcatLogBuffer("CommunalEditModeViewModelTest"), + kosmos.testDispatcher, ) } @@ -217,7 +233,69 @@ class CommunalEditModeViewModelTest : SysuiTestCase() { verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL) } + @Test + fun onOpenWidgetPicker_launchesWidgetPickerActivity() { + testScope.runTest { + whenever(packageManager.resolveActivity(any(), anyInt())).then { + ResolveInfo().apply { + activityInfo = ActivityInfo().apply { packageName = WIDGET_PICKER_PACKAGE_NAME } + } + } + + val success = + underTest.onOpenWidgetPicker( + testableResources.resources, + packageManager, + activityResultLauncher + ) + + verify(activityResultLauncher).launch(any()) + assertTrue(success) + } + } + + @Test + fun onOpenWidgetPicker_launcherActivityNotResolved_doesNotLaunchWidgetPickerActivity() { + testScope.runTest { + whenever(packageManager.resolveActivity(any(), anyInt())).thenReturn(null) + + val success = + underTest.onOpenWidgetPicker( + testableResources.resources, + packageManager, + activityResultLauncher + ) + + verify(activityResultLauncher, never()).launch(any()) + assertFalse(success) + } + } + + @Test + fun onOpenWidgetPicker_activityLaunchThrowsException_failure() { + testScope.runTest { + whenever(packageManager.resolveActivity(any(), anyInt())).then { + ResolveInfo().apply { + activityInfo = ActivityInfo().apply { packageName = WIDGET_PICKER_PACKAGE_NAME } + } + } + + whenever(activityResultLauncher.launch(any())) + .thenThrow(ActivityNotFoundException::class.java) + + val success = + underTest.onOpenWidgetPicker( + testableResources.resources, + packageManager, + activityResultLauncher, + ) + + assertFalse(success) + } + } + private companion object { val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN) + const val WIDGET_PICKER_PACKAGE_NAME = "widget_picker_package_name" } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt index 769caaa8454f..36458ede9506 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt @@ -270,12 +270,61 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { } @Test + fun transitionValue_canceled_toAnotherState() = + testScope.runTest { + val transitionValuesGone by collectValues(underTest.transitionValue(state = GONE)) + val transitionValuesAod by collectValues(underTest.transitionValue(state = AOD)) + val transitionValuesLs by collectValues(underTest.transitionValue(state = LOCKSCREEN)) + + listOf( + TransitionStep(GONE, AOD, 0f, STARTED), + TransitionStep(GONE, AOD, 0.5f, RUNNING), + TransitionStep(GONE, AOD, 0.5f, CANCELED), + TransitionStep(AOD, LOCKSCREEN, 0.5f, STARTED), + TransitionStep(AOD, LOCKSCREEN, 0.7f, RUNNING), + TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED), + ) + .forEach { + repository.sendTransitionStep(it) + runCurrent() + } + + assertThat(transitionValuesGone).isEqualTo(listOf(1f, 0.5f, 0f)) + assertThat(transitionValuesAod).isEqualTo(listOf(0f, 0.5f, 0.5f, 0.3f, 0f)) + assertThat(transitionValuesLs).isEqualTo(listOf(0.5f, 0.7f, 1f)) + } + + @Test + fun transitionValue_canceled_backToOriginalState() = + testScope.runTest { + val transitionValuesGone by collectValues(underTest.transitionValue(state = GONE)) + val transitionValuesAod by collectValues(underTest.transitionValue(state = AOD)) + + listOf( + TransitionStep(GONE, AOD, 0f, STARTED), + TransitionStep(GONE, AOD, 0.5f, RUNNING), + TransitionStep(GONE, AOD, 1f, CANCELED), + TransitionStep(AOD, GONE, 0.5f, STARTED), + TransitionStep(AOD, GONE, 0.7f, RUNNING), + TransitionStep(AOD, GONE, 1f, FINISHED), + ) + .forEach { + repository.sendTransitionStep(it) + runCurrent() + } + + assertThat(transitionValuesGone).isEqualTo(listOf(1f, 0.5f, 0.5f, 0.7f, 1f)) + assertThat(transitionValuesAod).isEqualTo(listOf(0f, 0.5f, 0.5f, 0.3f, 0f)) + } + + @Test fun isInTransitionToAnyState() = testScope.runTest { val inTransition by collectValues(underTest.isInTransitionToAnyState) assertEquals( listOf( + false, true, // The repo is seeded with a transition from OFF to LOCKSCREEN. false, ), @@ -288,6 +337,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { assertEquals( listOf( + false, true, false, true, @@ -301,6 +351,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { assertEquals( listOf( + false, true, false, true, @@ -314,6 +365,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { assertEquals( listOf( + false, true, false, true, @@ -330,6 +382,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { assertEquals( listOf( + false, true, false, ), @@ -345,6 +398,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { assertEquals( listOf( + false, true, false, true, @@ -359,6 +413,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { assertEquals( listOf( + false, true, false, true, @@ -379,6 +434,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { assertEquals( listOf( + false, true, false, true, @@ -398,6 +454,7 @@ class KeyguardTransitionInteractorTest : SysuiTestCase() { assertEquals( listOf( + false, true, false, true, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt index d4438516a023..0cc0c2fb530b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModelTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel 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.coroutines.collectValues import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic @@ -32,6 +33,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -49,9 +51,7 @@ class AlternateBouncerToGoneTransitionViewModelTest : SysuiTestCase() { } private val testScope = kosmos.testScope private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository - private val underTest by lazy { - kosmos.alternateBouncerToGoneTransitionViewModel - } + private val underTest by lazy { kosmos.alternateBouncerToGoneTransitionViewModel } @Test fun deviceEntryParentViewDisappear() = @@ -73,6 +73,61 @@ class AlternateBouncerToGoneTransitionViewModelTest : SysuiTestCase() { values.forEach { assertThat(it).isEqualTo(0f) } } + @Test + fun lockscreenAlpha() = + testScope.runTest { + val startAlpha = 0.6f + val viewState = ViewStateAccessor(alpha = { startAlpha }) + val alpha by collectLastValue(underTest.lockscreenAlpha(viewState)) + runCurrent() + + keyguardTransitionRepository.sendTransitionSteps( + listOf( + step(0f, TransitionState.STARTED), + step(0.25f), + step(0.5f), + step(0.75f), + step(1f), + ), + testScope, + ) + + // Alpha starts at the starting value from ViewStateAccessor. + keyguardTransitionRepository.sendTransitionStep( + step(0f, state = TransitionState.STARTED) + ) + runCurrent() + assertThat(alpha).isEqualTo(startAlpha) + + // Alpha finishes in 200ms out of 500ms, check the alpha at the halfway point. + val progress = 0.2f + keyguardTransitionRepository.sendTransitionStep(step(progress)) + runCurrent() + assertThat(alpha).isEqualTo(0.3f) + + // Alpha ends at 0. + keyguardTransitionRepository.sendTransitionStep(step(1f)) + runCurrent() + assertThat(alpha).isEqualTo(0f) + } + + @Test + fun lockscreenAlpha_zeroInitialAlpha() = + testScope.runTest { + // ViewState starts at 0 alpha. + val viewState = ViewStateAccessor(alpha = { 0f }) + val alpha by collectValues(underTest.lockscreenAlpha(viewState)) + + keyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.ALTERNATE_BOUNCER, + to = GONE, + testScope + ) + + // Alpha starts and ends at 0. + alpha.forEach { assertThat(it).isEqualTo(0f) } + } + private fun step(value: Float, state: TransitionState = RUNNING): TransitionStep { return TransitionStep( from = KeyguardState.ALTERNATE_BOUNCER, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt index 0796af065790..409c55144c6a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelTest.kt @@ -91,27 +91,6 @@ class PrimaryBouncerToLockscreenTransitionViewModelTest : SysuiTestCase() { assertThat(bgViewAlpha).isEqualTo(1f) } - @Test - fun deviceEntryBackgroundViewAlpha_rearFpEnrolled_noUpdates() = - testScope.runTest { - fingerprintPropertyRepository.supportsRearFps() - val bgViewAlpha by collectLastValue(underTest.deviceEntryBackgroundViewAlpha) - keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED)) - assertThat(bgViewAlpha).isNull() - - keyguardTransitionRepository.sendTransitionStep(step(0.5f)) - assertThat(bgViewAlpha).isNull() - - keyguardTransitionRepository.sendTransitionStep(step(.75f)) - assertThat(bgViewAlpha).isNull() - - keyguardTransitionRepository.sendTransitionStep(step(1f)) - assertThat(bgViewAlpha).isNull() - - keyguardTransitionRepository.sendTransitionStep(step(1f, TransitionState.FINISHED)) - assertThat(bgViewAlpha).isNull() - } - private fun step( value: Float, state: TransitionState = TransitionState.RUNNING diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/MediaTestHelper.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/MediaTestHelper.kt new file mode 100644 index 000000000000..8e44932fb38e --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/MediaTestHelper.kt @@ -0,0 +1,43 @@ +/* + * 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.media.controls + +import android.R +import android.app.smartspace.SmartspaceAction +import android.content.Context +import android.graphics.drawable.Icon +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever + +class MediaTestHelper { + companion object { + /** Returns a list of three mocked recommendations */ + fun getValidRecommendationList(context: Context): List<SmartspaceAction> { + val mediaRecommendationItem = + mock<SmartspaceAction> { + whenever(icon) + .thenReturn( + Icon.createWithResource( + context, + R.drawable.ic_media_play, + ) + ) + } + return listOf(mediaRecommendationItem, mediaRecommendationItem, mediaRecommendationItem) + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryTest.kt new file mode 100644 index 000000000000..6c41bc3c1000 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryTest.kt @@ -0,0 +1,124 @@ +/* + * 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.media.controls.data.repository + +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.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.testScope +import com.android.systemui.media.controls.MediaTestHelper +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +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 +@RunWith(AndroidJUnit4::class) +class MediaDataRepositoryTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val underTest: MediaDataRepository = kosmos.mediaDataRepository + + @Test + fun setRecommendation() = + testScope.runTest { + val smartspaceData by collectLastValue(underTest.smartspaceMediaData) + val recommendation = SmartspaceMediaData(isActive = true) + + underTest.setRecommendation(recommendation) + + assertThat(smartspaceData).isEqualTo(recommendation) + } + + @Test + fun addAndRemoveMediaData() = + testScope.runTest { + val entries by collectLastValue(underTest.mediaEntries) + + val firstKey = "key1" + val firstData = MediaData().copy(isPlaying = true) + + val secondKey = "key2" + val secondData = MediaData().copy(resumption = true) + + underTest.addMediaEntry(firstKey, firstData) + underTest.addMediaEntry(secondKey, secondData) + underTest.addMediaEntry(firstKey, firstData.copy(isPlaying = false)) + + assertThat(entries!!.size).isEqualTo(2) + assertThat(entries!![firstKey]).isNotEqualTo(firstData) + + underTest.removeMediaEntry(firstKey) + + assertThat(entries!!.size).isEqualTo(1) + assertThat(entries!![secondKey]).isEqualTo(secondData) + } + + @Test + fun setRecommendationInactive() = + testScope.runTest { + kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, true) + val smartspaceData by collectLastValue(underTest.smartspaceMediaData) + val recommendation = + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + recommendations = MediaTestHelper.getValidRecommendationList(context), + ) + + underTest.setRecommendation(recommendation) + + assertThat(smartspaceData).isEqualTo(recommendation) + + underTest.setRecommendationInactive(KEY_MEDIA_SMARTSPACE) + + assertThat(smartspaceData).isNotEqualTo(recommendation) + assertThat(smartspaceData!!.isActive).isFalse() + } + + @Test + fun dismissRecommendation() = + testScope.runTest { + val smartspaceData by collectLastValue(underTest.smartspaceMediaData) + val recommendation = + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + recommendations = MediaTestHelper.getValidRecommendationList(context), + ) + + underTest.setRecommendation(recommendation) + + assertThat(smartspaceData).isEqualTo(recommendation) + + underTest.dismissSmartspaceRecommendation(KEY_MEDIA_SMARTSPACE) + + assertThat(smartspaceData!!.isActive).isFalse() + } + + companion object { + private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt new file mode 100644 index 000000000000..d39e77da2f55 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryTest.kt @@ -0,0 +1,144 @@ +/* + * 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.media.controls.data.repository + +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.media.controls.MediaTestHelper +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +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 +@RunWith(AndroidJUnit4::class) +class MediaFilterRepositoryTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val underTest: MediaFilterRepository = kosmos.mediaFilterRepository + + @Test + fun addSelectedUserMediaEntry_activeThenInactivate() = + testScope.runTest { + val selectedUserEntries by collectLastValue(underTest.selectedUserEntries) + + val userMedia = MediaData().copy(active = true) + + underTest.addSelectedUserMediaEntry(KEY, userMedia) + + assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia) + + underTest.addSelectedUserMediaEntry(KEY, userMedia.copy(active = false)) + + assertThat(selectedUserEntries?.get(KEY)).isNotEqualTo(userMedia) + assertThat(selectedUserEntries?.get(KEY)?.active).isFalse() + } + + @Test + fun addSelectedUserMediaEntry_thenRemove_returnsBoolean() = + testScope.runTest { + val selectedUserEntries by collectLastValue(underTest.selectedUserEntries) + + val userMedia = MediaData() + + underTest.addSelectedUserMediaEntry(KEY, userMedia) + + assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia) + + assertThat(underTest.removeSelectedUserMediaEntry(KEY, userMedia)).isTrue() + } + + @Test + fun addSelectedUserMediaEntry_thenRemove_returnsValue() = + testScope.runTest { + val selectedUserEntries by collectLastValue(underTest.selectedUserEntries) + + val userMedia = MediaData() + + underTest.addSelectedUserMediaEntry(KEY, userMedia) + + assertThat(selectedUserEntries?.get(KEY)).isEqualTo(userMedia) + + assertThat(underTest.removeSelectedUserMediaEntry(KEY)).isEqualTo(userMedia) + } + + @Test + fun addAllUserMediaEntry_activeThenInactivate() = + testScope.runTest { + val allUserEntries by collectLastValue(underTest.allUserEntries) + + val userMedia = MediaData().copy(active = true) + + underTest.addMediaEntry(KEY, userMedia) + + assertThat(allUserEntries?.get(KEY)).isEqualTo(userMedia) + + underTest.addMediaEntry(KEY, userMedia.copy(active = false)) + + assertThat(allUserEntries?.get(KEY)).isNotEqualTo(userMedia) + assertThat(allUserEntries?.get(KEY)?.active).isFalse() + } + + @Test + fun addAllUserMediaEntry_thenRemove_returnsValue() = + testScope.runTest { + val allUserEntries by collectLastValue(underTest.allUserEntries) + + val userMedia = MediaData() + + underTest.addMediaEntry(KEY, userMedia) + + assertThat(allUserEntries?.get(KEY)).isEqualTo(userMedia) + + assertThat(underTest.removeMediaEntry(KEY)).isEqualTo(userMedia) + } + + @Test + fun addActiveRecommendation_thenInactive() = + testScope.runTest { + val smartspaceMediaData by collectLastValue(underTest.smartspaceMediaData) + + val mediaRecommendation = + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + recommendations = MediaTestHelper.getValidRecommendationList(context), + ) + + underTest.setRecommendation(mediaRecommendation) + + assertThat(smartspaceMediaData).isEqualTo(mediaRecommendation) + + underTest.setRecommendation(mediaRecommendation.copy(isActive = false)) + + assertThat(smartspaceMediaData).isNotEqualTo(mediaRecommendation) + assertThat(smartspaceMediaData?.isActive).isFalse() + } + + companion object { + private const val KEY = "KEY" + private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt new file mode 100644 index 000000000000..6e67000b1ab3 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/controls/domain/interactor/MediaCarouselInteractorTest.kt @@ -0,0 +1,199 @@ +/* + * 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.media.controls.domain.interactor + +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.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.testScope +import com.android.systemui.media.controls.MediaTestHelper +import com.android.systemui.media.controls.data.repository.MediaFilterRepository +import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor +import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +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 +@RunWith(AndroidJUnit4::class) +class MediaCarouselInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val mediaFilterRepository: MediaFilterRepository = kosmos.mediaFilterRepository + private val underTest: MediaCarouselInteractor = kosmos.mediaCarouselInteractor + + @Test + fun addUserMediaEntry_activeThenInactivate() = + testScope.runTest { + val hasActiveMediaOrRecommendation by + collectLastValue(underTest.hasActiveMediaOrRecommendation) + val hasActiveMedia by collectLastValue(underTest.hasActiveMedia) + val hasAnyMedia by collectLastValue(underTest.hasAnyMedia) + + val userMedia = MediaData().copy(active = true) + + mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia) + + assertThat(hasActiveMediaOrRecommendation).isTrue() + assertThat(hasActiveMedia).isTrue() + assertThat(hasAnyMedia).isTrue() + + mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia.copy(active = false)) + + assertThat(hasActiveMediaOrRecommendation).isFalse() + assertThat(hasActiveMedia).isFalse() + assertThat(hasAnyMedia).isTrue() + } + + @Test + fun addInactiveUserMediaEntry_thenRemove() = + testScope.runTest { + val hasActiveMediaOrRecommendation by + collectLastValue(underTest.hasActiveMediaOrRecommendation) + val hasActiveMedia by collectLastValue(underTest.hasActiveMedia) + val hasAnyMedia by collectLastValue(underTest.hasAnyMedia) + + val userMedia = MediaData().copy(active = false) + + mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia) + + assertThat(hasActiveMediaOrRecommendation).isFalse() + assertThat(hasActiveMedia).isFalse() + assertThat(hasAnyMedia).isTrue() + + assertThat(mediaFilterRepository.removeSelectedUserMediaEntry(KEY, userMedia)).isTrue() + + assertThat(hasActiveMediaOrRecommendation).isFalse() + assertThat(hasActiveMedia).isFalse() + assertThat(hasAnyMedia).isFalse() + } + + @Test + fun addActiveRecommendation_inactiveMedia() = + testScope.runTest { + val hasActiveMediaOrRecommendation by + collectLastValue(underTest.hasActiveMediaOrRecommendation) + val hasAnyMediaOrRecommendation by + collectLastValue(underTest.hasAnyMediaOrRecommendation) + kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false) + + val userMediaRecommendation = + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + recommendations = MediaTestHelper.getValidRecommendationList(context), + ) + val userMedia = MediaData().copy(active = false) + + mediaFilterRepository.setRecommendation(userMediaRecommendation) + + assertThat(hasActiveMediaOrRecommendation).isTrue() + assertThat(hasAnyMediaOrRecommendation).isTrue() + + mediaFilterRepository.addSelectedUserMediaEntry(KEY, userMedia) + + assertThat(hasActiveMediaOrRecommendation).isTrue() + assertThat(hasAnyMediaOrRecommendation).isTrue() + } + + @Test + fun addActiveRecommendation_thenInactive() = + testScope.runTest { + val hasActiveMediaOrRecommendation by + collectLastValue(underTest.hasActiveMediaOrRecommendation) + val hasAnyMediaOrRecommendation by + collectLastValue(underTest.hasAnyMediaOrRecommendation) + kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false) + + val mediaRecommendation = + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + recommendations = MediaTestHelper.getValidRecommendationList(context), + ) + + mediaFilterRepository.setRecommendation(mediaRecommendation) + + assertThat(hasActiveMediaOrRecommendation).isTrue() + assertThat(hasAnyMediaOrRecommendation).isTrue() + + mediaFilterRepository.setRecommendation(mediaRecommendation.copy(isActive = false)) + + assertThat(hasActiveMediaOrRecommendation).isFalse() + assertThat(hasAnyMediaOrRecommendation).isFalse() + } + + @Test + fun addActiveRecommendation_thenInvalid() = + testScope.runTest { + val hasActiveMediaOrRecommendation by + collectLastValue(underTest.hasActiveMediaOrRecommendation) + val hasAnyMediaOrRecommendation by + collectLastValue(underTest.hasAnyMediaOrRecommendation) + kosmos.fakeFeatureFlagsClassic.set(Flags.MEDIA_RETAIN_RECOMMENDATIONS, false) + + val mediaRecommendation = + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + recommendations = MediaTestHelper.getValidRecommendationList(context), + ) + + mediaFilterRepository.setRecommendation(mediaRecommendation) + + assertThat(hasActiveMediaOrRecommendation).isTrue() + assertThat(hasAnyMediaOrRecommendation).isTrue() + + mediaFilterRepository.setRecommendation( + mediaRecommendation.copy(recommendations = listOf()) + ) + + assertThat(hasActiveMediaOrRecommendation).isFalse() + assertThat(hasAnyMediaOrRecommendation).isFalse() + } + + @Test + fun hasAnyMedia_noMediaSet_returnsFalse() = + testScope.runTest { assertThat(underTest.hasAnyMedia.value).isFalse() } + + @Test + fun hasAnyMediaOrRecommendation_noMediaSet_returnsFalse() = + testScope.runTest { assertThat(underTest.hasAnyMediaOrRecommendation.value).isFalse() } + + @Test + fun hasActiveMedia_noMediaSet_returnsFalse() = + testScope.runTest { assertThat(underTest.hasActiveMedia.value).isFalse() } + + @Test + fun hasActiveMediaOrRecommendation_nothingSet_returnsFalse() = + testScope.runTest { assertThat(underTest.hasActiveMediaOrRecommendation.value).isFalse() } + + companion object { + private const val KEY = "KEY" + private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" + } +} 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 c2ce39249f9e..f1cd0c843256 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 @@ -185,7 +185,7 @@ class AlarmTileMapperTest : SysuiTestCase() { setOf(QSTileState.UserAction.CLICK), label, null, - QSTileState.SideViewIcon.None, + QSTileState.SideViewIcon.Chevron, QSTileState.EnabledState.ENABLED, Switch::class.qualifiedName ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt index f24723a2a9f3..97a10e68960f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegateTest.kt @@ -33,7 +33,6 @@ import kotlin.coroutines.EmptyCoroutineContext import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.mock import org.mockito.Mockito.verify /** Test [DataSaverDialogDelegate]. */ @@ -69,7 +68,7 @@ class DataSaverDialogDelegateTest : SysuiTestCase() { fun delegateSetsDialogTitleCorrectly() { val expectedResId = R.string.data_saver_enable_title - dataSaverDialogDelegate.onCreate(sysuiDialog, null) + dataSaverDialogDelegate.beforeCreate(sysuiDialog, null) verify(sysuiDialog).setTitle(eq(expectedResId)) } @@ -78,7 +77,7 @@ class DataSaverDialogDelegateTest : SysuiTestCase() { fun delegateSetsDialogMessageCorrectly() { val expectedResId = R.string.data_saver_description - dataSaverDialogDelegate.onCreate(sysuiDialog, null) + dataSaverDialogDelegate.beforeCreate(sysuiDialog, null) verify(sysuiDialog).setMessage(expectedResId) } @@ -87,7 +86,7 @@ class DataSaverDialogDelegateTest : SysuiTestCase() { fun delegateSetsDialogPositiveButtonCorrectly() { val expectedResId = R.string.data_saver_enable_button - dataSaverDialogDelegate.onCreate(sysuiDialog, null) + dataSaverDialogDelegate.beforeCreate(sysuiDialog, null) verify(sysuiDialog).setPositiveButton(eq(expectedResId), any()) } @@ -96,7 +95,7 @@ class DataSaverDialogDelegateTest : SysuiTestCase() { fun delegateSetsDialogCancelButtonCorrectly() { val expectedResId = R.string.cancel - dataSaverDialogDelegate.onCreate(sysuiDialog, null) + dataSaverDialogDelegate.beforeCreate(sysuiDialog, null) verify(sysuiDialog).setNeutralButton(eq(expectedResId), eq(null)) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractorTest.kt new file mode 100644 index 000000000000..86513006cef1 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractorTest.kt @@ -0,0 +1,93 @@ +/* + * 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.qs.tiles.impl.work.domain.interactor + +import android.os.UserHandle +import android.platform.test.annotations.EnabledOnRavenwood +import android.testing.LeakCheck +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.qs.tiles.base.interactor.DataUpdateTrigger +import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel +import com.android.systemui.utils.leaks.FakeManagedProfileController +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@EnabledOnRavenwood +@RunWith(AndroidJUnit4::class) +class WorkModeTileDataInteractorTest : SysuiTestCase() { + private val controller = FakeManagedProfileController(LeakCheck()) + private val underTest: WorkModeTileDataInteractor = WorkModeTileDataInteractor(controller) + + @Test + fun availability_matchesControllerHasActiveProfiles() = runTest { + val availability by collectLastValue(underTest.availability(TEST_USER)) + + assertThat(availability).isFalse() + + controller.setHasActiveProfile(true) + assertThat(availability).isTrue() + + controller.setHasActiveProfile(false) + assertThat(availability).isFalse() + } + + @Test + fun tileData_whenHasActiveProfile_matchesControllerIsEnabled() = runTest { + controller.setHasActiveProfile(true) + val data by + collectLastValue( + underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)) + ) + + assertThat(data).isInstanceOf(WorkModeTileModel.HasActiveProfile::class.java) + assertThat((data as WorkModeTileModel.HasActiveProfile).isEnabled).isFalse() + + controller.isWorkModeEnabled = true + assertThat(data).isInstanceOf(WorkModeTileModel.HasActiveProfile::class.java) + assertThat((data as WorkModeTileModel.HasActiveProfile).isEnabled).isTrue() + + controller.isWorkModeEnabled = false + assertThat(data).isInstanceOf(WorkModeTileModel.HasActiveProfile::class.java) + assertThat((data as WorkModeTileModel.HasActiveProfile).isEnabled).isFalse() + } + + @Test + fun tileData_matchesControllerHasActiveProfile() = runTest { + val data by + collectLastValue( + underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)) + ) + assertThat(data).isInstanceOf(WorkModeTileModel.NoActiveProfile::class.java) + + controller.setHasActiveProfile(true) + assertThat(data).isInstanceOf(WorkModeTileModel.HasActiveProfile::class.java) + + controller.setHasActiveProfile(false) + assertThat(data).isInstanceOf(WorkModeTileModel.NoActiveProfile::class.java) + } + + private companion object { + val TEST_USER = UserHandle.of(1)!! + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractorTest.kt new file mode 100644 index 000000000000..8a63e2c8800f --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractorTest.kt @@ -0,0 +1,115 @@ +/* + * 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.qs.tiles.impl.work.domain.interactor + +import android.platform.test.annotations.EnabledOnRavenwood +import android.provider.Settings +import android.testing.LeakCheck +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.qs.tiles.base.actions.FakeQSTileIntentUserInputHandler +import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandlerSubject +import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx +import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel +import com.android.systemui.utils.leaks.FakeManagedProfileController +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@EnabledOnRavenwood +@RunWith(AndroidJUnit4::class) +class WorkModeTileUserActionInteractorTest : SysuiTestCase() { + + private val inputHandler = FakeQSTileIntentUserInputHandler() + private val profileController = FakeManagedProfileController(LeakCheck()) + + private val underTest = + WorkModeTileUserActionInteractor( + profileController, + inputHandler, + ) + + @Test + fun handleClickWhenEnabled() = runTest { + val wasEnabled = true + profileController.isWorkModeEnabled = wasEnabled + + underTest.handleInput( + QSTileInputTestKtx.click(WorkModeTileModel.HasActiveProfile(wasEnabled)) + ) + + assertThat(profileController.isWorkModeEnabled).isEqualTo(!wasEnabled) + } + + @Test + fun handleClickWhenDisabled() = runTest { + val wasEnabled = false + profileController.isWorkModeEnabled = wasEnabled + + underTest.handleInput( + QSTileInputTestKtx.click(WorkModeTileModel.HasActiveProfile(wasEnabled)) + ) + + assertThat(profileController.isWorkModeEnabled).isEqualTo(!wasEnabled) + } + + @Test + fun handleClickWhenUnavailable() = runTest { + val wasEnabled = false + profileController.isWorkModeEnabled = wasEnabled + + underTest.handleInput(QSTileInputTestKtx.click(WorkModeTileModel.NoActiveProfile)) + + assertThat(profileController.isWorkModeEnabled).isEqualTo(wasEnabled) + } + + @Test + fun handleLongClickWhenDisabled() = runTest { + val enabled = false + + underTest.handleInput( + QSTileInputTestKtx.longClick(WorkModeTileModel.HasActiveProfile(enabled)) + ) + + QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { + assertThat(it.intent.action).isEqualTo(Settings.ACTION_MANAGED_PROFILE_SETTINGS) + } + } + + @Test + fun handleLongClickWhenEnabled() = runTest { + val enabled = true + + underTest.handleInput( + QSTileInputTestKtx.longClick(WorkModeTileModel.HasActiveProfile(enabled)) + ) + + QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledOneIntentInput { + assertThat(it.intent.action).isEqualTo(Settings.ACTION_MANAGED_PROFILE_SETTINGS) + } + } + + @Test + fun handleLongClickWhenUnavailable() = runTest { + underTest.handleInput(QSTileInputTestKtx.longClick(WorkModeTileModel.NoActiveProfile)) + + QSTileIntentUserInputHandlerSubject.assertThat(inputHandler).handledNoInputs() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt index 6dd425c2afbc..e3fa89c5760d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt @@ -20,9 +20,12 @@ 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.Kosmos import com.android.systemui.kosmos.testScope +import com.android.systemui.shade.data.repository.shadeRepository +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds +import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Test @@ -30,10 +33,9 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) -@android.platform.test.annotations.EnabledOnRavenwood class NotificationStackAppearanceInteractorTest : SysuiTestCase() { - private val kosmos = Kosmos() + private val kosmos = testKosmos() private val testScope = kosmos.testScope private val underTest = kosmos.notificationStackAppearanceInteractor @@ -59,6 +61,18 @@ class NotificationStackAppearanceInteractorTest : SysuiTestCase() { assertThat(stackBounds).isEqualTo(bounds2) } + @Test + fun stackRounding() = + testScope.runTest { + val stackRounding by collectLastValue(underTest.stackRounding) + + kosmos.shadeRepository.setShadeMode(ShadeMode.Single) + assertThat(stackRounding).isEqualTo(StackRounding(roundTop = true, roundBottom = false)) + + kosmos.shadeRepository.setShadeMode(ShadeMode.Split) + assertThat(stackRounding).isEqualTo(StackRounding(roundTop = true, roundBottom = true)) + } + @Test(expected = IllegalStateException::class) fun setStackBounds_withImproperBounds_throwsException() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt index be63301e5749..30564bb6eb84 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/AvalancheControllerTest.kt @@ -60,7 +60,7 @@ class AvalancheControllerTest : SysuiTestCase() { private val mGlobalSettings = FakeGlobalSettings() private val mSystemClock = FakeSystemClock() private val mExecutor = FakeExecutor(mSystemClock) - private var testableHeadsUpManager: BaseHeadsUpManager? = null + private lateinit var testableHeadsUpManager: BaseHeadsUpManager @Before fun setUp() { @@ -88,20 +88,15 @@ class AvalancheControllerTest : SysuiTestCase() { } private fun createHeadsUpEntry(id: Int): BaseHeadsUpManager.HeadsUpEntry { - val entry = testableHeadsUpManager!!.createHeadsUpEntry() - - entry.setEntry( + return testableHeadsUpManager.createHeadsUpEntry( NotificationEntryBuilder() .setSbn(HeadsUpManagerTestUtil.createSbn(id, Notification.Builder(mContext, ""))) .build() ) - return entry } private fun createFsiHeadsUpEntry(id: Int): BaseHeadsUpManager.HeadsUpEntry { - val entry = testableHeadsUpManager!!.createHeadsUpEntry() - entry.setEntry(createFullScreenIntentEntry(id, mContext)) - return entry + return testableHeadsUpManager.createHeadsUpEntry(createFullScreenIntentEntry(id, mContext)) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java index ed0d272cd848..3dc449514699 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/BaseHeadsUpManagerTest.java @@ -38,7 +38,6 @@ import static org.mockito.Mockito.when; import android.app.Notification; import android.app.PendingIntent; import android.app.Person; -import android.content.Intent; import android.testing.TestableLooper; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -498,16 +497,16 @@ public class BaseHeadsUpManagerTest extends SysuiTestCase { public void testAlertEntryCompareTo_ongoingCallLessThanActiveRemoteInput() { final BaseHeadsUpManager hum = createHeadsUpManager(); - final BaseHeadsUpManager.HeadsUpEntry ongoingCall = hum.new HeadsUpEntry(); - ongoingCall.setEntry(new NotificationEntryBuilder() - .setSbn(HeadsUpManagerTestUtil.createSbn(/* id = */ 0, - new Notification.Builder(mContext, "") - .setCategory(Notification.CATEGORY_CALL) - .setOngoing(true))) - .build()); + final BaseHeadsUpManager.HeadsUpEntry ongoingCall = hum.new HeadsUpEntry( + new NotificationEntryBuilder() + .setSbn(HeadsUpManagerTestUtil.createSbn(/* id = */ 0, + new Notification.Builder(mContext, "") + .setCategory(Notification.CATEGORY_CALL) + .setOngoing(true))) + .build()); - final BaseHeadsUpManager.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry(); - activeRemoteInput.setEntry(HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext)); + final BaseHeadsUpManager.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry( + HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext)); activeRemoteInput.mRemoteInputActive = true; assertThat(ongoingCall.compareTo(activeRemoteInput)).isLessThan(0); @@ -518,18 +517,18 @@ public class BaseHeadsUpManagerTest extends SysuiTestCase { public void testAlertEntryCompareTo_incomingCallLessThanActiveRemoteInput() { final BaseHeadsUpManager hum = createHeadsUpManager(); - final BaseHeadsUpManager.HeadsUpEntry incomingCall = hum.new HeadsUpEntry(); final Person person = new Person.Builder().setName("person").build(); final PendingIntent intent = mock(PendingIntent.class); - incomingCall.setEntry(new NotificationEntryBuilder() - .setSbn(HeadsUpManagerTestUtil.createSbn(/* id = */ 0, - new Notification.Builder(mContext, "") - .setStyle(Notification.CallStyle - .forIncomingCall(person, intent, intent)))) - .build()); - - final BaseHeadsUpManager.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry(); - activeRemoteInput.setEntry(HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext)); + final BaseHeadsUpManager.HeadsUpEntry incomingCall = hum.new HeadsUpEntry( + new NotificationEntryBuilder() + .setSbn(HeadsUpManagerTestUtil.createSbn(/* id = */ 0, + new Notification.Builder(mContext, "") + .setStyle(Notification.CallStyle + .forIncomingCall(person, intent, intent)))) + .build()); + + final BaseHeadsUpManager.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry( + HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext)); activeRemoteInput.mRemoteInputActive = true; assertThat(incomingCall.compareTo(activeRemoteInput)).isLessThan(0); @@ -541,8 +540,7 @@ public class BaseHeadsUpManagerTest extends SysuiTestCase { final BaseHeadsUpManager hum = createHeadsUpManager(); // Needs full screen intent in order to be pinned - final BaseHeadsUpManager.HeadsUpEntry entryToPin = hum.new HeadsUpEntry(); - entryToPin.setEntry( + final BaseHeadsUpManager.HeadsUpEntry entryToPin = hum.new HeadsUpEntry( HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id = */ 0, mContext)); // Note: the standard way to show a notification would be calling showNotification rather diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java index d8f77f054b49..3c9dc6345d31 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/TestableHeadsUpManager.java @@ -54,9 +54,10 @@ class TestableHeadsUpManager extends BaseHeadsUpManager { mStickyForSomeTimeAutoDismissTime = BaseHeadsUpManagerTest.TEST_STICKY_AUTO_DISMISS_TIME; } + @NonNull @Override - protected HeadsUpEntry createHeadsUpEntry() { - mLastCreatedEntry = spy(super.createHeadsUpEntry()); + protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) { + mLastCreatedEntry = spy(super.createHeadsUpEntry(entry)); return mLastCreatedEntry; } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt new file mode 100644 index 000000000000..a5ad3c325e51 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt @@ -0,0 +1,68 @@ +/* + * 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.util.kotlin + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.DisposableHandle +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class DisposableHandlesTest : SysuiTestCase() { + @Test + fun disposeWorksOnce() { + var handleDisposeCount = 0 + val underTest = DisposableHandles() + + // Add a handle + underTest += DisposableHandle { handleDisposeCount++ } + + // dispose() calls dispose() on children + underTest.dispose() + assertThat(handleDisposeCount).isEqualTo(1) + + // Once disposed, children are not disposed again + underTest.dispose() + assertThat(handleDisposeCount).isEqualTo(1) + } + + @Test + fun replaceCallsDispose() { + var handleDisposeCount1 = 0 + var handleDisposeCount2 = 0 + val underTest = DisposableHandles() + val handle1 = DisposableHandle { handleDisposeCount1++ } + val handle2 = DisposableHandle { handleDisposeCount2++ } + + // First add handle1 + underTest += handle1 + + // replace() calls dispose() on existing children + underTest.replaceAll(handle2) + assertThat(handleDisposeCount1).isEqualTo(1) + assertThat(handleDisposeCount2).isEqualTo(0) + + // Once disposed, replaced children are not disposed again + underTest.dispose() + assertThat(handleDisposeCount1).isEqualTo(1) + assertThat(handleDisposeCount2).isEqualTo(1) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt index 3d936545bbb3..5358a6dbb476 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioVolumeInteractorTest.kt @@ -200,6 +200,15 @@ class AudioVolumeInteractorTest : SysuiTestCase() { } } + @Test + fun alarmStream_isNotMutable() { + with(kosmos) { + val isMutable = underTest.isMutable(AudioStream(AudioManager.STREAM_ALARM)) + + assertThat(isMutable).isFalse() + } + } + private companion object { val audioStream = AudioStream(AudioManager.STREAM_SYSTEM) } diff --git a/packages/SystemUI/res/layout/screenshot_shelf.xml b/packages/SystemUI/res/layout/screenshot_shelf.xml new file mode 100644 index 000000000000..ef1a21f2fdf6 --- /dev/null +++ b/packages/SystemUI/res/layout/screenshot_shelf.xml @@ -0,0 +1,160 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<com.android.systemui.screenshot.ui.ScreenshotShelfView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <ImageView + android:id="@+id/actions_container_background" + android:visibility="gone" + android:layout_height="0dp" + android:layout_width="0dp" + android:elevation="4dp" + android:background="@drawable/action_chip_container_background" + android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" + android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/actions_container" + app:layout_constraintEnd_toEndOf="@+id/actions_container" + app:layout_constraintBottom_toTopOf="@id/guideline"/> + <HorizontalScrollView + android:id="@+id/actions_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal" + android:paddingEnd="@dimen/overlay_action_container_padding_end" + android:paddingVertical="@dimen/overlay_action_container_padding_vertical" + android:elevation="4dp" + android:scrollbars="none" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintWidth_percent="1.0" + app:layout_constraintWidth_max="wrap" + app:layout_constraintStart_toEndOf="@+id/screenshot_preview_border" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="@id/actions_container_background"> + <LinearLayout + android:id="@+id/screenshot_actions" + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + <include layout="@layout/overlay_action_chip" + android:id="@+id/screenshot_share_chip"/> + <include layout="@layout/overlay_action_chip" + android:id="@+id/screenshot_edit_chip"/> + <include layout="@layout/overlay_action_chip" + android:id="@+id/screenshot_scroll_chip" + android:visibility="gone" /> + </LinearLayout> + </HorizontalScrollView> + <View + android:id="@+id/screenshot_preview_border" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginStart="16dp" + android:layout_marginTop="@dimen/overlay_border_width_neg" + android:layout_marginEnd="@dimen/overlay_border_width_neg" + android:layout_marginBottom="14dp" + android:elevation="8dp" + android:background="@drawable/overlay_border" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/screenshot_preview" + app:layout_constraintEnd_toEndOf="@id/screenshot_preview" + app:layout_constraintBottom_toBottomOf="parent"/> + <ImageView + android:id="@+id/screenshot_preview" + android:layout_width="@dimen/overlay_x_scale" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/overlay_border_width" + android:layout_marginBottom="@dimen/overlay_border_width" + android:layout_gravity="center" + android:elevation="8dp" + android:contentDescription="@string/screenshot_edit_description" + android:scaleType="fitEnd" + android:background="@drawable/overlay_preview_background" + android:adjustViewBounds="true" + android:clickable="true" + app:layout_constraintStart_toStartOf="@id/screenshot_preview_border" + app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"/> + <ImageView + android:id="@+id/screenshot_badge" + android:layout_width="56dp" + android:layout_height="56dp" + android:visibility="gone" + android:elevation="9dp" + app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border" + app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"/> + <FrameLayout + android:id="@+id/screenshot_dismiss_button" + android:layout_width="@dimen/overlay_dismiss_button_tappable_size" + android:layout_height="@dimen/overlay_dismiss_button_tappable_size" + android:elevation="11dp" + android:visibility="gone" + app:layout_constraintStart_toEndOf="@id/screenshot_preview" + app:layout_constraintEnd_toEndOf="@id/screenshot_preview" + app:layout_constraintTop_toTopOf="@id/screenshot_preview" + app:layout_constraintBottom_toTopOf="@id/screenshot_preview" + android:contentDescription="@string/screenshot_dismiss_description"> + <ImageView + android:id="@+id/screenshot_dismiss_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_margin="@dimen/overlay_dismiss_button_margin" + android:background="@drawable/circular_background" + android:backgroundTint="?androidprv:attr/materialColorPrimary" + android:tint="?androidprv:attr/materialColorOnPrimary" + android:padding="4dp" + android:src="@drawable/ic_close"/> + </FrameLayout> + <ImageView + android:id="@+id/screenshot_scrollable_preview" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:scaleType="matrix" + android:visibility="gone" + app:layout_constraintStart_toStartOf="@id/screenshot_preview" + app:layout_constraintTop_toTopOf="@id/screenshot_preview" + android:elevation="7dp"/> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_end="0dp" /> + + <FrameLayout + android:id="@+id/screenshot_message_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/overlay_action_container_margin_horizontal" + android:layout_marginTop="4dp" + android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" + android:paddingHorizontal="@dimen/overlay_action_container_padding_end" + android:paddingVertical="@dimen/overlay_action_container_padding_vertical" + android:elevation="4dp" + android:background="@drawable/action_chip_container_background" + android:visibility="gone" + app:layout_constraintTop_toBottomOf="@id/guideline" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintWidth_max="450dp" + app:layout_constraintHorizontal_bias="0"> + <include layout="@layout/screenshot_work_profile_first_run" /> + <include layout="@layout/screenshot_detection_notice" /> + </FrameLayout> +</com.android.systemui.screenshot.ui.ScreenshotShelfView> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index bf5eeb9e8294..3029888c7e54 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -235,6 +235,8 @@ <string name="screenshot_edit_label">Edit</string> <!-- Content description indicating that tapping the element will allow editing the screenshot [CHAR LIMIT=NONE] --> <string name="screenshot_edit_description">Edit screenshot</string> + <!-- Label for UI element which allows sharing the screenshot [CHAR LIMIT=30] --> + <string name="screenshot_share_label">Share</string> <!-- Content description indicating that tapping the element will allow sharing the screenshot [CHAR LIMIT=NONE] --> <string name="screenshot_share_description">Share screenshot</string> <!-- Label for UI element which allows the user to capture additional off-screen content in a screenshot. [CHAR LIMIT=30] --> @@ -2011,6 +2013,10 @@ <string name="system_multitasking_lhs">Enter split screen with current app to LHS</string> <!-- User visible title for the keyboard shortcut that switches from split screen to full screen [CHAR LIMIT=70] --> <string name="system_multitasking_full_screen">Switch from split screen to full screen</string> + <!-- User visible title for the keyboard shortcut that switches to app on right or below while using split screen [CHAR LIMIT=70] --> + <string name="system_multitasking_splitscreen_focus_rhs">Switch to app on right or below while using split screen</string> + <!-- User visible title for the keyboard shortcut that switches to app on left or above while using split screen [CHAR LIMIT=70] --> + <string name="system_multitasking_splitscreen_focus_lhs">Switch to app on left or above while using split screen</string> <!-- User visible title for the keyboard shortcut that replaces an app from one to another during split screen [CHAR LIMIT=70] --> <string name="system_multitasking_replace">During split screen: replace an app from one to another</string> diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 8a2245d3d14c..48271dea31d8 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -31,7 +31,6 @@ import android.view.ViewTreeObserver.OnGlobalLayoutListener import androidx.annotation.VisibleForTesting import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.customization.R import com.android.systemui.dagger.qualifiers.Background @@ -39,6 +38,7 @@ import com.android.systemui.dagger.qualifiers.DisplaySpecific import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags.REGION_SAMPLING +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState.AOD @@ -328,7 +328,7 @@ constructor( object : KeyguardUpdateMonitorCallback() { override fun onKeyguardVisibilityChanged(visible: Boolean) { isKeyguardVisible = visible - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { if (!isKeyguardVisible) { clock?.run { smallClock.animations.doze(if (isDozing) 1f else 0f) @@ -368,7 +368,7 @@ constructor( } private fun refreshTime() { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } @@ -427,7 +427,7 @@ constructor( parent.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { listenForDozing(this) - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { listenForDozeAmountTransition(this) listenForAnyStateToAodTransition(this) } else { diff --git a/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt b/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt index 630610d1a85f..df77a58c3b34 100644 --- a/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt +++ b/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt @@ -30,7 +30,7 @@ import android.view.WindowManager import android.widget.FrameLayout import android.widget.FrameLayout.LayoutParams import com.android.keyguard.dagger.KeyguardStatusViewComponent -import com.android.systemui.Flags.migrateClocksToBlueprint +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockFaceController import com.android.systemui.res.R @@ -95,7 +95,7 @@ constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { onCreateV2() } else { onCreate() @@ -132,7 +132,7 @@ constructor( } override fun onAttachedToWindow() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { clockRegistry.registerClockChangeListener(clockChangedListener) clockEventController.registerListeners(clock!!) @@ -141,7 +141,7 @@ constructor( } override fun onDetachedFromWindow() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { clockEventController.unregisterListeners() clockRegistry.unregisterClockChangeListener(clockChangedListener) } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java index 28013c6c8289..4a96e9e0845a 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java @@ -3,7 +3,6 @@ package com.android.keyguard; import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_X_CLOCK_DESIGN; import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_DESIGN; import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_SIZE; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -23,6 +22,7 @@ import androidx.core.content.res.ResourcesCompat; import com.android.app.animation.Interpolators; import com.android.keyguard.dagger.KeyguardStatusViewScope; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.log.LogBuffer; import com.android.systemui.log.core.LogLevel; import com.android.systemui.plugins.clocks.ClockController; @@ -192,7 +192,7 @@ public class KeyguardClockSwitch extends RelativeLayout { @Override protected void onFinishInflate() { super.onFinishInflate(); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mSmallClockFrame = findViewById(R.id.lockscreen_clock_view); mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large); mStatusArea = findViewById(R.id.keyguard_status_area); @@ -266,7 +266,7 @@ public class KeyguardClockSwitch extends RelativeLayout { } void updateClockTargetRegions() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } if (mClock != null) { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index e621ffe4cbc4..5b8eb9d3da82 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -21,7 +21,6 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.android.keyguard.KeyguardClockSwitch.LARGE; import static com.android.keyguard.KeyguardClockSwitch.SMALL; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import static com.android.systemui.Flags.smartspaceRelocateToBottom; import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED; import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; @@ -45,6 +44,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlagsClassic; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager; @@ -202,7 +202,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mClockChangedListener = new ClockRegistry.ClockChangeListener() { @Override public void onCurrentClockChanged() { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { setClock(mClockRegistry.createCurrentClock()); } } @@ -245,7 +245,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS protected void onInit() { mKeyguardSliceViewController.init(); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mSmallClockFrame = mView.findViewById(R.id.lockscreen_clock_view); mLargeClockFrame = mView.findViewById(R.id.lockscreen_clock_view_large); } @@ -340,7 +340,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS addDateWeatherView(); } } - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { setDateWeatherVisibility(); setWeatherVisibility(); } @@ -348,7 +348,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } int getNotificationIconAreaHeight() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return 0; } else if (NotificationIconContainerRefactor.isEnabled()) { return mAodIconContainer != null ? mAodIconContainer.getHeight() : 0; @@ -391,7 +391,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } private void addDateWeatherView() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } mDateWeatherView = (ViewGroup) mSmartspaceController.buildAndConnectDateView(mView); @@ -407,7 +407,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } private void addWeatherView() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( @@ -420,7 +420,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } private void addSmartspaceView() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } @@ -528,7 +528,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS */ void updatePosition(int x, float scale, AnimationProperties props, boolean animate) { x = getCurrentLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? -x : x; - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { PropertyAnimator.setProperty(mSmallClockFrame, AnimatableProperty.TRANSLATION_X, x, props, animate); PropertyAnimator.setProperty(mLargeClockFrame, AnimatableProperty.SCALE_X, @@ -554,7 +554,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS return 0; } - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return 0; } @@ -589,14 +589,14 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } boolean isClockTopAligned() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return mKeyguardClockInteractor.getClockSize().getValue() == LARGE; } return mLargeClockFrame.getVisibility() != View.VISIBLE; } private void updateAodIcons() { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { NotificationIconContainer nic = (NotificationIconContainer) mView.findViewById( com.android.systemui.res.R.id.left_aligned_notification_icon_container); @@ -616,7 +616,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } private void setClock(ClockController clock) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } if (clock != null && mLogBuffer != null) { @@ -630,8 +630,8 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS @Nullable public ClockController getClock() { - if (migrateClocksToBlueprint()) { - return mKeyguardClockInteractor.getClock(); + if (MigrateClocksToBlueprint.isEnabled()) { + return mKeyguardClockInteractor.getCurrentClock().getValue(); } else { return mClockEventController.getClock(); } @@ -642,7 +642,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS } private void updateDoubleLineClock() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } mCanShowDoubleLineClock = mSecureSettings.getIntForUser( diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java index 7f9ae5e578e6..603a47e8d26e 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java @@ -20,7 +20,6 @@ import static androidx.constraintlayout.widget.ConstraintSet.END; import static androidx.constraintlayout.widget.ConstraintSet.PARENT_ID; import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_CLOCK_MOVE_ANIMATION; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.animation.Animator; @@ -52,6 +51,7 @@ import com.android.keyguard.logging.KeyguardLogger; import com.android.systemui.Dumpable; import com.android.systemui.animation.ViewHierarchyAnimator; import com.android.systemui.dump.DumpManager; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.plugins.clocks.ClockController; import com.android.systemui.power.domain.interactor.PowerInteractor; @@ -223,7 +223,7 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV } mDumpManager.registerDumpable(getInstanceName(), this); - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { startCoroutines(EmptyCoroutineContext.INSTANCE); mView.setVisibility(View.GONE); } @@ -250,7 +250,7 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV @Override protected void onViewAttached() { mStatusArea = mView.findViewById(R.id.keyguard_status_area); - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } @@ -261,7 +261,7 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV @Override protected void onViewDetached() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } @@ -485,7 +485,7 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV boolean splitShadeEnabled, boolean shouldBeCentered, boolean animate) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered); } else { mKeyguardClockSwitchController.setSplitShadeCentered( @@ -503,7 +503,7 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV ConstraintSet constraintSet = new ConstraintSet(); constraintSet.clone(layout); int guideline; - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { guideline = R.id.split_shade_guideline; } else { guideline = R.id.qs_edge_guideline; @@ -548,7 +548,7 @@ public class KeyguardStatusViewController extends ViewController<KeyguardStatusV && clock.getLargeClock().getConfig().getHasCustomPositionUpdatedAnimation(); // When migrateClocksToBlueprint is on, customized clock animation is conducted in // KeyguardClockViewBinder - if (customClockAnimation && !migrateClocksToBlueprint()) { + if (customClockAnimation && !MigrateClocksToBlueprint.isEnabled()) { // Find the clock, so we can exclude it from this transition. FrameLayout clockContainerView = mView.findViewById(R.id.lockscreen_clock_view_large); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java index f5a6cb35b545..fd8b6d5f05e1 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardVisibilityHelper.java @@ -16,7 +16,6 @@ package com.android.keyguard; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.StatusBarState.SHADE; @@ -24,6 +23,7 @@ import android.util.Property; import android.view.View; import com.android.app.animation.Interpolators; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.log.LogBuffer; import com.android.systemui.log.core.LogLevel; import com.android.systemui.statusbar.StatusBarState; @@ -88,7 +88,7 @@ public class KeyguardVisibilityHelper { boolean keyguardFadingAway, boolean goingToFullShade, int oldStatusBarState) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { log("Ignoring KeyguardVisibilityelper, migrateClocksToBlueprint flag on"); return; } @@ -113,7 +113,7 @@ public class KeyguardVisibilityHelper { animProps.setDelay(0).setDuration(160); log("goingToFullShade && !keyguardFadingAway"); } - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { log("Using LockscreenToGoneTransition 1"); } else { PropertyAnimator.setProperty( @@ -171,7 +171,7 @@ public class KeyguardVisibilityHelper { animProps, true /* animate */); } else if (mScreenOffAnimationController.shouldAnimateInKeyguard()) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { log("Using GoneToAodTransition"); mKeyguardViewVisibilityAnimating = false; } else { @@ -187,7 +187,7 @@ public class KeyguardVisibilityHelper { mView.setVisibility(View.VISIBLE); } } else { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { log("Using LockscreenToGoneTransition 2"); } else { log("Direct set Visibility to GONE"); diff --git a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java index 039a2e5a8ffc..8f1a5f79687c 100644 --- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java @@ -22,8 +22,6 @@ import static android.hardware.biometrics.BiometricSourceType.FINGERPRINT; import static com.android.keyguard.LockIconView.ICON_FINGERPRINT; import static com.android.keyguard.LockIconView.ICON_LOCK; import static com.android.keyguard.LockIconView.ICON_UNLOCK; -import static com.android.systemui.Flags.keyguardBottomAreaRefactor; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset; import static com.android.systemui.flags.Flags.DOZING_MIGRATION_1; import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED; @@ -68,6 +66,8 @@ import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.TransitionStep; @@ -453,7 +453,7 @@ public class LockIconViewController implements Dumpable { private void updateLockIconLocation() { final float scaleFactor = mAuthController.getScaleFactor(); final int scaledPadding = (int) (mDefaultPaddingPx * scaleFactor); - if (keyguardBottomAreaRefactor() || migrateClocksToBlueprint()) { + if (KeyguardBottomAreaRefactor.isEnabled() || MigrateClocksToBlueprint.isEnabled()) { mView.getLockIcon().setPadding(scaledPadding, scaledPadding, scaledPadding, scaledPadding); } else { diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java index a0f15efe7025..781f6dda18e8 100644 --- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java +++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java @@ -16,8 +16,6 @@ package com.android.keyguard.dagger; -import static com.android.systemui.Flags.migrateClocksToBlueprint; - import android.content.Context; import android.content.res.Resources; import android.view.LayoutInflater; @@ -28,6 +26,7 @@ import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.plugins.PluginManager; import com.android.systemui.plugins.clocks.ClockMessageBuffers; import com.android.systemui.res.R; @@ -70,7 +69,7 @@ public abstract class ClockRegistryModule { layoutInflater, resources, featureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION), - migrateClocksToBlueprint()), + MigrateClocksToBlueprint.isEnabled()), context.getString(R.string.lockscreen_clock_id_fallback), clockBuffers, /* keepAllLoaded = */ false, diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt index 964eb6f3a613..578389b57a99 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt @@ -54,6 +54,18 @@ constructor( } /** + * Returns a [Flow] that emits a dimension pixel size that is kept in sync with the device + * configuration. + * + * @see android.content.res.Resources.getDimensionPixelSize + */ + fun getDimensionPixelOffset(@DimenRes id: Int): Flow<Int> { + return configurationController.onDensityOrFontScaleChanged.emitOnStart().map { + context.resources.getDimensionPixelOffset(id) + } + } + + /** * Returns a [Flow] that emits a color that is kept in sync with the device theme. * * @see Utils.getColorAttrDefaultColor diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt index bfe751af7154..afa7c37c648e 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -16,24 +16,36 @@ package com.android.systemui.communal.ui.viewmodel +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.Intent +import android.content.pm.PackageManager +import android.content.res.Resources +import android.util.Log +import androidx.activity.result.ActivityResultLauncher import com.android.internal.logging.UiEventLogger import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.log.CommunalUiEvent import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.media.dagger.MediaModule +import com.android.systemui.res.R import javax.inject.Inject import javax.inject.Named +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext /** The view model for communal hub in edit mode. */ @SysUISingleton @@ -45,6 +57,7 @@ constructor( @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost, private val uiEventLogger: UiEventLogger, @CommunalLog logBuffer: LogBuffer, + @Background private val backgroundDispatcher: CoroutineDispatcher, ) : BaseCommunalViewModel(communalInteractor, mediaHost) { private val logger = Logger(logBuffer, "CommunalEditModeViewModel") @@ -86,10 +99,77 @@ constructor( uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL) } - /** Returns the widget categories to show on communal hub. */ - val getCommunalWidgetCategories: Int - get() = communalSettingsInteractor.communalWidgetCategories.value + /** Launch the widget picker activity using the given {@link ActivityResultLauncher}. */ + suspend fun onOpenWidgetPicker( + resources: Resources, + packageManager: PackageManager, + activityLauncher: ActivityResultLauncher<Intent> + ): Boolean = + withContext(backgroundDispatcher) { + val widgets = communalInteractor.widgetContent.first() + val excludeList = widgets.mapTo(ArrayList()) { it.providerInfo } + getWidgetPickerActivityIntent(resources, packageManager, excludeList)?.let { + try { + activityLauncher.launch(it) + return@withContext true + } catch (e: Exception) { + Log.e(TAG, "Failed to launch widget picker activity", e) + } + } + false + } + + private fun getWidgetPickerActivityIntent( + resources: Resources, + packageManager: PackageManager, + excludeList: ArrayList<AppWidgetProviderInfo> + ): Intent? { + val packageName = + getLauncherPackageName(packageManager) + ?: run { + Log.e(TAG, "Couldn't resolve launcher package name") + return@getWidgetPickerActivityIntent null + } + + return Intent(Intent.ACTION_PICK).apply { + setPackage(packageName) + putExtra( + EXTRA_DESIRED_WIDGET_WIDTH, + resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_width) + ) + putExtra( + EXTRA_DESIRED_WIDGET_HEIGHT, + resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_height) + ) + putExtra( + AppWidgetManager.EXTRA_CATEGORY_FILTER, + communalSettingsInteractor.communalWidgetCategories.value + ) + putExtra(EXTRA_UI_SURFACE_KEY, EXTRA_UI_SURFACE_VALUE) + putParcelableArrayListExtra(EXTRA_ADDED_APP_WIDGETS_KEY, excludeList) + } + } + + private fun getLauncherPackageName(packageManager: PackageManager): String? { + return packageManager + .resolveActivity( + Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) }, + PackageManager.MATCH_DEFAULT_ONLY + ) + ?.activityInfo + ?.packageName + } /** Sets whether edit mode is currently open */ fun setEditModeOpen(isOpen: Boolean) = communalInteractor.setEditModeOpen(isOpen) + + companion object { + private const val TAG = "CommunalEditModeViewModel" + + private const val EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width" + private const val EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height" + private const val EXTRA_UI_SURFACE_KEY = "ui_surface" + private const val EXTRA_UI_SURFACE_VALUE = "widgets_hub" + const val EXTRA_ADDED_APP_WIDGETS_KEY = "added_app_widgets" + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt index b6ad26b24dc7..ba18f0125a0a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -16,9 +16,7 @@ package com.android.systemui.communal.widgets -import android.appwidget.AppWidgetManager import android.content.Intent -import android.content.pm.PackageManager import android.os.Bundle import android.os.RemoteException import android.util.Log @@ -32,6 +30,8 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope +import com.android.app.tracing.coroutines.launch import com.android.compose.theme.LocalAndroidColorScheme import com.android.compose.theme.PlatformTheme import com.android.internal.logging.UiEventLogger @@ -43,8 +43,8 @@ import com.android.systemui.communal.util.WidgetPickerIntentUtils.getWidgetExtra import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog -import com.android.systemui.res.R import javax.inject.Inject +import kotlinx.coroutines.launch /** An Activity for editing the widgets that appear in hub mode. */ class EditWidgetsActivity @@ -57,11 +57,8 @@ constructor( @CommunalLog logBuffer: LogBuffer, ) : ComponentActivity() { companion object { - private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag" - private const val EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width" - private const val EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height" - private const val TAG = "EditWidgetsActivity" + private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag" const val EXTRA_PRESELECTED_KEY = "preselected_key" } @@ -136,39 +133,13 @@ constructor( } private fun onOpenWidgetPicker() { - val intent = Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) } - packageManager - .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) - ?.activityInfo - ?.packageName - ?.let { packageName -> - try { - addWidgetActivityLauncher.launch( - Intent(Intent.ACTION_PICK).apply { - setPackage(packageName) - putExtra( - EXTRA_DESIRED_WIDGET_WIDTH, - resources.getDimensionPixelSize( - R.dimen.communal_widget_picker_desired_width - ) - ) - putExtra( - EXTRA_DESIRED_WIDGET_HEIGHT, - resources.getDimensionPixelSize( - R.dimen.communal_widget_picker_desired_height - ) - ) - putExtra( - AppWidgetManager.EXTRA_CATEGORY_FILTER, - communalViewModel.getCommunalWidgetCategories - ) - } - ) - } catch (e: Exception) { - Log.e(TAG, "Failed to launch widget picker activity", e) - } - } - ?: run { Log.e(TAG, "Couldn't resolve launcher package name") } + lifecycleScope.launch { + communalViewModel.onOpenWidgetPicker( + resources, + packageManager, + addWidgetActivityLauncher + ) + } } private fun onEditDone() { diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index 298da1359728..1bcee74d70fc 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -23,13 +23,11 @@ import com.android.server.notification.Flags.crossAppPoliteNotifications import com.android.server.notification.Flags.politeNotifications import com.android.server.notification.Flags.vibrateWhileUnlocked import com.android.systemui.Flags.FLAG_COMMUNAL_HUB -import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR -import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT import com.android.systemui.Flags.communalHub -import com.android.systemui.Flags.keyguardBottomAreaRefactor -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.Flags.MIGRATE_KEYGUARD_STATUS_BAR_VIEW +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.ComposeLockscreen import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor @@ -58,11 +56,11 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha SceneContainerFlag.getMainStaticFlag() dependsOn MIGRATE_KEYGUARD_STATUS_BAR_VIEW // ComposeLockscreen dependencies - ComposeLockscreen.token dependsOn keyguardBottomAreaRefactor - ComposeLockscreen.token dependsOn migrateClocksToBlueprint + ComposeLockscreen.token dependsOn KeyguardBottomAreaRefactor.token + ComposeLockscreen.token dependsOn MigrateClocksToBlueprint.token // CommunalHub dependencies - communalHub dependsOn migrateClocksToBlueprint + communalHub dependsOn MigrateClocksToBlueprint.token } private inline val politeNotifications @@ -71,10 +69,6 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha get() = FlagToken(FLAG_CROSS_APP_POLITE_NOTIFICATIONS, crossAppPoliteNotifications()) private inline val vibrateWhileUnlockedToken: FlagToken get() = FlagToken(FLAG_VIBRATE_WHILE_UNLOCKED, vibrateWhileUnlocked()) - private inline val keyguardBottomAreaRefactor - get() = FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor()) - private inline val migrateClocksToBlueprint - get() = FlagToken(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, migrateClocksToBlueprint()) private inline val communalHub get() = FlagToken(FLAG_COMMUNAL_HUB, communalHub()) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardBottomAreaRefactor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardBottomAreaRefactor.kt new file mode 100644 index 000000000000..779b27b25375 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardBottomAreaRefactor.kt @@ -0,0 +1,53 @@ +/* + * 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 + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the keyguard bottom area refactor flag. */ +@Suppress("NOTHING_TO_INLINE") +object KeyguardBottomAreaRefactor { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.keyguardBottomAreaRefactor() + + /** + * 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 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/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index 5565ee295786..d9d747015abd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -36,7 +36,6 @@ import com.android.keyguard.LockIconView import com.android.keyguard.LockIconViewController import com.android.keyguard.dagger.KeyguardStatusViewComponent import com.android.systemui.CoreStartable -import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor @@ -166,7 +165,7 @@ constructor( fun bindIndicationArea() { indicationAreaHandle?.dispose() - if (!keyguardBottomAreaRefactor()) { + if (!KeyguardBottomAreaRefactor.isEnabled) { keyguardRootView.findViewById<View?>(R.id.keyguard_indication_area)?.let { keyguardRootView.removeView(it) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 3b34750756b4..f700e037f2fe 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -40,7 +40,6 @@ import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STR import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE; import static com.android.systemui.DejankUtils.whitelistIpcs; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import static com.android.systemui.Flags.notifyPowerManagerUserActivityBackground; import static com.android.systemui.Flags.refactorGetCurrentUser; import static com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel.DREAMING_ANIMATION_DURATION_MS; @@ -3404,7 +3403,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } // Ensure that keyguard becomes visible if the going away animation is canceled - if (showKeyguard && !KeyguardWmStateRefactor.isEnabled() && migrateClocksToBlueprint()) { + if (showKeyguard && !KeyguardWmStateRefactor.isEnabled() + && MigrateClocksToBlueprint.isEnabled()) { mKeyguardInteractor.showKeyguard(); } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/MigrateClocksToBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/MigrateClocksToBlueprint.kt new file mode 100644 index 000000000000..5a2943bd00b3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/MigrateClocksToBlueprint.kt @@ -0,0 +1,53 @@ +/* + * 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 + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the migrate clocks to blueprint flag. */ +@Suppress("NOTHING_TO_INLINE") +object MigrateClocksToBlueprint { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.migrateClocksToBlueprint() + + /** + * 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 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/keyguard/data/repository/KeyguardClockRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt index 7ad5aac63837..7a56554be1d2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt @@ -113,7 +113,10 @@ constructor( override val currentClock: StateFlow<ClockController?> = currentClockId - .map { clockRegistry.createCurrentClock() } + .map { + clockEventController.clock = clockRegistry.createCurrentClock() + clockEventController.clock + } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt index 9c68c45476d5..a36bf8bf8751 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -119,24 +119,7 @@ class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitio init { // Seed with transitions signaling a boot into lockscreen state. If updating this, please // also update FakeKeyguardTransitionRepository. - emitTransition( - TransitionStep( - KeyguardState.OFF, - KeyguardState.LOCKSCREEN, - 0f, - TransitionState.STARTED, - KeyguardTransitionRepositoryImpl::class.simpleName!!, - ) - ) - emitTransition( - TransitionStep( - KeyguardState.OFF, - KeyguardState.LOCKSCREEN, - 1f, - TransitionState.FINISHED, - KeyguardTransitionRepositoryImpl::class.simpleName!!, - ) - ) + initialTransitionSteps.forEach(::emitTransition) } override fun startTransition(info: TransitionInfo): UUID? { @@ -256,5 +239,31 @@ class KeyguardTransitionRepositoryImpl @Inject constructor() : KeyguardTransitio companion object { private const val TAG = "KeyguardTransitionRepository" + + /** + * Transition steps to seed the repository with, so that all of the transition interactor + * flows emit reasonable initial values. + */ + val initialTransitionSteps: List<TransitionStep> = + listOf( + TransitionStep( + KeyguardState.OFF, + KeyguardState.OFF, + 1f, + TransitionState.FINISHED, + ), + TransitionStep( + KeyguardState.OFF, + KeyguardState.LOCKSCREEN, + 0f, + TransitionState.STARTED, + ), + TransitionStep( + KeyguardState.OFF, + KeyguardState.LOCKSCREEN, + 1f, + TransitionState.FINISHED, + ), + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt index 9040e031d54e..d09ee54f2029 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAodTransitionInteractor.kt @@ -252,5 +252,6 @@ constructor( val TO_LOCKSCREEN_DURATION = 500.milliseconds val TO_GONE_DURATION = DEFAULT_DURATION val TO_OCCLUDED_DURATION = DEFAULT_DURATION + val TO_PRIMARY_BOUNCER_DURATION = DEFAULT_DURATION } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt index 7f752b468c13..1f24fc23bbdd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt @@ -231,6 +231,7 @@ constructor( private val DEFAULT_DURATION = 500.milliseconds val TO_GLANCEABLE_HUB_DURATION = 1.seconds val TO_LOCKSCREEN_DURATION = 1167.milliseconds + val TO_AOD_DURATION = 300.milliseconds val TO_GONE_DURATION = DEFAULT_DURATION } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt index b9ec58ccb925..53f241684a62 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBottomAreaInteractor.kt @@ -39,7 +39,7 @@ constructor( /** The position of the keyguard clock. */ private val _clockPosition = MutableStateFlow(Position(0, 0)) /** See [ClockSection] */ - @Deprecated("with migrateClocksToBlueprint()") + @Deprecated("with MigrateClocksToBlueprint.isEnabled") val clockPosition: Flow<Position> = _clockPosition.asStateFlow() fun setClockPosition(x: Int, y: Int) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index e6655ee3898f..0cd7d18b2342 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -91,11 +91,46 @@ constructor( } } + val transitions = repository.transitions + + /** + * A pair of the most recent STARTED step, and the transition step immediately preceding it. The + * transition framework enforces that the previous step is either a CANCELED or FINISHED step, + * and that the previous step was *to* the state the STARTED step is *from*. + * + * This flow can be used to access the previous step to determine whether it was CANCELED or + * FINISHED. In the case of a CANCELED step, we can also figure out which state we were coming + * from when we were canceled. + */ + val startedStepWithPrecedingStep = + transitions + .pairwise() + .filter { it.newValue.transitionState == TransitionState.STARTED } + .shareIn(scope, SharingStarted.Eagerly) + init { + // Collect non-canceled steps and emit transition values. scope.launch(mainDispatcher) { - repository.transitions.collect { step -> - getTransitionValueFlow(step.from).emit(1f - step.value) - getTransitionValueFlow(step.to).emit(step.value) + repository.transitions + .filter { it.transitionState != TransitionState.CANCELED } + .collect { step -> + getTransitionValueFlow(step.from).emit(1f - step.value) + getTransitionValueFlow(step.to).emit(step.value) + } + } + + // If a transition from state A -> B is canceled in favor of a transition from B -> C, we + // need to ensure we emit transitionValue(A) = 0f, since no further steps will be emitted + // where the from or to states are A. This would leave transitionValue(A) stuck at an + // arbitrary non-zero value. + scope.launch(mainDispatcher) { + startedStepWithPrecedingStep.collect { (prevStep, startedStep) -> + if ( + prevStep.transitionState == TransitionState.CANCELED && + startedStep.to != prevStep.from + ) { + getTransitionValueFlow(prevStep.from).emit(0f) + } } } } @@ -202,8 +237,6 @@ constructor( val dozingToLockscreenTransition: Flow<TransitionStep> = repository.transition(DOZING, LOCKSCREEN) - val transitions = repository.transitions - /** Receive all [TransitionStep] matching a filter of [from]->[to] */ fun transition(from: KeyguardState, to: KeyguardState): Flow<TransitionStep> { return repository.transition(from, to) @@ -250,21 +283,6 @@ constructor( .stateIn(scope, SharingStarted.Eagerly, DOZING) /** - * A pair of the most recent STARTED step, and the transition step immediately preceding it. The - * transition framework enforces that the previous step is either a CANCELED or FINISHED step, - * and that the previous step was *to* the state the STARTED step is *from*. - * - * This flow can be used to access the previous step to determine whether it was CANCELED or - * FINISHED. In the case of a CANCELED step, we can also figure out which state we were coming - * from when we were canceled. - */ - val startedStepWithPrecedingStep = - transitions - .pairwise() - .filter { it.newValue.transitionState == TransitionState.STARTED } - .stateIn(scope, SharingStarted.Eagerly, null) - - /** * The last [KeyguardState] to which we [TransitionState.FINISHED] a transition. * * WARNING: This will NOT emit a value if a transition is CANCELED, and will also not emit a diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt index 4812e03ec3f6..7e3ddf92c530 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt @@ -26,9 +26,9 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.BaseBlueprintTransition import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config @@ -105,7 +105,7 @@ constructor( var transition = if ( - !keyguardBottomAreaRefactor() && + !KeyguardBottomAreaRefactor.isEnabled && prevBluePrint != null && prevBluePrint != blueprint ) { @@ -213,9 +213,10 @@ constructor( cs: ConstraintSet, viewModel: KeyguardClockViewModel ) { - if (!DEBUG || viewModel.clock == null) return + val currentClock = viewModel.currentClock.value + if (!DEBUG || currentClock == null) return val smallClockViewId = R.id.lockscreen_clock_view - val largeClockViewId = viewModel.clock!!.largeClock.layout.views[0].id + val largeClockViewId = currentClock.largeClock.layout.views[0].id Log.i( TAG, "applyCsToSmallClock: vis=${cs.getVisibility(smallClockViewId)} " + diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt index 01596ed2e3ef..6255f0d44609 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.binder import android.transition.TransitionManager import android.transition.TransitionSet import android.view.View.INVISIBLE +import android.view.ViewGroup import androidx.annotation.VisibleForTesting import androidx.constraintlayout.helper.widget.Layer import androidx.constraintlayout.widget.ConstraintLayout @@ -27,7 +28,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.keyguard.KeyguardClockSwitch.LARGE import com.android.keyguard.KeyguardClockSwitch.SMALL -import com.android.systemui.Flags.migrateClocksToBlueprint +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type @@ -40,7 +41,8 @@ import kotlinx.coroutines.launch object KeyguardClockViewBinder { private val TAG = KeyguardClockViewBinder::class.simpleName!! - + // When changing to new clock, we need to remove old clock views from burnInLayer + private var lastClock: ClockController? = null @JvmStatic fun bind( clockSection: ClockSection, @@ -55,28 +57,27 @@ object KeyguardClockViewBinder { } } keyguardRootView.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { + repeatOnLifecycle(Lifecycle.State.CREATED) { launch { - if (!migrateClocksToBlueprint()) return@launch + if (!MigrateClocksToBlueprint.isEnabled) return@launch viewModel.currentClock.collect { currentClock -> - cleanupClockViews(viewModel.clock, keyguardRootView, viewModel.burnInLayer) - viewModel.clock = currentClock + cleanupClockViews(currentClock, keyguardRootView, viewModel.burnInLayer) addClockViews(currentClock, keyguardRootView) updateBurnInLayer(keyguardRootView, viewModel) applyConstraints(clockSection, keyguardRootView, true) } } launch { - if (!migrateClocksToBlueprint()) return@launch + if (!MigrateClocksToBlueprint.isEnabled) return@launch viewModel.clockSize.collect { updateBurnInLayer(keyguardRootView, viewModel) blueprintInteractor.refreshBlueprint(Type.ClockSize) } } launch { - if (!migrateClocksToBlueprint()) return@launch + if (!MigrateClocksToBlueprint.isEnabled) return@launch viewModel.clockShouldBeCentered.collect { clockShouldBeCentered -> - viewModel.clock?.let { + viewModel.currentClock.value?.let { // Weather clock also has hasCustomPositionUpdatedAnimation as true // TODO(b/323020908): remove ID check if ( @@ -91,9 +92,9 @@ object KeyguardClockViewBinder { } } launch { - if (!migrateClocksToBlueprint()) return@launch + if (!MigrateClocksToBlueprint.isEnabled) return@launch viewModel.isAodIconsVisible.collect { isAodIconsVisible -> - viewModel.clock?.let { + viewModel.currentClock.value?.let { // Weather clock also has hasCustomPositionUpdatedAnimation as true if ( viewModel.useLargeClock && it.config.id == "DIGITAL_CLOCK_WEATHER" @@ -132,11 +133,14 @@ object KeyguardClockViewBinder { } private fun cleanupClockViews( - clockController: ClockController?, + currentClock: ClockController?, rootView: ConstraintLayout, burnInLayer: Layer? ) { - clockController?.let { clock -> + if (lastClock == currentClock) { + return + } + lastClock?.let { clock -> clock.smallClock.layout.views.forEach { burnInLayer?.removeView(it) rootView.removeView(it) @@ -150,6 +154,7 @@ object KeyguardClockViewBinder { } clock.largeClock.layout.views.forEach { rootView.removeView(it) } } + lastClock = currentClock } @VisibleForTesting @@ -157,11 +162,19 @@ object KeyguardClockViewBinder { clockController: ClockController?, rootView: ConstraintLayout, ) { + // We'll collect the same clock when exiting wallpaper picker without changing clock + // so we need to remove clock views from parent before addView again clockController?.let { clock -> clock.smallClock.layout.views.forEach { + if (it.parent != null) { + (it.parent as ViewGroup).removeView(it) + } rootView.addView(it).apply { it.visibility = INVISIBLE } } clock.largeClock.layout.views.forEach { + if (it.parent != null) { + (it.parent as ViewGroup).removeView(it) + } rootView.addView(it).apply { it.visibility = INVISIBLE } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt index 841f52d7aa64..267d68e5e5e1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardIndicationAreaBinder.kt @@ -22,8 +22,8 @@ import android.view.ViewGroup import android.widget.TextView import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.Flags.keyguardBottomAreaRefactor -import com.android.systemui.Flags.migrateClocksToBlueprint +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R @@ -69,7 +69,10 @@ object KeyguardIndicationAreaBinder { launch { // Do not independently apply alpha, as [KeyguardRootViewModel] should work // for this and all its children - if (!(migrateClocksToBlueprint() || keyguardBottomAreaRefactor())) { + if ( + !(MigrateClocksToBlueprint.isEnabled || + KeyguardBottomAreaRefactor.isEnabled) + ) { viewModel.alpha.collect { alpha -> view.alpha = alpha } } } 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 d0246a8cd872..0ed42ef75026 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 @@ -36,8 +36,6 @@ import com.android.app.animation.Interpolators import com.android.internal.jank.InteractionJankMonitor import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD import com.android.keyguard.KeyguardClockSwitch.MISSING_CLOCK_ID -import com.android.systemui.Flags.keyguardBottomAreaRefactor -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.Flags.newAodTransition import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text @@ -45,6 +43,8 @@ import com.android.systemui.common.shared.model.TintedIcon import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters @@ -109,7 +109,7 @@ object KeyguardRootViewBinder { val endButton = R.id.end_button val lockIcon = R.id.lock_icon_view - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { view.setOnTouchListener { _, event -> if (falsingManager?.isFalseTap(FalsingManager.LOW_PENALTY) == false) { viewModel.setRootViewLastTapPosition(Point(event.x.toInt(), event.y.toInt())) @@ -143,11 +143,13 @@ object KeyguardRootViewBinder { } } - if (keyguardBottomAreaRefactor() || DeviceEntryUdfpsRefactor.isEnabled) { + if ( + KeyguardBottomAreaRefactor.isEnabled || DeviceEntryUdfpsRefactor.isEnabled + ) { launch { viewModel.alpha(viewState).collect { alpha -> view.alpha = alpha - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { childViews[statusViewId]?.alpha = alpha childViews[burnInLayerId]?.alpha = alpha } @@ -155,7 +157,7 @@ object KeyguardRootViewBinder { } } - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { launch { viewModel.burnInLayerVisibility.collect { visibility -> childViews[burnInLayerId]?.visibility = visibility @@ -342,13 +344,13 @@ object KeyguardRootViewBinder { } } - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { burnInParams.update { current -> current.copy(clockControllerProvider = clockControllerProvider) } } - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { burnInParams.update { current -> current.copy(translationY = { childViews[burnInLayerId]?.translationY }) } @@ -439,7 +441,7 @@ object KeyguardRootViewBinder { burnInParams.update { current -> current.copy( minViewY = - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { // To ensure burn-in doesn't enroach the top inset, get the min top Y childViews.entries.fold(Int.MAX_VALUE) { currentMin, (viewId, view) -> min( @@ -472,7 +474,7 @@ object KeyguardRootViewBinder { configuration: ConfigurationState, screenOffAnimationController: ScreenOffAnimationController, ) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { throw IllegalStateException("should only be called in legacy code paths") } if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return @@ -503,7 +505,7 @@ object KeyguardRootViewBinder { } when { !isVisible.isAnimating -> { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { translationY = 0f } visibility = @@ -553,7 +555,7 @@ object KeyguardRootViewBinder { animatorListener: Animator.AnimatorListener, ) { if (animate) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { translationY = -iconAppearTranslation.toFloat() } alpha = 0f @@ -561,19 +563,19 @@ object KeyguardRootViewBinder { .alpha(1f) .setInterpolator(Interpolators.LINEAR) .setDuration(AOD_ICONS_APPEAR_DURATION) - .apply { if (migrateClocksToBlueprint()) animateInIconTranslation() } + .apply { if (MigrateClocksToBlueprint.isEnabled) animateInIconTranslation() } .setListener(animatorListener) .start() } else { alpha = 1.0f - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { translationY = 0f } } } private fun View.animateInIconTranslation() { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { animate().animateInIconTranslation().setDuration(AOD_ICONS_APPEAR_DURATION).start() } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt index b77f0c5a1e60..9aebf66aa067 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt @@ -21,7 +21,7 @@ import androidx.constraintlayout.helper.widget.Layer import androidx.constraintlayout.widget.ConstraintLayout import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.Flags.migrateClocksToBlueprint +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Config import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition.Type @@ -41,9 +41,9 @@ object KeyguardSmartspaceViewBinder { blueprintInteractor: KeyguardBlueprintInteractor, ) { keyguardRootView.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { + repeatOnLifecycle(Lifecycle.State.CREATED) { launch { - if (!migrateClocksToBlueprint()) return@launch + if (!MigrateClocksToBlueprint.isEnabled) return@launch clockViewModel.hasCustomWeatherDataDisplay.collect { hasCustomWeatherDataDisplay -> updateDateWeatherToBurnInLayer( @@ -62,7 +62,7 @@ object KeyguardSmartspaceViewBinder { } launch { - if (!migrateClocksToBlueprint()) return@launch + if (!MigrateClocksToBlueprint.isEnabled) return@launch smartspaceViewModel.bcSmartspaceVisibility.collect { updateBCSmartspaceInBurnInLayer(keyguardRootView, clockViewModel) blueprintInteractor.refreshBlueprint( 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 7c76e6afc074..6f3919266e93 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 @@ -50,8 +50,6 @@ import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT import androidx.core.view.isInvisible import com.android.keyguard.ClockEventController import com.android.keyguard.KeyguardClockSwitch -import com.android.systemui.Flags.keyguardBottomAreaRefactor -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.animation.view.LaunchableImageView import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.broadcast.BroadcastDispatcher @@ -61,6 +59,8 @@ import com.android.systemui.communal.ui.viewmodel.CommunalTutorialIndicatorViewM 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.KeyguardBottomAreaRefactor +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardPreviewSmartspaceViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder @@ -90,6 +90,7 @@ import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController import com.android.systemui.statusbar.phone.KeyguardBottomAreaView import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator +import com.android.systemui.util.kotlin.DisposableHandles import com.android.systemui.util.settings.SecureSettings import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -173,7 +174,7 @@ constructor( private lateinit var smallClockHostView: FrameLayout private var smartSpaceView: View? = null - private val disposables = mutableSetOf<DisposableHandle>() + private val disposables = DisposableHandles() private var isDestroyed = false private val shortcutsBindings = mutableSetOf<KeyguardQuickAffordanceViewBinder.Binding>() @@ -183,9 +184,9 @@ constructor( init { coroutineScope = CoroutineScope(applicationScope.coroutineContext + Job()) - disposables.add(DisposableHandle { coroutineScope.cancel() }) + disposables += DisposableHandle { coroutineScope.cancel() } - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { quickAffordancesCombinedViewModel.enablePreviewMode( initiallySelectedSlotId = bundle.getString( @@ -203,7 +204,7 @@ constructor( shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance, ) } - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { clockViewModel.shouldHighlightSelectedAffordance = shouldHighlightSelectedAffordance } runBlocking(mainDispatcher) { @@ -214,7 +215,7 @@ constructor( if (hostToken == null) null else InputTransferToken(hostToken), "KeyguardPreviewRenderer" ) - disposables.add(DisposableHandle { host.release() }) + disposables += DisposableHandle { host.release() } } } @@ -230,7 +231,7 @@ constructor( setupKeyguardRootView(previewContext, rootView) - if (!keyguardBottomAreaRefactor()) { + if (!KeyguardBottomAreaRefactor.isEnabled) { setUpBottomArea(rootView) } @@ -274,7 +275,7 @@ constructor( } fun onSlotSelected(slotId: String) { - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { quickAffordancesCombinedViewModel.onPreviewSlotSelected(slotId = slotId) } else { bottomAreaViewModel.onPreviewSlotSelected(slotId = slotId) @@ -284,8 +285,8 @@ constructor( fun destroy() { isDestroyed = true lockscreenSmartspaceController.disconnect() - disposables.forEach { it.dispose() } - if (keyguardBottomAreaRefactor()) { + disposables.dispose() + if (KeyguardBottomAreaRefactor.isEnabled) { shortcutsBindings.forEach { it.destroy() } } } @@ -371,8 +372,8 @@ constructor( @OptIn(ExperimentalCoroutinesApi::class) private fun setupKeyguardRootView(previewContext: Context, rootView: FrameLayout) { val keyguardRootView = KeyguardRootView(previewContext, null) - if (!keyguardBottomAreaRefactor()) { - disposables.add( + if (!KeyguardBottomAreaRefactor.isEnabled) { + disposables += KeyguardRootViewBinder.bind( keyguardRootView, keyguardRootViewModel, @@ -387,7 +388,6 @@ constructor( null, // device entry haptics not required for preview mode null, // falsing manager not required for preview mode ) - ) } rootView.addView( keyguardRootView, @@ -397,15 +397,18 @@ constructor( ), ) - setUpUdfps(previewContext, if (migrateClocksToBlueprint()) keyguardRootView else rootView) + setUpUdfps( + previewContext, + if (MigrateClocksToBlueprint.isEnabled) keyguardRootView else rootView + ) - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { setupShortcuts(keyguardRootView) } if (!shouldHideClock) { setUpClock(previewContext, rootView) - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { KeyguardPreviewClockViewBinder.bind( context, displayId, @@ -482,7 +485,7 @@ constructor( ) as View // Place the UDFPS view in the proper sensor location - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { finger.id = R.id.lock_icon_view parentView.addView(finger) val cs = ConstraintSet() @@ -509,7 +512,7 @@ constructor( private fun setUpClock(previewContext: Context, parentView: ViewGroup) { val resources = parentView.resources - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { largeClockHostView = FrameLayout(previewContext) largeClockHostView.layoutParams = FrameLayout.LayoutParams( @@ -547,7 +550,7 @@ constructor( } // TODO (b/283465254): Move the listeners to KeyguardClockRepository - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { val clockChangeListener = object : ClockRegistry.ClockChangeListener { override fun onCurrentClockChanged() { @@ -555,14 +558,12 @@ constructor( } } clockRegistry.registerClockChangeListener(clockChangeListener) - disposables.add( - DisposableHandle { - clockRegistry.unregisterClockChangeListener(clockChangeListener) - } - ) + disposables += DisposableHandle { + clockRegistry.unregisterClockChangeListener(clockChangeListener) + } clockController.registerListeners(parentView) - disposables.add(DisposableHandle { clockController.unregisterListeners() }) + disposables += DisposableHandle { clockController.unregisterListeners() } } val receiver = @@ -581,9 +582,9 @@ constructor( addAction(Intent.ACTION_TIME_CHANGED) }, ) - disposables.add(DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) }) + disposables += DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) } - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { val layoutChangeListener = View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> if (clockController.clock !is DefaultClockController) { @@ -602,9 +603,9 @@ constructor( } } parentView.addOnLayoutChangeListener(layoutChangeListener) - disposables.add( - DisposableHandle { parentView.removeOnLayoutChangeListener(layoutChangeListener) } - ) + disposables += DisposableHandle { + parentView.removeOnLayoutChangeListener(layoutChangeListener) + } } onClockChanged() @@ -631,7 +632,7 @@ constructor( } } private fun onClockChanged() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { return } coroutineScope.launch { @@ -678,7 +679,7 @@ constructor( } private fun updateLargeClock(clock: ClockController) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { return } clock.largeClock.events.onTargetRegionChanged( @@ -692,7 +693,7 @@ constructor( } private fun updateSmallClock(clock: ClockController) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { return } clock.smallClock.events.onTargetRegionChanged( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt index f20c4acba448..3b21141273e0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/transitions/DeviceEntryIconTransitionModule.kt @@ -22,10 +22,12 @@ import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToPrimaryBounc import com.android.systemui.keyguard.ui.viewmodel.AodToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.AodToOccludedTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.AodToPrimaryBouncerTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToGoneTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DozingToPrimaryBouncerTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.DreamingToAodTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.GoneToAodTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.GoneToDozingTransitionViewModel @@ -89,6 +91,12 @@ abstract class DeviceEntryIconTransitionModule { @Binds @IntoSet + abstract fun aodToPrimaryBouncer( + impl: AodToPrimaryBouncerTransitionViewModel + ): DeviceEntryIconTransition + + @Binds + @IntoSet abstract fun dozingToGone(impl: DozingToGoneTransitionViewModel): DeviceEntryIconTransition @Binds @@ -111,6 +119,10 @@ abstract class DeviceEntryIconTransitionModule { @Binds @IntoSet + abstract fun dreamingToAod(impl: DreamingToAodTransitionViewModel): DeviceEntryIconTransition + + @Binds + @IntoSet abstract fun dreamingToLockscreen( impl: DreamingToLockscreenTransitionViewModel ): DeviceEntryIconTransition diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt index 9c9df806c38c..a215efa724f9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt @@ -41,7 +41,7 @@ class BaseBlueprintTransition(val clockViewModel: KeyguardClockViewModel) : Tran private fun excludeClockAndSmartspaceViews(transition: Transition) { transition.excludeTarget(SmartspaceView::class.java, true) - clockViewModel.clock?.let { clock -> + clockViewModel.currentClock.value?.let { clock -> clock.largeClock.layout.views.forEach { view -> transition.excludeTarget(view, true) } clock.smallClock.layout.views.forEach { view -> transition.excludeTarget(view, true) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt index 3adeb2aeb283..c69d868866d0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt @@ -57,7 +57,9 @@ class IntraBlueprintTransition( when (config.type) { Type.NoTransition -> {} Type.DefaultClockStepping -> - addTransition(clockViewModel.clock?.let { DefaultClockSteppingTransition(it) }) + addTransition( + clockViewModel.currentClock.value?.let { DefaultClockSteppingTransition(it) } + ) else -> addTransition(ClockSizeTransition(config, clockViewModel, smartspaceViewModel)) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt index cd46d6cf2188..2e9663897f89 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt @@ -25,9 +25,9 @@ import androidx.constraintlayout.widget.ConstraintSet.LEFT import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.RIGHT import androidx.constraintlayout.widget.ConstraintSet.TOP -import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel @@ -49,14 +49,14 @@ constructor( private val vibratorHelper: VibratorHelper, ) : BaseShortcutSection() { override fun addViews(constraintLayout: ConstraintLayout) { - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { addLeftShortcut(constraintLayout) addRightShortcut(constraintLayout) } } override fun bindData(constraintLayout: ConstraintLayout) { - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { leftShortcutHandle = KeyguardQuickAffordanceViewBinder.bind( constraintLayout.requireViewById(R.id.start_button), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt index 88ce9dc88a7b..d639978764f8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodBurnInSection.kt @@ -23,7 +23,7 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID -import com.android.systemui.Flags.migrateClocksToBlueprint +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.view.KeyguardRootView import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel @@ -47,7 +47,7 @@ constructor( } } override fun addViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } @@ -62,14 +62,14 @@ constructor( } override fun bindData(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } clockViewModel.burnInLayer = burnInLayer } override fun applyConstraints(constraintSet: ConstraintSet) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt index 3d9c04e39679..2832e9d8a35e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt @@ -26,8 +26,8 @@ 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.Flags.migrateClocksToBlueprint import com.android.systemui.common.ui.ConfigurationState +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.res.R import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore @@ -58,7 +58,7 @@ constructor( private lateinit var nic: NotificationIconContainer override fun addViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } nic = @@ -77,7 +77,7 @@ constructor( } override fun bindData(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } @@ -98,7 +98,7 @@ constructor( } override fun applyConstraints(constraintSet: ConstraintSet) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } val bottomMargin = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index a183b720c087..881467ff2724 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -30,8 +30,8 @@ import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.constraintlayout.widget.ConstraintSet.VISIBLE import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT -import com.android.systemui.Flags import com.android.systemui.customization.R as customizationR +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.KeyguardSection @@ -70,7 +70,7 @@ constructor( override fun addViews(constraintLayout: ConstraintLayout) {} override fun bindData(constraintLayout: ConstraintLayout) { - if (!Flags.migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } KeyguardClockViewBinder.bind( @@ -83,10 +83,10 @@ constructor( } override fun applyConstraints(constraintSet: ConstraintSet) { - if (!Flags.migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } - clockInteractor.clock?.let { clock -> + keyguardClockViewModel.currentClock.value?.let { clock -> constraintSet.applyDeltaFrom(buildConstraints(clock, constraintSet)) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt index 8fd8becab76f..4c846e424f4b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt @@ -28,13 +28,12 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import com.android.keyguard.LockIconView import com.android.keyguard.LockIconViewController -import com.android.systemui.Flags.keyguardBottomAreaRefactor -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.biometrics.AuthController import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.binder.DeviceEntryIconViewBinder import com.android.systemui.keyguard.ui.view.DeviceEntryIconView @@ -72,8 +71,8 @@ constructor( override fun addViews(constraintLayout: ConstraintLayout) { if ( - !keyguardBottomAreaRefactor() && - !migrateClocksToBlueprint() && + !KeyguardBottomAreaRefactor.isEnabled && + !DeviceEntryUdfpsRefactor.isEnabled && !DeviceEntryUdfpsRefactor.isEnabled ) { return @@ -87,7 +86,7 @@ constructor( if (DeviceEntryUdfpsRefactor.isEnabled) { DeviceEntryIconView(context, null).apply { id = deviceEntryIconViewId } } else { - // keyguardBottomAreaRefactor() or migrateClocksToBlueprint() + // KeyguardBottomAreaRefactor.isEnabled or MigrateClocksToBlueprint.isEnabled LockIconView(context, null).apply { id = R.id.lock_icon_view } } constraintLayout.addView(view) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt index 3361343423a9..af0528a4c354 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultIndicationAreaSection.kt @@ -21,7 +21,7 @@ import android.content.Context import android.view.ViewGroup import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet -import com.android.systemui.Flags.keyguardBottomAreaRefactor +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder import com.android.systemui.keyguard.ui.view.KeyguardIndicationArea @@ -42,14 +42,14 @@ constructor( private var indicationAreaHandle: DisposableHandle? = null override fun addViews(constraintLayout: ConstraintLayout) { - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { val view = KeyguardIndicationArea(context, null) constraintLayout.addView(view) } } override fun bindData(constraintLayout: ConstraintLayout) { - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { indicationAreaHandle = KeyguardIndicationAreaBinder.bind( constraintLayout.requireViewById(R.id.keyguard_indication_area), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt index 6a3b920f9692..380e361eb33e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt @@ -25,58 +25,42 @@ import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.systemui.Flags.centralizedStatusBarHeightFix -import com.android.systemui.Flags.migrateClocksToBlueprint -import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.res.R -import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.shade.NotificationPanelView -import com.android.systemui.statusbar.notification.stack.AmbientState -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import dagger.Lazy import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher /** Single column format for notifications (default for phones) */ class DefaultNotificationStackScrollLayoutSection @Inject constructor( context: Context, - sceneContainerFlags: SceneContainerFlags, notificationPanelView: NotificationPanelView, sharedNotificationContainer: SharedNotificationContainer, sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, - notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, - ambientState: AmbientState, - controller: NotificationStackScrollLayoutController, - notificationStackSizeCalculator: NotificationStackSizeCalculator, + sharedNotificationContainerBinder: SharedNotificationContainerBinder, private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>, - @Main mainDispatcher: CoroutineDispatcher, ) : NotificationStackScrollLayoutSection( context, - sceneContainerFlags, notificationPanelView, sharedNotificationContainer, sharedNotificationContainerViewModel, - notificationStackAppearanceViewModel, - ambientState, - controller, - notificationStackSizeCalculator, - mainDispatcher, + sharedNotificationContainerBinder, ) { override fun applyConstraints(constraintSet: ConstraintSet) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } constraintSet.apply { val bottomMargin = context.resources.getDimensionPixelSize(R.dimen.keyguard_status_view_bottom_margin) - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { val useLargeScreenHeader = context.resources.getBoolean(R.bool.config_use_large_screen_shade_header) val marginTopLargeScreen = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt index a203c53be01e..32e76d0b24ff 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultSettingsPopupMenuSection.kt @@ -29,9 +29,9 @@ import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.VISIBILITY_MODE_IGNORE import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT import androidx.core.view.isVisible -import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.animation.view.LaunchableLinearLayout import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.binder.KeyguardSettingsViewBinder import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel @@ -56,7 +56,7 @@ constructor( private var settingsPopupMenuHandle: DisposableHandle? = null override fun addViews(constraintLayout: ConstraintLayout) { - if (!keyguardBottomAreaRefactor()) { + if (!KeyguardBottomAreaRefactor.isEnabled) { return } val view = @@ -71,7 +71,7 @@ constructor( } override fun bindData(constraintLayout: ConstraintLayout) { - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { settingsPopupMenuHandle = KeyguardSettingsViewBinder.bind( constraintLayout.requireViewById<View>(R.id.keyguard_settings_button), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt index 0c0eb8a673a4..45b82576c6c4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt @@ -25,8 +25,8 @@ import androidx.constraintlayout.widget.ConstraintSet.LEFT import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.RIGHT import androidx.constraintlayout.widget.ConstraintSet.VISIBILITY_MODE_IGNORE -import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel @@ -48,14 +48,14 @@ constructor( private val vibratorHelper: VibratorHelper, ) : BaseShortcutSection() { override fun addViews(constraintLayout: ConstraintLayout) { - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { addLeftShortcut(constraintLayout) addRightShortcut(constraintLayout) } } override fun bindData(constraintLayout: ConstraintLayout) { - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { leftShortcutHandle = KeyguardQuickAffordanceViewBinder.bind( constraintLayout.requireViewById(R.id.start_button), diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultStatusViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultStatusViewSection.kt index 6e8605bde864..45641dbfc517 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultStatusViewSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultStatusViewSection.kt @@ -31,8 +31,8 @@ import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import com.android.keyguard.KeyguardStatusView import com.android.keyguard.dagger.KeyguardStatusViewComponent -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.keyguard.KeyguardViewConfigurator +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.media.controls.ui.controller.KeyguardMediaController import com.android.systemui.res.R @@ -58,7 +58,7 @@ constructor( private val statusViewId = R.id.keyguard_status_view override fun addViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } // At startup, 2 views with the ID `R.id.keyguard_status_view` will be available. @@ -83,7 +83,7 @@ constructor( } override fun bindData(constraintLayout: ConstraintLayout) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { constraintLayout.findViewById<KeyguardStatusView?>(R.id.keyguard_status_view)?.let { val statusViewComponent = keyguardStatusViewComponentFactory.build(it, context.display) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt index 3265d796ecc7..2abb7ba37340 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultUdfpsAccessibilityOverlaySection.kt @@ -20,10 +20,10 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet -import com.android.systemui.Flags import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay import com.android.systemui.deviceentry.ui.viewmodel.DeviceEntryUdfpsAccessibilityOverlayViewModel +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.res.R import javax.inject.Inject @@ -66,7 +66,7 @@ constructor( ConstraintSet.BOTTOM, ) - if (Flags.keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { connect( viewId, ConstraintSet.BOTTOM, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt index d572c51d1146..a17c5e538382 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt @@ -22,7 +22,7 @@ import android.view.ViewGroup import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet -import com.android.systemui.Flags.migrateClocksToBlueprint +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.res.R import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController @@ -34,7 +34,7 @@ constructor( val smartspaceController: LockscreenSmartspaceController, ) : KeyguardSection() { override fun addViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) return + if (!MigrateClocksToBlueprint.isEnabled) return if (smartspaceController.isEnabled()) return constraintLayout.findViewById<View?>(R.id.keyguard_slice_view)?.let { @@ -46,7 +46,7 @@ constructor( override fun bindData(constraintLayout: ConstraintLayout) {} override fun applyConstraints(constraintSet: ConstraintSet) { - if (!migrateClocksToBlueprint()) return + if (!MigrateClocksToBlueprint.isEnabled) return if (smartspaceController.isEnabled()) return constraintSet.apply { @@ -81,7 +81,7 @@ constructor( } override fun removeViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) return + if (!MigrateClocksToBlueprint.isEnabled) return if (smartspaceController.isEnabled()) return constraintLayout.removeView(R.id.keyguard_slice_view) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt index 5dea7cbb801d..2b601cdc012f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt @@ -25,38 +25,26 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM import androidx.constraintlayout.widget.ConstraintSet.TOP -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.res.R -import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.shade.NotificationPanelView -import com.android.systemui.statusbar.notification.stack.AmbientState -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle abstract class NotificationStackScrollLayoutSection constructor( protected val context: Context, - private val sceneContainerFlags: SceneContainerFlags, private val notificationPanelView: NotificationPanelView, private val sharedNotificationContainer: SharedNotificationContainer, private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, - private val notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, - private val ambientState: AmbientState, - private val controller: NotificationStackScrollLayoutController, - private val notificationStackSizeCalculator: NotificationStackSizeCalculator, - private val mainDispatcher: CoroutineDispatcher, + private val sharedNotificationContainerBinder: SharedNotificationContainerBinder, ) : KeyguardSection() { private val placeHolderId = R.id.nssl_placeholder - private val disposableHandles: MutableList<DisposableHandle> = mutableListOf() + private var disposableHandle: DisposableHandle? = null /** * Align the notification placeholder bottom to the top of either the lock icon or the ambient @@ -82,7 +70,7 @@ constructor( } override fun addViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } // This moves the existing NSSL view to a different parent, as the controller is a @@ -98,43 +86,21 @@ constructor( } override fun bindData(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } - disposeHandles() - disposableHandles.add( - SharedNotificationContainerBinder.bind( + disposableHandle?.dispose() + disposableHandle = + sharedNotificationContainerBinder.bind( sharedNotificationContainer, sharedNotificationContainerViewModel, - sceneContainerFlags, - controller, - notificationStackSizeCalculator, - mainImmediateDispatcher = mainDispatcher, ) - ) - - if (sceneContainerFlags.isEnabled()) { - disposableHandles.add( - NotificationStackAppearanceViewBinder.bind( - context, - sharedNotificationContainer, - notificationStackAppearanceViewModel, - ambientState, - controller, - mainImmediateDispatcher = mainDispatcher, - ) - ) - } } override fun removeViews(constraintLayout: ConstraintLayout) { - disposeHandles() + disposableHandle?.dispose() + disposableHandle = null constraintLayout.removeView(placeHolderId) } - - private fun disposeHandles() { - disposableHandles.forEach { it.dispose() } - disposableHandles.clear() - } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt index b0f7a258a4e6..1847d2794787 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt @@ -23,8 +23,8 @@ import android.view.ViewTreeObserver.OnGlobalLayoutListener import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.keyguard.KeyguardUnlockAnimationController +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor import com.android.systemui.keyguard.shared.model.KeyguardSection @@ -56,7 +56,7 @@ constructor( private var pastVisibility: Int = -1 override fun addViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) return + if (!MigrateClocksToBlueprint.isEnabled) return if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return smartspaceView = smartspaceController.buildAndConnectView(constraintLayout) weatherView = smartspaceController.buildAndConnectWeatherView(constraintLayout) @@ -83,7 +83,7 @@ constructor( } override fun bindData(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) return + if (!MigrateClocksToBlueprint.isEnabled) return if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return KeyguardSmartspaceViewBinder.bind( constraintLayout, @@ -94,7 +94,7 @@ constructor( } override fun applyConstraints(constraintSet: ConstraintSet) { - if (!migrateClocksToBlueprint()) return + if (!MigrateClocksToBlueprint.isEnabled) return if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return val horizontalPaddingStart = context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start) + @@ -191,7 +191,7 @@ constructor( } override fun removeViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) return + if (!MigrateClocksToBlueprint.isEnabled) return if (!keyguardSmartspaceViewModel.isSmartspaceEnabled) return listOf(smartspaceView, dateView, weatherView).forEach { it?.let { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt index 21e945582aff..5dbba75411a5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt @@ -28,7 +28,7 @@ import androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP -import com.android.systemui.Flags.migrateClocksToBlueprint +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.media.controls.ui.controller.KeyguardMediaController import com.android.systemui.res.R @@ -46,7 +46,7 @@ constructor( private val mediaContainerId = R.id.status_view_media_container override fun addViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } @@ -73,7 +73,7 @@ constructor( override fun bindData(constraintLayout: ConstraintLayout) {} override fun applyConstraints(constraintSet: ConstraintSet) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } @@ -87,7 +87,7 @@ constructor( } override fun removeViews(constraintLayout: ConstraintLayout) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } 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 2545302ccaa1..1a7386678e14 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,51 +23,33 @@ 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.Flags.migrateClocksToBlueprint -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.res.R -import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.shade.NotificationPanelView -import com.android.systemui.statusbar.notification.stack.AmbientState -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher /** Large-screen format for notifications, shown as two columns on the device */ class SplitShadeNotificationStackScrollLayoutSection @Inject constructor( context: Context, - sceneContainerFlags: SceneContainerFlags, notificationPanelView: NotificationPanelView, sharedNotificationContainer: SharedNotificationContainer, sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, - notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, - ambientState: AmbientState, - controller: NotificationStackScrollLayoutController, - notificationStackSizeCalculator: NotificationStackSizeCalculator, - private val smartspaceViewModel: KeyguardSmartspaceViewModel, - @Main mainDispatcher: CoroutineDispatcher, + sharedNotificationContainerBinder: SharedNotificationContainerBinder, ) : NotificationStackScrollLayoutSection( context, - sceneContainerFlags, notificationPanelView, sharedNotificationContainer, sharedNotificationContainerViewModel, - notificationStackAppearanceViewModel, - ambientState, - controller, - notificationStackSizeCalculator, - mainDispatcher, + sharedNotificationContainerBinder, ) { override fun applyConstraints(constraintSet: ConstraintSet) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled) { return } constraintSet.apply { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt index 6184c82cbff7..4d3a78d32b3a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt @@ -216,7 +216,9 @@ class ClockSizeTransition( captureSmartspace = !viewModel.useLargeClock && smartspaceViewModel.isSmartspaceEnabled if (viewModel.useLargeClock) { - viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } } + viewModel.currentClock.value?.let { + it.largeClock.layout.views.forEach { addTarget(it) } + } } else { addTarget(R.id.lockscreen_clock_view) } @@ -276,7 +278,9 @@ class ClockSizeTransition( if (viewModel.useLargeClock) { addTarget(R.id.lockscreen_clock_view) } else { - viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } } + viewModel.currentClock.value?.let { + it.largeClock.layout.views.forEach { addTarget(it) } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt index d26356ebc92b..ac2713d88f39 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToGoneTransitionViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import android.util.MathUtils import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor.Companion.TO_GONE_DURATION import com.android.systemui.keyguard.shared.model.KeyguardState @@ -47,13 +48,16 @@ constructor( to = KeyguardState.GONE, ) - val lockscreenAlpha: Flow<Float> = - transitionAnimation.sharedFlow( + fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> { + var startAlpha = 1f + return transitionAnimation.sharedFlow( duration = 200.milliseconds, - onStep = { 1 - it }, + onStart = { startAlpha = viewState.alpha() }, + onStep = { MathUtils.lerp(startAlpha, 0f, it) }, onFinish = { 0f }, - onCancel = { 1f }, + onCancel = { startAlpha }, ) + } /** Scrim alpha values */ val scrimAlpha: Flow<ScrimAlpha> = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt index 5741b9485287..1e5f5a70bac8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodAlphaViewModel.kt @@ -18,8 +18,8 @@ package com.android.systemui.keyguard.ui.viewmodel -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState.AOD @@ -60,7 +60,7 @@ constructor( emit(goneToAodAlpha) } else if (step.from == GONE && step.to == DOZING) { emit(goneToDozingAlpha) - } else if (!migrateClocksToBlueprint()) { + } else if (!MigrateClocksToBlueprint.isEnabled) { emit(keyguardAlpha) } } 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 f961e083e64f..20549328838f 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 @@ -22,9 +22,9 @@ import android.util.Log import android.util.MathUtils import com.android.app.animation.Interpolators import com.android.keyguard.KeyguardClockSwitch -import com.android.systemui.Flags import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.SysUISingleton +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 @@ -145,7 +145,7 @@ constructor( // Ensure the desired translation doesn't encroach on the top inset val burnInY = MathUtils.lerp(0, burnIn.translationY, interpolated).toInt() val translationY = - if (Flags.migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { max(params.topInset - params.minViewY, burnInY) } else { max(params.topInset, params.minViewY + burnInY) - params.minViewY @@ -168,8 +168,8 @@ constructor( private fun clockController( provider: Provider<ClockController>?, ): Provider<ClockController>? { - return if (Flags.migrateClocksToBlueprint()) { - Provider { keyguardClockViewModel.clock } + return if (MigrateClocksToBlueprint.isEnabled) { + Provider { keyguardClockViewModel.currentClock.value } } else { provider } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.kt new file mode 100644 index 000000000000..9a23007eea4a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodToPrimaryBouncerTransitionViewModel.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.keyguard.ui.viewmodel + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.FromAodTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow + +/** + * Breaks down AOD->PRIMARY BOUNCER transition into discrete steps for corresponding views to + * consume. + */ +@ExperimentalCoroutinesApi +@SysUISingleton +class AodToPrimaryBouncerTransitionViewModel +@Inject +constructor( + animationFlow: KeyguardTransitionAnimationFlow, +) : DeviceEntryIconTransition { + private val transitionAnimation = + animationFlow.setup( + duration = FromAodTransitionInteractor.TO_PRIMARY_BOUNCER_DURATION, + from = KeyguardState.AOD, + to = KeyguardState.PRIMARY_BOUNCER, + ) + + override val deviceEntryParentViewAlpha: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0f) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt index 4c0a9491b74a..1b91c4949018 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryBackgroundViewModel.kt @@ -55,6 +55,8 @@ constructor( lockscreenToDozingTransitionViewModel: LockscreenToDozingTransitionViewModel, dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel, alternateBouncerToDozingTransitionViewModel: AlternateBouncerToDozingTransitionViewModel, + dreamingToAodTransitionViewModel: DreamingToAodTransitionViewModel, + primaryBouncerToLockscreenTransitionViewModel: PrimaryBouncerToLockscreenTransitionViewModel, ) { val color: Flow<Int> = deviceEntryIconViewModel.useBackgroundProtection.flatMapLatest { useBackground -> @@ -96,6 +98,9 @@ constructor( lockscreenToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha, dozingToLockscreenTransitionViewModel.deviceEntryBackgroundViewAlpha, alternateBouncerToDozingTransitionViewModel.deviceEntryBackgroundViewAlpha, + dreamingToAodTransitionViewModel.deviceEntryBackgroundViewAlpha, + primaryBouncerToLockscreenTransitionViewModel + .deviceEntryBackgroundViewAlpha, ) .merge() .onStart { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt index 1a018977664a..bc4fd1c88298 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel import android.animation.FloatEvaluator import android.animation.IntEvaluator import com.android.keyguard.KeyguardViewController +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntrySourceInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor @@ -33,9 +34,11 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.util.kotlin.sample import dagger.Lazy import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -45,6 +48,7 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.shareIn /** Models the UI state for the containing device entry icon & long-press handling view. */ @ExperimentalCoroutinesApi @@ -62,6 +66,7 @@ constructor( private val keyguardViewController: Lazy<KeyguardViewController>, private val deviceEntryInteractor: DeviceEntryInteractor, private val deviceEntrySourceInteractor: DeviceEntrySourceInteractor, + @Application private val scope: CoroutineScope, ) { val isUdfpsSupported: StateFlow<Boolean> = deviceEntryUdfpsInteractor.isUdfpsSupported private val intEvaluator = IntEvaluator() @@ -73,7 +78,10 @@ constructor( private val qsProgress: Flow<Float> = shadeInteractor.qsExpansion.onStart { emit(0f) } private val shadeExpansion: Flow<Float> = shadeInteractor.shadeExpansion.onStart { emit(0f) } private val transitionAlpha: Flow<Float> = - transitions.map { it.deviceEntryParentViewAlpha }.merge() + transitions + .map { it.deviceEntryParentViewAlpha } + .merge() + .shareIn(scope, SharingStarted.WhileSubscribed()) private val alphaMultiplierFromShadeExpansion: Flow<Float> = combine( showingAlternateBouncer, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModel.kt new file mode 100644 index 000000000000..0fa74752ea0d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToAodTransitionViewModel.kt @@ -0,0 +1,62 @@ +/* + * 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.dagger.SysUISingleton +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor +import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow +import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest + +/** Breaks down DREAMING->AOD transition into discrete steps for corresponding views to consume. */ +@ExperimentalCoroutinesApi +@SysUISingleton +class DreamingToAodTransitionViewModel +@Inject +constructor( + deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor, + animationFlow: KeyguardTransitionAnimationFlow, +) : DeviceEntryIconTransition { + private val transitionAnimation = + animationFlow.setup( + duration = FromDreamingTransitionInteractor.TO_AOD_DURATION, + from = KeyguardState.DREAMING, + to = KeyguardState.AOD, + ) + + val deviceEntryBackgroundViewAlpha: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(0f) + override val deviceEntryParentViewAlpha: Flow<Float> = + deviceEntryUdfpsInteractor.isUdfpsEnrolledAndEnabled.flatMapLatest { udfpsEnrolledAndEnabled + -> + if (udfpsEnrolledAndEnabled) { + transitionAnimation.sharedFlow( + duration = FromDreamingTransitionInteractor.TO_AOD_DURATION, + onStep = { it }, + onFinish = { 1f }, + ) + } else { + emptyFlow() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt index e0b1c50a84bc..a2ce408955a1 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModel.kt @@ -43,9 +43,11 @@ constructor( transitionAnimation.sharedFlow( duration = 250.milliseconds, onStep = { it }, - onCancel = { 0f }, + onCancel = { 1f }, ) + val lockscreenAlpha: Flow<Float> = shortcutsAlpha + val deviceEntryBackgroundViewAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(1f) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt index b6622e5c07b1..1c1c33ab7e7e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt @@ -26,7 +26,6 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.ComposeLockscreen import com.android.systemui.keyguard.shared.model.SettingsClockSize -import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode @@ -54,8 +53,6 @@ constructor( val useLargeClock: Boolean get() = clockSize.value == LARGE - var clock: ClockController? by keyguardClockInteractor::clock - val clockSize = combine(keyguardClockInteractor.selectedClockSize, keyguardClockInteractor.clockSize) { selectedSize, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt index e35e06533f8c..8409f15dca81 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt @@ -16,10 +16,10 @@ package com.android.systemui.keyguard.ui.viewmodel -import com.android.systemui.Flags.keyguardBottomAreaRefactor -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.doze.util.BurnInHelperWrapper +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.domain.interactor.BurnInInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor @@ -52,7 +52,7 @@ constructor( /** An observable for whether the indication area should be padded. */ val isIndicationAreaPadded: Flow<Boolean> = - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled) { combine(shortcutsCombinedViewModel.startButton, shortcutsCombinedViewModel.endButton) { startButtonModel, endButtonModel -> @@ -79,7 +79,7 @@ constructor( /** An observable for the x-offset by which the indication area should be translated. */ val indicationAreaTranslationX: Flow<Float> = - if (migrateClocksToBlueprint() || keyguardBottomAreaRefactor()) { + if (MigrateClocksToBlueprint.isEnabled || KeyguardBottomAreaRefactor.isEnabled) { burnIn.map { it.translationX.toFloat() } } else { bottomAreaInteractor.clockPosition.map { it.x.toFloat() }.distinctUntilChanged() @@ -87,7 +87,7 @@ constructor( /** Returns an observable for the y-offset by which the indication area should be translated. */ fun indicationAreaTranslationY(defaultBurnInOffset: Int): Flow<Float> { - return if (migrateClocksToBlueprint()) { + return if (MigrateClocksToBlueprint.isEnabled) { burnIn.map { it.translationY.toFloat() } } else { keyguardInteractor.dozeAmount 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 301f00ee38db..5337ca3b9be1 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 @@ -91,6 +91,7 @@ constructor( private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel, private val goneToDozingTransitionViewModel: GoneToDozingTransitionViewModel, private val goneToDreamingTransitionViewModel: GoneToDreamingTransitionViewModel, + private val goneToLockscreenTransitionViewModel: GoneToLockscreenTransitionViewModel, private val lockscreenToAodTransitionViewModel: LockscreenToAodTransitionViewModel, private val lockscreenToDozingTransitionViewModel: LockscreenToDozingTransitionViewModel, private val lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel, @@ -205,7 +206,7 @@ constructor( merge( alphaOnShadeExpansion, keyguardInteractor.dismissAlpha.filterNotNull(), - alternateBouncerToGoneTransitionViewModel.lockscreenAlpha, + alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState), aodToGoneTransitionViewModel.lockscreenAlpha(viewState), aodToLockscreenTransitionViewModel.lockscreenAlpha(viewState), aodToOccludedTransitionViewModel.lockscreenAlpha(viewState), @@ -218,6 +219,7 @@ constructor( goneToAodTransitionViewModel.enterFromTopAnimationAlpha, goneToDozingTransitionViewModel.lockscreenAlpha, goneToDreamingTransitionViewModel.lockscreenAlpha, + goneToLockscreenTransitionViewModel.lockscreenAlpha, lockscreenToAodTransitionViewModel.lockscreenAlpha(viewState), lockscreenToDozingTransitionViewModel.lockscreenAlpha, lockscreenToDreamingTransitionViewModel.lockscreenAlpha, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt index 34c9ac92a3f3..25750415e88f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor import com.android.systemui.keyguard.domain.interactor.FromPrimaryBouncerTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow @@ -27,8 +26,6 @@ import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flatMapLatest /** * Breaks down PRIMARY BOUNCER->LOCKSCREEN transition into discrete steps for corresponding views to @@ -39,7 +36,6 @@ import kotlinx.coroutines.flow.flatMapLatest class PrimaryBouncerToLockscreenTransitionViewModel @Inject constructor( - deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor, animationFlow: KeyguardTransitionAnimationFlow, ) : DeviceEntryIconTransition { private val transitionAnimation = @@ -49,15 +45,6 @@ constructor( to = KeyguardState.LOCKSCREEN, ) - val deviceEntryBackgroundViewAlpha: Flow<Float> = - deviceEntryUdfpsInteractor.isUdfpsSupported.flatMapLatest { isUdfps -> - if (isUdfps) { - transitionAnimation.immediatelyTransitionTo(1f) - } else { - emptyFlow() - } - } - val shortcutsAlpha: Flow<Float> = transitionAnimation.sharedFlow( duration = 250.milliseconds, @@ -67,6 +54,8 @@ constructor( val lockscreenAlpha: Flow<Float> = shortcutsAlpha + val deviceEntryBackgroundViewAlpha: Flow<Float> = + transitionAnimation.immediatelyTransitionTo(1f) override val deviceEntryParentViewAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(1f) } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaDataRepository.kt b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaDataRepository.kt new file mode 100644 index 000000000000..b6fd287a675e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaDataRepository.kt @@ -0,0 +1,123 @@ +/* + * 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.media.controls.data.repository + +import android.util.Log +import com.android.systemui.Dumpable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.media.controls.util.MediaFlags +import java.io.PrintWriter +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +private const val TAG = "MediaDataRepository" +private const val DEBUG = true + +/** A repository that holds the state of all media controls in carousel. */ +@SysUISingleton +class MediaDataRepository +@Inject +constructor( + private val mediaFlags: MediaFlags, + dumpManager: DumpManager, +) : Dumpable { + + private val _mediaEntries: MutableStateFlow<Map<String, MediaData>> = + MutableStateFlow(LinkedHashMap()) + val mediaEntries: StateFlow<Map<String, MediaData>> = _mediaEntries.asStateFlow() + + private val _smartspaceMediaData: MutableStateFlow<SmartspaceMediaData> = + MutableStateFlow(SmartspaceMediaData()) + val smartspaceMediaData: StateFlow<SmartspaceMediaData> = _smartspaceMediaData.asStateFlow() + + init { + dumpManager.registerNormalDumpable(TAG, this) + } + + /** Updates the recommendation data with a new smartspace media data. */ + fun setRecommendation(recommendation: SmartspaceMediaData) { + _smartspaceMediaData.value = recommendation + } + + /** + * Marks the recommendation data as inactive. + * + * @return true if the recommendation was actually marked as inactive, false otherwise. + */ + fun setRecommendationInactive(key: String): Boolean { + if (!mediaFlags.isPersistentSsCardEnabled()) { + Log.e(TAG, "Only persistent recommendation can be inactive!") + return false + } + if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive") + + if (smartspaceMediaData.value.targetId != key || !smartspaceMediaData.value.isValid()) { + // If this doesn't match, or we've already invalidated the data, no action needed + return false + } + + setRecommendation(smartspaceMediaData.value.copy(isActive = false)) + return true + } + + /** + * Marks the recommendation data as dismissed. + * + * @return true if the recommendation was dismissed or already inactive, false otherwise. + */ + fun dismissSmartspaceRecommendation(key: String): Boolean { + val data = smartspaceMediaData.value + if (data.targetId != key || !data.isValid()) { + // If this doesn't match, or we've already invalidated the data, no action needed + return false + } + + if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target") + if (data.isActive) { + setRecommendation( + SmartspaceMediaData( + targetId = smartspaceMediaData.value.targetId, + instanceId = smartspaceMediaData.value.instanceId + ) + ) + } + return true + } + + fun removeMediaEntry(key: String): MediaData? { + val entries = LinkedHashMap<String, MediaData>(_mediaEntries.value) + val mediaData = entries.remove(key) + _mediaEntries.value = entries + return mediaData + } + + fun addMediaEntry(key: String, data: MediaData): MediaData? { + val entries = LinkedHashMap<String, MediaData>(_mediaEntries.value) + val mediaData = entries.put(key, data) + _mediaEntries.value = entries + return mediaData + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.apply { println("mediaEntries: ${mediaEntries.value}") } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt new file mode 100644 index 000000000000..b94a4af65649 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/data/repository/MediaFilterRepository.kt @@ -0,0 +1,111 @@ +/* + * 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.media.controls.data.repository + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** A repository that holds the state of filtered media data on the device. */ +@SysUISingleton +class MediaFilterRepository @Inject constructor() { + + /** Key of media control that recommendations card reactivated. */ + private val _reactivatedKey: MutableStateFlow<String?> = MutableStateFlow(null) + val reactivatedKey: StateFlow<String?> = _reactivatedKey.asStateFlow() + + private val _smartspaceMediaData: MutableStateFlow<SmartspaceMediaData> = + MutableStateFlow(SmartspaceMediaData()) + val smartspaceMediaData: StateFlow<SmartspaceMediaData> = _smartspaceMediaData.asStateFlow() + + private val _selectedUserEntries: MutableStateFlow<Map<String, MediaData>> = + MutableStateFlow(LinkedHashMap()) + val selectedUserEntries: StateFlow<Map<String, MediaData>> = _selectedUserEntries.asStateFlow() + + private val _allUserEntries: MutableStateFlow<Map<String, MediaData>> = + MutableStateFlow(LinkedHashMap()) + val allUserEntries: StateFlow<Map<String, MediaData>> = _allUserEntries.asStateFlow() + + fun addMediaEntry(key: String, data: MediaData) { + val entries = LinkedHashMap<String, MediaData>(_allUserEntries.value) + entries[key] = data + _allUserEntries.value = entries + } + + /** + * Removes the media entry corresponding to the given [key]. + * + * @return media data if an entry is actually removed, `null` otherwise. + */ + fun removeMediaEntry(key: String): MediaData? { + val entries = LinkedHashMap<String, MediaData>(_allUserEntries.value) + val mediaData = entries.remove(key) + _allUserEntries.value = entries + return mediaData + } + + fun addSelectedUserMediaEntry(key: String, data: MediaData) { + val entries = LinkedHashMap<String, MediaData>(_selectedUserEntries.value) + entries[key] = data + _selectedUserEntries.value = entries + } + + /** + * Removes selected user media entry given the corresponding key. + * + * @return media data if an entry is actually removed, `null` otherwise. + */ + fun removeSelectedUserMediaEntry(key: String): MediaData? { + val entries = LinkedHashMap<String, MediaData>(_selectedUserEntries.value) + val mediaData = entries.remove(key) + _selectedUserEntries.value = entries + return mediaData + } + + /** + * Removes selected user media entry given a key and media data. + * + * @return true if media data is removed, false otherwise. + */ + fun removeSelectedUserMediaEntry(key: String, data: MediaData): Boolean { + val entries = LinkedHashMap<String, MediaData>(_selectedUserEntries.value) + val succeed = entries.remove(key, data) + if (!succeed) { + return false + } + _selectedUserEntries.value = entries + return true + } + + fun clearSelectedUserMedia() { + _selectedUserEntries.value = LinkedHashMap() + } + + /** Updates recommendation data with a new smartspace media data. */ + fun setRecommendation(smartspaceMediaData: SmartspaceMediaData) { + _smartspaceMediaData.value = smartspaceMediaData + } + + /** Updates media control key that recommendations card reactivated. */ + fun setReactivatedKey(key: String?) { + _reactivatedKey.value = key + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt new file mode 100644 index 000000000000..e0c54190283a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/MediaDomainModule.kt @@ -0,0 +1,62 @@ +/* + * 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.media.controls.domain + +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.media.controls.domain.pipeline.LegacyMediaDataManagerImpl +import com.android.systemui.media.controls.domain.pipeline.MediaDataManager +import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor +import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor +import com.android.systemui.media.controls.util.MediaFlags +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Provider + +/** Dagger module for injecting media controls domain interfaces. */ +@Module +interface MediaDomainModule { + + @Binds + @IntoMap + @ClassKey(MediaCarouselInteractor::class) + fun bindMediaCarouselInteractor(interactor: MediaCarouselInteractor): CoreStartable + + @Binds + @IntoMap + @ClassKey(MediaDataProcessor::class) + fun bindMediaDataProcessor(interactor: MediaDataProcessor): CoreStartable + companion object { + + @Provides + @SysUISingleton + fun providesMediaDataManager( + legacyProvider: Provider<LegacyMediaDataManagerImpl>, + newProvider: Provider<MediaCarouselInteractor>, + mediaFlags: MediaFlags, + ): MediaDataManager { + return if (mediaFlags.isMediaControlsRefactorEnabled()) { + newProvider.get() + } else { + legacyProvider.get() + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilter.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImpl.kt index bc539efdfe69..c02478b02ec2 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilter.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImpl.kt @@ -61,7 +61,7 @@ internal val SMARTSPACE_MAX_AGE = * This is added at the end of the pipeline since we may still need to handle callbacks from * background users (e.g. timeouts). */ -class MediaDataFilter +class LegacyMediaDataFilterImpl @Inject constructor( private val context: Context, @@ -74,9 +74,9 @@ constructor( private val mediaFlags: MediaFlags, ) : MediaDataManager.Listener { private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() - internal val listeners: Set<MediaDataManager.Listener> + val listeners: Set<MediaDataManager.Listener> get() = _listeners.toSet() - internal lateinit var mediaDataManager: MediaDataManager + lateinit var mediaDataManager: MediaDataManager private val allEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() // The filtered userEntries, which will be a subset of all userEntries in MediaDataManager @@ -279,7 +279,7 @@ constructor( val mediaKeys = userEntries.keys.toSet() mediaKeys.forEach { // Force updates to listeners, needed for re-activated card - mediaDataManager.setTimedOut(it, timedOut = true, forceUpdate = true) + mediaDataManager.setInactive(it, timedOut = true, forceUpdate = true) } if (smartspaceMediaData.isActive) { val dismissIntent = smartspaceMediaData.dismissIntent diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt new file mode 100644 index 000000000000..3a83115642bc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImpl.kt @@ -0,0 +1,1693 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.domain.pipeline + +import android.annotation.SuppressLint +import android.app.ActivityOptions +import android.app.BroadcastOptions +import android.app.Notification +import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME +import android.app.PendingIntent +import android.app.StatusBarManager +import android.app.UriGrantsManager +import android.app.smartspace.SmartspaceAction +import android.app.smartspace.SmartspaceConfig +import android.app.smartspace.SmartspaceManager +import android.app.smartspace.SmartspaceSession +import android.app.smartspace.SmartspaceTarget +import android.content.BroadcastReceiver +import android.content.ContentProvider +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.ImageDecoder +import android.graphics.drawable.Animatable +import android.graphics.drawable.Icon +import android.media.MediaDescription +import android.media.MediaMetadata +import android.media.session.MediaController +import android.media.session.MediaSession +import android.media.session.PlaybackState +import android.net.Uri +import android.os.Parcelable +import android.os.Process +import android.os.UserHandle +import android.provider.Settings +import android.service.notification.StatusBarNotification +import android.support.v4.media.MediaMetadataCompat +import android.text.TextUtils +import android.util.Log +import android.util.Pair as APair +import androidx.media.utils.MediaConstants +import com.android.app.tracing.traceSection +import com.android.internal.annotations.Keep +import com.android.internal.logging.InstanceId +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.Dumpable +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification +import com.android.systemui.media.controls.domain.resume.MediaResumeListener +import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser +import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE +import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC +import com.android.systemui.media.controls.shared.model.MediaAction +import com.android.systemui.media.controls.shared.model.MediaButton +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.MediaDeviceData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider +import com.android.systemui.media.controls.ui.view.MediaViewHolder +import com.android.systemui.media.controls.util.MediaControllerFactory +import com.android.systemui.media.controls.util.MediaDataUtils +import com.android.systemui.media.controls.util.MediaFlags +import com.android.systemui.media.controls.util.MediaUiEventLogger +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.BcSmartspaceDataPlugin +import com.android.systemui.res.R +import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState +import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState +import com.android.systemui.statusbar.notification.row.HybridGroupManager +import com.android.systemui.tuner.TunerService +import com.android.systemui.util.Assert +import com.android.systemui.util.Utils +import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.concurrency.ThreadFactory +import com.android.systemui.util.time.SystemClock +import java.io.IOException +import java.io.PrintWriter +import java.util.concurrent.Executor +import javax.inject.Inject + +// URI fields to try loading album art from +private val ART_URIS = + arrayOf( + MediaMetadata.METADATA_KEY_ALBUM_ART_URI, + MediaMetadata.METADATA_KEY_ART_URI, + MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI + ) + +private const val TAG = "MediaDataManager" +private const val DEBUG = true +private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent" + +private val LOADING = + MediaData( + userId = -1, + initialized = false, + app = null, + appIcon = null, + artist = null, + song = null, + artwork = null, + actions = emptyList(), + actionsToShowInCompact = emptyList(), + packageName = "INVALID", + token = null, + clickIntent = null, + device = null, + active = true, + resumeAction = null, + instanceId = InstanceId.fakeInstanceId(-1), + appUid = Process.INVALID_UID + ) + +internal val EMPTY_SMARTSPACE_MEDIA_DATA = + SmartspaceMediaData( + targetId = "INVALID", + isActive = false, + packageName = "INVALID", + cardAction = null, + recommendations = emptyList(), + dismissIntent = null, + headphoneConnectionTimeMillis = 0, + instanceId = InstanceId.fakeInstanceId(-1), + expiryTimeMs = 0, + ) + +const val MEDIA_TITLE_ERROR_MESSAGE = "Invalid media data: title is null or blank." + +/** + * Allow recommendations from smartspace to show in media controls. Requires + * [Utils.useQsMediaPlayer] to be enabled. On by default, but can be disabled by setting to 0 + */ +private fun allowMediaRecommendations(context: Context): Boolean { + val flag = + Settings.Secure.getInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + 1 + ) + return Utils.useQsMediaPlayer(context) && flag > 0 +} + +/** A class that facilitates management and loading of Media Data, ready for binding. */ +@SysUISingleton +class LegacyMediaDataManagerImpl( + private val context: Context, + @Background private val backgroundExecutor: Executor, + @Main private val uiExecutor: Executor, + @Main private val foregroundExecutor: DelayableExecutor, + private val mediaControllerFactory: MediaControllerFactory, + private val broadcastDispatcher: BroadcastDispatcher, + dumpManager: DumpManager, + mediaTimeoutListener: MediaTimeoutListener, + mediaResumeListener: MediaResumeListener, + mediaSessionBasedFilter: MediaSessionBasedFilter, + private val mediaDeviceManager: MediaDeviceManager, + mediaDataCombineLatest: MediaDataCombineLatest, + private val mediaDataFilter: LegacyMediaDataFilterImpl, + private val activityStarter: ActivityStarter, + private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider, + private var useMediaResumption: Boolean, + private val useQsMediaPlayer: Boolean, + private val systemClock: SystemClock, + private val tunerService: TunerService, + private val mediaFlags: MediaFlags, + private val logger: MediaUiEventLogger, + private val smartspaceManager: SmartspaceManager?, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, +) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener, MediaDataManager { + + companion object { + // UI surface label for subscribing Smartspace updates. + @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager" + + // Smartspace package name's extra key. + @JvmField val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name" + + // Maximum number of actions allowed in compact view + @JvmField val MAX_COMPACT_ACTIONS = 3 + + // Maximum number of actions allowed in expanded view + @JvmField val MAX_NOTIFICATION_ACTIONS = MediaViewHolder.genericButtonIds.size + } + + private val themeText = + com.android.settingslib.Utils.getColorAttr( + context, + com.android.internal.R.attr.textColorPrimary + ) + .defaultColor + + // Internal listeners are part of the internal pipeline. External listeners (those registered + // with [MediaDeviceManager.addListener]) receive events after they have propagated through + // the internal pipeline. + // Another way to think of the distinction between internal and external listeners is the + // following. Internal listeners are listeners that MediaDataManager depends on, and external + // listeners are listeners that depend on MediaDataManager. + // TODO(b/159539991#comment5): Move internal listeners to separate package. + private val internalListeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() + private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() + // There should ONLY be at most one Smartspace media recommendation. + var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA + @Keep private var smartspaceSession: SmartspaceSession? = null + private var allowMediaRecommendations = allowMediaRecommendations(context) + + private val artworkWidth = + context.resources.getDimensionPixelSize( + com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize + ) + private val artworkHeight = + context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded) + + @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE + private val statusBarManager = + context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager + + /** Check whether this notification is an RCN */ + private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean { + return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE) + } + + @Inject + constructor( + context: Context, + threadFactory: ThreadFactory, + @Main uiExecutor: Executor, + @Main foregroundExecutor: DelayableExecutor, + mediaControllerFactory: MediaControllerFactory, + dumpManager: DumpManager, + broadcastDispatcher: BroadcastDispatcher, + mediaTimeoutListener: MediaTimeoutListener, + mediaResumeListener: MediaResumeListener, + mediaSessionBasedFilter: MediaSessionBasedFilter, + mediaDeviceManager: MediaDeviceManager, + mediaDataCombineLatest: MediaDataCombineLatest, + mediaDataFilter: LegacyMediaDataFilterImpl, + activityStarter: ActivityStarter, + smartspaceMediaDataProvider: SmartspaceMediaDataProvider, + clock: SystemClock, + tunerService: TunerService, + mediaFlags: MediaFlags, + logger: MediaUiEventLogger, + smartspaceManager: SmartspaceManager?, + keyguardUpdateMonitor: KeyguardUpdateMonitor, + ) : this( + context, + // Loading bitmap for UMO background can take longer time, so it cannot run on the default + // background thread. Use a custom thread for media. + threadFactory.buildExecutorOnNewThread(TAG), + uiExecutor, + foregroundExecutor, + mediaControllerFactory, + broadcastDispatcher, + dumpManager, + mediaTimeoutListener, + mediaResumeListener, + mediaSessionBasedFilter, + mediaDeviceManager, + mediaDataCombineLatest, + mediaDataFilter, + activityStarter, + smartspaceMediaDataProvider, + Utils.useMediaResumption(context), + Utils.useQsMediaPlayer(context), + clock, + tunerService, + mediaFlags, + logger, + smartspaceManager, + keyguardUpdateMonitor, + ) + + private val appChangeReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + when (intent.action) { + Intent.ACTION_PACKAGES_SUSPENDED -> { + val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST) + packages?.forEach { removeAllForPackage(it) } + } + Intent.ACTION_PACKAGE_REMOVED, + Intent.ACTION_PACKAGE_RESTARTED -> { + intent.data?.encodedSchemeSpecificPart?.let { removeAllForPackage(it) } + } + } + } + } + + init { + dumpManager.registerDumpable(TAG, this) + + // Initialize the internal processing pipeline. The listeners at the front of the pipeline + // are set as internal listeners so that they receive events. From there, events are + // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter, + // so it is responsible for dispatching events to external listeners. To achieve this, + // external listeners that are registered with [MediaDataManager.addListener] are actually + // registered as listeners to mediaDataFilter. + addInternalListener(mediaTimeoutListener) + addInternalListener(mediaResumeListener) + addInternalListener(mediaSessionBasedFilter) + mediaSessionBasedFilter.addListener(mediaDeviceManager) + mediaSessionBasedFilter.addListener(mediaDataCombineLatest) + mediaDeviceManager.addListener(mediaDataCombineLatest) + mediaDataCombineLatest.addListener(mediaDataFilter) + + // Set up links back into the pipeline for listeners that need to send events upstream. + mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean -> + setInactive(key, timedOut) + } + mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState -> + updateState(key, state) + } + mediaTimeoutListener.sessionCallback = { key: String -> onSessionDestroyed(key) } + mediaResumeListener.setManager(this) + mediaDataFilter.mediaDataManager = this + + val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED) + broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL) + + val uninstallFilter = + IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_REMOVED) + addAction(Intent.ACTION_PACKAGE_RESTARTED) + addDataScheme("package") + } + // BroadcastDispatcher does not allow filters with data schemes + context.registerReceiver(appChangeReceiver, uninstallFilter) + + // Register for Smartspace data updates. + smartspaceMediaDataProvider.registerListener(this) + smartspaceSession = + smartspaceManager?.createSmartspaceSession( + SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build() + ) + smartspaceSession?.let { + it.addOnTargetsAvailableListener( + // Use a main uiExecutor thread listening to Smartspace updates instead of using + // the existing background executor. + // SmartspaceSession has scheduled routine updates which can be unpredictable on + // test simulators, using the backgroundExecutor makes it's hard to test the threads + // numbers. + uiExecutor, + SmartspaceSession.OnTargetsAvailableListener { targets -> + smartspaceMediaDataProvider.onTargetsAvailable(targets) + } + ) + } + smartspaceSession?.let { it.requestSmartspaceUpdate() } + tunerService.addTunable( + object : TunerService.Tunable { + override fun onTuningChanged(key: String?, newValue: String?) { + allowMediaRecommendations = allowMediaRecommendations(context) + if (!allowMediaRecommendations) { + dismissSmartspaceRecommendation( + key = smartspaceMediaData.targetId, + delay = 0L + ) + } + } + }, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION + ) + } + + override fun destroy() { + smartspaceMediaDataProvider.unregisterListener(this) + smartspaceSession?.close() + smartspaceSession = null + context.unregisterReceiver(appChangeReceiver) + } + + override fun onNotificationAdded(key: String, sbn: StatusBarNotification) { + if (useQsMediaPlayer && isMediaNotification(sbn)) { + var isNewlyActiveEntry = false + Assert.isMainThread() + val oldKey = findExistingEntry(key, sbn.packageName) + if (oldKey == null) { + val instanceId = logger.getNewInstanceId() + val temp = + LOADING.copy( + packageName = sbn.packageName, + instanceId = instanceId, + createdTimestampMillis = systemClock.currentTimeMillis(), + ) + mediaEntries.put(key, temp) + isNewlyActiveEntry = true + } else if (oldKey != key) { + // Resume -> active conversion; move to new key + val oldData = mediaEntries.remove(oldKey)!! + isNewlyActiveEntry = true + mediaEntries.put(key, oldData) + } + loadMediaData(key, sbn, oldKey, isNewlyActiveEntry) + } else { + onNotificationRemoved(key) + } + } + + private fun removeAllForPackage(packageName: String) { + Assert.isMainThread() + val toRemove = mediaEntries.filter { it.value.packageName == packageName } + toRemove.forEach { removeEntry(it.key) } + } + + override fun setResumeAction(key: String, action: Runnable?) { + mediaEntries.get(key)?.let { + it.resumeAction = action + it.hasCheckedForResume = true + } + } + + override fun addResumptionControls( + userId: Int, + desc: MediaDescription, + action: Runnable, + token: MediaSession.Token, + appName: String, + appIntent: PendingIntent, + packageName: String + ) { + // Resume controls don't have a notification key, so store by package name instead + if (!mediaEntries.containsKey(packageName)) { + val instanceId = logger.getNewInstanceId() + val appUid = + try { + context.packageManager.getApplicationInfo(packageName, 0)?.uid!! + } catch (e: PackageManager.NameNotFoundException) { + Log.w(TAG, "Could not get app UID for $packageName", e) + Process.INVALID_UID + } + + val resumeData = + LOADING.copy( + packageName = packageName, + resumeAction = action, + hasCheckedForResume = true, + instanceId = instanceId, + appUid = appUid, + createdTimestampMillis = systemClock.currentTimeMillis(), + ) + mediaEntries.put(packageName, resumeData) + logSingleVsMultipleMediaAdded(appUid, packageName, instanceId) + logger.logResumeMediaAdded(appUid, packageName, instanceId) + } + backgroundExecutor.execute { + loadMediaDataInBgForResumption( + userId, + desc, + action, + token, + appName, + appIntent, + packageName + ) + } + } + + /** + * Check if there is an existing entry that matches the key or package name. Returns the key + * that matches, or null if not found. + */ + private fun findExistingEntry(key: String, packageName: String): String? { + if (mediaEntries.containsKey(key)) { + return key + } + // Check if we already had a resume player + if (mediaEntries.containsKey(packageName)) { + return packageName + } + return null + } + + private fun loadMediaData( + key: String, + sbn: StatusBarNotification, + oldKey: String?, + isNewlyActiveEntry: Boolean = false, + ) { + backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) } + } + + /** Add a listener for changes in this class */ + override fun addListener(listener: MediaDataManager.Listener) { + // mediaDataFilter is the current end of the internal pipeline. Register external + // listeners as listeners to it. + mediaDataFilter.addListener(listener) + } + + /** Remove a listener for changes in this class */ + override fun removeListener(listener: MediaDataManager.Listener) { + // Since mediaDataFilter is the current end of the internal pipelie, external listeners + // have been registered to it. So, they need to be removed from it too. + mediaDataFilter.removeListener(listener) + } + + /** Add a listener for internal events. */ + private fun addInternalListener(listener: MediaDataManager.Listener) = + internalListeners.add(listener) + + /** + * Notify internal listeners of media loaded event. + * + * External listeners registered with [addListener] will be notified after the event propagates + * through the internal listener pipeline. + */ + private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) { + internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) } + } + + /** + * Notify internal listeners of Smartspace media loaded event. + * + * External listeners registered with [addListener] will be notified after the event propagates + * through the internal listener pipeline. + */ + private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) { + internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) } + } + + /** + * Notify internal listeners of media removed event. + * + * External listeners registered with [addListener] will be notified after the event propagates + * through the internal listener pipeline. + */ + private fun notifyMediaDataRemoved(key: String) { + internalListeners.forEach { it.onMediaDataRemoved(key) } + } + + /** + * Notify internal listeners of Smartspace media removed event. + * + * External listeners registered with [addListener] will be notified after the event propagates + * through the internal listener pipeline. + * + * @param immediately indicates should apply the UI changes immediately, otherwise wait until + * the next refresh-round before UI becomes visible. Should only be true if the update is + * initiated by user's interaction. + */ + private fun notifySmartspaceMediaDataRemoved(key: String, immediately: Boolean) { + internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } + } + + /** + * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This + * will make the player not active anymore, hiding it from QQS and Keyguard. + * + * @see MediaData.active + */ + override fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean) { + mediaEntries[key]?.let { + if (timedOut && !forceUpdate) { + // Only log this event when media expires on its own + logger.logMediaTimeout(it.appUid, it.packageName, it.instanceId) + } + if (it.active == !timedOut && !forceUpdate) { + if (it.resumption) { + if (DEBUG) Log.d(TAG, "timing out resume player $key") + dismissMediaData(key, 0L /* delay */) + } + return + } + // Update last active if media was still active. + if (it.active) { + it.lastActive = systemClock.elapsedRealtime() + } + it.active = !timedOut + if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut") + onMediaDataLoaded(key, key, it) + } + + if (key == smartspaceMediaData.targetId) { + if (DEBUG) Log.d(TAG, "smartspace card expired") + dismissSmartspaceRecommendation(key, delay = 0L) + } + } + + /** Called when the player's [PlaybackState] has been updated with new actions and/or state */ + private fun updateState(key: String, state: PlaybackState) { + mediaEntries.get(key)?.let { + val token = it.token + if (token == null) { + if (DEBUG) Log.d(TAG, "State updated, but token was null") + return + } + val actions = + createActionsFromState( + it.packageName, + mediaControllerFactory.create(it.token), + UserHandle(it.userId) + ) + + // Control buttons + // If flag is enabled and controller has a PlaybackState, + // create actions from session info + // otherwise, no need to update semantic actions. + val data = + if (actions != null) { + it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state)) + } else { + it.copy(isPlaying = isPlayingState(state.state)) + } + if (DEBUG) Log.d(TAG, "State updated outside of notification") + onMediaDataLoaded(key, key, data) + } + } + + private fun removeEntry(key: String, logEvent: Boolean = true) { + mediaEntries.remove(key)?.let { + if (logEvent) { + logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId) + } + } + notifyMediaDataRemoved(key) + } + + /** Dismiss a media entry. Returns false if the key was not found. */ + override fun dismissMediaData(key: String, delay: Long): Boolean { + val existed = mediaEntries[key] != null + backgroundExecutor.execute { + mediaEntries[key]?.let { mediaData -> + if (mediaData.isLocalSession()) { + mediaData.token?.let { + val mediaController = mediaControllerFactory.create(it) + mediaController.transportControls.stop() + } + } + } + } + foregroundExecutor.executeDelayed({ removeEntry(key) }, delay) + return existed + } + + /** + * Called whenever the recommendation has been expired or removed by the user. This will remove + * the recommendation card entirely from the carousel. + */ + override fun dismissSmartspaceRecommendation(key: String, delay: Long) { + if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) { + // If this doesn't match, or we've already invalidated the data, no action needed + return + } + + if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target") + if (smartspaceMediaData.isActive) { + smartspaceMediaData = + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = smartspaceMediaData.targetId, + instanceId = smartspaceMediaData.instanceId + ) + } + foregroundExecutor.executeDelayed( + { notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = true) }, + delay + ) + } + + /** Called when the recommendation card should no longer be visible in QQS or lockscreen */ + override fun setRecommendationInactive(key: String) { + if (!mediaFlags.isPersistentSsCardEnabled()) { + Log.e(TAG, "Only persistent recommendation can be inactive!") + return + } + if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive") + + if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) { + // If this doesn't match, or we've already invalidated the data, no action needed + return + } + + smartspaceMediaData = smartspaceMediaData.copy(isActive = false) + notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData) + } + + private fun loadMediaDataInBgForResumption( + userId: Int, + desc: MediaDescription, + resumeAction: Runnable, + token: MediaSession.Token, + appName: String, + appIntent: PendingIntent, + packageName: String + ) { + if (desc.title.isNullOrBlank()) { + Log.e(TAG, "Description incomplete") + // Delete the placeholder entry + mediaEntries.remove(packageName) + return + } + + if (DEBUG) { + Log.d(TAG, "adding track for $userId from browser: $desc") + } + + val currentEntry = mediaEntries.get(packageName) + val appUid = currentEntry?.appUid ?: Process.INVALID_UID + + // Album art + var artworkBitmap = desc.iconBitmap + if (artworkBitmap == null && desc.iconUri != null) { + artworkBitmap = loadBitmapFromUriForUser(desc.iconUri!!, userId, appUid, packageName) + } + val artworkIcon = + if (artworkBitmap != null) { + Icon.createWithBitmap(artworkBitmap) + } else { + null + } + + val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() + val isExplicit = + desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + + val progress = + if (mediaFlags.isResumeProgressEnabled()) { + MediaDataUtils.getDescriptionProgress(desc.extras) + } else null + + val mediaAction = getResumeMediaAction(resumeAction) + val lastActive = systemClock.elapsedRealtime() + val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L + foregroundExecutor.execute { + onMediaDataLoaded( + packageName, + null, + MediaData( + userId, + true, + appName, + null, + desc.subtitle, + desc.title, + artworkIcon, + listOf(mediaAction), + listOf(0), + MediaButton(playOrPause = mediaAction), + packageName, + token, + appIntent, + device = null, + active = false, + resumeAction = resumeAction, + resumption = true, + notificationKey = packageName, + hasCheckedForResume = true, + lastActive = lastActive, + createdTimestampMillis = createdTimestampMillis, + instanceId = instanceId, + appUid = appUid, + isExplicit = isExplicit, + resumeProgress = progress, + ) + ) + } + } + + fun loadMediaDataInBg( + key: String, + sbn: StatusBarNotification, + oldKey: String?, + isNewlyActiveEntry: Boolean = false, + ) { + val token = + sbn.notification.extras.getParcelable( + Notification.EXTRA_MEDIA_SESSION, + MediaSession.Token::class.java + ) + if (token == null) { + return + } + val mediaController = mediaControllerFactory.create(token) + val metadata = mediaController.metadata + val notif: Notification = sbn.notification + + val appInfo = + notif.extras.getParcelable( + Notification.EXTRA_BUILDER_APPLICATION_INFO, + ApplicationInfo::class.java + ) + ?: getAppInfoFromPackage(sbn.packageName) + + // App name + val appName = getAppName(sbn, appInfo) + + // Song name + var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) + if (song.isNullOrBlank()) { + song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) + } + if (song.isNullOrBlank()) { + song = HybridGroupManager.resolveTitle(notif) + } + if (song.isNullOrBlank()) { + // For apps that don't include a title, log and add a placeholder + song = context.getString(R.string.controls_media_empty_title, appName) + try { + statusBarManager.logBlankMediaTitle(sbn.packageName, sbn.user.identifier) + } catch (e: RuntimeException) { + Log.e(TAG, "Error reporting blank media title for package ${sbn.packageName}") + } + } + + // Album art + var artworkBitmap = metadata?.let { loadBitmapFromUri(it) } + if (artworkBitmap == null) { + artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART) + } + if (artworkBitmap == null) { + artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) + } + val artWorkIcon = + if (artworkBitmap == null) { + notif.getLargeIcon() + } else { + Icon.createWithBitmap(artworkBitmap) + } + + // App Icon + val smallIcon = sbn.notification.smallIcon + + // Explicit Indicator + var isExplicit = false + val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata) + isExplicit = + mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + + // Artist name + var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) + if (artist.isNullOrBlank()) { + artist = HybridGroupManager.resolveText(notif) + } + + // Device name (used for remote cast notifications) + var device: MediaDeviceData? = null + if (isRemoteCastNotification(sbn)) { + val extras = sbn.notification.extras + val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null) + val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1) + val deviceIntent = + extras.getParcelable( + Notification.EXTRA_MEDIA_REMOTE_INTENT, + PendingIntent::class.java + ) + Log.d(TAG, "$key is RCN for $deviceName") + + if (deviceName != null && deviceIcon > -1) { + // Name and icon must be present, but intent may be null + val enabled = deviceIntent != null && deviceIntent.isActivity + val deviceDrawable = + Icon.createWithResource(sbn.packageName, deviceIcon) + .loadDrawable(sbn.getPackageContext(context)) + device = + MediaDeviceData( + enabled, + deviceDrawable, + deviceName, + deviceIntent, + showBroadcastButton = false + ) + } + } + + // Control buttons + // If flag is enabled and controller has a PlaybackState, create actions from session info + // Otherwise, use the notification actions + var actionIcons: List<MediaAction> = emptyList() + var actionsToShowCollapsed: List<Int> = emptyList() + val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user) + if (semanticActions == null) { + val actions = createActionsFromNotification(sbn) + actionIcons = actions.first + actionsToShowCollapsed = actions.second + } + + val playbackLocation = + if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE + else if ( + mediaController.playbackInfo?.playbackType == + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL + ) + MediaData.PLAYBACK_LOCAL + else MediaData.PLAYBACK_CAST_LOCAL + val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null + + val currentEntry = mediaEntries.get(key) + val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() + val appUid = appInfo?.uid ?: Process.INVALID_UID + + if (isNewlyActiveEntry) { + logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId) + logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation) + } else if (playbackLocation != currentEntry?.playbackLocation) { + logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation) + } + + val lastActive = systemClock.elapsedRealtime() + val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L + foregroundExecutor.execute { + val resumeAction: Runnable? = mediaEntries[key]?.resumeAction + val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true + val active = mediaEntries[key]?.active ?: true + onMediaDataLoaded( + key, + oldKey, + MediaData( + sbn.normalizedUserId, + true, + appName, + smallIcon, + artist, + song, + artWorkIcon, + actionIcons, + actionsToShowCollapsed, + semanticActions, + sbn.packageName, + token, + notif.contentIntent, + device, + active, + resumeAction = resumeAction, + playbackLocation = playbackLocation, + notificationKey = key, + hasCheckedForResume = hasCheckedForResume, + isPlaying = isPlaying, + isClearable = !sbn.isOngoing, + lastActive = lastActive, + createdTimestampMillis = createdTimestampMillis, + instanceId = instanceId, + appUid = appUid, + isExplicit = isExplicit, + ) + ) + } + } + + private fun logSingleVsMultipleMediaAdded( + appUid: Int, + packageName: String, + instanceId: InstanceId + ) { + if (mediaEntries.size == 1) { + logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId) + } else if (mediaEntries.size == 2) { + // Since this method is only called when there is a new media session added. + // logging needed once there is more than one media session in carousel. + logger.logMultipleMediaPlayersInCarousel(appUid, packageName, instanceId) + } + } + + private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? { + try { + return context.packageManager.getApplicationInfo(packageName, 0) + } catch (e: PackageManager.NameNotFoundException) { + Log.w(TAG, "Could not get app info for $packageName", e) + } + return null + } + + private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String { + val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME) + if (name != null) { + return name + } + + return if (appInfo != null) { + context.packageManager.getApplicationLabel(appInfo).toString() + } else { + sbn.packageName + } + } + + /** Generate action buttons based on notification actions */ + private fun createActionsFromNotification( + sbn: StatusBarNotification + ): Pair<List<MediaAction>, List<Int>> { + val notif = sbn.notification + val actionIcons: MutableList<MediaAction> = ArrayList() + val actions = notif.actions + var actionsToShowCollapsed = + notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() + ?: mutableListOf() + if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) { + Log.e( + TAG, + "Too many compact actions for ${sbn.key}," + + "limiting to first $MAX_COMPACT_ACTIONS" + ) + actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS) + } + + if (actions != null) { + for ((index, action) in actions.withIndex()) { + if (index == MAX_NOTIFICATION_ACTIONS) { + Log.w( + TAG, + "Too many notification actions for ${sbn.key}," + + " limiting to first $MAX_NOTIFICATION_ACTIONS" + ) + break + } + if (action.getIcon() == null) { + if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}") + actionsToShowCollapsed.remove(index) + continue + } + val runnable = + if (action.actionIntent != null) { + Runnable { + if (action.actionIntent.isActivity) { + activityStarter.startPendingIntentDismissingKeyguard( + action.actionIntent + ) + } else if (action.isAuthenticationRequired()) { + activityStarter.dismissKeyguardThenExecute( + { + var result = sendPendingIntent(action.actionIntent) + result + }, + {}, + true + ) + } else { + sendPendingIntent(action.actionIntent) + } + } + } else { + null + } + val mediaActionIcon = + if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) { + Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId()) + } else { + action.getIcon() + } + .setTint(themeText) + .loadDrawable(context) + val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null) + actionIcons.add(mediaAction) + } + } + return Pair(actionIcons, actionsToShowCollapsed) + } + + /** + * Generates action button info for this media session based on the PlaybackState + * + * @param packageName Package name for the media app + * @param controller MediaController for the current session + * @return a Pair consisting of a list of media actions, and a list of ints representing which + * + * ``` + * of those actions should be shown in the compact player + * ``` + */ + private fun createActionsFromState( + packageName: String, + controller: MediaController, + user: UserHandle + ): MediaButton? { + val state = controller.playbackState + if (state == null || !mediaFlags.areMediaSessionActionsEnabled(packageName, user)) { + return null + } + + // First, check for standard actions + val playOrPause = + if (isConnectingState(state.state)) { + // Spinner needs to be animating to render anything. Start it here. + val drawable = + context.getDrawable(com.android.internal.R.drawable.progress_small_material) + (drawable as Animatable).start() + MediaAction( + drawable, + null, // no action to perform when clicked + context.getString(R.string.controls_media_button_connecting), + context.getDrawable(R.drawable.ic_media_connecting_container), + // Specify a rebind id to prevent the spinner from restarting on later binds. + com.android.internal.R.drawable.progress_small_material + ) + } else if (isPlayingState(state.state)) { + getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE) + } else { + getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY) + } + val prevButton = + getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS) + val nextButton = + getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT) + + // Then, create a way to build any custom actions that will be needed + val customActions = + state.customActions + .asSequence() + .filterNotNull() + .map { getCustomAction(state, packageName, controller, it) } + .iterator() + fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null + + // Finally, assign the remaining button slots: play/pause A B C D + // A = previous, else custom action (if not reserved) + // B = next, else custom action (if not reserved) + // C and D are always custom actions + val reservePrev = + controller.extras?.getBoolean( + MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV + ) == true + val reserveNext = + controller.extras?.getBoolean( + MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT + ) == true + + val prevOrCustom = + if (prevButton != null) { + prevButton + } else if (!reservePrev) { + nextCustomAction() + } else { + null + } + + val nextOrCustom = + if (nextButton != null) { + nextButton + } else if (!reserveNext) { + nextCustomAction() + } else { + null + } + + return MediaButton( + playOrPause, + nextOrCustom, + prevOrCustom, + nextCustomAction(), + nextCustomAction(), + reserveNext, + reservePrev + ) + } + + /** + * Create a [MediaAction] for a given action and media session + * + * @param controller MediaController for the session + * @param stateActions The actions included with the session's [PlaybackState] + * @param action A [PlaybackState.Actions] value representing what action to generate. One of: + * ``` + * [PlaybackState.ACTION_PLAY] + * [PlaybackState.ACTION_PAUSE] + * [PlaybackState.ACTION_SKIP_TO_PREVIOUS] + * [PlaybackState.ACTION_SKIP_TO_NEXT] + * @return + * ``` + * + * A [MediaAction] with correct values set, or null if the state doesn't support it + */ + private fun getStandardAction( + controller: MediaController, + stateActions: Long, + @PlaybackState.Actions action: Long + ): MediaAction? { + if (!includesAction(stateActions, action)) { + return null + } + + return when (action) { + PlaybackState.ACTION_PLAY -> { + MediaAction( + context.getDrawable(R.drawable.ic_media_play), + { controller.transportControls.play() }, + context.getString(R.string.controls_media_button_play), + context.getDrawable(R.drawable.ic_media_play_container) + ) + } + PlaybackState.ACTION_PAUSE -> { + MediaAction( + context.getDrawable(R.drawable.ic_media_pause), + { controller.transportControls.pause() }, + context.getString(R.string.controls_media_button_pause), + context.getDrawable(R.drawable.ic_media_pause_container) + ) + } + PlaybackState.ACTION_SKIP_TO_PREVIOUS -> { + MediaAction( + context.getDrawable(R.drawable.ic_media_prev), + { controller.transportControls.skipToPrevious() }, + context.getString(R.string.controls_media_button_prev), + null + ) + } + PlaybackState.ACTION_SKIP_TO_NEXT -> { + MediaAction( + context.getDrawable(R.drawable.ic_media_next), + { controller.transportControls.skipToNext() }, + context.getString(R.string.controls_media_button_next), + null + ) + } + else -> null + } + } + + /** Check whether the actions from a [PlaybackState] include a specific action */ + private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean { + if ( + (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) && + (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L) + ) { + return true + } + return (stateActions and action != 0L) + } + + /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */ + private fun getCustomAction( + state: PlaybackState, + packageName: String, + controller: MediaController, + customAction: PlaybackState.CustomAction + ): MediaAction { + return MediaAction( + Icon.createWithResource(packageName, customAction.icon).loadDrawable(context), + { controller.transportControls.sendCustomAction(customAction, customAction.extras) }, + customAction.name, + null + ) + } + + /** Load a bitmap from the various Art metadata URIs */ + private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? { + for (uri in ART_URIS) { + val uriString = metadata.getString(uri) + if (!TextUtils.isEmpty(uriString)) { + val albumArt = loadBitmapFromUri(Uri.parse(uriString)) + if (albumArt != null) { + if (DEBUG) Log.d(TAG, "loaded art from $uri") + return albumArt + } + } + } + return null + } + + private fun sendPendingIntent(intent: PendingIntent): Boolean { + return try { + val options = BroadcastOptions.makeBasic() + options.setInteractive(true) + options.setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + ) + intent.send(options.toBundle()) + true + } catch (e: PendingIntent.CanceledException) { + Log.d(TAG, "Intent canceled", e) + false + } + } + + /** Returns a bitmap if the user can access the given URI, else null */ + private fun loadBitmapFromUriForUser( + uri: Uri, + userId: Int, + appUid: Int, + packageName: String, + ): Bitmap? { + try { + val ugm = UriGrantsManager.getService() + ugm.checkGrantUriPermission_ignoreNonSystem( + appUid, + packageName, + ContentProvider.getUriWithoutUserId(uri), + Intent.FLAG_GRANT_READ_URI_PERMISSION, + ContentProvider.getUserIdFromUri(uri, userId) + ) + return loadBitmapFromUri(uri) + } catch (e: SecurityException) { + Log.e(TAG, "Failed to get URI permission: $e") + } + return null + } + + /** + * Load a bitmap from a URI + * + * @param uri the uri to load + * @return bitmap, or null if couldn't be loaded + */ + private fun loadBitmapFromUri(uri: Uri): Bitmap? { + // ImageDecoder requires a scheme of the following types + if (uri.scheme == null) { + return null + } + + if ( + !uri.scheme.equals(ContentResolver.SCHEME_CONTENT) && + !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) && + !uri.scheme.equals(ContentResolver.SCHEME_FILE) + ) { + return null + } + + val source = ImageDecoder.createSource(context.contentResolver, uri) + return try { + ImageDecoder.decodeBitmap(source) { decoder, info, _ -> + val width = info.size.width + val height = info.size.height + val scale = + MediaDataUtils.getScaleFactor( + APair(width, height), + APair(artworkWidth, artworkHeight) + ) + + // Downscale if needed + if (scale != 0f && scale < 1) { + decoder.setTargetSize((scale * width).toInt(), (scale * height).toInt()) + } + decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE + } + } catch (e: IOException) { + Log.e(TAG, "Unable to load bitmap", e) + null + } catch (e: RuntimeException) { + Log.e(TAG, "Unable to load bitmap", e) + null + } + } + + private fun getResumeMediaAction(action: Runnable): MediaAction { + return MediaAction( + Icon.createWithResource(context, R.drawable.ic_media_play) + .setTint(themeText) + .loadDrawable(context), + action, + context.getString(R.string.controls_media_resume), + context.getDrawable(R.drawable.ic_media_play_container) + ) + } + + fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) = + traceSection("MediaDataManager#onMediaDataLoaded") { + Assert.isMainThread() + if (mediaEntries.containsKey(key)) { + // Otherwise this was removed already + mediaEntries.put(key, data) + notifyMediaDataLoaded(key, oldKey, data) + } + } + + override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) { + if (!allowMediaRecommendations) { + if (DEBUG) Log.d(TAG, "Smartspace recommendation is disabled in Settings.") + return + } + + val mediaTargets = targets.filterIsInstance<SmartspaceTarget>() + when (mediaTargets.size) { + 0 -> { + if (!smartspaceMediaData.isActive) { + return + } + if (DEBUG) { + Log.d(TAG, "Set Smartspace media to be inactive for the data update") + } + if (mediaFlags.isPersistentSsCardEnabled()) { + // Smartspace uses this signal to hide the card (e.g. when it expires or user + // disconnects headphones), so treat as setting inactive when flag is on + smartspaceMediaData = smartspaceMediaData.copy(isActive = false) + notifySmartspaceMediaDataLoaded( + smartspaceMediaData.targetId, + smartspaceMediaData, + ) + } else { + smartspaceMediaData = + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = smartspaceMediaData.targetId, + instanceId = smartspaceMediaData.instanceId, + ) + notifySmartspaceMediaDataRemoved( + smartspaceMediaData.targetId, + immediately = false, + ) + } + } + 1 -> { + val newMediaTarget = mediaTargets.get(0) + if (smartspaceMediaData.targetId == newMediaTarget.smartspaceTargetId) { + // The same Smartspace updates can be received. Skip the duplicate updates. + return + } + if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.") + smartspaceMediaData = toSmartspaceMediaData(newMediaTarget) + notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData) + } + else -> { + // There should NOT be more than 1 Smartspace media update. When it happens, it + // indicates a bad state or an error. Reset the status accordingly. + Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...") + notifySmartspaceMediaDataRemoved( + smartspaceMediaData.targetId, + immediately = false, + ) + smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA + } + } + } + + override fun onNotificationRemoved(key: String) { + Assert.isMainThread() + val removed = mediaEntries.remove(key) ?: return + if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) { + logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) + } else if (isAbleToResume(removed)) { + convertToResumePlayer(key, removed) + } else if (mediaFlags.isRetainingPlayersEnabled()) { + handlePossibleRemoval(key, removed, notificationRemoved = true) + } else { + notifyMediaDataRemoved(key) + logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) + } + } + + private fun onSessionDestroyed(key: String) { + if (DEBUG) Log.d(TAG, "session destroyed for $key") + val entry = mediaEntries.remove(key) ?: return + // Clear token since the session is no longer valid + val updated = entry.copy(token = null) + handlePossibleRemoval(key, updated) + } + + private fun isAbleToResume(data: MediaData): Boolean { + val isEligibleForResume = + data.isLocalSession() || + (mediaFlags.isRemoteResumeAllowed() && + data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE) + return useMediaResumption && data.resumeAction != null && isEligibleForResume + } + + /** + * Convert to resume state if the player is no longer valid and active, then notify listeners + * that the data was updated. Does not convert to resume state if the player is still valid, or + * if it was removed before becoming inactive. (Assumes that [removed] was removed from + * [mediaEntries] before this function was called) + */ + private fun handlePossibleRemoval( + key: String, + removed: MediaData, + notificationRemoved: Boolean = false + ) { + val hasSession = removed.token != null + if (hasSession && removed.semanticActions != null) { + // The app was using session actions, and the session is still valid: keep player + if (DEBUG) Log.d(TAG, "Notification removed but using session actions $key") + mediaEntries.put(key, removed) + notifyMediaDataLoaded(key, key, removed) + } else if (!notificationRemoved && removed.semanticActions == null) { + // The app was using notification actions, and notif wasn't removed yet: keep player + if (DEBUG) Log.d(TAG, "Session destroyed but using notification actions $key") + mediaEntries.put(key, removed) + notifyMediaDataLoaded(key, key, removed) + } else if (removed.active && !isAbleToResume(removed)) { + // This player was still active - it didn't last long enough to time out, + // and its app doesn't normally support resume: remove + if (DEBUG) Log.d(TAG, "Removing still-active player $key") + notifyMediaDataRemoved(key) + logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) + } else if (mediaFlags.isRetainingPlayersEnabled() || isAbleToResume(removed)) { + // Convert to resume + if (DEBUG) { + Log.d( + TAG, + "Notification ($notificationRemoved) and/or session " + + "($hasSession) gone for inactive player $key" + ) + } + convertToResumePlayer(key, removed) + } else { + // Retaining players flag is off and app doesn't support resume: remove player. + if (DEBUG) Log.d(TAG, "Removing player $key") + notifyMediaDataRemoved(key) + logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) + } + } + + /** Set the given [MediaData] as a resume state player and notify listeners */ + private fun convertToResumePlayer(key: String, data: MediaData) { + if (DEBUG) Log.d(TAG, "Converting $key to resume") + // Resumption controls must have a title. + if (data.song.isNullOrBlank()) { + Log.e(TAG, "Description incomplete") + notifyMediaDataRemoved(key) + logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId) + return + } + // Move to resume key (aka package name) if that key doesn't already exist. + val resumeAction = data.resumeAction?.let { getResumeMediaAction(it) } + val actions = resumeAction?.let { listOf(resumeAction) } ?: emptyList() + val launcherIntent = + context.packageManager.getLaunchIntentForPackage(data.packageName)?.let { + PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE) + } + val lastActive = + if (data.active) { + systemClock.elapsedRealtime() + } else { + data.lastActive + } + val updated = + data.copy( + token = null, + actions = actions, + semanticActions = MediaButton(playOrPause = resumeAction), + actionsToShowInCompact = listOf(0), + active = false, + resumption = true, + isPlaying = false, + isClearable = true, + clickIntent = launcherIntent, + lastActive = lastActive, + ) + val pkg = data.packageName + val migrate = mediaEntries.put(pkg, updated) == null + // Notify listeners of "new" controls when migrating or removed and update when not + Log.d(TAG, "migrating? $migrate from $key -> $pkg") + if (migrate) { + notifyMediaDataLoaded(key = pkg, oldKey = key, info = updated) + } else { + // Since packageName is used for the key of the resumption controls, it is + // possible that another notification has already been reused for the resumption + // controls of this package. In this case, rather than renaming this player as + // packageName, just remove it and then send a update to the existing resumption + // controls. + notifyMediaDataRemoved(key) + notifyMediaDataLoaded(key = pkg, oldKey = pkg, info = updated) + } + logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId) + + // Limit total number of resume controls + val resumeEntries = mediaEntries.filter { (key, data) -> data.resumption } + val numResume = resumeEntries.size + if (numResume > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) { + resumeEntries + .toList() + .sortedBy { (key, data) -> data.lastActive } + .subList(0, numResume - ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) + .forEach { (key, data) -> + Log.d(TAG, "Removing excess control $key") + mediaEntries.remove(key) + notifyMediaDataRemoved(key) + logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId) + } + } + } + + override fun setMediaResumptionEnabled(isEnabled: Boolean) { + if (useMediaResumption == isEnabled) { + return + } + + useMediaResumption = isEnabled + + if (!useMediaResumption) { + // Remove any existing resume controls + val filtered = mediaEntries.filter { !it.value.active } + filtered.forEach { + mediaEntries.remove(it.key) + notifyMediaDataRemoved(it.key) + logger.logMediaRemoved(it.value.appUid, it.value.packageName, it.value.instanceId) + } + } + } + + /** Invoked when the user has dismissed the media carousel */ + override fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss() + + /** Are there any media notifications active, including the recommendations? */ + override fun hasActiveMediaOrRecommendation() = mediaDataFilter.hasActiveMediaOrRecommendation() + + /** + * Are there any media entries we should display, including the recommendations? + * - If resumption is enabled, this will include inactive players + * - If resumption is disabled, we only want to show active players + */ + override fun hasAnyMediaOrRecommendation() = mediaDataFilter.hasAnyMediaOrRecommendation() + + /** Are there any resume media notifications active, excluding the recommendations? */ + override fun hasActiveMedia() = mediaDataFilter.hasActiveMedia() + + /** + * Are there any resume media notifications active, excluding the recommendations? + * - If resumption is enabled, this will include inactive players + * - If resumption is disabled, we only want to show active players + */ + override fun hasAnyMedia() = mediaDataFilter.hasAnyMedia() + override fun isRecommendationActive() = smartspaceMediaData.isActive + + /** + * Converts the pass-in SmartspaceTarget to SmartspaceMediaData + * + * @return An empty SmartspaceMediaData with the valid target Id is returned if the + * SmartspaceTarget's data is invalid. + */ + private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData { + val baseAction: SmartspaceAction? = target.baseAction + val dismissIntent = + baseAction?.extras?.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent? + + val isActive = + when { + !mediaFlags.isPersistentSsCardEnabled() -> true + baseAction == null -> true + else -> { + val triggerSource = baseAction.extras?.getString(EXTRA_KEY_TRIGGER_SOURCE) + triggerSource != EXTRA_VALUE_TRIGGER_PERIODIC + } + } + + packageName(target)?.let { + return SmartspaceMediaData( + targetId = target.smartspaceTargetId, + isActive = isActive, + packageName = it, + cardAction = target.baseAction, + recommendations = target.iconGrid, + dismissIntent = dismissIntent, + headphoneConnectionTimeMillis = target.creationTimeMillis, + instanceId = logger.getNewInstanceId(), + expiryTimeMs = target.expiryTimeMillis, + ) + } + return EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = target.smartspaceTargetId, + isActive = isActive, + dismissIntent = dismissIntent, + headphoneConnectionTimeMillis = target.creationTimeMillis, + instanceId = logger.getNewInstanceId(), + expiryTimeMs = target.expiryTimeMillis, + ) + } + + private fun packageName(target: SmartspaceTarget): String? { + val recommendationList = target.iconGrid + if (recommendationList == null || recommendationList.isEmpty()) { + Log.w(TAG, "Empty or null media recommendation list.") + return null + } + for (recommendation in recommendationList) { + val extras = recommendation.extras + extras?.let { + it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { packageName -> + return packageName + } + } + } + Log.w(TAG, "No valid package name is provided.") + return null + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.apply { + println("internalListeners: $internalListeners") + println("externalListeners: ${mediaDataFilter.listeners}") + println("mediaEntries: $mediaEntries") + println("useMediaResumption: $useMediaResumption") + println("allowMediaRecommendations: $allowMediaRecommendations") + } + mediaDeviceManager.dump(pw) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt new file mode 100644 index 000000000000..a65db35030ea --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImpl.kt @@ -0,0 +1,353 @@ +/* + * 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.media.controls.domain.pipeline + +import android.content.Context +import android.content.pm.UserInfo +import android.os.SystemProperties +import android.util.Log +import com.android.internal.annotations.KeepForWeakReference +import com.android.internal.annotations.VisibleForTesting +import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.media.controls.data.repository.MediaFilterRepository +import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_RESUME +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.media.controls.util.MediaFlags +import com.android.systemui.media.controls.util.MediaUiEventLogger +import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.NotificationLockscreenUserManager +import com.android.systemui.util.time.SystemClock +import java.util.SortedMap +import java.util.concurrent.Executor +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +private const val TAG = "MediaDataFilter" +private const val DEBUG = true +private const val EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME = + ("com.google" + + ".android.apps.gsa.staticplugins.opa.smartspace.ExportedSmartspaceTrampolineActivity") +private const val RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY = "resumable_media_max_age_seconds" + +/** + * Filters data updates from [MediaDataCombineLatest] based on the current user ID, and handles user + * switches (removing entries for the previous user, adding back entries for the current user). Also + * filters out smartspace updates in favor of local recent media, when avaialble. + * + * This is added at the end of the pipeline since we may still need to handle callbacks from + * background users (e.g. timeouts). + */ +class MediaDataFilterImpl +@Inject +constructor( + private val context: Context, + userTracker: UserTracker, + private val broadcastSender: BroadcastSender, + private val lockscreenUserManager: NotificationLockscreenUserManager, + @Main private val executor: Executor, + private val systemClock: SystemClock, + private val logger: MediaUiEventLogger, + private val mediaFlags: MediaFlags, + private val mediaFilterRepository: MediaFilterRepository, +) : MediaDataManager.Listener { + private val _listeners: MutableSet<MediaDataManager.Listener> = mutableSetOf() + val listeners: Set<MediaDataManager.Listener> + get() = _listeners.toSet() + lateinit var mediaDataManager: MediaDataManager + + // Ensure the field (and associated reference) isn't removed during optimization. + @KeepForWeakReference + private val userTrackerCallback = + object : UserTracker.Callback { + override fun onUserChanged(newUser: Int, userContext: Context) { + handleUserSwitched() + } + + override fun onProfilesChanged(profiles: List<UserInfo>) { + handleProfileChanged() + } + } + + init { + userTracker.addCallback(userTrackerCallback, executor) + } + + override fun onMediaDataLoaded( + key: String, + oldKey: String?, + data: MediaData, + immediately: Boolean, + receivedSmartspaceCardLatency: Int, + isSsReactivated: Boolean + ) { + if (oldKey != null && oldKey != key) { + mediaFilterRepository.removeMediaEntry(oldKey) + } + mediaFilterRepository.addMediaEntry(key, data) + + if ( + !lockscreenUserManager.isCurrentProfile(data.userId) || + !lockscreenUserManager.isProfileAvailable(data.userId) + ) { + return + } + + if (oldKey != null && oldKey != key) { + mediaFilterRepository.removeSelectedUserMediaEntry(oldKey) + } + mediaFilterRepository.addSelectedUserMediaEntry(key, data) + + // Notify listeners + listeners.forEach { it.onMediaDataLoaded(key, oldKey, data) } + } + + override fun onSmartspaceMediaDataLoaded( + key: String, + data: SmartspaceMediaData, + shouldPrioritize: Boolean + ) { + // With persistent recommendation card, we could get a background update while inactive + // Otherwise, consider it an invalid update + if (!data.isActive && !mediaFlags.isPersistentSsCardEnabled()) { + Log.d(TAG, "Inactive recommendation data. Skip triggering.") + return + } + + // Override the pass-in value here, as the order of Smartspace card is only determined here. + var shouldPrioritizeMutable = false + mediaFilterRepository.setRecommendation(data) + + // Before forwarding the smartspace target, first check if we have recently inactive media + val selectedUserEntries = mediaFilterRepository.selectedUserEntries.value + val sorted = + selectedUserEntries.toSortedMap(compareBy { selectedUserEntries[it]?.lastActive ?: -1 }) + val timeSinceActive = timeSinceActiveForMostRecentMedia(sorted) + var smartspaceMaxAgeMillis = SMARTSPACE_MAX_AGE + data.cardAction?.extras?.let { + val smartspaceMaxAgeSeconds = it.getLong(RESUMABLE_MEDIA_MAX_AGE_SECONDS_KEY, 0) + if (smartspaceMaxAgeSeconds > 0) { + smartspaceMaxAgeMillis = TimeUnit.SECONDS.toMillis(smartspaceMaxAgeSeconds) + } + } + + // Check if smartspace has explicitly specified whether to re-activate resumable media. + // The default behavior is to trigger if the smartspace data is active. + val shouldTriggerResume = + data.cardAction?.extras?.getBoolean(EXTRA_KEY_TRIGGER_RESUME, true) ?: true + val shouldReactivate = + shouldTriggerResume && + !selectedUserEntries.any { it.value.active } && + selectedUserEntries.isNotEmpty() && + data.isActive + + if (timeSinceActive < smartspaceMaxAgeMillis) { + // It could happen there are existing active media resume cards, then we don't need to + // reactivate. + if (shouldReactivate) { + val lastActiveKey = sorted.lastKey() // most recently active + // Notify listeners to consider this media active + Log.d(TAG, "reactivating $lastActiveKey instead of smartspace") + mediaFilterRepository.setReactivatedKey(lastActiveKey) + val mediaData = sorted[lastActiveKey]!!.copy(active = true) + logger.logRecommendationActivated( + mediaData.appUid, + mediaData.packageName, + mediaData.instanceId + ) + listeners.forEach { + it.onMediaDataLoaded( + lastActiveKey, + lastActiveKey, + mediaData, + receivedSmartspaceCardLatency = + (systemClock.currentTimeMillis() - data.headphoneConnectionTimeMillis) + .toInt(), + isSsReactivated = true + ) + } + } + } else if (data.isActive) { + // Mark to prioritize Smartspace card if no recent media. + shouldPrioritizeMutable = true + } + + if (!data.isValid()) { + Log.d(TAG, "Invalid recommendation data. Skip showing the rec card") + return + } + val smartspaceMediaData = mediaFilterRepository.smartspaceMediaData.value + logger.logRecommendationAdded( + smartspaceMediaData.packageName, + smartspaceMediaData.instanceId + ) + listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data, shouldPrioritizeMutable) } + } + + override fun onMediaDataRemoved(key: String) { + mediaFilterRepository.removeMediaEntry(key) + mediaFilterRepository.removeSelectedUserMediaEntry(key)?.let { + // Only notify listeners if something actually changed + listeners.forEach { it.onMediaDataRemoved(key) } + } + } + + override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) { + // First check if we had reactivated media instead of forwarding smartspace + mediaFilterRepository.reactivatedKey.value?.let { + val lastActiveKey = it + mediaFilterRepository.setReactivatedKey(null) + Log.d(TAG, "expiring reactivated key $lastActiveKey") + // Notify listeners to update with actual active value + mediaFilterRepository.selectedUserEntries.value[lastActiveKey]?.let { mediaData -> + listeners.forEach { listener -> + listener.onMediaDataLoaded(lastActiveKey, lastActiveKey, mediaData, immediately) + } + } + } + + val smartspaceMediaData = mediaFilterRepository.smartspaceMediaData.value + if (smartspaceMediaData.isActive) { + mediaFilterRepository.setRecommendation( + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = smartspaceMediaData.targetId, + instanceId = smartspaceMediaData.instanceId + ) + ) + } + listeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } + } + + @VisibleForTesting + internal fun handleProfileChanged() { + // TODO(b/317221348) re-add media removed when profile is available. + mediaFilterRepository.allUserEntries.value.forEach { (key, data) -> + if (!lockscreenUserManager.isProfileAvailable(data.userId)) { + // Only remove media when the profile is unavailable. + if (DEBUG) Log.d(TAG, "Removing $key after profile change") + mediaFilterRepository.removeSelectedUserMediaEntry(key, data) + listeners.forEach { listener -> listener.onMediaDataRemoved(key) } + } + } + } + + @VisibleForTesting + internal fun handleUserSwitched() { + // If the user changes, remove all current MediaData objects and inform listeners + val listenersCopy = listeners + val keyCopy = mediaFilterRepository.selectedUserEntries.value.keys.toMutableList() + // Clear the list first, to make sure callbacks from listeners if we have any entries + // are up to date + mediaFilterRepository.clearSelectedUserMedia() + keyCopy.forEach { + if (DEBUG) Log.d(TAG, "Removing $it after user change") + listenersCopy.forEach { listener -> listener.onMediaDataRemoved(it) } + } + + mediaFilterRepository.allUserEntries.value.forEach { (key, data) -> + if (lockscreenUserManager.isCurrentProfile(data.userId)) { + if (DEBUG) Log.d(TAG, "Re-adding $key after user change") + mediaFilterRepository.addSelectedUserMediaEntry(key, data) + listenersCopy.forEach { listener -> listener.onMediaDataLoaded(key, null, data) } + } + } + } + + /** Invoked when the user has dismissed the media carousel */ + fun onSwipeToDismiss() { + if (DEBUG) Log.d(TAG, "Media carousel swiped away") + val mediaKeys = mediaFilterRepository.selectedUserEntries.value.keys.toSet() + mediaKeys.forEach { + // Force updates to listeners, needed for re-activated card + mediaDataManager.setInactive(it, timedOut = true, forceUpdate = true) + } + val smartspaceMediaData = mediaFilterRepository.smartspaceMediaData.value + if (smartspaceMediaData.isActive) { + val dismissIntent = smartspaceMediaData.dismissIntent + if (dismissIntent == null) { + Log.w( + TAG, + "Cannot create dismiss action click action: extras missing dismiss_intent." + ) + } else if ( + dismissIntent.component?.className == EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME + ) { + // Dismiss the card Smartspace data through Smartspace trampoline activity. + context.startActivity(dismissIntent) + } else { + broadcastSender.sendBroadcast(dismissIntent) + } + + if (mediaFlags.isPersistentSsCardEnabled()) { + mediaFilterRepository.setRecommendation(smartspaceMediaData.copy(isActive = false)) + mediaDataManager.setRecommendationInactive(smartspaceMediaData.targetId) + } else { + mediaFilterRepository.setRecommendation( + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = smartspaceMediaData.targetId, + instanceId = smartspaceMediaData.instanceId, + ) + ) + mediaDataManager.dismissSmartspaceRecommendation( + smartspaceMediaData.targetId, + delay = 0L, + ) + } + } + } + + /** Add a listener for filtered [MediaData] changes */ + fun addListener(listener: MediaDataManager.Listener) = _listeners.add(listener) + + /** Remove a listener that was registered with addListener */ + fun removeListener(listener: MediaDataManager.Listener) = _listeners.remove(listener) + + /** + * Return the time since last active for the most-recent media. + * + * @param sortedEntries selectedUserEntries sorted from the earliest to the most-recent. + * @return The duration in milliseconds from the most-recent media's last active timestamp to + * the present. MAX_VALUE will be returned if there is no media. + */ + private fun timeSinceActiveForMostRecentMedia( + sortedEntries: SortedMap<String, MediaData> + ): Long { + if (sortedEntries.isEmpty()) { + return Long.MAX_VALUE + } + + val now = systemClock.elapsedRealtime() + val lastActiveKey = sortedEntries.lastKey() // most recently active + return sortedEntries[lastActiveKey]?.let { now - it.lastActive } ?: Long.MAX_VALUE + } + + companion object { + /** + * Maximum age of a media control to re-activate on smartspace signal. If there is no media + * control available within this time window, smartspace recommendations will be shown + * instead. + */ + @VisibleForTesting + internal val SMARTSPACE_MAX_AGE: Long + get() = + SystemProperties.getLong( + "debug.sysui.smartspace_max_age", + TimeUnit.MINUTES.toMillis(30) + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt index 865c49e1d817..2b1070cfeedf 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManager.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 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. @@ -16,554 +16,21 @@ package com.android.systemui.media.controls.domain.pipeline -import android.annotation.SuppressLint -import android.app.ActivityOptions -import android.app.BroadcastOptions -import android.app.Notification -import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME import android.app.PendingIntent -import android.app.StatusBarManager -import android.app.UriGrantsManager -import android.app.smartspace.SmartspaceAction -import android.app.smartspace.SmartspaceConfig -import android.app.smartspace.SmartspaceManager -import android.app.smartspace.SmartspaceSession -import android.app.smartspace.SmartspaceTarget -import android.content.BroadcastReceiver -import android.content.ContentProvider -import android.content.ContentResolver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.content.pm.ApplicationInfo -import android.content.pm.PackageManager -import android.graphics.Bitmap -import android.graphics.ImageDecoder -import android.graphics.drawable.Animatable -import android.graphics.drawable.Icon import android.media.MediaDescription -import android.media.MediaMetadata -import android.media.session.MediaController import android.media.session.MediaSession -import android.media.session.PlaybackState -import android.net.Uri -import android.os.Parcelable -import android.os.Process -import android.os.UserHandle -import android.provider.Settings import android.service.notification.StatusBarNotification -import android.support.v4.media.MediaMetadataCompat -import android.text.TextUtils -import android.util.Log -import android.util.Pair as APair -import androidx.media.utils.MediaConstants -import com.android.app.tracing.traceSection -import com.android.internal.annotations.Keep -import com.android.internal.logging.InstanceId -import com.android.keyguard.KeyguardUpdateMonitor -import com.android.systemui.Dumpable -import com.android.systemui.broadcast.BroadcastDispatcher -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.dump.DumpManager -import com.android.systemui.media.controls.domain.resume.MediaResumeListener -import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser -import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE -import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC -import com.android.systemui.media.controls.shared.model.MediaAction -import com.android.systemui.media.controls.shared.model.MediaButton import com.android.systemui.media.controls.shared.model.MediaData -import com.android.systemui.media.controls.shared.model.MediaDeviceData import com.android.systemui.media.controls.shared.model.SmartspaceMediaData -import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider -import com.android.systemui.media.controls.ui.view.MediaViewHolder -import com.android.systemui.media.controls.util.MediaControllerFactory -import com.android.systemui.media.controls.util.MediaDataUtils -import com.android.systemui.media.controls.util.MediaFlags -import com.android.systemui.media.controls.util.MediaUiEventLogger -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.plugins.BcSmartspaceDataPlugin -import com.android.systemui.res.R -import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState -import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState -import com.android.systemui.statusbar.notification.row.HybridGroupManager -import com.android.systemui.tuner.TunerService -import com.android.systemui.util.Assert -import com.android.systemui.util.Utils -import com.android.systemui.util.concurrency.DelayableExecutor -import com.android.systemui.util.concurrency.ThreadFactory -import com.android.systemui.util.time.SystemClock -import java.io.IOException -import java.io.PrintWriter -import java.util.concurrent.Executor -import javax.inject.Inject -// URI fields to try loading album art from -private val ART_URIS = - arrayOf( - MediaMetadata.METADATA_KEY_ALBUM_ART_URI, - MediaMetadata.METADATA_KEY_ART_URI, - MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI - ) - -private const val TAG = "MediaDataManager" -private const val DEBUG = true -private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent" - -private val LOADING = - MediaData( - userId = -1, - initialized = false, - app = null, - appIcon = null, - artist = null, - song = null, - artwork = null, - actions = emptyList(), - actionsToShowInCompact = emptyList(), - packageName = "INVALID", - token = null, - clickIntent = null, - device = null, - active = true, - resumeAction = null, - instanceId = InstanceId.fakeInstanceId(-1), - appUid = Process.INVALID_UID - ) - -internal val EMPTY_SMARTSPACE_MEDIA_DATA = - SmartspaceMediaData( - targetId = "INVALID", - isActive = false, - packageName = "INVALID", - cardAction = null, - recommendations = emptyList(), - dismissIntent = null, - headphoneConnectionTimeMillis = 0, - instanceId = InstanceId.fakeInstanceId(-1), - expiryTimeMs = 0, - ) - -const val MEDIA_TITLE_ERROR_MESSAGE = "Invalid media data: title is null or blank." - -fun isMediaNotification(sbn: StatusBarNotification): Boolean { - return sbn.notification.isMediaNotification() -} - -/** - * Allow recommendations from smartspace to show in media controls. Requires - * [Utils.useQsMediaPlayer] to be enabled. On by default, but can be disabled by setting to 0 - */ -private fun allowMediaRecommendations(context: Context): Boolean { - val flag = - Settings.Secure.getInt( - context.contentResolver, - Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, - 1 - ) - return Utils.useQsMediaPlayer(context) && flag > 0 -} - -/** A class that facilitates management and loading of Media Data, ready for binding. */ -@SysUISingleton -class MediaDataManager( - private val context: Context, - @Background private val backgroundExecutor: Executor, - @Main private val uiExecutor: Executor, - @Main private val foregroundExecutor: DelayableExecutor, - private val mediaControllerFactory: MediaControllerFactory, - private val broadcastDispatcher: BroadcastDispatcher, - dumpManager: DumpManager, - mediaTimeoutListener: MediaTimeoutListener, - mediaResumeListener: MediaResumeListener, - mediaSessionBasedFilter: MediaSessionBasedFilter, - mediaDeviceManager: MediaDeviceManager, - mediaDataCombineLatest: MediaDataCombineLatest, - private val mediaDataFilter: MediaDataFilter, - private val activityStarter: ActivityStarter, - private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider, - private var useMediaResumption: Boolean, - private val useQsMediaPlayer: Boolean, - private val systemClock: SystemClock, - private val tunerService: TunerService, - private val mediaFlags: MediaFlags, - private val logger: MediaUiEventLogger, - private val smartspaceManager: SmartspaceManager?, - private val keyguardUpdateMonitor: KeyguardUpdateMonitor, -) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener { - - companion object { - // UI surface label for subscribing Smartspace updates. - @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager" - - // Smartspace package name's extra key. - @JvmField val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name" - - // Maximum number of actions allowed in compact view - @JvmField val MAX_COMPACT_ACTIONS = 3 - - // Maximum number of actions allowed in expanded view - @JvmField val MAX_NOTIFICATION_ACTIONS = MediaViewHolder.genericButtonIds.size - } - - private val themeText = - com.android.settingslib.Utils.getColorAttr( - context, - com.android.internal.R.attr.textColorPrimary - ) - .defaultColor - - // Internal listeners are part of the internal pipeline. External listeners (those registered - // with [MediaDeviceManager.addListener]) receive events after they have propagated through - // the internal pipeline. - // Another way to think of the distinction between internal and external listeners is the - // following. Internal listeners are listeners that MediaDataManager depends on, and external - // listeners are listeners that depend on MediaDataManager. - // TODO(b/159539991#comment5): Move internal listeners to separate package. - private val internalListeners: MutableSet<Listener> = mutableSetOf() - private val mediaEntries: LinkedHashMap<String, MediaData> = LinkedHashMap() - // There should ONLY be at most one Smartspace media recommendation. - var smartspaceMediaData: SmartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA - @Keep private var smartspaceSession: SmartspaceSession? = null - private var allowMediaRecommendations = allowMediaRecommendations(context) - - private val artworkWidth = - context.resources.getDimensionPixelSize( - com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize - ) - private val artworkHeight = - context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded) - - @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE - private val statusBarManager = - context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager - - /** Check whether this notification is an RCN */ - private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean { - return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE) - } - - @Inject - constructor( - context: Context, - threadFactory: ThreadFactory, - @Main uiExecutor: Executor, - @Main foregroundExecutor: DelayableExecutor, - mediaControllerFactory: MediaControllerFactory, - dumpManager: DumpManager, - broadcastDispatcher: BroadcastDispatcher, - mediaTimeoutListener: MediaTimeoutListener, - mediaResumeListener: MediaResumeListener, - mediaSessionBasedFilter: MediaSessionBasedFilter, - mediaDeviceManager: MediaDeviceManager, - mediaDataCombineLatest: MediaDataCombineLatest, - mediaDataFilter: MediaDataFilter, - activityStarter: ActivityStarter, - smartspaceMediaDataProvider: SmartspaceMediaDataProvider, - clock: SystemClock, - tunerService: TunerService, - mediaFlags: MediaFlags, - logger: MediaUiEventLogger, - smartspaceManager: SmartspaceManager?, - keyguardUpdateMonitor: KeyguardUpdateMonitor, - ) : this( - context, - // Loading bitmap for UMO background can take longer time, so it cannot run on the default - // background thread. Use a custom thread for media. - threadFactory.buildExecutorOnNewThread(TAG), - uiExecutor, - foregroundExecutor, - mediaControllerFactory, - broadcastDispatcher, - dumpManager, - mediaTimeoutListener, - mediaResumeListener, - mediaSessionBasedFilter, - mediaDeviceManager, - mediaDataCombineLatest, - mediaDataFilter, - activityStarter, - smartspaceMediaDataProvider, - Utils.useMediaResumption(context), - Utils.useQsMediaPlayer(context), - clock, - tunerService, - mediaFlags, - logger, - smartspaceManager, - keyguardUpdateMonitor, - ) - - private val appChangeReceiver = - object : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - when (intent.action) { - Intent.ACTION_PACKAGES_SUSPENDED -> { - val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST) - packages?.forEach { removeAllForPackage(it) } - } - Intent.ACTION_PACKAGE_REMOVED, - Intent.ACTION_PACKAGE_RESTARTED -> { - intent.data?.encodedSchemeSpecificPart?.let { removeAllForPackage(it) } - } - } - } - } - - init { - dumpManager.registerDumpable(TAG, this) - - // Initialize the internal processing pipeline. The listeners at the front of the pipeline - // are set as internal listeners so that they receive events. From there, events are - // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter, - // so it is responsible for dispatching events to external listeners. To achieve this, - // external listeners that are registered with [MediaDataManager.addListener] are actually - // registered as listeners to mediaDataFilter. - addInternalListener(mediaTimeoutListener) - addInternalListener(mediaResumeListener) - addInternalListener(mediaSessionBasedFilter) - mediaSessionBasedFilter.addListener(mediaDeviceManager) - mediaSessionBasedFilter.addListener(mediaDataCombineLatest) - mediaDeviceManager.addListener(mediaDataCombineLatest) - mediaDataCombineLatest.addListener(mediaDataFilter) - - // Set up links back into the pipeline for listeners that need to send events upstream. - mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean -> - setTimedOut(key, timedOut) - } - mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState -> - updateState(key, state) - } - mediaTimeoutListener.sessionCallback = { key: String -> onSessionDestroyed(key) } - mediaResumeListener.setManager(this) - mediaDataFilter.mediaDataManager = this - - val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED) - broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL) - - val uninstallFilter = - IntentFilter().apply { - addAction(Intent.ACTION_PACKAGE_REMOVED) - addAction(Intent.ACTION_PACKAGE_RESTARTED) - addDataScheme("package") - } - // BroadcastDispatcher does not allow filters with data schemes - context.registerReceiver(appChangeReceiver, uninstallFilter) - - // Register for Smartspace data updates. - smartspaceMediaDataProvider.registerListener(this) - smartspaceSession = - smartspaceManager?.createSmartspaceSession( - SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build() - ) - smartspaceSession?.let { - it.addOnTargetsAvailableListener( - // Use a main uiExecutor thread listening to Smartspace updates instead of using - // the existing background executor. - // SmartspaceSession has scheduled routine updates which can be unpredictable on - // test simulators, using the backgroundExecutor makes it's hard to test the threads - // numbers. - uiExecutor, - SmartspaceSession.OnTargetsAvailableListener { targets -> - smartspaceMediaDataProvider.onTargetsAvailable(targets) - } - ) - } - smartspaceSession?.let { it.requestSmartspaceUpdate() } - tunerService.addTunable( - object : TunerService.Tunable { - override fun onTuningChanged(key: String?, newValue: String?) { - allowMediaRecommendations = allowMediaRecommendations(context) - if (!allowMediaRecommendations) { - dismissSmartspaceRecommendation( - key = smartspaceMediaData.targetId, - delay = 0L - ) - } - } - }, - Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION - ) - } - - fun destroy() { - smartspaceMediaDataProvider.unregisterListener(this) - smartspaceSession?.close() - smartspaceSession = null - context.unregisterReceiver(appChangeReceiver) - } - - fun onNotificationAdded(key: String, sbn: StatusBarNotification) { - if (useQsMediaPlayer && isMediaNotification(sbn)) { - var isNewlyActiveEntry = false - Assert.isMainThread() - val oldKey = findExistingEntry(key, sbn.packageName) - if (oldKey == null) { - val instanceId = logger.getNewInstanceId() - val temp = - LOADING.copy( - packageName = sbn.packageName, - instanceId = instanceId, - createdTimestampMillis = systemClock.currentTimeMillis(), - ) - mediaEntries.put(key, temp) - isNewlyActiveEntry = true - } else if (oldKey != key) { - // Resume -> active conversion; move to new key - val oldData = mediaEntries.remove(oldKey)!! - isNewlyActiveEntry = true - mediaEntries.put(key, oldData) - } - loadMediaData(key, sbn, oldKey, isNewlyActiveEntry) - } else { - onNotificationRemoved(key) - } - } - - private fun removeAllForPackage(packageName: String) { - Assert.isMainThread() - val toRemove = mediaEntries.filter { it.value.packageName == packageName } - toRemove.forEach { removeEntry(it.key) } - } - - fun setResumeAction(key: String, action: Runnable?) { - mediaEntries.get(key)?.let { - it.resumeAction = action - it.hasCheckedForResume = true - } - } - - fun addResumptionControls( - userId: Int, - desc: MediaDescription, - action: Runnable, - token: MediaSession.Token, - appName: String, - appIntent: PendingIntent, - packageName: String - ) { - // Resume controls don't have a notification key, so store by package name instead - if (!mediaEntries.containsKey(packageName)) { - val instanceId = logger.getNewInstanceId() - val appUid = - try { - context.packageManager.getApplicationInfo(packageName, 0)?.uid!! - } catch (e: PackageManager.NameNotFoundException) { - Log.w(TAG, "Could not get app UID for $packageName", e) - Process.INVALID_UID - } - - val resumeData = - LOADING.copy( - packageName = packageName, - resumeAction = action, - hasCheckedForResume = true, - instanceId = instanceId, - appUid = appUid, - createdTimestampMillis = systemClock.currentTimeMillis(), - ) - mediaEntries.put(packageName, resumeData) - logSingleVsMultipleMediaAdded(appUid, packageName, instanceId) - logger.logResumeMediaAdded(appUid, packageName, instanceId) - } - backgroundExecutor.execute { - loadMediaDataInBgForResumption( - userId, - desc, - action, - token, - appName, - appIntent, - packageName - ) - } - } - - /** - * Check if there is an existing entry that matches the key or package name. Returns the key - * that matches, or null if not found. - */ - private fun findExistingEntry(key: String, packageName: String): String? { - if (mediaEntries.containsKey(key)) { - return key - } - // Check if we already had a resume player - if (mediaEntries.containsKey(packageName)) { - return packageName - } - return null - } - - private fun loadMediaData( - key: String, - sbn: StatusBarNotification, - oldKey: String?, - isNewlyActiveEntry: Boolean = false, - ) { - backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) } - } +/** Facilitates management and loading of Media Data, ready for binding. */ +interface MediaDataManager { /** Add a listener for changes in this class */ - fun addListener(listener: Listener) { - // mediaDataFilter is the current end of the internal pipeline. Register external - // listeners as listeners to it. - mediaDataFilter.addListener(listener) - } + fun addListener(listener: Listener) /** Remove a listener for changes in this class */ - fun removeListener(listener: Listener) { - // Since mediaDataFilter is the current end of the internal pipelie, external listeners - // have been registered to it. So, they need to be removed from it too. - mediaDataFilter.removeListener(listener) - } - - /** Add a listener for internal events. */ - private fun addInternalListener(listener: Listener) = internalListeners.add(listener) - - /** - * Notify internal listeners of media loaded event. - * - * External listeners registered with [addListener] will be notified after the event propagates - * through the internal listener pipeline. - */ - private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) { - internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) } - } - - /** - * Notify internal listeners of Smartspace media loaded event. - * - * External listeners registered with [addListener] will be notified after the event propagates - * through the internal listener pipeline. - */ - private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) { - internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) } - } - - /** - * Notify internal listeners of media removed event. - * - * External listeners registered with [addListener] will be notified after the event propagates - * through the internal listener pipeline. - */ - private fun notifyMediaDataRemoved(key: String) { - internalListeners.forEach { it.onMediaDataRemoved(key) } - } - - /** - * Notify internal listeners of Smartspace media removed event. - * - * External listeners registered with [addListener] will be notified after the event propagates - * through the internal listener pipeline. - * - * @param immediately indicates should apply the UI changes immediately, otherwise wait until - * the next refresh-round before UI becomes visible. Should only be true if the update is - * initiated by user's interaction. - */ - private fun notifySmartspaceMediaDataRemoved(key: String, immediately: Boolean) { - internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } - } + fun removeListener(listener: Listener) /** * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This @@ -571,1055 +38,64 @@ class MediaDataManager( * * @see MediaData.active */ - internal fun setTimedOut(key: String, timedOut: Boolean, forceUpdate: Boolean = false) { - mediaEntries[key]?.let { - if (timedOut && !forceUpdate) { - // Only log this event when media expires on its own - logger.logMediaTimeout(it.appUid, it.packageName, it.instanceId) - } - if (it.active == !timedOut && !forceUpdate) { - if (it.resumption) { - if (DEBUG) Log.d(TAG, "timing out resume player $key") - dismissMediaData(key, 0L /* delay */) - } - return - } - // Update last active if media was still active. - if (it.active) { - it.lastActive = systemClock.elapsedRealtime() - } - it.active = !timedOut - if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut") - onMediaDataLoaded(key, key, it) - } + fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean = false) - if (key == smartspaceMediaData.targetId) { - if (DEBUG) Log.d(TAG, "smartspace card expired") - dismissSmartspaceRecommendation(key, delay = 0L) - } - } - - /** Called when the player's [PlaybackState] has been updated with new actions and/or state */ - private fun updateState(key: String, state: PlaybackState) { - mediaEntries.get(key)?.let { - val token = it.token - if (token == null) { - if (DEBUG) Log.d(TAG, "State updated, but token was null") - return - } - val actions = - createActionsFromState( - it.packageName, - mediaControllerFactory.create(it.token), - UserHandle(it.userId) - ) + /** Invoked when media notification is added. */ + fun onNotificationAdded(key: String, sbn: StatusBarNotification) - // Control buttons - // If flag is enabled and controller has a PlaybackState, - // create actions from session info - // otherwise, no need to update semantic actions. - val data = - if (actions != null) { - it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state)) - } else { - it.copy(isPlaying = isPlayingState(state.state)) - } - if (DEBUG) Log.d(TAG, "State updated outside of notification") - onMediaDataLoaded(key, key, data) - } - } + fun destroy() - private fun removeEntry(key: String, logEvent: Boolean = true) { - mediaEntries.remove(key)?.let { - if (logEvent) { - logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId) - } - } - notifyMediaDataRemoved(key) - } + /** Sets resume action. */ + fun setResumeAction(key: String, action: Runnable?) - /** Dismiss a media entry. Returns false if the key was not found. */ - fun dismissMediaData(key: String, delay: Long): Boolean { - val existed = mediaEntries[key] != null - backgroundExecutor.execute { - mediaEntries[key]?.let { mediaData -> - if (mediaData.isLocalSession()) { - mediaData.token?.let { - val mediaController = mediaControllerFactory.create(it) - mediaController.transportControls.stop() - } - } - } - } - foregroundExecutor.executeDelayed({ removeEntry(key) }, delay) - return existed - } - - /** - * Called whenever the recommendation has been expired or removed by the user. This will remove - * the recommendation card entirely from the carousel. - */ - fun dismissSmartspaceRecommendation(key: String, delay: Long) { - if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) { - // If this doesn't match, or we've already invalidated the data, no action needed - return - } - - if (DEBUG) Log.d(TAG, "Dismissing Smartspace media target") - if (smartspaceMediaData.isActive) { - smartspaceMediaData = - EMPTY_SMARTSPACE_MEDIA_DATA.copy( - targetId = smartspaceMediaData.targetId, - instanceId = smartspaceMediaData.instanceId - ) - } - foregroundExecutor.executeDelayed( - { notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = true) }, - delay - ) - } - - /** Called when the recommendation card should no longer be visible in QQS or lockscreen */ - fun setRecommendationInactive(key: String) { - if (!mediaFlags.isPersistentSsCardEnabled()) { - Log.e(TAG, "Only persistent recommendation can be inactive!") - return - } - if (DEBUG) Log.d(TAG, "Setting smartspace recommendation inactive") - - if (smartspaceMediaData.targetId != key || !smartspaceMediaData.isValid()) { - // If this doesn't match, or we've already invalidated the data, no action needed - return - } - - smartspaceMediaData = smartspaceMediaData.copy(isActive = false) - notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData) - } - - private fun loadMediaDataInBgForResumption( + /** Adds resume media data. */ + fun addResumptionControls( userId: Int, desc: MediaDescription, - resumeAction: Runnable, + action: Runnable, token: MediaSession.Token, appName: String, appIntent: PendingIntent, packageName: String - ) { - if (desc.title.isNullOrBlank()) { - Log.e(TAG, "Description incomplete") - // Delete the placeholder entry - mediaEntries.remove(packageName) - return - } - - if (DEBUG) { - Log.d(TAG, "adding track for $userId from browser: $desc") - } - - val currentEntry = mediaEntries.get(packageName) - val appUid = currentEntry?.appUid ?: Process.INVALID_UID - - // Album art - var artworkBitmap = desc.iconBitmap - if (artworkBitmap == null && desc.iconUri != null) { - artworkBitmap = loadBitmapFromUriForUser(desc.iconUri!!, userId, appUid, packageName) - } - val artworkIcon = - if (artworkBitmap != null) { - Icon.createWithBitmap(artworkBitmap) - } else { - null - } - - val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() - val isExplicit = - desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == - MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT - - val progress = - if (mediaFlags.isResumeProgressEnabled()) { - MediaDataUtils.getDescriptionProgress(desc.extras) - } else null - - val mediaAction = getResumeMediaAction(resumeAction) - val lastActive = systemClock.elapsedRealtime() - val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L - foregroundExecutor.execute { - onMediaDataLoaded( - packageName, - null, - MediaData( - userId, - true, - appName, - null, - desc.subtitle, - desc.title, - artworkIcon, - listOf(mediaAction), - listOf(0), - MediaButton(playOrPause = mediaAction), - packageName, - token, - appIntent, - device = null, - active = false, - resumeAction = resumeAction, - resumption = true, - notificationKey = packageName, - hasCheckedForResume = true, - lastActive = lastActive, - createdTimestampMillis = createdTimestampMillis, - instanceId = instanceId, - appUid = appUid, - isExplicit = isExplicit, - resumeProgress = progress, - ) - ) - } - } - - fun loadMediaDataInBg( - key: String, - sbn: StatusBarNotification, - oldKey: String?, - isNewlyActiveEntry: Boolean = false, - ) { - val token = - sbn.notification.extras.getParcelable( - Notification.EXTRA_MEDIA_SESSION, - MediaSession.Token::class.java - ) - if (token == null) { - return - } - val mediaController = mediaControllerFactory.create(token) - val metadata = mediaController.metadata - val notif: Notification = sbn.notification - - val appInfo = - notif.extras.getParcelable( - Notification.EXTRA_BUILDER_APPLICATION_INFO, - ApplicationInfo::class.java - ) - ?: getAppInfoFromPackage(sbn.packageName) - - // App name - val appName = getAppName(sbn, appInfo) - - // Song name - var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) - if (song.isNullOrBlank()) { - song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) - } - if (song.isNullOrBlank()) { - song = HybridGroupManager.resolveTitle(notif) - } - if (song.isNullOrBlank()) { - // For apps that don't include a title, log and add a placeholder - song = context.getString(R.string.controls_media_empty_title, appName) - try { - statusBarManager.logBlankMediaTitle(sbn.packageName, sbn.user.identifier) - } catch (e: RuntimeException) { - Log.e(TAG, "Error reporting blank media title for package ${sbn.packageName}") - } - } - - // Album art - var artworkBitmap = metadata?.let { loadBitmapFromUri(it) } - if (artworkBitmap == null) { - artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART) - } - if (artworkBitmap == null) { - artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) - } - val artWorkIcon = - if (artworkBitmap == null) { - notif.getLargeIcon() - } else { - Icon.createWithBitmap(artworkBitmap) - } - - // App Icon - val smallIcon = sbn.notification.smallIcon - - // Explicit Indicator - var isExplicit = false - val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata) - isExplicit = - mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == - MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT - - // Artist name - var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) - if (artist.isNullOrBlank()) { - artist = HybridGroupManager.resolveText(notif) - } - - // Device name (used for remote cast notifications) - var device: MediaDeviceData? = null - if (isRemoteCastNotification(sbn)) { - val extras = sbn.notification.extras - val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null) - val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1) - val deviceIntent = - extras.getParcelable( - Notification.EXTRA_MEDIA_REMOTE_INTENT, - PendingIntent::class.java - ) - Log.d(TAG, "$key is RCN for $deviceName") - - if (deviceName != null && deviceIcon > -1) { - // Name and icon must be present, but intent may be null - val enabled = deviceIntent != null && deviceIntent.isActivity - val deviceDrawable = - Icon.createWithResource(sbn.packageName, deviceIcon) - .loadDrawable(sbn.getPackageContext(context)) - device = - MediaDeviceData( - enabled, - deviceDrawable, - deviceName, - deviceIntent, - showBroadcastButton = false - ) - } - } - - // Control buttons - // If flag is enabled and controller has a PlaybackState, create actions from session info - // Otherwise, use the notification actions - var actionIcons: List<MediaAction> = emptyList() - var actionsToShowCollapsed: List<Int> = emptyList() - val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user) - if (semanticActions == null) { - val actions = createActionsFromNotification(sbn) - actionIcons = actions.first - actionsToShowCollapsed = actions.second - } - - val playbackLocation = - if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE - else if ( - mediaController.playbackInfo?.playbackType == - MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL - ) - MediaData.PLAYBACK_LOCAL - else MediaData.PLAYBACK_CAST_LOCAL - val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } ?: null - - val currentEntry = mediaEntries.get(key) - val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() - val appUid = appInfo?.uid ?: Process.INVALID_UID - - if (isNewlyActiveEntry) { - logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId) - logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation) - } else if (playbackLocation != currentEntry?.playbackLocation) { - logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation) - } - - val lastActive = systemClock.elapsedRealtime() - val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L - foregroundExecutor.execute { - val resumeAction: Runnable? = mediaEntries[key]?.resumeAction - val hasCheckedForResume = mediaEntries[key]?.hasCheckedForResume == true - val active = mediaEntries[key]?.active ?: true - onMediaDataLoaded( - key, - oldKey, - MediaData( - sbn.normalizedUserId, - true, - appName, - smallIcon, - artist, - song, - artWorkIcon, - actionIcons, - actionsToShowCollapsed, - semanticActions, - sbn.packageName, - token, - notif.contentIntent, - device, - active, - resumeAction = resumeAction, - playbackLocation = playbackLocation, - notificationKey = key, - hasCheckedForResume = hasCheckedForResume, - isPlaying = isPlaying, - isClearable = !sbn.isOngoing, - lastActive = lastActive, - createdTimestampMillis = createdTimestampMillis, - instanceId = instanceId, - appUid = appUid, - isExplicit = isExplicit, - ) - ) - } - } - - private fun logSingleVsMultipleMediaAdded( - appUid: Int, - packageName: String, - instanceId: InstanceId - ) { - if (mediaEntries.size == 1) { - logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId) - } else if (mediaEntries.size == 2) { - // Since this method is only called when there is a new media session added. - // logging needed once there is more than one media session in carousel. - logger.logMultipleMediaPlayersInCarousel(appUid, packageName, instanceId) - } - } - - private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? { - try { - return context.packageManager.getApplicationInfo(packageName, 0) - } catch (e: PackageManager.NameNotFoundException) { - Log.w(TAG, "Could not get app info for $packageName", e) - } - return null - } - - private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String { - val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME) - if (name != null) { - return name - } - - return if (appInfo != null) { - context.packageManager.getApplicationLabel(appInfo).toString() - } else { - sbn.packageName - } - } - - /** Generate action buttons based on notification actions */ - private fun createActionsFromNotification( - sbn: StatusBarNotification - ): Pair<List<MediaAction>, List<Int>> { - val notif = sbn.notification - val actionIcons: MutableList<MediaAction> = ArrayList() - val actions = notif.actions - var actionsToShowCollapsed = - notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() - ?: mutableListOf() - if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) { - Log.e( - TAG, - "Too many compact actions for ${sbn.key}," + - "limiting to first $MAX_COMPACT_ACTIONS" - ) - actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS) - } - - if (actions != null) { - for ((index, action) in actions.withIndex()) { - if (index == MAX_NOTIFICATION_ACTIONS) { - Log.w( - TAG, - "Too many notification actions for ${sbn.key}," + - " limiting to first $MAX_NOTIFICATION_ACTIONS" - ) - break - } - if (action.getIcon() == null) { - if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}") - actionsToShowCollapsed.remove(index) - continue - } - val runnable = - if (action.actionIntent != null) { - Runnable { - if (action.actionIntent.isActivity) { - activityStarter.startPendingIntentDismissingKeyguard( - action.actionIntent - ) - } else if (action.isAuthenticationRequired()) { - activityStarter.dismissKeyguardThenExecute( - { - var result = sendPendingIntent(action.actionIntent) - result - }, - {}, - true - ) - } else { - sendPendingIntent(action.actionIntent) - } - } - } else { - null - } - val mediaActionIcon = - if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) { - Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId()) - } else { - action.getIcon() - } - .setTint(themeText) - .loadDrawable(context) - val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null) - actionIcons.add(mediaAction) - } - } - return Pair(actionIcons, actionsToShowCollapsed) - } - - /** - * Generates action button info for this media session based on the PlaybackState - * - * @param packageName Package name for the media app - * @param controller MediaController for the current session - * @return a Pair consisting of a list of media actions, and a list of ints representing which - * - * ``` - * of those actions should be shown in the compact player - * ``` - */ - private fun createActionsFromState( - packageName: String, - controller: MediaController, - user: UserHandle - ): MediaButton? { - val state = controller.playbackState - if (state == null || !mediaFlags.areMediaSessionActionsEnabled(packageName, user)) { - return null - } - - // First, check for standard actions - val playOrPause = - if (isConnectingState(state.state)) { - // Spinner needs to be animating to render anything. Start it here. - val drawable = - context.getDrawable(com.android.internal.R.drawable.progress_small_material) - (drawable as Animatable).start() - MediaAction( - drawable, - null, // no action to perform when clicked - context.getString(R.string.controls_media_button_connecting), - context.getDrawable(R.drawable.ic_media_connecting_container), - // Specify a rebind id to prevent the spinner from restarting on later binds. - com.android.internal.R.drawable.progress_small_material - ) - } else if (isPlayingState(state.state)) { - getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE) - } else { - getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY) - } - val prevButton = - getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS) - val nextButton = - getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT) - - // Then, create a way to build any custom actions that will be needed - val customActions = - state.customActions - .asSequence() - .filterNotNull() - .map { getCustomAction(state, packageName, controller, it) } - .iterator() - fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null - - // Finally, assign the remaining button slots: play/pause A B C D - // A = previous, else custom action (if not reserved) - // B = next, else custom action (if not reserved) - // C and D are always custom actions - val reservePrev = - controller.extras?.getBoolean( - MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV - ) == true - val reserveNext = - controller.extras?.getBoolean( - MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT - ) == true - - val prevOrCustom = - if (prevButton != null) { - prevButton - } else if (!reservePrev) { - nextCustomAction() - } else { - null - } - - val nextOrCustom = - if (nextButton != null) { - nextButton - } else if (!reserveNext) { - nextCustomAction() - } else { - null - } - - return MediaButton( - playOrPause, - nextOrCustom, - prevOrCustom, - nextCustomAction(), - nextCustomAction(), - reserveNext, - reservePrev - ) - } - - /** - * Create a [MediaAction] for a given action and media session - * - * @param controller MediaController for the session - * @param stateActions The actions included with the session's [PlaybackState] - * @param action A [PlaybackState.Actions] value representing what action to generate. One of: - * ``` - * [PlaybackState.ACTION_PLAY] - * [PlaybackState.ACTION_PAUSE] - * [PlaybackState.ACTION_SKIP_TO_PREVIOUS] - * [PlaybackState.ACTION_SKIP_TO_NEXT] - * @return - * ``` - * - * A [MediaAction] with correct values set, or null if the state doesn't support it - */ - private fun getStandardAction( - controller: MediaController, - stateActions: Long, - @PlaybackState.Actions action: Long - ): MediaAction? { - if (!includesAction(stateActions, action)) { - return null - } - - return when (action) { - PlaybackState.ACTION_PLAY -> { - MediaAction( - context.getDrawable(R.drawable.ic_media_play), - { controller.transportControls.play() }, - context.getString(R.string.controls_media_button_play), - context.getDrawable(R.drawable.ic_media_play_container) - ) - } - PlaybackState.ACTION_PAUSE -> { - MediaAction( - context.getDrawable(R.drawable.ic_media_pause), - { controller.transportControls.pause() }, - context.getString(R.string.controls_media_button_pause), - context.getDrawable(R.drawable.ic_media_pause_container) - ) - } - PlaybackState.ACTION_SKIP_TO_PREVIOUS -> { - MediaAction( - context.getDrawable(R.drawable.ic_media_prev), - { controller.transportControls.skipToPrevious() }, - context.getString(R.string.controls_media_button_prev), - null - ) - } - PlaybackState.ACTION_SKIP_TO_NEXT -> { - MediaAction( - context.getDrawable(R.drawable.ic_media_next), - { controller.transportControls.skipToNext() }, - context.getString(R.string.controls_media_button_next), - null - ) - } - else -> null - } - } - - /** Check whether the actions from a [PlaybackState] include a specific action */ - private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean { - if ( - (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) && - (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L) - ) { - return true - } - return (stateActions and action != 0L) - } - - /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */ - private fun getCustomAction( - state: PlaybackState, - packageName: String, - controller: MediaController, - customAction: PlaybackState.CustomAction - ): MediaAction { - return MediaAction( - Icon.createWithResource(packageName, customAction.icon).loadDrawable(context), - { controller.transportControls.sendCustomAction(customAction, customAction.extras) }, - customAction.name, - null - ) - } - - /** Load a bitmap from the various Art metadata URIs */ - private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? { - for (uri in ART_URIS) { - val uriString = metadata.getString(uri) - if (!TextUtils.isEmpty(uriString)) { - val albumArt = loadBitmapFromUri(Uri.parse(uriString)) - if (albumArt != null) { - if (DEBUG) Log.d(TAG, "loaded art from $uri") - return albumArt - } - } - } - return null - } - - private fun sendPendingIntent(intent: PendingIntent): Boolean { - return try { - val options = BroadcastOptions.makeBasic() - options.setInteractive(true) - options.setPendingIntentBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED - ) - intent.send(options.toBundle()) - true - } catch (e: PendingIntent.CanceledException) { - Log.d(TAG, "Intent canceled", e) - false - } - } - - /** Returns a bitmap if the user can access the given URI, else null */ - private fun loadBitmapFromUriForUser( - uri: Uri, - userId: Int, - appUid: Int, - packageName: String, - ): Bitmap? { - try { - val ugm = UriGrantsManager.getService() - ugm.checkGrantUriPermission_ignoreNonSystem( - appUid, - packageName, - ContentProvider.getUriWithoutUserId(uri), - Intent.FLAG_GRANT_READ_URI_PERMISSION, - ContentProvider.getUserIdFromUri(uri, userId) - ) - return loadBitmapFromUri(uri) - } catch (e: SecurityException) { - Log.e(TAG, "Failed to get URI permission: $e") - } - return null - } - - /** - * Load a bitmap from a URI - * - * @param uri the uri to load - * @return bitmap, or null if couldn't be loaded - */ - private fun loadBitmapFromUri(uri: Uri): Bitmap? { - // ImageDecoder requires a scheme of the following types - if (uri.scheme == null) { - return null - } - - if ( - !uri.scheme.equals(ContentResolver.SCHEME_CONTENT) && - !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) && - !uri.scheme.equals(ContentResolver.SCHEME_FILE) - ) { - return null - } - - val source = ImageDecoder.createSource(context.contentResolver, uri) - return try { - ImageDecoder.decodeBitmap(source) { decoder, info, _ -> - val width = info.size.width - val height = info.size.height - val scale = - MediaDataUtils.getScaleFactor( - APair(width, height), - APair(artworkWidth, artworkHeight) - ) - - // Downscale if needed - if (scale != 0f && scale < 1) { - decoder.setTargetSize((scale * width).toInt(), (scale * height).toInt()) - } - decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE - } - } catch (e: IOException) { - Log.e(TAG, "Unable to load bitmap", e) - null - } catch (e: RuntimeException) { - Log.e(TAG, "Unable to load bitmap", e) - null - } - } - - private fun getResumeMediaAction(action: Runnable): MediaAction { - return MediaAction( - Icon.createWithResource(context, R.drawable.ic_media_play) - .setTint(themeText) - .loadDrawable(context), - action, - context.getString(R.string.controls_media_resume), - context.getDrawable(R.drawable.ic_media_play_container) - ) - } - - fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) = - traceSection("MediaDataManager#onMediaDataLoaded") { - Assert.isMainThread() - if (mediaEntries.containsKey(key)) { - // Otherwise this was removed already - mediaEntries.put(key, data) - notifyMediaDataLoaded(key, oldKey, data) - } - } - - override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) { - if (!allowMediaRecommendations) { - if (DEBUG) Log.d(TAG, "Smartspace recommendation is disabled in Settings.") - return - } - - val mediaTargets = targets.filterIsInstance<SmartspaceTarget>() - when (mediaTargets.size) { - 0 -> { - if (!smartspaceMediaData.isActive) { - return - } - if (DEBUG) { - Log.d(TAG, "Set Smartspace media to be inactive for the data update") - } - if (mediaFlags.isPersistentSsCardEnabled()) { - // Smartspace uses this signal to hide the card (e.g. when it expires or user - // disconnects headphones), so treat as setting inactive when flag is on - smartspaceMediaData = smartspaceMediaData.copy(isActive = false) - notifySmartspaceMediaDataLoaded( - smartspaceMediaData.targetId, - smartspaceMediaData, - ) - } else { - smartspaceMediaData = - EMPTY_SMARTSPACE_MEDIA_DATA.copy( - targetId = smartspaceMediaData.targetId, - instanceId = smartspaceMediaData.instanceId, - ) - notifySmartspaceMediaDataRemoved( - smartspaceMediaData.targetId, - immediately = false, - ) - } - } - 1 -> { - val newMediaTarget = mediaTargets.get(0) - if (smartspaceMediaData.targetId == newMediaTarget.smartspaceTargetId) { - // The same Smartspace updates can be received. Skip the duplicate updates. - return - } - if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.") - smartspaceMediaData = toSmartspaceMediaData(newMediaTarget) - notifySmartspaceMediaDataLoaded(smartspaceMediaData.targetId, smartspaceMediaData) - } - else -> { - // There should NOT be more than 1 Smartspace media update. When it happens, it - // indicates a bad state or an error. Reset the status accordingly. - Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...") - notifySmartspaceMediaDataRemoved( - smartspaceMediaData.targetId, - immediately = false, - ) - smartspaceMediaData = EMPTY_SMARTSPACE_MEDIA_DATA - } - } - } - - fun onNotificationRemoved(key: String) { - Assert.isMainThread() - val removed = mediaEntries.remove(key) ?: return - if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) { - logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) - } else if (isAbleToResume(removed)) { - convertToResumePlayer(key, removed) - } else if (mediaFlags.isRetainingPlayersEnabled()) { - handlePossibleRemoval(key, removed, notificationRemoved = true) - } else { - notifyMediaDataRemoved(key) - logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) - } - } - - private fun onSessionDestroyed(key: String) { - if (DEBUG) Log.d(TAG, "session destroyed for $key") - val entry = mediaEntries.remove(key) ?: return - // Clear token since the session is no longer valid - val updated = entry.copy(token = null) - handlePossibleRemoval(key, updated) - } + ) - private fun isAbleToResume(data: MediaData): Boolean { - val isEligibleForResume = - data.isLocalSession() || - (mediaFlags.isRemoteResumeAllowed() && - data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE) - return useMediaResumption && data.resumeAction != null && isEligibleForResume - } + /** Dismiss a media entry. Returns false if the key was not found. */ + fun dismissMediaData(key: String, delay: Long): Boolean /** - * Convert to resume state if the player is no longer valid and active, then notify listeners - * that the data was updated. Does not convert to resume state if the player is still valid, or - * if it was removed before becoming inactive. (Assumes that [removed] was removed from - * [mediaEntries] before this function was called) + * Called whenever the recommendation has been expired or removed by the user. This will remove + * the recommendation card entirely from the carousel. */ - private fun handlePossibleRemoval( - key: String, - removed: MediaData, - notificationRemoved: Boolean = false - ) { - val hasSession = removed.token != null - if (hasSession && removed.semanticActions != null) { - // The app was using session actions, and the session is still valid: keep player - if (DEBUG) Log.d(TAG, "Notification removed but using session actions $key") - mediaEntries.put(key, removed) - notifyMediaDataLoaded(key, key, removed) - } else if (!notificationRemoved && removed.semanticActions == null) { - // The app was using notification actions, and notif wasn't removed yet: keep player - if (DEBUG) Log.d(TAG, "Session destroyed but using notification actions $key") - mediaEntries.put(key, removed) - notifyMediaDataLoaded(key, key, removed) - } else if (removed.active && !isAbleToResume(removed)) { - // This player was still active - it didn't last long enough to time out, - // and its app doesn't normally support resume: remove - if (DEBUG) Log.d(TAG, "Removing still-active player $key") - notifyMediaDataRemoved(key) - logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) - } else if (mediaFlags.isRetainingPlayersEnabled() || isAbleToResume(removed)) { - // Convert to resume - if (DEBUG) { - Log.d( - TAG, - "Notification ($notificationRemoved) and/or session " + - "($hasSession) gone for inactive player $key" - ) - } - convertToResumePlayer(key, removed) - } else { - // Retaining players flag is off and app doesn't support resume: remove player. - if (DEBUG) Log.d(TAG, "Removing player $key") - notifyMediaDataRemoved(key) - logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) - } - } - - /** Set the given [MediaData] as a resume state player and notify listeners */ - private fun convertToResumePlayer(key: String, data: MediaData) { - if (DEBUG) Log.d(TAG, "Converting $key to resume") - // Resumption controls must have a title. - if (data.song.isNullOrBlank()) { - Log.e(TAG, "Description incomplete") - notifyMediaDataRemoved(key) - logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId) - return - } - // Move to resume key (aka package name) if that key doesn't already exist. - val resumeAction = data.resumeAction?.let { getResumeMediaAction(it) } - val actions = resumeAction?.let { listOf(resumeAction) } ?: emptyList() - val launcherIntent = - context.packageManager.getLaunchIntentForPackage(data.packageName)?.let { - PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE) - } - val lastActive = - if (data.active) { - systemClock.elapsedRealtime() - } else { - data.lastActive - } - val updated = - data.copy( - token = null, - actions = actions, - semanticActions = MediaButton(playOrPause = resumeAction), - actionsToShowInCompact = listOf(0), - active = false, - resumption = true, - isPlaying = false, - isClearable = true, - clickIntent = launcherIntent, - lastActive = lastActive, - ) - val pkg = data.packageName - val migrate = mediaEntries.put(pkg, updated) == null - // Notify listeners of "new" controls when migrating or removed and update when not - Log.d(TAG, "migrating? $migrate from $key -> $pkg") - if (migrate) { - notifyMediaDataLoaded(key = pkg, oldKey = key, info = updated) - } else { - // Since packageName is used for the key of the resumption controls, it is - // possible that another notification has already been reused for the resumption - // controls of this package. In this case, rather than renaming this player as - // packageName, just remove it and then send a update to the existing resumption - // controls. - notifyMediaDataRemoved(key) - notifyMediaDataLoaded(key = pkg, oldKey = pkg, info = updated) - } - logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId) + fun dismissSmartspaceRecommendation(key: String, delay: Long) - // Limit total number of resume controls - val resumeEntries = mediaEntries.filter { (key, data) -> data.resumption } - val numResume = resumeEntries.size - if (numResume > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) { - resumeEntries - .toList() - .sortedBy { (key, data) -> data.lastActive } - .subList(0, numResume - ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) - .forEach { (key, data) -> - Log.d(TAG, "Removing excess control $key") - mediaEntries.remove(key) - notifyMediaDataRemoved(key) - logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId) - } - } - } - - fun setMediaResumptionEnabled(isEnabled: Boolean) { - if (useMediaResumption == isEnabled) { - return - } + /** Called when the recommendation card should no longer be visible in QQS or lockscreen */ + fun setRecommendationInactive(key: String) - useMediaResumption = isEnabled + /** Invoked when notification is removed. */ + fun onNotificationRemoved(key: String) - if (!useMediaResumption) { - // Remove any existing resume controls - val filtered = mediaEntries.filter { !it.value.active } - filtered.forEach { - mediaEntries.remove(it.key) - notifyMediaDataRemoved(it.key) - logger.logMediaRemoved(it.value.appUid, it.value.packageName, it.value.instanceId) - } - } - } + fun setMediaResumptionEnabled(isEnabled: Boolean) /** Invoked when the user has dismissed the media carousel */ - fun onSwipeToDismiss() = mediaDataFilter.onSwipeToDismiss() + fun onSwipeToDismiss() /** Are there any media notifications active, including the recommendations? */ - fun hasActiveMediaOrRecommendation() = mediaDataFilter.hasActiveMediaOrRecommendation() + fun hasActiveMediaOrRecommendation(): Boolean - /** - * Are there any media entries we should display, including the recommendations? - * - If resumption is enabled, this will include inactive players - * - If resumption is disabled, we only want to show active players - */ - fun hasAnyMediaOrRecommendation() = mediaDataFilter.hasAnyMediaOrRecommendation() + /** Are there any media entries we should display, including the recommendations? */ + fun hasAnyMediaOrRecommendation(): Boolean /** Are there any resume media notifications active, excluding the recommendations? */ - fun hasActiveMedia() = mediaDataFilter.hasActiveMedia() + fun hasActiveMedia(): Boolean - /** - * Are there any resume media notifications active, excluding the recommendations? - * - If resumption is enabled, this will include inactive players - * - If resumption is disabled, we only want to show active players - */ - fun hasAnyMedia() = mediaDataFilter.hasAnyMedia() + /** Are there any resume media notifications active, excluding the recommendations? */ + fun hasAnyMedia(): Boolean + + /** Is recommendation card active? */ + fun isRecommendationActive(): Boolean - interface Listener { + // Uses [MediaDataProcessor.Listener] in order to link the new logic code with UI layer. + interface Listener : MediaDataProcessor.Listener { /** * Called whenever there's new MediaData Loaded for the consumption in views. @@ -1637,13 +113,13 @@ class MediaDataManager( * @param isSsReactivated indicates resume media card is reactivated by Smartspace * recommendation signal */ - fun onMediaDataLoaded( + override fun onMediaDataLoaded( key: String, oldKey: String?, data: MediaData, - immediately: Boolean = true, - receivedSmartspaceCardLatency: Int = 0, - isSsReactivated: Boolean = false + immediately: Boolean, + receivedSmartspaceCardLatency: Int, + isSsReactivated: Boolean, ) {} /** @@ -1653,14 +129,14 @@ class MediaDataManager( * it will be prioritized as the first card. Otherwise, it will show up as the last card * as default. */ - fun onSmartspaceMediaDataLoaded( + override fun onSmartspaceMediaDataLoaded( key: String, data: SmartspaceMediaData, - shouldPrioritize: Boolean = false + shouldPrioritize: Boolean, ) {} /** Called whenever a previously existing Media notification was removed. */ - fun onMediaDataRemoved(key: String) {} + override fun onMediaDataRemoved(key: String) {} /** * Called whenever a previously existing Smartspace media data was removed. @@ -1669,78 +145,14 @@ class MediaDataManager( * until the next refresh-round before UI becomes visible. True by default to take in * place immediately. */ - fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) {} + override fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean) {} } - /** - * Converts the pass-in SmartspaceTarget to SmartspaceMediaData - * - * @return An empty SmartspaceMediaData with the valid target Id is returned if the - * SmartspaceTarget's data is invalid. - */ - private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData { - val baseAction: SmartspaceAction? = target.baseAction - val dismissIntent = - baseAction?.extras?.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY) as Intent? - - val isActive = - when { - !mediaFlags.isPersistentSsCardEnabled() -> true - baseAction == null -> true - else -> { - val triggerSource = baseAction.extras?.getString(EXTRA_KEY_TRIGGER_SOURCE) - triggerSource != EXTRA_VALUE_TRIGGER_PERIODIC - } - } - - packageName(target)?.let { - return SmartspaceMediaData( - targetId = target.smartspaceTargetId, - isActive = isActive, - packageName = it, - cardAction = target.baseAction, - recommendations = target.iconGrid, - dismissIntent = dismissIntent, - headphoneConnectionTimeMillis = target.creationTimeMillis, - instanceId = logger.getNewInstanceId(), - expiryTimeMs = target.expiryTimeMillis, - ) - } - return EMPTY_SMARTSPACE_MEDIA_DATA.copy( - targetId = target.smartspaceTargetId, - isActive = isActive, - dismissIntent = dismissIntent, - headphoneConnectionTimeMillis = target.creationTimeMillis, - instanceId = logger.getNewInstanceId(), - expiryTimeMs = target.expiryTimeMillis, - ) - } - - private fun packageName(target: SmartspaceTarget): String? { - val recommendationList = target.iconGrid - if (recommendationList == null || recommendationList.isEmpty()) { - Log.w(TAG, "Empty or null media recommendation list.") - return null - } - for (recommendation in recommendationList) { - val extras = recommendation.extras - extras?.let { - it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { packageName -> - return packageName - } - } - } - Log.w(TAG, "No valid package name is provided.") - return null - } + companion object { - override fun dump(pw: PrintWriter, args: Array<out String>) { - pw.apply { - println("internalListeners: $internalListeners") - println("externalListeners: ${mediaDataFilter.listeners}") - println("mediaEntries: $mediaEntries") - println("useMediaResumption: $useMediaResumption") - println("allowMediaRecommendations: $allowMediaRecommendations") + @JvmStatic + fun isMediaNotification(sbn: StatusBarNotification): Boolean { + return sbn.notification.isMediaNotification() } } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt new file mode 100644 index 000000000000..7412290e8fc5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessor.kt @@ -0,0 +1,1654 @@ +/* + * 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.media.controls.domain.pipeline + +import android.annotation.SuppressLint +import android.app.ActivityOptions +import android.app.BroadcastOptions +import android.app.Notification +import android.app.Notification.EXTRA_SUBSTITUTE_APP_NAME +import android.app.PendingIntent +import android.app.StatusBarManager +import android.app.UriGrantsManager +import android.app.smartspace.SmartspaceAction +import android.app.smartspace.SmartspaceConfig +import android.app.smartspace.SmartspaceManager +import android.app.smartspace.SmartspaceSession +import android.app.smartspace.SmartspaceTarget +import android.content.BroadcastReceiver +import android.content.ContentProvider +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.ImageDecoder +import android.graphics.drawable.Animatable +import android.graphics.drawable.Icon +import android.media.MediaDescription +import android.media.MediaMetadata +import android.media.session.MediaController +import android.media.session.MediaSession +import android.media.session.PlaybackState +import android.net.Uri +import android.os.Handler +import android.os.Parcelable +import android.os.Process +import android.os.UserHandle +import android.provider.Settings +import android.service.notification.StatusBarNotification +import android.support.v4.media.MediaMetadataCompat +import android.text.TextUtils +import android.util.Log +import android.util.Pair as APair +import androidx.media.utils.MediaConstants +import com.android.app.tracing.traceSection +import com.android.internal.annotations.Keep +import com.android.internal.logging.InstanceId +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.CoreStartable +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.dagger.SysUISingleton +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.dump.DumpManager +import com.android.systemui.media.controls.data.repository.MediaDataRepository +import com.android.systemui.media.controls.domain.pipeline.MediaDataManager.Companion.isMediaNotification +import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor +import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser +import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE +import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC +import com.android.systemui.media.controls.shared.model.MediaAction +import com.android.systemui.media.controls.shared.model.MediaButton +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.MediaDeviceData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider +import com.android.systemui.media.controls.ui.view.MediaViewHolder +import com.android.systemui.media.controls.util.MediaControllerFactory +import com.android.systemui.media.controls.util.MediaDataUtils +import com.android.systemui.media.controls.util.MediaFlags +import com.android.systemui.media.controls.util.MediaUiEventLogger +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.BcSmartspaceDataPlugin +import com.android.systemui.res.R +import com.android.systemui.statusbar.NotificationMediaManager.isConnectingState +import com.android.systemui.statusbar.NotificationMediaManager.isPlayingState +import com.android.systemui.statusbar.notification.row.HybridGroupManager +import com.android.systemui.util.Assert +import com.android.systemui.util.Utils +import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.concurrency.ThreadFactory +import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import com.android.systemui.util.time.SystemClock +import java.io.IOException +import java.io.PrintWriter +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +// URI fields to try loading album art from +private val ART_URIS = + arrayOf( + MediaMetadata.METADATA_KEY_ALBUM_ART_URI, + MediaMetadata.METADATA_KEY_ART_URI, + MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI + ) + +private const val TAG = "MediaDataProcessor" +private const val DEBUG = true +private const val EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY = "dismiss_intent" + +/** Processes all media data fields and encapsulates logic for managing media data entries. */ +@SysUISingleton +class MediaDataProcessor( + private val context: Context, + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, + @Background private val backgroundExecutor: Executor, + @Main private val uiExecutor: Executor, + @Main private val foregroundExecutor: DelayableExecutor, + @Main private val handler: Handler, + private val mediaControllerFactory: MediaControllerFactory, + private val broadcastDispatcher: BroadcastDispatcher, + private val dumpManager: DumpManager, + private val activityStarter: ActivityStarter, + private val smartspaceMediaDataProvider: SmartspaceMediaDataProvider, + private var useMediaResumption: Boolean, + private val useQsMediaPlayer: Boolean, + private val systemClock: SystemClock, + private val secureSettings: SecureSettings, + private val mediaFlags: MediaFlags, + private val logger: MediaUiEventLogger, + private val smartspaceManager: SmartspaceManager?, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val mediaDataRepository: MediaDataRepository, +) : CoreStartable, BcSmartspaceDataPlugin.SmartspaceTargetListener { + + companion object { + /** + * UI surface label for subscribing Smartspace updates. String must match with + * [BcSmartspaceDataPlugin.UI_SURFACE_MEDIA] + */ + @JvmField val SMARTSPACE_UI_SURFACE_LABEL = "media_data_manager" + + // Smartspace package name's extra key. + @JvmField val EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name" + + // Maximum number of actions allowed in compact view + @JvmField val MAX_COMPACT_ACTIONS = 3 + + /** + * Maximum number of actions allowed in expanded view. Number must match with the size of + * [MediaViewHolder.genericButtonIds] + */ + @JvmField val MAX_NOTIFICATION_ACTIONS = 5 + } + + private val themeText = + com.android.settingslib.Utils.getColorAttr( + context, + com.android.internal.R.attr.textColorPrimary + ) + .defaultColor + + // Internal listeners are part of the internal pipeline. External listeners (those registered + // with [MediaDeviceManager.addListener]) receive events after they have propagated through + // the internal pipeline. + // Another way to think of the distinction between internal and external listeners is the + // following. Internal listeners are listeners that MediaDataProcessor depends on, and external + // listeners are listeners that depend on MediaDataProcessor. + private val internalListeners: MutableSet<Listener> = mutableSetOf() + + // There should ONLY be at most one Smartspace media recommendation. + @Keep private var smartspaceSession: SmartspaceSession? = null + private var allowMediaRecommendations = false + + private val artworkWidth = + context.resources.getDimensionPixelSize( + com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize + ) + private val artworkHeight = + context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded) + + @SuppressLint("WrongConstant") // sysui allowed to call STATUS_BAR_SERVICE + private val statusBarManager = + context.getSystemService(Context.STATUS_BAR_SERVICE) as StatusBarManager + + /** Check whether this notification is an RCN */ + private fun isRemoteCastNotification(sbn: StatusBarNotification): Boolean { + return sbn.notification.extras.containsKey(Notification.EXTRA_MEDIA_REMOTE_DEVICE) + } + + @Inject + constructor( + context: Context, + @Application applicationScope: CoroutineScope, + @Background backgroundDispatcher: CoroutineDispatcher, + threadFactory: ThreadFactory, + @Main uiExecutor: Executor, + @Main foregroundExecutor: DelayableExecutor, + @Main handler: Handler, + mediaControllerFactory: MediaControllerFactory, + dumpManager: DumpManager, + broadcastDispatcher: BroadcastDispatcher, + activityStarter: ActivityStarter, + smartspaceMediaDataProvider: SmartspaceMediaDataProvider, + clock: SystemClock, + secureSettings: SecureSettings, + mediaFlags: MediaFlags, + logger: MediaUiEventLogger, + smartspaceManager: SmartspaceManager?, + keyguardUpdateMonitor: KeyguardUpdateMonitor, + mediaDataRepository: MediaDataRepository, + ) : this( + context, + applicationScope, + backgroundDispatcher, + // Loading bitmap for UMO background can take longer time, so it cannot run on the default + // background thread. Use a custom thread for media. + threadFactory.buildExecutorOnNewThread(TAG), + uiExecutor, + foregroundExecutor, + handler, + mediaControllerFactory, + broadcastDispatcher, + dumpManager, + activityStarter, + smartspaceMediaDataProvider, + Utils.useMediaResumption(context), + Utils.useQsMediaPlayer(context), + clock, + secureSettings, + mediaFlags, + logger, + smartspaceManager, + keyguardUpdateMonitor, + mediaDataRepository, + ) + + private val appChangeReceiver = + object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + when (intent.action) { + Intent.ACTION_PACKAGES_SUSPENDED -> { + val packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST) + packages?.forEach { removeAllForPackage(it) } + } + Intent.ACTION_PACKAGE_REMOVED, + Intent.ACTION_PACKAGE_RESTARTED -> { + intent.data?.encodedSchemeSpecificPart?.let { removeAllForPackage(it) } + } + } + } + } + + override fun start() { + if (!mediaFlags.isMediaControlsRefactorEnabled()) { + return + } + + dumpManager.registerNormalDumpable(TAG, this) + + val suspendFilter = IntentFilter(Intent.ACTION_PACKAGES_SUSPENDED) + broadcastDispatcher.registerReceiver(appChangeReceiver, suspendFilter, null, UserHandle.ALL) + + val uninstallFilter = + IntentFilter().apply { + addAction(Intent.ACTION_PACKAGE_REMOVED) + addAction(Intent.ACTION_PACKAGE_RESTARTED) + addDataScheme("package") + } + // BroadcastDispatcher does not allow filters with data schemes + context.registerReceiver(appChangeReceiver, uninstallFilter) + + // Register for Smartspace data updates. + smartspaceMediaDataProvider.registerListener(this) + smartspaceSession = + smartspaceManager?.createSmartspaceSession( + SmartspaceConfig.Builder(context, SMARTSPACE_UI_SURFACE_LABEL).build() + ) + smartspaceSession?.let { + it.addOnTargetsAvailableListener( + // Use a main uiExecutor thread listening to Smartspace updates instead of using + // the existing background executor. + // SmartspaceSession has scheduled routine updates which can be unpredictable on + // test simulators, using the backgroundExecutor makes it's hard to test the threads + // numbers. + uiExecutor + ) { targets -> + smartspaceMediaDataProvider.onTargetsAvailable(targets) + } + } + smartspaceSession?.requestSmartspaceUpdate() + + // Track media controls recommendation setting. + applicationScope.launch { trackMediaControlsRecommendationSetting() } + } + + fun destroy() { + smartspaceMediaDataProvider.unregisterListener(this) + smartspaceSession?.close() + smartspaceSession = null + context.unregisterReceiver(appChangeReceiver) + internalListeners.clear() + } + + fun onNotificationAdded(key: String, sbn: StatusBarNotification) { + if (useQsMediaPlayer && isMediaNotification(sbn)) { + var isNewlyActiveEntry = false + Assert.isMainThread() + val oldKey = findExistingEntry(key, sbn.packageName) + if (oldKey == null) { + val instanceId = logger.getNewInstanceId() + val temp = + MediaData() + .copy( + packageName = sbn.packageName, + instanceId = instanceId, + createdTimestampMillis = systemClock.currentTimeMillis(), + ) + mediaDataRepository.addMediaEntry(key, temp) + isNewlyActiveEntry = true + } else if (oldKey != key) { + // Resume -> active conversion; move to new key + val oldData = mediaDataRepository.removeMediaEntry(oldKey)!! + isNewlyActiveEntry = true + mediaDataRepository.addMediaEntry(key, oldData) + } + loadMediaData(key, sbn, oldKey, isNewlyActiveEntry) + } else { + onNotificationRemoved(key) + } + } + + /** + * Allow recommendations from smartspace to show in media controls. Requires + * [Utils.useQsMediaPlayer] to be enabled. On by default, but can be disabled by setting to 0 + */ + private suspend fun allowMediaRecommendations(): Boolean { + return withContext(backgroundDispatcher) { + val flag = + secureSettings.getBoolForUser( + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + true, + UserHandle.USER_CURRENT + ) + + useQsMediaPlayer && flag + } + } + + private suspend fun trackMediaControlsRecommendationSetting() { + secureSettings + .observerFlow(UserHandle.USER_ALL, Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION) + // perform a query at the beginning. + .onStart { emit(Unit) } + .map { allowMediaRecommendations() } + .distinctUntilChanged() + // only track the most recent emission + .collectLatest { + allowMediaRecommendations = it + if (!allowMediaRecommendations) { + dismissSmartspaceRecommendation( + key = mediaDataRepository.smartspaceMediaData.value.targetId, + delay = 0L + ) + } + } + } + + private fun removeAllForPackage(packageName: String) { + Assert.isMainThread() + val toRemove = + mediaDataRepository.mediaEntries.value.filter { it.value.packageName == packageName } + toRemove.forEach { removeEntry(it.key) } + } + + fun setResumeAction(key: String, action: Runnable?) { + mediaDataRepository.mediaEntries.value.get(key)?.let { + it.resumeAction = action + it.hasCheckedForResume = true + } + } + + fun addResumptionControls( + userId: Int, + desc: MediaDescription, + action: Runnable, + token: MediaSession.Token, + appName: String, + appIntent: PendingIntent, + packageName: String + ) { + // Resume controls don't have a notification key, so store by package name instead + if (!mediaDataRepository.mediaEntries.value.containsKey(packageName)) { + val instanceId = logger.getNewInstanceId() + val appUid = + try { + context.packageManager.getApplicationInfo(packageName, 0).uid + } catch (e: PackageManager.NameNotFoundException) { + Log.w(TAG, "Could not get app UID for $packageName", e) + Process.INVALID_UID + } + + val resumeData = + MediaData() + .copy( + packageName = packageName, + resumeAction = action, + hasCheckedForResume = true, + instanceId = instanceId, + appUid = appUid, + createdTimestampMillis = systemClock.currentTimeMillis(), + ) + mediaDataRepository.addMediaEntry(packageName, resumeData) + logSingleVsMultipleMediaAdded(appUid, packageName, instanceId) + logger.logResumeMediaAdded(appUid, packageName, instanceId) + } + backgroundExecutor.execute { + loadMediaDataInBgForResumption( + userId, + desc, + action, + token, + appName, + appIntent, + packageName + ) + } + } + + /** + * Check if there is an existing entry that matches the key or package name. Returns the key + * that matches, or null if not found. + */ + private fun findExistingEntry(key: String, packageName: String): String? { + val mediaEntries = mediaDataRepository.mediaEntries.value + if (mediaEntries.containsKey(key)) { + return key + } + // Check if we already had a resume player + if (mediaEntries.containsKey(packageName)) { + return packageName + } + return null + } + + private fun loadMediaData( + key: String, + sbn: StatusBarNotification, + oldKey: String?, + isNewlyActiveEntry: Boolean = false, + ) { + backgroundExecutor.execute { loadMediaDataInBg(key, sbn, oldKey, isNewlyActiveEntry) } + } + + /** Add a listener for internal events. */ + fun addInternalListener(listener: Listener) = internalListeners.add(listener) + + /** + * Notify internal listeners of media loaded event. + * + * External listeners registered with [MediaCarouselInteractor.addListener] will be notified + * after the event propagates through the internal listener pipeline. + */ + private fun notifyMediaDataLoaded(key: String, oldKey: String?, info: MediaData) { + internalListeners.forEach { it.onMediaDataLoaded(key, oldKey, info) } + } + + /** + * Notify internal listeners of Smartspace media loaded event. + * + * External listeners registered with [MediaCarouselInteractor.addListener] will be notified + * after the event propagates through the internal listener pipeline. + */ + private fun notifySmartspaceMediaDataLoaded(key: String, info: SmartspaceMediaData) { + internalListeners.forEach { it.onSmartspaceMediaDataLoaded(key, info) } + } + + /** + * Notify internal listeners of media removed event. + * + * External listeners registered with [MediaCarouselInteractor.addListener] will be notified + * after the event propagates through the internal listener pipeline. + */ + private fun notifyMediaDataRemoved(key: String) { + internalListeners.forEach { it.onMediaDataRemoved(key) } + } + + /** + * Notify internal listeners of Smartspace media removed event. + * + * External listeners registered with [MediaCarouselInteractor.addListener] will be notified + * after the event propagates through the internal listener pipeline. + * + * @param immediately indicates should apply the UI changes immediately, otherwise wait until + * the next refresh-round before UI becomes visible. Should only be true if the update is + * initiated by user's interaction. + */ + private fun notifySmartspaceMediaDataRemoved(key: String, immediately: Boolean) { + internalListeners.forEach { it.onSmartspaceMediaDataRemoved(key, immediately) } + } + + /** + * Called whenever the player has been paused or stopped for a while, or swiped from QQS. This + * will make the player not active anymore, hiding it from QQS and Keyguard. + * + * @see MediaData.active + */ + fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean = false) { + mediaDataRepository.mediaEntries.value[key]?.let { + if (timedOut && !forceUpdate) { + // Only log this event when media expires on its own + logger.logMediaTimeout(it.appUid, it.packageName, it.instanceId) + } + if (it.active == !timedOut && !forceUpdate) { + if (it.resumption) { + if (DEBUG) Log.d(TAG, "timing out resume player $key") + dismissMediaData(key, 0L /* delay */) + } + return + } + // Update last active if media was still active. + if (it.active) { + it.lastActive = systemClock.elapsedRealtime() + } + it.active = !timedOut + if (DEBUG) Log.d(TAG, "Updating $key timedOut: $timedOut") + onMediaDataLoaded(key, key, it) + } + + if (key == mediaDataRepository.smartspaceMediaData.value.targetId) { + if (DEBUG) Log.d(TAG, "smartspace card expired") + dismissSmartspaceRecommendation(key, delay = 0L) + } + } + + /** Called when the player's [PlaybackState] has been updated with new actions and/or state */ + internal fun updateState(key: String, state: PlaybackState) { + mediaDataRepository.mediaEntries.value.get(key)?.let { + val token = it.token + if (token == null) { + if (DEBUG) Log.d(TAG, "State updated, but token was null") + return + } + val actions = + createActionsFromState( + it.packageName, + mediaControllerFactory.create(it.token), + UserHandle(it.userId) + ) + + // Control buttons + // If flag is enabled and controller has a PlaybackState, + // create actions from session info + // otherwise, no need to update semantic actions. + val data = + if (actions != null) { + it.copy(semanticActions = actions, isPlaying = isPlayingState(state.state)) + } else { + it.copy(isPlaying = isPlayingState(state.state)) + } + if (DEBUG) Log.d(TAG, "State updated outside of notification") + onMediaDataLoaded(key, key, data) + } + } + + private fun removeEntry(key: String, logEvent: Boolean = true) { + mediaDataRepository.removeMediaEntry(key)?.let { + if (logEvent) { + logger.logMediaRemoved(it.appUid, it.packageName, it.instanceId) + } + } + notifyMediaDataRemoved(key) + } + + /** Dismiss a media entry. Returns false if the key was not found. */ + fun dismissMediaData(key: String, delay: Long): Boolean { + val existed = mediaDataRepository.mediaEntries.value[key] != null + backgroundExecutor.execute { + mediaDataRepository.mediaEntries.value[key]?.let { mediaData -> + if (mediaData.isLocalSession()) { + mediaData.token?.let { + val mediaController = mediaControllerFactory.create(it) + mediaController.transportControls.stop() + } + } + } + } + foregroundExecutor.executeDelayed({ removeEntry(key) }, delay) + return existed + } + + /** + * Called whenever the recommendation has been expired or removed by the user. This will remove + * the recommendation card entirely from the carousel. + */ + fun dismissSmartspaceRecommendation(key: String, delay: Long) { + if (mediaDataRepository.dismissSmartspaceRecommendation(key)) { + foregroundExecutor.executeDelayed( + { notifySmartspaceMediaDataRemoved(key, immediately = true) }, + delay + ) + } + } + + /** Called when the recommendation card should no longer be visible in QQS or lockscreen */ + fun setRecommendationInactive(key: String) { + if (mediaDataRepository.setRecommendationInactive(key)) { + val recommendation = mediaDataRepository.smartspaceMediaData.value + notifySmartspaceMediaDataLoaded(recommendation.targetId, recommendation) + } + } + + private fun loadMediaDataInBgForResumption( + userId: Int, + desc: MediaDescription, + resumeAction: Runnable, + token: MediaSession.Token, + appName: String, + appIntent: PendingIntent, + packageName: String + ) { + if (desc.title.isNullOrBlank()) { + Log.e(TAG, "Description incomplete") + // Delete the placeholder entry + mediaDataRepository.removeMediaEntry(packageName) + return + } + + if (DEBUG) { + Log.d(TAG, "adding track for $userId from browser: $desc") + } + + val currentEntry = mediaDataRepository.mediaEntries.value.get(packageName) + val appUid = currentEntry?.appUid ?: Process.INVALID_UID + + // Album art + var artworkBitmap = desc.iconBitmap + if (artworkBitmap == null && desc.iconUri != null) { + artworkBitmap = loadBitmapFromUriForUser(desc.iconUri!!, userId, appUid, packageName) + } + val artworkIcon = + if (artworkBitmap != null) { + Icon.createWithBitmap(artworkBitmap) + } else { + null + } + + val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() + val isExplicit = + desc.extras?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + + val progress = + if (mediaFlags.isResumeProgressEnabled()) { + MediaDataUtils.getDescriptionProgress(desc.extras) + } else null + + val mediaAction = getResumeMediaAction(resumeAction) + val lastActive = systemClock.elapsedRealtime() + val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L + foregroundExecutor.execute { + onMediaDataLoaded( + packageName, + null, + MediaData( + userId, + true, + appName, + null, + desc.subtitle, + desc.title, + artworkIcon, + listOf(mediaAction), + listOf(0), + MediaButton(playOrPause = mediaAction), + packageName, + token, + appIntent, + device = null, + active = false, + resumeAction = resumeAction, + resumption = true, + notificationKey = packageName, + hasCheckedForResume = true, + lastActive = lastActive, + createdTimestampMillis = createdTimestampMillis, + instanceId = instanceId, + appUid = appUid, + isExplicit = isExplicit, + resumeProgress = progress, + ) + ) + } + } + + fun loadMediaDataInBg( + key: String, + sbn: StatusBarNotification, + oldKey: String?, + isNewlyActiveEntry: Boolean = false, + ) { + val token = + sbn.notification.extras.getParcelable( + Notification.EXTRA_MEDIA_SESSION, + MediaSession.Token::class.java + ) + if (token == null) { + return + } + val mediaController = mediaControllerFactory.create(token) + val metadata = mediaController.metadata + val notif: Notification = sbn.notification + + val appInfo = + notif.extras.getParcelable( + Notification.EXTRA_BUILDER_APPLICATION_INFO, + ApplicationInfo::class.java + ) + ?: getAppInfoFromPackage(sbn.packageName) + + // App name + val appName = getAppName(sbn, appInfo) + + // Song name + var song: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE) + if (song.isNullOrBlank()) { + song = metadata?.getString(MediaMetadata.METADATA_KEY_TITLE) + } + if (song.isNullOrBlank()) { + song = HybridGroupManager.resolveTitle(notif) + } + if (song.isNullOrBlank()) { + // For apps that don't include a title, log and add a placeholder + song = context.getString(R.string.controls_media_empty_title, appName) + try { + statusBarManager.logBlankMediaTitle(sbn.packageName, sbn.user.identifier) + } catch (e: RuntimeException) { + Log.e(TAG, "Error reporting blank media title for package ${sbn.packageName}") + } + } + + // Album art + var artworkBitmap = metadata?.let { loadBitmapFromUri(it) } + if (artworkBitmap == null) { + artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ART) + } + if (artworkBitmap == null) { + artworkBitmap = metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) + } + val artWorkIcon = + if (artworkBitmap == null) { + notif.getLargeIcon() + } else { + Icon.createWithBitmap(artworkBitmap) + } + + // App Icon + val smallIcon = sbn.notification.smallIcon + + // Explicit Indicator + val isExplicit: Boolean + val mediaMetadataCompat = MediaMetadataCompat.fromMediaMetadata(metadata) + isExplicit = + mediaMetadataCompat?.getLong(MediaConstants.METADATA_KEY_IS_EXPLICIT) == + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + + // Artist name + var artist: CharSequence? = metadata?.getString(MediaMetadata.METADATA_KEY_ARTIST) + if (artist.isNullOrBlank()) { + artist = HybridGroupManager.resolveText(notif) + } + + // Device name (used for remote cast notifications) + var device: MediaDeviceData? = null + if (isRemoteCastNotification(sbn)) { + val extras = sbn.notification.extras + val deviceName = extras.getCharSequence(Notification.EXTRA_MEDIA_REMOTE_DEVICE, null) + val deviceIcon = extras.getInt(Notification.EXTRA_MEDIA_REMOTE_ICON, -1) + val deviceIntent = + extras.getParcelable( + Notification.EXTRA_MEDIA_REMOTE_INTENT, + PendingIntent::class.java + ) + Log.d(TAG, "$key is RCN for $deviceName") + + if (deviceName != null && deviceIcon > -1) { + // Name and icon must be present, but intent may be null + val enabled = deviceIntent != null && deviceIntent.isActivity + val deviceDrawable = + Icon.createWithResource(sbn.packageName, deviceIcon) + .loadDrawable(sbn.getPackageContext(context)) + device = + MediaDeviceData( + enabled, + deviceDrawable, + deviceName, + deviceIntent, + showBroadcastButton = false + ) + } + } + + // Control buttons + // If flag is enabled and controller has a PlaybackState, create actions from session info + // Otherwise, use the notification actions + var actionIcons: List<MediaAction> = emptyList() + var actionsToShowCollapsed: List<Int> = emptyList() + val semanticActions = createActionsFromState(sbn.packageName, mediaController, sbn.user) + if (semanticActions == null) { + val actions = createActionsFromNotification(sbn) + actionIcons = actions.first + actionsToShowCollapsed = actions.second + } + + val playbackLocation = + if (isRemoteCastNotification(sbn)) MediaData.PLAYBACK_CAST_REMOTE + else if ( + mediaController.playbackInfo?.playbackType == + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL + ) + MediaData.PLAYBACK_LOCAL + else MediaData.PLAYBACK_CAST_LOCAL + val isPlaying = mediaController.playbackState?.let { isPlayingState(it.state) } + + val currentEntry = mediaDataRepository.mediaEntries.value.get(key) + val instanceId = currentEntry?.instanceId ?: logger.getNewInstanceId() + val appUid = appInfo?.uid ?: Process.INVALID_UID + + if (isNewlyActiveEntry) { + logSingleVsMultipleMediaAdded(appUid, sbn.packageName, instanceId) + logger.logActiveMediaAdded(appUid, sbn.packageName, instanceId, playbackLocation) + } else if (playbackLocation != currentEntry?.playbackLocation) { + logger.logPlaybackLocationChange(appUid, sbn.packageName, instanceId, playbackLocation) + } + + val lastActive = systemClock.elapsedRealtime() + val createdTimestampMillis = currentEntry?.createdTimestampMillis ?: 0L + foregroundExecutor.execute { + val resumeAction: Runnable? = mediaDataRepository.mediaEntries.value[key]?.resumeAction + val hasCheckedForResume = + mediaDataRepository.mediaEntries.value[key]?.hasCheckedForResume == true + val active = mediaDataRepository.mediaEntries.value[key]?.active ?: true + onMediaDataLoaded( + key, + oldKey, + MediaData( + sbn.normalizedUserId, + true, + appName, + smallIcon, + artist, + song, + artWorkIcon, + actionIcons, + actionsToShowCollapsed, + semanticActions, + sbn.packageName, + token, + notif.contentIntent, + device, + active, + resumeAction = resumeAction, + playbackLocation = playbackLocation, + notificationKey = key, + hasCheckedForResume = hasCheckedForResume, + isPlaying = isPlaying, + isClearable = !sbn.isOngoing, + lastActive = lastActive, + createdTimestampMillis = createdTimestampMillis, + instanceId = instanceId, + appUid = appUid, + isExplicit = isExplicit, + ) + ) + } + } + + private fun logSingleVsMultipleMediaAdded( + appUid: Int, + packageName: String, + instanceId: InstanceId + ) { + if (mediaDataRepository.mediaEntries.value.size == 1) { + logger.logSingleMediaPlayerInCarousel(appUid, packageName, instanceId) + } else if (mediaDataRepository.mediaEntries.value.size == 2) { + // Since this method is only called when there is a new media session added. + // logging needed once there is more than one media session in carousel. + logger.logMultipleMediaPlayersInCarousel(appUid, packageName, instanceId) + } + } + + private fun getAppInfoFromPackage(packageName: String): ApplicationInfo? { + try { + return context.packageManager.getApplicationInfo(packageName, 0) + } catch (e: PackageManager.NameNotFoundException) { + Log.w(TAG, "Could not get app info for $packageName", e) + } + return null + } + + private fun getAppName(sbn: StatusBarNotification, appInfo: ApplicationInfo?): String { + val name = sbn.notification.extras.getString(EXTRA_SUBSTITUTE_APP_NAME) + if (name != null) { + return name + } + + return if (appInfo != null) { + context.packageManager.getApplicationLabel(appInfo).toString() + } else { + sbn.packageName + } + } + + /** Generate action buttons based on notification actions */ + private fun createActionsFromNotification( + sbn: StatusBarNotification + ): Pair<List<MediaAction>, List<Int>> { + val notif = sbn.notification + val actionIcons: MutableList<MediaAction> = ArrayList() + val actions = notif.actions + var actionsToShowCollapsed = + notif.extras.getIntArray(Notification.EXTRA_COMPACT_ACTIONS)?.toMutableList() + ?: mutableListOf() + if (actionsToShowCollapsed.size > MAX_COMPACT_ACTIONS) { + Log.e( + TAG, + "Too many compact actions for ${sbn.key}," + + "limiting to first $MAX_COMPACT_ACTIONS" + ) + actionsToShowCollapsed = actionsToShowCollapsed.subList(0, MAX_COMPACT_ACTIONS) + } + + if (actions != null) { + for ((index, action) in actions.withIndex()) { + if (index == MAX_NOTIFICATION_ACTIONS) { + Log.w( + TAG, + "Too many notification actions for ${sbn.key}," + + " limiting to first $MAX_NOTIFICATION_ACTIONS" + ) + break + } + if (action.getIcon() == null) { + if (DEBUG) Log.i(TAG, "No icon for action $index ${action.title}") + actionsToShowCollapsed.remove(index) + continue + } + val runnable = + if (action.actionIntent != null) { + Runnable { + if (action.actionIntent.isActivity) { + activityStarter.startPendingIntentDismissingKeyguard( + action.actionIntent + ) + } else if (action.isAuthenticationRequired()) { + activityStarter.dismissKeyguardThenExecute( + { + var result = sendPendingIntent(action.actionIntent) + result + }, + {}, + true + ) + } else { + sendPendingIntent(action.actionIntent) + } + } + } else { + null + } + val mediaActionIcon = + if (action.getIcon()?.getType() == Icon.TYPE_RESOURCE) { + Icon.createWithResource(sbn.packageName, action.getIcon()!!.getResId()) + } else { + action.getIcon() + } + .setTint(themeText) + .loadDrawable(context) + val mediaAction = MediaAction(mediaActionIcon, runnable, action.title, null) + actionIcons.add(mediaAction) + } + } + return Pair(actionIcons, actionsToShowCollapsed) + } + + /** + * Generates action button info for this media session based on the PlaybackState + * + * @param packageName Package name for the media app + * @param controller MediaController for the current session + * @return a Pair consisting of a list of media actions, and a list of ints representing which + * + * ``` + * of those actions should be shown in the compact player + * ``` + */ + private fun createActionsFromState( + packageName: String, + controller: MediaController, + user: UserHandle + ): MediaButton? { + val state = controller.playbackState + if (state == null || !mediaFlags.areMediaSessionActionsEnabled(packageName, user)) { + return null + } + + // First, check for standard actions + val playOrPause = + if (isConnectingState(state.state)) { + // Spinner needs to be animating to render anything. Start it here. + val drawable = + context.getDrawable(com.android.internal.R.drawable.progress_small_material) + (drawable as Animatable).start() + MediaAction( + drawable, + null, // no action to perform when clicked + context.getString(R.string.controls_media_button_connecting), + context.getDrawable(R.drawable.ic_media_connecting_container), + // Specify a rebind id to prevent the spinner from restarting on later binds. + com.android.internal.R.drawable.progress_small_material + ) + } else if (isPlayingState(state.state)) { + getStandardAction(controller, state.actions, PlaybackState.ACTION_PAUSE) + } else { + getStandardAction(controller, state.actions, PlaybackState.ACTION_PLAY) + } + val prevButton = + getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_PREVIOUS) + val nextButton = + getStandardAction(controller, state.actions, PlaybackState.ACTION_SKIP_TO_NEXT) + + // Then, create a way to build any custom actions that will be needed + val customActions = + state.customActions + .asSequence() + .filterNotNull() + .map { getCustomAction(packageName, controller, it) } + .iterator() + fun nextCustomAction() = if (customActions.hasNext()) customActions.next() else null + + // Finally, assign the remaining button slots: play/pause A B C D + // A = previous, else custom action (if not reserved) + // B = next, else custom action (if not reserved) + // C and D are always custom actions + val reservePrev = + controller.extras?.getBoolean( + MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV + ) == true + val reserveNext = + controller.extras?.getBoolean( + MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT + ) == true + + val prevOrCustom = + if (prevButton != null) { + prevButton + } else if (!reservePrev) { + nextCustomAction() + } else { + null + } + + val nextOrCustom = + if (nextButton != null) { + nextButton + } else if (!reserveNext) { + nextCustomAction() + } else { + null + } + + return MediaButton( + playOrPause, + nextOrCustom, + prevOrCustom, + nextCustomAction(), + nextCustomAction(), + reserveNext, + reservePrev + ) + } + + /** + * Create a [MediaAction] for a given action and media session + * + * @param controller MediaController for the session + * @param stateActions The actions included with the session's [PlaybackState] + * @param action A [PlaybackState.Actions] value representing what action to generate. One of: + * ``` + * [PlaybackState.ACTION_PLAY] + * [PlaybackState.ACTION_PAUSE] + * [PlaybackState.ACTION_SKIP_TO_PREVIOUS] + * [PlaybackState.ACTION_SKIP_TO_NEXT] + * @return + * ``` + * + * A [MediaAction] with correct values set, or null if the state doesn't support it + */ + private fun getStandardAction( + controller: MediaController, + stateActions: Long, + @PlaybackState.Actions action: Long + ): MediaAction? { + if (!includesAction(stateActions, action)) { + return null + } + + return when (action) { + PlaybackState.ACTION_PLAY -> { + MediaAction( + context.getDrawable(R.drawable.ic_media_play), + { controller.transportControls.play() }, + context.getString(R.string.controls_media_button_play), + context.getDrawable(R.drawable.ic_media_play_container) + ) + } + PlaybackState.ACTION_PAUSE -> { + MediaAction( + context.getDrawable(R.drawable.ic_media_pause), + { controller.transportControls.pause() }, + context.getString(R.string.controls_media_button_pause), + context.getDrawable(R.drawable.ic_media_pause_container) + ) + } + PlaybackState.ACTION_SKIP_TO_PREVIOUS -> { + MediaAction( + context.getDrawable(R.drawable.ic_media_prev), + { controller.transportControls.skipToPrevious() }, + context.getString(R.string.controls_media_button_prev), + null + ) + } + PlaybackState.ACTION_SKIP_TO_NEXT -> { + MediaAction( + context.getDrawable(R.drawable.ic_media_next), + { controller.transportControls.skipToNext() }, + context.getString(R.string.controls_media_button_next), + null + ) + } + else -> null + } + } + + /** Check whether the actions from a [PlaybackState] include a specific action */ + private fun includesAction(stateActions: Long, @PlaybackState.Actions action: Long): Boolean { + if ( + (action == PlaybackState.ACTION_PLAY || action == PlaybackState.ACTION_PAUSE) && + (stateActions and PlaybackState.ACTION_PLAY_PAUSE > 0L) + ) { + return true + } + return (stateActions and action != 0L) + } + + /** Get a [MediaAction] representing a [PlaybackState.CustomAction] */ + private fun getCustomAction( + packageName: String, + controller: MediaController, + customAction: PlaybackState.CustomAction + ): MediaAction { + return MediaAction( + Icon.createWithResource(packageName, customAction.icon).loadDrawable(context), + { controller.transportControls.sendCustomAction(customAction, customAction.extras) }, + customAction.name, + null + ) + } + + /** Load a bitmap from the various Art metadata URIs */ + private fun loadBitmapFromUri(metadata: MediaMetadata): Bitmap? { + for (uri in ART_URIS) { + val uriString = metadata.getString(uri) + if (!TextUtils.isEmpty(uriString)) { + val albumArt = loadBitmapFromUri(Uri.parse(uriString)) + if (albumArt != null) { + if (DEBUG) Log.d(TAG, "loaded art from $uri") + return albumArt + } + } + } + return null + } + + private fun sendPendingIntent(intent: PendingIntent): Boolean { + return try { + val options = BroadcastOptions.makeBasic() + options.setInteractive(true) + options.setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + ) + intent.send(options.toBundle()) + true + } catch (e: PendingIntent.CanceledException) { + Log.d(TAG, "Intent canceled", e) + false + } + } + + /** Returns a bitmap if the user can access the given URI, else null */ + private fun loadBitmapFromUriForUser( + uri: Uri, + userId: Int, + appUid: Int, + packageName: String, + ): Bitmap? { + try { + val ugm = UriGrantsManager.getService() + ugm.checkGrantUriPermission_ignoreNonSystem( + appUid, + packageName, + ContentProvider.getUriWithoutUserId(uri), + Intent.FLAG_GRANT_READ_URI_PERMISSION, + ContentProvider.getUserIdFromUri(uri, userId) + ) + return loadBitmapFromUri(uri) + } catch (e: SecurityException) { + Log.e(TAG, "Failed to get URI permission: $e") + } + return null + } + + /** + * Load a bitmap from a URI + * + * @param uri the uri to load + * @return bitmap, or null if couldn't be loaded + */ + private fun loadBitmapFromUri(uri: Uri): Bitmap? { + // ImageDecoder requires a scheme of the following types + if (uri.scheme == null) { + return null + } + + if ( + !uri.scheme.equals(ContentResolver.SCHEME_CONTENT) && + !uri.scheme.equals(ContentResolver.SCHEME_ANDROID_RESOURCE) && + !uri.scheme.equals(ContentResolver.SCHEME_FILE) + ) { + return null + } + + val source = ImageDecoder.createSource(context.contentResolver, uri) + return try { + ImageDecoder.decodeBitmap(source) { decoder, info, _ -> + val width = info.size.width + val height = info.size.height + val scale = + MediaDataUtils.getScaleFactor( + APair(width, height), + APair(artworkWidth, artworkHeight) + ) + + // Downscale if needed + if (scale != 0f && scale < 1) { + decoder.setTargetSize((scale * width).toInt(), (scale * height).toInt()) + } + decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE + } + } catch (e: IOException) { + Log.e(TAG, "Unable to load bitmap", e) + null + } catch (e: RuntimeException) { + Log.e(TAG, "Unable to load bitmap", e) + null + } + } + + private fun getResumeMediaAction(action: Runnable): MediaAction { + return MediaAction( + Icon.createWithResource(context, R.drawable.ic_media_play) + .setTint(themeText) + .loadDrawable(context), + action, + context.getString(R.string.controls_media_resume), + context.getDrawable(R.drawable.ic_media_play_container) + ) + } + + fun onMediaDataLoaded(key: String, oldKey: String?, data: MediaData) = + traceSection("MediaDataProcessor#onMediaDataLoaded") { + Assert.isMainThread() + if (mediaDataRepository.mediaEntries.value.containsKey(key)) { + // Otherwise this was removed already + mediaDataRepository.addMediaEntry(key, data) + notifyMediaDataLoaded(key, oldKey, data) + } + } + + override fun onSmartspaceTargetsUpdated(targets: List<Parcelable>) { + if (!allowMediaRecommendations) { + if (DEBUG) Log.d(TAG, "Smartspace recommendation is disabled in Settings.") + return + } + + val mediaTargets = targets.filterIsInstance<SmartspaceTarget>() + val smartspaceMediaData = mediaDataRepository.smartspaceMediaData.value + when (mediaTargets.size) { + 0 -> { + if (!smartspaceMediaData.isActive) { + return + } + if (DEBUG) { + Log.d(TAG, "Set Smartspace media to be inactive for the data update") + } + if (mediaFlags.isPersistentSsCardEnabled()) { + // Smartspace uses this signal to hide the card (e.g. when it expires or user + // disconnects headphones), so treat as setting inactive when flag is on + val recommendation = smartspaceMediaData.copy(isActive = false) + mediaDataRepository.setRecommendation(recommendation) + notifySmartspaceMediaDataLoaded(recommendation.targetId, recommendation) + } else { + notifySmartspaceMediaDataRemoved( + smartspaceMediaData.targetId, + immediately = false + ) + mediaDataRepository.setRecommendation( + SmartspaceMediaData( + targetId = smartspaceMediaData.targetId, + instanceId = smartspaceMediaData.instanceId, + ) + ) + } + } + 1 -> { + val newMediaTarget = mediaTargets.get(0) + if (smartspaceMediaData.targetId == newMediaTarget.smartspaceTargetId) { + // The same Smartspace updates can be received. Skip the duplicate updates. + return + } + if (DEBUG) Log.d(TAG, "Forwarding Smartspace media update.") + val recommendation = toSmartspaceMediaData(newMediaTarget) + mediaDataRepository.setRecommendation(recommendation) + notifySmartspaceMediaDataLoaded(recommendation.targetId, recommendation) + } + else -> { + // There should NOT be more than 1 Smartspace media update. When it happens, it + // indicates a bad state or an error. Reset the status accordingly. + Log.wtf(TAG, "More than 1 Smartspace Media Update. Resetting the status...") + notifySmartspaceMediaDataRemoved(smartspaceMediaData.targetId, immediately = false) + mediaDataRepository.setRecommendation(SmartspaceMediaData()) + } + } + } + + fun onNotificationRemoved(key: String) { + Assert.isMainThread() + val removed = mediaDataRepository.removeMediaEntry(key) ?: return + if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) { + logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) + } else if (isAbleToResume(removed)) { + convertToResumePlayer(key, removed) + } else if (mediaFlags.isRetainingPlayersEnabled()) { + handlePossibleRemoval(key, removed, notificationRemoved = true) + } else { + notifyMediaDataRemoved(key) + logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) + } + } + + internal fun onSessionDestroyed(key: String) { + if (DEBUG) Log.d(TAG, "session destroyed for $key") + val entry = mediaDataRepository.removeMediaEntry(key) ?: return + // Clear token since the session is no longer valid + val updated = entry.copy(token = null) + handlePossibleRemoval(key, updated) + } + + private fun isAbleToResume(data: MediaData): Boolean { + val isEligibleForResume = + data.isLocalSession() || + (mediaFlags.isRemoteResumeAllowed() && + data.playbackLocation != MediaData.PLAYBACK_CAST_REMOTE) + return useMediaResumption && data.resumeAction != null && isEligibleForResume + } + + /** + * Convert to resume state if the player is no longer valid and active, then notify listeners + * that the data was updated. Does not convert to resume state if the player is still valid, or + * if it was removed before becoming inactive. (Assumes that [removed] was removed from + * [mediaDataRepository.mediaEntries] state before this function was called) + */ + private fun handlePossibleRemoval( + key: String, + removed: MediaData, + notificationRemoved: Boolean = false + ) { + val hasSession = removed.token != null + if (hasSession && removed.semanticActions != null) { + // The app was using session actions, and the session is still valid: keep player + if (DEBUG) Log.d(TAG, "Notification removed but using session actions $key") + mediaDataRepository.addMediaEntry(key, removed) + notifyMediaDataLoaded(key, key, removed) + } else if (!notificationRemoved && removed.semanticActions == null) { + // The app was using notification actions, and notif wasn't removed yet: keep player + if (DEBUG) Log.d(TAG, "Session destroyed but using notification actions $key") + mediaDataRepository.addMediaEntry(key, removed) + notifyMediaDataLoaded(key, key, removed) + } else if (removed.active && !isAbleToResume(removed)) { + // This player was still active - it didn't last long enough to time out, + // and its app doesn't normally support resume: remove + if (DEBUG) Log.d(TAG, "Removing still-active player $key") + notifyMediaDataRemoved(key) + logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) + } else if (mediaFlags.isRetainingPlayersEnabled() || isAbleToResume(removed)) { + // Convert to resume + if (DEBUG) { + Log.d( + TAG, + "Notification ($notificationRemoved) and/or session " + + "($hasSession) gone for inactive player $key" + ) + } + convertToResumePlayer(key, removed) + } else { + // Retaining players flag is off and app doesn't support resume: remove player. + if (DEBUG) Log.d(TAG, "Removing player $key") + notifyMediaDataRemoved(key) + logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId) + } + } + + /** Set the given [MediaData] as a resume state player and notify listeners */ + private fun convertToResumePlayer(key: String, data: MediaData) { + if (DEBUG) Log.d(TAG, "Converting $key to resume") + // Resumption controls must have a title. + if (data.song.isNullOrBlank()) { + Log.e(TAG, "Description incomplete") + notifyMediaDataRemoved(key) + logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId) + return + } + // Move to resume key (aka package name) if that key doesn't already exist. + val resumeAction = data.resumeAction?.let { getResumeMediaAction(it) } + val actions = resumeAction?.let { listOf(resumeAction) } ?: emptyList() + val launcherIntent = + context.packageManager.getLaunchIntentForPackage(data.packageName)?.let { + PendingIntent.getActivity(context, 0, it, PendingIntent.FLAG_IMMUTABLE) + } + val lastActive = + if (data.active) { + systemClock.elapsedRealtime() + } else { + data.lastActive + } + val updated = + data.copy( + token = null, + actions = actions, + semanticActions = MediaButton(playOrPause = resumeAction), + actionsToShowInCompact = listOf(0), + active = false, + resumption = true, + isPlaying = false, + isClearable = true, + clickIntent = launcherIntent, + lastActive = lastActive, + ) + val pkg = data.packageName + val migrate = mediaDataRepository.addMediaEntry(pkg, updated) == null + // Notify listeners of "new" controls when migrating or removed and update when not + Log.d(TAG, "migrating? $migrate from $key -> $pkg") + if (migrate) { + notifyMediaDataLoaded(key = pkg, oldKey = key, info = updated) + } else { + // Since packageName is used for the key of the resumption controls, it is + // possible that another notification has already been reused for the resumption + // controls of this package. In this case, rather than renaming this player as + // packageName, just remove it and then send a update to the existing resumption + // controls. + notifyMediaDataRemoved(key) + notifyMediaDataLoaded(key = pkg, oldKey = pkg, info = updated) + } + logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId) + + // Limit total number of resume controls + val resumeEntries = + mediaDataRepository.mediaEntries.value.filter { (_, data) -> data.resumption } + val numResume = resumeEntries.size + if (numResume > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) { + resumeEntries + .toList() + .sortedBy { (_, data) -> data.lastActive } + .subList(0, numResume - ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) + .forEach { (key, data) -> + Log.d(TAG, "Removing excess control $key") + mediaDataRepository.removeMediaEntry(key) + notifyMediaDataRemoved(key) + logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId) + } + } + } + + fun setMediaResumptionEnabled(isEnabled: Boolean) { + if (useMediaResumption == isEnabled) { + return + } + + useMediaResumption = isEnabled + + if (!useMediaResumption) { + // Remove any existing resume controls + val filtered = mediaDataRepository.mediaEntries.value.filter { !it.value.active } + filtered.forEach { + mediaDataRepository.removeMediaEntry(it.key) + notifyMediaDataRemoved(it.key) + logger.logMediaRemoved(it.value.appUid, it.value.packageName, it.value.instanceId) + } + } + } + + /** Listener to data changes. */ + interface Listener { + + /** + * Called whenever there's new MediaData Loaded for the consumption in views. + * + * oldKey is provided to check whether the view has changed keys, which can happen when a + * player has gone from resume state (key is package name) to active state (key is + * notification key) or vice versa. + * + * @param immediately indicates should apply the UI changes immediately, otherwise wait + * until the next refresh-round before UI becomes visible. True by default to take in + * place immediately. + * @param receivedSmartspaceCardLatency is the latency between headphone connects and sysUI + * displays Smartspace media targets. Will be 0 if the data is not activated by Smartspace + * signal. + * @param isSsReactivated indicates resume media card is reactivated by Smartspace + * recommendation signal + */ + fun onMediaDataLoaded( + key: String, + oldKey: String?, + data: MediaData, + immediately: Boolean = true, + receivedSmartspaceCardLatency: Int = 0, + isSsReactivated: Boolean = false + ) {} + + /** + * Called whenever there's new Smartspace media data loaded. + * + * @param shouldPrioritize indicates the sorting priority of the Smartspace card. If true, + * it will be prioritized as the first card. Otherwise, it will show up as the last card + * as default. + */ + fun onSmartspaceMediaDataLoaded( + key: String, + data: SmartspaceMediaData, + shouldPrioritize: Boolean = false + ) {} + + /** Called whenever a previously existing Media notification was removed. */ + fun onMediaDataRemoved(key: String) {} + + /** + * Called whenever a previously existing Smartspace media data was removed. + * + * @param immediately indicates should apply the UI changes immediately, otherwise wait + * until the next refresh-round before UI becomes visible. True by default to take in + * place immediately. + */ + fun onSmartspaceMediaDataRemoved(key: String, immediately: Boolean = true) {} + } + + /** + * Converts the pass-in SmartspaceTarget to SmartspaceMediaData + * + * @return An empty SmartspaceMediaData with the valid target Id is returned if the + * SmartspaceTarget's data is invalid. + */ + private fun toSmartspaceMediaData(target: SmartspaceTarget): SmartspaceMediaData { + val baseAction: SmartspaceAction? = target.baseAction + val dismissIntent = + baseAction + ?.extras + ?.getParcelable(EXTRAS_SMARTSPACE_DISMISS_INTENT_KEY, Intent::class.java) + + val isActive = + when { + !mediaFlags.isPersistentSsCardEnabled() -> true + baseAction == null -> true + else -> { + val triggerSource = baseAction.extras?.getString(EXTRA_KEY_TRIGGER_SOURCE) + triggerSource != EXTRA_VALUE_TRIGGER_PERIODIC + } + } + + packageName(target)?.let { + return SmartspaceMediaData( + targetId = target.smartspaceTargetId, + isActive = isActive, + packageName = it, + cardAction = target.baseAction, + recommendations = target.iconGrid, + dismissIntent = dismissIntent, + headphoneConnectionTimeMillis = target.creationTimeMillis, + instanceId = logger.getNewInstanceId(), + expiryTimeMs = target.expiryTimeMillis, + ) + } + return SmartspaceMediaData( + targetId = target.smartspaceTargetId, + isActive = isActive, + dismissIntent = dismissIntent, + headphoneConnectionTimeMillis = target.creationTimeMillis, + instanceId = logger.getNewInstanceId(), + expiryTimeMs = target.expiryTimeMillis, + ) + } + + private fun packageName(target: SmartspaceTarget): String? { + val recommendationList: MutableList<SmartspaceAction> = target.iconGrid + if (recommendationList.isEmpty()) { + Log.w(TAG, "Empty or null media recommendation list.") + return null + } + for (recommendation in recommendationList) { + val extras = recommendation.extras + extras?.let { + it.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME)?.let { packageName -> + return packageName + } + } + } + Log.w(TAG, "No valid package name is provided.") + return null + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.apply { + println("internalListeners: $internalListeners") + println("useMediaResumption: $useMediaResumption") + println("allowMediaRecommendations: $allowMediaRecommendations") + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt index f4d70a5e78c9..c7cfb0b7d775 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManager.kt @@ -35,10 +35,8 @@ import com.android.settingslib.flags.Flags.legacyLeAudioSharing import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice import com.android.settingslib.media.PhoneMediaDevice -import com.android.systemui.Dumpable import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.dump.DumpManager import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.MediaDeviceData import com.android.systemui.media.controls.util.LocalMediaManagerFactory @@ -70,16 +68,11 @@ constructor( private val localBluetoothManager: Lazy<LocalBluetoothManager?>, @Main private val fgExecutor: Executor, @Background private val bgExecutor: Executor, - dumpManager: DumpManager, -) : MediaDataManager.Listener, Dumpable { +) : MediaDataManager.Listener { private val listeners: MutableSet<Listener> = mutableSetOf() private val entries: MutableMap<String, Entry> = mutableMapOf() - init { - dumpManager.registerDumpable(this) - } - /** Add a listener for changes to the media route (ie. device). */ fun addListener(listener: Listener) = listeners.add(listener) @@ -123,7 +116,7 @@ constructor( token?.let { listeners.forEach { it.onKeyRemoved(key) } } } - override fun dump(pw: PrintWriter, args: Array<String>) { + fun dump(pw: PrintWriter) { with(pw) { println("MediaDeviceManager state:") entries.forEach { (key, entry) -> diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt new file mode 100644 index 000000000000..4a92b71f1155 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractor.kt @@ -0,0 +1,234 @@ +/* + * 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.media.controls.domain.pipeline.interactor + +import android.app.PendingIntent +import android.media.MediaDescription +import android.media.session.MediaSession +import android.media.session.PlaybackState +import android.service.notification.StatusBarNotification +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.media.controls.data.repository.MediaDataRepository +import com.android.systemui.media.controls.data.repository.MediaFilterRepository +import com.android.systemui.media.controls.domain.pipeline.MediaDataCombineLatest +import com.android.systemui.media.controls.domain.pipeline.MediaDataFilterImpl +import com.android.systemui.media.controls.domain.pipeline.MediaDataManager +import com.android.systemui.media.controls.domain.pipeline.MediaDataProcessor +import com.android.systemui.media.controls.domain.pipeline.MediaDeviceManager +import com.android.systemui.media.controls.domain.pipeline.MediaSessionBasedFilter +import com.android.systemui.media.controls.domain.pipeline.MediaTimeoutListener +import com.android.systemui.media.controls.domain.resume.MediaResumeListener +import com.android.systemui.media.controls.util.MediaFlags +import java.io.PrintWriter +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.stateIn + +/** Encapsulates business logic for media pipeline. */ +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class MediaCarouselInteractor +@Inject +constructor( + @Application applicationScope: CoroutineScope, + private val mediaDataRepository: MediaDataRepository, + private val mediaDataProcessor: MediaDataProcessor, + private val mediaTimeoutListener: MediaTimeoutListener, + private val mediaResumeListener: MediaResumeListener, + private val mediaSessionBasedFilter: MediaSessionBasedFilter, + private val mediaDeviceManager: MediaDeviceManager, + private val mediaDataCombineLatest: MediaDataCombineLatest, + private val mediaDataFilter: MediaDataFilterImpl, + mediaFilterRepository: MediaFilterRepository, + private val mediaFlags: MediaFlags, +) : MediaDataManager, CoreStartable { + + /** Are there any media notifications active, including the recommendations? */ + val hasActiveMediaOrRecommendation: StateFlow<Boolean> = + combine( + mediaFilterRepository.selectedUserEntries, + mediaFilterRepository.smartspaceMediaData, + mediaFilterRepository.reactivatedKey + ) { entries, smartspaceMediaData, reactivatedKey -> + entries.any { it.value.active } || + (smartspaceMediaData.isActive && + (smartspaceMediaData.isValid() || reactivatedKey != null)) + } + .distinctUntilChanged() + .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false) + + /** Are there any media entries we should display, including the recommendations? */ + val hasAnyMediaOrRecommendation: StateFlow<Boolean> = + combine( + mediaFilterRepository.selectedUserEntries, + mediaFilterRepository.smartspaceMediaData + ) { entries, smartspaceMediaData -> + entries.isNotEmpty() || + (if (mediaFlags.isPersistentSsCardEnabled()) { + smartspaceMediaData.isValid() + } else { + smartspaceMediaData.isActive && smartspaceMediaData.isValid() + }) + } + .distinctUntilChanged() + .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false) + + /** Are there any media notifications active, excluding the recommendations? */ + val hasActiveMedia: StateFlow<Boolean> = + mediaFilterRepository.selectedUserEntries + .mapLatest { entries -> entries.any { it.value.active } } + .distinctUntilChanged() + .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false) + + /** Are there any media notifications, excluding the recommendations? */ + val hasAnyMedia: StateFlow<Boolean> = + mediaFilterRepository.selectedUserEntries + .mapLatest { entries -> entries.isNotEmpty() } + .distinctUntilChanged() + .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false) + + override fun start() { + if (!mediaFlags.isMediaControlsRefactorEnabled()) { + return + } + + // Initialize the internal processing pipeline. The listeners at the front of the pipeline + // are set as internal listeners so that they receive events. From there, events are + // propagated through the pipeline. The end of the pipeline is currently mediaDataFilter, + // so it is responsible for dispatching events to external listeners. To achieve this, + // external listeners that are registered with [MediaDataManager.addListener] are actually + // registered as listeners to mediaDataFilter. + addInternalListener(mediaTimeoutListener) + addInternalListener(mediaResumeListener) + addInternalListener(mediaSessionBasedFilter) + mediaSessionBasedFilter.addListener(mediaDeviceManager) + mediaSessionBasedFilter.addListener(mediaDataCombineLatest) + mediaDeviceManager.addListener(mediaDataCombineLatest) + mediaDataCombineLatest.addListener(mediaDataFilter) + + // Set up links back into the pipeline for listeners that need to send events upstream. + mediaTimeoutListener.timeoutCallback = { key: String, timedOut: Boolean -> + setInactive(key, timedOut) + } + mediaTimeoutListener.stateCallback = { key: String, state: PlaybackState -> + mediaDataProcessor.updateState(key, state) + } + mediaTimeoutListener.sessionCallback = { key: String -> + mediaDataProcessor.onSessionDestroyed(key) + } + mediaResumeListener.setManager(this) + mediaDataFilter.mediaDataManager = this + } + + override fun addListener(listener: MediaDataManager.Listener) { + mediaDataFilter.addListener(listener) + } + + override fun removeListener(listener: MediaDataManager.Listener) { + mediaDataFilter.removeListener(listener) + } + + override fun setInactive(key: String, timedOut: Boolean, forceUpdate: Boolean) { + mediaDataProcessor.setInactive(key, timedOut, forceUpdate) + } + + override fun onNotificationAdded(key: String, sbn: StatusBarNotification) { + mediaDataProcessor.onNotificationAdded(key, sbn) + } + + override fun destroy() { + mediaSessionBasedFilter.removeListener(mediaDeviceManager) + mediaSessionBasedFilter.removeListener(mediaDataCombineLatest) + mediaDeviceManager.removeListener(mediaDataCombineLatest) + mediaDataCombineLatest.removeListener(mediaDataFilter) + mediaDataProcessor.destroy() + } + + override fun setResumeAction(key: String, action: Runnable?) { + mediaDataProcessor.setResumeAction(key, action) + } + + override fun addResumptionControls( + userId: Int, + desc: MediaDescription, + action: Runnable, + token: MediaSession.Token, + appName: String, + appIntent: PendingIntent, + packageName: String + ) { + mediaDataProcessor.addResumptionControls( + userId, + desc, + action, + token, + appName, + appIntent, + packageName + ) + } + + override fun dismissMediaData(key: String, delay: Long): Boolean { + return mediaDataProcessor.dismissMediaData(key, delay) + } + + override fun dismissSmartspaceRecommendation(key: String, delay: Long) { + return mediaDataProcessor.dismissSmartspaceRecommendation(key, delay) + } + + override fun setRecommendationInactive(key: String) { + mediaDataProcessor.setRecommendationInactive(key) + } + + override fun onNotificationRemoved(key: String) { + mediaDataProcessor.onNotificationRemoved(key) + } + + override fun setMediaResumptionEnabled(isEnabled: Boolean) { + mediaDataProcessor.setMediaResumptionEnabled(isEnabled) + } + + override fun onSwipeToDismiss() { + mediaDataFilter.onSwipeToDismiss() + } + + override fun hasActiveMediaOrRecommendation() = hasActiveMediaOrRecommendation.value + + override fun hasAnyMediaOrRecommendation() = hasAnyMediaOrRecommendation.value + + override fun hasActiveMedia() = hasActiveMedia.value + + override fun hasAnyMedia() = hasAnyMedia.value + + override fun isRecommendationActive() = mediaDataRepository.smartspaceMediaData.value.isActive + + /** Add a listener for internal events. */ + private fun addInternalListener(listener: MediaDataManager.Listener) = + mediaDataProcessor.addInternalListener(listener) + + override fun dump(pw: PrintWriter, args: Array<out String>) { + mediaDeviceManager.dump(pw) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt index 4fa7cb54431f..11a562911a85 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/MediaData.kt @@ -20,48 +20,49 @@ import android.app.PendingIntent import android.graphics.drawable.Drawable import android.graphics.drawable.Icon import android.media.session.MediaSession +import android.os.Process import com.android.internal.logging.InstanceId import com.android.systemui.res.R /** State of a media view. */ data class MediaData( - val userId: Int, + val userId: Int = -1, val initialized: Boolean = false, /** App name that will be displayed on the player. */ - val app: String?, + val app: String? = null, /** App icon shown on player. */ - val appIcon: Icon?, + val appIcon: Icon? = null, /** Artist name. */ - val artist: CharSequence?, + val artist: CharSequence? = null, /** Song name. */ - val song: CharSequence?, + val song: CharSequence? = null, /** Album artwork. */ - val artwork: Icon?, + val artwork: Icon? = null, /** List of generic action buttons for the media player, based on notification actions */ - val actions: List<MediaAction>, + val actions: List<MediaAction> = emptyList(), /** Same as above, but shown on smaller versions of the player, like in QQS or keyguard. */ - val actionsToShowInCompact: List<Int>, + val actionsToShowInCompact: List<Int> = emptyList(), /** * Semantic actions buttons, based on the PlaybackState of the media session. If present, these * actions will be preferred in the UI over [actions] */ val semanticActions: MediaButton? = null, /** Package name of the app that's posting the media. */ - val packageName: String, + val packageName: String = "INVALID", /** Unique media session identifier. */ - val token: MediaSession.Token?, + val token: MediaSession.Token? = null, /** Action to perform when the player is tapped. This is unrelated to {@link #actions}. */ - val clickIntent: PendingIntent?, + val clickIntent: PendingIntent? = null, /** Where the media is playing: phone, headphones, ear buds, remote session. */ - val device: MediaDeviceData?, + val device: MediaDeviceData? = null, /** * When active, a player will be displayed on keyguard and quick-quick settings. This is * unrelated to the stream being playing or not, a player will not be active if timed out, or in * resumption mode. */ - var active: Boolean, + var active: Boolean = true, /** Action that should be performed to restart a non active session. */ - var resumeAction: Runnable?, + var resumeAction: Runnable? = null, /** Playback location: one of PLAYBACK_LOCAL, PLAYBACK_CAST_LOCAL, or PLAYBACK_CAST_REMOTE */ var playbackLocation: Int = PLAYBACK_LOCAL, /** @@ -88,10 +89,10 @@ data class MediaData( var createdTimestampMillis: Long = 0L, /** Instance ID for logging purposes */ - val instanceId: InstanceId, + val instanceId: InstanceId = InstanceId.fakeInstanceId(-1), /** The UID of the app, used for logging */ - val appUid: Int, + val appUid: Int = Process.INVALID_UID, /** Whether explicit indicator exists */ val isExplicit: Boolean = false, diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaData.kt b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaData.kt index 52c605f55665..b44658502f48 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaData.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/shared/model/SmartspaceMediaData.kt @@ -30,23 +30,23 @@ import com.android.internal.logging.InstanceId /** State of a Smartspace media recommendations view. */ data class SmartspaceMediaData( /** Unique id of a Smartspace media target. */ - val targetId: String, + val targetId: String = "INVALID", /** Indicates if the status is active. */ - val isActive: Boolean, + val isActive: Boolean = false, /** Package name of the media recommendations' provider-app. */ - val packageName: String, + val packageName: String = "INVALID", /** Action to perform when the card is tapped. Also contains the target's extra info. */ - val cardAction: SmartspaceAction?, + val cardAction: SmartspaceAction? = null, /** List of media recommendations. */ - val recommendations: List<SmartspaceAction>, + val recommendations: List<SmartspaceAction> = emptyList(), /** Intent for the user's initiated dismissal. */ - val dismissIntent: Intent?, + val dismissIntent: Intent? = null, /** The timestamp in milliseconds that the card was generated */ - val headphoneConnectionTimeMillis: Long, + val headphoneConnectionTimeMillis: Long = 0L, /** Instance ID for [MediaUiEventLogger] */ - val instanceId: InstanceId, + val instanceId: InstanceId? = null, /** The timestamp in milliseconds indicating when the card should be removed */ - val expiryTimeMs: Long, + val expiryTimeMs: Long = 0L, ) { /** * Indicates if all the data is valid. diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt index ba7d41008a01..89a9ba7b61a3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt @@ -27,10 +27,10 @@ import android.view.View import android.view.ViewGroup import androidx.annotation.VisibleForTesting import com.android.systemui.Dumpable -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.media.controls.ui.view.MediaHost import com.android.systemui.media.controls.ui.view.MediaHostState import com.android.systemui.media.dagger.MediaModule.KEYGUARD @@ -185,7 +185,7 @@ constructor( refreshMediaPosition(reason = "onMediaHostVisibilityChanged") if (visible) { - if (migrateClocksToBlueprint() && useSplitShade) { + if (MigrateClocksToBlueprint.isEnabled && useSplitShade) { return } mediaHost.hostView.layoutParams.apply { diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt index b721236eab01..655e6a55fb95 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/MediaCarouselController.kt @@ -1163,7 +1163,7 @@ constructor( // Only log media resume card when Smartspace data is available if ( !mediaControlKey.isSsMediaRec && - !mediaManager.smartspaceMediaData.isActive && + !mediaManager.isRecommendationActive() && MediaPlayerData.smartspaceMediaData == null ) { return diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt index f8c816ca0b52..2c25fe2ecb29 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaUiEventLogger.kt @@ -161,7 +161,7 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) logger.log(event) } - fun logRecommendationAdded(packageName: String, instanceId: InstanceId) { + fun logRecommendationAdded(packageName: String, instanceId: InstanceId?) { logger.logWithInstanceId( MediaUiEvent.MEDIA_RECOMMENDATION_ADDED, 0, @@ -170,7 +170,7 @@ class MediaUiEventLogger @Inject constructor(private val logger: UiEventLogger) ) } - fun logRecommendationRemoved(packageName: String, instanceId: InstanceId) { + fun logRecommendationRemoved(packageName: String, instanceId: InstanceId?) { logger.logWithInstanceId( MediaUiEvent.MEDIA_RECOMMENDATION_REMOVED, 0, diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java index d84e5dde6967..0fa3605ecd6d 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java +++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaModule.java @@ -19,6 +19,7 @@ package com.android.systemui.media.dagger; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.log.LogBuffer; import com.android.systemui.log.LogBufferFactory; +import com.android.systemui.media.controls.domain.MediaDomainModule; import com.android.systemui.media.controls.domain.pipeline.MediaDataManager; import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager; import com.android.systemui.media.controls.ui.controller.MediaHostStatesManager; @@ -38,7 +39,11 @@ import java.util.Optional; import javax.inject.Named; /** Dagger module for the media package. */ -@Module(subcomponents = { +@Module( + includes = { + MediaDomainModule.class + }, + subcomponents = { MediaComplicationComponent.class, }) public interface MediaModule { diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java index dfe41eb9f7f2..d49a513f6e9f 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java @@ -243,7 +243,7 @@ public final class NavBarHelper implements Settings.Secure.getUriFor(Settings.Secure.ASSIST_LONG_PRESS_HOME_ENABLED), false, mAssistContentObserver, UserHandle.USER_ALL); mContentResolver.registerContentObserver( - Settings.Secure.getUriFor(Secure.SEARCH_LONG_PRESS_HOME_ENABLED), + Settings.Secure.getUriFor(Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED), false, mAssistContentObserver, UserHandle.USER_ALL); mContentResolver.registerContentObserver( Settings.Secure.getUriFor(Settings.Secure.ASSIST_TOUCH_GESTURE_ENABLED), @@ -443,10 +443,10 @@ public final class NavBarHelper implements boolean overrideLongPressHome = mAssistManagerLazy.get() .shouldOverrideAssist(AssistManager.INVOCATION_TYPE_HOME_BUTTON_LONG_PRESS); boolean longPressDefault = mContext.getResources().getBoolean(overrideLongPressHome - ? com.android.internal.R.bool.config_searchLongPressHomeEnabledDefault + ? com.android.internal.R.bool.config_searchAllEntrypointsEnabledDefault : com.android.internal.R.bool.config_assistLongPressHomeEnabledDefault); mLongPressHomeEnabled = Settings.Secure.getIntForUser(mContentResolver, - overrideLongPressHome ? Secure.SEARCH_LONG_PRESS_HOME_ENABLED + overrideLongPressHome ? Secure.SEARCH_ALL_ENTRYPOINTS_ENABLED : Settings.Secure.ASSIST_LONG_PRESS_HOME_ENABLED, longPressDefault ? 1 : 0, mUserTracker.getUserId()) != 0; diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index 768bb8e2e917..4fe3a11078db 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -934,48 +934,51 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private void orientSecondaryHomeHandle() { if (!canShowSecondaryHandle()) { - if (mStartingQuickSwitchRotation == -1) { - resetSecondaryHandle(); - } return; } - int deltaRotation = deltaRotation(mCurrentRotation, mStartingQuickSwitchRotation); - if (mStartingQuickSwitchRotation == -1 || deltaRotation == -1) { - // Curious if starting quickswitch can change between the if check and our delta - Log.d(TAG, "secondary nav delta rotation: " + deltaRotation - + " current: " + mCurrentRotation - + " starting: " + mStartingQuickSwitchRotation); - } - int height = 0; - int width = 0; - Rect dispSize = mWindowManager.getCurrentWindowMetrics().getBounds(); - mOrientationHandle.setDeltaRotation(deltaRotation); - switch (deltaRotation) { - case Surface.ROTATION_90, Surface.ROTATION_270: - height = dispSize.height(); - width = mView.getHeight(); - break; - case Surface.ROTATION_180, Surface.ROTATION_0: - // TODO(b/152683657): Need to determine best UX for this - if (!mShowOrientedHandleForImmersiveMode) { - resetSecondaryHandle(); - return; - } - width = dispSize.width(); - height = mView.getHeight(); - break; - } + if (mStartingQuickSwitchRotation == -1) { + resetSecondaryHandle(); + } else { + int deltaRotation = deltaRotation(mCurrentRotation, mStartingQuickSwitchRotation); + if (mStartingQuickSwitchRotation == -1 || deltaRotation == -1) { + // Curious if starting quickswitch can change between the if check and our delta + Log.d(TAG, "secondary nav delta rotation: " + deltaRotation + + " current: " + mCurrentRotation + + " starting: " + mStartingQuickSwitchRotation); + } + int height = 0; + int width = 0; + Rect dispSize = mWindowManager.getCurrentWindowMetrics().getBounds(); + mOrientationHandle.setDeltaRotation(deltaRotation); + switch (deltaRotation) { + case Surface.ROTATION_90: + case Surface.ROTATION_270: + height = dispSize.height(); + width = mView.getHeight(); + break; + case Surface.ROTATION_180: + case Surface.ROTATION_0: + // TODO(b/152683657): Need to determine best UX for this + if (!mShowOrientedHandleForImmersiveMode) { + resetSecondaryHandle(); + return; + } + width = dispSize.width(); + height = mView.getHeight(); + break; + } - mOrientationParams.gravity = - deltaRotation == Surface.ROTATION_0 ? Gravity.BOTTOM : - (deltaRotation == Surface.ROTATION_90 ? Gravity.LEFT : Gravity.RIGHT); - mOrientationParams.height = height; - mOrientationParams.width = width; - mWindowManager.updateViewLayout(mOrientationHandle, mOrientationParams); - mView.setVisibility(View.GONE); - mOrientationHandle.setVisibility(View.VISIBLE); - logNavbarOrientation("orientSecondaryHomeHandle"); + mOrientationParams.gravity = + deltaRotation == Surface.ROTATION_0 ? Gravity.BOTTOM : + (deltaRotation == Surface.ROTATION_90 ? Gravity.LEFT : Gravity.RIGHT); + mOrientationParams.height = height; + mOrientationParams.width = width; + mWindowManager.updateViewLayout(mOrientationHandle, mOrientationParams); + mView.setVisibility(View.GONE); + mOrientationHandle.setVisibility(View.VISIBLE); + logNavbarOrientation("orientSecondaryHomeHandle"); + } } private void resetSecondaryHandle() { @@ -1789,8 +1792,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } private boolean canShowSecondaryHandle() { - return mNavBarMode == NAV_BAR_MODE_GESTURAL && mOrientationHandle != null - && mStartingQuickSwitchRotation != -1; + return mNavBarMode == NAV_BAR_MODE_GESTURAL && mOrientationHandle != null; } private final UserTracker.Callback mUserChangedCallback = diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java index 5d2aeef5eb16..b34b3701528b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java +++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java @@ -432,6 +432,9 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { for (int i = 0; i < NP; i++) { mPages.get(i).removeAllViews(); } + if (mPageIndicator != null) { + mPageIndicator.setNumPages(numPages); + } if (NP == numPages) { return; } @@ -443,7 +446,6 @@ public class PagedTileLayout extends ViewPager implements QSTileLayout { mLogger.d("Removing page"); mPages.remove(mPages.size() - 1); } - mPageIndicator.setNumPages(mPages.size()); setAdapter(mAdapter); mAdapter.notifyDataSetChanged(); if (mPageToRestore != NO_PAGE) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt index d82b1755ac80..b418a174d84e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt @@ -44,6 +44,7 @@ import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.recordissue.IssueRecordingService +import com.android.systemui.recordissue.IssueRecordingState import com.android.systemui.recordissue.RecordIssueDialogDelegate import com.android.systemui.res.R import com.android.systemui.screenrecord.RecordingService @@ -69,6 +70,7 @@ constructor( private val dialogTransitionAnimator: DialogTransitionAnimator, private val panelInteractor: PanelInteractor, private val userContextProvider: UserContextProvider, + private val issueRecordingState: IssueRecordingState, private val delegateFactory: RecordIssueDialogDelegate.Factory, ) : QSTileImpl<QSTile.BooleanState>( @@ -83,7 +85,16 @@ constructor( qsLogger ) { - @VisibleForTesting var isRecording: Boolean = false + private val onRecordingChangeListener = Runnable { refreshState() } + + override fun handleSetListening(listening: Boolean) { + super.handleSetListening(listening) + if (listening) { + issueRecordingState.addListener(onRecordingChangeListener) + } else { + issueRecordingState.removeListener(onRecordingChangeListener) + } + } override fun getTileLabel(): CharSequence = mContext.getString(R.string.qs_record_issue_label) @@ -103,13 +114,11 @@ constructor( @VisibleForTesting public override fun handleClick(view: View?) { - if (isRecording) { - isRecording = false + if (issueRecordingState.isRecording) { stopIssueRecordingService() } else { mUiHandler.post { showPrompt(view) } } - refreshState() } private fun startIssueRecordingService(screenRecord: Boolean, winscopeTracing: Boolean) = @@ -138,11 +147,9 @@ constructor( val dialog: AlertDialog = delegateFactory .create { - isRecording = true startIssueRecordingService(it.screenRecord, it.winscopeTracing) dialogTransitionAnimator.disableAllCurrentDialogsExitAnimations() panelInteractor.collapsePanels() - refreshState() } .createDialog() val dismissAction = @@ -168,7 +175,7 @@ constructor( @VisibleForTesting public override fun handleUpdateState(qsTileState: QSTile.BooleanState, arg: Any?) { qsTileState.apply { - if (isRecording) { + if (issueRecordingState.isRecording) { value = true state = Tile.STATE_ACTIVE forceExpandIcon = false 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 2b8c335cb0ad..c0fc52e85866 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 @@ -83,6 +83,7 @@ constructor( } } + sideViewIcon = QSTileState.SideViewIcon.Chevron contentDescription = label supportedActions = setOf(QSTileState.UserAction.CLICK) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt index fc42ba495a51..b25c61cba2b7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/DataSaverDialogDelegate.kt @@ -39,7 +39,7 @@ class DataSaverDialogDelegate( return sysuiDialogFactory.create(this, context) } - override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { + override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { with(dialog) { setTitle(R.string.data_saver_enable_title) setMessage(R.string.data_saver_description) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractor.kt new file mode 100644 index 000000000000..a2a9e87a5981 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileDataInteractor.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.qs.tiles.impl.work.domain.interactor + +import android.os.UserHandle +import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger +import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor +import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel +import com.android.systemui.statusbar.phone.ManagedProfileController +import com.android.systemui.util.kotlin.hasActiveWorkProfile +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** Observes data saver state changes providing the [WorkModeTileModel]. */ +class WorkModeTileDataInteractor +@Inject +constructor( + private val profileController: ManagedProfileController, +) : QSTileDataInteractor<WorkModeTileModel> { + override fun tileData( + user: UserHandle, + triggers: Flow<DataUpdateTrigger> + ): Flow<WorkModeTileModel> = + profileController.hasActiveWorkProfile.map { hasActiveWorkProfile: Boolean -> + if (hasActiveWorkProfile) { + WorkModeTileModel.HasActiveProfile(profileController.isWorkModeEnabled) + } else { + WorkModeTileModel.NoActiveProfile + } + } + + override fun availability(user: UserHandle): Flow<Boolean> = + profileController.hasActiveWorkProfile +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt new file mode 100644 index 000000000000..f765f8b3ac77 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/interactor/WorkModeTileUserActionInteractor.kt @@ -0,0 +1,54 @@ +/* + * 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.qs.tiles.impl.work.domain.interactor + +import android.content.Intent +import android.provider.Settings +import com.android.systemui.qs.tiles.base.actions.QSTileIntentUserInputHandler +import com.android.systemui.qs.tiles.base.interactor.QSTileInput +import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor +import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel +import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction +import com.android.systemui.statusbar.phone.ManagedProfileController +import javax.inject.Inject + +/** Handles airplane mode tile clicks and long clicks. */ +class WorkModeTileUserActionInteractor +@Inject +constructor( + private val profileController: ManagedProfileController, + private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, +) : QSTileUserActionInteractor<WorkModeTileModel> { + override suspend fun handleInput(input: QSTileInput<WorkModeTileModel>) = + with(input) { + when (action) { + is QSTileUserAction.Click -> { + if (data is WorkModeTileModel.HasActiveProfile) { + profileController.setWorkModeEnabled(!data.isEnabled) + } + } + is QSTileUserAction.LongClick -> { + if (data is WorkModeTileModel.HasActiveProfile) { + qsTileIntentUserActionHandler.handle( + action.view, + Intent(Settings.ACTION_MANAGED_PROFILE_SETTINGS) + ) + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/model/WorkModeTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/model/WorkModeTileModel.kt new file mode 100644 index 000000000000..ae8382daf77d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/domain/model/WorkModeTileModel.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.work.domain.model + +/** Work mode tile model. */ +sealed interface WorkModeTileModel { + /** @param isEnabled is true when the work mode is enabled */ + data class HasActiveProfile(val isEnabled: Boolean) : WorkModeTileModel + data object NoActiveProfile : WorkModeTileModel +} 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 new file mode 100644 index 000000000000..55445bb922a5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapper.kt @@ -0,0 +1,84 @@ +/* + * 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.qs.tiles.impl.work.ui + +import android.app.admin.DevicePolicyManager +import android.app.admin.DevicePolicyResources.Strings.SystemUi.QS_WORK_PROFILE_LABEL +import android.content.res.Resources +import android.service.quicksettings.Tile +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper +import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel +import com.android.systemui.qs.tiles.viewmodel.QSTileConfig +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.res.R +import javax.inject.Inject + +/** Maps [WorkModeTileModel] to [QSTileState]. */ +class WorkModeTileMapper +@Inject +constructor( + @Main private val resources: Resources, + private val theme: Resources.Theme, + private val devicePolicyManager: DevicePolicyManager, +) : QSTileDataToStateMapper<WorkModeTileModel> { + override fun map(config: QSTileConfig, data: WorkModeTileModel): QSTileState = + QSTileState.build(resources, theme, config.uiConfig) { + label = getTileLabel()!! + contentDescription = label + + icon = { + Icon.Loaded( + resources.getDrawable( + com.android.internal.R.drawable.stat_sys_managed_profile_status, + theme + ), + contentDescription = null + ) + } + + when (data) { + is WorkModeTileModel.HasActiveProfile -> { + if (data.isEnabled) { + activationState = QSTileState.ActivationState.ACTIVE + secondaryLabel = "" + } else { + activationState = QSTileState.ActivationState.INACTIVE + secondaryLabel = + resources.getString(R.string.quick_settings_work_mode_paused_state) + } + supportedActions = + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) + } + is WorkModeTileModel.NoActiveProfile -> { + activationState = QSTileState.ActivationState.UNAVAILABLE + secondaryLabel = + resources.getStringArray(R.array.tile_states_work)[Tile.STATE_UNAVAILABLE] + supportedActions = setOf() + } + } + + sideViewIcon = QSTileState.SideViewIcon.None + } + + private fun getTileLabel(): CharSequence? { + return devicePolicyManager.resources.getString(QS_WORK_PROFILE_LABEL) { + resources.getString(R.string.quick_settings_work_mode_label) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt index 7009816942f2..5e4919d44f23 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt @@ -59,6 +59,7 @@ constructor( keyguardDismissUtil: KeyguardDismissUtil, private val dialogTransitionAnimator: DialogTransitionAnimator, private val panelInteractor: PanelInteractor, + private val issueRecordingState: IssueRecordingState, ) : RecordingService( controller, @@ -90,6 +91,7 @@ constructor( DEFAULT_MAX_TRACE_SIZE, DEFAULT_MAX_TRACE_DURATION_IN_MINUTES ) + issueRecordingState.isRecording = true if (!intent.getBooleanExtra(EXTRA_SCREEN_RECORD, false)) { // If we don't want to record the screen, the ACTION_SHOW_START_NOTIF action // will circumvent the RecordingService's screen recording start code. @@ -103,6 +105,7 @@ constructor( // this line should be removed. getSystemService(LauncherApps::class.java)?.saveViewCaptureData() TraceUtils.traceStop(contentResolver) + issueRecordingState.isRecording = false } ACTION_SHARE -> { shareRecording(intent) diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt new file mode 100644 index 000000000000..394c5c2775a4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingState.kt @@ -0,0 +1,41 @@ +/* + * 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.recordissue + +import com.android.systemui.dagger.SysUISingleton +import java.util.concurrent.CopyOnWriteArrayList +import javax.inject.Inject + +@SysUISingleton +class IssueRecordingState @Inject constructor() { + + private val listeners = CopyOnWriteArrayList<Runnable>() + + var isRecording = false + set(value) { + field = value + listeners.forEach(Runnable::run) + } + + fun addListener(listener: Runnable) { + listeners.add(listener) + } + + fun removeListener(listener: Runnable) { + listeners.remove(listener) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt index 7313a49be1bf..832fc3f00022 100644 --- a/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/recordissue/RecordIssueDialogDelegate.kt @@ -17,6 +17,7 @@ package com.android.systemui.recordissue import android.annotation.SuppressLint +import android.app.AlertDialog import android.content.Context import android.content.res.ColorStateList import android.graphics.Color @@ -74,7 +75,6 @@ constructor( @SuppressLint("UseSwitchCompatOrMaterialCode") private lateinit var screenRecordSwitch: Switch private lateinit var issueTypeButton: Button - private var hasSelectedIssueType: Boolean = false @MainThread override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { @@ -86,15 +86,13 @@ constructor( setPositiveButton( R.string.qs_record_issue_start, { _, _ -> - if (hasSelectedIssueType) { - onStarted.accept( - IssueRecordingConfig( - screenRecordSwitch.isChecked, - true /* TODO: Base this on issueType selected */ - ) + onStarted.accept( + IssueRecordingConfig( + screenRecordSwitch.isChecked, + true /* TODO: Base this on issueType selected */ ) - dismiss() - } + ) + dismiss() }, false ) @@ -115,8 +113,12 @@ constructor( bgExecutor.execute { onScreenRecordSwitchClicked() } } } + val startButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE) issueTypeButton = requireViewById(R.id.issue_type_button) - issueTypeButton.setOnClickListener { onIssueTypeClicked(context) } + issueTypeButton.setOnClickListener { + onIssueTypeClicked(context) { startButton.isEnabled = true } + } + startButton.isEnabled = false } } @@ -159,7 +161,7 @@ constructor( } @MainThread - private fun onIssueTypeClicked(context: Context) { + private fun onIssueTypeClicked(context: Context, onIssueTypeSelected: Runnable) { val selectedCategory = issueTypeButton.text.toString() val popupMenu = PopupMenu(context, issueTypeButton) @@ -174,11 +176,11 @@ constructor( popupMenu.apply { setOnMenuItemClickListener { issueTypeButton.text = it.title + onIssueTypeSelected.run() true } setForceShowIcon(true) show() } - hasSelectedIssueType = true } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt index 467089d24f2c..54ec398cd9a7 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/flag/SceneContainerFlags.kt @@ -18,18 +18,15 @@ package com.android.systemui.scene.shared.flag -import com.android.systemui.Flags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR -import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR -import com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT import com.android.systemui.Flags.FLAG_SCENE_CONTAINER -import com.android.systemui.Flags.keyguardBottomAreaRefactor -import com.android.systemui.Flags.keyguardWmStateRefactor -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.Flags.sceneContainer import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FlagToken import com.android.systemui.flags.Flags.SCENE_CONTAINER_ENABLED import com.android.systemui.flags.RefactorFlagUtils +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor +import com.android.systemui.keyguard.KeyguardWmStateRefactor +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.shared.ComposeLockscreen import com.android.systemui.media.controls.util.MediaInSceneContainerFlag import dagger.Module @@ -45,11 +42,11 @@ object SceneContainerFlag { get() = SCENE_CONTAINER_ENABLED && // mainStaticFlag sceneContainer() && // mainAconfigFlag - keyguardBottomAreaRefactor() && - migrateClocksToBlueprint() && + KeyguardBottomAreaRefactor.isEnabled && + MigrateClocksToBlueprint.isEnabled && ComposeLockscreen.isEnabled && MediaInSceneContainerFlag.isEnabled && - keyguardWmStateRefactor() + KeyguardWmStateRefactor.isEnabled // NOTE: Changes should also be made in getSecondaryFlags and @EnableSceneContainer /** @@ -66,9 +63,9 @@ object SceneContainerFlag { /** The set of secondary flags which must be enabled for scene container to work properly */ inline fun getSecondaryFlags(): Sequence<FlagToken> = sequenceOf( - FlagToken(FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR, keyguardBottomAreaRefactor()), - FlagToken(FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT, migrateClocksToBlueprint()), - FlagToken(FLAG_KEYGUARD_WM_STATE_REFACTOR, keyguardWmStateRefactor()), + KeyguardBottomAreaRefactor.token, + MigrateClocksToBlueprint.token, + KeyguardWmStateRefactor.token, ComposeLockscreen.token, MediaInSceneContainerFlag.token, // NOTE: Changes should also be made in isEnabled and @EnableSceneContainer diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt new file mode 100644 index 000000000000..abdbd6880b33 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.screenshot + +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.UserHandle +import androidx.appcompat.content.res.AppCompatResources +import com.android.systemui.res.R +import javax.inject.Inject + +/** + * Provides static actions for screenshots. This class can be overridden by a vendor-specific SysUI + * implementation. + */ +interface ScreenshotActionsProvider { + data class ScreenshotAction( + val icon: Drawable?, + val text: String?, + val overrideTransition: Boolean, + val retrieveIntent: (Uri) -> Intent + ) + + fun getPreviewAction(context: Context, uri: Uri, user: UserHandle): Intent + fun getActions(context: Context, user: UserHandle): List<ScreenshotAction> +} + +class DefaultScreenshotActionsProvider @Inject constructor() : ScreenshotActionsProvider { + override fun getPreviewAction(context: Context, uri: Uri, user: UserHandle): Intent { + return ActionIntentCreator.createEdit(uri, context) + } + + override fun getActions( + context: Context, + user: UserHandle + ): List<ScreenshotActionsProvider.ScreenshotAction> { + val editAction = + ScreenshotActionsProvider.ScreenshotAction( + AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_edit), + context.resources.getString(R.string.screenshot_edit_label), + true + ) { uri -> + ActionIntentCreator.createEdit(uri, context) + } + val shareAction = + ScreenshotActionsProvider.ScreenshotAction( + AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_share), + context.resources.getString(R.string.screenshot_share_label), + false + ) { uri -> + ActionIntentCreator.createShare(uri) + } + return listOf(editAction, shareAction) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt new file mode 100644 index 000000000000..9354fd27ce5a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt @@ -0,0 +1,226 @@ +/* + * 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.screenshot + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.app.Notification +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Rect +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.ScrollCaptureResponse +import android.view.View +import android.view.ViewTreeObserver +import android.view.WindowInsets +import android.window.OnBackInvokedCallback +import android.window.OnBackInvokedDispatcher +import com.android.internal.logging.UiEventLogger +import com.android.systemui.log.DebugLogger.debugLog +import com.android.systemui.res.R +import com.android.systemui.screenshot.LogConfig.DEBUG_ACTIONS +import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS +import com.android.systemui.screenshot.LogConfig.DEBUG_INPUT +import com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW +import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER +import com.android.systemui.screenshot.scroll.ScrollCaptureController +import com.android.systemui.screenshot.ui.ScreenshotAnimationController +import com.android.systemui.screenshot.ui.ScreenshotShelfView +import com.android.systemui.screenshot.ui.binder.ScreenshotShelfViewBinder +import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel +import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** Controls the screenshot view and viewModel. */ +class ScreenshotShelfViewProxy +@AssistedInject +constructor( + private val logger: UiEventLogger, + private val viewModel: ScreenshotViewModel, + private val staticActionsProvider: ScreenshotActionsProvider, + @Assisted private val context: Context, + @Assisted private val displayId: Int +) : ScreenshotViewProxy { + override val view: ScreenshotShelfView = + LayoutInflater.from(context).inflate(R.layout.screenshot_shelf, null) as ScreenshotShelfView + override val screenshotPreview: View + override var packageName: String = "" + override var callbacks: ScreenshotView.ScreenshotViewCallback? = null + override var screenshot: ScreenshotData? = null + set(value) { + viewModel.setScreenshotBitmap(value?.bitmap) + field = value + } + + override val isAttachedToWindow + get() = view.isAttachedToWindow + override var isDismissing = false + override var isPendingSharedTransition = false + + private val animationController = ScreenshotAnimationController(view) + + init { + ScreenshotShelfViewBinder.bind(view, viewModel, LayoutInflater.from(context)) + addPredictiveBackListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) } + setOnKeyListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) } + debugLog(DEBUG_WINDOW) { "adding OnComputeInternalInsetsListener" } + screenshotPreview = view.screenshotPreview + } + + override fun reset() { + animationController.cancel() + isPendingSharedTransition = false + viewModel.setScreenshotBitmap(null) + viewModel.setActions(listOf()) + } + override fun updateInsets(insets: WindowInsets) {} + override fun updateOrientation(insets: WindowInsets) {} + + override fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator { + return animationController.getEntranceAnimation() + } + + override fun addQuickShareChip(quickShareAction: Notification.Action) {} + + override fun setChipIntents(imageData: ScreenshotController.SavedImageData) { + val staticActions = + staticActionsProvider.getActions(context, imageData.owner).map { + ActionButtonViewModel(it.icon, it.text) { + val intent = it.retrieveIntent(imageData.uri) + debugLog(DEBUG_ACTIONS) { "Action tapped: $intent" } + isPendingSharedTransition = true + callbacks?.onAction(intent, imageData.owner, it.overrideTransition) + } + } + + viewModel.setActions(staticActions) + } + + override fun requestDismissal(event: ScreenshotEvent) { + debugLog(DEBUG_DISMISS) { "screenshot dismissal requested: $event" } + + // If we're already animating out, don't restart the animation + if (isDismissing) { + debugLog(DEBUG_DISMISS) { "Already dismissing, ignoring duplicate command $event" } + return + } + logger.log(event, 0, packageName) + val animator = animationController.getExitAnimation() + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animator: Animator) { + isDismissing = true + } + override fun onAnimationEnd(animator: Animator) { + isDismissing = false + callbacks?.onDismiss() + } + } + ) + animator.start() + } + + override fun showScrollChip(packageName: String, onClick: Runnable) {} + + override fun hideScrollChip() {} + + override fun prepareScrollingTransition( + response: ScrollCaptureResponse, + screenBitmap: Bitmap, + newScreenshot: Bitmap, + screenshotTakenInPortrait: Boolean, + onTransitionPrepared: Runnable, + ) {} + + override fun startLongScreenshotTransition( + transitionDestination: Rect, + onTransitionEnd: Runnable, + longScreenshot: ScrollCaptureController.LongScreenshot + ) {} + + override fun restoreNonScrollingUi() {} + + override fun stopInputListening() {} + + override fun requestFocus() { + view.requestFocus() + } + + override fun announceForAccessibility(string: String) = view.announceForAccessibility(string) + + override fun prepareEntranceAnimation(runnable: Runnable) { + view.viewTreeObserver.addOnPreDrawListener( + object : ViewTreeObserver.OnPreDrawListener { + override fun onPreDraw(): Boolean { + debugLog(DEBUG_WINDOW) { "onPreDraw: startAnimation" } + view.viewTreeObserver.removeOnPreDrawListener(this) + runnable.run() + return true + } + } + ) + } + + private fun addPredictiveBackListener(onDismissRequested: (ScreenshotEvent) -> Unit) { + val onBackInvokedCallback = OnBackInvokedCallback { + debugLog(DEBUG_INPUT) { "Predictive Back callback dispatched" } + onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER) + } + view.addOnAttachStateChangeListener( + object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + debugLog(DEBUG_INPUT) { "Registering Predictive Back callback" } + view + .findOnBackInvokedDispatcher() + ?.registerOnBackInvokedCallback( + OnBackInvokedDispatcher.PRIORITY_DEFAULT, + onBackInvokedCallback + ) + } + + override fun onViewDetachedFromWindow(view: View) { + debugLog(DEBUG_INPUT) { "Unregistering Predictive Back callback" } + view + .findOnBackInvokedDispatcher() + ?.unregisterOnBackInvokedCallback(onBackInvokedCallback) + } + } + ) + } + private fun setOnKeyListener(onDismissRequested: (ScreenshotEvent) -> Unit) { + view.setOnKeyListener( + object : View.OnKeyListener { + override fun onKey(view: View, keyCode: Int, event: KeyEvent): Boolean { + if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) { + debugLog(DEBUG_INPUT) { "onKeyEvent: $keyCode" } + onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER) + return true + } + return false + } + } + ) + } + + @AssistedFactory + interface Factory : ScreenshotViewProxy.Factory { + override fun getProxy(context: Context, displayId: Int): ScreenshotShelfViewProxy + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java index cdb9abb15e84..9118ee1dfc73 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java @@ -16,16 +16,23 @@ package com.android.systemui.screenshot.dagger; +import static com.android.systemui.Flags.screenshotShelfUi; + import android.app.Service; +import android.view.accessibility.AccessibilityManager; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.screenshot.DefaultScreenshotActionsProvider; import com.android.systemui.screenshot.ImageCapture; import com.android.systemui.screenshot.ImageCaptureImpl; import com.android.systemui.screenshot.LegacyScreenshotViewProxy; import com.android.systemui.screenshot.RequestProcessor; +import com.android.systemui.screenshot.ScreenshotActionsProvider; import com.android.systemui.screenshot.ScreenshotPolicy; import com.android.systemui.screenshot.ScreenshotPolicyImpl; import com.android.systemui.screenshot.ScreenshotProxyService; import com.android.systemui.screenshot.ScreenshotRequestProcessor; +import com.android.systemui.screenshot.ScreenshotShelfViewProxy; import com.android.systemui.screenshot.ScreenshotSoundController; import com.android.systemui.screenshot.ScreenshotSoundControllerImpl; import com.android.systemui.screenshot.ScreenshotSoundProvider; @@ -34,6 +41,7 @@ import com.android.systemui.screenshot.ScreenshotViewProxy; import com.android.systemui.screenshot.TakeScreenshotService; import com.android.systemui.screenshot.appclips.AppClipsScreenshotHelperService; import com.android.systemui.screenshot.appclips.AppClipsService; +import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel; import dagger.Binds; import dagger.Module; @@ -85,9 +93,25 @@ public abstract class ScreenshotModule { abstract ScreenshotSoundController bindScreenshotSoundController( ScreenshotSoundControllerImpl screenshotSoundProviderImpl); + @Binds + abstract ScreenshotActionsProvider bindScreenshotActionsProvider( + DefaultScreenshotActionsProvider defaultScreenshotActionsProvider); + + @Provides + @SysUISingleton + static ScreenshotViewModel providesScreenshotViewModel( + AccessibilityManager accessibilityManager) { + return new ScreenshotViewModel(accessibilityManager); + } + @Provides static ScreenshotViewProxy.Factory providesScreenshotViewProxyFactory( + ScreenshotShelfViewProxy.Factory shelfScreenshotViewProxyFactory, LegacyScreenshotViewProxy.Factory legacyScreenshotViewProxyFactory) { - return legacyScreenshotViewProxyFactory; + if (screenshotShelfUi()) { + return shelfScreenshotViewProxyFactory; + } else { + return legacyScreenshotViewProxyFactory; + } } } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt new file mode 100644 index 000000000000..2c178736d9c4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt @@ -0,0 +1,64 @@ +/* + * 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.screenshot.ui + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.view.View + +class ScreenshotAnimationController(private val view: View) { + private var animator: Animator? = null + + fun getEntranceAnimation(): Animator { + val animator = ValueAnimator.ofFloat(0f, 1f) + animator.addUpdateListener { view.alpha = it.animatedFraction } + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animator: Animator) { + view.alpha = 0f + } + override fun onAnimationEnd(animator: Animator) { + view.alpha = 1f + } + } + ) + this.animator = animator + return animator + } + + fun getExitAnimation(): Animator { + val animator = ValueAnimator.ofFloat(1f, 0f) + animator.addUpdateListener { view.alpha = it.animatedValue as Float } + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animator: Animator) { + view.alpha = 1f + } + override fun onAnimationEnd(animator: Animator) { + view.alpha = 0f + } + } + ) + this.animator = animator + return animator + } + + fun cancel() { + animator?.cancel() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt new file mode 100644 index 000000000000..747ad4f9e48c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt @@ -0,0 +1,33 @@ +/* + * 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.screenshot.ui + +import android.content.Context +import android.util.AttributeSet +import android.widget.ImageView +import androidx.constraintlayout.widget.ConstraintLayout +import com.android.systemui.res.R + +class ScreenshotShelfView(context: Context, attrs: AttributeSet? = null) : + ConstraintLayout(context, attrs) { + lateinit var screenshotPreview: ImageView + + override fun onFinishInflate() { + super.onFinishInflate() + screenshotPreview = requireViewById(R.id.screenshot_preview) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt new file mode 100644 index 000000000000..a5825b5f7797 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt @@ -0,0 +1,64 @@ +/* + * 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.screenshot.ui.binder + +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import com.android.systemui.res.R +import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel + +object ActionButtonViewBinder { + /** Binds the given view to the given view-model */ + fun bind(view: View, viewModel: ActionButtonViewModel) { + val iconView = view.requireViewById<ImageView>(R.id.overlay_action_chip_icon) + val textView = view.requireViewById<TextView>(R.id.overlay_action_chip_text) + iconView.setImageDrawable(viewModel.icon) + textView.text = viewModel.name + setMargins(iconView, textView, viewModel.name?.isNotEmpty() ?: false) + if (viewModel.onClicked != null) { + view.setOnClickListener { viewModel.onClicked.invoke() } + } else { + view.setOnClickListener(null) + } + view.visibility = View.VISIBLE + view.alpha = 1f + } + + private fun setMargins(iconView: View, textView: View, hasText: Boolean) { + val iconParams = iconView.layoutParams as LinearLayout.LayoutParams + val textParams = textView.layoutParams as LinearLayout.LayoutParams + if (hasText) { + iconParams.marginStart = iconView.dpToPx(R.dimen.overlay_action_chip_padding_start) + iconParams.marginEnd = iconView.dpToPx(R.dimen.overlay_action_chip_spacing) + textParams.marginStart = 0 + textParams.marginEnd = textView.dpToPx(R.dimen.overlay_action_chip_padding_end) + } else { + val paddingHorizontal = + iconView.dpToPx(R.dimen.overlay_action_chip_icon_only_padding_horizontal) + iconParams.marginStart = paddingHorizontal + iconParams.marginEnd = paddingHorizontal + } + iconView.layoutParams = iconParams + textView.layoutParams = textParams + } + + private fun View.dpToPx(dimenId: Int): Int { + return this.resources.getDimensionPixelSize(dimenId) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt new file mode 100644 index 000000000000..3bcd52cbc99e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt @@ -0,0 +1,90 @@ +/* + * 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.screenshot.ui.binder + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.LinearLayout +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R +import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel +import com.android.systemui.util.children +import kotlinx.coroutines.launch + +object ScreenshotShelfViewBinder { + fun bind( + view: ViewGroup, + viewModel: ScreenshotViewModel, + layoutInflater: LayoutInflater, + ) { + val previewView: ImageView = view.requireViewById(R.id.screenshot_preview) + val previewBorder = view.requireViewById<View>(R.id.screenshot_preview_border) + previewView.clipToOutline = true + val actionsContainer: LinearLayout = view.requireViewById(R.id.screenshot_actions) + view.requireViewById<View>(R.id.screenshot_dismiss_button).visibility = + if (viewModel.showDismissButton) View.VISIBLE else View.GONE + + view.repeatWhenAttached { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + launch { + viewModel.preview.collect { bitmap -> + if (bitmap != null) { + previewView.setImageBitmap(bitmap) + previewView.visibility = View.VISIBLE + previewBorder.visibility = View.VISIBLE + } else { + previewView.visibility = View.GONE + previewBorder.visibility = View.GONE + } + } + } + launch { + viewModel.actions.collect { actions -> + if (actions.isNotEmpty()) { + view + .requireViewById<View>(R.id.actions_container_background) + .visibility = View.VISIBLE + } + val viewPool = actionsContainer.children.toList() + actionsContainer.removeAllViews() + val actionButtons = + List(actions.size) { + viewPool.getOrElse(it) { + layoutInflater.inflate( + R.layout.overlay_action_chip, + actionsContainer, + false + ) + } + } + actionButtons.zip(actions).forEach { + actionsContainer.addView(it.first) + ActionButtonViewBinder.bind(it.first, it.second) + } + } + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt new file mode 100644 index 000000000000..6ee970534352 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt @@ -0,0 +1,25 @@ +/* + * 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.screenshot.ui.viewmodel + +import android.graphics.drawable.Drawable + +data class ActionButtonViewModel( + val icon: Drawable?, + val name: String?, + val onClicked: (() -> Unit)? +) diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt new file mode 100644 index 000000000000..3a652d90bb78 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt @@ -0,0 +1,39 @@ +/* + * 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.screenshot.ui.viewmodel + +import android.graphics.Bitmap +import android.view.accessibility.AccessibilityManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class ScreenshotViewModel(private val accessibilityManager: AccessibilityManager) { + private val _preview = MutableStateFlow<Bitmap?>(null) + val preview: StateFlow<Bitmap?> = _preview + private val _actions = MutableStateFlow(emptyList<ActionButtonViewModel>()) + val actions: StateFlow<List<ActionButtonViewModel>> = _actions + val showDismissButton: Boolean + get() = accessibilityManager.isEnabled + + fun setScreenshotBitmap(bitmap: Bitmap?) { + _preview.value = bitmap + } + + fun setActions(actions: List<ActionButtonViewModel>) { + _actions.value = actions + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 9cb920ab0a88..2de14dd072f0 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -24,8 +24,6 @@ import static com.android.app.animation.Interpolators.EMPHASIZED_ACCELERATE; import static com.android.app.animation.Interpolators.EMPHASIZED_DECELERATE; import static com.android.keyguard.KeyguardClockSwitch.LARGE; import static com.android.keyguard.KeyguardClockSwitch.SMALL; -import static com.android.systemui.Flags.keyguardBottomAreaRefactor; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import static com.android.systemui.Flags.predictiveBackAnimateShade; import static com.android.systemui.Flags.smartspaceRelocateToBottom; import static com.android.systemui.classifier.Classifier.BOUNCER_UNLOCK; @@ -129,8 +127,10 @@ import com.android.systemui.dump.DumpsysTableLogger; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.fragments.FragmentService; +import com.android.systemui.keyguard.KeyguardBottomAreaRefactor; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewConfigurator; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; @@ -1018,7 +1018,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump instantCollapse(); } else { mView.animate().cancel(); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mView.animate() .alpha(0f) .setStartDelay(0) @@ -1075,7 +1075,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mQsController.init(); mShadeHeadsUpTracker.addTrackingHeadsUpListener( mNotificationStackScrollLayoutController::setTrackingHeadsUp); - if (!keyguardBottomAreaRefactor()) { + if (!KeyguardBottomAreaRefactor.isEnabled()) { setKeyguardBottomArea(mView.findViewById(R.id.keyguard_bottom_area)); } @@ -1154,7 +1154,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // Occluded->Lockscreen collectFlow(mView, mKeyguardTransitionInteractor.getOccludedToLockscreenTransition(), mOccludedToLockscreenTransition, mMainDispatcher); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { collectFlow(mView, mOccludedToLockscreenTransitionViewModel.getLockscreenAlpha(), setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher); collectFlow(mView, @@ -1165,7 +1165,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // Lockscreen->Dreaming collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToDreamingTransition(), mLockscreenToDreamingTransition, mMainDispatcher); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { collectFlow(mView, mLockscreenToDreamingTransitionViewModel.getLockscreenAlpha(), setDreamLockscreenTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher); @@ -1177,7 +1177,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // Gone->Dreaming collectFlow(mView, mKeyguardTransitionInteractor.getGoneToDreamingTransition(), mGoneToDreamingTransition, mMainDispatcher); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { collectFlow(mView, mGoneToDreamingTransitionViewModel.getLockscreenAlpha(), setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher); } @@ -1188,7 +1188,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // Lockscreen->Occluded collectFlow(mView, mKeyguardTransitionInteractor.getLockscreenToOccludedTransition(), mLockscreenToOccludedTransition, mMainDispatcher); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { collectFlow(mView, mLockscreenToOccludedTransitionViewModel.getLockscreenAlpha(), setTransitionAlpha(mNotificationStackScrollLayoutController), mMainDispatcher); collectFlow(mView, mLockscreenToOccludedTransitionViewModel.getLockscreenTranslationY(), @@ -1196,7 +1196,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } // Primary bouncer->Gone (ensures lockscreen content is not visible on successful auth) - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { collectFlow(mView, mPrimaryBouncerToGoneTransitionViewModel.getLockscreenAlpha(), setTransitionAlpha(mNotificationStackScrollLayoutController, /* excludeNotifications=*/ true), mMainDispatcher); @@ -1280,7 +1280,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mKeyguardStatusViewController.onDestroy(); } - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { // Need a shared controller until mKeyguardStatusViewController can be removed from // here, due to important state being set in that controller. Rebind in order to pick // up config changes @@ -1332,13 +1332,13 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private void onSplitShadeEnabledChanged() { mShadeLog.logSplitShadeChanged(mSplitShadeEnabled); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mKeyguardStatusViewController.setSplitShadeEnabled(mSplitShadeEnabled); } // Reset any left over overscroll state. It is a rare corner case but can happen. mQsController.setOverScrollAmount(0); mScrimController.setNotificationsOverScrollAmount(0); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mNotificationStackScrollLayoutController.setOverExpansion(0); mNotificationStackScrollLayoutController.setOverScrollAmount(0); } @@ -1359,7 +1359,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } updateClockAppearance(); mQsController.updateQsState(); - if (!migrateClocksToBlueprint() && !FooterViewRefactor.isEnabled()) { + if (!MigrateClocksToBlueprint.isEnabled() && !FooterViewRefactor.isEnabled()) { mNotificationStackScrollLayoutController.updateFooter(); } } @@ -1391,7 +1391,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump void reInflateViews() { debugLog("reInflateViews"); // Re-inflate the status view group. - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { KeyguardStatusView keyguardStatusView = mNotificationContainerParent.findViewById(R.id.keyguard_status_view); int statusIndex = mNotificationContainerParent.indexOfChild(keyguardStatusView); @@ -1430,7 +1430,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump updateViewControllers(userAvatarView, keyguardUserSwitcherView); - if (!keyguardBottomAreaRefactor()) { + if (!KeyguardBottomAreaRefactor.isEnabled()) { // Update keyguard bottom area int index = mView.indexOfChild(mKeyguardBottomArea); mView.removeView(mKeyguardBottomArea); @@ -1449,7 +1449,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mStatusBarStateListener.onDozeAmountChanged(mStatusBarStateController.getDozeAmount(), mStatusBarStateController.getInterpolatedDozeAmount()); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mKeyguardStatusViewController.setKeyguardStatusViewVisibility( mBarState, false, @@ -1471,7 +1471,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mBarState); } - if (!keyguardBottomAreaRefactor()) { + if (!KeyguardBottomAreaRefactor.isEnabled()) { setKeyguardBottomAreaVisibility(mBarState, false); } @@ -1480,14 +1480,14 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } private void attachSplitShadeMediaPlayerContainer(FrameLayout container) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } mKeyguardMediaController.attachSplitShadeContainer(container); } private void initBottomArea() { - if (!keyguardBottomAreaRefactor()) { + if (!KeyguardBottomAreaRefactor.isEnabled()) { mKeyguardBottomArea.init( mKeyguardBottomAreaViewModel, mFalsingManager, @@ -1513,7 +1513,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } private void updateMaxDisplayedNotifications(boolean recompute) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } @@ -1630,7 +1630,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump int userSwitcherPreferredY = mStatusBarHeaderHeightKeyguard; boolean bypassEnabled = mKeyguardBypassController.getBypassEnabled(); boolean shouldAnimateClockChange = mScreenOffAnimationController.shouldAnimateClockChange(); - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { mKeyguardClockInteractor.setClockSize(computeDesiredClockSize()); } else { mKeyguardStatusViewController.displayClock(computeDesiredClockSize(), @@ -1671,11 +1671,11 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mKeyguardStatusViewController.getClockBottom(mStatusBarHeaderHeightKeyguard), mKeyguardStatusViewController.isClockTopAligned()); mClockPositionAlgorithm.run(mClockPositionResult); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mKeyguardStatusViewController.setLockscreenClockY( mClockPositionAlgorithm.getExpandedPreferredClockY()); } - if (!(migrateClocksToBlueprint() || keyguardBottomAreaRefactor())) { + if (!(MigrateClocksToBlueprint.isEnabled() || KeyguardBottomAreaRefactor.isEnabled())) { mKeyguardBottomAreaInteractor.setClockPosition( mClockPositionResult.clockX, mClockPositionResult.clockY); } @@ -1683,7 +1683,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump boolean animate = mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending(); boolean animateClock = (animate || mAnimateNextPositionUpdate) && shouldAnimateClockChange; - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mKeyguardStatusViewController.updatePosition( mClockPositionResult.clockX, mClockPositionResult.clockY, mClockPositionResult.clockScale, animateClock); @@ -1740,7 +1740,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump // To prevent the weather clock from overlapping with the notification shelf on AOD, we use // the small clock here // With migrateClocksToBlueprint, weather clock will have behaviors similar to other clocks - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { if (mKeyguardStatusViewController.isLargeClockBlockingNotificationShelf() && hasVisibleNotifications() && isOnAod()) { return SMALL; @@ -1758,7 +1758,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private void updateKeyguardStatusViewAlignment(boolean animate) { boolean shouldBeCentered = shouldKeyguardStatusViewBeCentered(); - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { mKeyguardInteractor.setClockShouldBeCentered(shouldBeCentered); return; } @@ -1941,7 +1941,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } float alpha = mClockPositionResult.clockAlpha * mKeyguardOnlyContentAlpha; mKeyguardStatusViewController.setAlpha(alpha); - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { // TODO (b/296373478) This is for split shade media movement. } else { mKeyguardStatusViewController @@ -2498,7 +2498,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } if (!mKeyguardBypassController.getBypassEnabled()) { - if (migrateClocksToBlueprint() && !mSplitShadeEnabled) { + if (MigrateClocksToBlueprint.isEnabled() && !mSplitShadeEnabled) { return (int) mKeyguardInteractor.getNotificationContainerBounds() .getValue().getTop(); } @@ -2531,7 +2531,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump void requestScrollerTopPaddingUpdate(boolean animate) { float padding = mQsController.calculateNotificationsTopPadding(mIsExpandingOrCollapsing, getKeyguardNotificationStaticPadding(), mExpandedFraction); - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { mSharedNotificationContainerInteractor.setTopPosition(padding); } else { mNotificationStackScrollLayoutController.updateTopPadding(padding, animate); @@ -2712,7 +2712,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump return; } - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { float alpha = 1f; if (mClosingWithAlphaFadeOut && !mExpandingFromHeadsUp && !mHeadsUpManager.hasPinnedHeadsUp()) { @@ -2748,7 +2748,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } private void updateKeyguardBottomAreaAlpha() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } if (mIsOcclusionTransitionRunning) { @@ -2766,7 +2766,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump float alpha = Math.min(expansionAlpha, 1 - mQsController.computeExpansionFraction()); alpha *= mBottomAreaShadeAlpha; - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled()) { mKeyguardInteractor.setAlpha(alpha); } else { mKeyguardBottomAreaInteractor.setAlpha(alpha); @@ -2978,7 +2978,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } private void updateDozingVisibilities(boolean animate) { - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled()) { mKeyguardInteractor.setAnimateDozingTransitions(animate); } else { mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate); @@ -2990,7 +2990,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @Override public void onScreenTurningOn() { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mKeyguardStatusViewController.dozeTimeTick(); } } @@ -3189,7 +3189,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mDozing = dozing; // TODO (b/) make listeners for this mNotificationStackScrollLayoutController.setDozing(mDozing, animate); - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled()) { mKeyguardInteractor.setAnimateDozingTransitions(animate); } else { mKeyguardBottomAreaInteractor.setAnimateDozingTransitions(animate); @@ -3245,7 +3245,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump public void dozeTimeTick() { mLockIconViewController.dozeTimeTick(); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mKeyguardStatusViewController.dozeTimeTick(); } if (mInterpolatedDarkAmount > 0) { @@ -3324,7 +3324,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump /** Updates the views to the initial state for the fold to AOD animation. */ @Override public void prepareFoldToAodAnimation() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } // Force show AOD UI even if we are not locked @@ -3348,7 +3348,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @Override public void startFoldToAodAnimation(Runnable startAction, Runnable endAction, Runnable cancelAction) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } final ViewPropertyAnimator viewAnimator = mView.animate(); @@ -3386,7 +3386,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump /** Cancels fold to AOD transition and resets view state. */ @Override public void cancelFoldToAodAnimation() { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return; } cancelAnimation(); @@ -4460,7 +4460,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump && statusBarState == KEYGUARD) { // This means we're doing the screen off animation - position the keyguard status // view where it'll be on AOD, so we can animate it in. - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mKeyguardStatusViewController.updatePosition( mClockPositionResult.clockX, mClockPositionResult.clockYFullyDozing, @@ -4469,7 +4469,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } } - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mKeyguardStatusViewController.setKeyguardStatusViewVisibility( statusBarState, keyguardFadingAway, @@ -4477,7 +4477,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mBarState); } - if (!keyguardBottomAreaRefactor()) { + if (!KeyguardBottomAreaRefactor.isEnabled()) { setKeyguardBottomAreaVisibility(statusBarState, goingToFullShade); } @@ -4582,7 +4582,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump setDozing(true /* dozing */, false /* animate */); mStatusBarStateController.setUpcomingState(KEYGUARD); - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { mStatusBarStateController.setState(KEYGUARD); } else { mStatusBarStateListener.onStateChanged(KEYGUARD); @@ -4645,7 +4645,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump setIsFullWidth(mNotificationStackScrollLayoutController.getWidth() == mView.getWidth()); // Update Clock Pivot (used by anti-burnin transformations) - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mKeyguardStatusViewController.updatePivot(mView.getWidth(), mView.getHeight()); } @@ -4746,7 +4746,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump stackScroller.setMaxAlphaForKeyguard(alpha, "NPVC.setTransitionAlpha()"); } - if (keyguardBottomAreaRefactor()) { + if (KeyguardBottomAreaRefactor.isEnabled()) { mKeyguardInteractor.setAlpha(alpha); } else { mKeyguardBottomAreaInteractor.setAlpha(alpha); @@ -4765,7 +4765,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private Consumer<Float> setTransitionY( NotificationStackScrollLayoutController stackScroller) { return (Float translationY) -> { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mKeyguardStatusViewController.setTranslationY(translationY, /* excludeMedia= */false); stackScroller.setTranslationY(translationY); @@ -4807,7 +4807,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump */ @Override public boolean onInterceptTouchEvent(MotionEvent event) { - if (migrateClocksToBlueprint() && !mUseExternalTouch) { + if (MigrateClocksToBlueprint.isEnabled() && !mUseExternalTouch) { return false; } @@ -4878,7 +4878,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mCentralSurfaces.userActivity(); } mAnimatingOnDown = mHeightAnimator != null && !mIsSpringBackAnimation; @@ -4979,7 +4979,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump */ @Override public boolean onTouchEvent(MotionEvent event) { - if (migrateClocksToBlueprint() && !mUseExternalTouch) { + if (MigrateClocksToBlueprint.isEnabled() && !mUseExternalTouch) { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index e5771785409f..e8e629ca907d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -16,7 +16,6 @@ package com.android.systemui.shade; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED; import static com.android.systemui.flags.Flags.TRACKPAD_GESTURE_COMMON; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; @@ -48,6 +47,7 @@ import com.android.systemui.flags.FeatureFlagsClassic; import com.android.systemui.flags.Flags; import com.android.systemui.keyevent.domain.interactor.SysUIKeyEventHandler; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; @@ -320,7 +320,7 @@ public class NotificationShadeWindowViewController implements Dumpable { mTouchActive = true; mTouchCancelled = false; mDownEvent = ev; - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { mService.userActivity(); } } else if (ev.getActionMasked() == MotionEvent.ACTION_UP @@ -475,7 +475,7 @@ public class NotificationShadeWindowViewController implements Dumpable { && !bouncerShowing && !mStatusBarStateController.isDozing()) { if (mDragDownHelper.isDragDownEnabled()) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { // When on lockscreen, if the touch originates at the top of the screen // go directly to QS and not the shade if (mStatusBarStateController.getState() == KEYGUARD @@ -488,7 +488,7 @@ public class NotificationShadeWindowViewController implements Dumpable { // This handles drag down over lockscreen boolean result = mDragDownHelper.onInterceptTouchEvent(ev); - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { if (result) { mLastInterceptWasDragDownHelper = true; if (ev.getAction() == MotionEvent.ACTION_DOWN) { @@ -511,7 +511,7 @@ public class NotificationShadeWindowViewController implements Dumpable { return true; } } - } else if (migrateClocksToBlueprint()) { + } else if (MigrateClocksToBlueprint.isEnabled()) { // This final check handles swipes on HUNs and when Pulsing if (!bouncerShowing && didNotificationPanelInterceptEvent(ev)) { mShadeLogger.d("NSWVC: intercepted for HUN/PULSING"); @@ -526,7 +526,7 @@ public class NotificationShadeWindowViewController implements Dumpable { MotionEvent cancellation = MotionEvent.obtain(ev); cancellation.setAction(MotionEvent.ACTION_CANCEL); mStackScrollLayout.onInterceptTouchEvent(cancellation); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mNotificationPanelViewController.handleExternalInterceptTouch(cancellation); } cancellation.recycle(); @@ -541,7 +541,7 @@ public class NotificationShadeWindowViewController implements Dumpable { if (mStatusBarKeyguardViewManager.onTouch(ev)) { return true; } - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { if (mLastInterceptWasDragDownHelper && (mDragDownHelper.isDraggingDown())) { // we still want to finish our drag down gesture when locking the screen handled |= mDragDownHelper.onTouchEvent(ev) || handled; @@ -631,7 +631,7 @@ public class NotificationShadeWindowViewController implements Dumpable { } private boolean didNotificationPanelInterceptEvent(MotionEvent ev) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { // Since NotificationStackScrollLayout is now a sibling of notification_panel, we need // to also ask NotificationPanelViewController directly, in order to process swipe up // events originating from notifications diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt index 29de688fa7bf..8b88da1754f0 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt @@ -28,10 +28,10 @@ import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.lifecycle.lifecycleScope import com.android.systemui.Flags.centralizedStatusBarHeightFix -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.fragments.FragmentService +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.navigationbar.NavigationModeController import com.android.systemui.plugins.qs.QS @@ -52,11 +52,12 @@ import javax.inject.Inject import kotlin.reflect.KMutableProperty0 import kotlinx.coroutines.launch -@VisibleForTesting -internal const val INSET_DEBOUNCE_MILLIS = 500L +@VisibleForTesting internal const val INSET_DEBOUNCE_MILLIS = 500L @SysUISingleton -class NotificationsQSContainerController @Inject constructor( +class NotificationsQSContainerController +@Inject +constructor( view: NotificationsQuickSettingsContainer, private val navigationModeController: NavigationModeController, private val overviewProxyService: OverviewProxyService, @@ -64,8 +65,7 @@ class NotificationsQSContainerController @Inject constructor( private val shadeInteractor: ShadeInteractor, private val fragmentService: FragmentService, @Main private val delayableExecutor: DelayableExecutor, - private val - notificationStackScrollLayoutController: NotificationStackScrollLayoutController, + private val notificationStackScrollLayoutController: NotificationStackScrollLayoutController, private val splitShadeStateController: SplitShadeStateController, private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>, ) : ViewController<NotificationsQuickSettingsContainer>(view), QSContainerController { @@ -88,45 +88,48 @@ class NotificationsQSContainerController @Inject constructor( private var isGestureNavigation = true private var taskbarVisible = false - private val taskbarVisibilityListener: OverviewProxyListener = object : OverviewProxyListener { - override fun onTaskbarStatusUpdated(visible: Boolean, stashed: Boolean) { - taskbarVisible = visible + private val taskbarVisibilityListener: OverviewProxyListener = + object : OverviewProxyListener { + override fun onTaskbarStatusUpdated(visible: Boolean, stashed: Boolean) { + taskbarVisible = visible + } } - } // With certain configuration changes (like light/dark changes), the nav bar will disappear // for a bit, causing `bottomStableInsets` to be unstable for some time. Debounce the value // for 500ms. // All interactions with this object happen in the main thread. - private val delayedInsetSetter = object : Runnable, Consumer<WindowInsets> { - private var canceller: Runnable? = null - private var stableInsets = 0 - private var cutoutInsets = 0 - - override fun accept(insets: WindowInsets) { - // when taskbar is visible, stableInsetBottom will include its height - stableInsets = insets.stableInsetBottom - cutoutInsets = insets.displayCutout?.safeInsetBottom ?: 0 - canceller?.run() - canceller = delayableExecutor.executeDelayed(this, INSET_DEBOUNCE_MILLIS) - } + private val delayedInsetSetter = + object : Runnable, Consumer<WindowInsets> { + private var canceller: Runnable? = null + private var stableInsets = 0 + private var cutoutInsets = 0 + + override fun accept(insets: WindowInsets) { + // when taskbar is visible, stableInsetBottom will include its height + stableInsets = insets.stableInsetBottom + cutoutInsets = insets.displayCutout?.safeInsetBottom ?: 0 + canceller?.run() + canceller = delayableExecutor.executeDelayed(this, INSET_DEBOUNCE_MILLIS) + } - override fun run() { - bottomStableInsets = stableInsets - bottomCutoutInsets = cutoutInsets - updateBottomSpacing() + override fun run() { + bottomStableInsets = stableInsets + bottomCutoutInsets = cutoutInsets + updateBottomSpacing() + } } - } override fun onInit() { mView.repeatWhenAttached { lifecycleScope.launch { - shadeInteractor.isQsExpanded.collect{ _ -> mView.invalidate() } + shadeInteractor.isQsExpanded.collect { _ -> mView.invalidate() } } } - val currentMode: Int = navigationModeController.addListener { mode: Int -> - isGestureNavigation = QuickStepContract.isGesturalMode(mode) - } + val currentMode: Int = + navigationModeController.addListener { mode: Int -> + isGestureNavigation = QuickStepContract.isGesturalMode(mode) + } isGestureNavigation = QuickStepContract.isGesturalMode(currentMode) mView.setStackScroller(notificationStackScrollLayoutController.getView()) @@ -151,30 +154,35 @@ class NotificationsQSContainerController @Inject constructor( fun updateResources() { val newSplitShadeEnabled = - splitShadeStateController.shouldUseSplitNotificationShade(resources) + splitShadeStateController.shouldUseSplitNotificationShade(resources) val splitShadeEnabledChanged = newSplitShadeEnabled != splitShadeEnabled splitShadeEnabled = newSplitShadeEnabled largeScreenShadeHeaderActive = LargeScreenUtils.shouldUseLargeScreenShadeHeader(resources) - notificationsBottomMargin = resources.getDimensionPixelSize( - R.dimen.notification_panel_margin_bottom) + notificationsBottomMargin = + resources.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom) largeScreenShadeHeaderHeight = calculateLargeShadeHeaderHeight() shadeHeaderHeight = calculateShadeHeaderHeight() - panelMarginHorizontal = resources.getDimensionPixelSize( - R.dimen.notification_panel_margin_horizontal) - topMargin = if (largeScreenShadeHeaderActive) { - largeScreenShadeHeaderHeight - } else { - resources.getDimensionPixelSize(R.dimen.notification_panel_margin_top) - } + panelMarginHorizontal = + resources.getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal) + topMargin = + if (largeScreenShadeHeaderActive) { + largeScreenShadeHeaderHeight + } else { + resources.getDimensionPixelSize(R.dimen.notification_panel_margin_top) + } updateConstraints() - val scrimMarginChanged = ::scrimShadeBottomMargin.setAndReportChange( - resources.getDimensionPixelSize(R.dimen.split_shade_notifications_scrim_margin_bottom) - ) - val footerOffsetChanged = ::footerActionsOffset.setAndReportChange( - resources.getDimensionPixelSize(R.dimen.qs_footer_action_inset) + - resources.getDimensionPixelSize(R.dimen.qs_footer_actions_bottom_padding) - ) + val scrimMarginChanged = + ::scrimShadeBottomMargin.setAndReportChange( + resources.getDimensionPixelSize( + R.dimen.split_shade_notifications_scrim_margin_bottom + ) + ) + val footerOffsetChanged = + ::footerActionsOffset.setAndReportChange( + resources.getDimensionPixelSize(R.dimen.qs_footer_action_inset) + + resources.getDimensionPixelSize(R.dimen.qs_footer_actions_bottom_padding) + ) val dimensChanged = scrimMarginChanged || footerOffsetChanged if (splitShadeEnabledChanged || dimensChanged) { @@ -198,7 +206,7 @@ class NotificationsQSContainerController @Inject constructor( // 2. carrier_group height (R.dimen.large_screen_shade_header_min_height) // 3. date height (R.dimen.new_qs_header_non_clickable_element_height) val estimatedHeight = - 2 * resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_min_height) + + 2 * resources.getDimensionPixelSize(R.dimen.large_screen_shade_header_min_height) + resources.getDimensionPixelSize(R.dimen.new_qs_header_non_clickable_element_height) return estimatedHeight.coerceAtLeast(minHeight) } @@ -250,16 +258,17 @@ class NotificationsQSContainerController @Inject constructor( containerPadding = 0 stackScrollMargin = bottomStableInsets + notificationsBottomMargin } - val qsContainerPadding = if (!isQSDetailShowing) { - // We also want this padding in the bottom in these cases - if (splitShadeEnabled) { - stackScrollMargin - scrimShadeBottomMargin - footerActionsOffset + val qsContainerPadding = + if (!isQSDetailShowing) { + // We also want this padding in the bottom in these cases + if (splitShadeEnabled) { + stackScrollMargin - scrimShadeBottomMargin - footerActionsOffset + } else { + bottomStableInsets + } } else { - bottomStableInsets + 0 } - } else { - 0 - } return Paddings(containerPadding, stackScrollMargin, qsContainerPadding) } @@ -284,7 +293,7 @@ class NotificationsQSContainerController @Inject constructor( } private fun setNotificationsConstraints(constraintSet: ConstraintSet) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled) { return } val startConstraintId = if (splitShadeEnabled) R.id.qs_edge_guideline else PARENT_ID @@ -309,8 +318,8 @@ class NotificationsQSContainerController @Inject constructor( } private fun setKeyguardStatusViewConstraints(constraintSet: ConstraintSet) { - val statusViewMarginHorizontal = resources.getDimensionPixelSize( - R.dimen.status_view_margin_horizontal) + val statusViewMarginHorizontal = + resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal) constraintSet.apply { setMargin(R.id.keyguard_status_view, START, statusViewMarginHorizontal) setMargin(R.id.keyguard_status_view, END, statusViewMarginHorizontal) diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java index e82f2d3cbd30..13330553b2de 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQuickSettingsContainer.java @@ -18,8 +18,6 @@ package com.android.systemui.shade; import static androidx.constraintlayout.core.widgets.Optimizer.OPTIMIZATION_GRAPH; -import static com.android.systemui.Flags.migrateClocksToBlueprint; - import android.app.Fragment; import android.content.Context; import android.content.res.Configuration; @@ -35,6 +33,7 @@ import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintSet; import com.android.systemui.fragments.FragmentHostManager.FragmentListener; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.plugins.qs.QS; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.AboveShelfObserver; @@ -190,7 +189,7 @@ public class NotificationsQuickSettingsContainer extends ConstraintLayout @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { - if (migrateClocksToBlueprint()) { + if (MigrateClocksToBlueprint.isEnabled()) { return super.drawChild(canvas, child, drawingTime); } int layoutIndex = mLayoutDrawingOrder.indexOf(child); diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java index 8dbceadbb7a8..3a0e1678ff40 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java @@ -21,7 +21,6 @@ import static android.view.WindowInsets.Type.ime; import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE; import static com.android.systemui.Flags.centralizedStatusBarHeightFix; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import static com.android.systemui.classifier.Classifier.QS_COLLAPSE; import static com.android.systemui.shade.NotificationPanelViewController.COUNTER_PANEL_OPEN_QS; import static com.android.systemui.shade.NotificationPanelViewController.FLING_COLLAPSE; @@ -71,6 +70,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.fragments.FragmentHostManager; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.media.controls.domain.pipeline.MediaDataManager; import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager; import com.android.systemui.plugins.FalsingManager; @@ -1778,7 +1778,7 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum // Dragging down on the lockscreen statusbar should prohibit other interactions // immediately, otherwise we'll wait on the touchslop. This is to allow // dragging down to expanded quick settings directly on the lockscreen. - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mPanelView.getParent().requestDisallowInterceptTouchEvent(true); } } @@ -1823,7 +1823,7 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum && Math.abs(h) > Math.abs(x - mInitialTouchX) && shouldQuickSettingsIntercept( mInitialTouchX, mInitialTouchY, h)) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mPanelView.getParent().requestDisallowInterceptTouchEvent(true); } mShadeLog.onQsInterceptMoveQsTrackingEnabled(h); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt index c20efea4700e..6bb1df7daed8 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt @@ -248,7 +248,7 @@ constructor( } override fun onStatusBarTouch(event: MotionEvent) { - // The only call to this doesn't happen with migrateClocksToBlueprint() enabled + // The only call to this doesn't happen with MigrateClocksToBlueprint.isEnabled enabled throw UnsupportedOperationException() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 44068139f66b..e7b159a2d057 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -529,9 +529,9 @@ public class CommandQueue extends IStatusBar.Stub implements default void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {} /** - * @see IStatusBar#enterDesktop(int) + * @see IStatusBar#moveFocusedTaskToDesktop(int) */ - default void enterDesktop(int displayId) {} + default void moveFocusedTaskToDesktop(int displayId) {} } @VisibleForTesting @@ -1444,7 +1444,7 @@ public class CommandQueue extends IStatusBar.Stub implements } @Override - public void enterDesktop(int displayId) { + public void moveFocusedTaskToDesktop(int displayId) { SomeArgs args = SomeArgs.obtain(); args.arg1 = displayId; mHandler.obtainMessage(MSG_ENTER_DESKTOP, args).sendToTarget(); @@ -1960,7 +1960,7 @@ public class CommandQueue extends IStatusBar.Stub implements args = (SomeArgs) msg.obj; int displayId = args.argi1; for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).enterDesktop(displayId); + mCallbacks.get(i).moveFocusedTaskToDesktop(displayId); } break; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java index da8c1bebce92..d6858cad6d0b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java @@ -631,34 +631,41 @@ public final class KeyboardShortcutListSearch { // Enter Split screen with current app to RHS: Meta + Ctrl + Right arrow // Enter Split screen with current app to LHS: Meta + Ctrl + Left arrow // Switch from Split screen to full screen: Meta + Ctrl + Up arrow - String[] shortcutLabels = { - context.getString(R.string.system_multitasking_rhs), - context.getString(R.string.system_multitasking_lhs), - context.getString(R.string.system_multitasking_full_screen), - }; - int[] keyCodes = { - KeyEvent.KEYCODE_DPAD_RIGHT, - KeyEvent.KEYCODE_DPAD_LEFT, - KeyEvent.KEYCODE_DPAD_UP, - }; - - for (int i = 0; i < shortcutLabels.length; i++) { - List<ShortcutKeyGroup> shortcutKeyGroups = Arrays.asList(new ShortcutKeyGroup( - new KeyboardShortcutInfo( - shortcutLabels[i], - keyCodes[i], - KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON), - null)); - ShortcutMultiMappingInfo shortcutMultiMappingInfo = - new ShortcutMultiMappingInfo( - shortcutLabels[i], - null, - shortcutKeyGroups); - systemMultitaskingGroup.addItem(shortcutMultiMappingInfo); - } + // Change split screen focus to RHS: Meta + Alt + Right arrow + // Change split screen focus to LHS: Meta + Alt + Left arrow + systemMultitaskingGroup.addItem( + getMultitaskingShortcut(context.getString(R.string.system_multitasking_rhs), + KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON)); + systemMultitaskingGroup.addItem( + getMultitaskingShortcut(context.getString(R.string.system_multitasking_lhs), + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON)); + systemMultitaskingGroup.addItem( + getMultitaskingShortcut(context.getString(R.string.system_multitasking_full_screen), + KeyEvent.KEYCODE_DPAD_UP, + KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON)); + systemMultitaskingGroup.addItem( + getMultitaskingShortcut( + context.getString(R.string.system_multitasking_splitscreen_focus_rhs), + KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.META_META_ON | KeyEvent.META_ALT_ON)); + systemMultitaskingGroup.addItem( + getMultitaskingShortcut( + context.getString(R.string.system_multitasking_splitscreen_focus_lhs), + KeyEvent.KEYCODE_DPAD_LEFT, + KeyEvent.META_META_ON | KeyEvent.META_ALT_ON)); return systemMultitaskingGroup; } + private static ShortcutMultiMappingInfo getMultitaskingShortcut(String shortcutLabel, + int keycode, int modifiers) { + List<ShortcutKeyGroup> shortcutKeyGroups = Arrays.asList( + new ShortcutKeyGroup(new KeyboardShortcutInfo(shortcutLabel, keycode, modifiers), + null)); + return new ShortcutMultiMappingInfo(shortcutLabel, null, shortcutKeyGroups); + } + private static KeyboardShortcutMultiMappingGroup getMultiMappingInputShortcuts( Context context) { List<ShortcutMultiMappingInfo> shortcutMultiMappingInfoList = Arrays.asList( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt index 4b161260e788..d974bc44bf03 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/LockscreenShadeTransitionController.kt @@ -15,13 +15,13 @@ import androidx.annotation.VisibleForTesting import com.android.systemui.Dumpable import com.android.systemui.ExpandHelper import com.android.systemui.Flags.nsslFalsingFix -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.Gefingerpoken import com.android.systemui.biometrics.UdfpsKeyguardViewControllerLegacy import com.android.systemui.classifier.Classifier import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.domain.interactor.NaturalScrollingSettingObserver import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager @@ -69,7 +69,7 @@ constructor( private val mediaHierarchyManager: MediaHierarchyManager, private val scrimTransitionController: LockscreenShadeScrimTransitionController, private val keyguardTransitionControllerFactory: - LockscreenShadeKeyguardTransitionController.Factory, + LockscreenShadeKeyguardTransitionController.Factory, private val depthController: NotificationShadeDepthController, private val context: Context, private val splitShadeOverScrollerFactory: SplitShadeLockScreenOverScroller.Factory, @@ -292,8 +292,7 @@ constructor( /** @return true if the interaction is accepted, false if it should be cancelled */ internal fun canDragDown(): Boolean { return (statusBarStateController.state == StatusBarState.KEYGUARD || - nsslController.isInLockedDownShade()) && - (isQsFullyCollapsed || useSplitShade) + nsslController.isInLockedDownShade()) && (isQsFullyCollapsed || useSplitShade) } /** Called by the touch helper when when a gesture has completed all the way and released. */ @@ -885,7 +884,7 @@ class DragDownHelper( isDraggingDown = false isTrackpadReverseScroll = false shadeRepository.setLegacyLockscreenShadeTracking(false) - if (nsslFalsingFix() || migrateClocksToBlueprint()) { + if (nsslFalsingFix() || MigrateClocksToBlueprint.isEnabled) { return true } } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 5171a5c9144c..9a82ecf01449 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -863,7 +863,7 @@ public class NotificationShelf extends ActivatableNotificationView { boolean isAppearing = row.isDrawingAppearAnimation() && !row.isInShelf(); iconState.hidden = isAppearing || (view instanceof ExpandableNotificationRow - && ((ExpandableNotificationRow) view).isLowPriority() + && ((ExpandableNotificationRow) view).isMinimized() && mShelfIcons.areIconsOverflowing()) || (transitionAmount == 0.0f && !iconState.isAnimating(icon)) || row.isAboveShelf() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java index e111525285e1..8cdf60b20786 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java @@ -42,7 +42,6 @@ import android.app.Person; import android.app.RemoteInput; import android.app.RemoteInputHistoryItem; import android.content.Context; -import android.content.pm.ShortcutInfo; import android.net.Uri; import android.os.Bundle; import android.os.Parcelable; @@ -133,7 +132,6 @@ public final class NotificationEntry extends ListEntry { public Uri remoteInputUri; public ContentInfo remoteInputAttachment; private Notification.BubbleMetadata mBubbleMetadata; - private ShortcutInfo mShortcutInfo; /** * If {@link RemoteInput#getEditChoicesBeforeSending} is enabled, and the user is @@ -168,10 +166,8 @@ public final class NotificationEntry extends ListEntry { private ListenerSet<OnSensitivityChangedListener> mOnSensitivityChangedListeners = new ListenerSet<>(); - private boolean mAutoHeadsUp; private boolean mPulseSupressed; private int mBucket = BUCKET_ALERTING; - @Nullable private Long mPendingAnimationDuration; private boolean mIsMarkedForUserTriggeredMovement; private boolean mIsHeadsUpEntry; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java index dcfccd8398b2..0bbde21ba6a5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/MediaCoordinator.java @@ -16,7 +16,7 @@ package com.android.systemui.statusbar.notification.collection.coordinator; -import static com.android.systemui.media.controls.domain.pipeline.MediaDataManagerKt.isMediaNotification; +import static com.android.systemui.media.controls.domain.pipeline.MediaDataManager.isMediaNotification; import android.os.RemoteException; import android.service.notification.StatusBarNotification; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java index dfb0f9bb2a87..7a7b18450b48 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java @@ -363,7 +363,7 @@ public class PreparationCoordinator implements Coordinator { NotifInflater.Params getInflaterParams(NotifUiAdjustment adjustment, String reason) { return new NotifInflater.Params( - /* isLowPriority = */ adjustment.isMinimized(), + /* isMinimized = */ adjustment.isMinimized(), /* reason = */ reason, /* showSnooze = */ adjustment.isSnoozeEnabled(), /* isChildInGroup = */ adjustment.isChildInGroup(), diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt index 7b8a062ec446..ff72888a5c26 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt @@ -56,7 +56,7 @@ interface NotifInflater { /** A class holding parameters used when inflating the notification row */ class Params( - val isLowPriority: Boolean, + val isMinimized: Boolean, val reason: String, val showSnooze: Boolean, val isChildInGroup: Boolean = false, 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 4bbe0357b335..4a895c0571d2 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 @@ -243,7 +243,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { @Nullable NotificationRowContentBinder.InflationCallback inflationCallback) { final boolean useIncreasedCollapsedHeight = mMessagingUtil.isImportantMessaging(entry.getSbn(), entry.getImportance()); - final boolean isLowPriority = inflaterParams.isLowPriority(); + final boolean isMinimized = inflaterParams.isMinimized(); // Set show snooze action row.setShowSnooze(inflaterParams.getShowSnooze()); @@ -252,7 +252,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { params.requireContentViews(FLAG_CONTENT_VIEW_CONTRACTED); params.requireContentViews(FLAG_CONTENT_VIEW_EXPANDED); params.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight); - params.setUseLowPriority(isLowPriority); + params.setUseMinimized(isMinimized); if (screenshareNotificationHiding() ? inflaterParams.getNeedsRedaction() @@ -275,7 +275,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { if (AsyncGroupHeaderViewInflation.isEnabled()) { if (inflaterParams.isGroupSummary()) { params.requireContentViews(FLAG_GROUP_SUMMARY_HEADER); - if (isLowPriority) { + if (isMinimized) { params.requireContentViews(FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER); } } else { @@ -288,7 +288,7 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { mRowContentBindStage.requestRebind(entry, en -> { mLogger.logRebindComplete(entry); row.setUsesIncreasedCollapsedHeight(useIncreasedCollapsedHeight); - row.setIsLowPriority(isLowPriority); + row.setIsMinimized(isMinimized); if (inflationCallback != null) { inflationCallback.onAsyncInflationFinished(en); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index c05c3c3df2c9..b8b4a03eae51 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -327,7 +327,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private OnClickListener mExpandClickListener = new OnClickListener() { @Override public void onClick(View v) { - if (!shouldShowPublic() && (!mIsLowPriority || isExpanded()) + if (!shouldShowPublic() && (!mIsMinimized || isExpanded()) && mGroupMembershipManager.isGroupSummary(mEntry)) { mGroupExpansionChanging = true; final boolean wasExpanded = mGroupExpansionManager.isGroupExpanded(mEntry); @@ -382,7 +382,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private boolean mAboveShelf; private OnUserInteractionCallback mOnUserInteractionCallback; private NotificationGutsManager mNotificationGutsManager; - private boolean mIsLowPriority; + private boolean mIsMinimized; private boolean mUseIncreasedCollapsedHeight; private boolean mUseIncreasedHeadsUpHeight; private float mTranslationWhenRemoved; @@ -467,7 +467,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (viewWrapper != null) { setIconAnimationRunningForChild(running, viewWrapper.getIcon()); } - NotificationViewWrapper lowPriWrapper = mChildrenContainer.getLowPriorityViewWrapper(); + NotificationViewWrapper lowPriWrapper = mChildrenContainer + .getMinimizedGroupHeaderWrapper(); if (lowPriWrapper != null) { setIconAnimationRunningForChild(running, lowPriWrapper.getIcon()); } @@ -680,7 +681,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (color != Notification.COLOR_INVALID) { return color; } else { - return mEntry.getContrastedColor(mContext, mIsLowPriority && !isExpanded(), + return mEntry.getContrastedColor(mContext, mIsMinimized && !isExpanded(), getBackgroundColorWithoutTint()); } } @@ -1545,7 +1546,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView * Set the low-priority group notification header view * @param headerView header view to set */ - public void setLowPriorityGroupHeader(NotificationHeaderView headerView) { + public void setMinimizedGroupHeader(NotificationHeaderView headerView) { NotificationChildrenContainer childrenContainer = getChildrenContainerNonNull(); childrenContainer.setLowPriorityGroupHeader( /* headerViewLowPriority= */ headerView, @@ -1664,16 +1665,19 @@ public class ExpandableNotificationRow extends ActivatableNotificationView } } - public void setIsLowPriority(boolean isLowPriority) { - mIsLowPriority = isLowPriority; - mPrivateLayout.setIsLowPriority(isLowPriority); + /** + * Set if the row is minimized. + */ + public void setIsMinimized(boolean isMinimized) { + mIsMinimized = isMinimized; + mPrivateLayout.setIsLowPriority(isMinimized); if (mChildrenContainer != null) { - mChildrenContainer.setIsLowPriority(isLowPriority); + mChildrenContainer.setIsMinimized(isMinimized); } } - public boolean isLowPriority() { - return mIsLowPriority; + public boolean isMinimized() { + return mIsMinimized; } public void setUsesIncreasedCollapsedHeight(boolean use) { @@ -2050,7 +2054,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mChildrenContainerStub = findViewById(R.id.child_container_stub); mChildrenContainerStub.setOnInflateListener((stub, inflated) -> { mChildrenContainer = (NotificationChildrenContainer) inflated; - mChildrenContainer.setIsLowPriority(mIsLowPriority); + mChildrenContainer.setIsMinimized(mIsMinimized); mChildrenContainer.setContainingNotification(ExpandableNotificationRow.this); mChildrenContainer.onNotificationUpdated(); mChildrenContainer.setLogger(mChildrenContainerLogger); @@ -3435,7 +3439,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView private void onExpansionChanged(boolean userAction, boolean wasExpanded) { boolean nowExpanded = isExpanded(); - if (mIsSummaryWithChildren && (!mIsLowPriority || wasExpanded)) { + if (mIsSummaryWithChildren && (!mIsMinimized || wasExpanded)) { nowExpanded = mGroupExpansionManager.isGroupExpanded(mEntry); } if (nowExpanded != wasExpanded) { @@ -3492,7 +3496,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView if (!expandable) { if (mIsSummaryWithChildren) { expandable = true; - if (!mIsLowPriority || isExpanded()) { + if (!mIsMinimized || isExpanded()) { isExpanded = isGroupExpanded(); } } else { 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 f835cca1a60c..ded635cb08bc 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 @@ -150,7 +150,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder entry, mConversationProcessor, row, - bindParams.isLowPriority, + bindParams.isMinimized, bindParams.usesIncreasedHeight, bindParams.usesIncreasedHeadsUpHeight, callback, @@ -178,7 +178,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder SmartReplyStateInflater smartRepliesInflater) { InflationProgress result = createRemoteViews(reInflateFlags, builder, - bindParams.isLowPriority, + bindParams.isMinimized, bindParams.usesIncreasedHeight, bindParams.usesIncreasedHeadsUpHeight, packageContext, @@ -215,6 +215,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder apply( mInflationExecutor, inflateSynchronously, + bindParams.isMinimized, result, reInflateFlags, mRemoteViewCache, @@ -365,7 +366,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder } private static InflationProgress createRemoteViews(@InflationFlag int reInflateFlags, - Notification.Builder builder, boolean isLowPriority, boolean usesIncreasedHeight, + Notification.Builder builder, boolean isMinimized, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, Context packageContext, ExpandableNotificationRow row, NotifLayoutInflaterFactory.Provider notifLayoutInflaterFactoryProvider, @@ -376,13 +377,13 @@ public class NotificationContentInflater implements NotificationRowContentBinder if ((reInflateFlags & FLAG_CONTENT_VIEW_CONTRACTED) != 0) { logger.logAsyncTaskProgress(entryForLogging, "creating contracted remote view"); - result.newContentView = createContentView(builder, isLowPriority, + result.newContentView = createContentView(builder, isMinimized, usesIncreasedHeight); } if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0) { logger.logAsyncTaskProgress(entryForLogging, "creating expanded remote view"); - result.newExpandedView = createExpandedView(builder, isLowPriority); + result.newExpandedView = createExpandedView(builder, isMinimized); } if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0) { @@ -393,7 +394,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder if ((reInflateFlags & FLAG_CONTENT_VIEW_PUBLIC) != 0) { logger.logAsyncTaskProgress(entryForLogging, "creating public remote view"); - result.newPublicView = builder.makePublicContentView(isLowPriority); + result.newPublicView = builder.makePublicContentView(isMinimized); } if (AsyncGroupHeaderViewInflation.isEnabled()) { @@ -406,7 +407,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) { logger.logAsyncTaskProgress(entryForLogging, "creating low-priority group summary remote view"); - result.mNewLowPriorityGroupHeaderView = + result.mNewMinimizedGroupHeaderView = builder.makeLowPriorityContentView(true /* useRegularSubtext */); } } @@ -444,6 +445,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder private static CancellationSignal apply( Executor inflationExecutor, boolean inflateSynchronously, + boolean isMinimized, InflationProgress result, @InflationFlag int reInflateFlags, NotifRemoteViewCache remoteViewCache, @@ -475,7 +477,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder } }; logger.logAsyncTaskProgress(entry, "applying contracted view"); - applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags, flag, + applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, result, + reInflateFlags, flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, privateLayout, privateLayout.getContractedChild(), privateLayout.getVisibleWrapper( @@ -502,7 +505,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder } }; logger.logAsyncTaskProgress(entry, "applying expanded view"); - applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags, + applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, result, + reInflateFlags, flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, privateLayout, privateLayout.getExpandedChild(), privateLayout.getVisibleWrapper(VISIBLE_TYPE_EXPANDED), runningInflations, @@ -529,7 +533,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder } }; logger.logAsyncTaskProgress(entry, "applying heads up view"); - applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags, + applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, + result, reInflateFlags, flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, privateLayout, privateLayout.getHeadsUpChild(), privateLayout.getVisibleWrapper(VISIBLE_TYPE_HEADSUP), runningInflations, @@ -555,7 +560,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder } }; logger.logAsyncTaskProgress(entry, "applying public view"); - applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags, flag, + applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, + result, reInflateFlags, flag, remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, publicLayout, publicLayout.getContractedChild(), publicLayout.getVisibleWrapper(NotificationContentView.VISIBLE_TYPE_CONTRACTED), @@ -583,11 +589,12 @@ public class NotificationContentInflater implements NotificationRowContentBinder } }; logger.logAsyncTaskProgress(entry, "applying group header view"); - applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags, + applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, + result, reInflateFlags, /* inflationId = */ FLAG_GROUP_SUMMARY_HEADER, remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, /* parentLayout = */ childrenContainer, - /* existingView = */ childrenContainer.getNotificationHeader(), + /* existingView = */ childrenContainer.getGroupHeader(), /* existingWrapper = */ childrenContainer.getNotificationHeaderWrapper(), runningInflations, applyCallback, logger); } @@ -595,7 +602,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) { boolean isNewView = !canReapplyRemoteView( - /* newView = */ result.mNewLowPriorityGroupHeaderView, + /* newView = */ result.mNewMinimizedGroupHeaderView, /* oldView = */ remoteViewCache.getCachedView( entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER)); ApplyCallback applyCallback = new ApplyCallback() { @@ -603,29 +610,30 @@ public class NotificationContentInflater implements NotificationRowContentBinder public void setResultView(View v) { logger.logAsyncTaskProgress(entry, "low-priority group header view applied"); - result.mInflatedLowPriorityGroupHeaderView = (NotificationHeaderView) v; + result.mInflatedMinimizedGroupHeaderView = (NotificationHeaderView) v; } @Override public RemoteViews getRemoteView() { - return result.mNewLowPriorityGroupHeaderView; + return result.mNewMinimizedGroupHeaderView; } }; logger.logAsyncTaskProgress(entry, "applying low priority group header view"); - applyRemoteView(inflationExecutor, inflateSynchronously, result, reInflateFlags, + applyRemoteView(inflationExecutor, inflateSynchronously, isMinimized, + result, reInflateFlags, /* inflationId = */ FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER, remoteViewCache, entry, row, isNewView, remoteViewClickHandler, callback, /* parentLayout = */ childrenContainer, - /* existingView = */ childrenContainer.getNotificationHeaderLowPriority(), + /* existingView = */ childrenContainer.getMinimizedNotificationHeader(), /* existingWrapper = */ childrenContainer - .getLowPriorityViewWrapper(), + .getMinimizedGroupHeaderWrapper(), runningInflations, applyCallback, logger); } } // Let's try to finish, maybe nobody is even inflating anything - finishIfDone(result, reInflateFlags, remoteViewCache, runningInflations, callback, entry, - row, logger); + finishIfDone(result, isMinimized, reInflateFlags, remoteViewCache, runningInflations, + callback, entry, row, logger); CancellationSignal cancellationSignal = new CancellationSignal(); cancellationSignal.setOnCancelListener( () -> { @@ -641,6 +649,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder static void applyRemoteView( Executor inflationExecutor, boolean inflateSynchronously, + boolean isMinimized, final InflationProgress result, final @InflationFlag int reInflateFlags, @InflationFlag int inflationId, @@ -707,7 +716,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder existingWrapper.onReinflated(); } runningInflations.remove(inflationId); - finishIfDone(result, reInflateFlags, remoteViewCache, runningInflations, + finishIfDone(result, isMinimized, + reInflateFlags, remoteViewCache, runningInflations, callback, entry, row, logger); } @@ -838,6 +848,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder * @return true if the inflation was finished */ private static boolean finishIfDone(InflationProgress result, + boolean isMinimized, @InflationFlag int reInflateFlags, NotifRemoteViewCache remoteViewCache, HashMap<Integer, CancellationSignal> runningInflations, @Nullable InflationCallback endListener, NotificationEntry entry, @@ -944,7 +955,9 @@ public class NotificationContentInflater implements NotificationRowContentBinder if (AsyncGroupHeaderViewInflation.isEnabled()) { if ((reInflateFlags & FLAG_GROUP_SUMMARY_HEADER) != 0) { if (result.mInflatedGroupHeaderView != null) { - row.setIsLowPriority(false); + // We need to set if the row is minimized before setting the group header to + // make sure the setting of header view works correctly + row.setIsMinimized(isMinimized); row.setGroupHeader(/* headerView= */ result.mInflatedGroupHeaderView); remoteViewCache.putCachedView(entry, FLAG_GROUP_SUMMARY_HEADER, result.mNewGroupHeaderView); @@ -957,13 +970,14 @@ public class NotificationContentInflater implements NotificationRowContentBinder } if ((reInflateFlags & FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) != 0) { - if (result.mInflatedLowPriorityGroupHeaderView != null) { - // New view case, set row to low priority - row.setIsLowPriority(true); - row.setLowPriorityGroupHeader( - /* headerView= */ result.mInflatedLowPriorityGroupHeaderView); + if (result.mInflatedMinimizedGroupHeaderView != null) { + // We need to set if the row is minimized before setting the group header to + // make sure the setting of header view works correctly + row.setIsMinimized(isMinimized); + row.setMinimizedGroupHeader( + /* headerView= */ result.mInflatedMinimizedGroupHeaderView); remoteViewCache.putCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER, - result.mNewLowPriorityGroupHeaderView); + result.mNewMinimizedGroupHeaderView); } else if (remoteViewCache.hasCachedView(entry, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER)) { // Re-inflation case. Only update if it's still cached (i.e. view has not @@ -984,12 +998,12 @@ public class NotificationContentInflater implements NotificationRowContentBinder } private static RemoteViews createExpandedView(Notification.Builder builder, - boolean isLowPriority) { + boolean isMinimized) { RemoteViews bigContentView = builder.createBigContentView(); if (bigContentView != null) { return bigContentView; } - if (isLowPriority) { + if (isMinimized) { RemoteViews contentView = builder.createContentView(); Notification.Builder.makeHeaderExpanded(contentView); return contentView; @@ -998,8 +1012,8 @@ public class NotificationContentInflater implements NotificationRowContentBinder } private static RemoteViews createContentView(Notification.Builder builder, - boolean isLowPriority, boolean useLarge) { - if (isLowPriority) { + boolean isMinimized, boolean useLarge) { + if (isMinimized) { return builder.makeLowPriorityContentView(false /* useRegularSubtext */); } return builder.createContentView(useLarge); @@ -1038,7 +1052,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder private final NotificationEntry mEntry; private final Context mContext; private final boolean mInflateSynchronously; - private final boolean mIsLowPriority; + private final boolean mIsMinimized; private final boolean mUsesIncreasedHeight; private final InflationCallback mCallback; private final boolean mUsesIncreasedHeadsUpHeight; @@ -1063,7 +1077,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder NotificationEntry entry, ConversationNotificationProcessor conversationProcessor, ExpandableNotificationRow row, - boolean isLowPriority, + boolean isMinimized, boolean usesIncreasedHeight, boolean usesIncreasedHeadsUpHeight, InflationCallback callback, @@ -1080,7 +1094,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mRemoteViewCache = cache; mSmartRepliesInflater = smartRepliesInflater; mContext = mRow.getContext(); - mIsLowPriority = isLowPriority; + mIsMinimized = isMinimized; mUsesIncreasedHeight = usesIncreasedHeight; mUsesIncreasedHeadsUpHeight = usesIncreasedHeadsUpHeight; mRemoteViewClickHandler = remoteViewClickHandler; @@ -1150,7 +1164,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mEntry, recoveredBuilder, mLogger); } InflationProgress inflationProgress = createRemoteViews(mReInflateFlags, - recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight, + recoveredBuilder, mIsMinimized, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, packageContext, mRow, mNotifLayoutInflaterFactoryProvider, mLogger); @@ -1209,6 +1223,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder mCancellationSignal = apply( mInflationExecutor, mInflateSynchronously, + mIsMinimized, result, mReInflateFlags, mRemoteViewCache, @@ -1295,7 +1310,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder private RemoteViews newExpandedView; private RemoteViews newPublicView; private RemoteViews mNewGroupHeaderView; - private RemoteViews mNewLowPriorityGroupHeaderView; + private RemoteViews mNewMinimizedGroupHeaderView; @VisibleForTesting Context packageContext; @@ -1305,7 +1320,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder private View inflatedExpandedView; private View inflatedPublicView; private NotificationHeaderView mInflatedGroupHeaderView; - private NotificationHeaderView mInflatedLowPriorityGroupHeaderView; + private NotificationHeaderView mInflatedMinimizedGroupHeaderView; private CharSequence headsUpStatusBarText; private CharSequence headsUpStatusBarTextPublic; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index 8a3e7e8a0580..6f00d96b6312 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -1514,7 +1514,7 @@ public class NotificationContentView extends FrameLayout implements Notification } ImageView bubbleButton = layout.findViewById(com.android.internal.R.id.bubble_button); View actionContainer = layout.findViewById(com.android.internal.R.id.actions_container); - LinearLayout actionListMarginTarget = layout.findViewById( + ViewGroup actionListMarginTarget = layout.findViewById( com.android.internal.R.id.notification_action_list_margin_target); if (bubbleButton == null || actionContainer == null) { return; 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 b0fd47587782..33339a7fe025 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 @@ -128,9 +128,9 @@ public interface NotificationRowContentBinder { class BindParams { /** - * Bind a low priority version of the content views. + * Bind a minimized version of the content views. */ - public boolean isLowPriority; + public boolean isMinimized; /** * Use increased height when binding contracted view. 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 1494c275d061..bae89fbf626f 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 @@ -26,7 +26,7 @@ import com.android.systemui.statusbar.notification.row.NotificationRowContentBin * Parameters for {@link RowContentBindStage}. */ public final class RowContentBindParams { - private boolean mUseLowPriority; + private boolean mUseMinimized; private boolean mUseIncreasedHeight; private boolean mUseIncreasedHeadsUpHeight; private boolean mViewsNeedReinflation; @@ -41,17 +41,20 @@ public final class RowContentBindParams { private @InflationFlag int mDirtyContentViews = mContentViews; /** - * Set whether content should use a low priority version of its content views. + * Set whether content should use a minimized version of its content views. */ - public void setUseLowPriority(boolean useLowPriority) { - if (mUseLowPriority != useLowPriority) { + public void setUseMinimized(boolean useMinimized) { + if (mUseMinimized != useMinimized) { mDirtyContentViews |= (FLAG_CONTENT_VIEW_CONTRACTED | FLAG_CONTENT_VIEW_EXPANDED); } - mUseLowPriority = useLowPriority; + mUseMinimized = useMinimized; } - public boolean useLowPriority() { - return mUseLowPriority; + /** + * @return Whether the row uses the minimized style. + */ + public boolean useMinimized() { + return mUseMinimized; } /** @@ -149,9 +152,9 @@ public final class RowContentBindParams { @Override public String toString() { return String.format("RowContentBindParams[mContentViews=%x mDirtyContentViews=%x " - + "mUseLowPriority=%b mUseIncreasedHeight=%b " + + "mUseMinimized=%b mUseIncreasedHeight=%b " + "mUseIncreasedHeadsUpHeight=%b mViewsNeedReinflation=%b]", - mContentViews, mDirtyContentViews, mUseLowPriority, mUseIncreasedHeight, + mContentViews, mDirtyContentViews, mUseMinimized, mUseIncreasedHeight, mUseIncreasedHeadsUpHeight, mViewsNeedReinflation); } 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 f4f8374d0a9f..89fcda949b5b 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 @@ -73,7 +73,7 @@ public class RowContentBindStage extends BindStage<RowContentBindParams> { mBinder.unbindContent(entry, row, contentToUnbind); BindParams bindParams = new BindParams(); - bindParams.isLowPriority = params.useLowPriority(); + bindParams.isMinimized = params.useMinimized(); bindParams.usesIncreasedHeight = params.useIncreasedHeight(); bindParams.usesIncreasedHeadsUpHeight = params.useIncreasedHeadsUpHeight(); boolean forceInflate = params.needsReinflation(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsHeadsUpRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsHeadsUpRefactor.kt new file mode 100644 index 000000000000..62641fe2f229 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsHeadsUpRefactor.kt @@ -0,0 +1,53 @@ +/* + * 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.shared + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the notifications heads up refactor flag state. */ +@Suppress("NOTHING_TO_INLINE") +object NotificationsHeadsUpRefactor { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_HEADS_UP_REFACTOR + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.notificationsHeadsUpRefactor() + + /** + * 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 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/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index 28f874da0c74..5dc37e0525da 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -110,14 +110,14 @@ public class NotificationChildrenContainer extends ViewGroup */ private boolean mEnableShadowOnChildNotifications; - private NotificationHeaderView mNotificationHeader; - private NotificationHeaderViewWrapper mNotificationHeaderWrapper; - private NotificationHeaderView mNotificationHeaderLowPriority; - private NotificationHeaderViewWrapper mNotificationHeaderWrapperLowPriority; + private NotificationHeaderView mGroupHeader; + private NotificationHeaderViewWrapper mGroupHeaderWrapper; + private NotificationHeaderView mMinimizedGroupHeader; + private NotificationHeaderViewWrapper mMinimizedGroupHeaderWrapper; private NotificationGroupingUtil mGroupingUtil; private ViewState mHeaderViewState; private int mClipBottomAmount; - private boolean mIsLowPriority; + private boolean mIsMinimized; private OnClickListener mHeaderClickListener; private ViewGroup mCurrentHeader; private boolean mIsConversation; @@ -217,14 +217,14 @@ public class NotificationChildrenContainer extends ViewGroup int right = left + mOverflowNumber.getMeasuredWidth(); mOverflowNumber.layout(left, 0, right, mOverflowNumber.getMeasuredHeight()); } - if (mNotificationHeader != null) { - mNotificationHeader.layout(0, 0, mNotificationHeader.getMeasuredWidth(), - mNotificationHeader.getMeasuredHeight()); + if (mGroupHeader != null) { + mGroupHeader.layout(0, 0, mGroupHeader.getMeasuredWidth(), + mGroupHeader.getMeasuredHeight()); } - if (mNotificationHeaderLowPriority != null) { - mNotificationHeaderLowPriority.layout(0, 0, - mNotificationHeaderLowPriority.getMeasuredWidth(), - mNotificationHeaderLowPriority.getMeasuredHeight()); + if (mMinimizedGroupHeader != null) { + mMinimizedGroupHeader.layout(0, 0, + mMinimizedGroupHeader.getMeasuredWidth(), + mMinimizedGroupHeader.getMeasuredHeight()); } } @@ -271,11 +271,11 @@ public class NotificationChildrenContainer extends ViewGroup } int headerHeightSpec = MeasureSpec.makeMeasureSpec(mHeaderHeight, MeasureSpec.EXACTLY); - if (mNotificationHeader != null) { - mNotificationHeader.measure(widthMeasureSpec, headerHeightSpec); + if (mGroupHeader != null) { + mGroupHeader.measure(widthMeasureSpec, headerHeightSpec); } - if (mNotificationHeaderLowPriority != null) { - mNotificationHeaderLowPriority.measure(widthMeasureSpec, headerHeightSpec); + if (mMinimizedGroupHeader != null) { + mMinimizedGroupHeader.measure(widthMeasureSpec, headerHeightSpec); } setMeasuredDimension(width, height); @@ -308,11 +308,11 @@ public class NotificationChildrenContainer extends ViewGroup * appropriately. */ public void setNotificationGroupWhen(long whenMillis) { - if (mNotificationHeaderWrapper != null) { - mNotificationHeaderWrapper.setNotificationWhen(whenMillis); + if (mGroupHeaderWrapper != null) { + mGroupHeaderWrapper.setNotificationWhen(whenMillis); } - if (mNotificationHeaderWrapperLowPriority != null) { - mNotificationHeaderWrapperLowPriority.setNotificationWhen(whenMillis); + if (mMinimizedGroupHeaderWrapper != null) { + mMinimizedGroupHeaderWrapper.setNotificationWhen(whenMillis); } } @@ -410,28 +410,28 @@ public class NotificationChildrenContainer extends ViewGroup Trace.beginSection("recreateHeader#makeNotificationGroupHeader"); RemoteViews header = builder.makeNotificationGroupHeader(); Trace.endSection(); - if (mNotificationHeader == null) { + if (mGroupHeader == null) { Trace.beginSection("recreateHeader#apply"); - mNotificationHeader = (NotificationHeaderView) header.apply(getContext(), this); + mGroupHeader = (NotificationHeaderView) header.apply(getContext(), this); Trace.endSection(); - mNotificationHeader.findViewById(com.android.internal.R.id.expand_button) + mGroupHeader.findViewById(com.android.internal.R.id.expand_button) .setVisibility(VISIBLE); - mNotificationHeader.setOnClickListener(mHeaderClickListener); - mNotificationHeaderWrapper = + mGroupHeader.setOnClickListener(mHeaderClickListener); + mGroupHeaderWrapper = (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap( getContext(), - mNotificationHeader, + mGroupHeader, mContainingNotification); - mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); - addView(mNotificationHeader, 0); + mGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); + addView(mGroupHeader, 0); invalidate(); } else { Trace.beginSection("recreateHeader#reapply"); - header.reapply(getContext(), mNotificationHeader); + header.reapply(getContext(), mGroupHeader); Trace.endSection(); } - mNotificationHeaderWrapper.setExpanded(mChildrenExpanded); - mNotificationHeaderWrapper.onContentUpdated(mContainingNotification); + mGroupHeaderWrapper.setExpanded(mChildrenExpanded); + mGroupHeaderWrapper.onContentUpdated(mContainingNotification); recreateLowPriorityHeader(builder, isConversation); updateHeaderVisibility(false /* animate */); updateChildrenAppearance(); @@ -439,21 +439,21 @@ public class NotificationChildrenContainer extends ViewGroup } private void removeGroupHeader() { - if (mNotificationHeader == null) { + if (mGroupHeader == null) { return; } - removeView(mNotificationHeader); - mNotificationHeader = null; - mNotificationHeaderWrapper = null; + removeView(mGroupHeader); + mGroupHeader = null; + mGroupHeaderWrapper = null; } private void removeLowPriorityGroupHeader() { - if (mNotificationHeaderLowPriority == null) { + if (mMinimizedGroupHeader == null) { return; } - removeView(mNotificationHeaderLowPriority); - mNotificationHeaderLowPriority = null; - mNotificationHeaderWrapperLowPriority = null; + removeView(mMinimizedGroupHeader); + mMinimizedGroupHeader = null; + mMinimizedGroupHeaderWrapper = null; } /** @@ -474,21 +474,21 @@ public class NotificationChildrenContainer extends ViewGroup return; } - mNotificationHeader = headerView; - mNotificationHeader.findViewById(com.android.internal.R.id.expand_button) + mGroupHeader = headerView; + mGroupHeader.findViewById(com.android.internal.R.id.expand_button) .setVisibility(VISIBLE); - mNotificationHeader.setOnClickListener(mHeaderClickListener); - mNotificationHeaderWrapper = + mGroupHeader.setOnClickListener(mHeaderClickListener); + mGroupHeaderWrapper = (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap( getContext(), - mNotificationHeader, + mGroupHeader, mContainingNotification); - mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); - addView(mNotificationHeader, 0); + mGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); + addView(mGroupHeader, 0); invalidate(); - mNotificationHeaderWrapper.setExpanded(mChildrenExpanded); - mNotificationHeaderWrapper.onContentUpdated(mContainingNotification); + mGroupHeaderWrapper.setExpanded(mChildrenExpanded); + mGroupHeaderWrapper.onContentUpdated(mContainingNotification); updateHeaderVisibility(false /* animate */); updateChildrenAppearance(); @@ -511,20 +511,20 @@ public class NotificationChildrenContainer extends ViewGroup return; } - mNotificationHeaderLowPriority = headerViewLowPriority; - mNotificationHeaderLowPriority.findViewById(com.android.internal.R.id.expand_button) + mMinimizedGroupHeader = headerViewLowPriority; + mMinimizedGroupHeader.findViewById(com.android.internal.R.id.expand_button) .setVisibility(VISIBLE); - mNotificationHeaderLowPriority.setOnClickListener(onClickListener); - mNotificationHeaderWrapperLowPriority = + mMinimizedGroupHeader.setOnClickListener(onClickListener); + mMinimizedGroupHeaderWrapper = (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap( getContext(), - mNotificationHeaderLowPriority, + mMinimizedGroupHeader, mContainingNotification); - mNotificationHeaderWrapperLowPriority.setOnRoundnessChangedListener(this::invalidate); - addView(mNotificationHeaderLowPriority, 0); + mMinimizedGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); + addView(mMinimizedGroupHeader, 0); invalidate(); - mNotificationHeaderWrapperLowPriority.onContentUpdated(mContainingNotification); + mMinimizedGroupHeaderWrapper.onContentUpdated(mContainingNotification); updateHeaderVisibility(false /* animate */); updateChildrenAppearance(); } @@ -539,35 +539,35 @@ public class NotificationChildrenContainer extends ViewGroup AsyncGroupHeaderViewInflation.assertInLegacyMode(); RemoteViews header; StatusBarNotification notification = mContainingNotification.getEntry().getSbn(); - if (mIsLowPriority) { + if (mIsMinimized) { if (builder == null) { builder = Notification.Builder.recoverBuilder(getContext(), notification.getNotification()); } header = builder.makeLowPriorityContentView(true /* useRegularSubtext */); - if (mNotificationHeaderLowPriority == null) { - mNotificationHeaderLowPriority = (NotificationHeaderView) header.apply(getContext(), + if (mMinimizedGroupHeader == null) { + mMinimizedGroupHeader = (NotificationHeaderView) header.apply(getContext(), this); - mNotificationHeaderLowPriority.findViewById(com.android.internal.R.id.expand_button) + mMinimizedGroupHeader.findViewById(com.android.internal.R.id.expand_button) .setVisibility(VISIBLE); - mNotificationHeaderLowPriority.setOnClickListener(mHeaderClickListener); - mNotificationHeaderWrapperLowPriority = + mMinimizedGroupHeader.setOnClickListener(mHeaderClickListener); + mMinimizedGroupHeaderWrapper = (NotificationHeaderViewWrapper) NotificationViewWrapper.wrap( getContext(), - mNotificationHeaderLowPriority, + mMinimizedGroupHeader, mContainingNotification); - mNotificationHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); - addView(mNotificationHeaderLowPriority, 0); + mGroupHeaderWrapper.setOnRoundnessChangedListener(this::invalidate); + addView(mMinimizedGroupHeader, 0); invalidate(); } else { - header.reapply(getContext(), mNotificationHeaderLowPriority); + header.reapply(getContext(), mMinimizedGroupHeader); } - mNotificationHeaderWrapperLowPriority.onContentUpdated(mContainingNotification); - resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, calculateDesiredHeader()); + mMinimizedGroupHeaderWrapper.onContentUpdated(mContainingNotification); + resetHeaderVisibilityIfNeeded(mMinimizedGroupHeader, calculateDesiredHeader()); } else { - removeView(mNotificationHeaderLowPriority); - mNotificationHeaderLowPriority = null; - mNotificationHeaderWrapperLowPriority = null; + removeView(mMinimizedGroupHeader); + mMinimizedGroupHeader = null; + mMinimizedGroupHeaderWrapper = null; } } @@ -588,8 +588,8 @@ public class NotificationChildrenContainer extends ViewGroup public void updateGroupOverflow() { if (mShowGroupCountInExpander) { - setExpandButtonNumber(mNotificationHeaderWrapper); - setExpandButtonNumber(mNotificationHeaderWrapperLowPriority); + setExpandButtonNumber(mGroupHeaderWrapper); + setExpandButtonNumber(mMinimizedGroupHeaderWrapper); return; } int maxAllowedVisibleChildren = getMaxAllowedVisibleChildren(true /* likeCollapsed */); @@ -641,9 +641,9 @@ public class NotificationChildrenContainer extends ViewGroup * @param alpha alpha value to apply to the content */ public void setContentAlpha(float alpha) { - if (mNotificationHeader != null) { - for (int i = 0; i < mNotificationHeader.getChildCount(); i++) { - mNotificationHeader.getChildAt(i).setAlpha(alpha); + if (mGroupHeader != null) { + for (int i = 0; i < mGroupHeader.getChildCount(); i++) { + mGroupHeader.getChildAt(i).setAlpha(alpha); } } for (ExpandableNotificationRow child : getAttachedChildren()) { @@ -683,7 +683,7 @@ public class NotificationChildrenContainer extends ViewGroup if (AsyncGroupHeaderViewInflation.isEnabled()) { return mHeaderHeight; } else { - return mNotificationHeaderLowPriority.getHeight(); + return mMinimizedGroupHeader.getHeight(); } } int intrinsicHeight = mNotificationHeaderMargin + mCurrentHeaderTranslation; @@ -837,15 +837,15 @@ public class NotificationChildrenContainer extends ViewGroup mGroupOverFlowState.setAlpha(0.0f); } } - if (mNotificationHeader != null) { + if (mGroupHeader != null) { if (mHeaderViewState == null) { mHeaderViewState = new ViewState(); } - mHeaderViewState.initFrom(mNotificationHeader); + mHeaderViewState.initFrom(mGroupHeader); if (mContainingNotification.hasExpandingChild()) { // Not modifying translationZ during expand animation. - mHeaderViewState.setZTranslation(mNotificationHeader.getTranslationZ()); + mHeaderViewState.setZTranslation(mGroupHeader.getTranslationZ()); } else if (childrenExpandedAndNotAnimating) { mHeaderViewState.setZTranslation(parentState.getZTranslation()); } else { @@ -898,7 +898,7 @@ public class NotificationChildrenContainer extends ViewGroup && !showingAsLowPriority()) { return NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED; } - if (mIsLowPriority + if (mIsMinimized || (!mContainingNotification.isOnKeyguard() && mContainingNotification.isExpanded()) || (mContainingNotification.isHeadsUpState() && mContainingNotification.canShowHeadsUp())) { @@ -946,7 +946,7 @@ public class NotificationChildrenContainer extends ViewGroup mNeverAppliedGroupState = false; } if (mHeaderViewState != null) { - mHeaderViewState.applyToView(mNotificationHeader); + mHeaderViewState.applyToView(mGroupHeader); } updateChildrenClipping(); } @@ -1006,8 +1006,8 @@ public class NotificationChildrenContainer extends ViewGroup } if (child instanceof NotificationHeaderView - && mNotificationHeaderWrapper.hasRoundedCorner()) { - float[] radii = mNotificationHeaderWrapper.getUpdatedRadii(); + && mGroupHeaderWrapper.hasRoundedCorner()) { + float[] radii = mGroupHeaderWrapper.getUpdatedRadii(); mHeaderPath.reset(); mHeaderPath.addRoundRect( child.getLeft(), @@ -1085,8 +1085,8 @@ public class NotificationChildrenContainer extends ViewGroup } mGroupOverFlowState.animateTo(mOverflowNumber, properties); } - if (mNotificationHeader != null) { - mHeaderViewState.applyToView(mNotificationHeader); + if (mGroupHeader != null) { + mHeaderViewState.applyToView(mGroupHeader); } updateChildrenClipping(); } @@ -1109,8 +1109,8 @@ public class NotificationChildrenContainer extends ViewGroup public void setChildrenExpanded(boolean childrenExpanded) { mChildrenExpanded = childrenExpanded; updateExpansionStates(); - if (mNotificationHeaderWrapper != null) { - mNotificationHeaderWrapper.setExpanded(childrenExpanded); + if (mGroupHeaderWrapper != null) { + mGroupHeaderWrapper.setExpanded(childrenExpanded); } final int count = mAttachedChildren.size(); for (int childIdx = 0; childIdx < count; childIdx++) { @@ -1130,11 +1130,11 @@ public class NotificationChildrenContainer extends ViewGroup } public NotificationViewWrapper getNotificationViewWrapper() { - return mNotificationHeaderWrapper; + return mGroupHeaderWrapper; } - public NotificationViewWrapper getLowPriorityViewWrapper() { - return mNotificationHeaderWrapperLowPriority; + public NotificationViewWrapper getMinimizedGroupHeaderWrapper() { + return mMinimizedGroupHeaderWrapper; } @VisibleForTesting @@ -1142,12 +1142,12 @@ public class NotificationChildrenContainer extends ViewGroup return mCurrentHeader; } - public NotificationHeaderView getNotificationHeader() { - return mNotificationHeader; + public NotificationHeaderView getGroupHeader() { + return mGroupHeader; } - public NotificationHeaderView getNotificationHeaderLowPriority() { - return mNotificationHeaderLowPriority; + public NotificationHeaderView getMinimizedNotificationHeader() { + return mMinimizedGroupHeader; } private void updateHeaderVisibility(boolean animate) { @@ -1171,7 +1171,7 @@ public class NotificationChildrenContainer extends ViewGroup NotificationViewWrapper hiddenWrapper = getWrapperForView(currentHeader); visibleWrapper.transformFrom(hiddenWrapper); hiddenWrapper.transformTo(visibleWrapper, () -> updateHeaderVisibility(false)); - startChildAlphaAnimations(desiredHeader == mNotificationHeader); + startChildAlphaAnimations(desiredHeader == mGroupHeader); } else { animate = false; } @@ -1192,8 +1192,8 @@ public class NotificationChildrenContainer extends ViewGroup } } - resetHeaderVisibilityIfNeeded(mNotificationHeader, desiredHeader); - resetHeaderVisibilityIfNeeded(mNotificationHeaderLowPriority, desiredHeader); + resetHeaderVisibilityIfNeeded(mGroupHeader, desiredHeader); + resetHeaderVisibilityIfNeeded(mMinimizedGroupHeader, desiredHeader); mCurrentHeader = desiredHeader; } @@ -1215,9 +1215,9 @@ public class NotificationChildrenContainer extends ViewGroup private ViewGroup calculateDesiredHeader() { ViewGroup desiredHeader; if (showingAsLowPriority()) { - desiredHeader = mNotificationHeaderLowPriority; + desiredHeader = mMinimizedGroupHeader; } else { - desiredHeader = mNotificationHeader; + desiredHeader = mGroupHeader; } return desiredHeader; } @@ -1244,20 +1244,20 @@ public class NotificationChildrenContainer extends ViewGroup private void updateHeaderTransformation() { if (mUserLocked && showingAsLowPriority()) { float fraction = getGroupExpandFraction(); - mNotificationHeaderWrapper.transformFrom(mNotificationHeaderWrapperLowPriority, + mGroupHeaderWrapper.transformFrom(mMinimizedGroupHeaderWrapper, fraction); - mNotificationHeader.setVisibility(VISIBLE); - mNotificationHeaderWrapperLowPriority.transformTo(mNotificationHeaderWrapper, + mGroupHeader.setVisibility(VISIBLE); + mMinimizedGroupHeaderWrapper.transformTo(mGroupHeaderWrapper, fraction); } } private NotificationViewWrapper getWrapperForView(View visibleHeader) { - if (visibleHeader == mNotificationHeader) { - return mNotificationHeaderWrapper; + if (visibleHeader == mGroupHeader) { + return mGroupHeaderWrapper; } - return mNotificationHeaderWrapperLowPriority; + return mMinimizedGroupHeaderWrapper; } /** @@ -1266,13 +1266,13 @@ public class NotificationChildrenContainer extends ViewGroup * @param expanded whether the group is expanded. */ public void updateHeaderForExpansion(boolean expanded) { - if (mNotificationHeader != null) { + if (mGroupHeader != null) { if (expanded) { ColorDrawable cd = new ColorDrawable(); cd.setColor(mContainingNotification.calculateBgColor()); - mNotificationHeader.setHeaderBackgroundDrawable(cd); + mGroupHeader.setHeaderBackgroundDrawable(cd); } else { - mNotificationHeader.setHeaderBackgroundDrawable(null); + mGroupHeader.setHeaderBackgroundDrawable(null); } } } @@ -1405,11 +1405,11 @@ public class NotificationChildrenContainer extends ViewGroup if (AsyncGroupHeaderViewInflation.isEnabled()) { return mHeaderHeight; } - if (mNotificationHeaderLowPriority == null) { + if (mMinimizedGroupHeader == null) { Log.e(TAG, "getMinHeight: low priority header is null", new Exception()); return 0; } - return mNotificationHeaderLowPriority.getHeight(); + return mMinimizedGroupHeader.getHeight(); } int minExpandHeight = mNotificationHeaderMargin + headerTranslation; int visibleChildren = 0; @@ -1443,20 +1443,20 @@ public class NotificationChildrenContainer extends ViewGroup } public boolean showingAsLowPriority() { - return mIsLowPriority && !mContainingNotification.isExpanded(); + return mIsMinimized && !mContainingNotification.isExpanded(); } public void reInflateViews(OnClickListener listener, StatusBarNotification notification) { if (!AsyncGroupHeaderViewInflation.isEnabled()) { // When Async header inflation is enabled, we do not reinflate headers because they are // inflated from the background thread - if (mNotificationHeader != null) { - removeView(mNotificationHeader); - mNotificationHeader = null; + if (mGroupHeader != null) { + removeView(mGroupHeader); + mGroupHeader = null; } - if (mNotificationHeaderLowPriority != null) { - removeView(mNotificationHeaderLowPriority); - mNotificationHeaderLowPriority = null; + if (mMinimizedGroupHeader != null) { + removeView(mMinimizedGroupHeader); + mMinimizedGroupHeader = null; } recreateNotificationHeader(listener, mIsConversation); } @@ -1489,8 +1489,8 @@ public class NotificationChildrenContainer extends ViewGroup } private void updateHeaderTouchability() { - if (mNotificationHeader != null) { - mNotificationHeader.setAcceptAllTouches(mChildrenExpanded || mUserLocked); + if (mGroupHeader != null) { + mGroupHeader.setAcceptAllTouches(mChildrenExpanded || mUserLocked); } } @@ -1534,8 +1534,11 @@ public class NotificationChildrenContainer extends ViewGroup updateChildrenClipping(); } - public void setIsLowPriority(boolean isLowPriority) { - mIsLowPriority = isLowPriority; + /** + * Set whether the children container is minimized. + */ + public void setIsMinimized(boolean isMinimized) { + mIsMinimized = isMinimized; if (mContainingNotification != null) { /* we're not yet set up yet otherwise */ if (!AsyncGroupHeaderViewInflation.isEnabled()) { recreateLowPriorityHeader(null /* existingBuilder */, mIsConversation); @@ -1552,13 +1555,13 @@ public class NotificationChildrenContainer extends ViewGroup */ public NotificationViewWrapper getVisibleWrapper() { if (showingAsLowPriority()) { - return mNotificationHeaderWrapperLowPriority; + return mMinimizedGroupHeaderWrapper; } - return mNotificationHeaderWrapper; + return mGroupHeaderWrapper; } public void onExpansionChanged() { - if (mIsLowPriority) { + if (mIsMinimized) { if (mUserLocked) { setUserLocked(mUserLocked); } @@ -1574,15 +1577,15 @@ public class NotificationChildrenContainer extends ViewGroup @Override public void applyRoundnessAndInvalidate() { boolean last = true; - if (mNotificationHeaderWrapper != null) { - mNotificationHeaderWrapper.requestTopRoundness( + if (mGroupHeaderWrapper != null) { + mGroupHeaderWrapper.requestTopRoundness( /* value = */ getTopRoundness(), /* sourceType = */ FROM_PARENT, /* animate = */ false ); } - if (mNotificationHeaderWrapperLowPriority != null) { - mNotificationHeaderWrapperLowPriority.requestTopRoundness( + if (mMinimizedGroupHeaderWrapper != null) { + mMinimizedGroupHeaderWrapper.requestTopRoundness( /* value = */ getTopRoundness(), /* sourceType = */ FROM_PARENT, /* animate = */ false @@ -1612,31 +1615,31 @@ public class NotificationChildrenContainer extends ViewGroup * Shows the given feedback icon, or hides the icon if null. */ public void setFeedbackIcon(@Nullable FeedbackIcon icon) { - if (mNotificationHeaderWrapper != null) { - mNotificationHeaderWrapper.setFeedbackIcon(icon); + if (mGroupHeaderWrapper != null) { + mGroupHeaderWrapper.setFeedbackIcon(icon); } - if (mNotificationHeaderWrapperLowPriority != null) { - mNotificationHeaderWrapperLowPriority.setFeedbackIcon(icon); + if (mMinimizedGroupHeaderWrapper != null) { + mMinimizedGroupHeaderWrapper.setFeedbackIcon(icon); } } public void setRecentlyAudiblyAlerted(boolean audiblyAlertedRecently) { - if (mNotificationHeaderWrapper != null) { - mNotificationHeaderWrapper.setRecentlyAudiblyAlerted(audiblyAlertedRecently); + if (mGroupHeaderWrapper != null) { + mGroupHeaderWrapper.setRecentlyAudiblyAlerted(audiblyAlertedRecently); } - if (mNotificationHeaderWrapperLowPriority != null) { - mNotificationHeaderWrapperLowPriority.setRecentlyAudiblyAlerted(audiblyAlertedRecently); + if (mMinimizedGroupHeaderWrapper != null) { + mMinimizedGroupHeaderWrapper.setRecentlyAudiblyAlerted(audiblyAlertedRecently); } } @Override public void setNotificationFaded(boolean faded) { mContainingNotificationIsFaded = faded; - if (mNotificationHeaderWrapper != null) { - mNotificationHeaderWrapper.setNotificationFaded(faded); + if (mGroupHeaderWrapper != null) { + mGroupHeaderWrapper.setNotificationFaded(faded); } - if (mNotificationHeaderWrapperLowPriority != null) { - mNotificationHeaderWrapperLowPriority.setNotificationFaded(faded); + if (mMinimizedGroupHeaderWrapper != null) { + mMinimizedGroupHeaderWrapper.setNotificationFaded(faded); } for (ExpandableNotificationRow child : mAttachedChildren) { child.setNotificationFaded(faded); @@ -1654,7 +1657,7 @@ public class NotificationChildrenContainer extends ViewGroup } public NotificationHeaderViewWrapper getNotificationHeaderWrapper() { - return mNotificationHeaderWrapper; + return mGroupHeaderWrapper; } public void setLogger(NotificationChildrenContainerLogger logger) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 8ed1ca28eaf1..2f577d02c903 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -23,7 +23,6 @@ import static com.android.app.animation.Interpolators.STANDARD; import static com.android.internal.jank.InteractionJankMonitor.CUJ_NOTIFICATION_SHADE_SCROLL_FLING; import static com.android.server.notification.Flags.screenshareNotificationHiding; import static com.android.systemui.Dependency.ALLOW_NOTIFICATION_LONG_PRESS_NAME; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import static com.android.systemui.Flags.nsslFalsingFix; import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.OnEmptySpaceClickListener; @@ -71,6 +70,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlagsClassic; import com.android.systemui.flags.Flags; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository; import com.android.systemui.keyguard.shared.model.KeyguardState; import com.android.systemui.keyguard.shared.model.TransitionStep; @@ -2090,7 +2090,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } boolean horizontalSwipeWantsIt = false; boolean scrollerWantsIt = false; - if (nsslFalsingFix() || migrateClocksToBlueprint()) { + if (nsslFalsingFix() || MigrateClocksToBlueprint.isEnabled()) { // Reverse the order relative to the else statement. onScrollTouch will reset on an // UP event, causing horizontalSwipeWantsIt to be set to true on vertical swipes. if (mLongPressedView == null && !mView.isBeingDragged() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 9b1952ba63fd..b42c07d2c93c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -53,9 +53,7 @@ public class StackScrollAlgorithm { public static final float START_FRACTION = 0.5f; private static final String TAG = "StackScrollAlgorithm"; - private static final Boolean DEBUG = false; private static final SourceType STACK_SCROLL_ALGO = SourceType.from("StackScrollAlgorithm"); - private final ViewGroup mHostView; private float mPaddingBetweenElements; private float mGapHeight; @@ -247,13 +245,11 @@ public class StackScrollAlgorithm { >= ambientState.getMaxHeadsUpTranslation(); } - public static void log(String s) { - if (DEBUG) { - android.util.Log.i(TAG, s); - } + public static void debugLog(String s) { + android.util.Log.i(TAG, s); } - public static void logView(View view, String s) { + public static void debugLogView(View view, String s) { String viewString = ""; if (view instanceof ExpandableNotificationRow row) { if (row.getEntry() == null) { @@ -274,7 +270,7 @@ public class StackScrollAlgorithm { } else { viewString = view.toString(); } - log(viewString + " " + s); + debugLog(viewString + " " + s); } private void resetChildViewStates() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt index 9fffb66ac831..79ba25e1e23e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt @@ -19,7 +19,6 @@ package com.android.systemui.statusbar.notification.stack.data.repository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds -import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -29,10 +28,6 @@ class NotificationStackAppearanceRepository @Inject constructor() { /** The bounds of the notification stack in the current scene. */ val stackBounds = MutableStateFlow(StackBounds()) - /** The whether the corners of the notification stack should be rounded */ - // TODO: replace with the logic from QSController - val stackRounding = MutableStateFlow(StackRounding(roundTop = true, roundBottom = false)) - /** * The height in px of the contents of notification stack. Depending on the number of * notifications, this can exceed the space available on screen to show notifications, at which diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt index 5a56ca1444dc..f05d01717a44 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.notification.stack.domain.interactor import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.statusbar.notification.stack.data.repository.NotificationStackAppearanceRepository import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding @@ -25,6 +27,9 @@ import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf /** An interactor which controls the appearance of the NSSL */ @SysUISingleton @@ -32,12 +37,30 @@ class NotificationStackAppearanceInteractor @Inject constructor( private val repository: NotificationStackAppearanceRepository, + shadeInteractor: ShadeInteractor, ) { /** The bounds of the notification stack in the current scene. */ val stackBounds: StateFlow<StackBounds> = repository.stackBounds.asStateFlow() + /** + * Whether the stack is expanding from GONE-with-HUN to SHADE + * + * TODO(b/296118689): implement this to match legacy QSController logic + */ + private val isExpandingFromHeadsUp: Flow<Boolean> = flowOf(false) + /** The rounding of the notification stack. */ - val stackRounding: StateFlow<StackRounding> = repository.stackRounding.asStateFlow() + val stackRounding: Flow<StackRounding> = + combine( + shadeInteractor.shadeMode, + isExpandingFromHeadsUp, + ) { shadeMode, isExpandingFromHeadsUp -> + StackRounding( + roundTop = !(shadeMode == ShadeMode.Split && isExpandingFromHeadsUp), + roundBottom = shadeMode != ShadeMode.Single, + ) + } + .distinctUntilChanged() /** * The height in px of the contents of notification stack. Depending on the number of diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt deleted file mode 100644 index 189c5e03ce07..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.notification.stack.ui.viewbinder - -import android.content.Context -import android.util.TypedValue -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.statusbar.notification.stack.AmbientState -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController -import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel -import kotlin.math.roundToInt -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.DisposableHandle -import kotlinx.coroutines.launch - -/** Binds the shared notification container to its view-model. */ -object NotificationStackAppearanceViewBinder { - const val SCRIM_CORNER_RADIUS = 32f - - @JvmStatic - fun bind( - context: Context, - view: SharedNotificationContainer, - viewModel: NotificationStackAppearanceViewModel, - ambientState: AmbientState, - controller: NotificationStackScrollLayoutController, - @Main mainImmediateDispatcher: CoroutineDispatcher, - ): DisposableHandle { - return view.repeatWhenAttached(mainImmediateDispatcher) { - repeatOnLifecycle(Lifecycle.State.CREATED) { - launch { - viewModel.stackClipping.collect { (bounds, rounding) -> - val viewLeft = controller.view.left - val viewTop = controller.view.top - val roundRadius = SCRIM_CORNER_RADIUS.dpToPx(context) - controller.setRoundedClippingBounds( - bounds.left.roundToInt() - viewLeft, - bounds.top.roundToInt() - viewTop, - bounds.right.roundToInt() - viewLeft, - bounds.bottom.roundToInt() - viewTop, - if (rounding.roundTop) roundRadius else 0, - if (rounding.roundBottom) roundRadius else 0, - ) - } - } - - launch { - viewModel.contentTop.collect { - controller.updateTopPadding(it, controller.isAddOrRemoveAnimationPending) - } - } - - launch { - var wasExpanding = false - viewModel.expandFraction.collect { expandFraction -> - val nowExpanding = expandFraction != 0f && expandFraction != 1f - if (nowExpanding && !wasExpanding) { - controller.onExpansionStarted() - } - ambientState.expansionFraction = expandFraction - controller.expandedHeight = expandFraction * controller.view.height - if (!nowExpanding && wasExpanding) { - controller.onExpansionStopped() - } - wasExpanding = nowExpanding - } - } - - launch { viewModel.isScrollable.collect { controller.setScrollingEnabled(it) } } - } - } - } - - private fun Float.dpToPx(context: Context): Int { - return TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - this, - context.resources.displayMetrics - ) - .roundToInt() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt new file mode 100644 index 000000000000..1a34bb4f02c7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.stack.ui.viewbinder + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.common.ui.ConfigurationState +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R +import com.android.systemui.statusbar.notification.stack.AmbientState +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel +import javax.inject.Inject +import kotlin.math.roundToInt +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.launch + +/** Binds the NSSL/Controller/AmbientState to their ViewModel. */ +@SysUISingleton +class NotificationStackViewBinder +@Inject +constructor( + @Main private val mainImmediateDispatcher: CoroutineDispatcher, + private val ambientState: AmbientState, + private val view: NotificationStackScrollLayout, + private val controller: NotificationStackScrollLayoutController, + private val viewModel: NotificationStackAppearanceViewModel, + private val configuration: ConfigurationState, +) { + + fun bindWhileAttached(): DisposableHandle { + return view.repeatWhenAttached(mainImmediateDispatcher) { + repeatOnLifecycle(Lifecycle.State.CREATED) { bind() } + } + } + + suspend fun bind() = coroutineScope { + launch { + combine(viewModel.stackClipping, clipRadius, ::Pair).collect { (clipping, clipRadius) -> + val (bounds, rounding) = clipping + val viewLeft = controller.view.left + val viewTop = controller.view.top + controller.setRoundedClippingBounds( + bounds.left.roundToInt() - viewLeft, + bounds.top.roundToInt() - viewTop, + bounds.right.roundToInt() - viewLeft, + bounds.bottom.roundToInt() - viewTop, + if (rounding.roundTop) clipRadius else 0, + if (rounding.roundBottom) clipRadius else 0, + ) + } + } + + launch { + viewModel.contentTop.collect { + controller.updateTopPadding(it, controller.isAddOrRemoveAnimationPending) + } + } + + launch { + var wasExpanding = false + viewModel.expandFraction.collect { expandFraction -> + val nowExpanding = expandFraction != 0f && expandFraction != 1f + if (nowExpanding && !wasExpanding) { + controller.onExpansionStarted() + } + ambientState.expansionFraction = expandFraction + controller.expandedHeight = expandFraction * controller.view.height + if (!nowExpanding && wasExpanding) { + controller.onExpansionStopped() + } + wasExpanding = nowExpanding + } + } + + launch { viewModel.isScrollable.collect { controller.setScrollingEnabled(it) } } + } + + private val clipRadius: Flow<Int> + get() = configuration.getDimensionPixelOffset(R.dimen.notification_scrim_corner_radius) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt index 7c76ddbec105..ecf737a8650f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -20,6 +20,7 @@ import android.view.View import android.view.WindowInsets import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor @@ -30,6 +31,8 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel +import com.android.systemui.util.kotlin.DisposableHandles +import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.flow.MutableStateFlow @@ -38,18 +41,24 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch /** Binds the shared notification container to its view-model. */ -object SharedNotificationContainerBinder { +@SysUISingleton +class SharedNotificationContainerBinder +@Inject +constructor( + private val sceneContainerFlags: SceneContainerFlags, + private val controller: NotificationStackScrollLayoutController, + private val notificationStackSizeCalculator: NotificationStackSizeCalculator, + private val notificationStackViewBinder: NotificationStackViewBinder, + @Main private val mainImmediateDispatcher: CoroutineDispatcher, +) { - @JvmStatic fun bind( view: SharedNotificationContainer, viewModel: SharedNotificationContainerViewModel, - sceneContainerFlags: SceneContainerFlags, - controller: NotificationStackScrollLayoutController, - notificationStackSizeCalculator: NotificationStackSizeCalculator, - @Main mainImmediateDispatcher: CoroutineDispatcher, ): DisposableHandle { - val disposableHandle = + val disposables = DisposableHandles() + + disposables += view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.CREATED) { launch { @@ -72,24 +81,6 @@ object SharedNotificationContainerBinder { } } - // Required to capture keyguard media changes and ensure the notification count is correct - val layoutChangeListener = - object : View.OnLayoutChangeListener { - override fun onLayoutChange( - view: View, - left: Int, - top: Int, - right: Int, - bottom: Int, - oldLeft: Int, - oldTop: Int, - oldRight: Int, - oldBottom: Int - ) { - viewModel.notificationStackChanged() - } - } - val burnInParams = MutableStateFlow(BurnInParameters()) val viewState = ViewStateAccessor( @@ -100,7 +91,7 @@ object SharedNotificationContainerBinder { * For animation sensitive coroutines, immediately run just like applicationScope does * instead of doing a post() to the main thread. This extra delay can cause visible jitter. */ - val disposableHandleMainImmediate = + disposables += view.repeatWhenAttached(mainImmediateDispatcher) { repeatOnLifecycle(Lifecycle.State.CREATED) { launch { @@ -167,7 +158,12 @@ object SharedNotificationContainerBinder { } } - controller.setOnHeightChangedRunnable(Runnable { viewModel.notificationStackChanged() }) + if (sceneContainerFlags.isEnabled()) { + disposables += notificationStackViewBinder.bindWhileAttached() + } + + controller.setOnHeightChangedRunnable { viewModel.notificationStackChanged() } + disposables += DisposableHandle { controller.setOnHeightChangedRunnable(null) } view.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets -> val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout() @@ -176,16 +172,16 @@ object SharedNotificationContainerBinder { } insets } - view.addOnLayoutChangeListener(layoutChangeListener) + disposables += DisposableHandle { view.setOnApplyWindowInsetsListener(null) } - return object : DisposableHandle { - override fun dispose() { - disposableHandle.dispose() - disposableHandleMainImmediate.dispose() - controller.setOnHeightChangedRunnable(null) - view.setOnApplyWindowInsetsListener(null) - view.removeOnLayoutChangeListener(layoutChangeListener) + // Required to capture keyguard media changes and ensure the notification count is correct + val layoutChangeListener = + View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + viewModel.notificationStackChanged() } - } + view.addOnLayoutChangeListener(layoutChangeListener) + disposables += DisposableHandle { view.removeOnLayoutChangeListener(layoutChangeListener) } + + return disposables } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt index ed44f20868b8..bd83121d9a34 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt @@ -28,7 +28,6 @@ import com.android.systemui.statusbar.notification.stack.shared.model.StackBound import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding import javax.inject.Inject import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow /** * ViewModel used by the Notification placeholders inside the scene container to update the @@ -73,7 +72,7 @@ constructor( } /** Corner rounding of the stack */ - val stackRounding: StateFlow<StackRounding> = interactor.stackRounding + val stackRounding: Flow<StackRounding> = interactor.stackRounding /** * The height in px of the contents of notification stack. Depending on the number of 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 a38840b10b5f..ab6c14892eea 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 @@ -386,7 +386,7 @@ constructor( // All transition view models are mututally exclusive, and safe to merge val alphaTransitions = merge( - alternateBouncerToGoneTransitionViewModel.lockscreenAlpha, + alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState), aodToLockscreenTransitionViewModel.notificationAlpha, aodToOccludedTransitionViewModel.lockscreenAlpha(viewState), dozingToLockscreenTransitionViewModel.lockscreenAlpha, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index d32e88b79776..f76de04c0c18 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -27,7 +27,6 @@ import static androidx.lifecycle.Lifecycle.State.RESUMED; import static com.android.systemui.Dependency.TIME_TICK_HANDLER_NAME; import static com.android.systemui.Flags.lightRevealMigration; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import static com.android.systemui.Flags.newAodTransition; import static com.android.systemui.Flags.predictiveBackSysui; import static com.android.systemui.Flags.truncatedStatusBarIconsFix; @@ -142,6 +141,7 @@ import com.android.systemui.fragments.FragmentHostManager; import com.android.systemui.fragments.FragmentService; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.ui.binder.LightRevealScrimViewBinder; @@ -1470,7 +1470,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { return (v, event) -> { mAutoHideController.checkUserAutoHide(event); mRemoteInputManager.checkRemoteInputOutside(event); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mShadeController.onStatusBarTouch(event); } return getNotificationShadeWindowView().onTouchEvent(event); @@ -2507,7 +2507,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mNotificationShadeWindowController.batchApplyWindowLayoutParams(()-> { mDeviceInteractive = true; - boolean isFlaggedOff = newAodTransition() && migrateClocksToBlueprint(); + boolean isFlaggedOff = newAodTransition() && MigrateClocksToBlueprint.isEnabled(); if (!isFlaggedOff && shouldAnimateDozeWakeup()) { // If this is false, the power button must be physically pressed in order to // trigger fingerprint authentication. @@ -3147,7 +3147,14 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { public void onDozeAmountChanged(float linear, float eased) { if (!lightRevealMigration() && !(mLightRevealScrim.getRevealEffect() instanceof CircleReveal)) { - mLightRevealScrim.setRevealAmount(1f - linear); + if (DeviceEntryUdfpsRefactor.isEnabled()) { + // If wakeAndUnlocking, this is handled in AuthRippleInteractor + if (!mBiometricUnlockController.isWakeAndUnlock()) { + mLightRevealScrim.setRevealAmount(1f - linear); + } + } else { + mLightRevealScrim.setRevealAmount(1f - linear); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java index 24be3db6231f..86bb844e7be3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/HeadsUpManagerPhone.java @@ -41,6 +41,7 @@ import com.android.systemui.statusbar.notification.collection.provider.OnReorder import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; +import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; import com.android.systemui.statusbar.policy.AnimationStateHandler; import com.android.systemui.statusbar.policy.AvalancheController; @@ -94,6 +95,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp @Override public HeadsUpEntryPhone acquire() { + NotificationsHeadsUpRefactor.assertInLegacyMode(); if (!mPoolObjects.isEmpty()) { return mPoolObjects.pop(); } @@ -102,6 +104,7 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp @Override public boolean release(@NonNull HeadsUpEntryPhone instance) { + NotificationsHeadsUpRefactor.assertInLegacyMode(); mPoolObjects.push(instance); return true; } @@ -371,15 +374,24 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp /////////////////////////////////////////////////////////////////////////////////////////////// // HeadsUpManager utility (protected) methods overrides: + @NonNull @Override - protected HeadsUpEntry createHeadsUpEntry() { - return mEntryPool.acquire(); + protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) { + if (NotificationsHeadsUpRefactor.isEnabled()) { + return new HeadsUpEntryPhone(entry); + } else { + HeadsUpEntryPhone headsUpEntry = mEntryPool.acquire(); + headsUpEntry.setEntry(entry); + return headsUpEntry; + } } @Override protected void onEntryRemoved(HeadsUpEntry headsUpEntry) { super.onEntryRemoved(headsUpEntry); - mEntryPool.release((HeadsUpEntryPhone) headsUpEntry); + if (!NotificationsHeadsUpRefactor.isEnabled()) { + mEntryPool.release((HeadsUpEntryPhone) headsUpEntry); + } } @Override @@ -439,14 +451,22 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp */ private boolean extended; - @Override public boolean isSticky() { return super.isSticky() || mGutsShownPinned; } - public void setEntry(@NonNull final NotificationEntry entry) { - Runnable removeHeadsUpRunnable = () -> { + public HeadsUpEntryPhone() { + super(); + } + + public HeadsUpEntryPhone(NotificationEntry entry) { + super(entry); + } + + @Override + protected Runnable createRemoveRunnable(NotificationEntry entry) { + return () -> { if (!mVisualStabilityProvider.isReorderingAllowed() // We don't want to allow reordering while pulsing, but headsup need to // time out anyway @@ -460,8 +480,6 @@ public class HeadsUpManagerPhone extends BaseHeadsUpManager implements OnHeadsUp removeEntry(entry.getKey()); } }; - - setEntry(entry, removeHeadsUpRunnable); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java index 94f62e075a4a..f84efbbf9293 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LegacyNotificationIconAreaControllerImpl.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.phone; import static com.android.systemui.Flags.newAodTransition; -import static com.android.systemui.Flags.migrateClocksToBlueprint; import android.content.Context; import android.content.res.Resources; @@ -41,6 +40,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.demomode.DemoMode; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.keyguard.MigrateClocksToBlueprint; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; import com.android.systemui.plugins.statusbar.StatusBarStateController; @@ -545,7 +545,7 @@ public class LegacyNotificationIconAreaControllerImpl implements return; } if (mScreenOffAnimationController.shouldAnimateAodIcons()) { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mAodIcons.setTranslationY(-mAodIconAppearTranslation); } mAodIcons.setAlpha(0); @@ -557,14 +557,14 @@ public class LegacyNotificationIconAreaControllerImpl implements .start(); } else { mAodIcons.setAlpha(1.0f); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mAodIcons.setTranslationY(0); } } } private void animateInAodIconTranslation() { - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mAodIcons.animate() .setInterpolator(Interpolators.DECELERATE_QUINT) .translationY(0) @@ -667,7 +667,7 @@ public class LegacyNotificationIconAreaControllerImpl implements } } else { mAodIcons.setAlpha(1.0f); - if (!migrateClocksToBlueprint()) { + if (!MigrateClocksToBlueprint.isEnabled()) { mAodIcons.setTranslationY(0); } mAodIcons.setVisibility(visible ? View.VISIBLE : View.INVISIBLE); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt index 67d2299a9a3d..f3c70907b182 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt @@ -19,9 +19,9 @@ import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD import com.android.systemui.DejankUtils import com.android.systemui.Flags.lightRevealMigration -import com.android.systemui.Flags.migrateClocksToBlueprint import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.KeyguardViewMediator +import com.android.systemui.keyguard.MigrateClocksToBlueprint import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.shade.ShadeViewController import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor @@ -45,9 +45,7 @@ import javax.inject.Inject */ private const val ANIMATE_IN_KEYGUARD_DELAY = 600L -/** - * Duration for the light reveal portion of the animation. - */ +/** Duration for the light reveal portion of the animation. */ private const val LIGHT_REVEAL_ANIMATION_DURATION = 500L /** @@ -58,7 +56,9 @@ private const val LIGHT_REVEAL_ANIMATION_DURATION = 500L * and then animates in the AOD UI. */ @SysUISingleton -class UnlockedScreenOffAnimationController @Inject constructor( +class UnlockedScreenOffAnimationController +@Inject +constructor( private val context: Context, private val wakefulnessLifecycle: WakefulnessLifecycle, private val statusBarStateControllerImpl: StatusBarStateControllerImpl, @@ -95,52 +95,61 @@ class UnlockedScreenOffAnimationController @Inject constructor( */ private var decidedToAnimateGoingToSleep: Boolean? = null - private val lightRevealAnimator = ValueAnimator.ofFloat(1f, 0f).apply { - duration = LIGHT_REVEAL_ANIMATION_DURATION - interpolator = Interpolators.LINEAR - addUpdateListener { - if (lightRevealMigration()) return@addUpdateListener - if (lightRevealScrim.revealEffect !is CircleReveal) { - lightRevealScrim.revealAmount = it.animatedValue as Float - } - if (lightRevealScrim.isScrimAlmostOccludes && - interactionJankMonitor.isInstrumenting(CUJ_SCREEN_OFF)) { - // ends the instrument when the scrim almost occludes the screen. - // because the following janky frames might not be perceptible. - interactionJankMonitor.end(CUJ_SCREEN_OFF) - } - } - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationCancel(animation: Animator) { - if (lightRevealMigration()) return + private val lightRevealAnimator = + ValueAnimator.ofFloat(1f, 0f).apply { + duration = LIGHT_REVEAL_ANIMATION_DURATION + interpolator = Interpolators.LINEAR + addUpdateListener { + if (lightRevealMigration()) return@addUpdateListener if (lightRevealScrim.revealEffect !is CircleReveal) { - lightRevealScrim.revealAmount = 1f + lightRevealScrim.revealAmount = it.animatedValue as Float + } + if ( + lightRevealScrim.isScrimAlmostOccludes && + interactionJankMonitor.isInstrumenting(CUJ_SCREEN_OFF) + ) { + // ends the instrument when the scrim almost occludes the screen. + // because the following janky frames might not be perceptible. + interactionJankMonitor.end(CUJ_SCREEN_OFF) } } + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationCancel(animation: Animator) { + if (lightRevealMigration()) return + if (lightRevealScrim.revealEffect !is CircleReveal) { + lightRevealScrim.revealAmount = 1f + } + } - override fun onAnimationEnd(animation: Animator) { - lightRevealAnimationPlaying = false - interactionJankMonitor.end(CUJ_SCREEN_OFF) - } + override fun onAnimationEnd(animation: Animator) { + lightRevealAnimationPlaying = false + interactionJankMonitor.end(CUJ_SCREEN_OFF) + } - override fun onAnimationStart(animation: Animator) { - interactionJankMonitor.begin( - notifShadeWindowControllerLazy.get().windowRootView, CUJ_SCREEN_OFF) - } - }) - } + override fun onAnimationStart(animation: Animator) { + interactionJankMonitor.begin( + notifShadeWindowControllerLazy.get().windowRootView, + CUJ_SCREEN_OFF + ) + } + } + ) + } // FrameCallback used to delay starting the light reveal animation until the next frame - private val startLightRevealCallback = namedRunnable("startLightReveal") { - lightRevealAnimationPlaying = true - lightRevealAnimator.start() - } + private val startLightRevealCallback = + namedRunnable("startLightReveal") { + lightRevealAnimationPlaying = true + lightRevealAnimator.start() + } - private val animatorDurationScaleObserver = object : ContentObserver(null) { - override fun onChange(selfChange: Boolean) { - updateAnimatorDurationScale() + private val animatorDurationScaleObserver = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + updateAnimatorDurationScale() + } } - } override fun initialize( centralSurfaces: CentralSurfaces, @@ -154,22 +163,21 @@ class UnlockedScreenOffAnimationController @Inject constructor( updateAnimatorDurationScale() globalSettings.registerContentObserver( - Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE), - /* notify for descendants */ false, - animatorDurationScaleObserver) + Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE), + /* notify for descendants */ false, + animatorDurationScaleObserver + ) wakefulnessLifecycle.addObserver(this) } fun updateAnimatorDurationScale() { - animatorDurationScale = fixScale( - globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f)) + animatorDurationScale = + fixScale(globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f)) } - override fun shouldDelayKeyguardShow(): Boolean = - shouldPlayAnimation() + override fun shouldDelayKeyguardShow(): Boolean = shouldPlayAnimation() - override fun isKeyguardShowDelayed(): Boolean = - isAnimationPlaying() + override fun isKeyguardShowDelayed(): Boolean = isAnimationPlaying() /** * Animates in the provided keyguard view, ending in the same position that it will be in on @@ -190,15 +198,21 @@ class UnlockedScreenOffAnimationController @Inject constructor( // We animate the Y properly separately using the PropertyAnimator, as the panel // view also needs to update the end position. PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.Y) - PropertyAnimator.setProperty(keyguardView, AnimatableProperty.Y, currentY, - AnimationProperties().setDuration(duration.toLong()), - true /* animate */) + PropertyAnimator.setProperty( + keyguardView, + AnimatableProperty.Y, + currentY, + AnimationProperties().setDuration(duration.toLong()), + true /* animate */ + ) // Cancel any existing CUJs before starting the animation interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD) PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.ALPHA) PropertyAnimator.setProperty( - keyguardView, AnimatableProperty.ALPHA, 1f, + keyguardView, + AnimatableProperty.ALPHA, + 1f, AnimationProperties() .setDelay(0) .setDuration(duration.toLong()) @@ -230,13 +244,14 @@ class UnlockedScreenOffAnimationController @Inject constructor( interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD) } .setCustomInterpolator(View.ALPHA, Interpolators.FAST_OUT_SLOW_IN), - true /* animate */) - val builder = InteractionJankMonitor.Configuration.Builder - .withView( + true /* animate */ + ) + val builder = + InteractionJankMonitor.Configuration.Builder.withView( InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD, checkNotNull(notifShadeWindowControllerLazy.get().windowRootView) - ) - .setTag(statusBarStateControllerImpl.getClockId()) + ) + .setTag(statusBarStateControllerImpl.getClockId()) interactionJankMonitor.begin(builder) } @@ -284,25 +299,34 @@ class UnlockedScreenOffAnimationController @Inject constructor( // chance of missing the first frame, so to mitigate this we should start the animation // on the next frame. DejankUtils.postAfterTraversal(startLightRevealCallback) - handler.postDelayed({ - // Only run this callback if the device is sleeping (not interactive). This callback - // is removed in onStartedWakingUp, but since that event is asynchronously - // dispatched, a race condition could make it possible for this callback to be run - // as the device is waking up. That results in the AOD UI being shown while we wake - // up, with unpredictable consequences. - if (!powerManager.isInteractive(Display.DEFAULT_DISPLAY) && - shouldAnimateInKeyguard) { - if (!migrateClocksToBlueprint()) { - // Tracking this state should no longer be relevant, as the isInteractive - // check covers it - aodUiAnimationPlaying = true + handler.postDelayed( + { + // Only run this callback if the device is sleeping (not interactive). This + // callback + // is removed in onStartedWakingUp, but since that event is asynchronously + // dispatched, a race condition could make it possible for this callback to be + // run + // as the device is waking up. That results in the AOD UI being shown while we + // wake + // up, with unpredictable consequences. + if ( + !powerManager.isInteractive(Display.DEFAULT_DISPLAY) && + shouldAnimateInKeyguard + ) { + if (!MigrateClocksToBlueprint.isEnabled) { + // Tracking this state should no longer be relevant, as the + // isInteractive + // check covers it + aodUiAnimationPlaying = true + } + + // Show AOD. That'll cause the KeyguardVisibilityHelper to call + // #animateInKeyguard. + shadeViewController.showAodUi() } - - // Show AOD. That'll cause the KeyguardVisibilityHelper to call - // #animateInKeyguard. - shadeViewController.showAodUi() - } - }, (ANIMATE_IN_KEYGUARD_DELAY * animatorDurationScale).toLong()) + }, + (ANIMATE_IN_KEYGUARD_DELAY * animatorDurationScale).toLong() + ) return true } else { @@ -335,8 +359,12 @@ class UnlockedScreenOffAnimationController @Inject constructor( } // If animations are disabled system-wide, don't play this one either. - if (Settings.Global.getString( - context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE) == "0") { + if ( + Settings.Global.getString( + context.contentResolver, + Settings.Global.ANIMATOR_DURATION_SCALE + ) == "0" + ) { return false } @@ -360,8 +388,10 @@ class UnlockedScreenOffAnimationController @Inject constructor( // If we're not allowed to rotate the keyguard, it can only be displayed in zero-degree // portrait. If we're in another orientation, disable the screen off animation so we don't // animate in the keyguard AOD UI sideways or upside down. - if (!keyguardStateController.isKeyguardScreenRotationAllowed && - context.display?.rotation != Surface.ROTATION_0) { + if ( + !keyguardStateController.isKeyguardScreenRotationAllowed && + context.display?.rotation != Surface.ROTATION_0 + ) { return false } @@ -380,23 +410,18 @@ class UnlockedScreenOffAnimationController @Inject constructor( return isScreenOffLightRevealAnimationPlaying() || aodUiAnimationPlaying } - override fun shouldAnimateInKeyguard(): Boolean = - shouldAnimateInKeyguard + override fun shouldAnimateInKeyguard(): Boolean = shouldAnimateInKeyguard - override fun shouldHideScrimOnWakeUp(): Boolean = - isScreenOffLightRevealAnimationPlaying() + override fun shouldHideScrimOnWakeUp(): Boolean = isScreenOffLightRevealAnimationPlaying() override fun overrideNotificationsDozeAmount(): Boolean = shouldPlayUnlockedScreenOffAnimation() && isAnimationPlaying() - override fun shouldShowAodIconsWhenShade(): Boolean = - isAnimationPlaying() + override fun shouldShowAodIconsWhenShade(): Boolean = isAnimationPlaying() - override fun shouldAnimateAodIcons(): Boolean = - shouldPlayUnlockedScreenOffAnimation() + override fun shouldAnimateAodIcons(): Boolean = shouldPlayUnlockedScreenOffAnimation() - override fun shouldPlayAnimation(): Boolean = - shouldPlayUnlockedScreenOffAnimation() + override fun shouldPlayAnimation(): Boolean = shouldPlayUnlockedScreenOffAnimation() /** * Whether the light reveal animation is playing. The second part of the screen off animation, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java index 50de3cba6b59..6f7e0468c246 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseHeadsUpManager.java @@ -39,6 +39,7 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; +import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor; import com.android.systemui.util.ListenerSet; import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.settings.GlobalSettings; @@ -162,11 +163,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { */ @Override public void showNotification(@NonNull NotificationEntry entry) { - HeadsUpEntry headsUpEntry = createHeadsUpEntry(); - - // Attach NotificationEntry for AvalancheController to log key and - // record mPostTime for AvalancheController sorting - headsUpEntry.setEntry(entry); + HeadsUpEntry headsUpEntry = createHeadsUpEntry(entry); Runnable runnable = () -> { // TODO(b/315362456) log outside runnable too @@ -375,7 +372,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { } /** - * Remove a notification and reset the entry. + * Remove a notification from the alerting entries. * @param key key of notification to remove */ protected final void removeEntry(@NonNull String key) { @@ -395,7 +392,11 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { mHeadsUpEntryMap.remove(key); onEntryRemoved(headsUpEntry); entry.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); - headsUpEntry.reset(); + if (NotificationsHeadsUpRefactor.isEnabled()) { + headsUpEntry.cancelAutoRemovalCallbacks("removeEntry"); + } else { + headsUpEntry.reset(); + } }; mAvalancheController.delete(headsUpEntry, runnable, "removeEntry"); } @@ -657,8 +658,8 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { } @NonNull - protected HeadsUpEntry createHeadsUpEntry() { - return new HeadsUpEntry(); + protected HeadsUpEntry createHeadsUpEntry(NotificationEntry entry) { + return new HeadsUpEntry(entry); } /** @@ -694,11 +695,23 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { @Nullable private Runnable mCancelRemoveRunnable; + public HeadsUpEntry() { + NotificationsHeadsUpRefactor.assertInLegacyMode(); + } + + public HeadsUpEntry(NotificationEntry entry) { + // Attach NotificationEntry for AvalancheController to log key and + // record mPostTime for AvalancheController sorting + setEntry(entry, createRemoveRunnable(entry)); + } + + /** Attach a NotificationEntry. */ public void setEntry(@NonNull final NotificationEntry entry) { - setEntry(entry, () -> removeEntry(entry.getKey())); + NotificationsHeadsUpRefactor.assertInLegacyMode(); + setEntry(entry, createRemoveRunnable(entry)); } - public void setEntry(@NonNull final NotificationEntry entry, + private void setEntry(@NonNull final NotificationEntry entry, @Nullable Runnable removeRunnable) { mEntry = entry; mRemoveRunnable = removeRunnable; @@ -847,6 +860,7 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { } public void reset() { + NotificationsHeadsUpRefactor.assertInLegacyMode(); cancelAutoRemovalCallbacks("reset()"); mEntry = null; mRemoveRunnable = null; @@ -919,6 +933,11 @@ public abstract class BaseHeadsUpManager implements HeadsUpManager { } } + /** Creates a runnable to remove this notification from the alerting entries. */ + protected Runnable createRemoveRunnable(NotificationEntry entry) { + return () -> removeEntry(entry.getKey()); + } + /** * Calculate what the post time of a notification is at some current time. * @return the post time diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt index 087e100e9b33..7a570275d868 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt @@ -42,6 +42,10 @@ import com.android.systemui.qs.tiles.impl.uimodenight.domain.UiModeNightTileMapp import com.android.systemui.qs.tiles.impl.uimodenight.domain.interactor.UiModeNightTileDataInteractor import com.android.systemui.qs.tiles.impl.uimodenight.domain.interactor.UiModeNightTileUserActionInteractor import com.android.systemui.qs.tiles.impl.uimodenight.domain.model.UiModeNightTileModel +import com.android.systemui.qs.tiles.impl.work.domain.interactor.WorkModeTileDataInteractor +import com.android.systemui.qs.tiles.impl.work.domain.interactor.WorkModeTileUserActionInteractor +import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel +import com.android.systemui.qs.tiles.impl.work.ui.WorkModeTileMapper import com.android.systemui.qs.tiles.viewmodel.QSTileConfig import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel @@ -69,6 +73,7 @@ interface PolicyModule { const val LOCATION_TILE_SPEC = "location" const val ALARM_TILE_SPEC = "alarm" const val UIMODENIGHT_TILE_SPEC = "dark" + const val WORK_MODE_TILE_SPEC = "work" /** Inject flashlight config */ @Provides @@ -197,6 +202,38 @@ interface PolicyModule { stateInteractor, mapper, ) + + /** Inject work mode tile config */ + @Provides + @IntoMap + @StringKey(WORK_MODE_TILE_SPEC) + fun provideWorkModeTileConfig(uiEventLogger: QsEventLogger): QSTileConfig = + QSTileConfig( + tileSpec = TileSpec.create(WORK_MODE_TILE_SPEC), + uiConfig = + QSTileUIConfig.Resource( + iconRes = com.android.internal.R.drawable.stat_sys_managed_profile_status, + labelRes = R.string.quick_settings_work_mode_label, + ), + instanceId = uiEventLogger.getNewInstanceId(), + ) + + /** Inject work mode into tileViewModelMap in QSModule */ + @Provides + @IntoMap + @StringKey(WORK_MODE_TILE_SPEC) + fun provideWorkModeTileViewModel( + factory: QSTileViewModelFactory.Static<WorkModeTileModel>, + mapper: WorkModeTileMapper, + stateInteractor: WorkModeTileDataInteractor, + userActionInteractor: WorkModeTileUserActionInteractor + ): QSTileViewModel = + factory.create( + TileSpec.create(WORK_MODE_TILE_SPEC), + userActionInteractor, + stateInteractor, + mapper, + ) } /** Inject FlashlightTile into tileMap in QSModule */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java index 18ec68bd89eb..1f4c3cd9a017 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerImpl.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.policy; +import static android.permission.flags.Flags.sensitiveNotificationAppProtection; import static android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS; import static com.android.server.notification.Flags.screenshareNotificationHiding; @@ -23,6 +24,7 @@ import static com.android.server.notification.Flags.screenshareNotificationHidin import android.annotation.MainThread; import android.app.IActivityManager; import android.content.Context; +import android.content.pm.PackageManager; import android.database.ExecutorContentObserver; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionManager; @@ -33,6 +35,9 @@ import android.service.notification.StatusBarNotification; import android.util.ArraySet; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.android.internal.annotations.VisibleForTesting; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; @@ -52,6 +57,7 @@ public class SensitiveNotificationProtectionControllerImpl implements SensitiveNotificationProtectionController { private static final String LOG_TAG = "SNPC"; private final SensitiveNotificationProtectionControllerLogger mLogger; + private final PackageManager mPackageManager; private final ArraySet<String> mExemptPackages = new ArraySet<>(); private final ListenerSet<Runnable> mListeners = new ListenerSet<>(); private volatile MediaProjectionInfo mProjection; @@ -64,17 +70,7 @@ public class SensitiveNotificationProtectionControllerImpl public void onStart(MediaProjectionInfo info) { Trace.beginSection("SNPC.onProjectionStart"); try { - if (mDisableScreenShareProtections) { - Log.w(LOG_TAG, - "Screen share protections disabled, ignoring projectionstart"); - mLogger.logProjectionStart(false, info.getPackageName()); - return; - } - - // Only enable sensitive content protection if sharing full screen - // Launch cookie only set (non-null) if sharing single app/task - updateProjectionStateAndNotifyListeners( - (info.getLaunchCookie() == null) ? info : null); + updateProjectionStateAndNotifyListeners(info); mLogger.logProjectionStart(isSensitiveStateActive(), info.getPackageName()); } finally { Trace.endSection(); @@ -99,10 +95,12 @@ public class SensitiveNotificationProtectionControllerImpl GlobalSettings settings, MediaProjectionManager mediaProjectionManager, IActivityManager activityManager, + PackageManager packageManager, @Main Handler mainHandler, @Background Executor bgExecutor, SensitiveNotificationProtectionControllerLogger logger) { mLogger = logger; + mPackageManager = packageManager; if (!screenshareNotificationHiding()) { return; @@ -168,7 +166,7 @@ public class SensitiveNotificationProtectionControllerImpl mExemptPackages.addAll(exemptPackages); if (mProjection != null) { - mListeners.forEach(Runnable::run); + updateProjectionStateAndNotifyListeners(mProjection); } } @@ -177,13 +175,13 @@ public class SensitiveNotificationProtectionControllerImpl * listeners */ @MainThread - private void updateProjectionStateAndNotifyListeners(MediaProjectionInfo info) { + private void updateProjectionStateAndNotifyListeners(@Nullable MediaProjectionInfo info) { Assert.isMainThread(); // capture previous state boolean wasSensitive = isSensitiveStateActive(); // update internal state - mProjection = info; + mProjection = getNonExemptProjectionInfo(info); // if either previous or new state is sensitive, notify listeners. if (wasSensitive || isSensitiveStateActive()) { @@ -191,6 +189,36 @@ public class SensitiveNotificationProtectionControllerImpl } } + private MediaProjectionInfo getNonExemptProjectionInfo(@Nullable MediaProjectionInfo info) { + if (mDisableScreenShareProtections) { + Log.w(LOG_TAG, "Screen share protections disabled"); + return null; + } else if (info != null && mExemptPackages.contains(info.getPackageName())) { + Log.w(LOG_TAG, "Screen share protections exempt for package " + info.getPackageName()); + return null; + } else if (info != null && canRecordSensitiveContent(info.getPackageName())) { + Log.w(LOG_TAG, "Screen share protections exempt for package " + info.getPackageName() + + " via permission"); + return null; + } else if (info != null && info.getLaunchCookie() != null) { + // Only enable sensitive content protection if sharing full screen + // Launch cookie only set (non-null) if sharing single app/task + Log.w(LOG_TAG, "Screen share protections exempt for single app screenshare"); + return null; + } + return info; + } + + private boolean canRecordSensitiveContent(@NonNull String packageName) { + // RECORD_SENSITIVE_CONTENT is flagged api on sensitiveNotificationAppProtection + if (sensitiveNotificationAppProtection()) { + return mPackageManager.checkPermission( + android.Manifest.permission.RECORD_SENSITIVE_CONTENT, packageName) + == PackageManager.PERMISSION_GRANTED; + } + return false; + } + @Override public void registerSensitiveStateListener(Runnable onSensitiveStateChanged) { mListeners.addIfAbsent(onSensitiveStateChanged); @@ -201,15 +229,9 @@ public class SensitiveNotificationProtectionControllerImpl mListeners.remove(onSensitiveStateChanged); } - // TODO(b/323396693): opportunity for optimization @Override public boolean isSensitiveStateActive() { - MediaProjectionInfo projection = mProjection; - if (projection == null) { - return false; - } - - return !mExemptPackages.contains(projection.getPackageName()); + return mProjection != null; } @Override diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt new file mode 100644 index 000000000000..de036eaebaa2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.kotlin + +import kotlinx.coroutines.DisposableHandle + +/** A mutable collection of [DisposableHandle] objects that is itself a [DisposableHandle] */ +class DisposableHandles : DisposableHandle { + private val handles = mutableListOf<DisposableHandle>() + + /** Add the provided handles to this collection. */ + fun add(vararg handles: DisposableHandle) { + this.handles.addAll(handles) + } + + /** Same as [add] */ + operator fun plusAssign(handle: DisposableHandle) { + this.handles.add(handle) + } + + /** Same as [add] */ + operator fun plusAssign(handles: Iterable<DisposableHandle>) { + this.handles.addAll(handles) + } + + /** [dispose] the current contents, then [add] the provided [handles] */ + fun replaceAll(vararg handles: DisposableHandle) { + dispose() + add(*handles) + } + + /** Dispose of all added handles and empty this collection. */ + override fun dispose() { + handles.forEach { it.dispose() } + handles.clear() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt new file mode 100644 index 000000000000..7a2f9b24700f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/ManagedProfileControllerExt.kt @@ -0,0 +1,37 @@ +/* + * 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.util.kotlin + +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.statusbar.phone.ManagedProfileController +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow + +val ManagedProfileController.hasActiveWorkProfile: Flow<Boolean> + get() = conflatedCallbackFlow { + val callback = + object : ManagedProfileController.Callback { + override fun onManagedProfileChanged() { + trySend(hasActiveProfile()) + } + override fun onManagedProfileRemoved() { + // no-op, because the other callback will also be called. + } + } + addCallback(callback) // calls onManagedProfileChanged + awaitClose { removeCallback(callback) } + } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt index d49442c149ee..3242c2814bc5 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt @@ -116,6 +116,7 @@ constructor( isEnabled = isEnabled, a11yStep = volumeRange.step, audioStreamModel = this, + isMutable = audioVolumeInteractor.isMutable(audioStream), ) } @@ -160,6 +161,7 @@ constructor( override val disabledMessage: String?, override val isEnabled: Boolean, override val a11yStep: Int, + override val isMutable: Boolean, val audioStreamModel: AudioStreamModel, ) : SliderState diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt index 0f240b37f02e..73c8bbfce6d9 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt @@ -90,6 +90,8 @@ constructor( ) : SliderState { override val disabledMessage: String? get() = null + override val isMutable: Boolean + get() = false } @AssistedFactory diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt index 3dca2724b095..8eb0b8947c37 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt @@ -36,6 +36,7 @@ sealed interface SliderState { */ val a11yStep: Int val disabledMessage: String? + val isMutable: Boolean data object Empty : SliderState { override val value: Float = 0f @@ -46,5 +47,6 @@ sealed interface SliderState { override val disabledMessage: String? = null override val a11yStep: Int = 0 override val isEnabled: Boolean = true + override val isMutable: Boolean = false } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt index d430e65770fd..c728fefa77e6 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt @@ -42,7 +42,6 @@ constructor( override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) - volumePanelFlag.assertNewVolumePanel() setContent { VolumePanelRoot(viewModel = viewModel, onDismiss = ::finish) } diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index 7931fab91f46..e48b6397457c 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -363,8 +363,8 @@ public final class WMShell implements }, mSysUiMainExecutor); mCommandQueue.addCallback(new CommandQueue.Callbacks() { @Override - public void enterDesktop(int displayId) { - desktopMode.enterDesktop(displayId); + public void moveFocusedTaskToDesktop(int displayId) { + desktopMode.moveFocusedTaskToDesktop(displayId); } @Override public void moveFocusedTaskToFullscreen(int displayId) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt index b73e4e6ab015..9182e4101f36 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/DialogTransitionAnimatorTest.kt @@ -36,6 +36,7 @@ import org.junit.runner.RunWith import org.mockito.Mockito.any import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule @SmallTest @RunWith(AndroidTestingRunner::class) @@ -44,8 +45,8 @@ class DialogTransitionAnimatorTest : SysuiTestCase() { private lateinit var mDialogTransitionAnimator: DialogTransitionAnimator private val attachedViews = mutableSetOf<View>() - val interactionJankMonitor = Kosmos().interactionJankMonitor - @get:Rule val rule = MockitoJUnit.rule() + private val interactionJankMonitor = Kosmos().interactionJankMonitor + @get:Rule val rule: MockitoRule = MockitoJUnit.rule() @Before fun setUp() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt index 5dd37ae46ee8..66aa572dbc48 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt @@ -131,7 +131,6 @@ class KeyguardClockViewBinderTest : SysuiTestCase() { whenever(clock.smallClock).thenReturn(smallClock) whenever(largeClock.layout).thenReturn(largeClockFaceLayout) whenever(smallClock.layout).thenReturn(smallClockFaceLayout) - whenever(clockViewModel.clock).thenReturn(clock) currentClock.value = clock } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt index 59eb7bb73de7..e56a25345436 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataFilterImplTest.kt @@ -66,7 +66,7 @@ private val SMARTSPACE_INSTANCE_ID = InstanceId.fakeInstanceId(456)!! @SmallTest @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper -class MediaDataFilterTest : SysuiTestCase() { +class LegacyMediaDataFilterImplTest : SysuiTestCase() { @Mock private lateinit var listener: MediaDataManager.Listener @Mock private lateinit var userTracker: UserTracker @@ -80,7 +80,7 @@ class MediaDataFilterTest : SysuiTestCase() { @Mock private lateinit var mediaFlags: MediaFlags @Mock private lateinit var cardAction: SmartspaceAction - private lateinit var mediaDataFilter: MediaDataFilter + private lateinit var mediaDataFilter: LegacyMediaDataFilterImpl private lateinit var dataMain: MediaData private lateinit var dataGuest: MediaData private lateinit var dataPrivateProfile: MediaData @@ -92,7 +92,7 @@ class MediaDataFilterTest : SysuiTestCase() { MediaPlayerData.clear() whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false) mediaDataFilter = - MediaDataFilter( + LegacyMediaDataFilterImpl( context, userTracker, broadcastSender, @@ -370,7 +370,7 @@ class MediaDataFilterTest : SysuiTestCase() { mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) mediaDataFilter.onSwipeToDismiss() - verify(mediaDataManager).setTimedOut(eq(KEY), eq(true), eq(true)) + verify(mediaDataManager).setInactive(eq(KEY), eq(true), eq(true)) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt index 61bfdb548b4f..5a2d22d0d503 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt @@ -114,7 +114,7 @@ private fun <T> anyObject(): T { @SmallTest @RunWithLooper(setAsMainLooper = true) @RunWith(AndroidTestingRunner::class) -class MediaDataManagerTest : SysuiTestCase() { +class LegacyMediaDataManagerImplTest : SysuiTestCase() { @JvmField @Rule val mockito = MockitoJUnit.rule() @Mock lateinit var mediaControllerFactory: MediaControllerFactory @@ -133,7 +133,7 @@ class MediaDataManagerTest : SysuiTestCase() { @Mock lateinit var mediaSessionBasedFilter: MediaSessionBasedFilter @Mock lateinit var mediaDeviceManager: MediaDeviceManager @Mock lateinit var mediaDataCombineLatest: MediaDataCombineLatest - @Mock lateinit var mediaDataFilter: MediaDataFilter + @Mock lateinit var mediaDataFilter: LegacyMediaDataFilterImpl @Mock lateinit var listener: MediaDataManager.Listener @Mock lateinit var pendingIntent: PendingIntent @Mock lateinit var activityStarter: ActivityStarter @@ -146,7 +146,7 @@ class MediaDataManagerTest : SysuiTestCase() { @Mock private lateinit var mediaSmartspaceBaseAction: SmartspaceAction @Mock private lateinit var mediaFlags: MediaFlags @Mock private lateinit var logger: MediaUiEventLogger - lateinit var mediaDataManager: MediaDataManager + lateinit var mediaDataManager: LegacyMediaDataManagerImpl lateinit var mediaNotification: StatusBarNotification lateinit var remoteCastNotification: StatusBarNotification @Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData> @@ -189,7 +189,7 @@ class MediaDataManagerTest : SysuiTestCase() { 1 ) mediaDataManager = - MediaDataManager( + LegacyMediaDataManagerImpl( context = context, backgroundExecutor = backgroundExecutor, uiExecutor = uiExecutor, @@ -304,13 +304,13 @@ class MediaDataManagerTest : SysuiTestCase() { val data = mediaDataCaptor.value assertThat(data.active).isTrue() - mediaDataManager.setTimedOut(KEY, timedOut = true) + mediaDataManager.setInactive(KEY, timedOut = true) assertThat(data.active).isFalse() verify(logger).logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) } @Test - fun testSetTimedOut_resume_dismissesMedia() { + fun testsetInactive_resume_dismissesMedia() { // WHEN resume controls are present, and time out val desc = MediaDescription.Builder().run { @@ -339,7 +339,7 @@ class MediaDataManagerTest : SysuiTestCase() { eq(false) ) - mediaDataManager.setTimedOut(PACKAGE_NAME, timedOut = true) + mediaDataManager.setInactive(PACKAGE_NAME, timedOut = true) verify(logger) .logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(mediaDataCaptor.value.instanceId)) @@ -1485,7 +1485,7 @@ class MediaDataManagerTest : SysuiTestCase() { // WHEN the notification times out clock.advanceTime(100) val currentTime = clock.elapsedRealtime() - mediaDataManager.setTimedOut(KEY, true, true) + mediaDataManager.setInactive(KEY, true, true) // THEN the last active time is changed verify(listener) @@ -1602,7 +1602,7 @@ class MediaDataManagerTest : SysuiTestCase() { eq(false) ) assertThat(mediaDataCaptor.value.actionsToShowInCompact.size) - .isEqualTo(MediaDataManager.MAX_COMPACT_ACTIONS) + .isEqualTo(LegacyMediaDataManagerImpl.MAX_COMPACT_ACTIONS) } @Test @@ -1615,7 +1615,7 @@ class MediaDataManagerTest : SysuiTestCase() { modifyNotification(context).also { it.setSmallIcon(android.R.drawable.ic_media_pause) it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) - for (i in 0..MediaDataManager.MAX_NOTIFICATION_ACTIONS) { + for (i in 0..LegacyMediaDataManagerImpl.MAX_NOTIFICATION_ACTIONS) { it.addAction(action) } } @@ -1638,7 +1638,7 @@ class MediaDataManagerTest : SysuiTestCase() { eq(false) ) assertThat(mediaDataCaptor.value.actions.size) - .isEqualTo(MediaDataManager.MAX_NOTIFICATION_ACTIONS) + .isEqualTo(LegacyMediaDataManagerImpl.MAX_NOTIFICATION_ACTIONS) } @Test @@ -2040,7 +2040,7 @@ class MediaDataManagerTest : SysuiTestCase() { // When a media control based on notification is added, times out, and then removed addNotificationAndLoad() - mediaDataManager.setTimedOut(KEY, timedOut = true) + mediaDataManager.setInactive(KEY, timedOut = true) assertThat(mediaDataCaptor.value.active).isFalse() mediaDataManager.onNotificationRemoved(KEY) @@ -2070,7 +2070,7 @@ class MediaDataManagerTest : SysuiTestCase() { // When a media control based on notification is added and times out addNotificationAndLoad() - mediaDataManager.setTimedOut(KEY, timedOut = true) + mediaDataManager.setInactive(KEY, timedOut = true) assertThat(mediaDataCaptor.value.active).isFalse() // and then the session is destroyed @@ -2142,7 +2142,7 @@ class MediaDataManagerTest : SysuiTestCase() { addNotificationAndLoad() val data = mediaDataCaptor.value assertThat(data.active).isTrue() - mediaDataManager.setTimedOut(KEY, timedOut = true) + mediaDataManager.setInactive(KEY, timedOut = true) mediaDataManager.onNotificationRemoved(KEY) // It remains as a regular player @@ -2162,7 +2162,7 @@ class MediaDataManagerTest : SysuiTestCase() { addNotificationAndLoad() val data = mediaDataCaptor.value assertThat(data.active).isTrue() - mediaDataManager.setTimedOut(KEY, timedOut = true) + mediaDataManager.setInactive(KEY, timedOut = true) sessionCallbackCaptor.value.invoke(KEY) // It is converted to a resume player @@ -2249,7 +2249,7 @@ class MediaDataManagerTest : SysuiTestCase() { addNotificationAndLoad() val data = mediaDataCaptor.value assertThat(data.active).isTrue() - mediaDataManager.setTimedOut(KEY, timedOut = true) + mediaDataManager.setInactive(KEY, timedOut = true) sessionCallbackCaptor.value.invoke(KEY) // It is fully removed. diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt new file mode 100644 index 000000000000..564bdc3f5880 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterImplTest.kt @@ -0,0 +1,931 @@ +/* + * 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.media.controls.domain.pipeline + +import android.app.smartspace.SmartspaceAction +import android.os.Bundle +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.internal.logging.InstanceId +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.media.controls.MediaTestUtils +import com.android.systemui.media.controls.data.repository.MediaFilterRepository +import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_RESUME +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.media.controls.ui.controller.MediaPlayerData +import com.android.systemui.media.controls.util.MediaFlags +import com.android.systemui.media.controls.util.MediaUiEventLogger +import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.NotificationLockscreenUserManager +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.Executor +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyLong +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +private const val KEY = "TEST_KEY" +private const val KEY_ALT = "TEST_KEY_2" +private const val USER_MAIN = 0 +private const val USER_GUEST = 10 +private const val PRIVATE_PROFILE = 12 +private const val PACKAGE = "PKG" +private val INSTANCE_ID = InstanceId.fakeInstanceId(123)!! +private const val APP_UID = 99 +private const val SMARTSPACE_KEY = "SMARTSPACE_KEY" +private const val SMARTSPACE_PACKAGE = "SMARTSPACE_PKG" +private val SMARTSPACE_INSTANCE_ID = InstanceId.fakeInstanceId(456)!! + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class MediaDataFilterImplTest : SysuiTestCase() { + + @Mock private lateinit var listener: MediaDataManager.Listener + @Mock private lateinit var userTracker: UserTracker + @Mock private lateinit var broadcastSender: BroadcastSender + @Mock private lateinit var mediaDataManager: MediaDataManager + @Mock private lateinit var lockscreenUserManager: NotificationLockscreenUserManager + @Mock private lateinit var executor: Executor + @Mock private lateinit var smartspaceData: SmartspaceMediaData + @Mock private lateinit var smartspaceMediaRecommendationItem: SmartspaceAction + @Mock private lateinit var logger: MediaUiEventLogger + @Mock private lateinit var mediaFlags: MediaFlags + @Mock private lateinit var cardAction: SmartspaceAction + + private lateinit var mediaDataFilter: MediaDataFilterImpl + private lateinit var mediaFilterRepository: MediaFilterRepository + private lateinit var testScope: TestScope + private lateinit var dataMain: MediaData + private lateinit var dataGuest: MediaData + private lateinit var dataPrivateProfile: MediaData + private val clock = FakeSystemClock() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + MediaPlayerData.clear() + whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false) + testScope = TestScope() + mediaFilterRepository = MediaFilterRepository() + mediaDataFilter = + MediaDataFilterImpl( + context, + userTracker, + broadcastSender, + lockscreenUserManager, + executor, + clock, + logger, + mediaFlags, + mediaFilterRepository, + ) + mediaDataFilter.mediaDataManager = mediaDataManager + mediaDataFilter.addListener(listener) + + // Start all tests as main user + setUser(USER_MAIN) + + // Set up test media data + dataMain = + MediaTestUtils.emptyMediaData.copy( + userId = USER_MAIN, + packageName = PACKAGE, + instanceId = INSTANCE_ID, + appUid = APP_UID + ) + dataGuest = dataMain.copy(userId = USER_GUEST) + dataPrivateProfile = dataMain.copy(userId = PRIVATE_PROFILE) + + whenever(smartspaceData.targetId).thenReturn(SMARTSPACE_KEY) + whenever(smartspaceData.isActive).thenReturn(true) + whenever(smartspaceData.isValid()).thenReturn(true) + whenever(smartspaceData.packageName).thenReturn(SMARTSPACE_PACKAGE) + whenever(smartspaceData.recommendations) + .thenReturn(listOf(smartspaceMediaRecommendationItem)) + whenever(smartspaceData.headphoneConnectionTimeMillis) + .thenReturn(clock.currentTimeMillis() - 100) + whenever(smartspaceData.instanceId).thenReturn(SMARTSPACE_INSTANCE_ID) + whenever(smartspaceData.cardAction).thenReturn(cardAction) + } + + private fun setUser(id: Int) { + whenever(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false) + whenever(lockscreenUserManager.isProfileAvailable(anyInt())).thenReturn(false) + whenever(lockscreenUserManager.isCurrentProfile(eq(id))).thenReturn(true) + whenever(lockscreenUserManager.isProfileAvailable(eq(id))).thenReturn(true) + whenever(lockscreenUserManager.isProfileAvailable(eq(PRIVATE_PROFILE))).thenReturn(true) + mediaDataFilter.handleUserSwitched() + } + + private fun setPrivateProfileUnavailable() { + whenever(lockscreenUserManager.isCurrentProfile(anyInt())).thenReturn(false) + whenever(lockscreenUserManager.isCurrentProfile(eq(USER_MAIN))).thenReturn(true) + whenever(lockscreenUserManager.isCurrentProfile(eq(PRIVATE_PROFILE))).thenReturn(true) + whenever(lockscreenUserManager.isProfileAvailable(eq(PRIVATE_PROFILE))).thenReturn(false) + mediaDataFilter.handleProfileChanged() + } + + @Test + fun testOnDataLoadedForCurrentUser_callsListener() { + // GIVEN a media for main user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + + // THEN we should tell the listener + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataMain), eq(true), eq(0), eq(false)) + } + + @Test + fun testOnDataLoadedForGuest_doesNotCallListener() { + // GIVEN a media for guest user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest) + + // THEN we should NOT tell the listener + verify(listener, never()) + .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + } + + @Test + fun testOnRemovedForCurrent_callsListener() { + // GIVEN a media was removed for main user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + mediaDataFilter.onMediaDataRemoved(KEY) + + // THEN we should tell the listener + verify(listener).onMediaDataRemoved(eq(KEY)) + } + + @Test + fun testOnRemovedForGuest_doesNotCallListener() { + // GIVEN a media was removed for guest user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataGuest) + mediaDataFilter.onMediaDataRemoved(KEY) + + // THEN we should NOT tell the listener + verify(listener, never()).onMediaDataRemoved(eq(KEY)) + } + + @Test + fun testOnUserSwitched_removesOldUserControls() { + // GIVEN that we have a media loaded for main user + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + + // and we switch to guest user + setUser(USER_GUEST) + + // THEN we should remove the main user's media + verify(listener).onMediaDataRemoved(eq(KEY)) + } + + @Test + fun testOnUserSwitched_addsNewUserControls() { + // GIVEN that we had some media for both users + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + mediaDataFilter.onMediaDataLoaded(KEY_ALT, null, dataGuest) + reset(listener) + + // and we switch to guest user + setUser(USER_GUEST) + + // THEN we should add back the guest user media + verify(listener) + .onMediaDataLoaded(eq(KEY_ALT), eq(null), eq(dataGuest), eq(true), eq(0), eq(false)) + + // but not the main user's + verify(listener, never()) + .onMediaDataLoaded(eq(KEY), any(), eq(dataMain), anyBoolean(), anyInt(), anyBoolean()) + } + + @Test + fun testOnProfileChanged_profileUnavailable_loadControls() { + // GIVEN that we had some media for both profiles + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + mediaDataFilter.onMediaDataLoaded(KEY_ALT, null, dataPrivateProfile) + reset(listener) + + // and we change profile status + setPrivateProfileUnavailable() + + // THEN we should add the private profile media + verify(listener).onMediaDataRemoved(eq(KEY_ALT)) + } + + @Test + fun hasAnyMedia_mediaSet_returnsTrue() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain) + + assertThat(hasAnyMedia(selectedUserEntries)).isTrue() + } + + @Test + fun hasAnyMedia_recommendationSet_returnsFalse() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + assertThat(hasAnyMedia(selectedUserEntries)).isFalse() + } + + @Test + fun hasAnyMediaOrRecommendation_mediaSet_returnsTrue() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain) + + assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData)) + .isTrue() + } + + @Test + fun hasAnyMediaOrRecommendation_recommendationSet_returnsTrue() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData)) + .isTrue() + } + + @Test + fun hasActiveMedia_inactiveMediaSet_returnsFalse() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + + val data = dataMain.copy(active = false) + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data) + + assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + } + + @Test + fun hasActiveMedia_activeMediaSet_returnsTrue() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val data = dataMain.copy(active = true) + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data) + + assertThat(hasActiveMedia(selectedUserEntries)).isTrue() + } + + @Test + fun hasActiveMediaOrRecommendation_inactiveMediaSet_returnsFalse() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + val data = dataMain.copy(active = false) + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data) + + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + } + + @Test + fun hasActiveMediaOrRecommendation_activeMediaSet_returnsTrue() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + val data = dataMain.copy(active = true) + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data) + + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isTrue() + } + + @Test + fun hasActiveMediaOrRecommendation_inactiveRecommendationSet_returnsFalse() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + whenever(smartspaceData.isActive).thenReturn(false) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + } + + @Test + fun hasActiveMediaOrRecommendation_invalidRecommendationSet_returnsFalse() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + whenever(smartspaceData.isValid()).thenReturn(false) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + } + + @Test + fun hasActiveMediaOrRecommendation_activeAndValidRecommendationSet_returnsTrue() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + whenever(smartspaceData.isActive).thenReturn(true) + whenever(smartspaceData.isValid()).thenReturn(true) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isTrue() + } + + @Test + fun testHasAnyMediaOrRecommendation_onlyCurrentUser() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData)) + .isFalse() + + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataGuest) + assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData)) + .isFalse() + assertThat(hasAnyMedia(selectedUserEntries)).isFalse() + } + + @Test + fun testHasActiveMediaOrRecommendation_onlyCurrentUser() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + val data = dataGuest.copy(active = true) + + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = data) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + assertThat(hasAnyMedia(selectedUserEntries)).isFalse() + } + + @Test + fun testOnNotificationRemoved_doesNotHaveMedia() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + + mediaDataFilter.onMediaDataLoaded(KEY, oldKey = null, data = dataMain) + mediaDataFilter.onMediaDataRemoved(KEY) + assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData)) + .isFalse() + assertThat(hasAnyMedia(selectedUserEntries)).isFalse() + } + + @Test + fun testOnSwipeToDismiss_setsTimedOut() { + mediaDataFilter.onMediaDataLoaded(KEY, null, dataMain) + mediaDataFilter.onSwipeToDismiss() + + verify(mediaDataManager).setInactive(eq(KEY), eq(true), eq(true)) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_noMedia_activeValidRec_prioritizesSmartspace() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true)) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isTrue() + assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID) + verify(logger, never()).logRecommendationActivated(any(), any(), any()) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_noMedia_inactiveRec_showsNothing() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + + whenever(smartspaceData.isActive).thenReturn(false) + + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + verify(listener, never()) + .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + verify(logger, never()).logRecommendationAdded(any(), any()) + verify(logger, never()).logRecommendationActivated(any(), any(), any()) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_noRecentMedia_activeValidRec_prioritizesSmartspace() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld) + clock.advanceTime(MediaDataFilterImpl.SMARTSPACE_MAX_AGE + 100) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(true)) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isTrue() + assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID) + verify(logger, never()).logRecommendationActivated(any(), any(), any()) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_noRecentMedia_inactiveRec_showsNothing() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + whenever(smartspaceData.isActive).thenReturn(false) + + val dataOld = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataOld) + clock.advanceTime(MediaDataFilterImpl.SMARTSPACE_MAX_AGE + 100) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + verify(logger, never()).logRecommendationAdded(any(), any()) + verify(logger, never()).logRecommendationActivated(any(), any(), any()) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_inactiveRec_showsNothing() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + + whenever(smartspaceData.isActive).thenReturn(false) + + // WHEN we have media that was recently played, but not currently active + val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) + + // AND we get a smartspace signal + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + // THEN we should tell listeners to treat the media as not active instead + verify(listener, never()) + .onMediaDataLoaded(eq(KEY), eq(KEY), any(), anyBoolean(), anyInt(), anyBoolean()) + verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + verify(logger, never()).logRecommendationAdded(any(), any()) + verify(logger, never()).logRecommendationActivated(any(), any(), any()) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeInvalidRec_usesMedia() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + whenever(smartspaceData.isValid()).thenReturn(false) + + // WHEN we have media that was recently played, but not currently active + val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) + + // AND we get a smartspace signal + runCurrent() + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + // THEN we should tell listeners to treat the media as active instead + val dataCurrentAndActive = dataCurrent.copy(active = true) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + eq(dataCurrentAndActive), + eq(true), + eq(100), + eq(true) + ) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isTrue() + // Smartspace update shouldn't be propagated for the empty rec list. + verify(listener, never()).onSmartspaceMediaDataLoaded(any(), any(), anyBoolean()) + verify(logger, never()).logRecommendationAdded(any(), any()) + verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID)) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasRecentMedia_activeValidRec_usesBoth() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + // WHEN we have media that was recently played, but not currently active + val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) + + // AND we get a smartspace signal + runCurrent() + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + // THEN we should tell listeners to treat the media as active instead + val dataCurrentAndActive = dataCurrent.copy(active = true) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + eq(dataCurrentAndActive), + eq(true), + eq(100), + eq(true) + ) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isTrue() + // Smartspace update should also be propagated but not prioritized. + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) + verify(logger).logRecommendationAdded(SMARTSPACE_PACKAGE, SMARTSPACE_INSTANCE_ID) + verify(logger).logRecommendationActivated(eq(APP_UID), eq(PACKAGE), eq(INSTANCE_ID)) + } + + @Test + fun testOnSmartspaceMediaDataRemoved_usedSmartspace_clearsSmartspace() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) + + verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + } + + @Test + fun testOnSmartspaceMediaDataRemoved_usedMediaAndSmartspace_clearsBoth() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) + + runCurrent() + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + val dataCurrentAndActive = dataCurrent.copy(active = true) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + eq(dataCurrentAndActive), + eq(true), + eq(100), + eq(true) + ) + + mediaDataFilter.onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) + + verify(listener).onSmartspaceMediaDataRemoved(SMARTSPACE_KEY) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + assertThat(hasActiveMedia(selectedUserEntries)).isFalse() + } + + @Test + fun testOnSmartspaceLoaded_persistentEnabled_isInactive_notifiesListeners() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + whenever(smartspaceData.isActive).thenReturn(false) + + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData)) + .isTrue() + } + + @Test + fun testOnSmartspaceLoaded_persistentEnabled_inactive_hasRecentMedia_staysInactive() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + + whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + whenever(smartspaceData.isActive).thenReturn(false) + + // If there is media that was recently played but inactive + val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) + + // And an inactive recommendation is loaded + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + // Smartspace is loaded but the media stays inactive + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) + verify(listener, never()) + .onMediaDataLoaded(any(), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isFalse() + assertThat(hasAnyMediaOrRecommendation(selectedUserEntries, smartspaceMediaData)) + .isTrue() + } + + @Test + fun testOnSwipeToDismiss_persistentEnabled_recommendationSetInactive() { + whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + + val data = + EMPTY_SMARTSPACE_MEDIA_DATA.copy( + targetId = SMARTSPACE_KEY, + isActive = true, + packageName = SMARTSPACE_PACKAGE, + recommendations = listOf(smartspaceMediaRecommendationItem), + ) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, data) + mediaDataFilter.onSwipeToDismiss() + + verify(mediaDataManager).setRecommendationInactive(eq(SMARTSPACE_KEY)) + verify(mediaDataManager, never()) + .dismissSmartspaceRecommendation(eq(SMARTSPACE_KEY), anyLong()) + } + + @Test + fun testSmartspaceLoaded_shouldTriggerResume_doesTrigger() = + testScope.runTest { + val selectedUserEntries by collectLastValue(mediaFilterRepository.selectedUserEntries) + val smartspaceMediaData by collectLastValue(mediaFilterRepository.smartspaceMediaData) + val reactivatedKey by collectLastValue(mediaFilterRepository.reactivatedKey) + // WHEN we have media that was recently played, but not currently active + val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) + + // AND we get a smartspace signal with extra to trigger resume + runCurrent() + val extras = Bundle().apply { putBoolean(EXTRA_KEY_TRIGGER_RESUME, true) } + whenever(cardAction.extras).thenReturn(extras) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + // THEN we should tell listeners to treat the media as active instead + val dataCurrentAndActive = dataCurrent.copy(active = true) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + eq(dataCurrentAndActive), + eq(true), + eq(100), + eq(true) + ) + assertThat( + hasActiveMediaOrRecommendation( + selectedUserEntries, + smartspaceMediaData, + reactivatedKey + ) + ) + .isTrue() + // And send the smartspace data, but not prioritized + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) + } + + @Test + fun testSmartspaceLoaded_notShouldTriggerResume_doesNotTrigger() { + // WHEN we have media that was recently played, but not currently active + val dataCurrent = dataMain.copy(active = false, lastActive = clock.elapsedRealtime()) + mediaDataFilter.onMediaDataLoaded(KEY, null, dataCurrent) + verify(listener) + .onMediaDataLoaded(eq(KEY), eq(null), eq(dataCurrent), eq(true), eq(0), eq(false)) + + // AND we get a smartspace signal with extra to not trigger resume + val extras = Bundle().apply { putBoolean(EXTRA_KEY_TRIGGER_RESUME, false) } + whenever(cardAction.extras).thenReturn(extras) + mediaDataFilter.onSmartspaceMediaDataLoaded(SMARTSPACE_KEY, smartspaceData) + + // THEN listeners are not updated to show media + verify(listener, never()) + .onMediaDataLoaded(eq(KEY), eq(KEY), any(), eq(true), eq(100), eq(true)) + // But the smartspace update is still propagated + verify(listener) + .onSmartspaceMediaDataLoaded(eq(SMARTSPACE_KEY), eq(smartspaceData), eq(false)) + } + + private fun hasActiveMediaOrRecommendation( + entries: Map<String, MediaData>?, + smartspaceMediaData: SmartspaceMediaData?, + reactivatedKey: String? + ): Boolean { + if (entries == null || smartspaceMediaData == null) { + return false + } + return entries.any { it.value.active } || + (smartspaceMediaData.isActive && + (smartspaceMediaData.isValid() || reactivatedKey != null)) + } + + private fun hasActiveMedia(entries: Map<String, MediaData>?): Boolean { + return entries?.any { it.value.active } ?: false + } + + private fun hasAnyMediaOrRecommendation( + entries: Map<String, MediaData>?, + smartspaceMediaData: SmartspaceMediaData? + ): Boolean { + if (entries == null || smartspaceMediaData == null) { + return false + } + return entries.isNotEmpty() || + (if (mediaFlags.isPersistentSsCardEnabled()) { + smartspaceMediaData.isValid() + } else { + smartspaceMediaData.isActive && smartspaceMediaData.isValid() + }) + } + + private fun hasAnyMedia(entries: Map<String, MediaData>?): Boolean { + return entries?.isNotEmpty() ?: false + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt new file mode 100644 index 000000000000..5c275b454681 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorTest.kt @@ -0,0 +1,2474 @@ +/* + * 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.media.controls.domain.pipeline + +import android.app.IUriGrantsManager +import android.app.Notification +import android.app.Notification.FLAG_NO_CLEAR +import android.app.Notification.MediaStyle +import android.app.PendingIntent +import android.app.UriGrantsManager +import android.app.smartspace.SmartspaceAction +import android.app.smartspace.SmartspaceConfig +import android.app.smartspace.SmartspaceManager +import android.app.smartspace.SmartspaceTarget +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.ImageDecoder +import android.graphics.drawable.Icon +import android.media.MediaDescription +import android.media.MediaMetadata +import android.media.session.MediaController +import android.media.session.MediaSession +import android.media.session.PlaybackState +import android.net.Uri +import android.os.Bundle +import android.provider.Settings +import android.service.notification.StatusBarNotification +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.TestableLooper.RunWithLooper +import androidx.media.utils.MediaConstants +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.internal.logging.InstanceId +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.InstanceIdSequenceFake +import com.android.systemui.SysuiTestCase +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.dump.DumpManager +import com.android.systemui.media.controls.data.repository.MediaDataRepository +import com.android.systemui.media.controls.data.repository.MediaFilterRepository +import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor +import com.android.systemui.media.controls.domain.resume.MediaResumeListener +import com.android.systemui.media.controls.domain.resume.ResumeMediaBrowser +import com.android.systemui.media.controls.shared.model.EXTRA_KEY_TRIGGER_SOURCE +import com.android.systemui.media.controls.shared.model.EXTRA_VALUE_TRIGGER_PERIODIC +import com.android.systemui.media.controls.shared.model.MediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaData +import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider +import com.android.systemui.media.controls.util.MediaControllerFactory +import com.android.systemui.media.controls.util.MediaFlags +import com.android.systemui.media.controls.util.MediaUiEventLogger +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.res.R +import com.android.systemui.statusbar.SbnBuilder +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.FakeSettings +import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.utils.os.FakeHandler +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.MockitoSession +import org.mockito.junit.MockitoJUnit +import org.mockito.quality.Strictness + +private const val KEY = "KEY" +private const val KEY_2 = "KEY_2" +private const val KEY_MEDIA_SMARTSPACE = "MEDIA_SMARTSPACE_ID" +private const val SMARTSPACE_CREATION_TIME = 1234L +private const val SMARTSPACE_EXPIRY_TIME = 5678L +private const val PACKAGE_NAME = "com.example.app" +private const val SYSTEM_PACKAGE_NAME = "com.android.systemui" +private const val APP_NAME = "SystemUI" +private const val SESSION_ARTIST = "artist" +private const val SESSION_TITLE = "title" +private const val SESSION_BLANK_TITLE = " " +private const val SESSION_EMPTY_TITLE = "" +private const val USER_ID = 0 +private val DISMISS_INTENT = Intent().apply { action = "dismiss" } + +private fun <T> anyObject(): T { + return Mockito.anyObject<T>() +} + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidTestingRunner::class) +class MediaDataProcessorTest : SysuiTestCase() { + + @JvmField @Rule val mockito = MockitoJUnit.rule() + @Mock lateinit var mediaControllerFactory: MediaControllerFactory + @Mock lateinit var controller: MediaController + @Mock lateinit var transportControls: MediaController.TransportControls + @Mock lateinit var playbackInfo: MediaController.PlaybackInfo + lateinit var session: MediaSession + private lateinit var metadataBuilder: MediaMetadata.Builder + lateinit var backgroundExecutor: FakeExecutor + private lateinit var foregroundExecutor: FakeExecutor + lateinit var uiExecutor: FakeExecutor + @Mock lateinit var dumpManager: DumpManager + @Mock lateinit var broadcastDispatcher: BroadcastDispatcher + @Mock lateinit var mediaTimeoutListener: MediaTimeoutListener + @Mock lateinit var mediaResumeListener: MediaResumeListener + @Mock lateinit var mediaSessionBasedFilter: MediaSessionBasedFilter + @Mock lateinit var mediaDeviceManager: MediaDeviceManager + @Mock lateinit var mediaDataCombineLatest: MediaDataCombineLatest + @Mock lateinit var mediaDataFilter: MediaDataFilterImpl + @Mock lateinit var listener: MediaDataManager.Listener + @Mock lateinit var pendingIntent: PendingIntent + @Mock lateinit var activityStarter: ActivityStarter + @Mock lateinit var smartspaceManager: SmartspaceManager + @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + private lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider + @Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget + @Mock private lateinit var mediaRecommendationItem: SmartspaceAction + private lateinit var validRecommendationList: List<SmartspaceAction> + @Mock private lateinit var mediaSmartspaceBaseAction: SmartspaceAction + @Mock private lateinit var mediaFlags: MediaFlags + @Mock private lateinit var logger: MediaUiEventLogger + private lateinit var mediaCarouselInteractor: MediaCarouselInteractor + private lateinit var mediaDataProcessor: MediaDataProcessor + private lateinit var mediaNotification: StatusBarNotification + private lateinit var remoteCastNotification: StatusBarNotification + @Captor lateinit var mediaDataCaptor: ArgumentCaptor<MediaData> + private val clock = FakeSystemClock() + @Captor lateinit var stateCallbackCaptor: ArgumentCaptor<(String, PlaybackState) -> Unit> + @Captor lateinit var sessionCallbackCaptor: ArgumentCaptor<(String) -> Unit> + @Captor lateinit var smartSpaceConfigBuilderCaptor: ArgumentCaptor<SmartspaceConfig> + @Mock private lateinit var ugm: IUriGrantsManager + @Mock private lateinit var imageSource: ImageDecoder.Source + private lateinit var mediaDataRepository: MediaDataRepository + private lateinit var mediaFilterRepository: MediaFilterRepository + private lateinit var testScope: TestScope + private lateinit var testDispatcher: TestDispatcher + private lateinit var testableLooper: TestableLooper + private lateinit var fakeHandler: FakeHandler + + private val settings = FakeSettings() + private val instanceIdSequence = InstanceIdSequenceFake(1 shl 20) + + private val originalSmartspaceSetting = + Settings.Secure.getInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + 1 + ) + + private lateinit var staticMockSession: MockitoSession + + @Before + fun setup() { + whenever(mediaFlags.isMediaControlsRefactorEnabled()).thenReturn(true) + + staticMockSession = + ExtendedMockito.mockitoSession() + .mockStatic<UriGrantsManager>(UriGrantsManager::class.java) + .mockStatic<ImageDecoder>(ImageDecoder::class.java) + .strictness(Strictness.LENIENT) + .startMocking() + whenever(UriGrantsManager.getService()).thenReturn(ugm) + foregroundExecutor = FakeExecutor(clock) + backgroundExecutor = FakeExecutor(clock) + uiExecutor = FakeExecutor(clock) + testableLooper = TestableLooper.get(this) + fakeHandler = FakeHandler(testableLooper.looper) + smartspaceMediaDataProvider = SmartspaceMediaDataProvider() + Settings.Secure.putInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + 1 + ) + testDispatcher = UnconfinedTestDispatcher() + testScope = TestScope(testDispatcher) + mediaFilterRepository = MediaFilterRepository() + mediaDataRepository = MediaDataRepository(mediaFlags, dumpManager) + mediaDataProcessor = + MediaDataProcessor( + context = context, + applicationScope = testScope, + backgroundDispatcher = testDispatcher, + backgroundExecutor = backgroundExecutor, + uiExecutor = uiExecutor, + foregroundExecutor = foregroundExecutor, + handler = fakeHandler, + mediaControllerFactory = mediaControllerFactory, + broadcastDispatcher = broadcastDispatcher, + dumpManager = dumpManager, + activityStarter = activityStarter, + smartspaceMediaDataProvider = smartspaceMediaDataProvider, + useMediaResumption = true, + useQsMediaPlayer = true, + systemClock = clock, + secureSettings = settings, + mediaFlags = mediaFlags, + logger = logger, + smartspaceManager = smartspaceManager, + keyguardUpdateMonitor = keyguardUpdateMonitor, + mediaDataRepository = mediaDataRepository, + ) + mediaDataProcessor.start() + mediaCarouselInteractor = + MediaCarouselInteractor( + applicationScope = testScope.backgroundScope, + mediaDataRepository = mediaDataRepository, + mediaDataProcessor = mediaDataProcessor, + mediaTimeoutListener = mediaTimeoutListener, + mediaResumeListener = mediaResumeListener, + mediaSessionBasedFilter = mediaSessionBasedFilter, + mediaDeviceManager = mediaDeviceManager, + mediaDataCombineLatest = mediaDataCombineLatest, + mediaDataFilter = mediaDataFilter, + mediaFilterRepository = mediaFilterRepository, + mediaFlags = mediaFlags + ) + mediaCarouselInteractor.start() + verify(mediaTimeoutListener).stateCallback = capture(stateCallbackCaptor) + verify(mediaTimeoutListener).sessionCallback = capture(sessionCallbackCaptor) + session = MediaSession(context, "MediaDataProcessorTestSession") + mediaNotification = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + } + build() + } + remoteCastNotification = + SbnBuilder().run { + setPkg(SYSTEM_PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle( + MediaStyle().apply { + setMediaSession(session.sessionToken) + setRemotePlaybackInfo("Remote device", 0, null) + } + ) + } + build() + } + metadataBuilder = + MediaMetadata.Builder().apply { + putString(MediaMetadata.METADATA_KEY_ARTIST, SESSION_ARTIST) + putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_TITLE) + } + verify(smartspaceManager).createSmartspaceSession(capture(smartSpaceConfigBuilderCaptor)) + whenever(mediaControllerFactory.create(eq(session.sessionToken))).thenReturn(controller) + whenever(controller.transportControls).thenReturn(transportControls) + whenever(controller.playbackInfo).thenReturn(playbackInfo) + whenever(controller.metadata).thenReturn(metadataBuilder.build()) + whenever(playbackInfo.playbackType) + .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) + + // This is an ugly hack for now. The mediaSessionBasedFilter is one of the internal + // listeners in the internal processing pipeline. It receives events, but ince it is a + // mock, it doesn't pass those events along the chain to the external listeners. So, just + // treat mediaSessionBasedFilter as a listener for testing. + listener = mediaSessionBasedFilter + + val recommendationExtras = + Bundle().apply { + putString("package_name", PACKAGE_NAME) + putParcelable("dismiss_intent", DISMISS_INTENT) + } + val icon = Icon.createWithResource(context, android.R.drawable.ic_media_play) + whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras) + whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction) + whenever(mediaRecommendationItem.extras).thenReturn(recommendationExtras) + whenever(mediaRecommendationItem.icon).thenReturn(icon) + validRecommendationList = + listOf(mediaRecommendationItem, mediaRecommendationItem, mediaRecommendationItem) + whenever(mediaSmartspaceTarget.smartspaceTargetId).thenReturn(KEY_MEDIA_SMARTSPACE) + whenever(mediaSmartspaceTarget.featureType).thenReturn(SmartspaceTarget.FEATURE_MEDIA) + whenever(mediaSmartspaceTarget.iconGrid).thenReturn(validRecommendationList) + whenever(mediaSmartspaceTarget.creationTimeMillis).thenReturn(SMARTSPACE_CREATION_TIME) + whenever(mediaSmartspaceTarget.expiryTimeMillis).thenReturn(SMARTSPACE_EXPIRY_TIME) + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(false) + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(false) + whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false) + whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(false) + whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId()) + whenever(keyguardUpdateMonitor.isUserInLockdown(any())).thenReturn(false) + } + + @After + fun tearDown() { + staticMockSession.finishMocking() + session.release() + mediaDataProcessor.destroy() + Settings.Secure.putInt( + context.contentResolver, + Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, + originalSmartspaceSetting + ) + } + + @Test + fun testsetInactive_active_deactivatesMedia() { + addNotificationAndLoad() + val data = mediaDataCaptor.value + assertThat(data.active).isTrue() + + mediaDataProcessor.setInactive(KEY, timedOut = true) + assertThat(data.active).isFalse() + verify(logger).logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test + fun testsetInactive_resume_dismissesMedia() { + // WHEN resume controls are present, and time out + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + mediaDataProcessor.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME + ) + + backgroundExecutor.runAllReady() + foregroundExecutor.runAllReady() + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + + mediaDataProcessor.setInactive(PACKAGE_NAME, timedOut = true) + verify(logger) + .logMediaTimeout(anyInt(), eq(PACKAGE_NAME), eq(mediaDataCaptor.value.instanceId)) + + // THEN it is removed and listeners are informed + foregroundExecutor.advanceClockToLast() + foregroundExecutor.runAllReady() + verify(listener).onMediaDataRemoved(PACKAGE_NAME) + } + + @Test + fun testLoadsMetadataOnBackground() { + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + assertThat(backgroundExecutor.numPending()).isEqualTo(1) + } + + @Test + fun testLoadMetadata_withExplicitIndicator() { + whenever(controller.metadata) + .thenReturn( + metadataBuilder + .putLong( + MediaConstants.METADATA_KEY_IS_EXPLICIT, + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + ) + .build() + ) + + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value!!.isExplicit).isTrue() + } + + @Test + fun testOnMetaDataLoaded_withoutExplicitIndicator() { + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value!!.isExplicit).isFalse() + } + + @Test + fun testOnMetaDataLoaded_callsListener() { + addNotificationAndLoad() + verify(logger) + .logActiveMediaAdded( + anyInt(), + eq(PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId), + eq(MediaData.PLAYBACK_LOCAL) + ) + } + + @Test + fun testOnMetaDataLoaded_conservesActiveFlag() { + whenever(mediaControllerFactory.create(anyObject())).thenReturn(controller) + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value!!.active).isTrue() + } + + @Test + fun testOnNotificationAdded_isRcn_markedRemote() { + addNotificationAndLoad(remoteCastNotification) + + assertThat(mediaDataCaptor.value!!.playbackLocation) + .isEqualTo(MediaData.PLAYBACK_CAST_REMOTE) + verify(logger) + .logActiveMediaAdded( + anyInt(), + eq(SYSTEM_PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId), + eq(MediaData.PLAYBACK_CAST_REMOTE) + ) + } + + @Test + fun testOnNotificationAdded_hasSubstituteName_isUsed() { + val subName = "Substitute Name" + val notif = + SbnBuilder().run { + modifyNotification(context).also { + it.extras = + Bundle().apply { + putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, subName) + } + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + } + build() + } + + mediaDataProcessor.onNotificationAdded(KEY, notif) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + + assertThat(mediaDataCaptor.value!!.app).isEqualTo(subName) + } + + @Test + fun testLoadMediaDataInBg_invalidTokenNoCrash() { + val bundle = Bundle() + // wrong data type + bundle.putParcelable(Notification.EXTRA_MEDIA_SESSION, Bundle()) + val rcn = + SbnBuilder().run { + setPkg(SYSTEM_PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.addExtras(bundle) + it.setStyle( + MediaStyle().apply { setRemotePlaybackInfo("Remote device", 0, null) } + ) + } + build() + } + + mediaDataProcessor.loadMediaDataInBg(KEY, rcn, null) + // no crash even though the data structure is incorrect + } + + @Test + fun testLoadMediaDataInBg_invalidMediaRemoteIntentNoCrash() { + val bundle = Bundle() + // wrong data type + bundle.putParcelable(Notification.EXTRA_MEDIA_REMOTE_INTENT, Bundle()) + val rcn = + SbnBuilder().run { + setPkg(SYSTEM_PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.addExtras(bundle) + it.setStyle( + MediaStyle().apply { + setMediaSession(session.sessionToken) + setRemotePlaybackInfo("Remote device", 0, null) + } + ) + } + build() + } + + mediaDataProcessor.loadMediaDataInBg(KEY, rcn, null) + // no crash even though the data structure is incorrect + } + + @Test + fun testOnNotificationRemoved_callsListener() { + addNotificationAndLoad() + val data = mediaDataCaptor.value + mediaDataProcessor.onNotificationRemoved(KEY) + verify(listener).onMediaDataRemoved(eq(KEY)) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test + fun testOnNotificationAdded_emptyTitle_hasPlaceholder() { + // When the manager has a notification with an empty title, and the app is not + // required to include a non-empty title + val mockPackageManager = mock(PackageManager::class.java) + context.setMockPackageManager(mockPackageManager) + whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME) + whenever(controller.metadata) + .thenReturn( + metadataBuilder + .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_EMPTY_TITLE) + .build() + ) + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + + // Then a media control is created with a placeholder title string + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + val placeholderTitle = context.getString(R.string.controls_media_empty_title, APP_NAME) + assertThat(mediaDataCaptor.value.song).isEqualTo(placeholderTitle) + } + + @Test + fun testOnNotificationAdded_blankTitle_hasPlaceholder() { + // GIVEN that the manager has a notification with a blank title, and the app is not + // required to include a non-empty title + val mockPackageManager = mock(PackageManager::class.java) + context.setMockPackageManager(mockPackageManager) + whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME) + whenever(controller.metadata) + .thenReturn( + metadataBuilder + .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_BLANK_TITLE) + .build() + ) + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + + // Then a media control is created with a placeholder title string + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + val placeholderTitle = context.getString(R.string.controls_media_empty_title, APP_NAME) + assertThat(mediaDataCaptor.value.song).isEqualTo(placeholderTitle) + } + + @Test + fun testOnNotificationAdded_emptyMetadata_usesNotificationTitle() { + // When the app sets the metadata title fields to empty strings, but does include a + // non-blank notification title + val mockPackageManager = mock(PackageManager::class.java) + context.setMockPackageManager(mockPackageManager) + whenever(mockPackageManager.getApplicationLabel(any())).thenReturn(APP_NAME) + whenever(controller.metadata) + .thenReturn( + metadataBuilder + .putString(MediaMetadata.METADATA_KEY_TITLE, SESSION_EMPTY_TITLE) + .putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, SESSION_EMPTY_TITLE) + .build() + ) + mediaNotification = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setContentTitle(SESSION_TITLE) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + } + build() + } + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + + // Then the media control is added using the notification's title + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.song).isEqualTo(SESSION_TITLE) + } + + @Test + fun testOnNotificationRemoved_emptyTitle_notConverted() { + // GIVEN that the manager has a notification with a resume action and empty title. + addNotificationAndLoad() + val data = mediaDataCaptor.value + val instanceId = data.instanceId + assertThat(data.resumption).isFalse() + mediaDataProcessor.onMediaDataLoaded( + KEY, + null, + data.copy(song = SESSION_EMPTY_TITLE, resumeAction = Runnable {}) + ) + + // WHEN the notification is removed + reset(listener) + mediaDataProcessor.onNotificationRemoved(KEY) + + // THEN active media is not converted to resume. + verify(listener, never()) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + verify(logger, never()) + .logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(instanceId)) + verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any()) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(instanceId)) + } + + @Test + fun testOnNotificationRemoved_blankTitle_notConverted() { + // GIVEN that the manager has a notification with a resume action and blank title. + addNotificationAndLoad() + val data = mediaDataCaptor.value + val instanceId = data.instanceId + assertThat(data.resumption).isFalse() + mediaDataProcessor.onMediaDataLoaded( + KEY, + null, + data.copy(song = SESSION_BLANK_TITLE, resumeAction = Runnable {}) + ) + + // WHEN the notification is removed + reset(listener) + mediaDataProcessor.onNotificationRemoved(KEY) + + // THEN active media is not converted to resume. + verify(listener, never()) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + verify(logger, never()) + .logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(instanceId)) + verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any()) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(instanceId)) + } + + @Test + fun testOnNotificationRemoved_withResumption() { + // GIVEN that the manager has a notification with a resume action + addNotificationAndLoad() + val data = mediaDataCaptor.value + assertThat(data.resumption).isFalse() + mediaDataProcessor.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {})) + // WHEN the notification is removed + mediaDataProcessor.onNotificationRemoved(KEY) + // THEN the media data indicates that it is for resumption + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.isPlaying).isFalse() + verify(logger).logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test + fun testOnNotificationRemoved_twoWithResumption() { + // GIVEN that the manager has two notifications with resume actions + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + mediaDataProcessor.onNotificationAdded(KEY_2, mediaNotification) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(2) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(2) + + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + val data = mediaDataCaptor.value + assertThat(data.resumption).isFalse() + + verify(listener) + .onMediaDataLoaded( + eq(KEY_2), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + val data2 = mediaDataCaptor.value + assertThat(data2.resumption).isFalse() + + mediaDataProcessor.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {})) + mediaDataProcessor.onMediaDataLoaded(KEY_2, null, data2.copy(resumeAction = Runnable {})) + reset(listener) + // WHEN the first is removed + mediaDataProcessor.onNotificationRemoved(KEY) + // THEN the data is for resumption and the key is migrated to the package name + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + verify(listener, never()).onMediaDataRemoved(eq(KEY)) + // WHEN the second is removed + mediaDataProcessor.onNotificationRemoved(KEY_2) + // THEN the data is for resumption and the second key is removed + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(PACKAGE_NAME), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + verify(listener).onMediaDataRemoved(eq(KEY_2)) + } + + @Test + fun testOnNotificationRemoved_withResumption_butNotLocal() { + // GIVEN that the manager has a notification with a resume action, but is not local + whenever(playbackInfo.playbackType) + .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) + addNotificationAndLoad() + val data = mediaDataCaptor.value + val dataRemoteWithResume = + data.copy(resumeAction = Runnable {}, playbackLocation = MediaData.PLAYBACK_CAST_LOCAL) + mediaDataProcessor.onMediaDataLoaded(KEY, null, dataRemoteWithResume) + verify(logger) + .logActiveMediaAdded( + anyInt(), + eq(PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId), + eq(MediaData.PLAYBACK_CAST_LOCAL) + ) + + // WHEN the notification is removed + mediaDataProcessor.onNotificationRemoved(KEY) + + // THEN the media data is removed + verify(listener).onMediaDataRemoved(eq(KEY)) + } + + @Test + fun testOnNotificationRemoved_withResumption_isRemoteAndRemoteAllowed() { + // With the flag enabled to allow remote media to resume + whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(true) + + // GIVEN that the manager has a notification with a resume action, but is not local + whenever(controller.metadata).thenReturn(metadataBuilder.build()) + whenever(playbackInfo.playbackType) + .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) + addNotificationAndLoad() + val data = mediaDataCaptor.value + val dataRemoteWithResume = + data.copy(resumeAction = Runnable {}, playbackLocation = MediaData.PLAYBACK_CAST_LOCAL) + mediaDataProcessor.onMediaDataLoaded(KEY, null, dataRemoteWithResume) + + // WHEN the notification is removed + mediaDataProcessor.onNotificationRemoved(KEY) + + // THEN the media data is converted to a resume state + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + } + + @Test + fun testOnNotificationRemoved_withResumption_isRcnAndRemoteAllowed() { + // With the flag enabled to allow remote media to resume + whenever(mediaFlags.isRemoteResumeAllowed()).thenReturn(true) + + // GIVEN that the manager has a remote cast notification + addNotificationAndLoad(remoteCastNotification) + val data = mediaDataCaptor.value + assertThat(data.playbackLocation).isEqualTo(MediaData.PLAYBACK_CAST_REMOTE) + val dataRemoteWithResume = data.copy(resumeAction = Runnable {}) + mediaDataProcessor.onMediaDataLoaded(KEY, null, dataRemoteWithResume) + + // WHEN the RCN is removed + mediaDataProcessor.onNotificationRemoved(KEY) + + // THEN the media data is removed + verify(listener).onMediaDataRemoved(eq(KEY)) + } + + @Test + fun testOnNotificationRemoved_withResumption_tooManyPlayers() { + // Given the maximum number of resume controls already + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + for (i in 0..ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) { + addResumeControlAndLoad(desc, "$i:$PACKAGE_NAME") + clock.advanceTime(1000) + } + + // And an active, resumable notification + addNotificationAndLoad() + val data = mediaDataCaptor.value + assertThat(data.resumption).isFalse() + mediaDataProcessor.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {})) + + // When the notification is removed + mediaDataProcessor.onNotificationRemoved(KEY) + + // Then it is converted to resumption + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.isPlaying).isFalse() + + // And the oldest resume control was removed + verify(listener).onMediaDataRemoved(eq("0:$PACKAGE_NAME")) + } + + fun testOnNotificationRemoved_lockDownMode() { + whenever(keyguardUpdateMonitor.isUserInLockdown(any())).thenReturn(true) + + addNotificationAndLoad() + val data = mediaDataCaptor.value + mediaDataProcessor.onNotificationRemoved(KEY) + + verify(listener, never()).onMediaDataRemoved(eq(KEY)) + verify(logger, never()) + .logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test + fun testAddResumptionControls() { + // WHEN resumption controls are added + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + val currentTime = clock.elapsedRealtime() + addResumeControlAndLoad(desc) + + val data = mediaDataCaptor.value + assertThat(data.resumption).isTrue() + assertThat(data.song).isEqualTo(SESSION_TITLE) + assertThat(data.app).isEqualTo(APP_NAME) + assertThat(data.actions).hasSize(1) + assertThat(data.semanticActions!!.playOrPause).isNotNull() + assertThat(data.lastActive).isAtLeast(currentTime) + verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test + fun testAddResumptionControls_withExplicitIndicator() { + val bundle = Bundle() + // WHEN resumption controls are added with explicit indicator + bundle.putLong( + MediaConstants.METADATA_KEY_IS_EXPLICIT, + MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT + ) + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + setExtras(bundle) + build() + } + val currentTime = clock.elapsedRealtime() + addResumeControlAndLoad(desc) + + val data = mediaDataCaptor.value + assertThat(data.resumption).isTrue() + assertThat(data.song).isEqualTo(SESSION_TITLE) + assertThat(data.app).isEqualTo(APP_NAME) + assertThat(data.actions).hasSize(1) + assertThat(data.semanticActions!!.playOrPause).isNotNull() + assertThat(data.lastActive).isAtLeast(currentTime) + assertThat(data.isExplicit).isTrue() + verify(logger).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test + fun testAddResumptionControls_hasPartialProgress() { + whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + + // WHEN resumption controls are added with partial progress + val progress = 0.5 + val extras = + Bundle().apply { + putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED + ) + putDouble(MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress) + } + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + setExtras(extras) + build() + } + addResumeControlAndLoad(desc) + + val data = mediaDataCaptor.value + assertThat(data.resumption).isTrue() + assertThat(data.resumeProgress).isEqualTo(progress) + } + + @Test + fun testAddResumptionControls_hasNotPlayedProgress() { + whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + + // WHEN resumption controls are added that have not been played + val extras = + Bundle().apply { + putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED + ) + } + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + setExtras(extras) + build() + } + addResumeControlAndLoad(desc) + + val data = mediaDataCaptor.value + assertThat(data.resumption).isTrue() + assertThat(data.resumeProgress).isEqualTo(0) + } + + @Test + fun testAddResumptionControls_hasFullProgress() { + whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + + // WHEN resumption controls are added with progress info + val extras = + Bundle().apply { + putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED + ) + } + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + setExtras(extras) + build() + } + addResumeControlAndLoad(desc) + + // THEN the media data includes the progress + val data = mediaDataCaptor.value + assertThat(data.resumption).isTrue() + assertThat(data.resumeProgress).isEqualTo(1) + } + + @Test + fun testAddResumptionControls_hasNoExtras() { + whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + + // WHEN resumption controls are added that do not have any extras + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + addResumeControlAndLoad(desc) + + // Resume progress is null + val data = mediaDataCaptor.value + assertThat(data.resumption).isTrue() + assertThat(data.resumeProgress).isEqualTo(null) + } + + @Test + fun testAddResumptionControls_hasEmptyTitle() { + whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + + // WHEN resumption controls are added that have empty title + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_EMPTY_TITLE) + build() + } + mediaDataProcessor.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME + ) + + // Resumption controls are not added. + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(0) + verify(listener, never()) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + } + + @Test + fun testAddResumptionControls_hasBlankTitle() { + whenever(mediaFlags.isResumeProgressEnabled()).thenReturn(true) + + // WHEN resumption controls are added that have a blank title + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_BLANK_TITLE) + build() + } + mediaDataProcessor.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME + ) + + // Resumption controls are not added. + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(0) + verify(listener, never()) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + } + + @Test + fun testResumptionDisabled_dismissesResumeControls() { + // WHEN there are resume controls and resumption is switched off + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + addResumeControlAndLoad(desc) + + val data = mediaDataCaptor.value + mediaDataProcessor.setMediaResumptionEnabled(false) + + // THEN the resume controls are dismissed + verify(listener).onMediaDataRemoved(eq(PACKAGE_NAME)) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test + fun testDismissMedia_listenerCalled() { + addNotificationAndLoad() + val data = mediaDataCaptor.value + val removed = mediaDataProcessor.dismissMediaData(KEY, 0L) + assertThat(removed).isTrue() + + foregroundExecutor.advanceClockToLast() + foregroundExecutor.runAllReady() + + verify(listener).onMediaDataRemoved(eq(KEY)) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + } + + @Test + fun testDismissMedia_keyDoesNotExist_returnsFalse() { + val removed = mediaDataProcessor.dismissMediaData(KEY, 0L) + assertThat(removed).isFalse() + } + + @Test + fun testBadArtwork_doesNotUse() { + // WHEN notification has a too-small artwork + val artwork = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + val notif = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + it.setLargeIcon(artwork) + } + build() + } + mediaDataProcessor.onNotificationAdded(KEY, notif) + + // THEN it still loads + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasNewValidMediaTarget_callsListener() { + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + verify(logger).getNewInstanceId() + val instanceId = instanceIdSequence.lastInstanceId + + verify(listener) + .onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq( + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + packageName = PACKAGE_NAME, + cardAction = mediaSmartspaceBaseAction, + recommendations = validRecommendationList, + dismissIntent = DISMISS_INTENT, + headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME, + instanceId = InstanceId.fakeInstanceId(instanceId), + expiryTimeMs = SMARTSPACE_EXPIRY_TIME, + ) + ), + eq(false) + ) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasNewInvalidMediaTarget_callsListener() { + whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf()) + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + verify(logger).getNewInstanceId() + val instanceId = instanceIdSequence.lastInstanceId + + verify(listener) + .onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq( + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + dismissIntent = DISMISS_INTENT, + headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME, + instanceId = InstanceId.fakeInstanceId(instanceId), + expiryTimeMs = SMARTSPACE_EXPIRY_TIME, + ) + ), + eq(false) + ) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasNullIntent_callsListener() { + val recommendationExtras = + Bundle().apply { + putString("package_name", PACKAGE_NAME) + putParcelable("dismiss_intent", null) + } + whenever(mediaSmartspaceBaseAction.extras).thenReturn(recommendationExtras) + whenever(mediaSmartspaceTarget.baseAction).thenReturn(mediaSmartspaceBaseAction) + whenever(mediaSmartspaceTarget.iconGrid).thenReturn(listOf()) + + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + verify(logger).getNewInstanceId() + val instanceId = instanceIdSequence.lastInstanceId + + verify(listener) + .onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq( + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + dismissIntent = null, + headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME, + instanceId = InstanceId.fakeInstanceId(instanceId), + expiryTimeMs = SMARTSPACE_EXPIRY_TIME, + ) + ), + eq(false) + ) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_notCallsListener() { + smartspaceMediaDataProvider.onTargetsAvailable(listOf()) + verify(logger, never()).getNewInstanceId() + verify(listener, never()) + .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean()) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_hasNoneMediaTarget_callsRemoveListener() { + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + verify(logger).getNewInstanceId() + + smartspaceMediaDataProvider.onTargetsAvailable(listOf()) + uiExecutor.advanceClockToLast() + uiExecutor.runAllReady() + + verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false)) + verifyNoMoreInteractions(logger) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_persistentEnabled_headphoneTrigger_isActive() { + whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + val instanceId = instanceIdSequence.lastInstanceId + + verify(listener) + .onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq( + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = true, + packageName = PACKAGE_NAME, + cardAction = mediaSmartspaceBaseAction, + recommendations = validRecommendationList, + dismissIntent = DISMISS_INTENT, + headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME, + instanceId = InstanceId.fakeInstanceId(instanceId), + expiryTimeMs = SMARTSPACE_EXPIRY_TIME, + ) + ), + eq(false) + ) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_persistentEnabled_periodicTrigger_notActive() { + whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + val extras = + Bundle().apply { + putString("package_name", PACKAGE_NAME) + putParcelable("dismiss_intent", DISMISS_INTENT) + putString(EXTRA_KEY_TRIGGER_SOURCE, EXTRA_VALUE_TRIGGER_PERIODIC) + } + whenever(mediaSmartspaceBaseAction.extras).thenReturn(extras) + + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + val instanceId = instanceIdSequence.lastInstanceId + + verify(listener) + .onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq( + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = false, + packageName = PACKAGE_NAME, + cardAction = mediaSmartspaceBaseAction, + recommendations = validRecommendationList, + dismissIntent = DISMISS_INTENT, + headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME, + instanceId = InstanceId.fakeInstanceId(instanceId), + expiryTimeMs = SMARTSPACE_EXPIRY_TIME, + ) + ), + eq(false) + ) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_persistentEnabled_noTargets_inactive() { + whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + val instanceId = instanceIdSequence.lastInstanceId + + smartspaceMediaDataProvider.onTargetsAvailable(listOf()) + uiExecutor.advanceClockToLast() + uiExecutor.runAllReady() + + verify(listener) + .onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq( + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = false, + packageName = PACKAGE_NAME, + cardAction = mediaSmartspaceBaseAction, + recommendations = validRecommendationList, + dismissIntent = DISMISS_INTENT, + headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME, + instanceId = InstanceId.fakeInstanceId(instanceId), + expiryTimeMs = SMARTSPACE_EXPIRY_TIME, + ) + ), + eq(false) + ) + verify(listener, never()).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(false)) + } + + @Test + fun testSetRecommendationInactive_notifiesListeners() { + whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(true) + + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + val instanceId = instanceIdSequence.lastInstanceId + + mediaDataProcessor.setRecommendationInactive(KEY_MEDIA_SMARTSPACE) + uiExecutor.advanceClockToLast() + uiExecutor.runAllReady() + + verify(listener) + .onSmartspaceMediaDataLoaded( + eq(KEY_MEDIA_SMARTSPACE), + eq( + SmartspaceMediaData( + targetId = KEY_MEDIA_SMARTSPACE, + isActive = false, + packageName = PACKAGE_NAME, + cardAction = mediaSmartspaceBaseAction, + recommendations = validRecommendationList, + dismissIntent = DISMISS_INTENT, + headphoneConnectionTimeMillis = SMARTSPACE_CREATION_TIME, + instanceId = InstanceId.fakeInstanceId(instanceId), + expiryTimeMs = SMARTSPACE_EXPIRY_TIME, + ) + ), + eq(false) + ) + } + + @Test + fun testOnSmartspaceMediaDataLoaded_settingDisabled_doesNothing() { + // WHEN media recommendation setting is off + settings.putInt(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0) + + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + + // THEN smartspace signal is ignored + verify(listener, never()) + .onSmartspaceMediaDataLoaded(anyObject(), anyObject(), anyBoolean()) + } + + @Test + fun testMediaRecommendationDisabled_removesSmartspaceData() { + // GIVEN a media recommendation card is present + smartspaceMediaDataProvider.onTargetsAvailable(listOf(mediaSmartspaceTarget)) + verify(listener) + .onSmartspaceMediaDataLoaded(eq(KEY_MEDIA_SMARTSPACE), anyObject(), anyBoolean()) + + // WHEN the media recommendation setting is turned off + settings.putInt(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION, 0) + + // THEN listeners are notified + uiExecutor.advanceClockToLast() + foregroundExecutor.advanceClockToLast() + uiExecutor.runAllReady() + foregroundExecutor.runAllReady() + verify(listener).onSmartspaceMediaDataRemoved(eq(KEY_MEDIA_SMARTSPACE), eq(true)) + } + + @Test + fun testOnMediaDataChanged_updatesLastActiveTime() { + val currentTime = clock.elapsedRealtime() + addNotificationAndLoad() + assertThat(mediaDataCaptor.value!!.lastActive).isAtLeast(currentTime) + } + + @Test + fun testOnMediaDataTimedOut_updatesLastActiveTime() { + // GIVEN that the manager has a notification + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + + // WHEN the notification times out + clock.advanceTime(100) + val currentTime = clock.elapsedRealtime() + mediaDataProcessor.setInactive(KEY, timedOut = true, forceUpdate = true) + + // THEN the last active time is changed + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.lastActive).isAtLeast(currentTime) + } + + @Test + fun testOnActiveMediaConverted_updatesLastActiveTime() { + // GIVEN that the manager has a notification with a resume action + addNotificationAndLoad() + val data = mediaDataCaptor.value + val instanceId = data.instanceId + assertThat(data.resumption).isFalse() + mediaDataProcessor.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {})) + + // WHEN the notification is removed + clock.advanceTime(100) + val currentTime = clock.elapsedRealtime() + mediaDataProcessor.onNotificationRemoved(KEY) + + // THEN the last active time is changed + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.lastActive).isAtLeast(currentTime) + + // Log as a conversion event, not as a new resume control + verify(logger).logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(instanceId)) + verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any()) + } + + @Test + fun testOnInactiveMediaConverted_doesNotUpdateLastActiveTime() { + // GIVEN that the manager has a notification with a resume action + addNotificationAndLoad() + val data = mediaDataCaptor.value + val instanceId = data.instanceId + assertThat(data.resumption).isFalse() + mediaDataProcessor.onMediaDataLoaded( + KEY, + null, + data.copy(resumeAction = Runnable {}, active = false) + ) + + // WHEN the notification is removed + clock.advanceTime(100) + val currentTime = clock.elapsedRealtime() + mediaDataProcessor.onNotificationRemoved(KEY) + + // THEN the last active time is not changed + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.lastActive).isLessThan(currentTime) + + // Log as a conversion event, not as a new resume control + verify(logger).logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(instanceId)) + verify(logger, never()).logResumeMediaAdded(anyInt(), eq(PACKAGE_NAME), any()) + } + + @Test + fun testTooManyCompactActions_isTruncated() { + // GIVEN a notification where too many compact actions were specified + val notif = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle( + MediaStyle().apply { + setMediaSession(session.sessionToken) + setShowActionsInCompactView(0, 1, 2, 3, 4) + } + ) + } + build() + } + + // WHEN the notification is loaded + mediaDataProcessor.onNotificationAdded(KEY, notif) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + + // THEN only the first MAX_COMPACT_ACTIONS are actually set + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.actionsToShowInCompact.size) + .isEqualTo(MediaDataProcessor.MAX_COMPACT_ACTIONS) + } + + @Test + fun testTooManyNotificationActions_isTruncated() { + // GIVEN a notification where too many notification actions are added + val action = Notification.Action(R.drawable.ic_android, "action", null) + val notif = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + for (i in 0..MediaDataProcessor.MAX_NOTIFICATION_ACTIONS) { + it.addAction(action) + } + } + build() + } + + // WHEN the notification is loaded + mediaDataProcessor.onNotificationAdded(KEY, notif) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + + // THEN only the first MAX_NOTIFICATION_ACTIONS are actually included + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.actions.size) + .isEqualTo(MediaDataProcessor.MAX_NOTIFICATION_ACTIONS) + } + + @Test + fun testPlaybackActions_noState_usesNotification() { + val desc = "Notification Action" + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + whenever(controller.playbackState).thenReturn(null) + + val notifWithAction = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + it.addAction(android.R.drawable.ic_media_play, desc, null) + } + build() + } + mediaDataProcessor.onNotificationAdded(KEY, notifWithAction) + + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + + assertThat(mediaDataCaptor.value!!.semanticActions).isNull() + assertThat(mediaDataCaptor.value!!.actions).hasSize(1) + assertThat(mediaDataCaptor.value!!.actions[0]!!.contentDescription).isEqualTo(desc) + } + + @Test + fun testPlaybackActions_hasPrevNext() { + val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + val stateActions = + PlaybackState.ACTION_PLAY or + PlaybackState.ACTION_SKIP_TO_PREVIOUS or + PlaybackState.ACTION_SKIP_TO_NEXT + val stateBuilder = PlaybackState.Builder().setActions(stateActions) + customDesc.forEach { + stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause) + } + whenever(controller.playbackState).thenReturn(stateBuilder.build()) + + addNotificationAndLoad() + + assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull() + val actions = mediaDataCaptor.value!!.semanticActions!! + + assertThat(actions.playOrPause).isNotNull() + assertThat(actions.playOrPause!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_play)) + actions.playOrPause!!.action!!.run() + verify(transportControls).play() + + assertThat(actions.prevOrCustom).isNotNull() + assertThat(actions.prevOrCustom!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_prev)) + actions.prevOrCustom!!.action!!.run() + verify(transportControls).skipToPrevious() + + assertThat(actions.nextOrCustom).isNotNull() + assertThat(actions.nextOrCustom!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_next)) + actions.nextOrCustom!!.action!!.run() + verify(transportControls).skipToNext() + + assertThat(actions.custom0).isNotNull() + assertThat(actions.custom0!!.contentDescription).isEqualTo(customDesc[0]) + + assertThat(actions.custom1).isNotNull() + assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[1]) + } + + @Test + fun testPlaybackActions_noPrevNext_usesCustom() { + val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4", "custom 5") + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + val stateActions = PlaybackState.ACTION_PLAY + val stateBuilder = PlaybackState.Builder().setActions(stateActions) + customDesc.forEach { + stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause) + } + whenever(controller.playbackState).thenReturn(stateBuilder.build()) + + addNotificationAndLoad() + + assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull() + val actions = mediaDataCaptor.value!!.semanticActions!! + + assertThat(actions.playOrPause).isNotNull() + assertThat(actions.playOrPause!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_play)) + + assertThat(actions.prevOrCustom).isNotNull() + assertThat(actions.prevOrCustom!!.contentDescription).isEqualTo(customDesc[0]) + + assertThat(actions.nextOrCustom).isNotNull() + assertThat(actions.nextOrCustom!!.contentDescription).isEqualTo(customDesc[1]) + + assertThat(actions.custom0).isNotNull() + assertThat(actions.custom0!!.contentDescription).isEqualTo(customDesc[2]) + + assertThat(actions.custom1).isNotNull() + assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[3]) + } + + @Test + fun testPlaybackActions_connecting() { + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + val stateActions = PlaybackState.ACTION_PLAY + val stateBuilder = + PlaybackState.Builder() + .setState(PlaybackState.STATE_BUFFERING, 0, 10f) + .setActions(stateActions) + whenever(controller.playbackState).thenReturn(stateBuilder.build()) + + addNotificationAndLoad() + + assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull() + val actions = mediaDataCaptor.value!!.semanticActions!! + + assertThat(actions.playOrPause).isNotNull() + assertThat(actions.playOrPause!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_connecting)) + } + + @Test + fun testPlaybackActions_reservedSpace() { + val customDesc = arrayOf("custom 1", "custom 2", "custom 3", "custom 4") + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + val stateActions = PlaybackState.ACTION_PLAY + val stateBuilder = PlaybackState.Builder().setActions(stateActions) + customDesc.forEach { + stateBuilder.addCustomAction("action: $it", it, android.R.drawable.ic_media_pause) + } + val extras = + Bundle().apply { + putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_PREV, true) + putBoolean(MediaConstants.SESSION_EXTRAS_KEY_SLOT_RESERVATION_SKIP_TO_NEXT, true) + } + whenever(controller.playbackState).thenReturn(stateBuilder.build()) + whenever(controller.extras).thenReturn(extras) + + addNotificationAndLoad() + + assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull() + val actions = mediaDataCaptor.value!!.semanticActions!! + + assertThat(actions.playOrPause).isNotNull() + assertThat(actions.playOrPause!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_play)) + + assertThat(actions.prevOrCustom).isNull() + assertThat(actions.nextOrCustom).isNull() + + assertThat(actions.custom0).isNotNull() + assertThat(actions.custom0!!.contentDescription).isEqualTo(customDesc[0]) + + assertThat(actions.custom1).isNotNull() + assertThat(actions.custom1!!.contentDescription).isEqualTo(customDesc[1]) + + assertThat(actions.reserveNext).isTrue() + assertThat(actions.reservePrev).isTrue() + } + + @Test + fun testPlaybackActions_playPause_hasButton() { + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + val stateActions = PlaybackState.ACTION_PLAY_PAUSE + val stateBuilder = PlaybackState.Builder().setActions(stateActions) + whenever(controller.playbackState).thenReturn(stateBuilder.build()) + + addNotificationAndLoad() + + assertThat(mediaDataCaptor.value!!.semanticActions).isNotNull() + val actions = mediaDataCaptor.value!!.semanticActions!! + + assertThat(actions.playOrPause).isNotNull() + assertThat(actions.playOrPause!!.contentDescription) + .isEqualTo(context.getString(R.string.controls_media_button_play)) + actions.playOrPause!!.action!!.run() + verify(transportControls).play() + } + + @Test + fun testPlaybackLocationChange_isLogged() { + // Media control added for local playback + addNotificationAndLoad() + val instanceId = mediaDataCaptor.value.instanceId + + // Location is updated to local cast + whenever(playbackInfo.playbackType) + .thenReturn(MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) + addNotificationAndLoad() + verify(logger) + .logPlaybackLocationChange( + anyInt(), + eq(PACKAGE_NAME), + eq(instanceId), + eq(MediaData.PLAYBACK_CAST_LOCAL) + ) + + // update to remote cast + mediaDataProcessor.onNotificationAdded(KEY, remoteCastNotification) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(logger) + .logPlaybackLocationChange( + anyInt(), + eq(SYSTEM_PACKAGE_NAME), + eq(instanceId), + eq(MediaData.PLAYBACK_CAST_REMOTE) + ) + } + + @Test + fun testPlaybackStateChange_keyExists_callsListener() { + // Notification has been added + addNotificationAndLoad() + + // Callback gets an updated state + val state = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0L, 1f).build() + stateCallbackCaptor.value.invoke(KEY, state) + + // Listener is notified of updated state + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.isPlaying).isTrue() + } + + @Test + fun testPlaybackStateChange_keyDoesNotExist_doesNothing() { + val state = PlaybackState.Builder().build() + + // No media added with this key + + stateCallbackCaptor.value.invoke(KEY, state) + verify(listener, never()) + .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + } + + @Test + fun testPlaybackStateChange_keyHasNullToken_doesNothing() { + // When we get an update that sets the data's token to null + addNotificationAndLoad() + val data = mediaDataCaptor.value + assertThat(data.resumption).isFalse() + mediaDataProcessor.onMediaDataLoaded(KEY, null, data.copy(token = null)) + + // And then get a state update + val state = PlaybackState.Builder().build() + + // Then no changes are made + stateCallbackCaptor.value.invoke(KEY, state) + verify(listener, never()) + .onMediaDataLoaded(eq(KEY), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + } + + @Test + fun testPlaybackState_PauseWhenFlagTrue_keyExists_callsListener() { + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + val state = PlaybackState.Builder().setState(PlaybackState.STATE_PAUSED, 0L, 1f).build() + whenever(controller.playbackState).thenReturn(state) + + addNotificationAndLoad() + stateCallbackCaptor.value.invoke(KEY, state) + + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.isPlaying).isFalse() + assertThat(mediaDataCaptor.value.semanticActions).isNotNull() + } + + @Test + fun testPlaybackState_PauseStateAfterAddingResumption_keyExists_callsListener() { + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + build() + } + val state = + PlaybackState.Builder() + .setState(PlaybackState.STATE_PAUSED, 0L, 1f) + .setActions(PlaybackState.ACTION_PLAY_PAUSE) + .build() + + // Add resumption controls in order to have semantic actions. + // To make sure that they are not null after changing state. + mediaDataProcessor.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + PACKAGE_NAME + ) + backgroundExecutor.runAllReady() + foregroundExecutor.runAllReady() + + stateCallbackCaptor.value.invoke(PACKAGE_NAME, state) + + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(PACKAGE_NAME), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.isPlaying).isFalse() + assertThat(mediaDataCaptor.value.semanticActions).isNotNull() + } + + @Test + fun testPlaybackStateNull_Pause_keyExists_callsListener() { + whenever(controller.playbackState).thenReturn(null) + val state = + PlaybackState.Builder() + .setState(PlaybackState.STATE_PAUSED, 0L, 1f) + .setActions(PlaybackState.ACTION_PLAY_PAUSE) + .build() + + addNotificationAndLoad() + stateCallbackCaptor.value.invoke(KEY, state) + + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.isPlaying).isFalse() + assertThat(mediaDataCaptor.value.semanticActions).isNull() + } + + @Test + fun testNoClearNotOngoing_canDismiss() { + mediaNotification = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + it.setOngoing(false) + it.setFlag(FLAG_NO_CLEAR, true) + } + build() + } + addNotificationAndLoad() + assertThat(mediaDataCaptor.value.isClearable).isTrue() + } + + @Test + fun testOngoing_cannotDismiss() { + mediaNotification = + SbnBuilder().run { + setPkg(PACKAGE_NAME) + modifyNotification(context).also { + it.setSmallIcon(android.R.drawable.ic_media_pause) + it.setStyle(MediaStyle().apply { setMediaSession(session.sessionToken) }) + it.setOngoing(true) + } + build() + } + addNotificationAndLoad() + assertThat(mediaDataCaptor.value.isClearable).isFalse() + } + + @Test + fun testRetain_notifPlayer_notifRemoved_setToResume() { + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + + // When a media control based on notification is added, times out, and then removed + addNotificationAndLoad() + mediaDataProcessor.setInactive(KEY, timedOut = true) + assertThat(mediaDataCaptor.value.active).isFalse() + mediaDataProcessor.onNotificationRemoved(KEY) + + // It is converted to a resume player + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.active).isFalse() + verify(logger) + .logActiveConvertedToResume( + anyInt(), + eq(PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId) + ) + } + + @Test + fun testRetain_notifPlayer_sessionDestroyed_doesNotChange() { + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + + // When a media control based on notification is added and times out + addNotificationAndLoad() + mediaDataProcessor.setInactive(KEY, timedOut = true) + assertThat(mediaDataCaptor.value.active).isFalse() + + // and then the session is destroyed + sessionCallbackCaptor.value.invoke(KEY) + + // It remains as a regular player + verify(listener, never()).onMediaDataRemoved(eq(KEY)) + verify(listener, never()) + .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + } + + @Test + fun testRetain_notifPlayer_removeWhileActive_fullyRemoved() { + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + + // When a media control based on notification is added and then removed, without timing out + addNotificationAndLoad() + val data = mediaDataCaptor.value + assertThat(data.active).isTrue() + mediaDataProcessor.onNotificationRemoved(KEY) + + // It is fully removed + verify(listener).onMediaDataRemoved(eq(KEY)) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + verify(listener, never()) + .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + } + + @Test + fun testRetain_canResume_removeWhileActive_setToResume() { + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + + // When a media control that supports resumption is added + addNotificationAndLoad() + val dataResumable = mediaDataCaptor.value.copy(resumeAction = Runnable {}) + mediaDataProcessor.onMediaDataLoaded(KEY, null, dataResumable) + + // And then removed while still active + mediaDataProcessor.onNotificationRemoved(KEY) + + // It is converted to a resume player + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.active).isFalse() + verify(logger) + .logActiveConvertedToResume( + anyInt(), + eq(PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId) + ) + } + + @Test + fun testRetain_sessionPlayer_notifRemoved_doesNotChange() { + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + addPlaybackStateAction() + + // When a media control with PlaybackState actions is added, times out, + // and then the notification is removed + addNotificationAndLoad() + val data = mediaDataCaptor.value + assertThat(data.active).isTrue() + mediaDataProcessor.setInactive(KEY, timedOut = true) + mediaDataProcessor.onNotificationRemoved(KEY) + + // It remains as a regular player + verify(listener, never()).onMediaDataRemoved(eq(KEY)) + verify(listener, never()) + .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + } + + @Test + fun testRetain_sessionPlayer_sessionDestroyed_setToResume() { + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + addPlaybackStateAction() + + // When a media control with PlaybackState actions is added, times out, + // and then the session is destroyed + addNotificationAndLoad() + val data = mediaDataCaptor.value + assertThat(data.active).isTrue() + mediaDataProcessor.setInactive(KEY, timedOut = true) + sessionCallbackCaptor.value.invoke(KEY) + + // It is converted to a resume player + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.active).isFalse() + verify(logger) + .logActiveConvertedToResume( + anyInt(), + eq(PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId) + ) + } + + @Test + fun testRetain_sessionPlayer_destroyedWhileActive_noResume_fullyRemoved() { + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + addPlaybackStateAction() + + // When a media control using session actions is added, and then the session is destroyed + // without timing out first + addNotificationAndLoad() + val data = mediaDataCaptor.value + assertThat(data.active).isTrue() + sessionCallbackCaptor.value.invoke(KEY) + + // It is fully removed + verify(listener).onMediaDataRemoved(eq(KEY)) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + verify(listener, never()) + .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + } + + @Test + fun testRetain_sessionPlayer_canResume_destroyedWhileActive_setToResume() { + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + addPlaybackStateAction() + + // When a media control using session actions and that does allow resumption is added, + addNotificationAndLoad() + val dataResumable = mediaDataCaptor.value.copy(resumeAction = Runnable {}) + mediaDataProcessor.onMediaDataLoaded(KEY, null, dataResumable) + + // And then the session is destroyed without timing out first + sessionCallbackCaptor.value.invoke(KEY) + + // It is converted to a resume player + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.active).isFalse() + verify(logger) + .logActiveConvertedToResume( + anyInt(), + eq(PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId) + ) + } + + @Test + fun testSessionPlayer_sessionDestroyed_noResume_fullyRemoved() { + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + addPlaybackStateAction() + + // When a media control with PlaybackState actions is added, times out, + // and then the session is destroyed + addNotificationAndLoad() + val data = mediaDataCaptor.value + assertThat(data.active).isTrue() + mediaDataProcessor.setInactive(KEY, timedOut = true) + sessionCallbackCaptor.value.invoke(KEY) + + // It is fully removed. + verify(listener).onMediaDataRemoved(eq(KEY)) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + verify(listener, never()) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + } + + @Test + fun testSessionPlayer_destroyedWhileActive_noResume_fullyRemoved() { + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + addPlaybackStateAction() + + // When a media control using session actions is added, and then the session is destroyed + // without timing out first + addNotificationAndLoad() + val data = mediaDataCaptor.value + assertThat(data.active).isTrue() + sessionCallbackCaptor.value.invoke(KEY) + + // It is fully removed + verify(listener).onMediaDataRemoved(eq(KEY)) + verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId)) + verify(listener, never()) + .onMediaDataLoaded(eq(PACKAGE_NAME), any(), any(), anyBoolean(), anyInt(), anyBoolean()) + } + + @Test + fun testSessionPlayer_canResume_destroyedWhileActive_setToResume() { + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + addPlaybackStateAction() + + // When a media control using session actions and that does allow resumption is added, + addNotificationAndLoad() + val dataResumable = mediaDataCaptor.value.copy(resumeAction = Runnable {}) + mediaDataProcessor.onMediaDataLoaded(KEY, null, dataResumable) + + // And then the session is destroyed without timing out first + sessionCallbackCaptor.value.invoke(KEY) + + // It is converted to a resume player + verify(listener) + .onMediaDataLoaded( + eq(PACKAGE_NAME), + eq(KEY), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + assertThat(mediaDataCaptor.value.resumption).isTrue() + assertThat(mediaDataCaptor.value.active).isFalse() + verify(logger) + .logActiveConvertedToResume( + anyInt(), + eq(PACKAGE_NAME), + eq(mediaDataCaptor.value.instanceId) + ) + } + + @Test + fun testSessionDestroyed_noNotificationKey_stillRemoved() { + whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(true) + whenever(mediaFlags.areMediaSessionActionsEnabled(any(), any())).thenReturn(true) + + // When a notiifcation is added and then removed before it is fully processed + mediaDataProcessor.onNotificationAdded(KEY, mediaNotification) + backgroundExecutor.runAllReady() + mediaDataProcessor.onNotificationRemoved(KEY) + + // We still make sure to remove it + verify(listener).onMediaDataRemoved(eq(KEY)) + } + + @Test + fun testResumeMediaLoaded_hasArtPermission_artLoaded() { + // When resume media is loaded and user/app has permission to access the art URI, + whenever( + ugm.checkGrantUriPermission_ignoreNonSystem( + anyInt(), + any(), + any(), + anyInt(), + anyInt() + ) + ) + .thenReturn(1) + val artwork = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + val uri = Uri.parse("content://example") + whenever(ImageDecoder.createSource(any(), eq(uri))).thenReturn(imageSource) + whenever(ImageDecoder.decodeBitmap(any(), any())).thenReturn(artwork) + + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + setIconUri(uri) + build() + } + addResumeControlAndLoad(desc) + + // Then the artwork is loaded + assertThat(mediaDataCaptor.value.artwork).isNotNull() + } + + @Test + fun testResumeMediaLoaded_noArtPermission_noArtLoaded() { + // When resume media is loaded and user/app does not have permission to access the art URI + whenever( + ugm.checkGrantUriPermission_ignoreNonSystem( + anyInt(), + any(), + any(), + anyInt(), + anyInt() + ) + ) + .thenThrow(SecurityException("Test no permission")) + val artwork = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + val uri = Uri.parse("content://example") + whenever(ImageDecoder.createSource(any(), eq(uri))).thenReturn(imageSource) + whenever(ImageDecoder.decodeBitmap(any(), any())).thenReturn(artwork) + + val desc = + MediaDescription.Builder().run { + setTitle(SESSION_TITLE) + setIconUri(uri) + build() + } + addResumeControlAndLoad(desc) + + // Then the artwork is not loaded + assertThat(mediaDataCaptor.value.artwork).isNull() + } + + /** Helper function to add a basic media notification and capture the resulting MediaData */ + private fun addNotificationAndLoad() { + addNotificationAndLoad(mediaNotification) + } + + /** Helper function to add the given notification and capture the resulting MediaData */ + private fun addNotificationAndLoad(sbn: StatusBarNotification) { + mediaDataProcessor.onNotificationAdded(KEY, sbn) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + verify(listener) + .onMediaDataLoaded( + eq(KEY), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + } + + /** Helper function to set up a PlaybackState with action */ + private fun addPlaybackStateAction() { + val stateActions = PlaybackState.ACTION_PLAY_PAUSE + val stateBuilder = PlaybackState.Builder().setActions(stateActions) + stateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 1.0f) + whenever(controller.playbackState).thenReturn(stateBuilder.build()) + } + + /** Helper function to add a resumption control and capture the resulting MediaData */ + private fun addResumeControlAndLoad( + desc: MediaDescription, + packageName: String = PACKAGE_NAME + ) { + mediaDataProcessor.addResumptionControls( + USER_ID, + desc, + Runnable {}, + session.sessionToken, + APP_NAME, + pendingIntent, + packageName + ) + assertThat(backgroundExecutor.runAllReady()).isEqualTo(1) + assertThat(foregroundExecutor.runAllReady()).isEqualTo(1) + + verify(listener) + .onMediaDataLoaded( + eq(packageName), + eq(null), + capture(mediaDataCaptor), + eq(true), + eq(0), + eq(false) + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt index 7f3d79f7e288..a447e442a384 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerTest.kt @@ -41,7 +41,6 @@ import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice import com.android.settingslib.media.PhoneMediaDevice import com.android.systemui.SysuiTestCase -import com.android.systemui.dump.DumpManager import com.android.systemui.media.controls.MediaTestUtils import com.android.systemui.media.controls.shared.model.MediaData import com.android.systemui.media.controls.shared.model.MediaDeviceData @@ -98,7 +97,6 @@ public class MediaDeviceManagerTest : SysuiTestCase() { @Mock private lateinit var muteAwaitManager: MediaMuteAwaitConnectionManager private lateinit var fakeFgExecutor: FakeExecutor private lateinit var fakeBgExecutor: FakeExecutor - @Mock private lateinit var dumpster: DumpManager @Mock private lateinit var listener: MediaDeviceManager.Listener @Mock private lateinit var device: MediaDevice @Mock private lateinit var icon: Drawable @@ -133,7 +131,6 @@ public class MediaDeviceManagerTest : SysuiTestCase() { { localBluetoothManager }, fakeFgExecutor, fakeBgExecutor, - dumpster, ) manager.addListener(listener) diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt index f755199b4c72..59e2696c6123 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt @@ -41,7 +41,6 @@ import com.android.systemui.media.controls.MediaTestUtils import com.android.systemui.media.controls.domain.pipeline.EMPTY_SMARTSPACE_MEDIA_DATA import com.android.systemui.media.controls.domain.pipeline.MediaDataManager import com.android.systemui.media.controls.shared.model.MediaData -import com.android.systemui.media.controls.shared.model.SmartspaceMediaData import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QS import com.android.systemui.media.controls.ui.view.MediaHostState import com.android.systemui.media.controls.ui.view.MediaScrollView @@ -111,7 +110,6 @@ class MediaCarouselControllerTest : SysuiTestCase() { @Mock lateinit var logger: MediaUiEventLogger @Mock lateinit var debugLogger: MediaCarouselControllerLogger @Mock lateinit var mediaViewController: MediaViewController - @Mock lateinit var smartspaceMediaData: SmartspaceMediaData @Mock lateinit var mediaCarousel: MediaScrollView @Mock lateinit var pageIndicator: PageIndicator @Mock lateinit var mediaFlags: MediaFlags @@ -165,7 +163,6 @@ class MediaCarouselControllerTest : SysuiTestCase() { verify(mediaHostStatesManager).addCallback(capture(hostStateCallback)) whenever(mediaControlPanelFactory.get()).thenReturn(panel) whenever(panel.mediaViewController).thenReturn(mediaViewController) - whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData) whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false) MediaPlayerData.clear() verify(globalSettings) diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt index aa54565c2aa0..6e0919f5f1d0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt @@ -28,9 +28,10 @@ import android.view.MotionEvent.ACTION_UP import android.view.ViewConfiguration import android.view.WindowManager import androidx.test.filters.SmallTest -import com.android.internal.jank.InteractionJankMonitor import com.android.internal.util.LatencyTracker import com.android.systemui.SysuiTestCase +import com.android.systemui.jank.interactionJankMonitor +import com.android.systemui.kosmos.Kosmos import com.android.systemui.plugins.NavigationEdgeBackPlugin import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController @@ -41,10 +42,8 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Mock -import org.mockito.Mockito.anyInt import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @SmallTest @@ -62,16 +61,13 @@ class BackPanelControllerTest : SysuiTestCase() { @Mock private lateinit var windowManager: WindowManager @Mock private lateinit var configurationController: ConfigurationController @Mock private lateinit var latencyTracker: LatencyTracker - @Mock private lateinit var interactionJankMonitor: InteractionJankMonitor + private val interactionJankMonitor = Kosmos().interactionJankMonitor @Mock private lateinit var layoutParams: WindowManager.LayoutParams @Mock private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback @Before fun setup() { MockitoAnnotations.initMocks(this) - `when`(interactionJankMonitor.begin(any(), anyInt())).thenReturn(true) - `when`(interactionJankMonitor.end(anyInt())).thenReturn(true) - `when`(interactionJankMonitor.cancel(anyInt())).thenReturn(true) mBackPanelController = BackPanelController( context, diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt index 761c411bdcb8..37654d515a21 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.qs.QSHost import com.android.systemui.qs.QsEventLogger import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor +import com.android.systemui.recordissue.IssueRecordingState import com.android.systemui.recordissue.RecordIssueDialogDelegate import com.android.systemui.res.R import com.android.systemui.settings.UserContextProvider @@ -74,6 +75,7 @@ class RecordIssueTileTest : SysuiTestCase() { @Mock private lateinit var dialog: SystemUIDialog private lateinit var testableLooper: TestableLooper + private val issueRecordingState = IssueRecordingState() private lateinit var tile: RecordIssueTile @Before @@ -100,13 +102,14 @@ class RecordIssueTileTest : SysuiTestCase() { dialogLauncherAnimator, panelInteractor, userContextProvider, + issueRecordingState, delegateFactory, ) } @Test fun qsTileUi_shouldLookCorrect_whenInactive() { - tile.isRecording = false + issueRecordingState.isRecording = false val testState = tile.newTileState() tile.handleUpdateState(testState, null) @@ -118,8 +121,7 @@ class RecordIssueTileTest : SysuiTestCase() { @Test fun qsTileUi_shouldLookCorrect_whenRecording() { - tile.isRecording = true - + issueRecordingState.isRecording = true val testState = tile.newTileState() tile.handleUpdateState(testState, null) @@ -130,7 +132,7 @@ class RecordIssueTileTest : SysuiTestCase() { @Test fun inActiveQsTile_switchesToActive_whenClicked() { - tile.isRecording = false + issueRecordingState.isRecording = false val testState = tile.newTileState() tile.handleUpdateState(testState, null) @@ -140,7 +142,7 @@ class RecordIssueTileTest : SysuiTestCase() { @Test fun activeQsTile_switchesToInActive_whenClicked() { - tile.isRecording = true + issueRecordingState.isRecording = true val testState = tile.newTileState() tile.handleUpdateState(testState, null) @@ -150,7 +152,8 @@ class RecordIssueTileTest : SysuiTestCase() { @Test fun showPrompt_shouldUseKeyguardDismissUtil_ToShowDialog() { - tile.isRecording = false + issueRecordingState.isRecording = false + tile.handleClick(null) testableLooper.processAllMessages() diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt new file mode 100644 index 000000000000..4215b8c9a1a3 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/impl/work/ui/WorkModeTileMapperTest.kt @@ -0,0 +1,145 @@ +/* + * 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.qs.tiles.impl.work.ui + +import android.app.admin.DevicePolicyResources +import android.app.admin.DevicePolicyResourcesManager +import android.app.admin.devicePolicyManager +import android.graphics.drawable.TestStubDrawable +import android.service.quicksettings.Tile +import android.widget.Switch +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject +import com.android.systemui.qs.tiles.impl.work.domain.model.WorkModeTileModel +import com.android.systemui.qs.tiles.impl.work.qsWorkModeTileConfig +import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.res.R +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class WorkModeTileMapperTest : SysuiTestCase() { + private val kosmos = Kosmos() + private val qsTileConfig = kosmos.qsWorkModeTileConfig + private val devicePolicyManager = kosmos.devicePolicyManager + private val testLabel = context.getString(R.string.quick_settings_work_mode_label) + private val devicePolicyResourceManager = mock<DevicePolicyResourcesManager>() + private lateinit var mapper: WorkModeTileMapper + + @Before + fun setup() { + whenever(devicePolicyManager.resources).thenReturn(devicePolicyResourceManager) + whenever( + devicePolicyResourceManager.getString( + eq(DevicePolicyResources.Strings.SystemUi.QS_WORK_PROFILE_LABEL), + any() + ) + ) + .thenReturn(testLabel) + mapper = + WorkModeTileMapper( + context.orCreateTestableResources + .apply { + addOverride( + com.android.internal.R.drawable.stat_sys_managed_profile_status, + TestStubDrawable() + ) + } + .resources, + context.theme, + devicePolicyManager + ) + } + + @Test + fun mapsDisabledDataToInactiveState() { + val isEnabled = false + + val actualState: QSTileState = + mapper.map(qsTileConfig, WorkModeTileModel.HasActiveProfile(isEnabled)) + + val expectedState = createWorkModeTileState(QSTileState.ActivationState.INACTIVE) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun mapsEnabledDataToActiveState() { + val isEnabled = true + + val actualState: QSTileState = + mapper.map(qsTileConfig, WorkModeTileModel.HasActiveProfile(isEnabled)) + + val expectedState = createWorkModeTileState(QSTileState.ActivationState.ACTIVE) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + @Test + fun mapsNoActiveProfileDataToUnavailableState() { + val actualState: QSTileState = mapper.map(qsTileConfig, WorkModeTileModel.NoActiveProfile) + + val expectedState = createWorkModeTileState(QSTileState.ActivationState.UNAVAILABLE) + QSTileStateSubject.assertThat(actualState).isEqualTo(expectedState) + } + + private fun createWorkModeTileState( + activationState: QSTileState.ActivationState, + ): QSTileState { + val label = testLabel + return QSTileState( + icon = { + Icon.Loaded( + context.getDrawable( + com.android.internal.R.drawable.stat_sys_managed_profile_status + )!!, + null + ) + }, + label = label, + activationState = activationState, + secondaryLabel = + if (activationState == QSTileState.ActivationState.INACTIVE) { + context.getString(R.string.quick_settings_work_mode_paused_state) + } else if (activationState == QSTileState.ActivationState.UNAVAILABLE) { + context.resources + .getStringArray(R.array.tile_states_work)[Tile.STATE_UNAVAILABLE] + } else { + "" + }, + supportedActions = + if (activationState == QSTileState.ActivationState.UNAVAILABLE) { + setOf() + } else { + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) + }, + contentDescription = label, + stateDescription = null, + sideViewIcon = QSTileState.SideViewIcon.None, + enabledState = QSTileState.EnabledState.ENABLED, + expandedAccessibilityClassName = Switch::class.qualifiedName + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt index 2e8160baa257..1cfca68cd452 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt @@ -222,4 +222,9 @@ class RecordIssueDialogDelegateTest : SysuiTestCase() { ) verify(factory, never()).create(any<ScreenCapturePermissionDialogDelegate>()) } + + @Test + fun startButton_isDisabled_beforeIssueTypeIsSelected() { + assertThat(dialog.getButton(Dialog.BUTTON_POSITIVE).isEnabled).isFalse() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 43fcdf3eeedd..c25b910557a7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -62,7 +62,6 @@ import android.view.accessibility.AccessibilityManager; import androidx.constraintlayout.widget.ConstraintSet; -import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.testing.UiEventLoggerFake; @@ -299,7 +298,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { @Mock protected RecordingController mRecordingController; @Mock protected LockscreenGestureLogger mLockscreenGestureLogger; @Mock protected DumpManager mDumpManager; - @Mock protected InteractionJankMonitor mInteractionJankMonitor; @Mock protected NotificationsQSContainerController mNotificationsQSContainerController; @Mock protected QsFrameTranslateController mQsFrameTranslateController; @Mock protected StatusBarWindowStateController mStatusBarWindowStateController; @@ -441,7 +439,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { SystemClock systemClock = new FakeSystemClock(); mStatusBarStateController = new StatusBarStateControllerImpl( mUiEventLogger, - mInteractionJankMonitor, + mKosmos.getInteractionJankMonitor(), mJavaAdapter, () -> mShadeInteractor, () -> mKosmos.getDeviceUnlockedInteractor(), @@ -459,7 +457,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mDozeParameters, mScreenOffAnimationController, mKeyguardLogger, - mInteractionJankMonitor, + mKosmos.getInteractionJankMonitor(), mKeyguardInteractor, mDumpManager, mPowerInteractor)); @@ -611,7 +609,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mock(HeadsUpManager.class), new StatusBarStateControllerImpl( new UiEventLoggerFake(), - mInteractionJankMonitor, + mKosmos.getInteractionJankMonitor(), mJavaAdapter, () -> mShadeInteractor, () -> mKosmos.getDeviceUnlockedInteractor(), @@ -651,10 +649,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { .thenReturn(mKeyguardBottomArea); when(mNotificationRemoteInputManager.isRemoteInputActive()) .thenReturn(false); - when(mInteractionJankMonitor.begin(any(), anyInt())) - .thenReturn(true); - when(mInteractionJankMonitor.end(anyInt())) - .thenReturn(true); doAnswer(invocation -> { ((Runnable) invocation.getArgument(0)).run(); return null; @@ -820,7 +814,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mAccessibilityManager, mLockscreenGestureLogger, mMetricsLogger, - mInteractionJankMonitor, + mKosmos.getInteractionJankMonitor(), mShadeLog, mDumpManager, mDeviceEntryFaceAuthInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java index 419b0fd2f89b..118d27a68c8c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java @@ -251,7 +251,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { mCollectionListener.onEntryInit(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any()); - assertFalse(mParamsCaptor.getValue().isLowPriority()); + assertFalse(mParamsCaptor.getValue().isMinimized()); mNotifInflater.invokeInflateCallbackForEntry(mEntry); // WHEN notification moves to a min priority section @@ -260,7 +260,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { // THEN we rebind it verify(mNotifInflater).rebindViews(eq(mEntry), mParamsCaptor.capture(), any()); - assertTrue(mParamsCaptor.getValue().isLowPriority()); + assertTrue(mParamsCaptor.getValue().isMinimized()); // THEN we do not filter it because it's not the first inflation. assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0)); @@ -273,7 +273,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { mCollectionListener.onEntryInit(mEntry); mBeforeFilterListener.onBeforeFinalizeFilter(List.of(mEntry)); verify(mNotifInflater).inflateViews(eq(mEntry), mParamsCaptor.capture(), any()); - assertTrue(mParamsCaptor.getValue().isLowPriority()); + assertTrue(mParamsCaptor.getValue().isMinimized()); mNotifInflater.invokeInflateCallbackForEntry(mEntry); // WHEN notification is moved under a parent @@ -282,7 +282,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { // THEN we rebind it as not-minimized verify(mNotifInflater).rebindViews(eq(mEntry), mParamsCaptor.capture(), any()); - assertFalse(mParamsCaptor.getValue().isLowPriority()); + assertFalse(mParamsCaptor.getValue().isMinimized()); // THEN we do not filter it because it's not the first inflation. assertFalse(mUninflatedFilter.shouldFilterOut(mEntry, 0)); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java index b114e13bb25c..ee2eb806341f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowTest.java @@ -741,7 +741,7 @@ public class ExpandableNotificationRowTest extends SysuiTestCase { when(mockViewWrapper.getIcon()).thenReturn(mockIcon); NotificationViewWrapper mockLowPriorityViewWrapper = mock(NotificationViewWrapper.class); - when(mockContainer.getLowPriorityViewWrapper()).thenReturn(mockLowPriorityViewWrapper); + when(mockContainer.getMinimizedGroupHeaderWrapper()).thenReturn(mockLowPriorityViewWrapper); CachingIconView mockLowPriorityIcon = mock(CachingIconView.class); when(mockLowPriorityViewWrapper.getIcon()).thenReturn(mockLowPriorityIcon); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java index a0d10759ba56..8c225113677b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java @@ -231,6 +231,7 @@ public class NotificationContentInflaterTest extends SysuiTestCase { NotificationContentInflater.applyRemoteView( AsyncTask.SERIAL_EXECUTOR, false /* inflateSynchronously */, + /* isMinimized= */ false, result, FLAG_CONTENT_VIEW_EXPANDED, 0, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java index 76470dbe6d21..1534c84fd99a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/RowContentBindStageTest.java @@ -197,7 +197,7 @@ public class RowContentBindStageTest extends SysuiTestCase { params.clearDirtyContentViews(); // WHEN low priority is set and stage executed. - params.setUseLowPriority(true); + params.setUseMinimized(true); mRowContentBindStage.executeStage(mEntry, mRow, (en) -> { }); // THEN binder is called with use low priority and contracted/expanded are called to bind. @@ -210,7 +210,7 @@ public class RowContentBindStageTest extends SysuiTestCase { anyBoolean(), any()); BindParams usedParams = bindParamsCaptor.getValue(); - assertTrue(usedParams.isLowPriority); + assertTrue(usedParams.isMinimized); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java index 1f38a73020b2..3b16f1416935 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java @@ -67,7 +67,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { @Test public void testGetMaxAllowedVisibleChildren_lowPriority() { - mChildrenContainer.setIsLowPriority(true); + mChildrenContainer.setIsMinimized(true); Assert.assertEquals(mChildrenContainer.getMaxAllowedVisibleChildren(), NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED); } @@ -81,7 +81,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { @Test public void testGetMaxAllowedVisibleChildren_lowPriority_expandedChildren() { - mChildrenContainer.setIsLowPriority(true); + mChildrenContainer.setIsMinimized(true); mChildrenContainer.setChildrenExpanded(true); Assert.assertEquals(mChildrenContainer.getMaxAllowedVisibleChildren(), NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED); @@ -89,7 +89,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { @Test public void testGetMaxAllowedVisibleChildren_lowPriority_userLocked() { - mChildrenContainer.setIsLowPriority(true); + mChildrenContainer.setIsMinimized(true); mChildrenContainer.setUserLocked(true); Assert.assertEquals(mChildrenContainer.getMaxAllowedVisibleChildren(), NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_SYSTEM_EXPANDED); @@ -118,7 +118,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { @Test public void testShowingAsLowPriority_lowPriority() { - mChildrenContainer.setIsLowPriority(true); + mChildrenContainer.setIsMinimized(true); Assert.assertTrue(mChildrenContainer.showingAsLowPriority()); } @@ -129,7 +129,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { @Test public void testShowingAsLowPriority_lowPriority_expanded() { - mChildrenContainer.setIsLowPriority(true); + mChildrenContainer.setIsMinimized(true); mGroup.setExpandable(true); mGroup.setUserExpanded(true, false); Assert.assertFalse(mChildrenContainer.showingAsLowPriority()); @@ -140,7 +140,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { mGroup.setUserLocked(true); mGroup.setExpandable(true); mGroup.setUserExpanded(true); - mChildrenContainer.setIsLowPriority(true); + mChildrenContainer.setIsMinimized(true); Assert.assertEquals(mChildrenContainer.getMaxAllowedVisibleChildren(), NotificationChildrenContainer.NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED); } @@ -148,14 +148,14 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { @Test @DisableFlags(AsyncGroupHeaderViewInflation.FLAG_NAME) public void testLowPriorityHeaderCleared() { - mGroup.setIsLowPriority(true); + mGroup.setIsMinimized(true); NotificationHeaderView lowPriorityHeaderView = - mChildrenContainer.getLowPriorityViewWrapper().getNotificationHeader(); + mChildrenContainer.getMinimizedGroupHeaderWrapper().getNotificationHeader(); Assert.assertEquals(View.VISIBLE, lowPriorityHeaderView.getVisibility()); Assert.assertSame(mChildrenContainer, lowPriorityHeaderView.getParent()); - mGroup.setIsLowPriority(false); + mGroup.setIsMinimized(false); assertNull(lowPriorityHeaderView.getParent()); - assertNull(mChildrenContainer.getLowPriorityViewWrapper()); + assertNull(mChildrenContainer.getMinimizedGroupHeaderWrapper()); } @Test @@ -169,7 +169,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { @Test @EnableFlags(AsyncGroupHeaderViewInflation.FLAG_NAME) public void testSetLowPriorityWithAsyncInflation_noHeaderReInflation() { - mChildrenContainer.setIsLowPriority(true); + mChildrenContainer.setIsMinimized(true); assertNull("We don't inflate header from the main thread with Async " + "Inflation enabled", mChildrenContainer.getCurrentHeaderView()); } @@ -179,21 +179,21 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { public void setLowPriorityBeforeLowPriorityHeaderSet() { //Given: the children container does not have a low-priority header, and is not low-priority - assertNull(mChildrenContainer.getLowPriorityViewWrapper()); - mGroup.setIsLowPriority(false); + assertNull(mChildrenContainer.getMinimizedGroupHeaderWrapper()); + mGroup.setIsMinimized(false); //When: set the children container to be low-priority and set the low-priority header - mGroup.setIsLowPriority(true); - mGroup.setLowPriorityGroupHeader(createHeaderView(/* lowPriorityHeader= */ true)); + mGroup.setIsMinimized(true); + mGroup.setMinimizedGroupHeader(createHeaderView(/* lowPriorityHeader= */ true)); //Then: the low-priority group header should be visible NotificationHeaderView lowPriorityHeaderView = - mChildrenContainer.getLowPriorityViewWrapper().getNotificationHeader(); + mChildrenContainer.getMinimizedGroupHeaderWrapper().getNotificationHeader(); Assert.assertEquals(View.VISIBLE, lowPriorityHeaderView.getVisibility()); Assert.assertSame(mChildrenContainer, lowPriorityHeaderView.getParent()); //When: set the children container to be not low-priority and set the normal header - mGroup.setIsLowPriority(false); + mGroup.setIsMinimized(false); mGroup.setGroupHeader(createHeaderView(/* lowPriorityHeader= */ false)); //Then: the low-priority group header should not be visible , normal header should be @@ -211,9 +211,9 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { public void changeLowPriorityAfterHeaderSet() { //Given: the children container does not have headers, and is not low-priority - assertNull(mChildrenContainer.getLowPriorityViewWrapper()); + assertNull(mChildrenContainer.getMinimizedGroupHeaderWrapper()); assertNull(mChildrenContainer.getNotificationHeaderWrapper()); - mGroup.setIsLowPriority(false); + mGroup.setIsMinimized(false); //When: set the set the normal header mGroup.setGroupHeader(createHeaderView(/* lowPriorityHeader= */ false)); @@ -225,14 +225,14 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { Assert.assertSame(mChildrenContainer, headerView.getParent()); //When: set the set the row to be low priority, and set the low-priority header - mGroup.setIsLowPriority(true); - mGroup.setLowPriorityGroupHeader(createHeaderView(/* lowPriorityHeader= */ true)); + mGroup.setIsMinimized(true); + mGroup.setMinimizedGroupHeader(createHeaderView(/* lowPriorityHeader= */ true)); //Then: the header view should not be visible, the low-priority group header should be // visible Assert.assertEquals(View.INVISIBLE, headerView.getVisibility()); NotificationHeaderView lowPriorityHeaderView = - mChildrenContainer.getLowPriorityViewWrapper().getNotificationHeader(); + mChildrenContainer.getMinimizedGroupHeaderWrapper().getNotificationHeader(); Assert.assertEquals(View.VISIBLE, lowPriorityHeaderView.getVisibility()); } @@ -263,7 +263,7 @@ public class NotificationChildrenContainerTest extends SysuiTestCase { @Test @DisableFlags(AsyncGroupHeaderViewInflation.FLAG_NAME) public void applyRoundnessAndInvalidate_should_be_immediately_applied_on_headerLowPriority() { - mChildrenContainer.setIsLowPriority(true); + mChildrenContainer.setIsMinimized(true); NotificationHeaderViewWrapper header = mChildrenContainer.getNotificationHeaderWrapper(); Assert.assertEquals(0f, header.getTopRoundness(), 0.001f); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index a4f88fbe1469..10d2191c0e07 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -49,7 +49,6 @@ import android.view.ViewTreeObserver; import androidx.test.filters.SmallTest; -import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto; @@ -63,6 +62,7 @@ import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository; import com.android.systemui.keyguard.shared.model.KeyguardState; import com.android.systemui.keyguard.shared.model.TransitionStep; +import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.media.controls.ui.controller.KeyguardMediaController; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; @@ -130,6 +130,7 @@ import javax.inject.Provider; @RunWith(AndroidTestingRunner.class) public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { + protected KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); @Mock private NotificationGutsManager mNotificationGutsManager; @Mock private NotificationsController mNotificationsController; @@ -167,7 +168,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { @Mock private SceneContainerFlags mSceneContainerFlags; @Mock private Provider<WindowRootView> mWindowRootView; @Mock private NotificationStackAppearanceInteractor mNotificationStackAppearanceInteractor; - @Mock private InteractionJankMonitor mJankMonitor; private final StackStateLogger mStackLogger = new StackStateLogger(logcatLogBuffer(), logcatLogBuffer()); private final NotificationStackScrollLogger mLogger = new NotificationStackScrollLogger( @@ -1030,7 +1030,7 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mSceneContainerFlags, mWindowRootView, mNotificationStackAppearanceInteractor, - mJankMonitor, + mKosmos.getInteractionJankMonitor(), mStackLogger, mLogger, mNotificationStackSizeCalculator, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt index 933b5b519672..358709f48ea8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerFlagDisabledTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.policy import android.app.IActivityManager +import android.content.pm.PackageManager import android.media.projection.MediaProjectionManager import android.os.Handler import android.platform.test.annotations.DisableFlags @@ -44,6 +45,7 @@ class SensitiveNotificationProtectionControllerFlagDisabledTest : SysuiTestCase( @Mock private lateinit var handler: Handler @Mock private lateinit var activityManager: IActivityManager @Mock private lateinit var mediaProjectionManager: MediaProjectionManager + @Mock private lateinit var packageManager: PackageManager private lateinit var controller: SensitiveNotificationProtectionControllerImpl @Before @@ -56,6 +58,7 @@ class SensitiveNotificationProtectionControllerFlagDisabledTest : SysuiTestCase( FakeGlobalSettings(), mediaProjectionManager, activityManager, + packageManager, handler, FakeExecutor(FakeSystemClock()), logger diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt index 4b4e315f5533..7dfe6d01912f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SensitiveNotificationProtectionControllerTest.kt @@ -25,9 +25,14 @@ import android.app.Notification.VISIBILITY_PUBLIC import android.app.NotificationChannel import android.app.NotificationManager.IMPORTANCE_HIGH import android.app.NotificationManager.VISIBILITY_NO_OVERRIDE +import android.content.pm.PackageManager import android.media.projection.MediaProjectionInfo import android.media.projection.MediaProjectionManager +import android.permission.flags.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION 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 import android.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper @@ -48,9 +53,11 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyString import org.mockito.Mock import org.mockito.Mockito.mock import org.mockito.Mockito.times @@ -64,10 +71,13 @@ import org.mockito.MockitoAnnotations @RunWithLooper @EnableFlags(Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING) class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { + @get:Rule val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + private val logger = SensitiveNotificationProtectionControllerLogger(logcatLogBuffer()) @Mock private lateinit var activityManager: IActivityManager @Mock private lateinit var mediaProjectionManager: MediaProjectionManager + @Mock private lateinit var packageManager: PackageManager @Mock private lateinit var mediaProjectionInfo: MediaProjectionInfo @Mock private lateinit var listener1: Runnable @Mock private lateinit var listener2: Runnable @@ -87,6 +97,9 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { whenever(activityManager.bugreportWhitelistedPackages) .thenReturn(listOf(BUGREPORT_PACKAGE_NAME)) + whenever(packageManager.checkPermission(anyString(), anyString())) + .thenReturn(PackageManager.PERMISSION_DENIED) + executor = FakeExecutor(FakeSystemClock()) globalSettings = FakeGlobalSettings() controller = @@ -95,6 +108,7 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { globalSettings, mediaProjectionManager, activityManager, + packageManager, mockExecutorHandler(executor), executor, logger @@ -237,6 +251,36 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { } @Test + @RequiresFlagsDisabled(FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION) + fun isSensitiveStateActive_projectionActive_permissionExempt_flagDisabled_true() { + whenever( + packageManager.checkPermission( + android.Manifest.permission.RECORD_SENSITIVE_CONTENT, + mediaProjectionInfo.packageName + ) + ) + .thenReturn(PackageManager.PERMISSION_GRANTED) + mediaProjectionCallback.onStart(mediaProjectionInfo) + + assertTrue(controller.isSensitiveStateActive) + } + + @Test + @RequiresFlagsEnabled(FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION) + fun isSensitiveStateActive_projectionActive_permissionExempt_false() { + whenever( + packageManager.checkPermission( + android.Manifest.permission.RECORD_SENSITIVE_CONTENT, + mediaProjectionInfo.packageName + ) + ) + .thenReturn(PackageManager.PERMISSION_GRANTED) + mediaProjectionCallback.onStart(mediaProjectionInfo) + + assertFalse(controller.isSensitiveStateActive) + } + + @Test fun isSensitiveStateActive_projectionActive_bugReportHandlerExempt_false() { whenever(mediaProjectionInfo.packageName).thenReturn(BUGREPORT_PACKAGE_NAME) mediaProjectionCallback.onStart(mediaProjectionInfo) @@ -309,6 +353,40 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { } @Test + @RequiresFlagsDisabled(FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION) + fun shouldProtectNotification_projectionActive_permissionExempt_flagDisabled_true() { + whenever( + packageManager.checkPermission( + android.Manifest.permission.RECORD_SENSITIVE_CONTENT, + mediaProjectionInfo.packageName + ) + ) + .thenReturn(PackageManager.PERMISSION_GRANTED) + mediaProjectionCallback.onStart(mediaProjectionInfo) + + val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false) + + assertTrue(controller.shouldProtectNotification(notificationEntry)) + } + + @Test + @RequiresFlagsEnabled(FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION) + fun shouldProtectNotification_projectionActive_permissionExempt_false() { + whenever( + packageManager.checkPermission( + android.Manifest.permission.RECORD_SENSITIVE_CONTENT, + mediaProjectionInfo.packageName + ) + ) + .thenReturn(PackageManager.PERMISSION_GRANTED) + mediaProjectionCallback.onStart(mediaProjectionInfo) + + val notificationEntry = setupNotificationEntry(TEST_PACKAGE_NAME, false) + + assertFalse(controller.shouldProtectNotification(notificationEntry)) + } + + @Test fun shouldProtectNotification_projectionActive_bugReportHandlerExempt_false() { whenever(mediaProjectionInfo.packageName).thenReturn(BUGREPORT_PACKAGE_NAME) mediaProjectionCallback.onStart(mediaProjectionInfo) @@ -327,6 +405,7 @@ class SensitiveNotificationProtectionControllerTest : SysuiTestCase() { assertFalse(controller.shouldProtectNotification(notificationEntry)) } + @Test fun shouldProtectNotification_projectionActive_publicNotification_false() { mediaProjectionCallback.onStart(mediaProjectionInfo) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt index 8ed9f45bd1ba..02b79af15c05 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt @@ -38,7 +38,7 @@ val Kosmos.simBouncerInteractor by Fixture { telephonyManager = telephonyManager, resources = mainResources, keyguardUpdateMonitor = keyguardUpdateMonitor, - euiccManager = applicationContext.getSystemService(Context.EUICC_SERVICE) as EuiccManager, + euiccManager = applicationContext.getSystemService(Context.EUICC_SERVICE) as EuiccManager?, mobileConnectionsRepository = mobileConnectionsRepository, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt index dcbd5777489a..de6bfb2f8756 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt @@ -18,7 +18,6 @@ package com.android.systemui.keyguard.data.repository import android.annotation.FloatRange -import android.util.Log import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo @@ -48,21 +47,8 @@ class FakeKeyguardTransitionRepository @Inject constructor() : KeyguardTransitio override val transitions: SharedFlow<TransitionStep> = _transitions init { - _transitions.tryEmit( - TransitionStep( - transitionState = TransitionState.STARTED, - from = KeyguardState.OFF, - to = KeyguardState.LOCKSCREEN, - ) - ) - - _transitions.tryEmit( - TransitionStep( - transitionState = TransitionState.FINISHED, - from = KeyguardState.OFF, - to = KeyguardState.LOCKSCREEN, - ) - ) + // Seed the fake repository with the same initial steps the actual repository uses. + KeyguardTransitionRepositoryImpl.initialTransitionSteps.forEach { _transitions.tryEmit(it) } } /** @@ -207,16 +193,15 @@ class FakeKeyguardTransitionRepository @Inject constructor() : KeyguardTransitio suspend fun sendTransitionSteps( steps: List<TransitionStep>, testScope: TestScope, - validateStep: Boolean = true + validateSteps: Boolean = true ) { steps.forEach { - sendTransitionStep(step = it, validateStep = validateStep) + sendTransitionStep(step = it, validateStep = validateSteps) testScope.testScheduler.runCurrent() } } override fun startTransition(info: TransitionInfo): UUID? { - Log.i("TEST", "Start transition: ", Exception()) return if (info.animator == null) UUID.randomUUID() else null } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt index 73fd9991945c..709f86426f94 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt @@ -25,6 +25,7 @@ import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInterac import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransition import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.testScope import com.android.systemui.scene.shared.flag.sceneContainerFlags import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager @@ -50,5 +51,6 @@ val Kosmos.deviceEntryIconViewModel by Fixture { keyguardViewController = { statusBarKeyguardViewManager }, deviceEntryInteractor = deviceEntryInteractor, deviceEntrySourceInteractor = deviceEntrySourceInteractor, + scope = testScope.backgroundScope, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModelKosmos.kt new file mode 100644 index 000000000000..1b6fa064854d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/GoneToLockscreenTransitionViewModelKosmos.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.keyguard.ui.viewmodel + +import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import kotlinx.coroutines.ExperimentalCoroutinesApi + +var Kosmos.goneToLockscreenTransitionViewModel by Fixture { + GoneToLockscreenTransitionViewModel( + animationFlow = keyguardTransitionAnimationFlow, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt index a84899e0e6ab..b91aafea9c38 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 @@ -52,6 +52,7 @@ val Kosmos.keyguardRootViewModel by Fixture { goneToAodTransitionViewModel = goneToAodTransitionViewModel, goneToDozingTransitionViewModel = goneToDozingTransitionViewModel, goneToDreamingTransitionViewModel = goneToDreamingTransitionViewModel, + goneToLockscreenTransitionViewModel = goneToLockscreenTransitionViewModel, lockscreenToAodTransitionViewModel = lockscreenToAodTransitionViewModel, lockscreenToDozingTransitionViewModel = lockscreenToDozingTransitionViewModel, lockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt index 85662512a5ee..370afc3b660b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToLockscreenTransitionViewModelKosmos.kt @@ -18,7 +18,6 @@ package com.android.systemui.keyguard.ui.viewmodel -import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture @@ -26,7 +25,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.primaryBouncerToLockscreenTransitionViewModel by Fixture { PrimaryBouncerToLockscreenTransitionViewModel( - deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor, animationFlow = keyguardTransitionAnimationFlow, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryKosmos.kt new file mode 100644 index 000000000000..5c17cb95de84 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaDataRepositoryKosmos.kt @@ -0,0 +1,26 @@ +/* + * 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.media.controls.data.repository + +import com.android.systemui.dump.dumpManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.media.controls.util.mediaFlags + +val Kosmos.mediaDataRepository by Fixture { + MediaDataRepository(mediaFlags = mediaFlags, dumpManager = dumpManager) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryKosmos.kt new file mode 100644 index 000000000000..7ce810eb7818 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/data/repository/MediaFilterRepositoryKosmos.kt @@ -0,0 +1,21 @@ +/* + * 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.media.controls.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.mediaFilterRepository by Kosmos.Fixture { MediaFilterRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestKosmos.kt new file mode 100644 index 000000000000..12a63250fcfc --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataCombineLatestKosmos.kt @@ -0,0 +1,21 @@ +/* + * 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.media.controls.domain.pipeline + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.mediaDataCombineLatest by Kosmos.Fixture { MediaDataCombineLatest() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.kt new file mode 100644 index 000000000000..d56222ed45a4 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataFilterKosmos.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.media.controls.domain.pipeline + +import android.content.applicationContext +import com.android.systemui.broadcast.BroadcastSender +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.util.mediaFlags +import com.android.systemui.media.controls.util.mediaUiEventLogger +import com.android.systemui.settings.userTracker +import com.android.systemui.statusbar.notificationLockscreenUserManager +import com.android.systemui.util.time.systemClock +import com.android.systemui.util.wakelock.WakeLockFake + +val Kosmos.mediaDataFilter by + Kosmos.Fixture { + MediaDataFilterImpl( + context = applicationContext, + userTracker = userTracker, + broadcastSender = + BroadcastSender( + applicationContext, + WakeLockFake.Builder(applicationContext), + fakeExecutor + ), + lockscreenUserManager = notificationLockscreenUserManager, + executor = fakeExecutor, + systemClock = systemClock, + logger = mediaUiEventLogger, + mediaFlags = mediaFlags, + mediaFilterRepository = mediaFilterRepository, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt new file mode 100644 index 000000000000..cc1ad1fda6dd --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataProcessorKosmos.kt @@ -0,0 +1,64 @@ +/* + * 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.media.controls.domain.pipeline + +import android.app.smartspace.SmartspaceManager +import android.content.applicationContext +import android.os.fakeExecutorHandler +import com.android.keyguard.keyguardUpdateMonitor +import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.dump.dumpManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.media.controls.data.repository.mediaDataRepository +import com.android.systemui.media.controls.shared.model.SmartspaceMediaDataProvider +import com.android.systemui.media.controls.util.mediaControllerFactory +import com.android.systemui.media.controls.util.mediaFlags +import com.android.systemui.media.controls.util.mediaUiEventLogger +import com.android.systemui.plugins.activityStarter +import com.android.systemui.util.Utils +import com.android.systemui.util.settings.fakeSettings +import com.android.systemui.util.time.systemClock + +val Kosmos.mediaDataProcessor by + Kosmos.Fixture { + MediaDataProcessor( + context = applicationContext, + applicationScope = applicationCoroutineScope, + backgroundDispatcher = testDispatcher, + backgroundExecutor = fakeExecutor, + uiExecutor = fakeExecutor, + foregroundExecutor = fakeExecutor, + handler = fakeExecutorHandler, + mediaControllerFactory = mediaControllerFactory, + broadcastDispatcher = broadcastDispatcher, + dumpManager = dumpManager, + activityStarter = activityStarter, + smartspaceMediaDataProvider = SmartspaceMediaDataProvider(), + useMediaResumption = Utils.useMediaResumption(applicationContext), + useQsMediaPlayer = Utils.useQsMediaPlayer(applicationContext), + systemClock = systemClock, + secureSettings = fakeSettings, + mediaFlags = mediaFlags, + logger = mediaUiEventLogger, + smartspaceManager = SmartspaceManager(applicationContext), + keyguardUpdateMonitor = keyguardUpdateMonitor, + mediaDataRepository = mediaDataRepository, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerKosmos.kt new file mode 100644 index 000000000000..b98f557c0c34 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDeviceManagerKosmos.kt @@ -0,0 +1,45 @@ +/* + * 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.media.controls.domain.pipeline + +import android.content.applicationContext +import android.media.MediaRouter2Manager +import android.os.fakeExecutorHandler +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.media.controls.util.localMediaManagerFactory +import com.android.systemui.media.controls.util.mediaControllerFactory +import com.android.systemui.media.muteawait.mediaMuteAwaitConnectionManagerFactory +import com.android.systemui.statusbar.policy.configurationController + +val Kosmos.mediaDeviceManager by + Kosmos.Fixture { + MediaDeviceManager( + context = applicationContext, + controllerFactory = mediaControllerFactory, + localMediaManagerFactory = localMediaManagerFactory, + mr2manager = { MediaRouter2Manager.getInstance(applicationContext) }, + muteAwaitConnectionManagerFactory = mediaMuteAwaitConnectionManagerFactory, + configurationController = configurationController, + localBluetoothManager = { + LocalBluetoothManager.create(applicationContext, fakeExecutorHandler) + }, + fgExecutor = fakeExecutor, + bgExecutor = fakeExecutor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaResumeListenerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaResumeListenerKosmos.kt new file mode 100644 index 000000000000..2a3e84b74369 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaResumeListenerKosmos.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.media.controls.domain.pipeline + +import android.content.applicationContext +import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.dump.dumpManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.media.controls.domain.resume.MediaResumeListener +import com.android.systemui.media.controls.domain.resume.resumeMediaBrowserFactory +import com.android.systemui.media.controls.util.mediaFlags +import com.android.systemui.settings.userTracker +import com.android.systemui.tuner.TunerService +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.time.systemClock + +val Kosmos.mediaResumeListener by + Kosmos.Fixture { + MediaResumeListener( + context = applicationContext, + broadcastDispatcher = broadcastDispatcher, + userTracker = userTracker, + mainExecutor = fakeExecutor, + backgroundExecutor = fakeExecutor, + tunerService = mock<TunerService> {}, + mediaBrowserFactory = resumeMediaBrowserFactory, + dumpManager = dumpManager, + systemClock = systemClock, + mediaFlags = mediaFlags, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilterKosmos.kt new file mode 100644 index 000000000000..9b02a5b10492 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaSessionBasedFilterKosmos.kt @@ -0,0 +1,32 @@ +/* + * 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.media.controls.domain.pipeline + +import android.content.applicationContext +import android.media.session.MediaSessionManager +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos + +val Kosmos.mediaSessionBasedFilter by + Kosmos.Fixture { + MediaSessionBasedFilter( + context = applicationContext, + sessionManager = MediaSessionManager(applicationContext), + foregroundExecutor = fakeExecutor, + backgroundExecutor = fakeExecutor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt new file mode 100644 index 000000000000..6ec6378e3bc2 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaTimeoutListenerKosmos.kt @@ -0,0 +1,37 @@ +/* + * 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.media.controls.domain.pipeline + +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.media.controls.util.mediaControllerFactory +import com.android.systemui.media.controls.util.mediaFlags +import com.android.systemui.plugins.statusbar.statusBarStateController +import com.android.systemui.util.time.systemClock + +val Kosmos.mediaTimeoutListener by + Kosmos.Fixture { + MediaTimeoutListener( + mediaControllerFactory = mediaControllerFactory, + mainExecutor = fakeExecutor, + logger = MediaTimeoutLogger(logcatLogBuffer("MediaTimeoutLogBuffer")), + statusBarStateController = statusBarStateController, + systemClock = systemClock, + mediaFlags = mediaFlags, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractorKosmos.kt new file mode 100644 index 000000000000..e5e2affdc49a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/interactor/MediaCarouselInteractorKosmos.kt @@ -0,0 +1,47 @@ +/* + * 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.media.controls.domain.pipeline.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.media.controls.data.repository.mediaDataRepository +import com.android.systemui.media.controls.data.repository.mediaFilterRepository +import com.android.systemui.media.controls.domain.pipeline.mediaDataCombineLatest +import com.android.systemui.media.controls.domain.pipeline.mediaDataFilter +import com.android.systemui.media.controls.domain.pipeline.mediaDataProcessor +import com.android.systemui.media.controls.domain.pipeline.mediaDeviceManager +import com.android.systemui.media.controls.domain.pipeline.mediaResumeListener +import com.android.systemui.media.controls.domain.pipeline.mediaSessionBasedFilter +import com.android.systemui.media.controls.domain.pipeline.mediaTimeoutListener +import com.android.systemui.media.controls.util.mediaFlags + +val Kosmos.mediaCarouselInteractor by + Kosmos.Fixture { + MediaCarouselInteractor( + applicationScope = applicationCoroutineScope, + mediaDataRepository = mediaDataRepository, + mediaDataProcessor = mediaDataProcessor, + mediaTimeoutListener = mediaTimeoutListener, + mediaResumeListener = mediaResumeListener, + mediaSessionBasedFilter = mediaSessionBasedFilter, + mediaDeviceManager = mediaDeviceManager, + mediaDataCombineLatest = mediaDataCombineLatest, + mediaDataFilter = mediaDataFilter, + mediaFilterRepository = mediaFilterRepository, + mediaFlags = mediaFlags, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/MediaBrowserFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/MediaBrowserFactoryKosmos.kt new file mode 100644 index 000000000000..2621869786d0 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/MediaBrowserFactoryKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.domain.resume + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos + +val Kosmos.mediaBrowserFactory by Kosmos.Fixture { MediaBrowserFactory(applicationContext) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/ResumeMediaBrowserFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/ResumeMediaBrowserFactoryKosmos.kt new file mode 100644 index 000000000000..ed720bd7d7ca --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/resume/ResumeMediaBrowserFactoryKosmos.kt @@ -0,0 +1,30 @@ +/* + * 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.media.controls.domain.resume + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.log.logcatLogBuffer + +val Kosmos.resumeMediaBrowserFactory by + Kosmos.Fixture { + ResumeMediaBrowserFactory( + applicationContext, + mediaBrowserFactory, + ResumeMediaBrowserLogger(logcatLogBuffer("ResumeMediaLogBuffer")) + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/LocalMediaManagerFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/LocalMediaManagerFactoryKosmos.kt new file mode 100644 index 000000000000..2e0c9b848d1f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/LocalMediaManagerFactoryKosmos.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.util + +import android.content.applicationContext +import android.os.fakeExecutorHandler +import com.android.settingslib.bluetooth.LocalBluetoothManager +import com.android.systemui.kosmos.Kosmos + +val Kosmos.localMediaManagerFactory by + Kosmos.Fixture { + LocalMediaManagerFactory( + context = applicationContext, + localBluetoothManager = + LocalBluetoothManager.create(applicationContext, fakeExecutorHandler), + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaControllerFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaControllerFactoryKosmos.kt new file mode 100644 index 000000000000..1ce6e82f71d8 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaControllerFactoryKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.util + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos + +val Kosmos.mediaControllerFactory by Kosmos.Fixture { MediaControllerFactory(applicationContext) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaFlagsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaFlagsKosmos.kt new file mode 100644 index 000000000000..6f652f224975 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaFlagsKosmos.kt @@ -0,0 +1,26 @@ +/* + * 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.media.controls.util + +import com.android.systemui.flags.featureFlagsClassic +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.scene.shared.flag.sceneContainerFlags + +val Kosmos.mediaFlags by + Kosmos.Fixture { + MediaFlags(featureFlags = featureFlagsClassic, sceneContainerFlags = sceneContainerFlags) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaUiEventLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaUiEventLoggerKosmos.kt new file mode 100644 index 000000000000..b01876d887bb --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/util/MediaUiEventLoggerKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.util + +import com.android.internal.logging.uiEventLogger +import com.android.systemui.kosmos.Kosmos + +val Kosmos.mediaUiEventLogger by Kosmos.Fixture { MediaUiEventLogger(uiEventLogger) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactoryKosmos.kt new file mode 100644 index 000000000000..b78bd588869f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/muteawait/MediaMuteAwaitConnectionManagerFactoryKosmos.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.muteawait + +import android.content.applicationContext +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.log.logcatLogBuffer + +val Kosmos.mediaMuteAwaitConnectionManagerFactory by + Kosmos.Fixture { + MediaMuteAwaitConnectionManagerFactory( + context = applicationContext, + logger = MediaMuteAwaitLogger(logcatLogBuffer("MediaMuteAwaitLogBuffer")), + mainExecutor = fakeExecutor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/work/WorkModeTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/work/WorkModeTileKosmos.kt new file mode 100644 index 000000000000..c04c5ed49b33 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/work/WorkModeTileKosmos.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.tiles.impl.work + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.qs.qsEventLogger +import com.android.systemui.statusbar.policy.PolicyModule + +val Kosmos.qsWorkModeTileConfig by + Kosmos.Fixture { PolicyModule.provideWorkModeTileConfig(qsEventLogger) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt index 546a1e019c6b..5605d1000f4e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt @@ -18,10 +18,12 @@ package com.android.systemui.statusbar.notification.stack.domain.interactor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.notification.stack.data.repository.notificationStackAppearanceRepository val Kosmos.notificationStackAppearanceInteractor by Fixture { NotificationStackAppearanceInteractor( repository = notificationStackAppearanceRepository, + shadeInteractor = shadeInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeManagedProfileController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeManagedProfileController.java index 18b07cf25fbc..59adb11e9054 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeManagedProfileController.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeManagedProfileController.java @@ -19,24 +19,65 @@ import android.testing.LeakCheck; import com.android.systemui.statusbar.phone.ManagedProfileController; import com.android.systemui.statusbar.phone.ManagedProfileController.Callback; +import java.util.ArrayList; +import java.util.List; + public class FakeManagedProfileController extends BaseLeakChecker<Callback> implements ManagedProfileController { + + private List<Callback> mCallbackList = new ArrayList<>(); + private boolean mIsEnabled = false; + private boolean mHasActiveProfile = false; + public FakeManagedProfileController(LeakCheck test) { super(test, "profile"); } @Override + public void addCallback(Callback cb) { + mCallbackList.add(cb); + cb.onManagedProfileChanged(); + } + + @Override + public void removeCallback(Callback cb) { + mCallbackList.remove(cb); + } + + @Override public void setWorkModeEnabled(boolean enabled) { + if (mIsEnabled != enabled) { + mIsEnabled = enabled; + for (Callback cb: mCallbackList) { + cb.onManagedProfileChanged(); + } + } } @Override public boolean hasActiveProfile() { - return false; + return mHasActiveProfile; + } + + /** + * Triggers onManagedProfileChanged on callbacks when value flips. + */ + public void setHasActiveProfile(boolean hasActiveProfile) { + if (mHasActiveProfile != hasActiveProfile) { + mHasActiveProfile = hasActiveProfile; + for (Callback cb: mCallbackList) { + cb.onManagedProfileChanged(); + if (!hasActiveProfile) { + cb.onManagedProfileRemoved(); + } + } + } + } @Override public boolean isWorkModeEnabled() { - return false; + return mIsEnabled; } } diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java index 81ad31e631fe..61ec7b4bbc72 100644 --- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java +++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java @@ -383,9 +383,21 @@ public class Parcel_host { // Assume false for now, because we don't support writing FDs yet. return false; } + public static boolean nativeHasFileDescriptorsInRange( long nativePtr, int offset, int length) { // Assume false for now, because we don't support writing FDs yet. return false; } + + public static boolean nativeHasBinders(long nativePtr) { + // Assume false for now, because we don't support adding binders. + return false; + } + + public static boolean nativeHasBindersInRange( + long nativePtr, int offset, int length) { + // Assume false for now, because we don't support writing FDs yet. + return false; + } } diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 880a68776055..3e7682a645ee 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -2703,11 +2703,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub Map<ComponentName, AccessibilityServiceConnection> componentNameToServiceMap = userState.mComponentNameToServiceMap; boolean isUnlockingOrUnlocked = mUmi.isUserUnlockingOrUnlocked(userState.mUserId); + Set<ComponentName> installedComponentNames = new HashSet<>(); for (int i = 0, count = userState.mInstalledServices.size(); i < count; i++) { AccessibilityServiceInfo installedService = userState.mInstalledServices.get(i); ComponentName componentName = ComponentName.unflattenFromString( installedService.getId()); + installedComponentNames.add(componentName); AccessibilityServiceConnection service = componentNameToServiceMap.get(componentName); @@ -2767,6 +2769,28 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub audioManager.setAccessibilityServiceUids(mTempIntArray); } mActivityTaskManagerService.setAccessibilityServiceUids(mTempIntArray); + final Iterator<ComponentName> it = userState.mEnabledServices.iterator(); + boolean anyServiceRemoved = false; + while (it.hasNext()) { + final ComponentName comp = it.next(); + if (!installedComponentNames.contains(comp)) { + it.remove(); + userState.mTouchExplorationGrantedServices.remove(comp); + anyServiceRemoved = true; + } + } + if (anyServiceRemoved) { + // Update the enabled services setting. + persistComponentNamesToSettingLocked( + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, + userState.mEnabledServices, + userState.mUserId); + // Update the touch exploration granted services setting. + persistComponentNamesToSettingLocked( + Settings.Secure.TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES, + userState.mTouchExplorationGrantedServices, + userState.mUserId); + } updateAccessibilityEnabledSettingLocked(userState); } diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java index e4f1d3acce6d..07fcb5042cbc 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerService.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerService.java @@ -718,7 +718,9 @@ public final class AutofillManagerService + ", mPccUseFallbackDetection=" + mPccUseFallbackDetection + ", mPccProviderHints=" + mPccProviderHints + ", mAutofillCredmanIntegrationEnabled=" - + mAutofillCredmanIntegrationEnabled); + + mAutofillCredmanIntegrationEnabled + + ", mIsFillFieldsFromCurrentSessionOnly=" + + mIsFillFieldsFromCurrentSessionOnly); } } } diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index e1291e5f75ec..14a331c6ffe0 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -1672,9 +1672,10 @@ final class AutofillManagerServiceImpl @Override // from InlineSuggestionRenderCallbacksImpl public void onServiceDied(@NonNull RemoteInlineSuggestionRenderService service) { - // Don't do anything; eventually the system will bind to it again... Slog.w(TAG, "remote service died: " + service); - mRemoteInlineSuggestionRenderService = null; + synchronized (mLock) { + resetExtServiceLocked(); + } } } diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java index 8244d20e8e6a..3ec6e475179a 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java @@ -23,6 +23,7 @@ import static android.companion.virtual.VirtualDeviceParams.ACTIVITY_POLICY_DEFA import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; import static android.companion.virtual.VirtualDeviceParams.NAVIGATION_POLICY_DEFAULT_ALLOWED; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_ACTIVITY; +import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CAMERA; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_CLIPBOARD; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS; @@ -82,6 +83,8 @@ import android.hardware.input.VirtualStylusConfig; import android.hardware.input.VirtualStylusMotionEvent; import android.hardware.input.VirtualTouchEvent; import android.hardware.input.VirtualTouchscreenConfig; +import android.media.AudioManager; +import android.media.audiopolicy.AudioMix; import android.os.Binder; import android.os.IBinder; import android.os.LocaleList; @@ -1063,6 +1066,37 @@ final class VirtualDeviceImpl extends IVirtualDevice.Stub } @Override + public boolean hasCustomAudioInputSupport() throws RemoteException { + if (!Flags.vdmPublicApis()) { + return false; + } + + if (!android.media.audiopolicy.Flags.audioMixTestApi()) { + return false; + } + if (!android.media.audiopolicy.Flags.recordAudioDeviceAwarePermission()) { + return false; + } + + if (getDevicePolicy(POLICY_TYPE_AUDIO) == VirtualDeviceParams.DEVICE_POLICY_DEFAULT) { + return false; + } + final long token = Binder.clearCallingIdentity(); + try { + AudioManager audioManager = mContext.getSystemService(AudioManager.class); + for (AudioMix mix : audioManager.getRegisteredPolicyMixes()) { + if (mix.matchesVirtualDeviceId(getDeviceId()) + && mix.getMixType() == AudioMix.MIX_TYPE_RECORDERS) { + return true; + } + } + } finally { + Binder.restoreCallingIdentity(token); + } + return false; + } + + @Override protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) { String indent = " "; fout.println(" VirtualDevice: "); diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java index 6b5ba96f6b1b..2607ed3193eb 100644 --- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java +++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java @@ -1297,15 +1297,19 @@ public class ContentCaptureManagerService extends @Override public void onLoginDetected(@NonNull ParceledListSlice<ContentCaptureEvent> events) { - RemoteContentProtectionService service = createRemoteContentProtectionService(); - if (service == null) { - return; - } - try { - service.onLoginDetected(events); - } catch (Exception ex) { - Slog.e(TAG, "Failed to call remote service", ex); - } + Binder.withCleanCallingIdentity( + () -> { + RemoteContentProtectionService service = + createRemoteContentProtectionService(); + if (service == null) { + return; + } + try { + service.onLoginDetected(events); + } catch (Exception ex) { + Slog.e(TAG, "Failed to call remote service", ex); + } + }); } } diff --git a/services/core/Android.bp b/services/core/Android.bp index d1d7ee7ba0e4..7f5867fb1a74 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -242,6 +242,7 @@ java_library_static { "apache-commons-math", "backstage_power_flags_lib", "notification_flags_lib", + "power_hint_flags_lib", "biometrics_flags_lib", "am_flags_lib", "com_android_server_accessibility_flags_lib", diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index 4ebabdc4cc66..5a97e87f53f7 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -1164,8 +1164,7 @@ final class ActivityManagerShellCommand extends ShellCommand { synchronized (mInternal) { synchronized (mInternal.mProcLock) { app.mOptRecord.setFreezeSticky(isSticky); - mInternal.mOomAdjuster.mCachedAppOptimizer.freezeAppAsyncInternalLSP( - app, 0 /* delayMillis */, true /* force */, false /* immediate */); + mInternal.mOomAdjuster.mCachedAppOptimizer.forceFreezeAppAsyncLSP(app); } } return 0; diff --git a/services/core/java/com/android/server/am/BroadcastConstants.java b/services/core/java/com/android/server/am/BroadcastConstants.java index 91cfb8fe45eb..e676b1fca7fb 100644 --- a/services/core/java/com/android/server/am/BroadcastConstants.java +++ b/services/core/java/com/android/server/am/BroadcastConstants.java @@ -281,7 +281,7 @@ public class BroadcastConstants { * For {@link BroadcastQueueModernImpl}: Maximum number of outgoing broadcasts from a * freezable process that will be allowed before killing the process. */ - public long MAX_FROZEN_OUTGOING_BROADCASTS = DEFAULT_MAX_FROZEN_OUTGOING_BROADCASTS; + public int MAX_FROZEN_OUTGOING_BROADCASTS = DEFAULT_MAX_FROZEN_OUTGOING_BROADCASTS; private static final String KEY_MAX_FROZEN_OUTGOING_BROADCASTS = "max_frozen_outgoing_broadcasts"; private static final int DEFAULT_MAX_FROZEN_OUTGOING_BROADCASTS = 32; diff --git a/services/core/java/com/android/server/am/BroadcastProcessQueue.java b/services/core/java/com/android/server/am/BroadcastProcessQueue.java index e98e1ba6a44e..ed3cd1ea03c8 100644 --- a/services/core/java/com/android/server/am/BroadcastProcessQueue.java +++ b/services/core/java/com/android/server/am/BroadcastProcessQueue.java @@ -277,6 +277,10 @@ class BroadcastProcessQueue { mOutgoingBroadcasts.clear(); } + public void clearOutgoingBroadcasts() { + mOutgoingBroadcasts.clear(); + } + /** * Enqueue the given broadcast to be dispatched to this process at some * future point in time. The target receiver is indicated by the given index diff --git a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java index a6f6b3422066..c08288901157 100644 --- a/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java +++ b/services/core/java/com/android/server/am/BroadcastQueueModernImpl.java @@ -166,7 +166,7 @@ class BroadcastQueueModernImpl extends BroadcastQueue { /** * Map from UID to per-process broadcast queues. If a UID hosts more than * one process, each additional process is stored as a linked list using - * {@link BroadcastProcessQueue#next}. + * {@link BroadcastProcessQueue#processNameNext}. * * @see #getProcessQueue * @see #getOrCreateProcessQueue @@ -661,6 +661,10 @@ class BroadcastQueueModernImpl extends BroadcastQueue { final BroadcastProcessQueue queue = getProcessQueue(app); if (queue != null) { setQueueProcess(queue, app); + // Outgoing broadcasts should be cleared when the process dies but there have been + // issues due to AMS not always informing the BroadcastQueue of process deaths. + // So, clear them when a new process starts as well. + queue.clearOutgoingBroadcasts(); } boolean didSomething = false; @@ -730,6 +734,8 @@ class BroadcastQueueModernImpl extends BroadcastQueue { demoteFromRunningLocked(queue); } + queue.clearOutgoingBroadcasts(); + // Skip any pending registered receivers, since the old process // would never be around to receive them boolean didSomething = queue.forEachMatchingBroadcast((r, i) -> { @@ -781,8 +787,11 @@ class BroadcastQueueModernImpl extends BroadcastQueue { final BroadcastProcessQueue queue = getOrCreateProcessQueue( r.callerApp.processName, r.callerApp.uid); if (queue.getOutgoingBroadcastCount() >= mConstants.MAX_FROZEN_OUTGOING_BROADCASTS) { - // TODO: Kill the process if the outgoing broadcasts count is - // beyond a certain limit. + r.callerApp.killLocked("Too many outgoing broadcasts in cached state", + ApplicationExitInfo.REASON_OTHER, + ApplicationExitInfo.SUBREASON_EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED, + true /* noisy */); + return; } queue.enqueueOutgoingBroadcast(r); mHistory.onBroadcastFrozenLocked(r); diff --git a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java index 66abb4238726..b8ef03f36c23 100644 --- a/services/core/java/com/android/server/am/BroadcastSkipPolicy.java +++ b/services/core/java/com/android/server/am/BroadcastSkipPolicy.java @@ -19,6 +19,7 @@ package com.android.server.am; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_PERMISSIONS_REVIEW; import static com.android.server.am.ActivityManagerService.checkComponentPermission; import static com.android.server.am.BroadcastQueue.TAG; +import static com.android.server.am.Flags.usePermissionManagerForBroadcastDeliveryCheck; import android.annotation.NonNull; import android.annotation.Nullable; @@ -27,6 +28,7 @@ import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.BroadcastOptions; import android.app.PendingIntent; +import android.content.AttributionSource; import android.content.ComponentName; import android.content.IIntentSender; import android.content.Intent; @@ -39,6 +41,7 @@ import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; import android.permission.IPermissionManager; +import android.permission.PermissionManager; import android.util.Slog; import com.android.internal.util.ArrayUtils; @@ -54,6 +57,9 @@ import java.util.Objects; public class BroadcastSkipPolicy { private final ActivityManagerService mService; + @Nullable + private PermissionManager mPermissionManager; + public BroadcastSkipPolicy(@NonNull ActivityManagerService service) { mService = Objects.requireNonNull(service); } @@ -283,14 +289,35 @@ public class BroadcastSkipPolicy { if (info.activityInfo.applicationInfo.uid != Process.SYSTEM_UID && r.requiredPermissions != null && r.requiredPermissions.length > 0) { + final AttributionSource attributionSource; + if (usePermissionManagerForBroadcastDeliveryCheck()) { + attributionSource = + new AttributionSource.Builder(info.activityInfo.applicationInfo.uid) + .setPackageName(info.activityInfo.packageName) + .build(); + } else { + attributionSource = null; + } for (int i = 0; i < r.requiredPermissions.length; i++) { String requiredPermission = r.requiredPermissions[i]; try { - perm = AppGlobals.getPackageManager(). - checkPermission(requiredPermission, - info.activityInfo.applicationInfo.packageName, - UserHandle - .getUserId(info.activityInfo.applicationInfo.uid)); + if (usePermissionManagerForBroadcastDeliveryCheck()) { + final PermissionManager permissionManager = getPermissionManager(); + if (permissionManager != null) { + perm = permissionManager.checkPermissionForDataDelivery( + requiredPermission, attributionSource, null /* message */); + } else { + // Assume permission denial if PermissionManager is not yet available. + perm = PackageManager.PERMISSION_DENIED; + } + } else { + perm = AppGlobals.getPackageManager() + .checkPermission( + requiredPermission, + info.activityInfo.applicationInfo.packageName, + UserHandle + .getUserId(info.activityInfo.applicationInfo.uid)); + } } catch (RemoteException e) { perm = PackageManager.PERMISSION_DENIED; } @@ -302,11 +329,13 @@ public class BroadcastSkipPolicy { + " due to sender " + r.callerPackage + " (uid " + r.callingUid + ")"; } - int appOp = AppOpsManager.permissionToOpCode(requiredPermission); - if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp) { - if (!noteOpForManifestReceiver(appOp, r, info, component)) { - return "Skipping delivery to " + info.activityInfo.packageName - + " due to required appop " + appOp; + if (!usePermissionManagerForBroadcastDeliveryCheck()) { + int appOp = AppOpsManager.permissionToOpCode(requiredPermission); + if (appOp != AppOpsManager.OP_NONE && appOp != r.appOp) { + if (!noteOpForManifestReceiver(appOp, r, info, component)) { + return "Skipping delivery to " + info.activityInfo.packageName + + " due to required appop " + appOp; + } } } } @@ -694,4 +723,11 @@ public class BroadcastSkipPolicy { return false; } + + private PermissionManager getPermissionManager() { + if (mPermissionManager == null) { + mPermissionManager = mService.mContext.getSystemService(PermissionManager.class); + } + return mPermissionManager; + } } diff --git a/services/core/java/com/android/server/am/CachedAppOptimizer.java b/services/core/java/com/android/server/am/CachedAppOptimizer.java index 0cf557588958..6e20f6cc877d 100644 --- a/services/core/java/com/android/server/am/CachedAppOptimizer.java +++ b/services/core/java/com/android/server/am/CachedAppOptimizer.java @@ -1414,8 +1414,13 @@ public final class CachedAppOptimizer { } @GuardedBy({"mAm", "mProcLock"}) + void forceFreezeAppAsyncLSP(ProcessRecord app) { + freezeAppAsyncInternalLSP(app, 0 /* delayMillis */, true /* force */); + } + + @GuardedBy({"mAm", "mProcLock"}) private void freezeAppAsyncLSP(ProcessRecord app, @UptimeMillisLong long delayMillis) { - freezeAppAsyncInternalLSP(app, delayMillis, false, false); + freezeAppAsyncInternalLSP(app, delayMillis, false /* force */); } @GuardedBy({"mAm", "mProcLock"}) @@ -1427,17 +1432,18 @@ public final class CachedAppOptimizer { // and remove this method. @GuardedBy({"mAm", "mProcLock"}) void freezeAppAsyncImmediateLSP(ProcessRecord app) { - freezeAppAsyncInternalLSP(app, 0, false, true); + freezeAppAsyncInternalLSP(app, 0 /* delayMillis */, false /* force */); } - // TODO: Update this method to be private and have the existing clients call different methods. - // This "internal" method should not be directly triggered by clients outside this class. @GuardedBy({"mAm", "mProcLock"}) - void freezeAppAsyncInternalLSP(ProcessRecord app, @UptimeMillisLong long delayMillis, - boolean force, boolean immediate) { + private void freezeAppAsyncInternalLSP(ProcessRecord app, @UptimeMillisLong long delayMillis, + boolean force) { final ProcessCachedOptimizerRecord opt = app.mOptRecord; if (opt.isPendingFreeze()) { - if (immediate) { + if (delayMillis == 0) { + // Caller is requesting to freeze the process without delay, so remove + // any already posted messages which would have been handled with a delay and + // post a new message without a delay. mFreezeHandler.removeMessages(SET_FROZEN_PROCESS_MSG, app); mFreezeHandler.sendMessage(mFreezeHandler.obtainMessage( SET_FROZEN_PROCESS_MSG, DO_FREEZE, 0, app)); diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig index 0209944f9fd0..fd847f11157f 100644 --- a/services/core/java/com/android/server/am/flags.aconfig +++ b/services/core/java/com/android/server/am/flags.aconfig @@ -86,3 +86,11 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + namespace: "backstage_power" + name: "use_permission_manager_for_broadcast_delivery_check" + description: "Use PermissionManager API for broadcast delivery permission checks." + bug: "315468967" + is_fixed_read_only: true +} diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index e8c05c6d9899..53c0f58d0067 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -73,6 +73,7 @@ import android.app.role.RoleManager; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothProfile; +import android.content.AttributionSource; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; @@ -6853,15 +6854,6 @@ public class AudioService extends IAudioService.Stub ringerMode = RINGER_MODE_SILENT; } } - } else if (mIsSingleVolume && (direction == AudioManager.ADJUST_TOGGLE_MUTE - || direction == AudioManager.ADJUST_MUTE)) { - if (mHasVibrator) { - ringerMode = RINGER_MODE_VIBRATE; - } else { - ringerMode = RINGER_MODE_SILENT; - } - // Setting the ringer mode will toggle mute - result &= ~FLAG_ADJUST_VOLUME; } break; case RINGER_MODE_VIBRATE: @@ -6870,11 +6862,8 @@ public class AudioService extends IAudioService.Stub "but no vibrator is present"); break; } - if ((direction == AudioManager.ADJUST_LOWER)) { - // This is the case we were muted with the volume turned up - if (mIsSingleVolume && oldIndex >= 2 * step && isMuted) { - ringerMode = RINGER_MODE_NORMAL; - } else if (mPrevVolDirection != AudioManager.ADJUST_LOWER) { + if (direction == AudioManager.ADJUST_LOWER) { + if (mPrevVolDirection != AudioManager.ADJUST_LOWER) { if (mVolumePolicy.volumeDownToEnterSilent) { final long diff = SystemClock.uptimeMillis() - mLoweredFromNormalToVibrateTime; @@ -6894,10 +6883,7 @@ public class AudioService extends IAudioService.Stub result &= ~FLAG_ADJUST_VOLUME; break; case RINGER_MODE_SILENT: - if (mIsSingleVolume && direction == AudioManager.ADJUST_LOWER && oldIndex >= 2 * step && isMuted) { - // This is the case we were muted with the volume turned up - ringerMode = RINGER_MODE_NORMAL; - } else if (direction == AudioManager.ADJUST_RAISE + if (direction == AudioManager.ADJUST_RAISE || direction == AudioManager.ADJUST_TOGGLE_MUTE || direction == AudioManager.ADJUST_UNMUTE) { if (!mVolumePolicy.volumeUpToExitSilent) { @@ -12207,7 +12193,9 @@ public class AudioService extends IAudioService.Stub //========================================================================================== public String registerAudioPolicy(AudioPolicyConfig policyConfig, IAudioPolicyCallback pcb, boolean hasFocusListener, boolean isFocusPolicy, boolean isTestFocusPolicy, - boolean isVolumeController, IMediaProjection projection) { + boolean isVolumeController, IMediaProjection projection, + AttributionSource attributionSource) { + Objects.requireNonNull(attributionSource); AudioSystem.setDynamicPolicyCallback(mDynPolicyCallback); if (!isPolicyRegisterAllowed(policyConfig, @@ -12228,7 +12216,8 @@ public class AudioService extends IAudioService.Stub } try { AudioPolicyProxy app = new AudioPolicyProxy(policyConfig, pcb, hasFocusListener, - isFocusPolicy, isTestFocusPolicy, isVolumeController, projection); + isFocusPolicy, isTestFocusPolicy, isVolumeController, projection, + attributionSource); pcb.asBinder().linkToDeath(app, 0/*flags*/); // logging after registration so we have the registration id @@ -13200,6 +13189,7 @@ public class AudioService extends IAudioService.Stub public class AudioPolicyProxy extends AudioPolicyConfig implements IBinder.DeathRecipient { private static final String TAG = "AudioPolicyProxy"; final IAudioPolicyCallback mPolicyCallback; + final AttributionSource mAttributionSource; final boolean mHasFocusListener; final boolean mIsVolumeController; final HashMap<Integer, AudioDeviceArray> mUidDeviceAffinities = @@ -13239,10 +13229,12 @@ public class AudioService extends IAudioService.Stub AudioPolicyProxy(AudioPolicyConfig config, IAudioPolicyCallback token, boolean hasFocusListener, boolean isFocusPolicy, boolean isTestFocusPolicy, - boolean isVolumeController, IMediaProjection projection) { + boolean isVolumeController, IMediaProjection projection, + AttributionSource attributionSource) { super(config); setRegistration(new String(config.hashCode() + ":ap:" + mAudioPolicyCounter++)); mPolicyCallback = token; + mAttributionSource = attributionSource; mHasFocusListener = hasFocusListener; mIsVolumeController = isVolumeController; mProjection = projection; @@ -13370,6 +13362,7 @@ public class AudioService extends IAudioService.Stub if (android.media.audiopolicy.Flags.audioMixOwnership()) { for (AudioMix mix : mixes) { setMixRegistration(mix); + mix.setVirtualDeviceId(mAttributionSource.getDeviceId()); } int result = mAudioSystem.registerPolicyMixes(mixes, true); @@ -13393,6 +13386,9 @@ public class AudioService extends IAudioService.Stub @AudioSystem.AudioSystemError int connectMixes() { final long identity = Binder.clearCallingIdentity(); try { + for (AudioMix mix : mMixes) { + mix.setVirtualDeviceId(mAttributionSource.getDeviceId()); + } return mAudioSystem.registerPolicyMixes(mMixes, true); } finally { Binder.restoreCallingIdentity(identity); @@ -13406,6 +13402,9 @@ public class AudioService extends IAudioService.Stub Objects.requireNonNull(mixesToUpdate); Objects.requireNonNull(updatedMixingRules); + for (AudioMix mix : mixesToUpdate) { + mix.setVirtualDeviceId(mAttributionSource.getDeviceId()); + } if (mixesToUpdate.length != updatedMixingRules.length) { Log.e(TAG, "Provided list of audio mixes to update and corresponding mixing rules " + "have mismatching length (mixesToUpdate.length = " + mixesToUpdate.length diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index b7ece2ea65b1..5905b7de5b6e 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -366,7 +366,6 @@ public class Vpn { private PendingIntent mStatusIntent; private volatile boolean mEnableTeardown = true; - private final INetworkManagementService mNms; private final INetd mNetd; @VisibleForTesting @GuardedBy("this") @@ -626,7 +625,6 @@ public class Vpn { mSubscriptionManager = mContext.getSystemService(SubscriptionManager.class); mDeps = deps; - mNms = netService; mNetd = netd; mUserId = userId; mLooper = looper; diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 05b1cb69235b..468b90259fc7 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -2604,6 +2604,19 @@ public class InputManagerService extends IInputManager.Stub mBatteryController.notifyStylusGestureStarted(deviceId, eventTime); } + // Native callback. + @SuppressWarnings("unused") + private int getPackageUid(String pkg) { + if (TextUtils.isEmpty(pkg)) { + return Process.INVALID_UID; + } + try { + return mContext.getPackageManager().getPackageUid(pkg, 0 /*flags*/); + } catch (PackageManager.NameNotFoundException e) { + return Process.INVALID_UID; + } + } + /** * Flatten a map into a string list, with value positioned directly next to the * key. diff --git a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java index 23fe5cca3d96..dbdac4184f28 100644 --- a/services/core/java/com/android/server/inputmethod/HandwritingModeController.java +++ b/services/core/java/com/android/server/inputmethod/HandwritingModeController.java @@ -16,6 +16,8 @@ package com.android.server.inputmethod; +import static com.android.text.flags.Flags.handwritingEndOfLineTap; + import android.Manifest; import android.annotation.AnyThread; import android.annotation.NonNull; @@ -30,6 +32,7 @@ import android.hardware.input.InputManagerGlobal; import android.os.Handler; import android.os.IBinder; import android.os.Looper; +import android.os.SystemClock; import android.text.TextUtils; import android.util.Slog; import android.view.BatchedInputEventReceiver; @@ -66,6 +69,7 @@ final class HandwritingModeController { // Use getHandwritingBufferSize() and not this value directly. private static final int LONG_EVENT_BUFFER_SIZE = EVENT_BUFFER_SIZE * 20; private static final long HANDWRITING_DELEGATION_IDLE_TIMEOUT_MS = 3000; + private static final long AFTER_STYLUS_UP_ALLOW_PERIOD_MS = 200L; private final Context mContext; // This must be the looper for the UiThread. @@ -78,6 +82,7 @@ final class HandwritingModeController { private InputEventReceiver mHandwritingEventReceiver; private Runnable mInkWindowInitRunnable; private boolean mRecordingGesture; + private boolean mRecordingGestureAfterStylusUp; private int mCurrentDisplayId; // when set, package names are used for handwriting delegation. private @Nullable String mDelegatePackageName; @@ -155,6 +160,15 @@ final class HandwritingModeController { } boolean isStylusGestureOngoing() { + if (mRecordingGestureAfterStylusUp && !mHandwritingBuffer.isEmpty()) { + // If it is less than AFTER_STYLUS_UP_ALLOW_PERIOD_MS after the stylus up event, return + // true so that handwriting can start. + MotionEvent lastEvent = mHandwritingBuffer.get(mHandwritingBuffer.size() - 1); + if (lastEvent.getActionMasked() == MotionEvent.ACTION_UP) { + return SystemClock.uptimeMillis() - lastEvent.getEventTime() + < AFTER_STYLUS_UP_ALLOW_PERIOD_MS; + } + } return mRecordingGesture; } @@ -277,7 +291,7 @@ final class HandwritingModeController { Slog.e(TAG, "Cannot start handwriting session: Invalid request id: " + requestId); return null; } - if (!mRecordingGesture || mHandwritingBuffer.isEmpty()) { + if (!isStylusGestureOngoing()) { Slog.e(TAG, "Cannot start handwriting session: No stylus gesture is being recorded."); return null; } @@ -300,6 +314,7 @@ final class HandwritingModeController { mHandwritingEventReceiver.dispose(); mHandwritingEventReceiver = null; mRecordingGesture = false; + mRecordingGestureAfterStylusUp = false; if (mHandwritingSurface.isIntercepting()) { throw new IllegalStateException( @@ -362,6 +377,7 @@ final class HandwritingModeController { clearPendingHandwritingDelegation(); } mRecordingGesture = false; + mRecordingGestureAfterStylusUp = false; } private boolean onInputEvent(InputEvent ev) { @@ -412,15 +428,20 @@ final class HandwritingModeController { if ((TextUtils.isEmpty(mDelegatePackageName) || mDelegationConnectionlessFlow) && (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL)) { mRecordingGesture = false; - mHandwritingBuffer.clear(); - return; + if (handwritingEndOfLineTap() && action == MotionEvent.ACTION_UP) { + mRecordingGestureAfterStylusUp = true; + } else { + mHandwritingBuffer.clear(); + return; + } } if (action == MotionEvent.ACTION_DOWN) { + clearBufferIfRecordingAfterStylusUp(); mRecordingGesture = true; } - if (!mRecordingGesture) { + if (!mRecordingGesture && !mRecordingGestureAfterStylusUp) { return; } @@ -430,12 +451,20 @@ final class HandwritingModeController { + " The rest of the gesture will not be recorded."); } mRecordingGesture = false; + clearBufferIfRecordingAfterStylusUp(); return; } mHandwritingBuffer.add(MotionEvent.obtain(event)); } + private void clearBufferIfRecordingAfterStylusUp() { + if (mRecordingGestureAfterStylusUp) { + mHandwritingBuffer.clear(); + mRecordingGestureAfterStylusUp = false; + } + } + static final class HandwritingSession { private final int mRequestId; private final InputChannel mHandwritingChannel; diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index d0a83a66dfba..cfd64c47718c 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -1248,7 +1248,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mService.publishLocalService(); IInputMethodManager.Stub service; if (Flags.useZeroJankProxy()) { - service = new ZeroJankProxy(mService.mHandler::post, mService); + service = + new ZeroJankProxy( + mService.mHandler::post, + mService, + () -> { + synchronized (ImfLock.class) { + return mService.isInputShown(); + } + }); } else { service = mService; } diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java index 396192e085e7..31ce63056864 100644 --- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java +++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java @@ -46,7 +46,6 @@ import android.os.IBinder; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; -import android.util.ExceptionUtils; import android.util.Slog; import android.view.WindowManager; import android.view.inputmethod.CursorAnchorInfo; @@ -77,6 +76,7 @@ import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; +import java.util.function.BooleanSupplier; /** * A proxy that processes all {@link IInputMethodManager} calls asynchronously. @@ -86,10 +86,12 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { private final IInputMethodManager mInner; private final Executor mExecutor; + private final BooleanSupplier mIsInputShown; - ZeroJankProxy(Executor executor, IInputMethodManager inner) { + ZeroJankProxy(Executor executor, IInputMethodManager inner, BooleanSupplier isInputShown) { mInner = inner; mExecutor = executor; + mIsInputShown = isInputShown; } private void offload(ThrowingRunnable r) { @@ -163,8 +165,19 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { int lastClickTooType, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) throws RemoteException { - offload(() -> mInner.showSoftInput(client, windowToken, statsToken, flags, lastClickTooType, - resultReceiver, reason)); + offload( + () -> { + if (!mInner.showSoftInput( + client, + windowToken, + statsToken, + flags, + lastClickTooType, + resultReceiver, + reason)) { + sendResultReceiverFailure(resultReceiver); + } + }); return true; } @@ -173,11 +186,24 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { @Nullable ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) throws RemoteException { - offload(() -> mInner.hideSoftInput(client, windowToken, statsToken, flags, resultReceiver, - reason)); + offload( + () -> { + if (!mInner.hideSoftInput( + client, windowToken, statsToken, flags, resultReceiver, reason)) { + sendResultReceiverFailure(resultReceiver); + } + }); return true; } + private void sendResultReceiverFailure(ResultReceiver resultReceiver) { + resultReceiver.send( + mIsInputShown.getAsBoolean() + ? InputMethodManager.RESULT_UNCHANGED_SHOWN + : InputMethodManager.RESULT_UNCHANGED_HIDDEN, + null); + } + @Override @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD) public void hideSoftInputFromServerForTest() throws RemoteException { @@ -415,14 +441,17 @@ public class ZeroJankProxy extends IInputMethodManager.Stub { private void sendOnStartInputResult( IInputMethodClient client, InputBindResult res, int startInputSeq) { - InputMethodManagerService service = (InputMethodManagerService) mInner; - final ClientState cs = service.getClientState(client); - if (cs != null && cs.mClient != null) { - cs.mClient.onStartInputResult(res, startInputSeq); - } else { - // client is unbound. - Slog.i(TAG, "Client that requested startInputOrWindowGainedFocus is no longer" - + " bound. InputBindResult: " + res + " for startInputSeq: " + startInputSeq); + synchronized (ImfLock.class) { + InputMethodManagerService service = (InputMethodManagerService) mInner; + final ClientState cs = service.getClientState(client); + if (cs != null && cs.mClient != null) { + cs.mClient.onStartInputResult(res, startInputSeq); + } else { + // client is unbound. + Slog.i(TAG, "Client that requested startInputOrWindowGainedFocus is no longer" + + " bound. InputBindResult: " + res + " for startInputSeq: " + + startInputSeq); + } } } } diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java index a9a82725223d..5b3934ea9b13 100644 --- a/services/core/java/com/android/server/media/MediaSessionRecord.java +++ b/services/core/java/com/android/server/media/MediaSessionRecord.java @@ -687,27 +687,20 @@ public class MediaSessionRecord extends MediaSessionRecordImpl implements IBinde private static String toVolumeControlTypeString( @VolumeProvider.ControlType int volumeControlType) { - switch (volumeControlType) { - case VOLUME_CONTROL_FIXED: - return "FIXED"; - case VOLUME_CONTROL_RELATIVE: - return "RELATIVE"; - case VOLUME_CONTROL_ABSOLUTE: - return "ABSOLUTE"; - default: - return TextUtils.formatSimple("unknown(%d)", volumeControlType); - } + return switch (volumeControlType) { + case VOLUME_CONTROL_FIXED -> "FIXED"; + case VOLUME_CONTROL_RELATIVE -> "RELATIVE"; + case VOLUME_CONTROL_ABSOLUTE -> "ABSOLUTE"; + default -> TextUtils.formatSimple("unknown(%d)", volumeControlType); + }; } private static String toVolumeTypeString(@PlaybackInfo.PlaybackType int volumeType) { - switch (volumeType) { - case PLAYBACK_TYPE_LOCAL: - return "LOCAL"; - case PLAYBACK_TYPE_REMOTE: - return "REMOTE"; - default: - return TextUtils.formatSimple("unknown(%d)", volumeType); - } + return switch (volumeType) { + case PLAYBACK_TYPE_LOCAL -> "LOCAL"; + case PLAYBACK_TYPE_REMOTE -> "REMOTE"; + default -> TextUtils.formatSimple("unknown(%d)", volumeType); + }; } @Override diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index 4f3cdbc52259..50ca984dcf57 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -310,6 +310,7 @@ public class PreferencesHelper implements RankingConfig { parser.getAttributeInt(null, ATT_VISIBILITY, DEFAULT_VISIBILITY), parser.getAttributeBoolean(null, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE), bubblePref); + r.bubblePreference = bubblePref; r.priority = parser.getAttributeInt(null, ATT_PRIORITY, DEFAULT_PRIORITY); r.visibility = parser.getAttributeInt(null, ATT_VISIBILITY, DEFAULT_VISIBILITY); r.showBadge = parser.getAttributeBoolean(null, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE); @@ -676,7 +677,7 @@ public class PreferencesHelper implements RankingConfig { * @param bubblePreference whether bubbles are allowed. */ public void setBubblesAllowed(String pkg, int uid, int bubblePreference) { - boolean changed = false; + boolean changed; synchronized (mPackagePreferences) { PackagePreferences p = getOrCreatePackagePreferencesLocked(pkg, uid); changed = p.bubblePreference != bubblePreference; diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java index 37023e14eb41..953300ac43a6 100644 --- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java +++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java @@ -163,7 +163,7 @@ public class OnDeviceIntelligenceManagerService extends SystemService { } @Override - public void getVersion(RemoteCallback remoteCallback) throws RemoteException { + public void getVersion(RemoteCallback remoteCallback) { Slog.i(TAG, "OnDeviceIntelligenceManagerInternal getVersion"); Objects.requireNonNull(remoteCallback); mContext.enforceCallingOrSelfPermission( @@ -244,7 +244,7 @@ public class OnDeviceIntelligenceManagerService extends SystemService { @Override public void requestFeatureDownload(Feature feature, - ICancellationSignal cancellationSignal, + AndroidFuture cancellationSignalFuture, IDownloadCallback downloadCallback) throws RemoteException { Slog.i(TAG, "OnDeviceIntelligenceManagerInternal requestFeatureDownload"); Objects.requireNonNull(feature); @@ -261,16 +261,17 @@ public class OnDeviceIntelligenceManagerService extends SystemService { ensureRemoteIntelligenceServiceInitialized(); mRemoteOnDeviceIntelligenceService.run( service -> service.requestFeatureDownload(Binder.getCallingUid(), feature, - cancellationSignal, + cancellationSignalFuture, downloadCallback)); } @Override public void requestTokenInfo(Feature feature, - Bundle request, ICancellationSignal cancellationSignal, + Bundle request, + AndroidFuture cancellationSignalFuture, ITokenInfoCallback tokenInfoCallback) throws RemoteException { - Slog.i(TAG, "OnDeviceIntelligenceManagerInternal prepareFeatureProcessing"); + Slog.i(TAG, "OnDeviceIntelligenceManagerInternal requestTokenInfo"); Objects.requireNonNull(feature); Objects.requireNonNull(request); Objects.requireNonNull(tokenInfoCallback); @@ -285,10 +286,11 @@ public class OnDeviceIntelligenceManagerService extends SystemService { PersistableBundle.EMPTY); } ensureRemoteInferenceServiceInitialized(); + mRemoteInferenceService.run( service -> service.requestTokenInfo(Binder.getCallingUid(), feature, request, - cancellationSignal, + cancellationSignalFuture, tokenInfoCallback)); } @@ -296,8 +298,8 @@ public class OnDeviceIntelligenceManagerService extends SystemService { public void processRequest(Feature feature, Bundle request, int requestType, - ICancellationSignal cancellationSignal, - IProcessingSignal processingSignal, + AndroidFuture cancellationSignalFuture, + AndroidFuture processingSignalFuture, IResponseCallback responseCallback) throws RemoteException { Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequest"); @@ -316,7 +318,7 @@ public class OnDeviceIntelligenceManagerService extends SystemService { mRemoteInferenceService.run( service -> service.processRequest(Binder.getCallingUid(), feature, request, requestType, - cancellationSignal, processingSignal, + cancellationSignalFuture, processingSignalFuture, responseCallback)); } @@ -324,8 +326,8 @@ public class OnDeviceIntelligenceManagerService extends SystemService { public void processRequestStreaming(Feature feature, Bundle request, int requestType, - ICancellationSignal cancellationSignal, - IProcessingSignal processingSignal, + AndroidFuture cancellationSignalFuture, + AndroidFuture processingSignalFuture, IStreamingResponseCallback streamingCallback) throws RemoteException { Slog.i(TAG, "OnDeviceIntelligenceManagerInternal processRequestStreaming"); Objects.requireNonNull(feature); @@ -343,7 +345,7 @@ public class OnDeviceIntelligenceManagerService extends SystemService { mRemoteInferenceService.run( service -> service.processRequestStreaming(Binder.getCallingUid(), feature, request, requestType, - cancellationSignal, processingSignal, + cancellationSignalFuture, processingSignalFuture, streamingCallback)); } @@ -356,7 +358,7 @@ public class OnDeviceIntelligenceManagerService extends SystemService { }; } - private void ensureRemoteIntelligenceServiceInitialized() throws RemoteException { + private void ensureRemoteIntelligenceServiceInitialized() { synchronized (mLock) { if (mRemoteOnDeviceIntelligenceService == null) { String serviceName = getServiceNames()[0]; @@ -388,25 +390,15 @@ public class OnDeviceIntelligenceManagerService extends SystemService { public void updateProcessingState( Bundle processingState, IProcessingUpdateStatusCallback callback) { - try { - ensureRemoteInferenceServiceInitialized(); - mRemoteInferenceService.run( - service -> service.updateProcessingState( - processingState, callback)); - } catch (RemoteException unused) { - try { - callback.onFailure( - OnDeviceIntelligenceException.PROCESSING_UPDATE_STATUS_CONNECTION_FAILED, - "Received failure invoking the remote processing service."); - } catch (RemoteException ex) { - Slog.w(TAG, "Failed to send failure status.", ex); - } - } + ensureRemoteInferenceServiceInitialized(); + mRemoteInferenceService.run( + service -> service.updateProcessingState( + processingState, callback)); } }; } - private void ensureRemoteInferenceServiceInitialized() throws RemoteException { + private void ensureRemoteInferenceServiceInitialized() { synchronized (mLock) { if (mRemoteInferenceService == null) { String serviceName = getServiceNames()[1]; @@ -457,34 +449,38 @@ public class OnDeviceIntelligenceManagerService extends SystemService { }; } - private static void validateServiceElevated(String serviceName, boolean checkIsolated) - throws RemoteException { - if (TextUtils.isEmpty(serviceName)) { - throw new IllegalArgumentException("Received null/empty service name : " + serviceName); - } - ComponentName serviceComponent = ComponentName.unflattenFromString( - serviceName); - ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo( - serviceComponent, - PackageManager.MATCH_DIRECT_BOOT_AWARE - | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, 0); - if (serviceInfo != null) { - if (!checkIsolated) { - checkServiceRequiresPermission(serviceInfo, - Manifest.permission.BIND_ON_DEVICE_INTELLIGENCE_SERVICE); - return; + private void validateServiceElevated(String serviceName, boolean checkIsolated) { + try { + if (TextUtils.isEmpty(serviceName)) { + throw new IllegalStateException( + "Remote service is not configured to complete the request"); } + ComponentName serviceComponent = ComponentName.unflattenFromString( + serviceName); + ServiceInfo serviceInfo = AppGlobals.getPackageManager().getServiceInfo( + serviceComponent, + PackageManager.MATCH_DIRECT_BOOT_AWARE + | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, 0); + if (serviceInfo != null) { + if (!checkIsolated) { + checkServiceRequiresPermission(serviceInfo, + Manifest.permission.BIND_ON_DEVICE_INTELLIGENCE_SERVICE); + return; + } - checkServiceRequiresPermission(serviceInfo, - Manifest.permission.BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE); - if (!isIsolatedService(serviceInfo)) { - throw new SecurityException( - "Call required an isolated service, but the configured service: " - + serviceName + ", is not isolated"); + checkServiceRequiresPermission(serviceInfo, + Manifest.permission.BIND_ON_DEVICE_SANDBOXED_INFERENCE_SERVICE); + if (!isIsolatedService(serviceInfo)) { + throw new SecurityException( + "Call required an isolated service, but the configured service: " + + serviceName + ", is not isolated"); + } + } else { + throw new IllegalStateException( + "Remote service is not configured to complete the request."); } - } else { - throw new RuntimeException( - "Could not find service info for serviceName: " + serviceName); + } catch (RemoteException e) { + throw new IllegalStateException("Could not fetch service info for remote services", e); } } @@ -542,7 +538,8 @@ public class OnDeviceIntelligenceManagerService extends SystemService { Manifest.permission.USE_ON_DEVICE_INTELLIGENCE, TAG); synchronized (mLock) { mTemporaryServiceNames = componentNames; - + mRemoteOnDeviceIntelligenceService = null; + mRemoteInferenceService = null; if (mTemporaryHandler == null) { mTemporaryHandler = new Handler(Looper.getMainLooper(), null, true) { @Override diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 9480c8e72402..2005b17e82a6 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -137,6 +137,7 @@ import com.android.internal.util.CollectionUtils; import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.Preconditions; import com.android.modules.utils.TypedXmlSerializer; +import com.android.server.ondeviceintelligence.OnDeviceIntelligenceManagerInternal; import com.android.server.pm.dex.DexManager; import com.android.server.pm.dex.PackageDexUsage; import com.android.server.pm.parsing.PackageInfoUtils; @@ -4353,9 +4354,8 @@ public class ComputerEngine implements Computer { if (Process.isSdkSandboxUid(uid)) { uid = getBaseSdkSandboxUid(); } - if (Process.isIsolatedUid(uid) - && mPermissionManager.getHotwordDetectionServiceProvider() != null - && uid == mPermissionManager.getHotwordDetectionServiceProvider().getUid()) { + final int callingUserId = UserHandle.getUserId(callingUid); + if (isKnownIsolatedComputeApp(uid, callingUserId)) { try { uid = getIsolatedOwner(uid); } catch (IllegalStateException e) { @@ -4363,7 +4363,6 @@ public class ComputerEngine implements Computer { Slog.wtf(TAG, "Expected isolated uid " + uid + " to have an owner", e); } } - final int callingUserId = UserHandle.getUserId(callingUid); final int appId = UserHandle.getAppId(uid); final Object obj = mSettings.getSettingBase(appId); if (obj instanceof SharedUserSetting) { @@ -4399,9 +4398,7 @@ public class ComputerEngine implements Computer { if (Process.isSdkSandboxUid(uid)) { uid = getBaseSdkSandboxUid(); } - if (Process.isIsolatedUid(uid) - && mPermissionManager.getHotwordDetectionServiceProvider() != null - && uid == mPermissionManager.getHotwordDetectionServiceProvider().getUid()) { + if (isKnownIsolatedComputeApp(uid, callingUserId)) { try { uid = getIsolatedOwner(uid); } catch (IllegalStateException e) { @@ -5802,6 +5799,43 @@ public class ComputerEngine implements Computer { return getPackage(mService.getSdkSandboxPackageName()).getUid(); } + + private boolean isKnownIsolatedComputeApp(int uid, int callingUserId) { + if (!Process.isIsolatedUid(uid)) { + return false; + } + final boolean isHotword = + mPermissionManager.getHotwordDetectionServiceProvider() != null + && uid + == mPermissionManager.getHotwordDetectionServiceProvider().getUid(); + if (isHotword) { + return true; + } + OnDeviceIntelligenceManagerInternal onDeviceIntelligenceManagerInternal = + mInjector.getLocalService(OnDeviceIntelligenceManagerInternal.class); + if (onDeviceIntelligenceManagerInternal == null) { + return false; + } + + String onDeviceIntelligencePackage = + onDeviceIntelligenceManagerInternal.getRemoteServicePackageName(); + if (onDeviceIntelligencePackage == null) { + return false; + } + + try { + if (getIsolatedOwner(uid) == getPackageUid(onDeviceIntelligencePackage, 0, + callingUserId)) { + return true; + } + } catch (IllegalStateException e) { + // If the owner uid doesn't exist, just use the current uid + Slog.wtf(TAG, "Expected isolated uid " + uid + " to have an owner", e); + } + + return false; + } + @Nullable @Override public SharedUserApi getSharedUser(int sharedUserAppId) { diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index c6bb99eed7ee..20b669b96609 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -18,12 +18,12 @@ package com.android.server.pm; import static android.Manifest.permission.READ_FRAME_BUFFER; import static android.app.ActivityOptions.KEY_SPLASH_SCREEN_THEME; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.MODE_IGNORED; import static android.app.AppOpsManager.OP_ARCHIVE_ICON_OVERLAY; import static android.app.AppOpsManager.OP_UNARCHIVAL_CONFIRMATION; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; import static android.app.PendingIntent.FLAG_IMMUTABLE; import static android.app.PendingIntent.FLAG_MUTABLE; import static android.app.PendingIntent.FLAG_UPDATE_CURRENT; @@ -555,12 +555,6 @@ public class LauncherAppsService extends SystemService { return false; } - if (!mRoleManager - .getRoleHoldersAsUser( - RoleManager.ROLE_HOME, UserHandle.getUserHandleForUid(callingUid)) - .contains(callingPackage.getPackageName())) { - return false; - } if (mContext.checkPermission( Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL, callingPid, @@ -569,6 +563,13 @@ public class LauncherAppsService extends SystemService { return true; } + if (!mRoleManager + .getRoleHoldersAsUser( + RoleManager.ROLE_HOME, UserHandle.getUserHandleForUid(callingUid)) + .contains(callingPackage.getPackageName())) { + return false; + } + // TODO(b/321988638): add option to disable with a flag return mContext.checkPermission( android.Manifest.permission.ACCESS_HIDDEN_PROFILES, diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 12a589264c28..76bf8fd45a43 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -530,6 +530,14 @@ public class PhoneWindowManager implements WindowManagerPolicy { // TODO(b/178103325): Track sleep/requested sleep for every display. volatile boolean mRequestedOrSleepingDefaultDisplay; + /** + * This is used to check whether to invoke {@link #updateScreenOffSleepToken} when screen is + * turned off. E.g. if it is false when screen is turned off and the display is swapping, it + * is expected that the screen will be on in a short time. Then it is unnecessary to acquire + * screen-off-sleep-token, so it can avoid intermediate visibility or lifecycle changes. + */ + volatile boolean mIsGoingToSleepDefaultDisplay; + volatile boolean mRecentsVisible; volatile boolean mNavBarVirtualKeyHapticFeedbackEnabled = true; volatile boolean mPictureInPictureVisible; @@ -3500,7 +3508,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (firstDown && event.isMetaPressed() && event.isCtrlPressed()) { StatusBarManagerInternal statusbar = getStatusBarManagerInternal(); if (statusbar != null) { - statusbar.enterDesktop(getTargetDisplayIdForKeyEvent(event)); + statusbar.moveFocusedTaskToDesktop(getTargetDisplayIdForKeyEvent(event)); logKeyboardSystemsEvent(event, KeyboardLogEvent.DESKTOP_MODE); return true; } @@ -5470,6 +5478,15 @@ public class PhoneWindowManager implements WindowManagerPolicy { } mRequestedOrSleepingDefaultDisplay = true; + mIsGoingToSleepDefaultDisplay = true; + + // In case startedGoingToSleep is called after screenTurnedOff (the source caller is in + // order but the methods run on different threads) and updateScreenOffSleepToken was + // skipped. Then acquire sleep token if screen was off. + if (!mDefaultDisplayPolicy.isScreenOnFully() && !mDefaultDisplayPolicy.isScreenOnEarly() + && com.android.window.flags.Flags.skipSleepingWhenSwitchingDisplay()) { + updateScreenOffSleepToken(true /* acquire */, false /* isSwappingDisplay */); + } if (mKeyguardDelegate != null) { mKeyguardDelegate.onStartedGoingToSleep(pmSleepReason); @@ -5493,6 +5510,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { MetricsLogger.histogram(mContext, "screen_timeout", mLockScreenTimeout / 1000); mRequestedOrSleepingDefaultDisplay = false; + mIsGoingToSleepDefaultDisplay = false; mDefaultDisplayPolicy.setAwake(false); // We must get this work done here because the power manager will drop @@ -5528,7 +5546,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { } EventLogTags.writeScreenToggled(1); - + mIsGoingToSleepDefaultDisplay = false; mDefaultDisplayPolicy.setAwake(true); // Since goToSleep performs these functions synchronously, we must @@ -5630,7 +5648,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { if (DEBUG_WAKEUP) Slog.i(TAG, "Display" + displayId + " turned off..."); if (displayId == DEFAULT_DISPLAY) { - updateScreenOffSleepToken(true, isSwappingDisplay); + if (!isSwappingDisplay || mIsGoingToSleepDefaultDisplay + || !com.android.window.flags.Flags.skipSleepingWhenSwitchingDisplay()) { + updateScreenOffSleepToken(true /* acquire */, isSwappingDisplay); + } mRequestedOrSleepingDefaultDisplay = false; mDefaultDisplayPolicy.screenTurnedOff(); synchronized (mLock) { diff --git a/services/core/java/com/android/server/power/hint/Android.bp b/services/core/java/com/android/server/power/hint/Android.bp new file mode 100644 index 000000000000..8a98de673c3d --- /dev/null +++ b/services/core/java/com/android/server/power/hint/Android.bp @@ -0,0 +1,12 @@ +aconfig_declarations { + name: "power_hint_flags", + package: "com.android.server.power.hint", + srcs: [ + "flags.aconfig", + ], +} + +java_aconfig_library { + name: "power_hint_flags_lib", + aconfig_declarations: "power_hint_flags", +} 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 aa1a41eee220..3f1b1c1e99df 100644 --- a/services/core/java/com/android/server/power/hint/HintManagerService.java +++ b/services/core/java/com/android/server/power/hint/HintManagerService.java @@ -17,6 +17,7 @@ package com.android.server.power.hint; import static com.android.internal.util.ConcurrentUtils.DIRECT_EXECUTOR; +import static com.android.server.power.hint.Flags.powerhintThreadCleanup; import android.annotation.NonNull; import android.app.ActivityManager; @@ -26,9 +27,12 @@ import android.app.UidObserver; import android.content.Context; import android.hardware.power.WorkDuration; import android.os.Binder; +import android.os.Handler; import android.os.IBinder; import android.os.IHintManager; import android.os.IHintSession; +import android.os.Looper; +import android.os.Message; import android.os.PerformanceHintManager; import android.os.Process; import android.os.RemoteException; @@ -36,6 +40,8 @@ import android.os.SystemProperties; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.IntArray; +import android.util.Slog; import android.util.SparseIntArray; import android.util.StatsEvent; @@ -46,20 +52,31 @@ import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.Preconditions; import com.android.server.FgThread; import com.android.server.LocalServices; +import com.android.server.ServiceThread; import com.android.server.SystemService; import com.android.server.utils.Slogf; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; /** An hint service implementation that runs in System Server process. */ public final class HintManagerService extends SystemService { private static final String TAG = "HintManagerService"; private static final boolean DEBUG = false; + + private static final int EVENT_CLEAN_UP_UID = 3; + @VisibleForTesting static final int CLEAN_UP_UID_DELAY_MILLIS = 1000; + + @VisibleForTesting final long mHintSessionPreferredRate; // Multi-level map storing all active AppHintSessions. @@ -73,9 +90,15 @@ public final class HintManagerService extends SystemService { /** Lock to protect HAL handles and listen list. */ private final Object mLock = new Object(); + @GuardedBy("mNonIsolatedTidsLock") + private final Map<Integer, Set<Long>> mNonIsolatedTids; + + private final Object mNonIsolatedTidsLock = new Object(); + @VisibleForTesting final MyUidObserver mUidObserver; private final NativeWrapper mNativeWrapper; + private final CleanUpHandler mCleanUpHandler; private final ActivityManagerInternal mAmInternal; @@ -94,6 +117,13 @@ public final class HintManagerService extends SystemService { HintManagerService(Context context, Injector injector) { super(context); mContext = context; + if (powerhintThreadCleanup()) { + mCleanUpHandler = new CleanUpHandler(createCleanUpThread().getLooper()); + mNonIsolatedTids = new HashMap<>(); + } else { + mCleanUpHandler = null; + mNonIsolatedTids = null; + } mActiveSessions = new ArrayMap<>(); mNativeWrapper = injector.createNativeWrapper(); mNativeWrapper.halInit(); @@ -103,6 +133,13 @@ public final class HintManagerService extends SystemService { LocalServices.getService(ActivityManagerInternal.class)); } + private ServiceThread createCleanUpThread() { + final ServiceThread handlerThread = new ServiceThread(TAG, + Process.THREAD_PRIORITY_LOWEST, true /*allowIo*/); + handlerThread.start(); + return handlerThread; + } + @VisibleForTesting static class Injector { NativeWrapper createNativeWrapper() { @@ -306,7 +343,18 @@ public final class HintManagerService extends SystemService { public void onUidStateChanged(int uid, int procState, long procStateSeq, int capability) { FgThread.getHandler().post(() -> { synchronized (mCacheLock) { - mProcStatesCache.put(uid, procState); + if (powerhintThreadCleanup()) { + final boolean before = isUidForeground(uid); + mProcStatesCache.put(uid, procState); + final boolean after = isUidForeground(uid); + if (before != after) { + final Message msg = mCleanUpHandler.obtainMessage(EVENT_CLEAN_UP_UID, + uid); + mCleanUpHandler.sendMessageDelayed(msg, CLEAN_UP_UID_DELAY_MILLIS); + } + } else { + mProcStatesCache.put(uid, procState); + } } boolean shouldAllowUpdate = isUidForeground(uid); synchronized (mLock) { @@ -314,9 +362,10 @@ public final class HintManagerService extends SystemService { if (tokenMap == null) { return; } - for (ArraySet<AppHintSession> sessionSet : tokenMap.values()) { - for (AppHintSession s : sessionSet) { - s.onProcStateChanged(shouldAllowUpdate); + for (int i = tokenMap.size() - 1; i >= 0; i--) { + final ArraySet<AppHintSession> sessionSet = tokenMap.valueAt(i); + for (int j = sessionSet.size() - 1; j >= 0; j--) { + sessionSet.valueAt(j).onProcStateChanged(shouldAllowUpdate); } } } @@ -324,52 +373,237 @@ public final class HintManagerService extends SystemService { } } + final class CleanUpHandler extends Handler { + // status of processed tid used for caching + private static final int TID_NOT_CHECKED = 0; + private static final int TID_PASSED_CHECK = 1; + private static final int TID_EXITED = 2; + + CleanUpHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + if (msg.what == EVENT_CLEAN_UP_UID) { + if (hasEqualMessages(msg.what, msg.obj)) { + removeEqualMessages(msg.what, msg.obj); + final Message newMsg = obtainMessage(msg.what, msg.obj); + sendMessageDelayed(newMsg, CLEAN_UP_UID_DELAY_MILLIS); + return; + } + final int uid = (int) msg.obj; + boolean isForeground = mUidObserver.isUidForeground(uid); + // store all sessions in a list and release the global lock + // we don't need to worry about stale data or racing as the session is synchronized + // itself and will perform its own closed status check in setThreads call + final List<AppHintSession> sessions; + synchronized (mLock) { + final ArrayMap<IBinder, ArraySet<AppHintSession>> tokenMap = + mActiveSessions.get(uid); + if (tokenMap == null || tokenMap.isEmpty()) { + return; + } + sessions = new ArrayList<>(tokenMap.size()); + for (int i = tokenMap.size() - 1; i >= 0; i--) { + final ArraySet<AppHintSession> set = tokenMap.valueAt(i); + for (int j = set.size() - 1; j >= 0; j--) { + sessions.add(set.valueAt(j)); + } + } + } + final long[] durationList = new long[sessions.size()]; + final int[] invalidTidCntList = new int[sessions.size()]; + final SparseIntArray checkedTids = new SparseIntArray(); + int[] totalTidCnt = new int[1]; + for (int i = sessions.size() - 1; i >= 0; i--) { + final AppHintSession session = sessions.get(i); + final long start = System.nanoTime(); + try { + final int invalidCnt = cleanUpSession(session, checkedTids, totalTidCnt); + final long elapsed = System.nanoTime() - start; + invalidTidCntList[i] = invalidCnt; + durationList[i] = elapsed; + } catch (Exception e) { + Slog.e(TAG, "Failed to clean up session " + session.mHalSessionPtr + + " for UID " + session.mUid); + } + } + logCleanUpMetrics(uid, invalidTidCntList, durationList, sessions.size(), + totalTidCnt[0], isForeground); + } + } + + private void logCleanUpMetrics(int uid, int[] count, long[] durationNsList, int sessionCnt, + int totalTidCnt, boolean isForeground) { + int maxInvalidTidCnt = Integer.MIN_VALUE; + int totalInvalidTidCnt = 0; + for (int i = 0; i < count.length; i++) { + totalInvalidTidCnt += count[i]; + maxInvalidTidCnt = Math.max(maxInvalidTidCnt, count[i]); + } + if (DEBUG || totalInvalidTidCnt > 0) { + Arrays.sort(durationNsList); + long totalDurationNs = 0; + for (int i = 0; i < durationNsList.length; i++) { + totalDurationNs += durationNsList[i]; + } + int totalDurationUs = (int) TimeUnit.NANOSECONDS.toMicros(totalDurationNs); + int maxDurationUs = (int) TimeUnit.NANOSECONDS.toMicros( + durationNsList[durationNsList.length - 1]); + int minDurationUs = (int) TimeUnit.NANOSECONDS.toMicros(durationNsList[0]); + int avgDurationUs = (int) TimeUnit.NANOSECONDS.toMicros( + totalDurationNs / durationNsList.length); + int th90DurationUs = (int) TimeUnit.NANOSECONDS.toMicros( + durationNsList[(int) (durationNsList.length * 0.9)]); + Slog.d(TAG, + "Invalid tid found for UID" + uid + " in " + totalDurationUs + "us:\n\t" + + "count(" + + " session: " + sessionCnt + + " totalTid: " + totalTidCnt + + " maxInvalidTid: " + maxInvalidTidCnt + + " totalInvalidTid: " + totalInvalidTidCnt + ")\n\t" + + "time per session(" + + " min: " + minDurationUs + "us" + + " max: " + maxDurationUs + "us" + + " avg: " + avgDurationUs + "us" + + " 90%: " + th90DurationUs + "us" + ")\n\t" + + "isForeground: " + isForeground); + } + } + + // This will check if each TID currently linked to the session still exists. If it's + // previously registered as not an isolated process, then it will run tkill(pid, tid, 0) to + // verify that it's still running under the same pid. Otherwise, it will run + // kill(tid, 0) to only check if it exists. The result will be cached in checkedTids + // map with tid as the key and checked status as value. + public int cleanUpSession(AppHintSession session, SparseIntArray checkedTids, int[] total) { + if (session.isClosed()) { + return 0; + } + final int pid = session.mPid; + final int[] tids = session.getTidsInternal(); + if (total != null && total.length == 1) { + total[0] += tids.length; + } + final IntArray filtered = new IntArray(tids.length); + for (int i = 0; i < tids.length; i++) { + int tid = tids[i]; + if (checkedTids.get(tid, 0) != TID_NOT_CHECKED) { + if (checkedTids.get(tid) == TID_PASSED_CHECK) { + filtered.add(tid); + } + continue; + } + // if it was registered as a non-isolated then we perform more restricted check + final boolean isNotIsolated; + synchronized (mNonIsolatedTidsLock) { + isNotIsolated = mNonIsolatedTids.containsKey(tid); + } + try { + if (isNotIsolated) { + Process.checkTid(pid, tid); + } else { + Process.checkPid(tid); + } + checkedTids.put(tid, TID_PASSED_CHECK); + filtered.add(tid); + } catch (NoSuchElementException e) { + checkedTids.put(tid, TID_EXITED); + } catch (Exception e) { + Slog.w(TAG, "Unexpected exception when checking TID " + tid + " under PID " + + pid + "(isolated: " + !isNotIsolated + ")", e); + // if anything unexpected happens then we keep it, but don't store it as checked + filtered.add(tid); + } + } + final int diff = tids.length - filtered.size(); + if (diff > 0) { + synchronized (session) { + // in case thread list is updated during the cleanup then we skip updating + // the session but just return the number for reporting purpose + final int[] newTids = session.getTidsInternal(); + if (newTids.length != tids.length) { + Slog.d(TAG, "Skipped cleaning up the session as new tids are added"); + return diff; + } + Arrays.sort(newTids); + Arrays.sort(tids); + if (!Arrays.equals(newTids, tids)) { + Slog.d(TAG, "Skipped cleaning up the session as new tids are updated"); + return diff; + } + Slog.d(TAG, "Cleaned up " + diff + " invalid tids for session " + + session.mHalSessionPtr + " with UID " + session.mUid + "\n\t" + + "before: " + Arrays.toString(tids) + "\n\t" + + "after: " + filtered); + final int[] filteredTids = filtered.toArray(); + if (filteredTids.length == 0) { + session.mShouldForcePause = true; + if (session.mUpdateAllowed) { + session.pause(); + } + } else { + session.setThreadsInternal(filteredTids, false); + } + } + } + return diff; + } + } + @VisibleForTesting IHintManager.Stub getBinderServiceInstance() { return mService; } // returns the first invalid tid or null if not found - private Integer checkTidValid(int uid, int tgid, int [] tids) { + private Integer checkTidValid(int uid, int tgid, int [] tids, IntArray nonIsolated) { // Make sure all tids belongs to the same UID (including isolated UID), // tids can belong to different application processes. List<Integer> isolatedPids = null; - for (int threadId : tids) { + for (int i = 0; i < tids.length; i++) { + int tid = tids[i]; final String[] procStatusKeys = new String[] { "Uid:", "Tgid:" }; long[] output = new long[procStatusKeys.length]; - Process.readProcLines("/proc/" + threadId + "/status", procStatusKeys, output); + Process.readProcLines("/proc/" + tid + "/status", procStatusKeys, output); int uidOfThreadId = (int) output[0]; int pidOfThreadId = (int) output[1]; - // use PID check for isolated processes, use UID check for non-isolated processes. - if (pidOfThreadId == tgid || uidOfThreadId == uid) { + // use PID check for non-isolated processes + if (nonIsolated != null && pidOfThreadId == tgid) { + nonIsolated.add(tid); + continue; + } + // use UID check for isolated processes. + if (uidOfThreadId == uid) { continue; } // 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) { - return threadId; + return tid; } isolatedPids = mAmInternal.getIsolatedProcesses(uid); if (isolatedPids == null) { - return threadId; + return tid; } } if (isolatedPids.contains(pidOfThreadId)) { continue; } - return threadId; + return tid; } return null; } private String formatTidCheckErrMsg(int callingUid, int[] tids, Integer invalidTid) { return "Tid" + invalidTid + " from list " + Arrays.toString(tids) - + " doesn't belong to the calling application" + callingUid; + + " doesn't belong to the calling application " + callingUid; } @VisibleForTesting @@ -387,7 +621,10 @@ public final class HintManagerService extends SystemService { final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid()); final long identity = Binder.clearCallingIdentity(); try { - final Integer invalidTid = checkTidValid(callingUid, callingTgid, tids); + final IntArray nonIsolated = powerhintThreadCleanup() ? new IntArray(tids.length) + : null; + final Integer invalidTid = checkTidValid(callingUid, callingTgid, tids, + nonIsolated); if (invalidTid != null) { final String errMsg = formatTidCheckErrMsg(callingUid, tids, invalidTid); Slogf.w(TAG, errMsg); @@ -396,6 +633,14 @@ public final class HintManagerService extends SystemService { long halSessionPtr = mNativeWrapper.halCreateHintSession(callingTgid, callingUid, tids, durationNanos); + if (powerhintThreadCleanup()) { + synchronized (mNonIsolatedTidsLock) { + for (int i = nonIsolated.size() - 1; i >= 0; i--) { + mNonIsolatedTids.putIfAbsent(nonIsolated.get(i), new ArraySet<>()); + mNonIsolatedTids.get(nonIsolated.get(i)).add(halSessionPtr); + } + } + } if (halSessionPtr == 0) { return null; } @@ -482,6 +727,7 @@ public final class HintManagerService extends SystemService { protected boolean mUpdateAllowed; protected int[] mNewThreadIds; protected boolean mPowerEfficient; + protected boolean mShouldForcePause; private enum SessionModes { POWER_EFFICIENCY, @@ -498,6 +744,7 @@ public final class HintManagerService extends SystemService { mTargetDurationNanos = durationNanos; mUpdateAllowed = true; mPowerEfficient = false; + mShouldForcePause = false; final boolean allowed = mUidObserver.isUidForeground(mUid); updateHintAllowed(allowed); try { @@ -511,7 +758,7 @@ public final class HintManagerService extends SystemService { @VisibleForTesting boolean updateHintAllowed(boolean allowed) { synchronized (this) { - if (allowed && !mUpdateAllowed) resume(); + if (allowed && !mUpdateAllowed && !mShouldForcePause) resume(); if (!allowed && mUpdateAllowed) pause(); mUpdateAllowed = allowed; return mUpdateAllowed; @@ -521,7 +768,7 @@ public final class HintManagerService extends SystemService { @Override public void updateTargetWorkDuration(long targetDurationNanos) { synchronized (this) { - if (mHalSessionPtr == 0 || !mUpdateAllowed) { + if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) { return; } Preconditions.checkArgument(targetDurationNanos > 0, "Expected" @@ -534,7 +781,7 @@ public final class HintManagerService extends SystemService { @Override public void reportActualWorkDuration(long[] actualDurationNanos, long[] timeStampNanos) { synchronized (this) { - if (mHalSessionPtr == 0 || !mUpdateAllowed) { + if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) { return; } Preconditions.checkArgument(actualDurationNanos.length != 0, "the count" @@ -581,12 +828,25 @@ public final class HintManagerService extends SystemService { if (sessionSet.isEmpty()) tokenMap.remove(mToken); if (tokenMap.isEmpty()) mActiveSessions.remove(mUid); } + if (powerhintThreadCleanup()) { + synchronized (mNonIsolatedTidsLock) { + final int[] tids = getTidsInternal(); + for (int tid : tids) { + if (mNonIsolatedTids.containsKey(tid)) { + mNonIsolatedTids.get(tid).remove(mHalSessionPtr); + if (mNonIsolatedTids.get(tid).isEmpty()) { + mNonIsolatedTids.remove(tid); + } + } + } + } + } } @Override public void sendHint(@PerformanceHintManager.Session.Hint int hint) { synchronized (this) { - if (mHalSessionPtr == 0 || !mUpdateAllowed) { + if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) { return; } Preconditions.checkArgument(hint >= 0, "the hint ID value should be" @@ -596,33 +856,60 @@ public final class HintManagerService extends SystemService { } public void setThreads(@NonNull int[] tids) { + setThreadsInternal(tids, true); + } + + private void setThreadsInternal(int[] tids, boolean checkTid) { + if (tids.length == 0) { + throw new IllegalArgumentException("Thread id list can't be empty."); + } + synchronized (this) { if (mHalSessionPtr == 0) { return; } - if (tids.length == 0) { - throw new IllegalArgumentException("Thread id list can't be empty."); - } - final int callingUid = Binder.getCallingUid(); - final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid()); - final long identity = Binder.clearCallingIdentity(); - try { - final Integer invalidTid = checkTidValid(callingUid, callingTgid, tids); - if (invalidTid != null) { - final String errMsg = formatTidCheckErrMsg(callingUid, tids, invalidTid); - Slogf.w(TAG, errMsg); - throw new SecurityException(errMsg); - } - } finally { - Binder.restoreCallingIdentity(identity); - } if (!mUpdateAllowed) { Slogf.v(TAG, "update hint not allowed, storing tids."); mNewThreadIds = tids; + mShouldForcePause = false; return; } + if (checkTid) { + final int callingUid = Binder.getCallingUid(); + final int callingTgid = Process.getThreadGroupLeader(Binder.getCallingPid()); + final IntArray nonIsolated = powerhintThreadCleanup() ? new IntArray() : null; + final long identity = Binder.clearCallingIdentity(); + try { + final Integer invalidTid = checkTidValid(callingUid, callingTgid, tids, + nonIsolated); + if (invalidTid != null) { + final String errMsg = formatTidCheckErrMsg(callingUid, tids, + invalidTid); + Slogf.w(TAG, errMsg); + throw new SecurityException(errMsg); + } + if (powerhintThreadCleanup()) { + synchronized (mNonIsolatedTidsLock) { + for (int i = nonIsolated.size() - 1; i >= 0; i--) { + mNonIsolatedTids.putIfAbsent(nonIsolated.get(i), + new ArraySet<>()); + mNonIsolatedTids.get(nonIsolated.get(i)).add(mHalSessionPtr); + } + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } mNativeWrapper.halSetThreads(mHalSessionPtr, tids); mThreadIds = tids; + mNewThreadIds = null; + // if the update is allowed but the session is force paused by tid clean up, then + // it's waiting for this tid update to resume + if (mShouldForcePause) { + resume(); + mShouldForcePause = false; + } } } @@ -632,10 +919,24 @@ public final class HintManagerService extends SystemService { } } + @VisibleForTesting + int[] getTidsInternal() { + synchronized (this) { + return mNewThreadIds != null ? Arrays.copyOf(mNewThreadIds, mNewThreadIds.length) + : Arrays.copyOf(mThreadIds, mThreadIds.length); + } + } + + boolean isClosed() { + synchronized (this) { + return mHalSessionPtr == 0; + } + } + @Override public void setMode(int mode, boolean enabled) { synchronized (this) { - if (mHalSessionPtr == 0 || !mUpdateAllowed) { + if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) { return; } Preconditions.checkArgument(mode >= 0, "the mode Id value should be" @@ -650,13 +951,13 @@ public final class HintManagerService extends SystemService { @Override public void reportActualWorkDuration2(WorkDuration[] workDurations) { synchronized (this) { - if (mHalSessionPtr == 0 || !mUpdateAllowed) { + if (mHalSessionPtr == 0 || !mUpdateAllowed || mShouldForcePause) { return; } Preconditions.checkArgument(workDurations.length != 0, "the count" + " of work durations shouldn't be 0."); - for (WorkDuration workDuration : workDurations) { - validateWorkDuration(workDuration); + for (int i = 0; i < workDurations.length; i++) { + validateWorkDuration(workDurations[i]); } mNativeWrapper.halReportActualWorkDuration(mHalSessionPtr, workDurations); } @@ -743,6 +1044,7 @@ public final class HintManagerService extends SystemService { pw.println(prefix + "SessionTIDs: " + Arrays.toString(mThreadIds)); pw.println(prefix + "SessionTargetDurationNanos: " + mTargetDurationNanos); pw.println(prefix + "SessionAllowed: " + mUpdateAllowed); + pw.println(prefix + "SessionForcePaused: " + mShouldForcePause); pw.println(prefix + "PowerEfficient: " + (mPowerEfficient ? "true" : "false")); } } diff --git a/services/core/java/com/android/server/power/hint/flags.aconfig b/services/core/java/com/android/server/power/hint/flags.aconfig new file mode 100644 index 000000000000..f4afcb141b19 --- /dev/null +++ b/services/core/java/com/android/server/power/hint/flags.aconfig @@ -0,0 +1,8 @@ +package: "com.android.server.power.hint" + +flag { + name: "powerhint_thread_cleanup" + namespace: "game" + description: "Feature flag for auto PowerHintSession dead thread cleanup" + bug: "296160319" +} diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java index f7c236afda20..2ff38616fce5 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java @@ -267,7 +267,7 @@ public interface StatusBarManagerInternal { void removeQsTile(ComponentName tile); /** - * Called when requested to enter desktop from an app. + * Called when requested to enter desktop from a focused app. */ - void enterDesktop(int displayId); + void moveFocusedTaskToDesktop(int displayId); } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index 7b3e23776a55..cca5beb13405 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -838,15 +838,17 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } catch (RemoteException ex) { } } } + @Override - public void enterDesktop(int displayId) { + public void moveFocusedTaskToDesktop(int displayId) { IStatusBar bar = mBar; if (bar != null) { try { - bar.enterDesktop(displayId); + bar.moveFocusedTaskToDesktop(displayId); } catch (RemoteException ex) { } } } + @Override public void showMediaOutputSwitcher(String packageName) { IStatusBar bar = mBar; diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 060f1c8cfac0..6af496f4af24 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -5682,29 +5682,6 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { throw e; } - /** - * Sets the corresponding {@link DisplayArea} information for the process global - * configuration. To be called when we need to show IME on a different {@link DisplayArea} - * or display. - * - * @param pid The process id associated with the IME window. - * @param imeContainer The DisplayArea that contains the IME window. - */ - void onImeWindowSetOnDisplayArea(final int pid, @NonNull final DisplayArea imeContainer) { - if (pid == MY_PID || pid < 0) { - ProtoLog.w(WM_DEBUG_CONFIGURATION, - "Trying to update display configuration for system/invalid process."); - return; - } - final WindowProcessController process = mProcessMap.getProcess(pid); - if (process == null) { - ProtoLog.w(WM_DEBUG_CONFIGURATION, "Trying to update display " - + "configuration for invalid process, pid=%d", pid); - return; - } - process.registerDisplayAreaConfigurationListener(imeContainer); - } - @Override public void setRunningRemoteTransitionDelegate(IApplicationThread delegate) { final TransitionController controller = getTransitionController(); diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index eb1f052baac6..46d4ce400053 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -4171,11 +4171,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp */ void setInputMethodWindowLocked(WindowState win) { mInputMethodWindow = win; - // Update display configuration for IME process. - if (mInputMethodWindow != null) { - final int imePid = mInputMethodWindow.mSession.mPid; - mAtmService.onImeWindowSetOnDisplayArea(imePid, mImeWindowsContainer); - } mInsetsStateController.getImeSourceProvider().setWindowContainer(win, mDisplayPolicy.getImeSourceFrameProvider(), null); computeImeTarget(true /* updateImeTarget */); diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index 30134d815fa6..e157318543f6 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -283,14 +283,14 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { int lastSyncSeqId, ClientWindowFrames outFrames, MergedConfiguration mergedConfiguration, SurfaceControl outSurfaceControl, InsetsState outInsetsState, InsetsSourceControl.Array outActiveControls, - Bundle outSyncSeqIdBundle) { + Bundle outBundle) { if (false) Slog.d(TAG_WM, ">>>>>> ENTERED relayout from " + Binder.getCallingPid()); Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, mRelayoutTag); int res = mService.relayoutWindow(this, window, attrs, requestedWidth, requestedHeight, viewFlags, flags, seq, lastSyncSeqId, outFrames, mergedConfiguration, outSurfaceControl, outInsetsState, - outActiveControls, outSyncSeqIdBundle); + outActiveControls, outBundle); Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER); if (false) Slog.d(TAG_WM, "<<<<<< EXITING relayout to " + Binder.getCallingPid()); diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 4c282bd1cb65..18d2718437a6 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -6822,8 +6822,8 @@ class Task extends TaskFragment { * A decor surface is requested by a {@link TaskFragmentOrganizer} and is placed below children * windows in the Task except for own Activities and TaskFragments in fully trusted mode. The * decor surface is created and shared with the client app with - * {@link android.window.TaskFragmentOperation#OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE} and - * be removed with + * {@link android.window.TaskFragmentOperation#OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE} + * and be removed with * {@link android.window.TaskFragmentOperation#OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE}. * * When boosted with diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 2934574acc03..0effa6c05801 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -304,6 +304,7 @@ import android.view.WindowManagerPolicyConstants.PointerEventListener; import android.view.displayhash.DisplayHash; import android.view.displayhash.VerifiedDisplayHash; import android.view.inputmethod.ImeTracker; +import android.window.ActivityWindowInfo; import android.window.AddToSurfaceSyncGroupResult; import android.window.ClientWindowFrames; import android.window.IGlobalDragListener; @@ -794,6 +795,8 @@ public class WindowManagerService extends IWindowManager.Stub Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE); private final Uri mImmersiveModeConfirmationsUri = Settings.Secure.getUriFor(Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS); + private final Uri mDisableSecureWindowsUri = + Settings.Secure.getUriFor(Settings.Secure.DISABLE_SECURE_WINDOWS); private final Uri mPolicyControlUri = Settings.Global.getUriFor(Settings.Global.POLICY_CONTROL); private final Uri mForceDesktopModeOnExternalDisplaysUri = Settings.Global.getUriFor( @@ -822,6 +825,8 @@ public class WindowManagerService extends IWindowManager.Stub UserHandle.USER_ALL); resolver.registerContentObserver(mImmersiveModeConfirmationsUri, false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(mDisableSecureWindowsUri, false, this, + UserHandle.USER_ALL); resolver.registerContentObserver(mPolicyControlUri, false, this, UserHandle.USER_ALL); resolver.registerContentObserver(mForceDesktopModeOnExternalDisplaysUri, false, this, UserHandle.USER_ALL); @@ -876,6 +881,11 @@ public class WindowManagerService extends IWindowManager.Stub return; } + if (mDisableSecureWindowsUri.equals(uri)) { + updateDisableSecureWindows(); + return; + } + @UpdateAnimationScaleMode final int mode; if (mWindowAnimationScaleUri.equals(uri)) { @@ -895,6 +905,7 @@ public class WindowManagerService extends IWindowManager.Stub void loadSettings() { updateSystemUiSettings(false /* handleChange */); updateMaximumObscuringOpacityForTouch(); + updateDisableSecureWindows(); } void updateMaximumObscuringOpacityForTouch() { @@ -977,6 +988,28 @@ public class WindowManagerService extends IWindowManager.Stub }); } } + + void updateDisableSecureWindows() { + if (!SystemProperties.getBoolean(SYSTEM_DEBUGGABLE, false)) { + return; + } + + final boolean disableSecureWindows; + try { + disableSecureWindows = Settings.Secure.getIntForUser(mContext.getContentResolver(), + Settings.Secure.DISABLE_SECURE_WINDOWS, 0) != 0; + } catch (Settings.SettingNotFoundException e) { + return; + } + if (mDisableSecureWindows == disableSecureWindows) { + return; + } + + synchronized (mGlobalLock) { + mDisableSecureWindows = disableSecureWindows; + mRoot.refreshSecureSurfaceState(); + } + } } PowerManager mPowerManager; @@ -1115,6 +1148,8 @@ public class WindowManagerService extends IWindowManager.Stub private final ScreenRecordingCallbackController mScreenRecordingCallbackController; + private volatile boolean mDisableSecureWindows = false; + public static WindowManagerService main(final Context context, final InputManagerService im, final boolean showBootMsgs, WindowManagerPolicy policy, ActivityTaskManagerService atm) { @@ -2213,7 +2248,7 @@ public class WindowManagerService extends IWindowManager.Stub int lastSyncSeqId, ClientWindowFrames outFrames, MergedConfiguration outMergedConfiguration, SurfaceControl outSurfaceControl, InsetsState outInsetsState, InsetsSourceControl.Array outActiveControls, - Bundle outSyncIdBundle) { + Bundle outBundle) { if (outActiveControls != null) { outActiveControls.set(null); } @@ -2544,6 +2579,13 @@ public class WindowManagerService extends IWindowManager.Stub if (outFrames != null && outMergedConfiguration != null) { win.fillClientWindowFramesAndConfiguration(outFrames, outMergedConfiguration, false /* useLatestConfig */, shouldRelayout); + if (Flags.activityWindowInfoFlag() && outBundle != null + && win.mActivityRecord != null) { + final ActivityWindowInfo activityWindowInfo = win.mActivityRecord + .getActivityWindowInfo(); + outBundle.putParcelable(IWindowSession.KEY_RELAYOUT_BUNDLE_ACTIVITY_WINDOW_INFO, + activityWindowInfo); + } // Set resize-handled here because the values are sent back to the client. win.onResizeHandled(); @@ -2573,7 +2615,7 @@ public class WindowManagerService extends IWindowManager.Stub win.isVisible() /* visible */, false /* removed */); } - if (outSyncIdBundle != null) { + if (outBundle != null) { final int maybeSyncSeqId; if (win.syncNextBuffer() && viewVisibility == View.VISIBLE && win.mSyncSeqId > lastSyncSeqId) { @@ -2582,7 +2624,7 @@ public class WindowManagerService extends IWindowManager.Stub } else { maybeSyncSeqId = -1; } - outSyncIdBundle.putInt("seqid", maybeSyncSeqId); + outBundle.putInt(IWindowSession.KEY_RELAYOUT_BUNDLE_SEQID, maybeSyncSeqId); } if (configChanged) { @@ -6897,6 +6939,7 @@ public class WindowManagerService extends IWindowManager.Stub pw.print(mLastFinishedFreezeSource); } pw.println(); + pw.print(" mDisableSecureWindows="); pw.println(mDisableSecureWindows); mInputManagerCallback.dump(pw, " "); mSnapshotController.dump(pw, " "); @@ -10068,4 +10111,8 @@ public class WindowManagerService extends IWindowManager.Stub mDragDropController.setGlobalDragListener(listener); } } + + boolean getDisableSecureWindows() { + return mDisableSecureWindows; + } } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index d967cde84cbf..14ec41f072dd 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -23,7 +23,7 @@ import static android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS; import static android.view.Display.DEFAULT_DISPLAY; import static android.window.TaskFragmentOperation.OP_TYPE_CLEAR_ADJACENT_TASK_FRAGMENTS; import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT; -import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE; +import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE; import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE; import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_BOTTOM_OF_TASK; @@ -1558,7 +1558,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } break; } - case OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE: { + case OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE: { taskFragment.getTask().moveOrCreateDecorSurfaceFor(taskFragment); break; } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index c0cf97d6d4ae..ca8f7909220d 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -1898,6 +1898,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP } boolean isSecureLocked() { + if (mWmService.getDisableSecureWindows()) { + return false; + } + if ((mAttrs.flags & WindowManager.LayoutParams.FLAG_SECURE) != 0) { return true; } diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 610fcb5962c8..70224db061c7 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -143,6 +143,7 @@ static struct { jmethodID getTouchCalibrationForInputDevice; jmethodID notifyDropWindow; jmethodID getParentSurfaceForPointers; + jmethodID getPackageUid; } gServiceClassInfo; static struct { @@ -362,6 +363,7 @@ public: void notifyDropWindow(const sp<IBinder>& token, float x, float y) override; void notifyDeviceInteraction(int32_t deviceId, nsecs_t timestamp, const std::set<gui::Uid>& uids) override; + gui::Uid getPackageUid(std::string package) override; /* --- PointerControllerPolicyInterface implementation --- */ @@ -1116,6 +1118,21 @@ void NativeInputManager::notifyDeviceInteraction(int32_t deviceId, nsecs_t times mInputManager->getMetricsCollector().notifyDeviceInteraction(deviceId, timestamp, uids); } +gui::Uid NativeInputManager::getPackageUid(std::string package) { + ATRACE_CALL(); + JNIEnv* env = jniEnv(); + ScopedLocalFrame localFrame(env); + + ScopedLocalRef<jstring> javaPackage(env, env->NewStringUTF(package.c_str())); + const jint uid = + env->CallIntMethod(mServiceObj, gServiceClassInfo.getPackageUid, javaPackage.get()); + if (checkAndClearExceptionFromCallback(env, "getPackageUid")) { + LOG(FATAL) << __func__ << ": Failed to get UID for package: " << package; + } + + return gui::Uid{static_cast<uint32_t>(uid)}; +} + void NativeInputManager::notifySensorEvent(int32_t deviceId, InputDeviceSensorType sensorType, InputDeviceSensorAccuracy accuracy, nsecs_t timestamp, const std::vector<float>& values) { @@ -3101,6 +3118,8 @@ int register_android_server_InputManager(JNIEnv* env) { GET_METHOD_ID(gServiceClassInfo.getParentSurfaceForPointers, clazz, "getParentSurfaceForPointers", "(I)J"); + GET_METHOD_ID(gServiceClassInfo.getPackageUid, clazz, "getPackageUid", "(Ljava/lang/String;)I"); + // InputDevice FIND_CLASS(gInputDeviceClassInfo.clazz, "android/view/InputDevice"); diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java index 173cb36a1a34..cac42b17553a 100644 --- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java @@ -112,7 +112,8 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS), /*defaultProviderId=*/flattenedPrimaryProviders, /*isShowAllOptionsRequested=*/ false), - providerDataList); + providerDataList, + mRequestSessionMetric); mClientCallback.onPendingIntent(mPendingIntent); } catch (RemoteException e) { mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false); diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java index f5e1e41dbae4..24f66977ee90 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerUi.java @@ -25,6 +25,7 @@ import android.content.Intent; import android.credentials.CredentialManager; import android.credentials.CredentialProviderInfo; import android.credentials.selection.DisabledProviderData; +import android.credentials.selection.IntentCreationResult; import android.credentials.selection.IntentFactory; import android.credentials.selection.ProviderData; import android.credentials.selection.RequestInfo; @@ -37,6 +38,8 @@ import android.os.ResultReceiver; import android.os.UserHandle; import android.service.credentials.CredentialProviderInfoFactory; +import com.android.server.credentials.metrics.RequestSessionMetric; + import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -159,7 +162,8 @@ public class CredentialManagerUi { * @param providerDataList the list of provider data from remote providers */ public PendingIntent createPendingIntent( - RequestInfo requestInfo, ArrayList<ProviderData> providerDataList) { + RequestInfo requestInfo, ArrayList<ProviderData> providerDataList, + RequestSessionMetric requestSessionMetric) { List<CredentialProviderInfo> allProviders = CredentialProviderInfoFactory.getCredentialProviderServices( mContext, @@ -174,10 +178,12 @@ public class CredentialManagerUi { .map(disabledProvider -> new DisabledProviderData( disabledProvider.getComponentName().flattenToString())).toList(); - Intent intent; - intent = IntentFactory.createCredentialSelectorIntent( - mContext, requestInfo, providerDataList, - new ArrayList<>(disabledProviderDataList), mResultReceiver); + IntentCreationResult intentCreationResult = IntentFactory + .createCredentialSelectorIntentForCredMan(mContext, requestInfo, providerDataList, + new ArrayList<>(disabledProviderDataList), mResultReceiver); + requestSessionMetric.collectUiConfigurationResults( + mContext, intentCreationResult, mUserId); + Intent intent = intentCreationResult.getIntent(); intent.setAction(UUID.randomUUID().toString()); //TODO: Create unique pending intent using request code and cancel any pre-existing pending // intents @@ -197,10 +203,15 @@ public class CredentialManagerUi { * of the pinned entry. * * @param requestInfo the information about the request + * @param requestSessionMetric the metric object for logging */ - public Intent createIntentForAutofill(RequestInfo requestInfo) { - return IntentFactory.createCredentialSelectorIntentForAutofill( - mContext, requestInfo, new ArrayList<>(), - mResultReceiver); + public Intent createIntentForAutofill(RequestInfo requestInfo, + RequestSessionMetric requestSessionMetric) { + IntentCreationResult intentCreationResult = IntentFactory + .createCredentialSelectorIntentForAutofill(mContext, requestInfo, new ArrayList<>(), + mResultReceiver); + requestSessionMetric.collectUiConfigurationResults( + mContext, intentCreationResult, mUserId); + return intentCreationResult.getIntent(); } } diff --git a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java index eff53de75ff4..fd2a9a20640b 100644 --- a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java @@ -122,7 +122,8 @@ public class GetCandidateRequestSession extends RequestSession<GetCredentialRequ mRequestId, mClientRequest, mClientAppInfo.getPackageName(), PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(), Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS), - /*isShowAllOptionsRequested=*/ true)); + /*isShowAllOptionsRequested=*/ true), + mRequestSessionMetric); List<GetCredentialProviderData> candidateProviderDataList = new ArrayList<>(); for (ProviderData providerData : providerDataList) { diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java index 6513ae1af369..d55d8effd381 100644 --- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java @@ -111,7 +111,8 @@ public class GetRequestSession extends RequestSession<GetCredentialRequest, Manifest.permission .CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS), /*isShowAllOptionsRequested=*/ false), - providerDataList); + providerDataList, + mRequestSessionMetric); mClientCallback.onPendingIntent(mPendingIntent); } catch (RemoteException e) { mRequestSessionMetric.collectUiReturnedFinalPhase(/*uiReturned=*/ false); diff --git a/services/credentials/java/com/android/server/credentials/MetricUtilities.java b/services/credentials/java/com/android/server/credentials/MetricUtilities.java index bdea4f9d2baa..16bf17781eea 100644 --- a/services/credentials/java/com/android/server/credentials/MetricUtilities.java +++ b/services/credentials/java/com/android/server/credentials/MetricUtilities.java @@ -16,6 +16,7 @@ package com.android.server.credentials; +import android.annotation.UserIdInt; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; @@ -68,17 +69,27 @@ public class MetricUtilities { * * @return the uid of a given package */ - protected static int getPackageUid(Context context, ComponentName componentName) { - int sessUid = -1; + protected static int getPackageUid(Context context, ComponentName componentName, + @UserIdInt int userId) { + if (componentName == null) { + return -1; + } + return getPackageUid(context, componentName.getPackageName(), userId); + } + + /** Returns the package uid, or -1 if not found. */ + public static int getPackageUid(Context context, String packageName, + @UserIdInt int userId) { + if (packageName == null) { + return -1; + } try { - // Only for T and above, which is fine for our use case - sessUid = context.getPackageManager().getApplicationInfo( - componentName.getPackageName(), - PackageManager.ApplicationInfoFlags.of(0)).uid; + return context.getPackageManager().getPackageUidAsUser(packageName, + PackageManager.PackageInfoFlags.of(0), userId); } catch (Throwable t) { - Slog.i(TAG, "Couldn't find required uid"); + Slog.i(TAG, "Couldn't find uid for " + packageName + ": " + t); + return -1; } - return sessUid; } /** diff --git a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java index 6e8f7c8d7722..e4b5c776301e 100644 --- a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java @@ -193,7 +193,8 @@ public class PrepareGetRequestSession extends GetRequestSession { PermissionUtils.hasPermission(mContext, mClientAppInfo.getPackageName(), Manifest.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS), /*isShowAllOptionsRequested=*/ false), - providerDataList); + providerDataList, + mRequestSessionMetric); } else { return null; } diff --git a/services/credentials/java/com/android/server/credentials/ProviderSession.java b/services/credentials/java/com/android/server/credentials/ProviderSession.java index c16e2327abfb..dfc08f04386e 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderSession.java @@ -153,7 +153,7 @@ public abstract class ProviderSession<T, R> mUserId = userId; mComponentName = componentName; mRemoteCredentialService = remoteCredentialService; - mProviderSessionUid = MetricUtilities.getPackageUid(mContext, mComponentName); + mProviderSessionUid = MetricUtilities.getPackageUid(mContext, mComponentName, userId); mProviderSessionMetric = new ProviderSessionMetric( ((RequestSession) mCallbacks).mRequestSessionMetric.getSessionIdTrackTwo()); } diff --git a/services/credentials/java/com/android/server/credentials/metrics/OemUiUsageStatus.java b/services/credentials/java/com/android/server/credentials/metrics/OemUiUsageStatus.java index 2fd3a868369d..80ce354c4972 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/OemUiUsageStatus.java +++ b/services/credentials/java/com/android/server/credentials/metrics/OemUiUsageStatus.java @@ -22,7 +22,12 @@ import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_FIN import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_FINAL_NO_UID_REPORTED__OEM_UI_USAGE_STATUS__OEM_UI_USAGE_STATUS_SPECIFIED_BUT_NOT_FOUND; import static com.android.internal.util.FrameworkStatsLog.CREDENTIAL_MANAGER_FINAL_NO_UID_REPORTED__OEM_UI_USAGE_STATUS__OEM_UI_USAGE_STATUS_SPECIFIED_BUT_NOT_ENABLED; +import android.credentials.selection.IntentCreationResult; +/** + * Result of attempting to use the config_oemCredentialManagerDialogComponent as the Credential + * Manager UI. + */ public enum OemUiUsageStatus { UNKNOWN(CREDENTIAL_MANAGER_FINAL_NO_UID_REPORTED__OEM_UI_USAGE_STATUS__OEM_UI_USAGE_STATUS_UNKNOWN), SUCCESS(CREDENTIAL_MANAGER_FINAL_NO_UID_REPORTED__OEM_UI_USAGE_STATUS__OEM_UI_USAGE_STATUS_SUCCESS), @@ -39,4 +44,21 @@ public enum OemUiUsageStatus { public int getLoggingInt() { return mLoggingInt; } + + /** Factory method. */ + public static OemUiUsageStatus createFrom(IntentCreationResult.OemUiUsageStatus from) { + switch (from) { + case UNKNOWN: + return OemUiUsageStatus.UNKNOWN; + case SUCCESS: + return OemUiUsageStatus.SUCCESS; + case OEM_UI_CONFIG_NOT_SPECIFIED: + return OemUiUsageStatus.FAILURE_NOT_SPECIFIED; + case OEM_UI_CONFIG_SPECIFIED_BUT_NOT_FOUND: + return OemUiUsageStatus.FAILURE_SPECIFIED_BUT_NOT_FOUND; + case OEM_UI_CONFIG_SPECIFIED_FOUND_BUT_NOT_ENABLED: + return OemUiUsageStatus.FAILURE_SPECIFIED_BUT_NOT_ENABLED; + } + return OemUiUsageStatus.UNKNOWN; + } } diff --git a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java index a77bd3e280dd..619a56846e95 100644 --- a/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java +++ b/services/credentials/java/com/android/server/credentials/metrics/RequestSessionMetric.java @@ -30,9 +30,12 @@ import static com.android.server.credentials.metrics.ApiName.GET_CREDENTIAL; import static com.android.server.credentials.metrics.ApiName.GET_CREDENTIAL_VIA_REGISTRY; import android.annotation.NonNull; +import android.annotation.UserIdInt; import android.content.ComponentName; +import android.content.Context; import android.credentials.CreateCredentialRequest; import android.credentials.GetCredentialRequest; +import android.credentials.selection.IntentCreationResult; import android.credentials.selection.UserSelectionDialogResult; import android.util.Slog; @@ -270,6 +273,21 @@ public class RequestSessionMetric { } } + /** Log results of the device Credential Manager UI configuration. */ + public void collectUiConfigurationResults(Context context, IntentCreationResult result, + @UserIdInt int userId) { + try { + mChosenProviderFinalPhaseMetric.setOemUiUid(MetricUtilities.getPackageUid( + context, result.getOemUiPackageName(), userId)); + mChosenProviderFinalPhaseMetric.setFallbackUiUid(MetricUtilities.getPackageUid( + context, result.getFallbackUiPackageName(), userId)); + mChosenProviderFinalPhaseMetric.setOemUiUsageStatus( + OemUiUsageStatus.createFrom(result.getOemUiUsageStatus())); + } catch (Exception e) { + Slog.w(TAG, "Unexpected error during ui configuration result collection: " + e); + } + } + /** * Allows encapsulating the overall final phase metric status from the chosen and final * provider. diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 3b2a3dd9763a..e202bbf022bc 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -1230,10 +1230,6 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(ThermalManagerService.class); t.traceEnd(); - t.traceBegin("StartHintManager"); - mSystemServiceManager.startService(HintManagerService.class); - t.traceEnd(); - // Now that the power manager has been started, let the activity manager // initialize power management features. t.traceBegin("InitPowerManagement"); @@ -1614,6 +1610,10 @@ public final class SystemServer implements Dumpable { t.traceEnd(); } + t.traceBegin("StartHintManager"); + mSystemServiceManager.startService(HintManagerService.class); + t.traceEnd(); + // Grants default permissions and defines roles t.traceBegin("StartRoleManagerService"); LocalManagerRegistry.addManager(RoleServicePlatformHelper.class, diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java index cea65b55494d..9f46d0ba7df6 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java @@ -198,7 +198,9 @@ public class InputMethodManagerServiceWindowGainedFocusTest @Test public void startInputOrWindowGainedFocus_userNotRunning() throws RemoteException { - when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(false); + // Run blockingly on ServiceThread to avoid that interfering with our stubbing. + mServiceThread.getThreadHandler().runWithScissors( + () -> when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(false), 0); assertThat( startInputOrWindowGainedFocus( diff --git a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java index c30ac2d6c248..682569f1d9ab 100644 --- a/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/RescuePartyTest.java @@ -26,6 +26,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.android.server.RescueParty.LEVEL_FACTORY_RESET; +import static com.android.server.RescueParty.RESCUE_LEVEL_FACTORY_RESET; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -41,9 +42,11 @@ import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; +import android.crashrecovery.flags.Flags; import android.os.RecoverySystem; import android.os.SystemProperties; import android.os.UserHandle; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.DeviceConfig; import android.provider.Settings; import android.util.ArraySet; @@ -55,6 +58,7 @@ import com.android.server.am.SettingsToPropertiesMapper; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.Answers; import org.mockito.ArgumentCaptor; @@ -69,6 +73,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; @@ -100,6 +105,9 @@ public class RescuePartyTest { private static final int THROTTLING_DURATION_MIN = 10; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private MockitoSession mSession; private HashMap<String, String> mSystemSettingsMap; private HashMap<String, String> mCrashRecoveryPropertiesMap; @@ -267,6 +275,42 @@ public class RescuePartyTest { } @Test + public void testBootLoopDetectionWithExecutionForAllRescueLevelsRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + RescueParty.onSettingsProviderPublished(mMockContext); + verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver), + any(Executor.class), + mMonitorCallbackCaptor.capture())); + HashMap<String, Integer> verifiedTimesMap = new HashMap<String, Integer>(); + + // Record DeviceConfig accesses + DeviceConfig.MonitorCallback monitorCallback = mMonitorCallbackCaptor.getValue(); + monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE1); + monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE2); + + final String[] expectedAllResetNamespaces = new String[]{NAMESPACE1, NAMESPACE2}; + + noteBoot(1); + verifyDeviceConfigReset(expectedAllResetNamespaces, verifiedTimesMap); + + noteBoot(2); + assertTrue(RescueParty.isRebootPropertySet()); + + noteBoot(3); + verifyOnlySettingsReset(Settings.RESET_MODE_UNTRUSTED_DEFAULTS); + + noteBoot(4); + verifyOnlySettingsReset(Settings.RESET_MODE_UNTRUSTED_CHANGES); + + noteBoot(5); + verifyOnlySettingsReset(Settings.RESET_MODE_TRUSTED_DEFAULTS); + + setCrashRecoveryPropAttemptingReboot(false); + noteBoot(6); + assertTrue(RescueParty.isFactoryResetPropertySet()); + } + + @Test public void testPersistentAppCrashDetectionWithExecutionForAllRescueLevels() { noteAppCrash(1, true); @@ -292,6 +336,47 @@ public class RescuePartyTest { } @Test + public void testPersistentAppCrashDetectionWithExecutionForAllRescueLevelsRecoverability() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + RescueParty.onSettingsProviderPublished(mMockContext); + verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver), + any(Executor.class), + mMonitorCallbackCaptor.capture())); + HashMap<String, Integer> verifiedTimesMap = new HashMap<String, Integer>(); + + // Record DeviceConfig accesses + DeviceConfig.MonitorCallback monitorCallback = mMonitorCallbackCaptor.getValue(); + monitorCallback.onDeviceConfigAccess(PERSISTENT_PACKAGE, NAMESPACE1); + monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE1); + monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE2); + + final String[] expectedResetNamespaces = new String[]{NAMESPACE1}; + final String[] expectedAllResetNamespaces = new String[]{NAMESPACE1, NAMESPACE2}; + + noteAppCrash(1, true); + verifyDeviceConfigReset(expectedResetNamespaces, verifiedTimesMap); + + noteAppCrash(2, true); + verifyDeviceConfigReset(expectedAllResetNamespaces, verifiedTimesMap); + + noteAppCrash(3, true); + assertTrue(RescueParty.isRebootPropertySet()); + + noteAppCrash(4, true); + verifyOnlySettingsReset(Settings.RESET_MODE_UNTRUSTED_DEFAULTS); + + noteAppCrash(5, true); + verifyOnlySettingsReset(Settings.RESET_MODE_UNTRUSTED_CHANGES); + + noteAppCrash(6, true); + verifyOnlySettingsReset(Settings.RESET_MODE_TRUSTED_DEFAULTS); + + setCrashRecoveryPropAttemptingReboot(false); + noteAppCrash(7, true); + assertTrue(RescueParty.isFactoryResetPropertySet()); + } + + @Test public void testNonPersistentAppOnlyPerformsFlagResets() { noteAppCrash(1, false); @@ -316,6 +401,45 @@ public class RescuePartyTest { } @Test + public void testNonPersistentAppOnlyPerformsFlagResetsRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + RescueParty.onSettingsProviderPublished(mMockContext); + verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver), + any(Executor.class), + mMonitorCallbackCaptor.capture())); + HashMap<String, Integer> verifiedTimesMap = new HashMap<String, Integer>(); + + // Record DeviceConfig accesses + DeviceConfig.MonitorCallback monitorCallback = mMonitorCallbackCaptor.getValue(); + monitorCallback.onDeviceConfigAccess(NON_PERSISTENT_PACKAGE, NAMESPACE1); + monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE1); + monitorCallback.onDeviceConfigAccess(CALLING_PACKAGE1, NAMESPACE2); + + final String[] expectedResetNamespaces = new String[]{NAMESPACE1}; + final String[] expectedAllResetNamespaces = new String[]{NAMESPACE1, NAMESPACE2}; + + noteAppCrash(1, false); + verifyDeviceConfigReset(expectedResetNamespaces, verifiedTimesMap); + + noteAppCrash(2, false); + verifyDeviceConfigReset(expectedAllResetNamespaces, verifiedTimesMap); + + noteAppCrash(3, false); + assertFalse(RescueParty.isRebootPropertySet()); + + noteAppCrash(4, false); + verifyNoSettingsReset(Settings.RESET_MODE_UNTRUSTED_DEFAULTS); + noteAppCrash(5, false); + verifyNoSettingsReset(Settings.RESET_MODE_UNTRUSTED_CHANGES); + noteAppCrash(6, false); + verifyNoSettingsReset(Settings.RESET_MODE_TRUSTED_DEFAULTS); + + setCrashRecoveryPropAttemptingReboot(false); + noteAppCrash(7, false); + assertFalse(RescueParty.isFactoryResetPropertySet()); + } + + @Test public void testNonPersistentAppCrashDetectionWithScopedResets() { RescueParty.onSettingsProviderPublished(mMockContext); verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver), @@ -451,6 +575,19 @@ public class RescuePartyTest { } @Test + public void testIsRecoveryTriggeredRebootRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + for (int i = 0; i < RESCUE_LEVEL_FACTORY_RESET; i++) { + noteBoot(i + 1); + } + assertFalse(RescueParty.isFactoryResetPropertySet()); + setCrashRecoveryPropAttemptingReboot(false); + noteBoot(RESCUE_LEVEL_FACTORY_RESET + 1); + assertTrue(RescueParty.isRecoveryTriggeredReboot()); + assertTrue(RescueParty.isFactoryResetPropertySet()); + } + + @Test public void testIsRecoveryTriggeredRebootOnlyAfterRebootCompleted() { for (int i = 0; i < LEVEL_FACTORY_RESET; i++) { noteBoot(i + 1); @@ -469,6 +606,25 @@ public class RescuePartyTest { } @Test + public void testIsRecoveryTriggeredRebootOnlyAfterRebootCompletedRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + for (int i = 0; i < RESCUE_LEVEL_FACTORY_RESET; i++) { + noteBoot(i + 1); + } + int mitigationCount = RESCUE_LEVEL_FACTORY_RESET + 1; + assertFalse(RescueParty.isFactoryResetPropertySet()); + noteBoot(mitigationCount++); + assertFalse(RescueParty.isFactoryResetPropertySet()); + noteBoot(mitigationCount++); + assertFalse(RescueParty.isFactoryResetPropertySet()); + noteBoot(mitigationCount++); + setCrashRecoveryPropAttemptingReboot(false); + noteBoot(mitigationCount + 1); + assertTrue(RescueParty.isRecoveryTriggeredReboot()); + assertTrue(RescueParty.isFactoryResetPropertySet()); + } + + @Test public void testThrottlingOnBootFailures() { setCrashRecoveryPropAttemptingReboot(false); long now = System.currentTimeMillis(); @@ -481,6 +637,19 @@ public class RescuePartyTest { } @Test + public void testThrottlingOnBootFailuresRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + setCrashRecoveryPropAttemptingReboot(false); + long now = System.currentTimeMillis(); + long beforeTimeout = now - TimeUnit.MINUTES.toMillis(THROTTLING_DURATION_MIN - 1); + setCrashRecoveryPropLastFactoryReset(beforeTimeout); + for (int i = 1; i <= RESCUE_LEVEL_FACTORY_RESET; i++) { + noteBoot(i); + } + assertFalse(RescueParty.isRecoveryTriggeredReboot()); + } + + @Test public void testThrottlingOnAppCrash() { setCrashRecoveryPropAttemptingReboot(false); long now = System.currentTimeMillis(); @@ -493,6 +662,19 @@ public class RescuePartyTest { } @Test + public void testThrottlingOnAppCrashRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + setCrashRecoveryPropAttemptingReboot(false); + long now = System.currentTimeMillis(); + long beforeTimeout = now - TimeUnit.MINUTES.toMillis(THROTTLING_DURATION_MIN - 1); + setCrashRecoveryPropLastFactoryReset(beforeTimeout); + for (int i = 0; i <= RESCUE_LEVEL_FACTORY_RESET; i++) { + noteAppCrash(i + 1, true); + } + assertFalse(RescueParty.isRecoveryTriggeredReboot()); + } + + @Test public void testNotThrottlingAfterTimeoutOnBootFailures() { setCrashRecoveryPropAttemptingReboot(false); long now = System.currentTimeMillis(); @@ -503,6 +685,20 @@ public class RescuePartyTest { } assertTrue(RescueParty.isRecoveryTriggeredReboot()); } + + @Test + public void testNotThrottlingAfterTimeoutOnBootFailuresRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + setCrashRecoveryPropAttemptingReboot(false); + long now = System.currentTimeMillis(); + long afterTimeout = now - TimeUnit.MINUTES.toMillis(THROTTLING_DURATION_MIN + 1); + setCrashRecoveryPropLastFactoryReset(afterTimeout); + for (int i = 1; i <= RESCUE_LEVEL_FACTORY_RESET; i++) { + noteBoot(i); + } + assertTrue(RescueParty.isRecoveryTriggeredReboot()); + } + @Test public void testNotThrottlingAfterTimeoutOnAppCrash() { setCrashRecoveryPropAttemptingReboot(false); @@ -516,6 +712,19 @@ public class RescuePartyTest { } @Test + public void testNotThrottlingAfterTimeoutOnAppCrashRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + setCrashRecoveryPropAttemptingReboot(false); + long now = System.currentTimeMillis(); + long afterTimeout = now - TimeUnit.MINUTES.toMillis(THROTTLING_DURATION_MIN + 1); + setCrashRecoveryPropLastFactoryReset(afterTimeout); + for (int i = 0; i <= RESCUE_LEVEL_FACTORY_RESET; i++) { + noteAppCrash(i + 1, true); + } + assertTrue(RescueParty.isRecoveryTriggeredReboot()); + } + + @Test public void testNativeRescuePartyResets() { doReturn(true).when(() -> SettingsToPropertiesMapper.isNativeFlagsResetPerformed()); doReturn(FAKE_RESET_NATIVE_NAMESPACES).when( @@ -531,6 +740,7 @@ public class RescuePartyTest { @Test public void testExplicitlyEnablingAndDisablingRescue() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); SystemProperties.set(RescueParty.PROP_ENABLE_RESCUE, Boolean.toString(false)); SystemProperties.set(PROP_DISABLE_RESCUE, Boolean.toString(true)); assertEquals(RescuePartyObserver.getInstance(mMockContext).execute(sFailingPackage, @@ -543,6 +753,7 @@ public class RescuePartyTest { @Test public void testDisablingRescueByDeviceConfigFlag() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); SystemProperties.set(RescueParty.PROP_ENABLE_RESCUE, Boolean.toString(false)); SystemProperties.set(PROP_DEVICE_CONFIG_DISABLE_FLAG, Boolean.toString(true)); @@ -568,6 +779,20 @@ public class RescuePartyTest { } @Test + public void testDisablingFactoryResetByDeviceConfigFlagRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + SystemProperties.set(PROP_DISABLE_FACTORY_RESET_FLAG, Boolean.toString(true)); + + for (int i = 0; i < RESCUE_LEVEL_FACTORY_RESET; i++) { + noteBoot(i + 1); + } + assertFalse(RescueParty.isFactoryResetPropertySet()); + + // Restore the property value initialized in SetUp() + SystemProperties.set(PROP_DISABLE_FACTORY_RESET_FLAG, ""); + } + + @Test public void testHealthCheckLevels() { RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext); @@ -594,6 +819,46 @@ public class RescuePartyTest { } @Test + public void testHealthCheckLevelsRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext); + + // Ensure that no action is taken for cases where the failure reason is unknown + assertEquals(observer.onHealthCheckFailed(sFailingPackage, + PackageWatchdog.FAILURE_REASON_UNKNOWN, 1), + PackageHealthObserverImpact.USER_IMPACT_LEVEL_0); + + // Ensure the correct user impact is returned for each mitigation count. + assertEquals(observer.onHealthCheckFailed(sFailingPackage, + PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 1), + PackageHealthObserverImpact.USER_IMPACT_LEVEL_10); + + assertEquals(observer.onHealthCheckFailed(sFailingPackage, + PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 2), + PackageHealthObserverImpact.USER_IMPACT_LEVEL_20); + + assertEquals(observer.onHealthCheckFailed(sFailingPackage, + PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 3), + PackageHealthObserverImpact.USER_IMPACT_LEVEL_20); + + assertEquals(observer.onHealthCheckFailed(sFailingPackage, + PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 4), + PackageHealthObserverImpact.USER_IMPACT_LEVEL_20); + + assertEquals(observer.onHealthCheckFailed(sFailingPackage, + PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 5), + PackageHealthObserverImpact.USER_IMPACT_LEVEL_20); + + assertEquals(observer.onHealthCheckFailed(sFailingPackage, + PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 6), + PackageHealthObserverImpact.USER_IMPACT_LEVEL_20); + + assertEquals(observer.onHealthCheckFailed(sFailingPackage, + PackageWatchdog.FAILURE_REASON_APP_NOT_RESPONDING, 7), + PackageHealthObserverImpact.USER_IMPACT_LEVEL_20); + } + + @Test public void testBootLoopLevels() { RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext); @@ -606,6 +871,19 @@ public class RescuePartyTest { } @Test + public void testBootLoopLevelsRecoverabilityDetection() { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + RescuePartyObserver observer = RescuePartyObserver.getInstance(mMockContext); + + assertEquals(observer.onBootLoop(1), PackageHealthObserverImpact.USER_IMPACT_LEVEL_20); + assertEquals(observer.onBootLoop(2), PackageHealthObserverImpact.USER_IMPACT_LEVEL_50); + assertEquals(observer.onBootLoop(3), PackageHealthObserverImpact.USER_IMPACT_LEVEL_71); + assertEquals(observer.onBootLoop(4), PackageHealthObserverImpact.USER_IMPACT_LEVEL_75); + assertEquals(observer.onBootLoop(5), PackageHealthObserverImpact.USER_IMPACT_LEVEL_80); + assertEquals(observer.onBootLoop(6), PackageHealthObserverImpact.USER_IMPACT_LEVEL_100); + } + + @Test public void testResetDeviceConfigForPackagesOnlyRuntimeMap() { RescueParty.onSettingsProviderPublished(mMockContext); verify(() -> DeviceConfig.setMonitorCallback(eq(mMockContentResolver), @@ -727,11 +1005,26 @@ public class RescuePartyTest { private void verifySettingsResets(int resetMode, String[] resetNamespaces, HashMap<String, Integer> configResetVerifiedTimesMap) { + verifyOnlySettingsReset(resetMode); + verifyDeviceConfigReset(resetNamespaces, configResetVerifiedTimesMap); + } + + private void verifyOnlySettingsReset(int resetMode) { verify(() -> Settings.Global.resetToDefaultsAsUser(mMockContentResolver, null, resetMode, UserHandle.USER_SYSTEM)); verify(() -> Settings.Secure.resetToDefaultsAsUser(eq(mMockContentResolver), isNull(), eq(resetMode), anyInt())); - // Verify DeviceConfig resets + } + + private void verifyNoSettingsReset(int resetMode) { + verify(() -> Settings.Global.resetToDefaultsAsUser(mMockContentResolver, null, + resetMode, UserHandle.USER_SYSTEM), never()); + verify(() -> Settings.Secure.resetToDefaultsAsUser(eq(mMockContentResolver), isNull(), + eq(resetMode), anyInt()), never()); + } + + private void verifyDeviceConfigReset(String[] resetNamespaces, + Map<String, Integer> configResetVerifiedTimesMap) { if (resetNamespaces == null) { verify(() -> DeviceConfig.resetToDefaults(anyInt(), anyString()), never()); } else { @@ -818,9 +1111,16 @@ public class RescuePartyTest { // mock properties in BootThreshold try { - mSpyBootThreshold = spy(watchdog.new BootThreshold( - PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT, - PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS)); + if (Flags.recoverabilityDetection()) { + mSpyBootThreshold = spy(watchdog.new BootThreshold( + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS, + PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT)); + } else { + mSpyBootThreshold = spy(watchdog.new BootThreshold( + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS)); + } mCrashRecoveryPropertiesMap = new HashMap<>(); doAnswer((Answer<Integer>) invocationOnMock -> { diff --git a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java index 420af86c4408..1b2c0e4949e2 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/BroadcastQueueTest.java @@ -41,6 +41,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; @@ -57,6 +58,7 @@ import android.annotation.Nullable; import android.app.Activity; import android.app.ActivityManager; import android.app.AppOpsManager; +import android.app.ApplicationExitInfo; import android.app.BackgroundStartPrivileges; import android.app.BroadcastOptions; import android.app.IApplicationThread; @@ -239,6 +241,7 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { mConstants.TIMEOUT = 200; mConstants.ALLOW_BG_ACTIVITY_START_TIMEOUT = 0; mConstants.PENDING_COLD_START_CHECK_INTERVAL_MILLIS = 500; + mConstants.MAX_FROZEN_OUTGOING_BROADCASTS = 10; } @After @@ -2368,6 +2371,34 @@ public class BroadcastQueueTest extends BaseBroadcastQueueTest { verifyScheduleReceiver(times(1), receiverYellowApp, timeTick); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_DEFER_OUTGOING_BROADCASTS) + public void testKillProcess_excessiveOutgoingBroadcastsWhileCached() throws Exception { + final ProcessRecord callerApp = makeActiveProcessRecord(PACKAGE_RED); + setProcessFreezable(callerApp, true /* pendingFreeze */, false /* frozen */); + waitForIdle(); + + final int count = mConstants.MAX_FROZEN_OUTGOING_BROADCASTS + 1; + for (int i = 0; i < count; ++i) { + final Intent timeTick = new Intent(Intent.ACTION_TIME_TICK + "_" + i); + enqueueBroadcast(makeBroadcastRecord(timeTick, callerApp, List.of( + makeManifestReceiver(PACKAGE_BLUE, CLASS_BLUE)))); + } + // Verify that we invoke the call to freeze the caller app. + verify(mAms.mOomAdjuster.mCachedAppOptimizer, atLeastOnce()) + .freezeAppAsyncImmediateLSP(callerApp); + + // Verify that the caller process is killed + assertTrue(callerApp.isKilled()); + verify(mProcessList).noteAppKill(same(callerApp), + eq(ApplicationExitInfo.REASON_OTHER), + eq(ApplicationExitInfo.SUBREASON_EXCESSIVE_OUTGOING_BROADCASTS_WHILE_CACHED), + any(String.class)); + + waitForIdle(); + assertNull(mAms.getProcessRecordLocked(PACKAGE_BLUE, getUidForPackage(PACKAGE_BLUE))); + } + private long getReceiverScheduledTime(@NonNull BroadcastRecord r, @NonNull Object receiver) { for (int i = 0; i < r.receivers.size(); ++i) { if (isReceiverEquals(receiver, r.receivers.get(i))) { diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java index 97b7af8e43ad..680ab1634cb2 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ServiceBindingOomAdjPolicyTest.java @@ -36,7 +36,6 @@ import static com.android.server.am.ProcessList.SERVICE_ADJ; import static org.junit.Assert.assertNotEquals; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; @@ -185,8 +184,8 @@ public final class ServiceBindingOomAdjPolicyTest { doReturn(false).when(mAms.mAtmInternal).hasSystemAlertWindowPermission(anyInt(), anyInt(), any()); doReturn(true).when(mAms.mOomAdjuster.mCachedAppOptimizer).useFreezer(); - doNothing().when(mAms.mOomAdjuster.mCachedAppOptimizer).freezeAppAsyncInternalLSP( - any(), anyLong(), anyBoolean(), anyBoolean()); + doNothing().when(mAms.mOomAdjuster.mCachedAppOptimizer).freezeAppAsyncAtEarliestLSP( + any()); doReturn(false).when(mAms.mAppProfiler).updateLowMemStateLSP(anyInt(), anyInt(), anyInt(), anyLong()); @@ -503,7 +502,7 @@ public final class ServiceBindingOomAdjPolicyTest { if (clientApp.isFreezable()) { verify(mAms.mOomAdjuster.mCachedAppOptimizer, times(Flags.serviceBindingOomAdjPolicy() ? 1 : 0)) - .freezeAppAsyncInternalLSP(eq(clientApp), eq(0L), anyBoolean(), anyBoolean()); + .freezeAppAsyncAtEarliestLSP(eq(clientApp)); clearInvocations(mAms.mOomAdjuster.mCachedAppOptimizer); } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index 53c460c44354..9d32ed847645 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -21,7 +21,6 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; -import static android.view.accessibility.Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG; import static android.view.accessibility.Flags.FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES; import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME; @@ -885,7 +884,6 @@ public class AccessibilityManagerServiceTest { } @Test - @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void testIsAccessibilityServiceWarningRequired_requiredByDefault() { mockManageAccessibilityGranted(mTestableContext); final AccessibilityServiceInfo info = mockAccessibilityServiceInfo(COMPONENT_NAME); @@ -894,7 +892,6 @@ public class AccessibilityManagerServiceTest { } @Test - @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void testIsAccessibilityServiceWarningRequired_notRequiredIfAlreadyEnabled() { mockManageAccessibilityGranted(mTestableContext); final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(COMPONENT_NAME); @@ -909,7 +906,6 @@ public class AccessibilityManagerServiceTest { } @Test - @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void testIsAccessibilityServiceWarningRequired_notRequiredIfExistingShortcut() { mockManageAccessibilityGranted(mTestableContext); final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo( @@ -930,9 +926,7 @@ public class AccessibilityManagerServiceTest { } @Test - @RequiresFlagsEnabled({ - FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG, - FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES}) + @RequiresFlagsEnabled(FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES) public void testIsAccessibilityServiceWarningRequired_notRequiredIfAllowlisted() { mockManageAccessibilityGranted(mTestableContext); final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo( diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java index a4628ee3b52b..4d1d17f184d1 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceTest.java @@ -141,6 +141,7 @@ public class VirtualDeviceTest { @Test public void virtualDevice_hasCustomAudioInputSupport() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_VDM_PUBLIC_APIS); + mSetFlagsRule.enableFlags(android.media.audiopolicy.Flags.FLAG_AUDIO_MIX_TEST_API); VirtualDevice virtualDevice = new VirtualDevice( @@ -150,6 +151,10 @@ public class VirtualDeviceTest { assertThat(virtualDevice.hasCustomAudioInputSupport()).isFalse(); when(mVirtualDevice.getDevicePolicy(POLICY_TYPE_AUDIO)).thenReturn(DEVICE_POLICY_CUSTOM); + when(mVirtualDevice.hasCustomAudioInputSupport()).thenReturn(false); + assertThat(virtualDevice.hasCustomAudioInputSupport()).isFalse(); + + when(mVirtualDevice.hasCustomAudioInputSupport()).thenReturn(true); assertThat(virtualDevice.hasCustomAudioInputSupport()).isTrue(); } diff --git a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java index 66599e9e9125..510e7c42f12d 100644 --- a/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/power/hint/HintManagerServiceTest.java @@ -17,6 +17,8 @@ package com.android.server.power.hint; +import static com.android.server.power.hint.HintManagerService.CLEAN_UP_UID_DELAY_MILLIS; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertArrayEquals; @@ -45,6 +47,9 @@ import android.os.IBinder; import android.os.IHintSession; import android.os.PerformanceHintManager; import android.os.Process; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.util.Log; import com.android.server.FgThread; @@ -54,11 +59,13 @@ import com.android.server.power.hint.HintManagerService.Injector; import com.android.server.power.hint.HintManagerService.NativeWrapper; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.CountDownLatch; @@ -71,7 +78,7 @@ import java.util.concurrent.locks.LockSupport; * Tests for {@link com.android.server.power.hint.HintManagerService}. * * Build/Install/Run: - * atest FrameworksServicesTests:HintManagerServiceTest + * atest FrameworksServicesTests:HintManagerServiceTest */ public class HintManagerServiceTest { private static final String TAG = "HintManagerServiceTest"; @@ -110,9 +117,15 @@ public class HintManagerServiceTest { makeWorkDuration(2L, 13L, 2L, 8L, 0L), }; - @Mock private Context mContext; - @Mock private HintManagerService.NativeWrapper mNativeWrapperMock; - @Mock private ActivityManagerInternal mAmInternalMock; + @Mock + private Context mContext; + @Mock + private HintManagerService.NativeWrapper mNativeWrapperMock; + @Mock + private ActivityManagerInternal mAmInternalMock; + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); private HintManagerService mService; @@ -122,12 +135,11 @@ public class HintManagerServiceTest { when(mNativeWrapperMock.halGetHintSessionPreferredRate()) .thenReturn(DEFAULT_HINT_PREFERRED_RATE); when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_A), - eq(DEFAULT_TARGET_DURATION))).thenReturn(1L); + eq(DEFAULT_TARGET_DURATION))).thenReturn(1L); when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_B), - eq(DEFAULT_TARGET_DURATION))).thenReturn(2L); + eq(DEFAULT_TARGET_DURATION))).thenReturn(2L); when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(SESSION_TIDS_C), - eq(0L))).thenReturn(1L); - when(mAmInternalMock.getIsolatedProcesses(anyInt())).thenReturn(null); + eq(0L))).thenReturn(1L); LocalServices.removeServiceForTest(ActivityManagerInternal.class); LocalServices.addService(ActivityManagerInternal.class, mAmInternalMock); } @@ -434,6 +446,163 @@ public class HintManagerServiceTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_POWERHINT_THREAD_CLEANUP) + public void testCleanupDeadThreads() throws Exception { + HintManagerService service = createService(); + IBinder token = new Binder(); + CountDownLatch stopLatch1 = new CountDownLatch(1); + int threadCount = 3; + int[] tids1 = createThreads(threadCount, stopLatch1); + long sessionPtr1 = 111; + when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(tids1), + eq(DEFAULT_TARGET_DURATION))).thenReturn(sessionPtr1); + AppHintSession session1 = (AppHintSession) service.getBinderServiceInstance() + .createHintSession(token, tids1, DEFAULT_TARGET_DURATION); + assertNotNull(session1); + + // for test only to avoid conflicting with any real thread that exists on device + int isoProc1 = -100; + int isoProc2 = 9999; + when(mAmInternalMock.getIsolatedProcesses(eq(UID))).thenReturn(List.of(0)); + + CountDownLatch stopLatch2 = new CountDownLatch(1); + int[] tids2 = createThreads(threadCount, stopLatch2); + int[] tids2WithIsolated = Arrays.copyOf(tids2, tids2.length + 2); + int[] expectedTids2 = Arrays.copyOf(tids2, tids2.length + 1); + expectedTids2[tids2.length] = isoProc1; + tids2WithIsolated[threadCount] = isoProc1; + tids2WithIsolated[threadCount + 1] = isoProc2; + long sessionPtr2 = 222; + when(mNativeWrapperMock.halCreateHintSession(eq(TGID), eq(UID), eq(tids2WithIsolated), + eq(DEFAULT_TARGET_DURATION))).thenReturn(sessionPtr2); + AppHintSession session2 = (AppHintSession) service.getBinderServiceInstance() + .createHintSession(token, tids2WithIsolated, DEFAULT_TARGET_DURATION); + assertNotNull(session2); + + // trigger clean up through UID state change by making the process background + service.mUidObserver.onUidStateChanged(UID, + ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0); + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos( + CLEAN_UP_UID_DELAY_MILLIS)); + verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any()); + verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr2), any()); + // the new TIDs pending list should be updated + assertArrayEquals(session2.getTidsInternal(), expectedTids2); + reset(mNativeWrapperMock); + + // this should resume and update the threads so those never-existed invalid isolated + // processes should be cleaned up + service.mUidObserver.onUidStateChanged(UID, + ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0); + // wait for the async uid state change to trigger resume and setThreads + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500)); + verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr2), eq(expectedTids2)); + reset(mNativeWrapperMock); + + // let all session 1 threads to exit and the cleanup should force pause the session + stopLatch1.countDown(); + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100)); + service.mUidObserver.onUidStateChanged(UID, + ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0); + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos( + CLEAN_UP_UID_DELAY_MILLIS)); + verify(mNativeWrapperMock, times(1)).halPauseHintSession(eq(sessionPtr1)); + verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any()); + verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr2), any()); + // all hints will have no effect as the session is force paused while proc in foreground + verifyAllHintsEnabled(session1, false); + verifyAllHintsEnabled(session2, true); + reset(mNativeWrapperMock); + + // in foreground, set new tids for session 1 then it should be resumed and all hints allowed + stopLatch1 = new CountDownLatch(1); + tids1 = createThreads(threadCount, stopLatch1); + session1.setThreads(tids1); + verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr1), eq(tids1)); + verify(mNativeWrapperMock, times(1)).halResumeHintSession(eq(sessionPtr1)); + verifyAllHintsEnabled(session1, true); + reset(mNativeWrapperMock); + + // let all session 1 and 2 non isolated threads to exit + stopLatch1.countDown(); + stopLatch2.countDown(); + expectedTids2 = new int[]{isoProc1}; + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100)); + service.mUidObserver.onUidStateChanged(UID, + ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND, 0, 0); + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos( + CLEAN_UP_UID_DELAY_MILLIS)); + verify(mNativeWrapperMock, times(1)).halPauseHintSession(eq(sessionPtr1)); + verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr1), any()); + verify(mNativeWrapperMock, never()).halSetThreads(eq(sessionPtr2), any()); + // in background, set threads for session 1 then it should not be force paused next time + session1.setThreads(SESSION_TIDS_A); + // the new TIDs pending list should be updated + assertArrayEquals(session1.getTidsInternal(), SESSION_TIDS_A); + assertArrayEquals(session2.getTidsInternal(), expectedTids2); + verifyAllHintsEnabled(session1, false); + verifyAllHintsEnabled(session2, false); + reset(mNativeWrapperMock); + + service.mUidObserver.onUidStateChanged(UID, + ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND, 0, 0); + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos( + CLEAN_UP_UID_DELAY_MILLIS)); + verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr1), + eq(SESSION_TIDS_A)); + verify(mNativeWrapperMock, times(1)).halSetThreads(eq(sessionPtr2), + eq(expectedTids2)); + verifyAllHintsEnabled(session1, true); + verifyAllHintsEnabled(session2, true); + } + + private void verifyAllHintsEnabled(AppHintSession session, boolean verifyEnabled) { + session.reportActualWorkDuration2(new WorkDuration[]{makeWorkDuration(1, 3, 2, 1, 1000)}); + session.reportActualWorkDuration(new long[]{1}, new long[]{2}); + session.updateTargetWorkDuration(3); + session.setMode(0, true); + session.sendHint(1); + if (verifyEnabled) { + verify(mNativeWrapperMock, times(1)).halReportActualWorkDuration( + eq(session.mHalSessionPtr), any()); + verify(mNativeWrapperMock, times(1)).halSetMode(eq(session.mHalSessionPtr), anyInt(), + anyBoolean()); + verify(mNativeWrapperMock, times(1)).halUpdateTargetWorkDuration( + eq(session.mHalSessionPtr), anyLong()); + verify(mNativeWrapperMock, times(1)).halSendHint(eq(session.mHalSessionPtr), anyInt()); + } else { + verify(mNativeWrapperMock, never()).halReportActualWorkDuration( + eq(session.mHalSessionPtr), any()); + verify(mNativeWrapperMock, never()).halSetMode(eq(session.mHalSessionPtr), anyInt(), + anyBoolean()); + verify(mNativeWrapperMock, never()).halUpdateTargetWorkDuration( + eq(session.mHalSessionPtr), anyLong()); + verify(mNativeWrapperMock, never()).halSendHint(eq(session.mHalSessionPtr), anyInt()); + } + } + + private int[] createThreads(int threadCount, CountDownLatch stopLatch) + throws InterruptedException { + int[] tids = new int[threadCount]; + AtomicInteger k = new AtomicInteger(0); + CountDownLatch latch = new CountDownLatch(threadCount); + for (int j = 0; j < threadCount; j++) { + Thread thread = new Thread(() -> { + try { + tids[k.getAndIncrement()] = android.os.Process.myTid(); + latch.countDown(); + stopLatch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + thread.start(); + } + latch.await(); + return tids; + } + + @Test public void testSetMode() throws Exception { HintManagerService service = createService(); IBinder token = new Binder(); @@ -457,7 +626,8 @@ public class HintManagerServiceTest { // Set session to background, then the duration would not be updated. service.mUidObserver.onUidStateChanged( a.mUid, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0); - FgThread.getHandler().runWithScissors(() -> { }, 500); + FgThread.getHandler().runWithScissors(() -> { + }, 500); assertFalse(service.mUidObserver.isUidForeground(a.mUid)); a.setMode(0, true); verify(mNativeWrapperMock, never()).halSetMode(anyLong(), anyInt(), anyBoolean()); @@ -519,7 +689,10 @@ public class HintManagerServiceTest { LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500)); service.mUidObserver.onUidStateChanged(UID, ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND, 0, 0); - LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(500)); + // let the cleanup work proceed + LockSupport.parkNanos( + TimeUnit.MILLISECONDS.toNanos(500) + TimeUnit.MILLISECONDS.toNanos( + CLEAN_UP_UID_DELAY_MILLIS)); } Log.d(TAG, "notifier thread min " + min + " max " + max + " avg " + sum / count); service.mUidObserver.onUidGone(UID, true); diff --git a/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING b/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING new file mode 100644 index 000000000000..2d5df077b128 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/power/hint/TEST_MAPPING @@ -0,0 +1,15 @@ +{ + "postsubmit": [ + { + "name": "FrameworksServicesTests", + "options": [ + { + "include-filter": "com.android.server.power.hint" + }, + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + } + ] +} 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 bfc47fdef5cb..cee6cdb06bf5 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -3962,6 +3962,20 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test + public void testReadXml_existingPackage_bubblePrefsRestored() throws Exception { + mHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_ALL); + assertEquals(BUBBLE_PREFERENCE_ALL, mHelper.getBubblePreference(PKG_O, UID_O)); + + mXmlHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_NONE); + assertEquals(BUBBLE_PREFERENCE_NONE, mXmlHelper.getBubblePreference(PKG_O, UID_O)); + + ByteArrayOutputStream stream = writeXmlAndPurge(PKG_O, UID_O, false, UserHandle.USER_ALL); + loadStreamXml(stream, true, UserHandle.USER_ALL); + + assertEquals(BUBBLE_PREFERENCE_ALL, mXmlHelper.getBubblePreference(PKG_O, UID_O)); + } + + @Test public void testUpdateNotificationChannel_fixedPermission() { List<UserInfo> users = ImmutableList.of(new UserInfo(UserHandle.USER_SYSTEM, "user0", 0)); when(mPermissionHelper.isPermissionFixed(PKG_O, 0)).thenReturn(true); diff --git a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java index 29467f259ac3..a80e2f8ae28c 100644 --- a/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/PhoneWindowManagerTests.java @@ -16,10 +16,14 @@ package com.android.server.policy; +import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; import static android.view.WindowManagerGlobal.ADD_OKAY; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; @@ -33,18 +37,27 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import android.app.ActivityManager; import android.app.AppOpsManager; +import android.content.Context; +import android.os.PowerManager; import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.filters.SmallTest; +import com.android.server.LocalServices; import com.android.server.pm.UserManagerInternal; import com.android.server.wm.ActivityTaskManagerInternal; +import com.android.server.wm.DisplayPolicy; +import com.android.server.wm.DisplayRotation; +import com.android.server.wm.WindowManagerInternal; import org.junit.After; import org.junit.Before; @@ -64,16 +77,27 @@ public class PhoneWindowManagerTests { public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); PhoneWindowManager mPhoneWindowManager; + private ActivityTaskManagerInternal mAtmInternal; + private Context mContext; @Before public void setUp() { mPhoneWindowManager = spy(new PhoneWindowManager()); spyOn(ActivityManager.getService()); + mContext = getInstrumentation().getTargetContext(); + spyOn(mContext); + mAtmInternal = mock(ActivityTaskManagerInternal.class); + LocalServices.addService(ActivityTaskManagerInternal.class, mAtmInternal); + mPhoneWindowManager.mActivityTaskManagerInternal = mAtmInternal; + LocalServices.addService(WindowManagerInternal.class, mock(WindowManagerInternal.class)); } @After public void tearDown() { reset(ActivityManager.getService()); + reset(mContext); + LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class); + LocalServices.removeServiceForTest(WindowManagerInternal.class); } @Test @@ -99,6 +123,60 @@ public class PhoneWindowManagerTests { } @Test + public void testScreenTurnedOff() { + mSetFlagsRule.enableFlags(com.android.window.flags.Flags + .FLAG_SKIP_SLEEPING_WHEN_SWITCHING_DISPLAY); + doNothing().when(mPhoneWindowManager).updateSettings(any()); + doNothing().when(mPhoneWindowManager).initializeHdmiState(); + final boolean[] isScreenTurnedOff = { false }; + final DisplayPolicy displayPolicy = mock(DisplayPolicy.class); + doAnswer(invocation -> isScreenTurnedOff[0] = true).when(displayPolicy).screenTurnedOff(); + doAnswer(invocation -> !isScreenTurnedOff[0]).when(displayPolicy).isScreenOnEarly(); + doAnswer(invocation -> !isScreenTurnedOff[0]).when(displayPolicy).isScreenOnFully(); + + mPhoneWindowManager.mDefaultDisplayPolicy = displayPolicy; + mPhoneWindowManager.mDefaultDisplayRotation = mock(DisplayRotation.class); + final ActivityTaskManagerInternal.SleepTokenAcquirer tokenAcquirer = + mock(ActivityTaskManagerInternal.SleepTokenAcquirer.class); + doReturn(tokenAcquirer).when(mAtmInternal).createSleepTokenAcquirer(anyString()); + final PowerManager pm = mock(PowerManager.class); + doReturn(true).when(pm).isInteractive(); + doReturn(pm).when(mContext).getSystemService(eq(Context.POWER_SERVICE)); + + mContext.getMainThreadHandler().runWithScissors(() -> mPhoneWindowManager.init( + new PhoneWindowManager.Injector(mContext, + mock(WindowManagerPolicy.WindowManagerFuncs.class))), 0); + assertThat(isScreenTurnedOff[0]).isFalse(); + assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isFalse(); + + // Skip sleep-token for non-sleep-screen-off. + clearInvocations(tokenAcquirer); + mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */); + verify(tokenAcquirer, never()).acquire(anyInt(), anyBoolean()); + assertThat(isScreenTurnedOff[0]).isTrue(); + + // Apply sleep-token for sleep-screen-off. + mPhoneWindowManager.startedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */); + assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isTrue(); + mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */); + verify(tokenAcquirer).acquire(eq(DEFAULT_DISPLAY), eq(true)); + + mPhoneWindowManager.finishedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */); + assertThat(mPhoneWindowManager.mIsGoingToSleepDefaultDisplay).isFalse(); + + // Simulate unexpected reversed order: screenTurnedOff -> startedGoingToSleep. The sleep + // token can still be acquired. + isScreenTurnedOff[0] = false; + clearInvocations(tokenAcquirer); + mPhoneWindowManager.screenTurnedOff(DEFAULT_DISPLAY, true /* isSwappingDisplay */); + verify(tokenAcquirer, never()).acquire(anyInt(), anyBoolean()); + assertThat(displayPolicy.isScreenOnEarly()).isFalse(); + assertThat(displayPolicy.isScreenOnFully()).isFalse(); + mPhoneWindowManager.startedGoingToSleep(DEFAULT_DISPLAY, 0 /* reason */); + verify(tokenAcquirer).acquire(eq(DEFAULT_DISPLAY), eq(false)); + } + + @Test public void testCheckAddPermission_withoutAccessibilityOverlay_noAccessibilityAppOpLogged() { mSetFlagsRule.enableFlags(android.view.contentprotection.flags.Flags .FLAG_CREATE_ACCESSIBILITY_OVERLAY_APP_OP_ENABLED); @@ -130,11 +208,8 @@ public class PhoneWindowManagerTests { private void mockStartDockOrHome() throws Exception { doNothing().when(ActivityManager.getService()).stopAppSwitches(); - ActivityTaskManagerInternal mMockActivityTaskManagerInternal = - mock(ActivityTaskManagerInternal.class); - when(mMockActivityTaskManagerInternal.startHomeOnDisplay( + when(mAtmInternal.startHomeOnDisplay( anyInt(), anyString(), anyInt(), anyBoolean(), anyBoolean())).thenReturn(false); - mPhoneWindowManager.mActivityTaskManagerInternal = mMockActivityTaskManagerInternal; mPhoneWindowManager.mUserManagerInternal = mock(UserManagerInternal.class); } } diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 4e360d06ce6a..2c88ed2db2d6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -1068,16 +1068,6 @@ public class DisplayContentTests extends WindowTestsBase { mDisplayContent.getImeTarget(IME_TARGET_LAYERING)); } - @SetupWindows(addWindows = W_INPUT_METHOD) - @Test - public void testInputMethodSet_listenOnDisplayAreaConfigurationChanged() { - spyOn(mAtm); - mDisplayContent.setInputMethodWindowLocked(mImeWindow); - - verify(mAtm).onImeWindowSetOnDisplayArea( - mImeWindow.mSession.mPid, mDisplayContent.getImeContainer()); - } - @Test public void testAllowsTopmostFullscreenOrientation() { final DisplayContent dc = createNewDisplay(); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index 897a3da07473..52485eec8505 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -25,7 +25,7 @@ import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_NONE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT; -import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE; +import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE; import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE; import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_BOTTOM_OF_TASK; @@ -1835,7 +1835,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { final TaskFragment tf = createTaskFragment(task); final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( - OP_TYPE_CREATE_TASK_FRAGMENT_DECOR_SURFACE).build(); + OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE).build(); mTransaction.addTaskFragmentOperation(tf.getFragmentToken(), operation); assertApplyTransactionAllowed(mTransaction); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index 12f46df451fe..48b12f729e08 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -90,6 +90,7 @@ import android.util.ArraySet; import android.util.MergedConfiguration; import android.view.ContentRecordingSession; import android.view.IWindow; +import android.view.IWindowSession; import android.view.InputChannel; import android.view.InsetsSourceControl; import android.view.InsetsState; @@ -99,6 +100,7 @@ import android.view.View; import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; +import android.window.ActivityWindowInfo; import android.window.ClientWindowFrames; import android.window.InputTransferToken; import android.window.ScreenCapture; @@ -1216,6 +1218,35 @@ public class WindowManagerServiceTests extends WindowTestsBase { mWm.reportKeepClearAreasChanged(session, window, new ArrayList<>(), new ArrayList<>()); } + @Test + public void testRelayout_appWindowSendActivityWindowInfo() { + mSetFlagsRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG); + + // Skip unnecessary operations of relayout. + spyOn(mWm.mWindowPlacerLocked); + doNothing().when(mWm.mWindowPlacerLocked).performSurfacePlacement(anyBoolean()); + + final Task task = createTask(mDisplayContent); + final WindowState win = createAppWindow(task, ACTIVITY_TYPE_STANDARD, "appWindow"); + mWm.mWindowMap.put(win.mClient.asBinder(), win); + + final int w = 100; + final int h = 200; + final ClientWindowFrames outFrames = new ClientWindowFrames(); + final MergedConfiguration outConfig = new MergedConfiguration(); + final SurfaceControl outSurfaceControl = new SurfaceControl(); + final InsetsState outInsetsState = new InsetsState(); + final InsetsSourceControl.Array outControls = new InsetsSourceControl.Array(); + final Bundle outBundle = new Bundle(); + + mWm.relayoutWindow(win.mSession, win.mClient, win.mAttrs, w, h, View.GONE, 0, 0, 0, + outFrames, outConfig, outSurfaceControl, outInsetsState, outControls, outBundle); + + final ActivityWindowInfo activityWindowInfo = outBundle.getParcelable( + IWindowSession.KEY_RELAYOUT_BUNDLE_ACTIVITY_WINDOW_INFO, ActivityWindowInfo.class); + assertEquals(win.mActivityRecord.getActivityWindowInfo(), activityWindowInfo); + } + class TestResultReceiver implements IResultReceiver { public android.os.Bundle resultData; private final IBinder mBinder = mock(IBinder.class); diff --git a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl index 9441fb5d02ef..36485c6b6fb5 100644 --- a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl +++ b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl @@ -347,28 +347,6 @@ oneway interface ISatellite { in IIntegerConsumer callback); /** - * Request to get whether satellite communication is allowed for the current location. - * - * @param resultCallback The callback to receive the error code result of the operation. - * This must only be sent when the result is not - * SatelliteResult#SATELLITE_RESULT_SUCCESS. - * @param callback If the result is SatelliteResult#SATELLITE_RESULT_SUCCESS, the callback to - * receive whether satellite communication is allowed for the current location. - * - * Valid result codes returned: - * SatelliteResult:SATELLITE_RESULT_SUCCESS - * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR - * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR - * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE - * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS - * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE - * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED - * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES - */ - void requestIsSatelliteCommunicationAllowedForCurrentLocation( - in IIntegerConsumer resultCallback, in IBooleanConsumer callback); - - /** * Request to get the time after which the satellite will be visible. This is an int * representing the duration in seconds after which the satellite will be visible. * This will return 0 if the satellite is currently visible. diff --git a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java index f17ff17497f2..b7dc79ff7283 100644 --- a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java +++ b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java @@ -194,17 +194,6 @@ public class SatelliteImplBase extends SatelliteService { } @Override - public void requestIsSatelliteCommunicationAllowedForCurrentLocation( - IIntegerConsumer resultCallback, IBooleanConsumer callback) - throws RemoteException { - executeMethodAsync( - () -> SatelliteImplBase.this - .requestIsSatelliteCommunicationAllowedForCurrentLocation( - resultCallback, callback), - "requestIsCommunicationAllowedForCurrentLocation"); - } - - @Override public void requestTimeForNextSatelliteVisibility(IIntegerConsumer resultCallback, IIntegerConsumer callback) throws RemoteException { executeMethodAsync( @@ -638,30 +627,6 @@ public class SatelliteImplBase extends SatelliteService { } /** - * Request to get whether satellite communication is allowed for the current location. - * - * @param resultCallback The callback to receive the error code result of the operation. - * This must only be sent when the result is not - * SatelliteResult#SATELLITE_RESULT_SUCCESS. - * @param callback If the result is SatelliteResult#SATELLITE_RESULT_SUCCESS, the callback to - * receive whether satellite communication is allowed for the current location. - * - * Valid result codes returned: - * SatelliteResult:SATELLITE_RESULT_SUCCESS - * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR - * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR - * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE - * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS - * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE - * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED - * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES - */ - public void requestIsSatelliteCommunicationAllowedForCurrentLocation( - @NonNull IIntegerConsumer resultCallback, @NonNull IBooleanConsumer callback) { - // stub implementation - } - - /** * Request to get the time after which the satellite will be visible. This is an int * representing the duration in seconds after which the satellite will be visible. * This will return 0 if the satellite is currently visible. 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 caaee634c57a..4d4827676c74 100644 --- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java +++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlTest.java @@ -30,10 +30,12 @@ import com.android.compatibility.common.util.DisplayUtil; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +@Ignore // b/330376055: Write tests for functionality for both dVRR and MRR devices. @RunWith(AndroidJUnit4.class) public class SurfaceControlTest { private static final String TAG = "SurfaceControlTest"; diff --git a/tests/PackageWatchdog/Android.bp b/tests/PackageWatchdog/Android.bp index e0e6c4c43b16..2c5fdd3228ed 100644 --- a/tests/PackageWatchdog/Android.bp +++ b/tests/PackageWatchdog/Android.bp @@ -28,8 +28,10 @@ android_test { static_libs: [ "junit", "mockito-target-extended-minus-junit4", + "flag-junit", "frameworks-base-testutils", "androidx.test.rules", + "PlatformProperties", "services.core", "services.net", "truth", diff --git a/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java b/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java new file mode 100644 index 000000000000..081da11f2aa8 --- /dev/null +++ b/tests/PackageWatchdog/src/com/android/server/CrashRecoveryTest.java @@ -0,0 +1,644 @@ +/* + * 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.server; + +import static android.service.watchdog.ExplicitHealthCheckService.PackageConfig; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.VersionedPackage; +import android.content.rollback.PackageRollbackInfo; +import android.content.rollback.RollbackInfo; +import android.content.rollback.RollbackManager; +import android.crashrecovery.flags.Flags; +import android.net.ConnectivityModuleConnector; +import android.net.ConnectivityModuleConnector.ConnectivityModuleHealthListener; +import android.os.Handler; +import android.os.SystemProperties; +import android.os.test.TestLooper; +import android.platform.test.flag.junit.SetFlagsRule; +import android.provider.DeviceConfig; +import android.util.AtomicFile; + +import androidx.test.InstrumentationRegistry; + +import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.server.RescueParty.RescuePartyObserver; +import com.android.server.pm.ApexManager; +import com.android.server.rollback.RollbackPackageHealthObserver; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; +import org.mockito.stubbing.Answer; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +/** + * Test CrashRecovery, integration tests that include PackageWatchdog, RescueParty and + * RollbackPackageHealthObserver + */ +public class CrashRecoveryTest { + private static final String PROP_DEVICE_CONFIG_DISABLE_FLAG = + "persist.device_config.configuration.disable_rescue_party"; + + private static final String APP_A = "com.package.a"; + private static final String APP_B = "com.package.b"; + private static final String APP_C = "com.package.c"; + private static final long VERSION_CODE = 1L; + private static final long SHORT_DURATION = TimeUnit.SECONDS.toMillis(1); + + private static final RollbackInfo ROLLBACK_INFO_LOW = getRollbackInfo(APP_A, VERSION_CODE, 1, + PackageManager.ROLLBACK_USER_IMPACT_LOW); + private static final RollbackInfo ROLLBACK_INFO_HIGH = getRollbackInfo(APP_B, VERSION_CODE, 2, + PackageManager.ROLLBACK_USER_IMPACT_HIGH); + private static final RollbackInfo ROLLBACK_INFO_MANUAL = getRollbackInfo(APP_C, VERSION_CODE, 3, + PackageManager.ROLLBACK_USER_IMPACT_ONLY_MANUAL); + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + private final TestClock mTestClock = new TestClock(); + private TestLooper mTestLooper; + private Context mSpyContext; + // Keep track of all created watchdogs to apply device config changes + private List<PackageWatchdog> mAllocatedWatchdogs; + @Mock + private ConnectivityModuleConnector mConnectivityModuleConnector; + @Mock + private PackageManager mMockPackageManager; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private ApexManager mApexManager; + @Mock + RollbackManager mRollbackManager; + // Mock only sysprop apis + private PackageWatchdog.BootThreshold mSpyBootThreshold; + @Captor + private ArgumentCaptor<ConnectivityModuleHealthListener> mConnectivityModuleCallbackCaptor; + private MockitoSession mSession; + private HashMap<String, String> mSystemSettingsMap; + private HashMap<String, String> mCrashRecoveryPropertiesMap; + + @Before + public void setUp() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); + MockitoAnnotations.initMocks(this); + new File(InstrumentationRegistry.getContext().getFilesDir(), + "package-watchdog.xml").delete(); + adoptShellPermissions(Manifest.permission.READ_DEVICE_CONFIG, + Manifest.permission.WRITE_DEVICE_CONFIG); + mTestLooper = new TestLooper(); + mSpyContext = spy(InstrumentationRegistry.getContext()); + when(mSpyContext.getPackageManager()).thenReturn(mMockPackageManager); + when(mMockPackageManager.getPackageInfo(anyString(), anyInt())).then(inv -> { + final PackageInfo res = new PackageInfo(); + res.packageName = inv.getArgument(0); + res.setLongVersionCode(VERSION_CODE); + return res; + }); + mSession = ExtendedMockito.mockitoSession() + .initMocks(this) + .strictness(Strictness.LENIENT) + .spyStatic(SystemProperties.class) + .spyStatic(RescueParty.class) + .startMocking(); + mSystemSettingsMap = new HashMap<>(); + + // Mock SystemProperties setter and various getters + doAnswer((Answer<Void>) invocationOnMock -> { + String key = invocationOnMock.getArgument(0); + String value = invocationOnMock.getArgument(1); + + mSystemSettingsMap.put(key, value); + return null; + } + ).when(() -> SystemProperties.set(anyString(), anyString())); + + doAnswer((Answer<Integer>) invocationOnMock -> { + String key = invocationOnMock.getArgument(0); + int defaultValue = invocationOnMock.getArgument(1); + + String storedValue = mSystemSettingsMap.get(key); + return storedValue == null ? defaultValue : Integer.parseInt(storedValue); + } + ).when(() -> SystemProperties.getInt(anyString(), anyInt())); + + doAnswer((Answer<Long>) invocationOnMock -> { + String key = invocationOnMock.getArgument(0); + long defaultValue = invocationOnMock.getArgument(1); + + String storedValue = mSystemSettingsMap.get(key); + return storedValue == null ? defaultValue : Long.parseLong(storedValue); + } + ).when(() -> SystemProperties.getLong(anyString(), anyLong())); + + doAnswer((Answer<Boolean>) invocationOnMock -> { + String key = invocationOnMock.getArgument(0); + boolean defaultValue = invocationOnMock.getArgument(1); + + String storedValue = mSystemSettingsMap.get(key); + return storedValue == null ? defaultValue : Boolean.parseBoolean(storedValue); + } + ).when(() -> SystemProperties.getBoolean(anyString(), anyBoolean())); + + SystemProperties.set(RescueParty.PROP_ENABLE_RESCUE, Boolean.toString(true)); + SystemProperties.set(PROP_DEVICE_CONFIG_DISABLE_FLAG, Boolean.toString(false)); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK, + PackageWatchdog.PROPERTY_WATCHDOG_EXPLICIT_HEALTH_CHECK_ENABLED, + Boolean.toString(true), false); + + DeviceConfig.setProperty(DeviceConfig.NAMESPACE_ROLLBACK, + PackageWatchdog.PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT, + Integer.toString(PackageWatchdog.DEFAULT_TRIGGER_FAILURE_COUNT), false); + + mAllocatedWatchdogs = new ArrayList<>(); + RescuePartyObserver.reset(); + } + + @After + public void tearDown() throws Exception { + dropShellPermissions(); + mSession.finishMocking(); + // Clean up listeners since too many listeners will delay notifications significantly + for (PackageWatchdog watchdog : mAllocatedWatchdogs) { + watchdog.removePropertyChangedListener(); + } + mAllocatedWatchdogs.clear(); + } + + @Test + public void testBootLoopWithRescueParty() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog); + + verify(rescuePartyObserver, never()).executeBootLoopMitigation(1); + int bootCounter = 0; + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) { + watchdog.noteBoot(); + bootCounter += 1; + } + verify(rescuePartyObserver).executeBootLoopMitigation(1); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(2); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + bootCounter += 1; + } + verify(rescuePartyObserver).executeBootLoopMitigation(2); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(3); + + int bootLoopThreshold = PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - bootCounter; + for (int i = 0; i < bootLoopThreshold; i++) { + watchdog.noteBoot(); + } + verify(rescuePartyObserver).executeBootLoopMitigation(3); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(4); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + } + verify(rescuePartyObserver).executeBootLoopMitigation(4); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(5); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + } + verify(rescuePartyObserver).executeBootLoopMitigation(5); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(6); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + } + verify(rescuePartyObserver).executeBootLoopMitigation(6); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(7); + } + + @Test + public void testBootLoopWithRollbackPackageHealthObserver() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + RollbackPackageHealthObserver rollbackObserver = + setUpRollbackPackageHealthObserver(watchdog); + + verify(rollbackObserver, never()).executeBootLoopMitigation(1); + int bootCounter = 0; + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) { + watchdog.noteBoot(); + bootCounter += 1; + } + verify(rollbackObserver).executeBootLoopMitigation(1); + verify(rollbackObserver, never()).executeBootLoopMitigation(2); + + // Update the list of available rollbacks after executing bootloop mitigation once + when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_HIGH, + ROLLBACK_INFO_MANUAL)); + + int bootLoopThreshold = PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - bootCounter; + for (int i = 0; i < bootLoopThreshold; i++) { + watchdog.noteBoot(); + } + verify(rollbackObserver).executeBootLoopMitigation(2); + verify(rollbackObserver, never()).executeBootLoopMitigation(3); + + // Update the list of available rollbacks after executing bootloop mitigation once + when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_MANUAL)); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + } + verify(rollbackObserver, never()).executeBootLoopMitigation(3); + } + + @Test + public void testBootLoopWithRescuePartyAndRollbackPackageHealthObserver() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + RescuePartyObserver rescuePartyObserver = setUpRescuePartyObserver(watchdog); + RollbackPackageHealthObserver rollbackObserver = + setUpRollbackPackageHealthObserver(watchdog); + + verify(rescuePartyObserver, never()).executeBootLoopMitigation(1); + verify(rollbackObserver, never()).executeBootLoopMitigation(1); + int bootCounter = 0; + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT; i++) { + watchdog.noteBoot(); + bootCounter += 1; + } + verify(rescuePartyObserver).executeBootLoopMitigation(1); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(2); + verify(rollbackObserver, never()).executeBootLoopMitigation(1); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + bootCounter += 1; + } + verify(rescuePartyObserver).executeBootLoopMitigation(2); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(3); + verify(rollbackObserver, never()).executeBootLoopMitigation(2); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + bootCounter += 1; + } + verify(rescuePartyObserver, never()).executeBootLoopMitigation(3); + verify(rollbackObserver).executeBootLoopMitigation(1); + verify(rollbackObserver, never()).executeBootLoopMitigation(2); + // Update the list of available rollbacks after executing bootloop mitigation once + when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_HIGH, + ROLLBACK_INFO_MANUAL)); + + int bootLoopThreshold = PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - bootCounter; + for (int i = 0; i < bootLoopThreshold; i++) { + watchdog.noteBoot(); + } + verify(rescuePartyObserver).executeBootLoopMitigation(3); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(4); + verify(rollbackObserver, never()).executeBootLoopMitigation(2); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + } + verify(rescuePartyObserver).executeBootLoopMitigation(4); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(5); + verify(rollbackObserver, never()).executeBootLoopMitigation(2); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + } + verify(rescuePartyObserver).executeBootLoopMitigation(5); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(6); + verify(rollbackObserver, never()).executeBootLoopMitigation(2); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + } + verify(rescuePartyObserver, never()).executeBootLoopMitigation(6); + verify(rollbackObserver).executeBootLoopMitigation(2); + verify(rollbackObserver, never()).executeBootLoopMitigation(3); + // Update the list of available rollbacks after executing bootloop mitigation + when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_MANUAL)); + + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; i++) { + watchdog.noteBoot(); + } + verify(rescuePartyObserver).executeBootLoopMitigation(6); + verify(rescuePartyObserver, never()).executeBootLoopMitigation(7); + verify(rollbackObserver, never()).executeBootLoopMitigation(3); + } + + RollbackPackageHealthObserver setUpRollbackPackageHealthObserver(PackageWatchdog watchdog) { + RollbackPackageHealthObserver rollbackObserver = + spy(new RollbackPackageHealthObserver(mSpyContext, mApexManager)); + when(mSpyContext.getSystemService(RollbackManager.class)).thenReturn(mRollbackManager); + when(mRollbackManager.getAvailableRollbacks()).thenReturn(List.of(ROLLBACK_INFO_LOW, + ROLLBACK_INFO_HIGH, ROLLBACK_INFO_MANUAL)); + when(mSpyContext.getPackageManager()).thenReturn(mMockPackageManager); + + watchdog.registerHealthObserver(rollbackObserver); + return rollbackObserver; + } + + RescuePartyObserver setUpRescuePartyObserver(PackageWatchdog watchdog) { + setCrashRecoveryPropRescueBootCount(0); + RescuePartyObserver rescuePartyObserver = spy(RescuePartyObserver.getInstance(mSpyContext)); + assertFalse(RescueParty.isRebootPropertySet()); + watchdog.registerHealthObserver(rescuePartyObserver); + return rescuePartyObserver; + } + + private static RollbackInfo getRollbackInfo(String packageName, long versionCode, + int rollbackId, int rollbackUserImpact) { + VersionedPackage appFrom = new VersionedPackage(packageName, versionCode + 1); + VersionedPackage appTo = new VersionedPackage(packageName, versionCode); + PackageRollbackInfo packageRollbackInfo = new PackageRollbackInfo(appFrom, appTo, null, + null, false, false, null); + RollbackInfo rollbackInfo = new RollbackInfo(rollbackId, List.of(packageRollbackInfo), + false, null, 111, rollbackUserImpact); + return rollbackInfo; + } + + private void adoptShellPermissions(String... permissions) { + androidx.test.platform.app.InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .adoptShellPermissionIdentity(permissions); + } + + private void dropShellPermissions() { + androidx.test.platform.app.InstrumentationRegistry + .getInstrumentation() + .getUiAutomation() + .dropShellPermissionIdentity(); + } + + + private PackageWatchdog createWatchdog() { + return createWatchdog(new TestController(), true /* withPackagesReady */); + } + + private PackageWatchdog createWatchdog(TestController controller, boolean withPackagesReady) { + AtomicFile policyFile = + new AtomicFile(new File(mSpyContext.getFilesDir(), "package-watchdog.xml")); + Handler handler = new Handler(mTestLooper.getLooper()); + PackageWatchdog watchdog = + new PackageWatchdog(mSpyContext, policyFile, handler, handler, controller, + mConnectivityModuleConnector, mTestClock); + mockCrashRecoveryProperties(watchdog); + + // Verify controller is not automatically started + assertThat(controller.mIsEnabled).isFalse(); + if (withPackagesReady) { + // Only capture the NetworkStack callback for the latest registered watchdog + reset(mConnectivityModuleConnector); + watchdog.onPackagesReady(); + // Verify controller by default is started when packages are ready + assertThat(controller.mIsEnabled).isTrue(); + + verify(mConnectivityModuleConnector).registerHealthListener( + mConnectivityModuleCallbackCaptor.capture()); + } + mAllocatedWatchdogs.add(watchdog); + return watchdog; + } + + // Mock CrashRecoveryProperties as they cannot be accessed due to SEPolicy restrictions + private void mockCrashRecoveryProperties(PackageWatchdog watchdog) { + mCrashRecoveryPropertiesMap = new HashMap<>(); + + // mock properties in RescueParty + try { + + doAnswer((Answer<Boolean>) invocationOnMock -> { + String storedValue = mCrashRecoveryPropertiesMap + .getOrDefault("crashrecovery.attempting_factory_reset", "false"); + return Boolean.parseBoolean(storedValue); + }).when(() -> RescueParty.isFactoryResetPropertySet()); + doAnswer((Answer<Void>) invocationOnMock -> { + boolean value = invocationOnMock.getArgument(0); + mCrashRecoveryPropertiesMap.put("crashrecovery.attempting_factory_reset", + Boolean.toString(value)); + return null; + }).when(() -> RescueParty.setFactoryResetProperty(anyBoolean())); + + doAnswer((Answer<Boolean>) invocationOnMock -> { + String storedValue = mCrashRecoveryPropertiesMap + .getOrDefault("crashrecovery.attempting_reboot", "false"); + return Boolean.parseBoolean(storedValue); + }).when(() -> RescueParty.isRebootPropertySet()); + doAnswer((Answer<Void>) invocationOnMock -> { + boolean value = invocationOnMock.getArgument(0); + setCrashRecoveryPropAttemptingReboot(value); + return null; + }).when(() -> RescueParty.setRebootProperty(anyBoolean())); + + doAnswer((Answer<Long>) invocationOnMock -> { + String storedValue = mCrashRecoveryPropertiesMap + .getOrDefault("persist.crashrecovery.last_factory_reset", "0"); + return Long.parseLong(storedValue); + }).when(() -> RescueParty.getLastFactoryResetTimeMs()); + doAnswer((Answer<Void>) invocationOnMock -> { + long value = invocationOnMock.getArgument(0); + setCrashRecoveryPropLastFactoryReset(value); + return null; + }).when(() -> RescueParty.setLastFactoryResetTimeMs(anyLong())); + + doAnswer((Answer<Integer>) invocationOnMock -> { + String storedValue = mCrashRecoveryPropertiesMap + .getOrDefault("crashrecovery.max_rescue_level_attempted", "0"); + return Integer.parseInt(storedValue); + }).when(() -> RescueParty.getMaxRescueLevelAttempted()); + doAnswer((Answer<Void>) invocationOnMock -> { + int value = invocationOnMock.getArgument(0); + mCrashRecoveryPropertiesMap.put("crashrecovery.max_rescue_level_attempted", + Integer.toString(value)); + return null; + }).when(() -> RescueParty.setMaxRescueLevelAttempted(anyInt())); + + } catch (Exception e) { + // tests will fail, just printing the error + System.out.println("Error while mocking crashrecovery properties " + e.getMessage()); + } + + try { + if (Flags.recoverabilityDetection()) { + mSpyBootThreshold = spy(watchdog.new BootThreshold( + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS, + PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT)); + } else { + mSpyBootThreshold = spy(watchdog.new BootThreshold( + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS)); + } + + doAnswer((Answer<Integer>) invocationOnMock -> { + String storedValue = mCrashRecoveryPropertiesMap + .getOrDefault("crashrecovery.rescue_boot_count", "0"); + return Integer.parseInt(storedValue); + }).when(mSpyBootThreshold).getCount(); + doAnswer((Answer<Void>) invocationOnMock -> { + int count = invocationOnMock.getArgument(0); + mCrashRecoveryPropertiesMap.put("crashrecovery.rescue_boot_count", + Integer.toString(count)); + return null; + }).when(mSpyBootThreshold).setCount(anyInt()); + + doAnswer((Answer<Integer>) invocationOnMock -> { + String storedValue = mCrashRecoveryPropertiesMap + .getOrDefault("crashrecovery.boot_mitigation_count", "0"); + return Integer.parseInt(storedValue); + }).when(mSpyBootThreshold).getMitigationCount(); + doAnswer((Answer<Void>) invocationOnMock -> { + int count = invocationOnMock.getArgument(0); + mCrashRecoveryPropertiesMap.put("crashrecovery.boot_mitigation_count", + Integer.toString(count)); + return null; + }).when(mSpyBootThreshold).setMitigationCount(anyInt()); + + doAnswer((Answer<Long>) invocationOnMock -> { + String storedValue = mCrashRecoveryPropertiesMap + .getOrDefault("crashrecovery.rescue_boot_start", "0"); + return Long.parseLong(storedValue); + }).when(mSpyBootThreshold).getStart(); + doAnswer((Answer<Void>) invocationOnMock -> { + long count = invocationOnMock.getArgument(0); + mCrashRecoveryPropertiesMap.put("crashrecovery.rescue_boot_start", + Long.toString(count)); + return null; + }).when(mSpyBootThreshold).setStart(anyLong()); + + doAnswer((Answer<Long>) invocationOnMock -> { + String storedValue = mCrashRecoveryPropertiesMap + .getOrDefault("crashrecovery.boot_mitigation_start", "0"); + return Long.parseLong(storedValue); + }).when(mSpyBootThreshold).getMitigationStart(); + doAnswer((Answer<Void>) invocationOnMock -> { + long count = invocationOnMock.getArgument(0); + mCrashRecoveryPropertiesMap.put("crashrecovery.boot_mitigation_start", + Long.toString(count)); + return null; + }).when(mSpyBootThreshold).setMitigationStart(anyLong()); + + Field mBootThresholdField = watchdog.getClass().getDeclaredField("mBootThreshold"); + mBootThresholdField.setAccessible(true); + mBootThresholdField.set(watchdog, mSpyBootThreshold); + } catch (Exception e) { + // tests will fail, just printing the error + System.out.println("Error detected while spying BootThreshold" + e.getMessage()); + } + } + + private void setCrashRecoveryPropRescueBootCount(int count) { + mCrashRecoveryPropertiesMap.put("crashrecovery.rescue_boot_count", + Integer.toString(count)); + } + + private void setCrashRecoveryPropAttemptingReboot(boolean value) { + mCrashRecoveryPropertiesMap.put("crashrecovery.attempting_reboot", + Boolean.toString(value)); + } + + private void setCrashRecoveryPropLastFactoryReset(long value) { + mCrashRecoveryPropertiesMap.put("persist.crashrecovery.last_factory_reset", + Long.toString(value)); + } + + private static class TestController extends ExplicitHealthCheckController { + TestController() { + super(null /* controller */); + } + + private boolean mIsEnabled; + private List<String> mSupportedPackages = new ArrayList<>(); + private List<String> mRequestedPackages = new ArrayList<>(); + private Consumer<List<PackageConfig>> mSupportedConsumer; + private List<Set> mSyncRequests = new ArrayList<>(); + + @Override + public void setEnabled(boolean enabled) { + mIsEnabled = enabled; + if (!mIsEnabled) { + mSupportedPackages.clear(); + } + } + + @Override + public void setCallbacks(Consumer<String> passedConsumer, + Consumer<List<PackageConfig>> supportedConsumer, Runnable notifySyncRunnable) { + mSupportedConsumer = supportedConsumer; + } + + @Override + public void syncRequests(Set<String> packages) { + mSyncRequests.add(packages); + mRequestedPackages.clear(); + if (mIsEnabled) { + packages.retainAll(mSupportedPackages); + mRequestedPackages.addAll(packages); + List<PackageConfig> packageConfigs = new ArrayList<>(); + for (String packageName: packages) { + packageConfigs.add(new PackageConfig(packageName, SHORT_DURATION)); + } + mSupportedConsumer.accept(packageConfigs); + } else { + mSupportedConsumer.accept(Collections.emptyList()); + } + } + } + + private static class TestClock implements PackageWatchdog.SystemClock { + // Note 0 is special to the internal clock of PackageWatchdog. We need to start from + // a non-zero value in order not to disrupt the logic of PackageWatchdog. + private long mUpTimeMillis = 1; + @Override + public long uptimeMillis() { + return mUpTimeMillis; + } + } +} diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java index 75284c712bd2..4f27e06083ba 100644 --- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java +++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java @@ -36,11 +36,13 @@ import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; +import android.crashrecovery.flags.Flags; import android.net.ConnectivityModuleConnector; import android.net.ConnectivityModuleConnector.ConnectivityModuleHealthListener; import android.os.Handler; import android.os.SystemProperties; import android.os.test.TestLooper; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.DeviceConfig; import android.util.AtomicFile; import android.util.Xml; @@ -54,11 +56,13 @@ import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.PackageWatchdog.HealthCheckState; import com.android.server.PackageWatchdog.MonitoredPackage; +import com.android.server.PackageWatchdog.ObserverInternal; import com.android.server.PackageWatchdog.PackageHealthObserver; import com.android.server.PackageWatchdog.PackageHealthObserverImpact; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; @@ -99,6 +103,10 @@ public class PackageWatchdogTest { private static final String OBSERVER_NAME_4 = "observer4"; private static final long SHORT_DURATION = TimeUnit.SECONDS.toMillis(1); private static final long LONG_DURATION = TimeUnit.SECONDS.toMillis(5); + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private final TestClock mTestClock = new TestClock(); private TestLooper mTestLooper; private Context mSpyContext; @@ -128,6 +136,7 @@ public class PackageWatchdogTest { @Before public void setUp() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); MockitoAnnotations.initMocks(this); new File(InstrumentationRegistry.getContext().getFilesDir(), "package-watchdog.xml").delete(); @@ -444,6 +453,7 @@ public class PackageWatchdogTest { */ @Test public void testPackageFailureNotifyAllDifferentImpacts() throws Exception { + mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); PackageWatchdog watchdog = createWatchdog(); TestObserver observerNone = new TestObserver(OBSERVER_NAME_1, PackageHealthObserverImpact.USER_IMPACT_LEVEL_0); @@ -488,6 +498,52 @@ public class PackageWatchdogTest { assertThat(observerLowPackages).containsExactly(APP_A); } + @Test + public void testPackageFailureNotifyAllDifferentImpactsRecoverability() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observerNone = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_0); + TestObserver observerHigh = new TestObserver(OBSERVER_NAME_2, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_50); + TestObserver observerMid = new TestObserver(OBSERVER_NAME_3, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_30); + TestObserver observerLow = new TestObserver(OBSERVER_NAME_4, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_10); + + // Start observing for all impact observers + watchdog.startObservingHealth(observerNone, Arrays.asList(APP_A, APP_B, APP_C, APP_D), + SHORT_DURATION); + watchdog.startObservingHealth(observerHigh, Arrays.asList(APP_A, APP_B, APP_C), + SHORT_DURATION); + watchdog.startObservingHealth(observerMid, Arrays.asList(APP_A, APP_B), + SHORT_DURATION); + watchdog.startObservingHealth(observerLow, Arrays.asList(APP_A), + SHORT_DURATION); + + // Then fail all apps above the threshold + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE), + new VersionedPackage(APP_B, VERSION_CODE), + new VersionedPackage(APP_C, VERSION_CODE), + new VersionedPackage(APP_D, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + + // Verify least impact observers are notifed of package failures + List<String> observerNonePackages = observerNone.mMitigatedPackages; + List<String> observerHighPackages = observerHigh.mMitigatedPackages; + List<String> observerMidPackages = observerMid.mMitigatedPackages; + List<String> observerLowPackages = observerLow.mMitigatedPackages; + + // APP_D failure observed by only observerNone is not caught cos its impact is none + assertThat(observerNonePackages).isEmpty(); + // APP_C failure is caught by observerHigh cos it's the lowest impact observer + assertThat(observerHighPackages).containsExactly(APP_C); + // APP_B failure is caught by observerMid cos it's the lowest impact observer + assertThat(observerMidPackages).containsExactly(APP_B); + // APP_A failure is caught by observerLow cos it's the lowest impact observer + assertThat(observerLowPackages).containsExactly(APP_A); + } + /** * Test package failure and least impact observers are notified successively. * State transistions: @@ -501,6 +557,7 @@ public class PackageWatchdogTest { */ @Test public void testPackageFailureNotifyLeastImpactSuccessively() throws Exception { + mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); PackageWatchdog watchdog = createWatchdog(); TestObserver observerFirst = new TestObserver(OBSERVER_NAME_1, PackageHealthObserverImpact.USER_IMPACT_LEVEL_10); @@ -563,11 +620,76 @@ public class PackageWatchdogTest { assertThat(observerSecond.mMitigatedPackages).isEmpty(); } + @Test + public void testPackageFailureNotifyLeastImpactSuccessivelyRecoverability() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observerFirst = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_10); + TestObserver observerSecond = new TestObserver(OBSERVER_NAME_2, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_30); + + // Start observing for observerFirst and observerSecond with failure handling + watchdog.startObservingHealth(observerFirst, Arrays.asList(APP_A), LONG_DURATION); + watchdog.startObservingHealth(observerSecond, Arrays.asList(APP_A), LONG_DURATION); + + // Then fail APP_A above the threshold + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + + // Verify only observerFirst is notifed + assertThat(observerFirst.mMitigatedPackages).containsExactly(APP_A); + assertThat(observerSecond.mMitigatedPackages).isEmpty(); + + // After observerFirst handles failure, next action it has is high impact + observerFirst.mImpact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_50; + observerFirst.mMitigatedPackages.clear(); + observerSecond.mMitigatedPackages.clear(); + + // Then fail APP_A again above the threshold + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + + // Verify only observerSecond is notifed cos it has least impact + assertThat(observerSecond.mMitigatedPackages).containsExactly(APP_A); + assertThat(observerFirst.mMitigatedPackages).isEmpty(); + + // After observerSecond handles failure, it has no further actions + observerSecond.mImpact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + observerFirst.mMitigatedPackages.clear(); + observerSecond.mMitigatedPackages.clear(); + + // Then fail APP_A again above the threshold + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + + // Verify only observerFirst is notifed cos it has the only action + assertThat(observerFirst.mMitigatedPackages).containsExactly(APP_A); + assertThat(observerSecond.mMitigatedPackages).isEmpty(); + + // After observerFirst handles failure, it too has no further actions + observerFirst.mImpact = PackageHealthObserverImpact.USER_IMPACT_LEVEL_0; + observerFirst.mMitigatedPackages.clear(); + observerSecond.mMitigatedPackages.clear(); + + // Then fail APP_A again above the threshold + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + + // Verify no observer is notified cos no actions left + assertThat(observerFirst.mMitigatedPackages).isEmpty(); + assertThat(observerSecond.mMitigatedPackages).isEmpty(); + } + /** * Test package failure and notifies only one observer even with observer impact tie. */ @Test public void testPackageFailureNotifyOneSameImpact() throws Exception { + mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); PackageWatchdog watchdog = createWatchdog(); TestObserver observer1 = new TestObserver(OBSERVER_NAME_1, PackageHealthObserverImpact.USER_IMPACT_LEVEL_100); @@ -588,6 +710,28 @@ public class PackageWatchdogTest { assertThat(observer2.mMitigatedPackages).isEmpty(); } + @Test + public void testPackageFailureNotifyOneSameImpactRecoverabilityDetection() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + TestObserver observer1 = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_50); + TestObserver observer2 = new TestObserver(OBSERVER_NAME_2, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_50); + + // Start observing for observer1 and observer2 with failure handling + watchdog.startObservingHealth(observer2, Arrays.asList(APP_A), SHORT_DURATION); + watchdog.startObservingHealth(observer1, Arrays.asList(APP_A), SHORT_DURATION); + + // Then fail APP_A above the threshold + raiseFatalFailureAndDispatch(watchdog, + Arrays.asList(new VersionedPackage(APP_A, VERSION_CODE)), + PackageWatchdog.FAILURE_REASON_UNKNOWN); + + // Verify only one observer is notifed + assertThat(observer1.mMitigatedPackages).containsExactly(APP_A); + assertThat(observer2.mMitigatedPackages).isEmpty(); + } + /** * Test package passing explicit health checks does not fail and vice versa. */ @@ -818,6 +962,7 @@ public class PackageWatchdogTest { @Test public void testNetworkStackFailure() { + mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); final PackageWatchdog wd = createWatchdog(); // Start observing with failure handling @@ -835,6 +980,25 @@ public class PackageWatchdogTest { assertThat(observer.mMitigatedPackages).containsExactly(APP_A); } + @Test + public void testNetworkStackFailureRecoverabilityDetection() { + final PackageWatchdog wd = createWatchdog(); + + // Start observing with failure handling + TestObserver observer = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_100); + wd.startObservingHealth(observer, Collections.singletonList(APP_A), SHORT_DURATION); + + // Notify of NetworkStack failure + mConnectivityModuleCallbackCaptor.getValue().onNetworkStackFailure(APP_A); + + // Run handler so package failures are dispatched to observers + mTestLooper.dispatchAll(); + + // Verify the NetworkStack observer is notified + assertThat(observer.mMitigatedPackages).isEmpty(); + } + /** Test default values are used when device property is invalid. */ @Test public void testInvalidConfig_watchdogTriggerFailureCount() { @@ -1045,6 +1209,7 @@ public class PackageWatchdogTest { /** Ensure that boot loop mitigation is done when the number of boots meets the threshold. */ @Test public void testBootLoopDetection_meetsThreshold() { + mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); PackageWatchdog watchdog = createWatchdog(); TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1); watchdog.registerHealthObserver(bootObserver); @@ -1054,6 +1219,16 @@ public class PackageWatchdogTest { assertThat(bootObserver.mitigatedBootLoop()).isTrue(); } + @Test + public void testBootLoopDetection_meetsThresholdRecoverability() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1); + watchdog.registerHealthObserver(bootObserver); + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD; i++) { + watchdog.noteBoot(); + } + assertThat(bootObserver.mitigatedBootLoop()).isTrue(); + } /** * Ensure that boot loop mitigation is not done when the number of boots does not meet the @@ -1071,10 +1246,43 @@ public class PackageWatchdogTest { } /** + * Ensure that boot loop mitigation is not done when the number of boots does not meet the + * threshold. + */ + @Test + public void testBootLoopDetection_doesNotMeetThresholdRecoverabilityLowImpact() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_30); + watchdog.registerHealthObserver(bootObserver); + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT - 1; i++) { + watchdog.noteBoot(); + } + assertThat(bootObserver.mitigatedBootLoop()).isFalse(); + } + + /** + * Ensure that boot loop mitigation is not done when the number of boots does not meet the + * threshold. + */ + @Test + public void testBootLoopDetection_doesNotMeetThresholdRecoverabilityHighImpact() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_80); + watchdog.registerHealthObserver(bootObserver); + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - 1; i++) { + watchdog.noteBoot(); + } + assertThat(bootObserver.mitigatedBootLoop()).isFalse(); + } + + /** * Ensure that boot loop mitigation is done for the observer with the lowest user impact */ @Test public void testBootLoopMitigationDoneForLowestUserImpact() { + mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); PackageWatchdog watchdog = createWatchdog(); TestObserver bootObserver1 = new TestObserver(OBSERVER_NAME_1); bootObserver1.setImpact(PackageHealthObserverImpact.USER_IMPACT_LEVEL_10); @@ -1089,11 +1297,28 @@ public class PackageWatchdogTest { assertThat(bootObserver2.mitigatedBootLoop()).isFalse(); } + @Test + public void testBootLoopMitigationDoneForLowestUserImpactRecoverability() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver bootObserver1 = new TestObserver(OBSERVER_NAME_1); + bootObserver1.setImpact(PackageHealthObserverImpact.USER_IMPACT_LEVEL_10); + TestObserver bootObserver2 = new TestObserver(OBSERVER_NAME_2); + bootObserver2.setImpact(PackageHealthObserverImpact.USER_IMPACT_LEVEL_30); + watchdog.registerHealthObserver(bootObserver1); + watchdog.registerHealthObserver(bootObserver2); + for (int i = 0; i < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD; i++) { + watchdog.noteBoot(); + } + assertThat(bootObserver1.mitigatedBootLoop()).isTrue(); + assertThat(bootObserver2.mitigatedBootLoop()).isFalse(); + } + /** * Ensure that the correct mitigation counts are sent to the boot loop observer. */ @Test public void testMultipleBootLoopMitigation() { + mSetFlagsRule.disableFlags(Flags.FLAG_RECOVERABILITY_DETECTION); PackageWatchdog watchdog = createWatchdog(); TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1); watchdog.registerHealthObserver(bootObserver); @@ -1114,6 +1339,64 @@ public class PackageWatchdogTest { assertThat(bootObserver.mBootMitigationCounts).isEqualTo(List.of(1, 2, 3, 4, 1, 2, 3, 4)); } + @Test + public void testMultipleBootLoopMitigationRecoverabilityLowImpact() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_30); + watchdog.registerHealthObserver(bootObserver); + for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT - 1; j++) { + watchdog.noteBoot(); + } + for (int i = 0; i < 4; i++) { + for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; j++) { + watchdog.noteBoot(); + } + } + + moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS + 1); + + for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT - 1; j++) { + watchdog.noteBoot(); + } + for (int i = 0; i < 4; i++) { + for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; j++) { + watchdog.noteBoot(); + } + } + + assertThat(bootObserver.mBootMitigationCounts).isEqualTo(List.of(1, 2, 3, 4, 1, 2, 3, 4)); + } + + @Test + public void testMultipleBootLoopMitigationRecoverabilityHighImpact() { + PackageWatchdog watchdog = createWatchdog(); + TestObserver bootObserver = new TestObserver(OBSERVER_NAME_1, + PackageHealthObserverImpact.USER_IMPACT_LEVEL_80); + watchdog.registerHealthObserver(bootObserver); + for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - 1; j++) { + watchdog.noteBoot(); + } + for (int i = 0; i < 4; i++) { + for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; j++) { + watchdog.noteBoot(); + } + } + + moveTimeForwardAndDispatch(PackageWatchdog.DEFAULT_DEESCALATION_WINDOW_MS + 1); + + for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_THRESHOLD - 1; j++) { + watchdog.noteBoot(); + } + for (int i = 0; i < 4; i++) { + for (int j = 0; j < PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT; j++) { + watchdog.noteBoot(); + } + } + + assertThat(bootObserver.mBootMitigationCounts).isEqualTo(List.of(1, 2, 3, 4, 1, 2, 3, 4)); + } + /** * Ensure that passing a null list of failed packages does not cause any mitigation logic to * execute. @@ -1304,6 +1587,78 @@ public class PackageWatchdogTest { } /** + * Ensure that a {@link ObserverInternal} may be correctly written and read in order to persist + * across reboots. + */ + @Test + @SuppressWarnings("GuardedBy") + public void testWritingAndReadingObserverInternalRecoverability() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + + LongArrayQueue mitigationCalls = new LongArrayQueue(); + mitigationCalls.addLast(1000); + mitigationCalls.addLast(2000); + mitigationCalls.addLast(3000); + MonitoredPackage writePkg = watchdog.newMonitoredPackage( + "test.package", 1000, 2000, true, mitigationCalls); + final int bootMitigationCount = 4; + ObserverInternal writeObserver = new ObserverInternal("test", List.of(writePkg), + bootMitigationCount); + + // Write the observer + File tmpFile = File.createTempFile("observer-watchdog-test", ".xml"); + AtomicFile testFile = new AtomicFile(tmpFile); + FileOutputStream stream = testFile.startWrite(); + TypedXmlSerializer outputSerializer = Xml.resolveSerializer(stream); + outputSerializer.startDocument(null, true); + writeObserver.writeLocked(outputSerializer); + outputSerializer.endDocument(); + testFile.finishWrite(stream); + + // Read the observer + TypedXmlPullParser parser = Xml.resolvePullParser(testFile.openRead()); + XmlUtils.beginDocument(parser, "observer"); + ObserverInternal readObserver = ObserverInternal.read(parser, watchdog); + + assertThat(readObserver.name).isEqualTo(writeObserver.name); + assertThat(readObserver.getBootMitigationCount()).isEqualTo(bootMitigationCount); + } + + /** + * Ensure that boot mitigation counts may be correctly written and read as metadata + * in order to persist across reboots. + */ + @Test + @SuppressWarnings("GuardedBy") + public void testWritingAndReadingMetadataBootMitigationCountRecoverability() throws Exception { + PackageWatchdog watchdog = createWatchdog(); + String filePath = InstrumentationRegistry.getContext().getFilesDir().toString() + + "metadata_file.txt"; + + ObserverInternal observer1 = new ObserverInternal("test1", List.of(), 1); + ObserverInternal observer2 = new ObserverInternal("test2", List.of(), 2); + watchdog.registerObserverInternal(observer1); + watchdog.registerObserverInternal(observer2); + + mSpyBootThreshold = spy(watchdog.new BootThreshold( + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS, + PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT)); + + watchdog.saveAllObserversBootMitigationCountToMetadata(filePath); + + observer1.setBootMitigationCount(0); + observer2.setBootMitigationCount(0); + assertThat(observer1.getBootMitigationCount()).isEqualTo(0); + assertThat(observer2.getBootMitigationCount()).isEqualTo(0); + + mSpyBootThreshold.readAllObserversBootMitigationCountIfNecessary(filePath); + + assertThat(observer1.getBootMitigationCount()).isEqualTo(1); + assertThat(observer2.getBootMitigationCount()).isEqualTo(2); + } + + /** * Tests device config changes are propagated correctly. */ @Test @@ -1440,11 +1795,19 @@ public class PackageWatchdogTest { // Mock CrashRecoveryProperties as they cannot be accessed due to SEPolicy restrictions private void mockCrashRecoveryProperties(PackageWatchdog watchdog) { + mCrashRecoveryPropertiesMap = new HashMap<>(); + try { - mSpyBootThreshold = spy(watchdog.new BootThreshold( - PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT, - PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS)); - mCrashRecoveryPropertiesMap = new HashMap<>(); + if (Flags.recoverabilityDetection()) { + mSpyBootThreshold = spy(watchdog.new BootThreshold( + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS, + PackageWatchdog.DEFAULT_BOOT_LOOP_MITIGATION_INCREMENT)); + } else { + mSpyBootThreshold = spy(watchdog.new BootThreshold( + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_COUNT, + PackageWatchdog.DEFAULT_BOOT_LOOP_TRIGGER_WINDOW_MS)); + } doAnswer((Answer<Integer>) invocationOnMock -> { String storedValue = mCrashRecoveryPropertiesMap diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp b/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp index 0e0d212efcf1..8d05a974dc40 100644 --- a/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp +++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp @@ -26,11 +26,6 @@ android_test { "platform-test-annotations", "platform-test-rules", "truth", - - // beadstead - "Nene", - "Harrier", - "TestApp", ], test_suites: [ "general-tests", diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java index 867c0a6e8a02..b66ceba458ac 100644 --- a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java +++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java @@ -23,20 +23,14 @@ import android.content.pm.PackageManager; import androidx.test.platform.app.InstrumentationRegistry; -import com.android.bedstead.harrier.BedsteadJUnit4; -import com.android.bedstead.harrier.DeviceState; - import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; -@RunWith(BedsteadJUnit4.class) +@RunWith(JUnit4.class) public final class ConcurrentMultiUserTest { - @Rule - public static final DeviceState sDeviceState = new DeviceState(); - @Before public void doBeforeEachTest() { // No op diff --git a/tools/app_metadata_bundles/Android.bp b/tools/app_metadata_bundles/Android.bp new file mode 100644 index 000000000000..be6bea6b7fea --- /dev/null +++ b/tools/app_metadata_bundles/Android.bp @@ -0,0 +1,26 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library_host { + name: "asllib", + srcs: [ + "src/lib/java/**/*.java", + ], +} + +java_binary_host { + name: "aslgen", + manifest: "src/aslgen/aslgen.mf", + srcs: [ + "src/aslgen/java/**/*.java", + ], + static_libs: [ + "asllib", + ], +} diff --git a/tools/app_metadata_bundles/OWNERS b/tools/app_metadata_bundles/OWNERS new file mode 100644 index 000000000000..a2a250b2d5b7 --- /dev/null +++ b/tools/app_metadata_bundles/OWNERS @@ -0,0 +1,2 @@ +wenhaowang@google.com +mloh@google.com diff --git a/tools/app_metadata_bundles/README.md b/tools/app_metadata_bundles/README.md new file mode 100644 index 000000000000..6e8d287b41dd --- /dev/null +++ b/tools/app_metadata_bundles/README.md @@ -0,0 +1,9 @@ +# App metadata bundles + +This project delivers a comprehensive toolchain solution for developers +to efficiently manage app metadata bundles. + +The project consists of two subprojects: + + * A pure Java library, and + * A pure Java command-line tool. diff --git a/tools/app_metadata_bundles/src/aslgen/aslgen.mf b/tools/app_metadata_bundles/src/aslgen/aslgen.mf new file mode 100644 index 000000000000..fc656e2155a7 --- /dev/null +++ b/tools/app_metadata_bundles/src/aslgen/aslgen.mf @@ -0,0 +1 @@ +Main-Class: com.android.aslgen.Main
\ No newline at end of file diff --git a/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java b/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java new file mode 100644 index 000000000000..df003b6aeab2 --- /dev/null +++ b/tools/app_metadata_bundles/src/aslgen/java/com/android/aslgen/Main.java @@ -0,0 +1,110 @@ +/* + * 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.aslgen; + +import com.android.asllib.AndroidSafetyLabel; +import com.android.asllib.AndroidSafetyLabel.Format; + +import org.xml.sax.SAXException; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; + +public class Main { + + /** Takes the options to make file conversion. */ + public static void main(String[] args) + throws IOException, ParserConfigurationException, SAXException, TransformerException { + + String inFile = null; + String outFile = null; + Format inFormat = Format.NULL; + Format outFormat = Format.NULL; + + + // Except for "--help", all arguments require a value currently. + // So just make sure we have an even number and + // then process them all two at a time. + if (args.length == 1 && "--help".equals(args[0])) { + showUsage(); + return; + } + if (args.length % 2 != 0) { + throw new IllegalArgumentException("Argument is missing corresponding value"); + } + for (int i = 0; i < args.length - 1; i += 2) { + final String arg = args[i].trim(); + final String argValue = args[i + 1].trim(); + if ("--in-path".equals(arg)) { + inFile = argValue; + } else if ("--out-path".equals(arg)) { + outFile = argValue; + } else if ("--in-format".equals(arg)) { + inFormat = getFormat(argValue); + } else if ("--out-format".equals(arg)) { + outFormat = getFormat(argValue); + } else { + throw new IllegalArgumentException("Unknown argument: " + arg); + } + } + + if (inFile == null) { + throw new IllegalArgumentException("input file is required"); + } + + if (outFile == null) { + throw new IllegalArgumentException("output file is required"); + } + + if (inFormat == Format.NULL) { + throw new IllegalArgumentException("input format is required"); + } + + if (outFormat == Format.NULL) { + throw new IllegalArgumentException("output format is required"); + } + + System.out.println("in path: " + inFile); + System.out.println("out path: " + outFile); + System.out.println("in format: " + inFormat); + System.out.println("out format: " + outFormat); + + var asl = AndroidSafetyLabel.readFromStream(new FileInputStream(inFile), inFormat); + asl.writeToStream(new FileOutputStream(outFile), outFormat); + } + + private static Format getFormat(String argValue) { + if ("hr".equals(argValue)) { + return Format.HUMAN_READABLE; + } else if ("od".equals(argValue)) { + return Format.ON_DEVICE; + } else { + return Format.NULL; + } + } + + private static void showUsage() { + AndroidSafetyLabel.test(); + System.err.println( + "Usage:\n" + ); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java new file mode 100644 index 000000000000..0f7ce6894063 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java @@ -0,0 +1,120 @@ +/* + * 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.asllib; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +public class AndroidSafetyLabel implements AslMarshallable { + + public enum Format { + NULL, HUMAN_READABLE, ON_DEVICE; + } + + private final SafetyLabels mSafetyLabels; + + public SafetyLabels getSafetyLabels() { + return mSafetyLabels; + } + + public AndroidSafetyLabel(SafetyLabels safetyLabels) { + this.mSafetyLabels = safetyLabels; + } + + /** Reads a {@link AndroidSafetyLabel} from an {@link InputStream}. */ + // TODO(b/329902686): Support parsing from on-device. + public static AndroidSafetyLabel readFromStream(InputStream in, Format format) + throws IOException, ParserConfigurationException, SAXException { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + Document document = factory.newDocumentBuilder().parse(in); + + switch (format) { + case HUMAN_READABLE: + Element appMetadataBundles = + XmlUtils.getSingleElement(document, XmlUtils.HR_TAG_APP_METADATA_BUNDLES); + + return new AndroidSafetyLabelFactory() + .createFromHrElements( + XmlUtils.asElementList( + document.getElementsByTagName( + XmlUtils.HR_TAG_APP_METADATA_BUNDLES))); + case ON_DEVICE: + throw new IllegalArgumentException( + "Parsing from on-device format is not supported at this time."); + default: + throw new IllegalStateException("Unrecognized input format."); + } + } + + /** Write the content of the {@link AndroidSafetyLabel} to a {@link OutputStream}. */ + // TODO(b/329902686): Support outputting human-readable format. + public void writeToStream(OutputStream out, Format format) + throws IOException, ParserConfigurationException, TransformerException { + var docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + var document = docBuilder.newDocument(); + + switch (format) { + case HUMAN_READABLE: + throw new IllegalArgumentException( + "Outputting human-readable format is not supported at this time."); + case ON_DEVICE: + for (var child : this.toOdDomElements(document)) { + document.appendChild(child); + } + break; + default: + throw new IllegalStateException("Unrecognized input format."); + } + + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, "yes"); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + StreamResult streamResult = new StreamResult(out); // out + DOMSource domSource = new DOMSource(document); + transformer.transform(domSource, streamResult); + } + + /** Creates an on-device DOM element from an {@link AndroidSafetyLabel} */ + @Override + public List<Element> toOdDomElements(Document doc) { + Element aslEle = doc.createElement(XmlUtils.OD_TAG_BUNDLE); + XmlUtils.appendChildren(aslEle, mSafetyLabels.toOdDomElements(doc)); + return List.of(aslEle); + } + + public static void test() { + // TODO(b/329902686): Add tests. + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java new file mode 100644 index 000000000000..9b0f05b0c633 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabelFactory.java @@ -0,0 +1,36 @@ +/* + * 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.asllib; + +import org.w3c.dom.Element; + +import java.util.List; + +public class AndroidSafetyLabelFactory implements AslMarshallableFactory<AndroidSafetyLabel> { + + /** Creates an {@link AndroidSafetyLabel} from human-readable DOM element */ + @Override + public AndroidSafetyLabel createFromHrElements(List<Element> appMetadataBundles) { + Element appMetadataBundlesEle = XmlUtils.getSingleElement(appMetadataBundles); + Element safetyLabelsEle = + XmlUtils.getSingleChildElement( + appMetadataBundlesEle, XmlUtils.HR_TAG_SAFETY_LABELS); + SafetyLabels safetyLabels = + new SafetyLabelsFactory().createFromHrElements(List.of(safetyLabelsEle)); + return new AndroidSafetyLabel(safetyLabels); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallable.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallable.java new file mode 100644 index 000000000000..4e64ab0c53c1 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallable.java @@ -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 com.android.asllib; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.List; + +public interface AslMarshallable { + + /** Creates the on-device DOM element from the AslMarshallable Java Object. */ + List<Element> toOdDomElements(Document doc); +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java new file mode 100644 index 000000000000..b607353791ff --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AslMarshallableFactory.java @@ -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. + */ + +package com.android.asllib; + +import org.w3c.dom.Element; + +import java.util.List; + +public interface AslMarshallableFactory<T extends AslMarshallable> { + + /** Creates an {@link AslMarshallableFactory} from human-readable DOM element */ + T createFromHrElements(List<Element> elements); +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java new file mode 100644 index 000000000000..e5ed63b74ebf --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java @@ -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.asllib; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.List; +import java.util.Map; + +/** + * Data usage category representation containing one or more {@link DataType}. Valid category keys + * are defined in {@link DataCategoryConstants}, each category has a valid set of types {@link + * DataType}, which are mapped in {@link DataTypeConstants} + */ +public class DataCategory implements AslMarshallable { + private final String mCategoryName; + private final Map<String, DataType> mDataTypes; + + public DataCategory(String categoryName, Map<String, DataType> dataTypes) { + this.mCategoryName = categoryName; + this.mDataTypes = dataTypes; + } + + public String getCategoryName() { + return mCategoryName; + } + + /** Return the type {@link Map} of String type key to {@link DataType} */ + + public Map<String, DataType> getDataTypes() { + return mDataTypes; + } + + /** Creates on-device DOM element(s) from the {@link DataCategory}. */ + @Override + public List<Element> toOdDomElements(Document doc) { + Element dataCategoryEle = XmlUtils.createPbundleEleWithName(doc, this.getCategoryName()); + for (DataType dataType : mDataTypes.values()) { + XmlUtils.appendChildren(dataCategoryEle, dataType.toOdDomElements(doc)); + } + return List.of(dataCategoryEle); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java new file mode 100644 index 000000000000..b364c8b37194 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryConstants.java @@ -0,0 +1,74 @@ +/* + * 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.asllib; + + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Constants for determining valid {@link String} data types for usage within {@link SafetyLabels}, + * {@link DataCategory}, and {@link DataType} + */ +public class DataCategoryConstants { + + public static final String CATEGORY_PERSONAL = "personal"; + public static final String CATEGORY_FINANCIAL = "financial"; + public static final String CATEGORY_LOCATION = "location"; + public static final String CATEGORY_EMAIL_TEXT_MESSAGE = "email_text_message"; + public static final String CATEGORY_PHOTO_VIDEO = "photo_video"; + public static final String CATEGORY_AUDIO = "audio"; + public static final String CATEGORY_STORAGE = "storage"; + public static final String CATEGORY_HEALTH_FITNESS = "health_fitness"; + public static final String CATEGORY_CONTACTS = "contacts"; + public static final String CATEGORY_CALENDAR = "calendar"; + public static final String CATEGORY_IDENTIFIERS = "identifiers"; + public static final String CATEGORY_APP_PERFORMANCE = "app_performance"; + public static final String CATEGORY_ACTIONS_IN_APP = "actions_in_app"; + public static final String CATEGORY_SEARCH_AND_BROWSING = "search_and_browsing"; + + /** Set of valid categories */ + public static final Set<String> VALID_CATEGORIES = + Collections.unmodifiableSet( + new HashSet<>( + Arrays.asList( + CATEGORY_PERSONAL, + CATEGORY_FINANCIAL, + CATEGORY_LOCATION, + CATEGORY_EMAIL_TEXT_MESSAGE, + CATEGORY_PHOTO_VIDEO, + CATEGORY_AUDIO, + CATEGORY_STORAGE, + CATEGORY_HEALTH_FITNESS, + CATEGORY_CONTACTS, + CATEGORY_CALENDAR, + CATEGORY_IDENTIFIERS, + CATEGORY_APP_PERFORMANCE, + CATEGORY_ACTIONS_IN_APP, + CATEGORY_SEARCH_AND_BROWSING))); + + /** Returns {@link Set} of valid {@link String} category keys */ + public static Set<String> getValidDataCategories() { + return VALID_CATEGORIES; + } + + private DataCategoryConstants() { + /* do nothing - hide constructor */ + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java new file mode 100644 index 000000000000..5a52591eaf8c --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategoryFactory.java @@ -0,0 +1,38 @@ +/* + * 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.asllib; + +import org.w3c.dom.Element; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DataCategoryFactory implements AslMarshallableFactory<DataCategory> { + @Override + public DataCategory createFromHrElements(List<Element> elements) { + String categoryName = null; + Map<String, DataType> dataTypeMap = new HashMap<String, DataType>(); + for (Element ele : elements) { + categoryName = ele.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY); + String dataTypeName = ele.getAttribute(XmlUtils.HR_ATTR_DATA_TYPE); + dataTypeMap.put(dataTypeName, new DataTypeFactory().createFromHrElements(List.of(ele))); + } + + return new DataCategory(categoryName, dataTypeMap); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java new file mode 100644 index 000000000000..d2fffc0a36f6 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java @@ -0,0 +1,101 @@ +/* + * 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.asllib; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.List; +import java.util.Map; + +/** + * Data label representation with data shared and data collected maps containing zero or more {@link + * DataCategory} + */ +public class DataLabels implements AslMarshallable { + private final Map<String, DataCategory> mDataAccessed; + private final Map<String, DataCategory> mDataCollected; + private final Map<String, DataCategory> mDataShared; + + public DataLabels( + Map<String, DataCategory> dataAccessed, + Map<String, DataCategory> dataCollected, + Map<String, DataCategory> dataShared) { + mDataAccessed = dataAccessed; + mDataCollected = dataCollected; + mDataShared = dataShared; + } + + /** + * Returns the data accessed {@link Map} of {@link com.android.asllib.DataCategoryConstants} to + * {@link DataCategory} + */ + public Map<String, DataCategory> getDataAccessed() { + return mDataAccessed; + } + + /** + * Returns the data collected {@link Map} of {@link com.android.asllib.DataCategoryConstants} to + * {@link DataCategory} + */ + public Map<String, DataCategory> getDataCollected() { + return mDataCollected; + } + + /** + * Returns the data shared {@link Map} of {@link com.android.asllib.DataCategoryConstants} to + * {@link DataCategory} + */ + public Map<String, DataCategory> getDataShared() { + return mDataShared; + } + + /** Gets the on-device DOM element for the {@link DataLabels}. */ + @Override + public List<Element> toOdDomElements(Document doc) { + Element dataLabelsEle = + XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_DATA_LABELS); + + maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_ACCESSED); + maybeAppendDataUsages(doc, dataLabelsEle, mDataCollected, XmlUtils.OD_NAME_DATA_COLLECTED); + maybeAppendDataUsages(doc, dataLabelsEle, mDataShared, XmlUtils.OD_NAME_DATA_SHARED); + + return List.of(dataLabelsEle); + } + + private void maybeAppendDataUsages( + Document doc, + Element dataLabelsEle, + Map<String, DataCategory> dataCategoriesMap, + String dataUsageTypeName) { + if (dataCategoriesMap.isEmpty()) { + return; + } + Element dataUsageEle = XmlUtils.createPbundleEleWithName(doc, dataUsageTypeName); + + for (String dataCategoryName : dataCategoriesMap.keySet()) { + Element dataCategoryEle = XmlUtils.createPbundleEleWithName(doc, dataCategoryName); + DataCategory dataCategory = dataCategoriesMap.get(dataCategoryName); + for (String dataTypeName : dataCategory.getDataTypes().keySet()) { + DataType dataType = dataCategory.getDataTypes().get(dataTypeName); + XmlUtils.appendChildren(dataCategoryEle, dataType.toOdDomElements(doc)); + } + dataUsageEle.appendChild(dataCategoryEle); + } + dataLabelsEle.appendChild(dataUsageEle); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java new file mode 100644 index 000000000000..c758ab923bbf --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabelsFactory.java @@ -0,0 +1,70 @@ +/* + * 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.asllib; + +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class DataLabelsFactory implements AslMarshallableFactory<DataLabels> { + + /** Creates a {@link DataLabels} from the human-readable DOM element. */ + @Override + public DataLabels createFromHrElements(List<Element> elements) { + Element ele = XmlUtils.getSingleElement(elements); + Map<String, DataCategory> dataAccessed = + getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_ACCESSED); + Map<String, DataCategory> dataCollected = + getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_COLLECTED); + Map<String, DataCategory> dataShared = + getDataCategoriesWithTag(ele, XmlUtils.HR_TAG_DATA_SHARED); + return new DataLabels(dataAccessed, dataCollected, dataShared); + } + + private static Map<String, DataCategory> getDataCategoriesWithTag( + Element dataLabelsEle, String dataCategoryUsageTypeTag) { + Map<String, Map<String, DataType>> dataTypeMap = + new HashMap<String, Map<String, DataType>>(); + NodeList dataUsedNodeList = dataLabelsEle.getElementsByTagName(dataCategoryUsageTypeTag); + Map<String, DataCategory> dataCategoryMap = new HashMap<String, DataCategory>(); + + Set<String> dataCategoryNames = new HashSet<String>(); + for (int i = 0; i < dataUsedNodeList.getLength(); i++) { + Element dataUsedEle = (Element) dataUsedNodeList.item(i); + String dataCategoryName = dataUsedEle.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY); + dataCategoryNames.add(dataCategoryName); + } + for (String dataCategoryName : dataCategoryNames) { + var dataCategoryElements = + XmlUtils.asElementList(dataUsedNodeList).stream() + .filter( + ele -> + ele.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY) + .equals(dataCategoryName)) + .toList(); + DataCategory dataCategory = + new DataCategoryFactory().createFromHrElements(dataCategoryElements); + dataCategoryMap.put(dataCategoryName, dataCategory); + } + return dataCategoryMap; + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java new file mode 100644 index 000000000000..5ba29757e19e --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java @@ -0,0 +1,176 @@ +/* + * 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.asllib; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.List; +import java.util.Set; + +/** + * Data usage type representation. Types are specific to a {@link DataCategory} and contains + * metadata related to the data usage purpose. + */ +public class DataType implements AslMarshallable { + + public enum Purpose { + PURPOSE_APP_FUNCTIONALITY(1), + PURPOSE_ANALYTICS(2), + PURPOSE_DEVELOPER_COMMUNICATIONS(3), + PURPOSE_FRAUD_PREVENTION_SECURITY(4), + PURPOSE_ADVERTISING(5), + PURPOSE_PERSONALIZATION(6), + PURPOSE_ACCOUNT_MANAGEMENT(7); + + private static final String PURPOSE_PREFIX = "PURPOSE_"; + + private final int mValue; + + Purpose(int value) { + this.mValue = value; + } + + /** Get the int value associated with the Purpose. */ + public int getValue() { + return mValue; + } + + /** Get the Purpose associated with the int value. */ + public static Purpose forValue(int value) { + for (Purpose e : values()) { + if (e.getValue() == value) { + return e; + } + } + throw new IllegalArgumentException("No enum for value: " + value); + } + + /** Get the Purpose associated with the human-readable String. */ + public static Purpose forString(String s) { + for (Purpose e : values()) { + if (e.toString().equals(s)) { + return e; + } + } + throw new IllegalArgumentException("No enum for str: " + s); + } + + /** Human-readable String representation of Purpose. */ + public String toString() { + if (!this.name().startsWith(PURPOSE_PREFIX)) { + return this.name(); + } + return this.name().substring(PURPOSE_PREFIX.length()).toLowerCase(); + } + } + + private final String mDataTypeName; + + private final Set<Purpose> mPurposeSet; + private final Boolean mIsCollectionOptional; + private final Boolean mIsSharingOptional; + private final Boolean mEphemeral; + + public DataType( + String dataTypeName, + Set<Purpose> purposeSet, + Boolean isCollectionOptional, + Boolean isSharingOptional, + Boolean ephemeral) { + this.mDataTypeName = dataTypeName; + this.mPurposeSet = purposeSet; + this.mIsCollectionOptional = isCollectionOptional; + this.mIsSharingOptional = isSharingOptional; + this.mEphemeral = ephemeral; + } + + public String getDataTypeName() { + return mDataTypeName; + } + + /** + * Returns {@link Set} of valid {@link Integer} purposes for using the associated data category + * and type + */ + public Set<Purpose> getPurposeSet() { + return mPurposeSet; + } + + /** + * For data-collected, returns {@code true} if data usage is user optional and {@code false} if + * data usage is required. Should return {@code null} for data-accessed and data-shared. + */ + public Boolean getIsCollectionOptional() { + return mIsCollectionOptional; + } + + /** + * For data-shared, returns {@code true} if data usage is user optional and {@code false} if + * data usage is required. Should return {@code null} for data-accessed and data-collected. + */ + public Boolean getIsSharingOptional() { + return mIsSharingOptional; + } + + /** + * For data-collected, returns {@code true} if data usage is user optional and {@code false} if + * data usage is processed ephemerally. Should return {@code null} for data-shared. + */ + public Boolean getEphemeral() { + return mEphemeral; + } + + @Override + public List<Element> toOdDomElements(Document doc) { + Element dataTypeEle = XmlUtils.createPbundleEleWithName(doc, this.getDataTypeName()); + if (!this.getPurposeSet().isEmpty()) { + Element purposesEle = doc.createElement(XmlUtils.OD_TAG_INT_ARRAY); + purposesEle.setAttribute(XmlUtils.OD_ATTR_NAME, XmlUtils.OD_NAME_PURPOSES); + purposesEle.setAttribute( + XmlUtils.OD_ATTR_NUM, String.valueOf(this.getPurposeSet().size())); + for (DataType.Purpose purpose : this.getPurposeSet()) { + Element purposeEle = doc.createElement(XmlUtils.OD_TAG_ITEM); + purposeEle.setAttribute(XmlUtils.OD_ATTR_VALUE, String.valueOf(purpose.getValue())); + purposesEle.appendChild(purposeEle); + } + dataTypeEle.appendChild(purposesEle); + } + + maybeAddBoolToOdElement( + doc, + dataTypeEle, + this.getIsCollectionOptional(), + XmlUtils.OD_NAME_IS_COLLECTION_OPTIONAL); + maybeAddBoolToOdElement( + doc, + dataTypeEle, + this.getIsSharingOptional(), + XmlUtils.OD_NAME_IS_SHARING_OPTIONAL); + maybeAddBoolToOdElement(doc, dataTypeEle, this.getEphemeral(), XmlUtils.OD_NAME_EPHEMERAL); + return List.of(dataTypeEle); + } + + private static void maybeAddBoolToOdElement( + Document doc, Element parentEle, Boolean b, String odName) { + if (b == null) { + return; + } + Element ele = XmlUtils.createOdBooleanEle(doc, odName, b); + parentEle.appendChild(ele); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java new file mode 100644 index 000000000000..a0a75377e988 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeConstants.java @@ -0,0 +1,156 @@ +/* + * 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.asllib; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * Constants for determining valid {@link String} data types for usage within {@link SafetyLabels}, + * {@link DataCategory}, and {@link DataType} + */ +public class DataTypeConstants { + /** Data types for {@link DataCategoryConstants.CATEGORY_PERSONAL} */ + public static final String TYPE_NAME = "name"; + + public static final String TYPE_EMAIL_ADDRESS = "email_address"; + public static final String TYPE_PHONE_NUMBER = "phone_number"; + public static final String TYPE_RACE_ETHNICITY = "race_ethnicity"; + public static final String TYPE_POLITICAL_OR_RELIGIOUS_BELIEFS = + "political_or_religious_beliefs"; + public static final String TYPE_SEXUAL_ORIENTATION_OR_GENDER_IDENTITY = + "sexual_orientation_or_gender_identity"; + public static final String TYPE_PERSONAL_IDENTIFIERS = "personal_identifiers"; + public static final String TYPE_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_FINANCIAL} */ + public static final String TYPE_CARD_BANK_ACCOUNT = "card_bank_account"; + + public static final String TYPE_PURCHASE_HISTORY = "purchase_history"; + public static final String TYPE_CREDIT_SCORE = "credit_score"; + public static final String TYPE_FINANCIAL_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_LOCATION} */ + public static final String TYPE_APPROX_LOCATION = "approx_location"; + + public static final String TYPE_PRECISE_LOCATION = "precise_location"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_EMAIL_TEXT_MESSAGE} */ + public static final String TYPE_EMAILS = "emails"; + + public static final String TYPE_TEXT_MESSAGES = "text_messages"; + public static final String TYPE_EMAIL_TEXT_MESSAGE_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_PHOTO_VIDEO} */ + public static final String TYPE_PHOTOS = "photos"; + + public static final String TYPE_VIDEOS = "videos"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_AUDIO} */ + public static final String TYPE_SOUND_RECORDINGS = "sound_recordings"; + + public static final String TYPE_MUSIC_FILES = "music_files"; + public static final String TYPE_AUDIO_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_STORAGE} */ + public static final String TYPE_FILES_DOCS = "files_docs"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_HEALTH_FITNESS} */ + public static final String TYPE_HEALTH = "health"; + + public static final String TYPE_FITNESS = "fitness"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_CONTACTS} */ + public static final String TYPE_CONTACTS = "contacts"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_CALENDAR} */ + public static final String TYPE_CALENDAR = "calendar"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_IDENTIFIERS} */ + public static final String TYPE_IDENTIFIERS_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_APP_PERFORMANCE} */ + public static final String TYPE_CRASH_LOGS = "crash_logs"; + + public static final String TYPE_PERFORMANCE_DIAGNOSTICS = "performance_diagnostics"; + public static final String TYPE_APP_PERFORMANCE_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_ACTIONS_IN_APP} */ + public static final String TYPE_USER_INTERACTION = "user_interaction"; + + public static final String TYPE_IN_APP_SEARCH_HISTORY = "in_app_search_history"; + public static final String TYPE_INSTALLED_APPS = "installed_apps"; + public static final String TYPE_USER_GENERATED_CONTENT = "user_generated_content"; + public static final String TYPE_ACTIONS_IN_APP_OTHER = "other"; + + /** Data types for {@link DataCategoryConstants.CATEGORY_SEARCH_AND_BROWSING} */ + public static final String TYPE_WEB_BROWSING_HISTORY = "web_browsing_history"; + + /** Set of valid categories */ + public static final Set<String> VALID_TYPES = + Collections.unmodifiableSet( + new HashSet<>( + Arrays.asList( + TYPE_NAME, + TYPE_EMAIL_ADDRESS, + TYPE_PHONE_NUMBER, + TYPE_RACE_ETHNICITY, + TYPE_POLITICAL_OR_RELIGIOUS_BELIEFS, + TYPE_SEXUAL_ORIENTATION_OR_GENDER_IDENTITY, + TYPE_PERSONAL_IDENTIFIERS, + TYPE_OTHER, + TYPE_CARD_BANK_ACCOUNT, + TYPE_PURCHASE_HISTORY, + TYPE_CREDIT_SCORE, + TYPE_FINANCIAL_OTHER, + TYPE_APPROX_LOCATION, + TYPE_PRECISE_LOCATION, + TYPE_EMAILS, + TYPE_TEXT_MESSAGES, + TYPE_EMAIL_TEXT_MESSAGE_OTHER, + TYPE_PHOTOS, + TYPE_VIDEOS, + TYPE_SOUND_RECORDINGS, + TYPE_MUSIC_FILES, + TYPE_AUDIO_OTHER, + TYPE_FILES_DOCS, + TYPE_HEALTH, + TYPE_FITNESS, + TYPE_CONTACTS, + TYPE_CALENDAR, + TYPE_IDENTIFIERS_OTHER, + TYPE_CRASH_LOGS, + TYPE_PERFORMANCE_DIAGNOSTICS, + TYPE_APP_PERFORMANCE_OTHER, + TYPE_USER_INTERACTION, + TYPE_IN_APP_SEARCH_HISTORY, + TYPE_INSTALLED_APPS, + TYPE_USER_GENERATED_CONTENT, + TYPE_ACTIONS_IN_APP_OTHER, + TYPE_WEB_BROWSING_HISTORY))); + + /** Returns {@link Set} of valid {@link String} category keys */ + public static Set<String> getValidDataTypes() { + return VALID_TYPES; + } + + private DataTypeConstants() { + /* do nothing - hide constructor */ + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeFactory.java new file mode 100644 index 000000000000..99f8a8b1b152 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataTypeFactory.java @@ -0,0 +1,47 @@ +/* + * 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.asllib; + +import org.w3c.dom.Element; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class DataTypeFactory implements AslMarshallableFactory<DataType> { + /** Creates a {@link DataType} from the human-readable DOM element. */ + @Override + public DataType createFromHrElements(List<Element> elements) { + Element hrDataTypeEle = XmlUtils.getSingleElement(elements); + String dataTypeName = hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_DATA_TYPE); + Set<DataType.Purpose> purposeSet = + Arrays.stream(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_PURPOSES).split("\\|")) + .map(DataType.Purpose::forString) + .collect(Collectors.toUnmodifiableSet()); + Boolean isCollectionOptional = + XmlUtils.fromString( + hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_IS_SHARING_OPTIONAL)); + Boolean isSharingOptional = + XmlUtils.fromString( + hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_IS_COLLECTION_OPTIONAL)); + Boolean ephemeral = + XmlUtils.fromString(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_EPHEMERAL)); + return new DataType( + dataTypeName, purposeSet, isCollectionOptional, isSharingOptional, ephemeral); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java new file mode 100644 index 000000000000..f06522fc2a5c --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java @@ -0,0 +1,53 @@ +/* + * 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.asllib; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.List; + +/** Safety Label representation containing zero or more {@link DataCategory} for data shared */ +public class SafetyLabels implements AslMarshallable { + + private final Long mVersion; + private final DataLabels mDataLabels; + + public SafetyLabels(Long version, DataLabels dataLabels) { + this.mVersion = version; + this.mDataLabels = dataLabels; + } + + /** Returns the data label for the safety label */ + public DataLabels getDataLabel() { + return mDataLabels; + } + + /** Gets the version of the {@link SafetyLabels}. */ + public Long getVersion() { + return mVersion; + } + + /** Creates an on-device DOM element from the {@link SafetyLabels}. */ + @Override + public List<Element> toOdDomElements(Document doc) { + Element safetyLabelsEle = + XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_SAFETY_LABELS); + XmlUtils.appendChildren(safetyLabelsEle, mDataLabels.toOdDomElements(doc)); + return List.of(safetyLabelsEle); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.java new file mode 100644 index 000000000000..68e83fe30db1 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabelsFactory.java @@ -0,0 +1,45 @@ +/* + * 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.asllib; + +import org.w3c.dom.Element; + +import java.util.List; + +public class SafetyLabelsFactory implements AslMarshallableFactory<SafetyLabels> { + + /** Creates a {@link SafetyLabels} from the human-readable DOM element. */ + @Override + public SafetyLabels createFromHrElements(List<Element> elements) { + Element safetyLabelsEle = XmlUtils.getSingleElement(elements); + Long version; + try { + version = Long.parseLong(safetyLabelsEle.getAttribute(XmlUtils.HR_ATTR_VERSION)); + } catch (Exception e) { + throw new IllegalArgumentException( + "Malformed or missing required version in safety labels."); + } + + DataLabels dataLabels = + new DataLabelsFactory() + .createFromHrElements( + List.of( + XmlUtils.getSingleChildElement( + safetyLabelsEle, XmlUtils.HR_TAG_DATA_LABELS))); + return new SafetyLabels(version, dataLabels); + } +} diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java new file mode 100644 index 000000000000..3c89a308036f --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.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 com.android.asllib; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import java.util.ArrayList; +import java.util.List; + +public class XmlUtils { + public static final String HR_TAG_APP_METADATA_BUNDLES = "app-metadata-bundles"; + public static final String HR_TAG_SAFETY_LABELS = "safety-labels"; + public static final String HR_TAG_DATA_LABELS = "data-labels"; + public static final String HR_TAG_DATA_ACCESSED = "data-accessed"; + public static final String HR_TAG_DATA_COLLECTED = "data-collected"; + public static final String HR_TAG_DATA_SHARED = "data-shared"; + + public static final String HR_ATTR_DATA_CATEGORY = "dataCategory"; + public static final String HR_ATTR_DATA_TYPE = "dataType"; + public static final String HR_ATTR_IS_COLLECTION_OPTIONAL = "isCollectionOptional"; + public static final String HR_ATTR_IS_SHARING_OPTIONAL = "isSharingOptional"; + public static final String HR_ATTR_EPHEMERAL = "ephemeral"; + public static final String HR_ATTR_PURPOSES = "purposes"; + public static final String HR_ATTR_VERSION = "version"; + + public static final String OD_TAG_BUNDLE = "bundle"; + public static final String OD_TAG_PBUNDLE_AS_MAP = "pbundle_as_map"; + public static final String OD_TAG_BOOLEAN = "boolean"; + public static final String OD_TAG_INT_ARRAY = "int-array"; + public static final String OD_TAG_ITEM = "item"; + public static final String OD_ATTR_NAME = "name"; + public static final String OD_ATTR_VALUE = "value"; + public static final String OD_ATTR_NUM = "num"; + public static final String OD_NAME_SAFETY_LABELS = "safety_labels"; + public static final String OD_NAME_DATA_LABELS = "data_labels"; + public static final String OD_NAME_DATA_ACCESSED = "data_accessed"; + public static final String OD_NAME_DATA_COLLECTED = "data_collected"; + public static final String OD_NAME_DATA_SHARED = "data_shared"; + public static final String OD_NAME_PURPOSES = "purposes"; + public static final String OD_NAME_IS_COLLECTION_OPTIONAL = "is_collection_optional"; + public static final String OD_NAME_IS_SHARING_OPTIONAL = "is_sharing_optional"; + public static final String OD_NAME_EPHEMERAL = "ephemeral"; + + public static final String TRUE_STR = "true"; + public static final String FALSE_STR = "false"; + + /** Gets the single top-level {@link Element} having the {@param tagName}. */ + public static Element getSingleElement(Document doc, String tagName) { + var elements = doc.getElementsByTagName(tagName); + return getSingleElement(elements); + } + + /** + * Gets the single {@link Element} within {@param parentEle} and having the {@param tagName}. + */ + public static Element getSingleChildElement(Element parentEle, String tagName) { + var elements = parentEle.getElementsByTagName(tagName); + return getSingleElement(elements); + } + + /** Gets the single {@link Element} from {@param elements} */ + public static Element getSingleElement(NodeList elements) { + if (elements.getLength() != 1) { + throw new IllegalArgumentException( + String.format( + "Expected 1 element in NodeList but got %s.", elements.getLength())); + } + var elementAsNode = elements.item(0); + if (!(elementAsNode instanceof Element)) { + throw new IllegalStateException( + String.format("%s was not an element.", elementAsNode.getNodeName())); + } + return ((Element) elementAsNode); + } + + /** Gets the single {@link Element} within {@param elements}. */ + public static Element getSingleElement(List<Element> elements) { + if (elements.size() != 1) { + throw new IllegalStateException( + String.format("Expected 1 element in list but got %s.", elements.size())); + } + return elements.get(0); + } + + /** Converts {@param nodeList} into List of {@link Element}. */ + public static List<Element> asElementList(NodeList nodeList) { + List<Element> elementList = new ArrayList<Element>(); + for (int i = 0; i < nodeList.getLength(); i++) { + var elementAsNode = nodeList.item(0); + if (elementAsNode instanceof Element) { + elementList.add(((Element) elementAsNode)); + } + } + return elementList; + } + + /** Appends {@param children} to the {@param ele}. */ + public static void appendChildren(Element ele, List<Element> children) { + for (Element c : children) { + ele.appendChild(c); + } + } + + /** Gets the Boolean from the String value. */ + public static Boolean fromString(String s) { + if (s == null) { + return null; + } + if (s.equals(TRUE_STR)) { + return true; + } else if (s.equals(FALSE_STR)) { + return false; + } + return null; + } + + /** Creates an on-device PBundle DOM Element with the given attribute name. */ + public static Element createPbundleEleWithName(Document doc, String name) { + var ele = doc.createElement(XmlUtils.OD_TAG_PBUNDLE_AS_MAP); + ele.setAttribute(XmlUtils.OD_ATTR_NAME, name); + return ele; + } + + /** Create an on-device Boolean DOM Element with the given attribute name. */ + public static Element createOdBooleanEle(Document doc, String name, boolean b) { + var ele = doc.createElement(XmlUtils.OD_TAG_BOOLEAN); + ele.setAttribute(XmlUtils.OD_ATTR_NAME, name); + ele.setAttribute(XmlUtils.OD_ATTR_VALUE, String.valueOf(b)); + return ele; + } + + /** Returns whether the String is null or empty. */ + public static boolean isNullOrEmpty(String s) { + return s == null || s.isEmpty(); + } +} |