diff options
293 files changed, 7927 insertions, 2484 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/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..0023e2a3d579 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -3797,7 +3797,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"; 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/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/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/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/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/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/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/provider/Settings.java b/core/java/android/provider/Settings.java index e26dc73f7172..aad2b4ef9242 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 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/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index bbda0684f1d8..cd486d0e7c2e 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -211,7 +211,7 @@ public abstract class WallpaperService extends Service { * @hide */ @ChangeId - @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) public static final long WEAROS_WALLPAPER_HANDLES_SCALING = 272527315L; static final class WallpaperCommand { 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..41bfb24884a2 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -12695,7 +12695,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 +13062,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 +13080,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..dc060ba6e60d 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -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/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/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 78f06b6bddb3..84715aa80edb 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -217,6 +217,12 @@ public class ResolverActivity extends Activity implements public static final String EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device"; /** + * Boolean extra to indicate if Resolver Sheet needs to be started in single user mode. + */ + protected static final String EXTRA_RESTRICT_TO_SINGLE_USER = + "com.android.internal.app.ResolverActivity.EXTRA_RESTRICT_TO_SINGLE_USER"; + + /** * Integer extra to indicate which profile should be automatically selected. * <p>Can only be used if there is a work profile. * <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}. @@ -750,8 +756,10 @@ public class ResolverActivity extends Activity implements } protected UserHandle getPersonalProfileUserHandle() { - if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()){ - return mPrivateProfileUserHandle; + // When launched in single user mode, only personal tab is populated, so we use + // tabOwnerUserHandleForLaunch as personal tab's user handle. + if (privateSpaceEnabled() && isLaunchedInSingleUserMode()) { + return getTabOwnerUserHandleForLaunch(); } return mPersonalProfileUserHandle; } @@ -822,11 +830,11 @@ public class ResolverActivity extends Activity implements // If we are in work or private profile's process, return WorkProfile/PrivateProfile user // as owner, otherwise we always return PersonalProfile user as owner if (UserHandle.of(UserHandle.myUserId()).equals(getWorkProfileUserHandle())) { - return getWorkProfileUserHandle(); + return mWorkProfileUserHandle; } else if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()) { - return getPrivateProfileUserHandle(); + return mPrivateProfileUserHandle; } - return getPersonalProfileUserHandle(); + return mPersonalProfileUserHandle; } private boolean hasWorkProfile() { @@ -847,8 +855,18 @@ public class ResolverActivity extends Activity implements && (UserHandle.myUserId() == getPrivateProfileUserHandle().getIdentifier()); } + protected final boolean isLaunchedInSingleUserMode() { + // When launched from Private Profile, return true + if (isLaunchedAsPrivateProfile()) { + return true; + } + return getIntent() + .getBooleanExtra(EXTRA_RESTRICT_TO_SINGLE_USER, /* defaultValue = */ false); + } + protected boolean shouldShowTabs() { - if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()) { + // No Tabs are shown when launched in single user mode. + if (privateSpaceEnabled() && isLaunchedInSingleUserMode()) { return false; } return hasWorkProfile() && ENABLE_TABBED_VIEW; diff --git a/core/jni/OWNERS b/core/jni/OWNERS index 3aca751edb0d..2a4f062478bd 100644 --- a/core/jni/OWNERS +++ b/core/jni/OWNERS @@ -27,6 +27,7 @@ per-file android_view_VelocityTracker.* = file:/services/core/java/com/android/s # WindowManager per-file android_graphics_BLASTBufferQueue.cpp = file:/services/core/java/com/android/server/wm/OWNERS per-file android_view_Surface* = file:/services/core/java/com/android/server/wm/OWNERS +per-file android_view_WindowManagerGlobal.cpp = file:/services/core/java/com/android/server/wm/OWNERS per-file android_window_* = file:/services/core/java/com/android/server/wm/OWNERS # Resources 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_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/jni/android_view_WindowManagerGlobal.cpp b/core/jni/android_view_WindowManagerGlobal.cpp index b03ac88a36ca..abc621d8dc90 100644 --- a/core/jni/android_view_WindowManagerGlobal.cpp +++ b/core/jni/android_view_WindowManagerGlobal.cpp @@ -48,7 +48,7 @@ std::shared_ptr<InputChannel> createInputChannel( surfaceControlObj(env, android_view_SurfaceControl_getJavaSurfaceControl(env, surfaceControl)); - jobject clientTokenObj = javaObjectForIBinder(env, clientToken); + ScopedLocalRef<jobject> clientTokenObj(env, javaObjectForIBinder(env, clientToken)); ScopedLocalRef<jobject> clientInputTransferTokenObj( env, android_window_InputTransferToken_getJavaInputTransferToken(env, @@ -57,7 +57,7 @@ std::shared_ptr<InputChannel> createInputChannel( inputChannelObj(env, env->CallStaticObjectMethod(gWindowManagerGlobal.clazz, gWindowManagerGlobal.createInputChannel, - clientTokenObj, + clientTokenObj.get(), hostInputTransferTokenObj.get(), surfaceControlObj.get(), clientInputTransferTokenObj.get())); @@ -68,9 +68,9 @@ std::shared_ptr<InputChannel> createInputChannel( void removeInputChannel(const sp<IBinder>& clientToken) { JNIEnv* env = AndroidRuntime::getJNIEnv(); - jobject clientTokenObj(javaObjectForIBinder(env, clientToken)); + ScopedLocalRef<jobject> clientTokenObj(env, javaObjectForIBinder(env, clientToken)); env->CallStaticObjectMethod(gWindowManagerGlobal.clazz, gWindowManagerGlobal.removeInputChannel, - clientTokenObj); + clientTokenObj.get()); } int register_android_view_WindowManagerGlobal(JNIEnv* env) { 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/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 f915f038dc0d..a3dba48bbb7d 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -231,8 +231,10 @@ <string name="NetworkPreferenceSwitchSummary">Try changing preferred network. Tap to change.</string> <!-- Displayed to tell the user that emergency calls might not be available. --> <string name="EmergencyCallWarningTitle">Emergency calling unavailable</string> - <!-- Displayed to tell the user that emergency calls might not be available. --> - <string name="EmergencyCallWarningSummary">Can\u2019t make emergency calls over Wi\u2011Fi</string> + <!-- Displayed to tell the user that emergency calls might not be available; this is shown to + the user when only WiFi calling is available and the carrier does not support emergency + calls over WiFi calling. --> + <string name="EmergencyCallWarningSummary">Emergency calls require a mobile network</string> <!-- Telephony notification channel name for a channel containing network alert notifications. --> <string name="notification_channel_network_alert">Alerts</string> @@ -3247,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/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/app/ResolverActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java index cb8754ae9962..488f017872b1 100644 --- a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java @@ -27,6 +27,7 @@ import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; import static com.android.internal.app.MatcherUtils.first; +import static com.android.internal.app.ResolverActivity.EXTRA_RESTRICT_TO_SINGLE_USER; import static com.android.internal.app.ResolverDataProvider.createPackageManagerMockedInfo; import static com.android.internal.app.ResolverWrapperActivity.sOverrides; @@ -1254,6 +1255,51 @@ public class ResolverActivityTest { } } + @Test + public void testTriggerFromMainProfile_inSingleUserMode_withWorkProfilePresent() { + mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, + android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE); + markWorkProfileUserAvailable(); + setTabOwnerUserHandleForLaunch(PERSONAL_USER_HANDLE); + Intent sendIntent = createSendImageIntent(); + sendIntent.putExtra(EXTRA_RESTRICT_TO_SINGLE_USER, true); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE); + List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + assertThat(activity.getPersonalListAdapter().getCount(), is(2)); + onView(withId(R.id.tabs)).check(matches(not(isDisplayed()))); + assertEquals(activity.getMultiProfilePagerAdapterCount(), 1); + for (ResolvedComponentInfo resolvedInfo : personalResolvedComponentInfos) { + assertEquals(resolvedInfo.getResolveInfoAt(0).userHandle, PERSONAL_USER_HANDLE); + } + } + + @Test + public void testTriggerFromWorkProfile_inSingleUserMode() { + mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE, + android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE); + markWorkProfileUserAvailable(); + setTabOwnerUserHandleForLaunch(sOverrides.workProfileUserHandle); + Intent sendIntent = createSendImageIntent(); + sendIntent.putExtra(EXTRA_RESTRICT_TO_SINGLE_USER, true); + List<ResolvedComponentInfo> personalResolvedComponentInfos = + createResolvedComponentsForTest(3, sOverrides.workProfileUserHandle); + setupResolverControllers(personalResolvedComponentInfos); + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + assertThat(activity.getPersonalListAdapter().getCount(), is(3)); + onView(withId(R.id.tabs)).check(matches(not(isDisplayed()))); + assertEquals(activity.getMultiProfilePagerAdapterCount(), 1); + for (ResolvedComponentInfo resolvedInfo : personalResolvedComponentInfos) { + assertEquals(resolvedInfo.getResolveInfoAt(0).userHandle, + sOverrides.workProfileUserHandle); + } + } + private Intent createSendImageIntent() { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); @@ -1339,6 +1385,10 @@ public class ResolverActivityTest { ResolverWrapperActivity.sOverrides.privateProfileUserHandle = UserHandle.of(12); } + private void setTabOwnerUserHandleForLaunch(UserHandle tabOwnerUserHandleForLaunch) { + sOverrides.tabOwnerUserHandleForLaunch = tabOwnerUserHandleForLaunch; + } + private void setupResolverControllers( List<ResolvedComponentInfo> personalResolvedComponentInfos, List<ResolvedComponentInfo> workResolvedComponentInfos) { diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java index 862cbd5b5e01..4604b01d1bd2 100644 --- a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java +++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java @@ -116,6 +116,10 @@ public class ResolverWrapperActivity extends ResolverActivity { when(sOverrides.resolverListController.getUserHandle()).thenReturn(UserHandle.SYSTEM); return sOverrides.resolverListController; } + if (isLaunchedInSingleUserMode()) { + when(sOverrides.resolverListController.getUserHandle()).thenReturn(userHandle); + return sOverrides.resolverListController; + } when(sOverrides.workResolverListController.getUserHandle()).thenReturn(userHandle); return sOverrides.workResolverListController; } 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/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/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/surface_control_input_receiver.cpp b/native/android/surface_control_input_receiver.cpp index da0defd9fd17..d178abc2c3d7 100644 --- a/native/android/surface_control_input_receiver.cpp +++ b/native/android/surface_control_input_receiver.cpp @@ -45,6 +45,8 @@ public: mClientToken(clientToken), mInputTransferToken(inputTransferToken) {} + // The InputConsumer does not keep the InputReceiver alive so the receiver is cleared once the + // owner releases it. ~InputReceiver() { remove(); } diff --git a/nfc/api/current.txt b/nfc/api/current.txt index da292a818396..80b2be2567a7 100644 --- a/nfc/api/current.txt +++ b/nfc/api/current.txt @@ -268,10 +268,9 @@ package android.nfc.cardemulation { } @FlaggedApi("android.nfc.nfc_read_polling_loop") public final class PollingFrame implements android.os.Parcelable { - ctor public PollingFrame(int, @Nullable byte[], int, int, boolean); method public int describeContents(); method @NonNull public byte[] getData(); - method public int getTimestamp(); + method public long getTimestamp(); method public boolean getTriggeredAutoTransact(); method public int getType(); method public int getVendorSpecificGain(); 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/nfc/java/android/nfc/cardemulation/PollingFrame.java b/nfc/java/android/nfc/cardemulation/PollingFrame.java index af63a6e4350b..654e8cc574ba 100644 --- a/nfc/java/android/nfc/cardemulation/PollingFrame.java +++ b/nfc/java/android/nfc/cardemulation/PollingFrame.java @@ -16,6 +16,7 @@ package android.nfc.cardemulation; +import android.annotation.DurationMillisLong; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; @@ -148,7 +149,8 @@ public final class PollingFrame implements Parcelable{ private final int mType; private final byte[] mData; private final int mGain; - private final int mTimestamp; + @DurationMillisLong + private final long mTimestamp; private final boolean mTriggeredAutoTransact; public static final @NonNull Parcelable.Creator<PollingFrame> CREATOR = @@ -180,16 +182,18 @@ public final class PollingFrame implements Parcelable{ * @param type the type of the frame * @param data a byte array of the data contained in the frame * @param gain the vendor-specific gain of the field - * @param timestamp the timestamp in millisecones + * @param timestampMillis the timestamp in millisecones * @param triggeredAutoTransact whether or not this frame triggered the device to start a * transaction automatically + * + * @hide */ public PollingFrame(@PollingFrameType int type, @Nullable byte[] data, - int gain, int timestamp, boolean triggeredAutoTransact) { + int gain, @DurationMillisLong long timestampMillis, boolean triggeredAutoTransact) { mType = type; mData = data == null ? new byte[0] : data; mGain = gain; - mTimestamp = timestamp; + mTimestamp = timestampMillis; mTriggeredAutoTransact = triggeredAutoTransact; } @@ -230,7 +234,7 @@ public final class PollingFrame implements Parcelable{ * frames relative to each other. * @return the timestamp in milliseconds */ - public int getTimestamp() { + public @DurationMillisLong long getTimestamp() { return mTimestamp; } @@ -264,7 +268,7 @@ public final class PollingFrame implements Parcelable{ frame.putInt(KEY_POLLING_LOOP_GAIN, (byte) getVendorSpecificGain()); } frame.putByteArray(KEY_POLLING_LOOP_DATA, getData()); - frame.putInt(KEY_POLLING_LOOP_TIMESTAMP, getTimestamp()); + frame.putLong(KEY_POLLING_LOOP_TIMESTAMP, getTimestamp()); frame.putBoolean(KEY_POLLING_LOOP_TRIGGERED_AUTOTRANSACT, getTriggeredAutoTransact()); return frame; } @@ -273,7 +277,7 @@ public final class PollingFrame implements Parcelable{ public String toString() { return "PollingFrame { Type: " + (char) getType() + ", gain: " + getVendorSpecificGain() - + ", timestamp: " + Integer.toUnsignedString(getTimestamp()) + + ", timestamp: " + Long.toUnsignedString(getTimestamp()) + ", data: [" + HexFormat.ofDelimiter(" ").formatHex(getData()) + "] }"; } } 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/media/session/MediaSessionManagerExt.kt b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt index cda6b8bb36be..68f471dd4e4f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt +++ b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt @@ -17,6 +17,7 @@ package com.android.settingslib.media.session import android.media.session.MediaController +import android.media.session.MediaSession import android.media.session.MediaSessionManager import android.os.UserHandle import androidx.concurrent.futures.DirectExecutor @@ -28,7 +29,7 @@ import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.launch /** [Flow] for [MediaSessionManager.OnActiveSessionsChangedListener]. */ -val MediaSessionManager.activeMediaChanges: Flow<Collection<MediaController>?> +val MediaSessionManager.activeMediaChanges: Flow<List<MediaController>?> get() = callbackFlow { val listener = @@ -42,3 +43,24 @@ val MediaSessionManager.activeMediaChanges: Flow<Collection<MediaController>?> awaitClose { removeOnActiveSessionsChangedListener(listener) } } .buffer(capacity = Channel.CONFLATED) + +/** [Flow] for [MediaSessionManager.RemoteSessionCallback]. */ +val MediaSessionManager.remoteSessionChanges: Flow<MediaSession.Token?> + get() = + callbackFlow { + val callback = + object : MediaSessionManager.RemoteSessionCallback { + override fun onVolumeChanged(sessionToken: MediaSession.Token, flags: Int) { + launch { send(sessionToken) } + } + + override fun onDefaultRemoteSessionChanged( + sessionToken: MediaSession.Token? + ) { + launch { send(sessionToken) } + } + } + registerRemoteSessionCallback(DirectExecutor.INSTANCE, callback) + awaitClose { unregisterRemoteSessionCallback(callback) } + } + .buffer(capacity = Channel.CONFLATED) diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt index 298dd71e555e..724dd51b8fe4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt @@ -15,14 +15,10 @@ */ package com.android.settingslib.volume.data.repository -import android.media.MediaRouter2Manager -import android.media.RoutingSessionInfo import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice -import com.android.settingslib.volume.data.model.RoutingSession import com.android.settingslib.volume.shared.AudioManagerEventsReceiver import com.android.settingslib.volume.shared.model.AudioManagerEvent -import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -30,35 +26,23 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.withContext /** Repository providing data about connected media devices. */ interface LocalMediaRepository { - /** Available devices list */ - val mediaDevices: StateFlow<Collection<MediaDevice>> - /** Currently connected media device */ val currentConnectedDevice: StateFlow<MediaDevice?> - - val remoteRoutingSessions: StateFlow<Collection<RoutingSession>> - - suspend fun adjustSessionVolume(sessionId: String?, volume: Int) } class LocalMediaRepositoryImpl( audioManagerEventsReceiver: AudioManagerEventsReceiver, private val localMediaManager: LocalMediaManager, - private val mediaRouter2Manager: MediaRouter2Manager, coroutineScope: CoroutineScope, - private val backgroundContext: CoroutineContext, ) : LocalMediaRepository { private val devicesChanges = @@ -94,18 +78,6 @@ class LocalMediaRepositoryImpl( } .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0) - override val mediaDevices: StateFlow<Collection<MediaDevice>> = - mediaDevicesUpdates - .mapNotNull { - if (it is DevicesUpdate.DeviceListUpdate) { - it.newDevices ?: emptyList() - } else { - null - } - } - .flowOn(backgroundContext) - .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList()) - override val currentConnectedDevice: StateFlow<MediaDevice?> = merge(devicesChanges, mediaDevicesUpdates) .map { localMediaManager.currentConnectedDevice } @@ -116,30 +88,6 @@ class LocalMediaRepositoryImpl( localMediaManager.currentConnectedDevice ) - override val remoteRoutingSessions: StateFlow<Collection<RoutingSession>> = - merge(devicesChanges, mediaDevicesUpdates) - .onStart { emit(Unit) } - .map { localMediaManager.remoteRoutingSessions.map(::toRoutingSession) } - .flowOn(backgroundContext) - .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList()) - - override suspend fun adjustSessionVolume(sessionId: String?, volume: Int) { - withContext(backgroundContext) { - if (sessionId == null) { - localMediaManager.adjustSessionVolume(volume) - } else { - localMediaManager.adjustSessionVolume(sessionId, volume) - } - } - } - - private fun toRoutingSession(info: RoutingSessionInfo): RoutingSession = - RoutingSession( - info, - isMediaOutputDisabled = mediaRouter2Manager.getTransferableRoutes(info).isEmpty(), - isVolumeSeekBarEnabled = localMediaManager.shouldEnableVolumeSeekBar(info) - ) - private sealed interface DevicesUpdate { data class DeviceListUpdate(val newDevices: List<MediaDevice>?) : DevicesUpdate diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt index 7c231d1fad4e..e4ac9fe686a3 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt @@ -27,18 +27,26 @@ import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn /** Provides controllers for currently active device media sessions. */ interface MediaControllerRepository { - /** Current [MediaController]. Null is emitted when there is no active [MediaController]. */ - val activeLocalMediaController: StateFlow<MediaController?> + /** + * Get a list of controllers for all ongoing sessions. The controllers will be provided in + * priority order with the most important controller at index 0. + * + * This requires the [android.Manifest.permission.MEDIA_CONTENT_CONTROL] permission be held by + * the calling app. + */ + val activeSessions: StateFlow<List<MediaController>> } class MediaControllerRepositoryImpl( @@ -49,51 +57,17 @@ class MediaControllerRepositoryImpl( backgroundContext: CoroutineContext, ) : MediaControllerRepository { - private val devicesChanges = - audioManagerEventsReceiver.events.filterIsInstance( - AudioManagerEvent.StreamDevicesChanged::class - ) - - override val activeLocalMediaController: StateFlow<MediaController?> = - combine( - mediaSessionManager.activeMediaChanges.onStart { - emit(mediaSessionManager.getActiveSessions(null)) - }, - localBluetoothManager?.headsetAudioModeChanges?.onStart { emit(Unit) } - ?: flowOf(null), - devicesChanges.onStart { emit(AudioManagerEvent.StreamDevicesChanged) }, - ) { controllers, _, _ -> - controllers?.let(::findLocalMediaController) - } + override val activeSessions: StateFlow<List<MediaController>> = + merge( + mediaSessionManager.activeMediaChanges.filterNotNull(), + localBluetoothManager?.headsetAudioModeChanges?.map { + mediaSessionManager.getActiveSessions(null) + } ?: emptyFlow(), + audioManagerEventsReceiver.events + .filterIsInstance(AudioManagerEvent.StreamDevicesChanged::class) + .map { mediaSessionManager.getActiveSessions(null) }, + ) + .onStart { emit(mediaSessionManager.getActiveSessions(null)) } .flowOn(backgroundContext) - .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null) - - private fun findLocalMediaController( - controllers: Collection<MediaController>, - ): MediaController? { - var localController: MediaController? = null - val remoteMediaSessionLists: MutableList<String> = ArrayList() - for (controller in controllers) { - val playbackInfo: MediaController.PlaybackInfo = controller.playbackInfo ?: continue - when (playbackInfo.playbackType) { - MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> { - if (localController?.packageName.equals(controller.packageName)) { - localController = null - } - if (!remoteMediaSessionLists.contains(controller.packageName)) { - remoteMediaSessionLists.add(controller.packageName) - } - } - MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> { - if ( - localController == null && - !remoteMediaSessionLists.contains(controller.packageName) - ) { - localController = controller - } - } - } - } - return localController - } + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList()) } diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt deleted file mode 100644 index f6213351ae0d..000000000000 --- a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settingslib.volume.domain.interactor - -import com.android.settingslib.media.MediaDevice -import com.android.settingslib.volume.data.repository.LocalMediaRepository -import com.android.settingslib.volume.domain.model.RoutingSession -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn - -class LocalMediaInteractor( - private val repository: LocalMediaRepository, - coroutineScope: CoroutineScope, -) { - - /** Available devices list */ - val mediaDevices: StateFlow<Collection<MediaDevice>> - get() = repository.mediaDevices - - /** Currently connected media device */ - val currentConnectedDevice: StateFlow<MediaDevice?> - get() = repository.currentConnectedDevice - - val remoteRoutingSessions: StateFlow<List<RoutingSession>> = - repository.remoteRoutingSessions - .map { sessions -> - sessions.map { - RoutingSession( - routingSessionInfo = it.routingSessionInfo, - isMediaOutputDisabled = it.isMediaOutputDisabled, - isVolumeSeekBarEnabled = - it.isVolumeSeekBarEnabled && it.routingSessionInfo.volumeMax > 0 - ) - } - } - .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList()) - - suspend fun adjustSessionVolume(sessionId: String?, volume: Int) = - repository.adjustSessionVolume(sessionId, volume) -} diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt index 2d12dae36ff1..caf41f21afb7 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt @@ -15,17 +15,12 @@ */ package com.android.settingslib.volume.data.repository -import android.media.MediaRoute2Info -import android.media.MediaRouter2Manager -import android.media.RoutingSessionInfo import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.media.LocalMediaManager import com.android.settingslib.media.MediaDevice -import com.android.settingslib.volume.data.model.RoutingSession import com.android.settingslib.volume.shared.FakeAudioManagerEventsReceiver import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.TestScope @@ -37,15 +32,10 @@ import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock -import org.mockito.Mockito.any -import org.mockito.Mockito.anyInt -import org.mockito.Mockito.anyString -import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) @SmallTest class LocalMediaRepositoryImplTest { @@ -53,7 +43,6 @@ class LocalMediaRepositoryImplTest { @Mock private lateinit var localMediaManager: LocalMediaManager @Mock private lateinit var mediaDevice1: MediaDevice @Mock private lateinit var mediaDevice2: MediaDevice - @Mock private lateinit var mediaRouter2Manager: MediaRouter2Manager @Captor private lateinit var deviceCallbackCaptor: ArgumentCaptor<LocalMediaManager.DeviceCallback> @@ -71,29 +60,11 @@ class LocalMediaRepositoryImplTest { LocalMediaRepositoryImpl( eventsReceiver, localMediaManager, - mediaRouter2Manager, testScope.backgroundScope, - testScope.testScheduler, ) } @Test - fun mediaDevices_areUpdated() { - testScope.runTest { - var mediaDevices: Collection<MediaDevice>? = null - underTest.mediaDevices.onEach { mediaDevices = it }.launchIn(backgroundScope) - runCurrent() - verify(localMediaManager).registerCallback(deviceCallbackCaptor.capture()) - deviceCallbackCaptor.value.onDeviceListUpdate(listOf(mediaDevice1, mediaDevice2)) - runCurrent() - - assertThat(mediaDevices).hasSize(2) - assertThat(mediaDevices).contains(mediaDevice1) - assertThat(mediaDevices).contains(mediaDevice2) - } - } - - @Test fun deviceListUpdated_currentConnectedDeviceUpdated() { testScope.runTest { var currentConnectedDevice: MediaDevice? = null @@ -110,78 +81,4 @@ class LocalMediaRepositoryImplTest { assertThat(currentConnectedDevice).isEqualTo(mediaDevice1) } } - - @Test - fun kek() { - testScope.runTest { - `when`(localMediaManager.remoteRoutingSessions) - .thenReturn( - listOf( - testRoutingSessionInfo1, - testRoutingSessionInfo2, - testRoutingSessionInfo3, - ) - ) - `when`(localMediaManager.shouldEnableVolumeSeekBar(any())).then { - (it.arguments[0] as RoutingSessionInfo) == testRoutingSessionInfo1 - } - `when`(mediaRouter2Manager.getTransferableRoutes(any<RoutingSessionInfo>())).then { - if ((it.arguments[0] as RoutingSessionInfo) == testRoutingSessionInfo2) { - return@then listOf(mock(MediaRoute2Info::class.java)) - } - emptyList<MediaRoute2Info>() - } - var remoteRoutingSessions: Collection<RoutingSession>? = null - underTest.remoteRoutingSessions - .onEach { remoteRoutingSessions = it } - .launchIn(backgroundScope) - - runCurrent() - - assertThat(remoteRoutingSessions) - .containsExactlyElementsIn( - listOf( - RoutingSession( - routingSessionInfo = testRoutingSessionInfo1, - isVolumeSeekBarEnabled = true, - isMediaOutputDisabled = true, - ), - RoutingSession( - routingSessionInfo = testRoutingSessionInfo2, - isVolumeSeekBarEnabled = false, - isMediaOutputDisabled = false, - ), - RoutingSession( - routingSessionInfo = testRoutingSessionInfo3, - isVolumeSeekBarEnabled = false, - isMediaOutputDisabled = true, - ) - ) - ) - } - } - - @Test - fun adjustSessionVolume_adjusts() { - testScope.runTest { - var volume = 0 - `when`(localMediaManager.adjustSessionVolume(anyString(), anyInt())).then { - volume = it.arguments[1] as Int - Unit - } - - underTest.adjustSessionVolume("test_session", 10) - - assertThat(volume).isEqualTo(10) - } - } - - private companion object { - val testRoutingSessionInfo1 = - RoutingSessionInfo.Builder("id_1", "test.pkg.1").addSelectedRoute("route_1").build() - val testRoutingSessionInfo2 = - RoutingSessionInfo.Builder("id_2", "test.pkg.2").addSelectedRoute("route_2").build() - val testRoutingSessionInfo3 = - RoutingSessionInfo.Builder("id_3", "test.pkg.3").addSelectedRoute("route_3").build() - } } diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt index f3d17141334e..964c3f7d13d4 100644 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt @@ -22,13 +22,10 @@ import android.media.session.MediaSessionManager import android.media.session.PlaybackState import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.settingslib.bluetooth.BluetoothCallback import com.android.settingslib.bluetooth.BluetoothEventManager import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.volume.shared.FakeAudioManagerEventsReceiver -import com.android.settingslib.volume.shared.model.AudioManagerEvent import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.TestScope @@ -37,21 +34,15 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.Captor import org.mockito.Mock import org.mockito.Mockito.any -import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) @SmallTest class MediaControllerRepositoryImplTest { - @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothCallback> - @Mock private lateinit var mediaSessionManager: MediaSessionManager @Mock private lateinit var localBluetoothManager: LocalBluetoothManager @Mock private lateinit var eventManager: BluetoothEventManager @@ -103,7 +94,7 @@ class MediaControllerRepositoryImplTest { } @Test - fun playingMediaDevicesAvailable_sessionIsActive() { + fun mediaDevicesAvailable_returnsAllActiveOnes() { testScope.runTest { `when`(mediaSessionManager.getActiveSessions(any())) .thenReturn( @@ -112,53 +103,25 @@ class MediaControllerRepositoryImplTest { statelessMediaController, errorMediaController, remoteMediaController, - localMediaController + localMediaController, ) ) - var mediaController: MediaController? = null - underTest.activeLocalMediaController - .onEach { mediaController = it } - .launchIn(backgroundScope) - runCurrent() - eventsReceiver.triggerEvent(AudioManagerEvent.StreamDevicesChanged) - triggerOnAudioModeChanged() + var mediaControllers: Collection<MediaController>? = null + underTest.activeSessions.onEach { mediaControllers = it }.launchIn(backgroundScope) runCurrent() - assertThat(mediaController).isSameInstanceAs(localMediaController) - } - } - - @Test - fun noPlayingMediaDevicesAvailable_sessionIsInactive() { - testScope.runTest { - `when`(mediaSessionManager.getActiveSessions(any())) - .thenReturn( - listOf( - stoppedMediaController, - statelessMediaController, - errorMediaController, - ) + assertThat(mediaControllers) + .containsExactly( + stoppedMediaController, + statelessMediaController, + errorMediaController, + remoteMediaController, + localMediaController, ) - var mediaController: MediaController? = null - underTest.activeLocalMediaController - .onEach { mediaController = it } - .launchIn(backgroundScope) - runCurrent() - - eventsReceiver.triggerEvent(AudioManagerEvent.StreamDevicesChanged) - triggerOnAudioModeChanged() - runCurrent() - - assertThat(mediaController).isNull() } } - private fun triggerOnAudioModeChanged() { - verify(eventManager).registerCallback(callbackCaptor.capture()) - callbackCaptor.value.onAudioModeChanged() - } - private companion object { val statePlaying: PlaybackState = PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0, 0f).build() 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/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/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt index 621ddf796f58..1da6c1ee6638 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt @@ -53,6 +53,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -71,6 +72,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.times import com.android.compose.PlatformButton import com.android.compose.animation.scene.ElementKey @@ -84,7 +86,9 @@ import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel import com.android.systemui.bouncer.ui.BouncerDialogFactory import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModel import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.MessageViewModel import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel @@ -166,7 +170,7 @@ private fun StandardLayout( modifier = Modifier.fillMaxWidth(), ) { StatusMessage( - viewModel = viewModel, + viewModel = viewModel.message, modifier = Modifier, ) @@ -228,7 +232,7 @@ private fun SplitLayout( when (authMethod) { is PinBouncerViewModel -> { StatusMessage( - viewModel = viewModel, + viewModel = viewModel.message, modifier = Modifier.align(Alignment.TopCenter), ) @@ -241,7 +245,7 @@ private fun SplitLayout( } is PatternBouncerViewModel -> { StatusMessage( - viewModel = viewModel, + viewModel = viewModel.message, modifier = Modifier.align(Alignment.TopCenter), ) @@ -280,7 +284,7 @@ private fun SplitLayout( modifier = Modifier.fillMaxWidth().align(Alignment.Center), ) { StatusMessage( - viewModel = viewModel, + viewModel = viewModel.message, ) OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp)) @@ -376,7 +380,7 @@ private fun BesideUserSwitcherLayout( modifier = Modifier.fillMaxWidth() ) { StatusMessage( - viewModel = viewModel, + viewModel = viewModel.message, ) OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp)) @@ -441,7 +445,7 @@ private fun BelowUserSwitcherLayout( modifier = Modifier.fillMaxWidth(), ) { StatusMessage( - viewModel = viewModel, + viewModel = viewModel.message, ) OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp)) @@ -548,26 +552,44 @@ private fun SceneScope.FoldableScene( @Composable private fun StatusMessage( - viewModel: BouncerViewModel, + viewModel: BouncerMessageViewModel, modifier: Modifier = Modifier, ) { - val message: BouncerViewModel.MessageViewModel by viewModel.message.collectAsState() + val message: MessageViewModel? by viewModel.message.collectAsState() + + DisposableEffect(Unit) { + viewModel.onShown() + onDispose {} + } Crossfade( targetState = message, label = "Bouncer message", - animationSpec = if (message.isUpdateAnimated) tween() else snap(), + animationSpec = if (message?.isUpdateAnimated == true) tween() else snap(), modifier = modifier.fillMaxWidth(), - ) { - Box( - contentAlignment = Alignment.Center, + ) { msg -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth(), ) { - Text( - text = it.text, - color = MaterialTheme.colorScheme.onSurface, - style = MaterialTheme.typography.bodyLarge, - ) + msg?.let { + Text( + text = it.text, + color = MaterialTheme.colorScheme.onSurface, + fontSize = 18.sp, + lineHeight = 24.sp, + overflow = TextOverflow.Ellipsis, + ) + Spacer(modifier = Modifier.size(10.dp)) + Text( + text = it.secondaryText ?: "", + color = MaterialTheme.colorScheme.onSurface, + fontSize = 14.sp, + lineHeight = 20.sp, + overflow = TextOverflow.Ellipsis, + maxLines = 2 + ) + } } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt index 2a13d4931b69..c34f2fd26d0c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt @@ -74,10 +74,7 @@ internal fun PasswordBouncer( val isImeSwitcherButtonVisible by viewModel.isImeSwitcherButtonVisible.collectAsState() val selectedUserId by viewModel.selectedUserId.collectAsState() - DisposableEffect(Unit) { - viewModel.onShown() - onDispose { viewModel.onHidden() } - } + DisposableEffect(Unit) { onDispose { viewModel.onHidden() } } LaunchedEffect(animateFailure) { if (animateFailure) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt index 0a5f5d281f83..a78c2c0d16c6 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt @@ -72,10 +72,7 @@ internal fun PatternBouncer( centerDotsVertically: Boolean, modifier: Modifier = Modifier, ) { - DisposableEffect(Unit) { - viewModel.onShown() - onDispose { viewModel.onHidden() } - } + DisposableEffect(Unit) { onDispose { viewModel.onHidden() } } val colCount = viewModel.columnCount val rowCount = viewModel.rowCount diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt index f505b9067140..5651a4646b2d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt @@ -72,10 +72,7 @@ fun PinPad( verticalSpacing: Dp, modifier: Modifier = Modifier, ) { - DisposableEffect(Unit) { - viewModel.onShown() - onDispose { viewModel.onHidden() } - } + DisposableEffect(Unit) { onDispose { viewModel.onHidden() } } val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState() val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState() 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/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt index 5c9b271b342c..525ad161c94f 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,45 +16,33 @@ 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.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.NotificationStackViewBinder 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, + notificationStackViewBinder: NotificationStackViewBinder, ) { init { @@ -73,24 +61,13 @@ 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, - ) + notificationStackViewBinder.bindWhileAttached() } } 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 d78097815b5e..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,9 +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.ui.viewbinder.NotificationStackAppearanceViewBinder.SCRIM_CORNER_RADIUS +import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import kotlin.math.roundToInt @@ -139,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) @@ -156,6 +159,8 @@ fun SceneScope.NotificationScrollingStack( val contentHeight = viewModel.intrinsicContentHeight.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 // height of the Shade Header, and at rest (scrimOffset = 0) its top bound is at maxScrimStartY. @@ -222,16 +227,12 @@ fun SceneScope.NotificationScrollingStack( .graphicsLayer { shape = calculateCornerRadius( + scrimCornerRadius, screenCornerRadius, { expansionFraction }, layoutState.isTransitioningBetween(Scenes.Gone, Scenes.Shade) ) - .let { - RoundedCornerShape( - topStart = it, - topEnd = it, - ) - } + .let { stackRounding.value.toRoundedCornerShape(it) } clip = true } ) { @@ -359,6 +360,7 @@ private fun SceneScope.NotificationPlaceholder( } private fun calculateCornerRadius( + scrimCornerRadius: Dp, screenCornerRadius: Dp, expansionFraction: () -> Float, transitioning: Boolean, @@ -366,12 +368,12 @@ private fun calculateCornerRadius( return if (transitioning) { lerp( start = screenCornerRadius.value, - stop = SCRIM_CORNER_RADIUS, - fraction = (expansionFraction() / EXPANSION_FOR_MAX_CORNER_RADIUS).coerceAtMost(1f), + stop = scrimCornerRadius.value, + fraction = (expansionFraction() / EXPANSION_FOR_MAX_CORNER_RADIUS).coerceIn(0f, 1f), ) .dp } else { - SCRIM_CORNER_RADIUS.dp + scrimCornerRadius } } @@ -394,5 +396,16 @@ private fun Modifier.debugBackground( this } +fun StackRounding.toRoundedCornerShape(radius: Dp): RoundedCornerShape { + val topRadius = if (roundTop) radius else 0.dp + val bottomRadius = if (roundBottom) radius else 0.dp + return RoundedCornerShape( + topStart = topRadius, + topEnd = topRadius, + bottomStart = bottomRadius, + bottomEnd = bottomRadius, + ) +} + private const val TAG = "FlexiNotifs" private val DEBUG_COLOR = Color(1f, 0f, 0f, 0.2f) 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 677fb1d13fa7..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 @@ -55,6 +55,7 @@ import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.LowestZIndexScenePicker import com.android.compose.animation.scene.SceneScope +import com.android.compose.animation.scene.TransitionState import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.animateSceneFloatAsState @@ -294,6 +295,7 @@ private fun SceneScope.SplitShade( } val quickSettingsScrollState = rememberScrollState() + val isScrollable = layoutState.transitionState is TransitionState.Idle LaunchedEffect(isCustomizing, quickSettingsScrollState) { if (isCustomizing) { quickSettingsScrollState.scrollTo(0) @@ -322,36 +324,41 @@ private fun SceneScope.SplitShade( Column( verticalArrangement = Arrangement.Top, modifier = - Modifier.weight(1f).fillMaxHeight().thenIf(!isCustomizing) { - Modifier.verticalNestedScrollToScene() - .verticalScroll(quickSettingsScrollState) - .clipScrollableContainer(Orientation.Horizontal) - .padding(bottom = navBarBottomHeight) - } + Modifier.weight(1f).fillMaxSize().thenIf(!isCustomizing) { + Modifier.padding(bottom = navBarBottomHeight) + }, ) { - Box( - modifier = Modifier.element(QuickSettings.Elements.SplitShadeQuickSettings) + Column( + modifier = + Modifier.fillMaxSize().weight(1f).thenIf(!isCustomizing) { + Modifier.verticalNestedScrollToScene() + .verticalScroll( + quickSettingsScrollState, + enabled = isScrollable + ) + .clipScrollableContainer(Orientation.Horizontal) + } ) { - QuickSettings( - qsSceneAdapter = viewModel.qsSceneAdapter, - heightProvider = { viewModel.qsSceneAdapter.qsHeight }, - isSplitShade = true, + Box( + modifier = + Modifier.element(QuickSettings.Elements.SplitShadeQuickSettings) + ) { + QuickSettings( + qsSceneAdapter = viewModel.qsSceneAdapter, + heightProvider = { viewModel.qsSceneAdapter.qsHeight }, + isSplitShade = true, + modifier = Modifier.fillMaxWidth(), + squishiness = tileSquishiness, + ) + } + + MediaIfVisible( + viewModel = viewModel, + mediaCarouselController = mediaCarouselController, + mediaHost = mediaHost, modifier = Modifier.fillMaxWidth(), - squishiness = tileSquishiness, ) } - - MediaIfVisible( - viewModel = viewModel, - mediaCarouselController = mediaCarouselController, - mediaHost = mediaHost, - modifier = Modifier.fillMaxWidth(), - ) - - Spacer( - modifier = Modifier.weight(1f), - ) - FooterActionsWithAnimatedVisibility( viewModel = footerActionsViewModel, isCustomizing = isCustomizing, @@ -363,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/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt index af51cee2a255..dc3b612d3594 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt @@ -73,7 +73,7 @@ internal class Scene( internal class SceneScopeImpl( private val layoutImpl: SceneTransitionLayoutImpl, private val scene: Scene, -) : SceneScope { +) : SceneScope, ElementStateScope by layoutImpl.elementStateScope { override val layoutState: SceneTransitionLayoutState = layoutImpl.state override fun Modifier.element(key: ElementKey): Modifier { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index b7e2dd13f321..ebc90990275d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -131,9 +131,30 @@ interface SceneTransitionLayoutScope { */ @DslMarker annotation class ElementDsl +/** A scope that can be used to query the target state of an element or scene. */ +interface ElementStateScope { + /** + * Return the *target* size of [this] element in the given [scene], i.e. the size of the element + * when idle, or `null` if the element is not composed and measured in that scene (yet). + */ + fun ElementKey.targetSize(scene: SceneKey): IntSize? + + /** + * Return the *target* offset of [this] element in the given [scene], i.e. the size of the + * element when idle, or `null` if the element is not composed and placed in that scene (yet). + */ + fun ElementKey.targetOffset(scene: SceneKey): Offset? + + /** + * Return the *target* size of [this] scene, i.e. the size of the scene when idle, or `null` if + * the scene was never composed. + */ + fun SceneKey.targetSize(): IntSize? +} + @Stable @ElementDsl -interface BaseSceneScope { +interface BaseSceneScope : ElementStateScope { /** The state of the [SceneTransitionLayout] in which this scene is contained. */ val layoutState: SceneTransitionLayoutState @@ -415,25 +436,7 @@ interface UserActionDistance { ): Float } -interface UserActionDistanceScope : Density { - /** - * Return the *target* size of [this] element in the given [scene], i.e. the size of the element - * when idle, or `null` if the element is not composed and measured in that scene (yet). - */ - fun ElementKey.targetSize(scene: SceneKey): IntSize? - - /** - * Return the *target* offset of [this] element in the given [scene], i.e. the size of the - * element when idle, or `null` if the element is not composed and placed in that scene (yet). - */ - fun ElementKey.targetOffset(scene: SceneKey): Offset? - - /** - * Return the *target* size of [this] scene, i.e. the size of the scene when idle, or `null` if - * the scene was never composed. - */ - fun SceneKey.targetSize(): IntSize? -} +interface UserActionDistanceScope : Density, ElementStateScope /** The user action has a fixed [absoluteDistance]. */ class FixedDistance(private val distance: Dp) : UserActionDistance { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index 25b0895fafb3..b1cfdcf07977 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -98,6 +98,7 @@ internal class SceneTransitionLayoutImpl( private val horizontalDraggableHandler: DraggableHandlerImpl private val verticalDraggableHandler: DraggableHandlerImpl + internal val elementStateScope = ElementStateScopeImpl(this) private var _userActionDistanceScope: UserActionDistanceScope? = null internal val userActionDistanceScope: UserActionDistanceScope get() = diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt index 228d19f09cff..b7abb33c1242 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt @@ -19,15 +19,9 @@ package com.android.compose.animation.scene import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.IntSize -internal class UserActionDistanceScopeImpl( +internal class ElementStateScopeImpl( private val layoutImpl: SceneTransitionLayoutImpl, -) : UserActionDistanceScope { - override val density: Float - get() = layoutImpl.density.density - - override val fontScale: Float - get() = layoutImpl.density.fontScale - +) : ElementStateScope { override fun ElementKey.targetSize(scene: SceneKey): IntSize? { return layoutImpl.elements[this]?.sceneStates?.get(scene)?.targetSize.takeIf { it != Element.SizeUnspecified @@ -44,3 +38,13 @@ internal class UserActionDistanceScopeImpl( return layoutImpl.scenes[this]?.targetSize.takeIf { it != IntSize.Zero } } } + +internal class UserActionDistanceScopeImpl( + private val layoutImpl: SceneTransitionLayoutImpl, +) : UserActionDistanceScope, ElementStateScope by layoutImpl.elementStateScope { + override val density: Float + get() = layoutImpl.density.density + + override val fontScale: Float + get() = layoutImpl.density.fontScale +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt index 707777b9f728..b0d03b15d310 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt @@ -71,34 +71,6 @@ class BouncerInteractorTest : SysuiTestCase() { } @Test - fun pinAuthMethod() = - testScope.runTest { - val message by collectLastValue(underTest.message) - - kosmos.fakeAuthenticationRepository.setAuthenticationMethod( - AuthenticationMethodModel.Pin - ) - runCurrent() - underTest.clearMessage() - assertThat(message).isNull() - - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN) - - // Wrong input. - assertThat(underTest.authenticate(listOf(9, 8, 7))) - .isEqualTo(AuthenticationResult.FAILED) - assertThat(message).isEqualTo(MESSAGE_WRONG_PIN) - - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN) - - // Correct input. - assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)) - .isEqualTo(AuthenticationResult.SUCCEEDED) - } - - @Test fun pinAuthMethod_sim_skipsAuthentication() = testScope.runTest { kosmos.fakeAuthenticationRepository.setAuthenticationMethod( @@ -146,8 +118,6 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun pinAuthMethod_tryAutoConfirm_withoutAutoConfirmPin() = testScope.runTest { - val message by collectLastValue(underTest.message) - kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pin ) @@ -156,7 +126,6 @@ class BouncerInteractorTest : SysuiTestCase() { // Incomplete input. assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true)) .isEqualTo(AuthenticationResult.SKIPPED) - assertThat(message).isNull() // Correct input. assertThat( @@ -166,28 +135,19 @@ class BouncerInteractorTest : SysuiTestCase() { ) ) .isEqualTo(AuthenticationResult.SKIPPED) - assertThat(message).isNull() } @Test fun passwordAuthMethod() = testScope.runTest { - val message by collectLastValue(underTest.message) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password ) runCurrent() - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD) - // Wrong input. assertThat(underTest.authenticate("alohamora".toList())) .isEqualTo(AuthenticationResult.FAILED) - assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD) - - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD) // Too short input. assertThat( @@ -201,7 +161,6 @@ class BouncerInteractorTest : SysuiTestCase() { ) ) .isEqualTo(AuthenticationResult.SKIPPED) - assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD) // Correct input. assertThat(underTest.authenticate("password".toList())) @@ -211,13 +170,10 @@ class BouncerInteractorTest : SysuiTestCase() { @Test fun patternAuthMethod() = testScope.runTest { - val message by collectLastValue(underTest.message) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pattern ) runCurrent() - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN) // Wrong input. val wrongPattern = @@ -231,10 +187,6 @@ class BouncerInteractorTest : SysuiTestCase() { assertThat(wrongPattern.size) .isAtLeast(kosmos.fakeAuthenticationRepository.minPatternLength) assertThat(underTest.authenticate(wrongPattern)).isEqualTo(AuthenticationResult.FAILED) - assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN) - - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN) // Too short input. val tooShortPattern = @@ -244,10 +196,6 @@ class BouncerInteractorTest : SysuiTestCase() { ) assertThat(underTest.authenticate(tooShortPattern)) .isEqualTo(AuthenticationResult.SKIPPED) - assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN) - - underTest.resetMessage() - assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN) // Correct input. assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN)) @@ -258,7 +206,6 @@ class BouncerInteractorTest : SysuiTestCase() { fun lockoutStarted() = testScope.runTest { val lockoutStartedEvents by collectValues(underTest.onLockoutStarted) - val message by collectLastValue(underTest.message) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Pin @@ -272,17 +219,14 @@ class BouncerInteractorTest : SysuiTestCase() { .isEqualTo(AuthenticationResult.FAILED) if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) { assertThat(lockoutStartedEvents).isEmpty() - assertThat(message).isNotEmpty() } } assertThat(authenticationInteractor.lockoutEndTimestamp).isNotNull() assertThat(lockoutStartedEvents.size).isEqualTo(1) - assertThat(message).isNull() // Advance the time to finish the lockout: advanceTimeBy(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS.seconds) assertThat(authenticationInteractor.lockoutEndTimestamp).isNull() - assertThat(message).isNull() assertThat(lockoutStartedEvents.size).isEqualTo(1) // Trigger lockout again: diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt index 701b7039a1ed..c878e0b4757d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt @@ -17,7 +17,6 @@ package com.android.systemui.bouncer.domain.interactor import android.content.pm.UserInfo -import android.os.Handler import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -28,27 +27,25 @@ import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.FaceSensorInfo -import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository +import com.android.systemui.biometrics.data.repository.fakeFacePropertyRepository +import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.bouncer.data.repository.BouncerMessageRepositoryImpl -import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository +import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository import com.android.systemui.bouncer.shared.model.BouncerMessageModel -import com.android.systemui.bouncer.ui.BouncerView -import com.android.systemui.classifier.FalsingCollector import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor import com.android.systemui.flags.SystemPropertiesHelper -import com.android.systemui.keyguard.DismissCallbackRegistry -import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository -import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository -import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository -import com.android.systemui.keyguard.data.repository.FakeTrustRepository +import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.fakeTrustRepository import com.android.systemui.keyguard.shared.model.AuthenticationFlags +import com.android.systemui.kosmos.testScope import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown import com.android.systemui.res.R.string.kg_trust_agent_disabled -import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.systemui.user.data.repository.FakeUserRepository -import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.KotlinArgumentCaptor import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -61,7 +58,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.eq import org.mockito.Mock -import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @@ -70,34 +66,22 @@ import org.mockito.MockitoAnnotations @TestableLooper.RunWithLooper(setAsMainLooper = true) @RunWith(AndroidJUnit4::class) class BouncerMessageInteractorTest : SysuiTestCase() { - + private val kosmos = testKosmos() private val countDownTimerCallback = KotlinArgumentCaptor(CountDownTimerCallback::class.java) private val repository = BouncerMessageRepositoryImpl() - private val userRepository = FakeUserRepository() - private val fakeTrustRepository = FakeTrustRepository() - private val fakeFacePropertyRepository = FakeFacePropertyRepository() - private val bouncerRepository = FakeKeyguardBouncerRepository() - private val fakeDeviceEntryFingerprintAuthRepository = - FakeDeviceEntryFingerprintAuthRepository() - private val fakeDeviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository() - private val biometricSettingsRepository: FakeBiometricSettingsRepository = - FakeBiometricSettingsRepository() + private val biometricSettingsRepository = kosmos.fakeBiometricSettingsRepository + private val testScope = kosmos.testScope @Mock private lateinit var updateMonitor: KeyguardUpdateMonitor @Mock private lateinit var securityModel: KeyguardSecurityModel @Mock private lateinit var countDownTimerUtil: CountDownTimerUtil @Mock private lateinit var systemPropertiesHelper: SystemPropertiesHelper - @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor - @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor - private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor - private lateinit var testScope: TestScope private lateinit var underTest: BouncerMessageInteractor @Before fun setUp() { MockitoAnnotations.initMocks(this) - userRepository.setUserInfos(listOf(PRIMARY_USER)) - testScope = TestScope() + kosmos.fakeUserRepository.setUserInfos(listOf(PRIMARY_USER)) allowTestableLooperAsMainThread() whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN) biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) @@ -105,44 +89,28 @@ class BouncerMessageInteractorTest : SysuiTestCase() { } suspend fun TestScope.init() { - userRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) mSetFlagsRule.enableFlags(Flags.FLAG_REVAMPED_BOUNCER_MESSAGES) - primaryBouncerInteractor = - PrimaryBouncerInteractor( - bouncerRepository, - mock(BouncerView::class.java), - mock(Handler::class.java), - mock(KeyguardStateController::class.java), - mock(KeyguardSecurityModel::class.java), - mock(PrimaryBouncerCallbackInteractor::class.java), - mock(FalsingCollector::class.java), - mock(DismissCallbackRegistry::class.java), - context, - keyguardUpdateMonitor, - fakeTrustRepository, - testScope.backgroundScope, - mSelectedUserInteractor, - mock(DeviceEntryFaceAuthInteractor::class.java), - ) underTest = BouncerMessageInteractor( repository = repository, - userRepository = userRepository, + userRepository = kosmos.fakeUserRepository, countDownTimerUtil = countDownTimerUtil, updateMonitor = updateMonitor, biometricSettingsRepository = biometricSettingsRepository, - applicationScope = this.backgroundScope, - trustRepository = fakeTrustRepository, + applicationScope = testScope.backgroundScope, + trustRepository = kosmos.fakeTrustRepository, systemPropertiesHelper = systemPropertiesHelper, - primaryBouncerInteractor = primaryBouncerInteractor, - facePropertyRepository = fakeFacePropertyRepository, - deviceEntryFingerprintAuthRepository = fakeDeviceEntryFingerprintAuthRepository, - faceAuthRepository = fakeDeviceEntryFaceAuthRepository, + primaryBouncerInteractor = kosmos.primaryBouncerInteractor, + facePropertyRepository = kosmos.fakeFacePropertyRepository, + deviceEntryFingerprintAuthInteractor = kosmos.deviceEntryFingerprintAuthInteractor, + faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository, securityModel = securityModel ) biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) - fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) - bouncerRepository.setPrimaryShow(true) + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) + kosmos.fakeFingerprintPropertyRepository.supportsSideFps() + kosmos.fakeKeyguardBouncerRepository.setPrimaryShow(true) runCurrent() } @@ -268,7 +236,7 @@ class BouncerMessageInteractorTest : SysuiTestCase() { init() val lockoutMessage by collectLastValue(underTest.bouncerMessage) - fakeDeviceEntryFaceAuthRepository.setLockedOut(true) + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true) runCurrent() assertThat(primaryResMessage(lockoutMessage)) @@ -276,7 +244,7 @@ class BouncerMessageInteractorTest : SysuiTestCase() { assertThat(secondaryResMessage(lockoutMessage)) .isEqualTo("Can’t unlock with face. Too many attempts.") - fakeDeviceEntryFaceAuthRepository.setLockedOut(false) + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false) runCurrent() assertThat(primaryResMessage(lockoutMessage)) @@ -289,15 +257,17 @@ class BouncerMessageInteractorTest : SysuiTestCase() { testScope.runTest { init() val lockoutMessage by collectLastValue(underTest.bouncerMessage) - fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.STRONG)) - fakeDeviceEntryFaceAuthRepository.setLockedOut(true) + kosmos.fakeFacePropertyRepository.setSensorInfo( + FaceSensorInfo(1, SensorStrength.STRONG) + ) + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true) runCurrent() assertThat(primaryResMessage(lockoutMessage)).isEqualTo("Enter PIN") assertThat(secondaryResMessage(lockoutMessage)) .isEqualTo("PIN is required after too many attempts") - fakeDeviceEntryFaceAuthRepository.setLockedOut(false) + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false) runCurrent() assertThat(primaryResMessage(lockoutMessage)) @@ -311,14 +281,14 @@ class BouncerMessageInteractorTest : SysuiTestCase() { init() val lockedOutMessage by collectLastValue(underTest.bouncerMessage) - fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true) + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true) runCurrent() assertThat(primaryResMessage(lockedOutMessage)).isEqualTo("Enter PIN") assertThat(secondaryResMessage(lockedOutMessage)) .isEqualTo("PIN is required after too many attempts") - fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) runCurrent() assertThat(primaryResMessage(lockedOutMessage)) @@ -327,6 +297,19 @@ class BouncerMessageInteractorTest : SysuiTestCase() { } @Test + fun onUdfpsFingerprint_DoesNotShowFingerprintMessage() = + testScope.runTest { + init() + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) + kosmos.fakeFingerprintPropertyRepository.supportsUdfps() + val lockedOutMessage by collectLastValue(underTest.bouncerMessage) + + runCurrent() + + assertThat(primaryResMessage(lockedOutMessage)).isEqualTo("Enter PIN") + } + + @Test fun onRestartForMainlineUpdate_shouldProvideRelevantMessage() = testScope.runTest { init() @@ -344,9 +327,10 @@ class BouncerMessageInteractorTest : SysuiTestCase() { fun onAuthFlagsChanged_withTrustNotManagedAndNoBiometrics_isANoop() = testScope.runTest { init() - fakeTrustRepository.setTrustUsuallyManaged(false) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + runCurrent() val defaultMessage = Pair("Enter PIN", null) @@ -377,12 +361,13 @@ class BouncerMessageInteractorTest : SysuiTestCase() { testScope.runTest { init() - userRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + runCurrent() - fakeTrustRepository.setCurrentUserTrustManaged(true) - fakeTrustRepository.setTrustUsuallyManaged(true) + kosmos.fakeTrustRepository.setCurrentUserTrustManaged(true) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(true) val defaultMessage = Pair("Enter PIN", null) @@ -415,8 +400,8 @@ class BouncerMessageInteractorTest : SysuiTestCase() { fun authFlagsChanges_withFaceEnrolled_providesDifferentMessages() = testScope.runTest { init() - userRepository.setSelectedUserInfo(PRIMARY_USER) - fakeTrustRepository.setTrustUsuallyManaged(false) + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(false) biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) @@ -453,12 +438,13 @@ class BouncerMessageInteractorTest : SysuiTestCase() { fun authFlagsChanges_withFingerprintEnrolled_providesDifferentMessages() = testScope.runTest { init() - userRepository.setSelectedUserInfo(PRIMARY_USER) - fakeTrustRepository.setCurrentUserTrustManaged(false) + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false) biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + runCurrent() verifyMessagesForAuthFlag( LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to @@ -466,6 +452,7 @@ class BouncerMessageInteractorTest : SysuiTestCase() { ) biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(false) + runCurrent() verifyMessagesForAuthFlag( LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt index d30e33332926..c9fa671ad34f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt @@ -48,6 +48,7 @@ class AuthMethodBouncerViewModelTest : SysuiTestCase() { isInputEnabled = MutableStateFlow(true), simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Pin, + onIntentionalUserInput = {}, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt new file mode 100644 index 000000000000..16ec9aa897fb --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt @@ -0,0 +1,455 @@ +/* + * 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.bouncer.ui.viewmodel + +import android.content.pm.UserInfo +import android.hardware.biometrics.BiometricFaceConstants +import android.hardware.fingerprint.FingerprintManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.widget.LockPatternUtils +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.domain.interactor.authenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin +import com.android.systemui.biometrics.data.repository.FaceSensorInfo +import com.android.systemui.biometrics.data.repository.fakeFacePropertyRepository +import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository +import com.android.systemui.biometrics.shared.model.SensorStrength +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor +import com.android.systemui.bouncer.shared.flag.fakeComposeBouncerFlags +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus +import com.android.systemui.flags.fakeSystemPropertiesHelper +import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.fakeTrustRepository +import com.android.systemui.keyguard.shared.model.AuthenticationFlags +import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus +import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import com.google.common.truth.Truth.assertThat +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.currentTime +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class BouncerMessageViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val authenticationInteractor by lazy { kosmos.authenticationInteractor } + private val bouncerInteractor by lazy { kosmos.bouncerInteractor } + private lateinit var underTest: BouncerMessageViewModel + + @Before + fun setUp() { + kosmos.fakeUserRepository.setUserInfos(listOf(PRIMARY_USER)) + kosmos.fakeComposeBouncerFlags.composeBouncerEnabled = true + underTest = kosmos.bouncerMessageViewModel + overrideResource(R.string.kg_trust_agent_disabled, "Trust agent is unavailable") + kosmos.fakeSystemPropertiesHelper.set( + DeviceEntryInteractor.SYS_BOOT_REASON_PROP, + "not mainline reboot" + ) + } + + @Test + fun message_defaultMessage_basedOnAuthMethod() = + testScope.runTest { + val message by collectLastValue(underTest.message) + + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) + kosmos.fakeFingerprintPropertyRepository.supportsSideFps() + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + runCurrent() + + assertThat(message!!.text).isEqualTo("Unlock with PIN or fingerprint") + + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pattern) + runCurrent() + assertThat(message!!.text).isEqualTo("Unlock with pattern or fingerprint") + + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Password + ) + runCurrent() + assertThat(message!!.text).isEqualTo("Unlock with password or fingerprint") + } + + @Test + fun message() = + testScope.runTest { + val message by collectLastValue(underTest.message) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) + assertThat(message?.isUpdateAnimated).isTrue() + + repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { + bouncerInteractor.authenticate(WRONG_PIN) + } + assertThat(message?.isUpdateAnimated).isFalse() + + val lockoutEndMs = authenticationInteractor.lockoutEndTimestamp ?: 0 + advanceTimeBy(lockoutEndMs - testScope.currentTime) + assertThat(message?.isUpdateAnimated).isTrue() + } + + @Test + fun lockoutMessage() = + testScope.runTest { + val message by collectLastValue(underTest.message) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) + assertThat(kosmos.fakeAuthenticationRepository.lockoutEndTimestamp).isNull() + runCurrent() + + repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { times -> + bouncerInteractor.authenticate(WRONG_PIN) + runCurrent() + if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) { + assertThat(message?.text).isEqualTo("Wrong PIN. Try again.") + assertThat(message?.isUpdateAnimated).isTrue() + } + } + val lockoutSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS + assertTryAgainMessage(message?.text, lockoutSeconds) + assertThat(message?.isUpdateAnimated).isFalse() + + repeat(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS) { time -> + advanceTimeBy(1.seconds) + val remainingSeconds = lockoutSeconds - time - 1 + if (remainingSeconds > 0) { + assertTryAgainMessage(message?.text, remainingSeconds) + } + } + assertThat(message?.text).isEqualTo("Enter PIN") + assertThat(message?.isUpdateAnimated).isTrue() + } + + @Test + fun defaultMessage_mapsToDeviceEntryRestrictionReason_whenTrustAgentIsEnabled() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + kosmos.fakeTrustRepository.setTrustUsuallyManaged(true) + kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false) + runCurrent() + + val defaultMessage = Pair("Enter PIN", null) + + verifyMessagesForAuthFlags( + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to defaultMessage, + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to + Pair("Enter PIN", "PIN is required after device restarts"), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to + Pair("Enter PIN", "Added security required. PIN not used for a while."), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to + Pair("Enter PIN", "For added security, device was locked by work policy"), + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to + Pair("Enter PIN", "Trust agent is unavailable"), + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to + Pair("Enter PIN", "Trust agent is unavailable"), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to + Pair("Enter PIN", "PIN is required after lockdown"), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to + Pair("Enter PIN", "PIN required for additional security"), + LockPatternUtils.StrongAuthTracker + .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to + Pair( + "Enter PIN", + "Added security required. Device wasn’t unlocked for a while." + ), + ) + } + + @Test + fun defaultMessage_mapsToDeviceEntryRestrictionReason_whenFingerprintIsAvailable() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeFingerprintPropertyRepository.supportsSideFps() + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false) + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + runCurrent() + + verifyMessagesForAuthFlags( + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to + Pair("Unlock with PIN or fingerprint", null), + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to + Pair("Unlock with PIN or fingerprint", null), + LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to + Pair("Unlock with PIN or fingerprint", null), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to + Pair("Enter PIN", "PIN is required after device restarts"), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to + Pair("Enter PIN", "Added security required. PIN not used for a while."), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to + Pair("Enter PIN", "For added security, device was locked by work policy"), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to + Pair("Enter PIN", "PIN is required after lockdown"), + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to + Pair("Enter PIN", "PIN required for additional security"), + LockPatternUtils.StrongAuthTracker + .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to + Pair( + "Unlock with PIN or fingerprint", + "Added security required. Device wasn’t unlocked for a while." + ), + ) + } + + @Test + fun onFingerprintLockout_messageUpdated() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeFingerprintPropertyRepository.supportsSideFps() + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + + val lockedOutMessage by collectLastValue(underTest.message) + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true) + runCurrent() + + assertThat(lockedOutMessage?.text).isEqualTo("Enter PIN") + assertThat(lockedOutMessage?.secondaryText) + .isEqualTo("PIN is required after too many attempts") + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) + runCurrent() + + assertThat(lockedOutMessage?.text).isEqualTo("Unlock with PIN or fingerprint") + assertThat(lockedOutMessage?.secondaryText.isNullOrBlank()).isTrue() + } + + @Test + fun onUdfpsFingerprint_DoesNotShowFingerprintMessage() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeFingerprintPropertyRepository.supportsUdfps() + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false) + val message by collectLastValue(underTest.message) + + runCurrent() + + assertThat(message?.text).isEqualTo("Enter PIN") + } + + @Test + fun onRestartForMainlineUpdate_shouldProvideRelevantMessage() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeSystemPropertiesHelper.set("sys.boot.reason.last", "reboot,mainline_update") + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + runCurrent() + + verifyMessagesForAuthFlags( + LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to + Pair("Enter PIN", "Device updated. Enter PIN to continue.") + ) + } + + @Test + fun onFaceLockout_whenItIsClass3_shouldProvideRelevantMessage() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + val lockoutMessage by collectLastValue(underTest.message) + kosmos.fakeFacePropertyRepository.setSensorInfo( + FaceSensorInfo(1, SensorStrength.STRONG) + ) + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true) + runCurrent() + + assertThat(lockoutMessage?.text).isEqualTo("Enter PIN") + assertThat(lockoutMessage?.secondaryText) + .isEqualTo("PIN is required after too many attempts") + + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false) + runCurrent() + + assertThat(lockoutMessage?.text).isEqualTo("Enter PIN") + assertThat(lockoutMessage?.secondaryText.isNullOrBlank()).isTrue() + } + + @Test + fun onFaceLockout_whenItIsNotStrong_shouldProvideRelevantMessage() = + testScope.runTest { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + val lockoutMessage by collectLastValue(underTest.message) + kosmos.fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.WEAK)) + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true) + runCurrent() + + assertThat(lockoutMessage?.text).isEqualTo("Enter PIN") + assertThat(lockoutMessage?.secondaryText) + .isEqualTo("Can’t unlock with face. Too many attempts.") + + kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false) + runCurrent() + + assertThat(lockoutMessage?.text).isEqualTo("Enter PIN") + assertThat(lockoutMessage?.secondaryText.isNullOrBlank()).isTrue() + } + + @Test + fun setFingerprintMessage_propagateValue() = + testScope.runTest { + val bouncerMessage by collectLastValue(underTest.message) + + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + kosmos.fakeFingerprintPropertyRepository.supportsSideFps() + runCurrent() + + kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus( + HelpFingerprintAuthenticationStatus(1, "some helpful message") + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Unlock with PIN or fingerprint") + assertThat(bouncerMessage?.secondaryText).isEqualTo("some helpful message") + + kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus( + FailFingerprintAuthenticationStatus + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Fingerprint not recognized") + assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again or enter PIN") + + kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus( + ErrorFingerprintAuthenticationStatus( + FingerprintManager.FINGERPRINT_ERROR_LOCKOUT, + "locked out" + ) + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Enter PIN") + assertThat(bouncerMessage?.secondaryText) + .isEqualTo("PIN is required after too many attempts") + } + + @Test + fun setFaceMessage_propagateValue() = + testScope.runTest { + val bouncerMessage by collectLastValue(underTest.message) + + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + kosmos.fakeBiometricSettingsRepository.setIsFaceAuthCurrentlyAllowed(true) + runCurrent() + + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + HelpFaceAuthenticationStatus(1, "some helpful message") + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Enter PIN") + assertThat(bouncerMessage?.secondaryText).isEqualTo("some helpful message") + + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + ErrorFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ERROR_TIMEOUT, + "Try again" + ) + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Enter PIN") + assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again") + + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + FailedFaceAuthenticationStatus() + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Face not recognized") + assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again or enter PIN") + + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + ErrorFaceAuthenticationStatus( + BiometricFaceConstants.FACE_ERROR_LOCKOUT, + "locked out" + ) + ) + runCurrent() + assertThat(bouncerMessage?.text).isEqualTo("Enter PIN") + assertThat(bouncerMessage?.secondaryText) + .isEqualTo("Can’t unlock with face. Too many attempts.") + } + + private fun TestScope.verifyMessagesForAuthFlags( + vararg authFlagToMessagePair: Pair<Int, Pair<String, String?>> + ) { + val actualMessage by collectLastValue(underTest.message) + + authFlagToMessagePair.forEach { (flag, expectedMessagePair) -> + kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags( + AuthenticationFlags(userId = PRIMARY_USER_ID, flag = flag) + ) + runCurrent() + + assertThat(actualMessage?.text).isEqualTo(expectedMessagePair.first) + + if (expectedMessagePair.second == null) { + assertThat(actualMessage?.secondaryText.isNullOrBlank()).isTrue() + } else { + assertThat(actualMessage?.secondaryText).isEqualTo(expectedMessagePair.second) + } + } + } + + private fun assertTryAgainMessage( + message: String?, + time: Int, + ) { + assertThat(message).contains("Try again in $time second") + } + + companion object { + private val WRONG_PIN = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 } + private const val PRIMARY_USER_ID = 0 + private val PRIMARY_USER = + UserInfo( + /* id= */ PRIMARY_USER_ID, + /* name= */ "primary user", + /* flags= */ UserInfo.FLAG_PRIMARY + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt index 73db1757c06a..3afca96e07a0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt @@ -37,7 +37,6 @@ import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage -import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flatMapLatest @@ -142,54 +141,6 @@ class BouncerViewModelTest : SysuiTestCase() { } @Test - fun message() = - testScope.runTest { - val message by collectLastValue(underTest.message) - kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) - assertThat(message?.isUpdateAnimated).isTrue() - - repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { - bouncerInteractor.authenticate(WRONG_PIN) - } - assertThat(message?.isUpdateAnimated).isFalse() - - val lockoutEndMs = authenticationInteractor.lockoutEndTimestamp ?: 0 - advanceTimeBy(lockoutEndMs - testScope.currentTime) - assertThat(message?.isUpdateAnimated).isTrue() - } - - @Test - fun lockoutMessage() = - testScope.runTest { - val authMethodViewModel by collectLastValue(underTest.authMethodViewModel) - val message by collectLastValue(underTest.message) - kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) - assertThat(kosmos.fakeAuthenticationRepository.lockoutEndTimestamp).isNull() - assertThat(authMethodViewModel?.lockoutMessageId).isNotNull() - - repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { times -> - bouncerInteractor.authenticate(WRONG_PIN) - if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) { - assertThat(message?.text).isEqualTo(bouncerInteractor.message.value) - assertThat(message?.isUpdateAnimated).isTrue() - } - } - val lockoutSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS - assertTryAgainMessage(message?.text, lockoutSeconds) - assertThat(message?.isUpdateAnimated).isFalse() - - repeat(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS) { time -> - advanceTimeBy(1.seconds) - val remainingSeconds = lockoutSeconds - time - 1 - if (remainingSeconds > 0) { - assertTryAgainMessage(message?.text, remainingSeconds) - } - } - assertThat(message?.text).isEmpty() - assertThat(message?.isUpdateAnimated).isTrue() - } - - @Test fun isInputEnabled() = testScope.runTest { val isInputEnabled by @@ -212,25 +163,6 @@ class BouncerViewModelTest : SysuiTestCase() { } @Test - fun dialogViewModel() = - testScope.runTest { - val authMethodViewModel by collectLastValue(underTest.authMethodViewModel) - val dialogViewModel by collectLastValue(underTest.dialogViewModel) - kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) - assertThat(authMethodViewModel?.lockoutMessageId).isNotNull() - - repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { - assertThat(dialogViewModel).isNull() - bouncerInteractor.authenticate(WRONG_PIN) - } - assertThat(dialogViewModel).isNotNull() - assertThat(dialogViewModel?.text).isNotEmpty() - - dialogViewModel?.onDismiss?.invoke() - assertThat(dialogViewModel).isNull() - } - - @Test fun isSideBySideSupported() = testScope.runTest { val isSideBySideSupported by collectLastValue(underTest.isSideBySideSupported) @@ -265,13 +197,6 @@ class BouncerViewModelTest : SysuiTestCase() { return listOf(None, Pin, Password, Pattern, Sim) } - private fun assertTryAgainMessage( - message: String?, - time: Int, - ) { - assertThat(message).isEqualTo("Try again in $time seconds.") - } - companion object { private val WRONG_PIN = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt index df50eb64f8b6..71c578545647 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt @@ -66,7 +66,6 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { private val bouncerInteractor by lazy { kosmos.bouncerInteractor } private val selectedUserInteractor by lazy { kosmos.selectedUserInteractor } private val inputMethodInteractor by lazy { kosmos.inputMethodInteractor } - private val bouncerViewModel by lazy { kosmos.bouncerViewModel } private val isInputEnabled = MutableStateFlow(true) private val underTest = @@ -76,6 +75,7 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { interactor = bouncerInteractor, inputMethodInteractor = inputMethodInteractor, selectedUserInteractor = selectedUserInteractor, + onIntentionalUserInput = {}, ) @Before @@ -88,11 +88,9 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { fun onShown() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) lockDeviceAndOpenPasswordBouncer() - assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD) assertThat(password).isEmpty() assertThat(currentScene).isEqualTo(Scenes.Bouncer) assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Password) @@ -101,16 +99,13 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { @Test fun onHidden_resetsPasswordInputAndMessage() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) lockDeviceAndOpenPasswordBouncer() underTest.onPasswordInputChanged("password") - assertThat(message?.text).isNotEqualTo(ENTER_YOUR_PASSWORD) assertThat(password).isNotEmpty() underTest.onHidden() - assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD) assertThat(password).isEmpty() } @@ -118,13 +113,11 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { fun onPasswordInputChanged() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) lockDeviceAndOpenPasswordBouncer() underTest.onPasswordInputChanged("password") - assertThat(message?.text).isEmpty() assertThat(password).isEqualTo("password") assertThat(currentScene).isEqualTo(Scenes.Bouncer) } @@ -144,7 +137,6 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { @Test fun onAuthenticateKeyPressed_whenWrong() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) lockDeviceAndOpenPasswordBouncer() @@ -152,13 +144,11 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { underTest.onAuthenticateKeyPressed() assertThat(password).isEmpty() - assertThat(message?.text).isEqualTo(WRONG_PASSWORD) } @Test fun onAuthenticateKeyPressed_whenEmpty() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) kosmos.fakeAuthenticationRepository.setAuthenticationMethod( AuthenticationMethodModel.Password @@ -171,14 +161,12 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { underTest.onAuthenticateKeyPressed() assertThat(password).isEmpty() - assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD) } @Test fun onAuthenticateKeyPressed_correctAfterWrong() = testScope.runTest { val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult) - val message by collectLastValue(bouncerViewModel.message) val password by collectLastValue(underTest.password) lockDeviceAndOpenPasswordBouncer() @@ -186,12 +174,10 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { underTest.onPasswordInputChanged("wrong") underTest.onAuthenticateKeyPressed() assertThat(password).isEqualTo("") - assertThat(message?.text).isEqualTo(WRONG_PASSWORD) assertThat(authResult).isFalse() // Enter the correct password: underTest.onPasswordInputChanged("password") - assertThat(message?.text).isEmpty() underTest.onAuthenticateKeyPressed() @@ -331,10 +317,8 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { private fun TestScope.switchToScene(toScene: SceneKey) { val currentScene by collectLastValue(sceneInteractor.currentScene) - val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer sceneInteractor.changeScene(toScene, "reason") - if (bouncerShown) underTest.onShown() if (bouncerHidden) underTest.onHidden() runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt index 91a056ddd685..51b73ee92df5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt @@ -63,6 +63,7 @@ class PatternBouncerViewModelTest : SysuiTestCase() { viewModelScope = testScope.backgroundScope, interactor = bouncerInteractor, isInputEnabled = MutableStateFlow(true).asStateFlow(), + onIntentionalUserInput = {}, ) } @@ -79,12 +80,10 @@ class PatternBouncerViewModelTest : SysuiTestCase() { fun onShown() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) lockDeviceAndOpenPatternBouncer() - assertThat(message?.text).isEqualTo(ENTER_YOUR_PATTERN) assertThat(selectedDots).isEmpty() assertThat(currentDot).isNull() assertThat(currentScene).isEqualTo(Scenes.Bouncer) @@ -95,14 +94,12 @@ class PatternBouncerViewModelTest : SysuiTestCase() { fun onDragStart() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) lockDeviceAndOpenPatternBouncer() underTest.onDragStart() - assertThat(message?.text).isEmpty() assertThat(selectedDots).isEmpty() assertThat(currentDot).isNull() assertThat(currentScene).isEqualTo(Scenes.Bouncer) @@ -148,7 +145,6 @@ class PatternBouncerViewModelTest : SysuiTestCase() { fun onDragEnd_whenWrong() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) lockDeviceAndOpenPatternBouncer() @@ -159,7 +155,6 @@ class PatternBouncerViewModelTest : SysuiTestCase() { assertThat(selectedDots).isEmpty() assertThat(currentDot).isNull() - assertThat(message?.text).isEqualTo(WRONG_PATTERN) assertThat(currentScene).isEqualTo(Scenes.Bouncer) } @@ -302,7 +297,6 @@ class PatternBouncerViewModelTest : SysuiTestCase() { @Test fun onDragEnd_whenPatternTooShort() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val dialogViewModel by collectLastValue(bouncerViewModel.dialogViewModel) lockDeviceAndOpenPatternBouncer() @@ -325,7 +319,6 @@ class PatternBouncerViewModelTest : SysuiTestCase() { underTest.onDragEnd() - assertWithMessage("Attempt #$attempt").that(message?.text).isEqualTo(WRONG_PATTERN) assertWithMessage("Attempt #$attempt").that(dialogViewModel).isNull() } } @@ -334,7 +327,6 @@ class PatternBouncerViewModelTest : SysuiTestCase() { fun onDragEnd_correctAfterWrong() = testScope.runTest { val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult) - val message by collectLastValue(bouncerViewModel.message) val selectedDots by collectLastValue(underTest.selectedDots) val currentDot by collectLastValue(underTest.currentDot) lockDeviceAndOpenPatternBouncer() @@ -344,7 +336,6 @@ class PatternBouncerViewModelTest : SysuiTestCase() { underTest.onDragEnd() assertThat(selectedDots).isEmpty() assertThat(currentDot).isNull() - assertThat(message?.text).isEqualTo(WRONG_PATTERN) assertThat(authResult).isFalse() // Enter the correct pattern: @@ -370,10 +361,8 @@ class PatternBouncerViewModelTest : SysuiTestCase() { private fun TestScope.switchToScene(toScene: SceneKey) { val currentScene by collectLastValue(sceneInteractor.currentScene) - val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer sceneInteractor.changeScene(toScene, "reason") - if (bouncerShown) underTest.onShown() if (bouncerHidden) underTest.onHidden() runCurrent() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt index 7b75a3715415..564795429fa6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt @@ -56,7 +56,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { private val sceneInteractor by lazy { kosmos.sceneInteractor } private val authenticationInteractor by lazy { kosmos.authenticationInteractor } private val bouncerInteractor by lazy { kosmos.bouncerInteractor } - private val bouncerViewModel by lazy { kosmos.bouncerViewModel } private lateinit var underTest: PinBouncerViewModel @Before @@ -69,6 +68,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { isInputEnabled = MutableStateFlow(true).asStateFlow(), simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Pin, + onIntentionalUserInput = {}, ) overrideResource(R.string.keyguard_enter_your_pin, ENTER_YOUR_PIN) @@ -78,11 +78,9 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onShown() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() - assertThat(message?.text).ignoringCase().isEqualTo(ENTER_YOUR_PIN) assertThat(pin).isEmpty() assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Pin) } @@ -98,6 +96,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { isInputEnabled = MutableStateFlow(true).asStateFlow(), simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Sim, + onIntentionalUserInput = {}, ) assertThat(underTest.isSimAreaVisible).isTrue() @@ -126,6 +125,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { isInputEnabled = MutableStateFlow(true).asStateFlow(), simBouncerInteractor = kosmos.simBouncerInteractor, authenticationMethod = AuthenticationMethodModel.Sim, + onIntentionalUserInput = {}, ) kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) val hintedPinLength by collectLastValue(underTest.hintedPinLength) @@ -136,20 +136,17 @@ class PinBouncerViewModelTest : SysuiTestCase() { @Test fun onPinButtonClicked() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() underTest.onPinButtonClicked(1) - assertThat(message?.text).isEmpty() assertThat(pin).containsExactly(1) } @Test fun onBackspaceButtonClicked() = testScope.runTest { - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() @@ -158,7 +155,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onBackspaceButtonClicked() - assertThat(message?.text).isEmpty() assertThat(pin).isEmpty() } @@ -183,7 +179,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun onBackspaceButtonLongPressed() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() @@ -195,7 +190,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onBackspaceButtonLongPressed() - assertThat(message?.text).isEmpty() assertThat(pin).isEmpty() assertThat(currentScene).isEqualTo(Scenes.Bouncer) } @@ -217,7 +211,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun onAuthenticateButtonClicked_whenWrong() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() @@ -230,7 +223,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onAuthenticateButtonClicked() assertThat(pin).isEmpty() - assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN) assertThat(currentScene).isEqualTo(Scenes.Bouncer) } @@ -238,7 +230,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun onAuthenticateButtonClicked_correctAfterWrong() = testScope.runTest { val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult) - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) lockDeviceAndOpenPinBouncer() @@ -248,13 +239,11 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onPinButtonClicked(4) underTest.onPinButtonClicked(5) // PIN is now wrong! underTest.onAuthenticateButtonClicked() - assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN) assertThat(pin).isEmpty() assertThat(authResult).isFalse() // Enter the correct PIN: FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked) - assertThat(message?.text).isEmpty() underTest.onAuthenticateButtonClicked() @@ -277,7 +266,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { fun onAutoConfirm_whenWrong() = testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene) - val message by collectLastValue(bouncerViewModel.message) val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true) lockDeviceAndOpenPinBouncer() @@ -290,7 +278,6 @@ class PinBouncerViewModelTest : SysuiTestCase() { ) // PIN is now wrong! assertThat(pin).isEmpty() - assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN) assertThat(currentScene).isEqualTo(Scenes.Bouncer) } @@ -390,10 +377,8 @@ class PinBouncerViewModelTest : SysuiTestCase() { private fun TestScope.switchToScene(toScene: SceneKey) { val currentScene by collectLastValue(sceneInteractor.currentScene) - val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer sceneInteractor.changeScene(toScene, "reason") - if (bouncerShown) underTest.onShown() if (bouncerHidden) underTest.onHidden() runCurrent() 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/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt index decbdaf0feee..51f99570b51e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt @@ -26,12 +26,10 @@ import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthR import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat -import kotlin.test.Test -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.junit.Test import org.junit.runner.RunWith -@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class DeviceEntryFingerprintAuthInteractorTest : SysuiTestCase() { @@ -59,17 +57,20 @@ class DeviceEntryFingerprintAuthInteractorTest : SysuiTestCase() { } @Test - fun isSensorUnderDisplay_trueForUdfpsSensorTypes() = + fun isFingerprintCurrentlyAllowedInBouncer_trueForNonUdfpsSensorTypes() = testScope.runTest { - val isSensorUnderDisplay by collectLastValue(underTest.isSensorUnderDisplay) + biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true) + + val isFingerprintCurrentlyAllowedInBouncer by + collectLastValue(underTest.isFingerprintCurrentlyAllowedOnBouncer) fingerprintPropertyRepository.supportsUdfps() - assertThat(isSensorUnderDisplay).isTrue() + assertThat(isFingerprintCurrentlyAllowedInBouncer).isFalse() fingerprintPropertyRepository.supportsRearFps() - assertThat(isSensorUnderDisplay).isFalse() + assertThat(isFingerprintCurrentlyAllowedInBouncer).isTrue() fingerprintPropertyRepository.supportsSideFps() - assertThat(isSensorUnderDisplay).isFalse() + assertThat(isFingerprintCurrentlyAllowedInBouncer).isTrue() } } 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/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt index 1c5496142fec..d1c4ec3ddacf 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt @@ -95,7 +95,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { scope = testScope.backgroundScope, ) - private val qsFlexiglassAdapter = FakeQSSceneAdapter({ mock() }) + private val qsSceneAdapter = FakeQSSceneAdapter({ mock() }) private lateinit var shadeHeaderViewModel: ShadeHeaderViewModel @@ -122,7 +122,7 @@ class ShadeSceneViewModelTest : SysuiTestCase() { applicationScope = testScope.backgroundScope, deviceEntryInteractor = deviceEntryInteractor, shadeHeaderViewModel = shadeHeaderViewModel, - qsSceneAdapter = qsFlexiglassAdapter, + qsSceneAdapter = qsSceneAdapter, notifications = kosmos.notificationsPlaceholderViewModel, mediaDataManager = mediaDataManager, shadeInteractor = kosmos.shadeInteractor, @@ -279,6 +279,20 @@ class ShadeSceneViewModelTest : SysuiTestCase() { } @Test + fun upTransitionSceneKey_customizing_noTransition() = + testScope.runTest { + val destinationScenes by collectLastValue(underTest.destinationScenes) + + qsSceneAdapter.setCustomizing(true) + assertThat( + destinationScenes!! + .keys + .filterIsInstance<Swipe>() + .filter { it.direction == SwipeDirection.Up } + ).isEmpty() + } + + @Test fun shadeMode() = testScope.runTest { val shadeMode by collectLastValue(underTest.shadeMode) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt index 2689fc111142..94539a39869e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt @@ -22,7 +22,6 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.SysuiTestCase -import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic @@ -31,6 +30,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.fakeSceneDataSource +import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationStackAppearanceViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel import com.android.systemui.testKosmos @@ -64,7 +64,7 @@ class NotificationStackAppearanceIntegrationTest : SysuiTestCase() { @Test fun updateBounds() = testScope.runTest { - val bounds by collectLastValue(appearanceViewModel.stackBounds) + val clipping by collectLastValue(appearanceViewModel.stackClipping) val top = 200f val left = 0f @@ -76,15 +76,8 @@ class NotificationStackAppearanceIntegrationTest : SysuiTestCase() { right = right, bottom = bottom ) - assertThat(bounds) - .isEqualTo( - NotificationContainerBounds( - left = left, - top = top, - right = right, - bottom = bottom - ) - ) + assertThat(clipping?.bounds) + .isEqualTo(StackBounds(left = left, top = top, right = right, bottom = bottom)) } @Test 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 ffe6e6df6b48..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 @@ -19,10 +19,13 @@ package com.android.systemui.statusbar.notification.stack.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.common.shared.model.NotificationContainerBounds 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 @@ -43,29 +45,39 @@ class NotificationStackAppearanceInteractorTest : SysuiTestCase() { val stackBounds by collectLastValue(underTest.stackBounds) val bounds1 = - NotificationContainerBounds( + StackBounds( top = 100f, bottom = 200f, - isAnimated = true, ) underTest.setStackBounds(bounds1) assertThat(stackBounds).isEqualTo(bounds1) val bounds2 = - NotificationContainerBounds( + StackBounds( top = 200f, bottom = 300f, - isAnimated = false, ) underTest.setStackBounds(bounds2) 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 { underTest.setStackBounds( - NotificationContainerBounds( + StackBounds( top = 100f, bottom = 99f, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt index 693de55211f8..2ccc8b44eff8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt @@ -22,6 +22,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor +import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -36,9 +37,9 @@ class NotificationsPlaceholderViewModelTest : SysuiTestCase() { fun onBoundsChanged_setsNotificationContainerBounds() { underTest.onBoundsChanged(left = 5f, top = 5f, right = 5f, bottom = 5f) assertThat(kosmos.keyguardInteractor.notificationContainerBounds.value) - .isEqualTo(NotificationContainerBounds(left = 5f, top = 5f, right = 5f, bottom = 5f)) + .isEqualTo(NotificationContainerBounds(top = 5f, bottom = 5f)) assertThat(kosmos.notificationStackAppearanceInteractor.stackBounds.value) - .isEqualTo(NotificationContainerBounds(left = 5f, top = 5f, right = 5f, bottom = 5f)) + .isEqualTo(StackBounds(left = 5f, top = 5f, right = 5f, bottom = 5f)) } @Test fun onContentTopChanged_setsContentTop() { 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/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractorTest.kt new file mode 100644 index 000000000000..b5c580978737 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractorTest.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.volume.panel.component.mediaoutput.domain.interactor + +import android.os.Handler +import android.testing.TestableLooper +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.testCase +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.volume.localMediaController +import com.android.systemui.volume.mediaControllerRepository +import com.android.systemui.volume.mediaOutputInteractor +import com.android.systemui.volume.remoteMediaController +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class MediaDeviceSessionInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + private lateinit var underTest: MediaDeviceSessionInteractor + + @Before + fun setup() { + with(kosmos) { + mediaControllerRepository.setActiveSessions( + listOf(localMediaController, remoteMediaController) + ) + + underTest = + MediaDeviceSessionInteractor( + testScope.testScheduler, + Handler(TestableLooper.get(kosmos.testCase).looper), + mediaControllerRepository, + ) + } + } + + @Test + fun playbackInfo_returnsPlaybackInfo() { + with(kosmos) { + testScope.runTest { + val session by collectLastValue(mediaOutputInteractor.defaultActiveMediaSession) + runCurrent() + val info by collectLastValue(underTest.playbackInfo(session!!)) + runCurrent() + + assertThat(info).isEqualTo(localMediaController.playbackInfo) + } + } + } + + @Test + fun playbackState_returnsPlaybackState() { + with(kosmos) { + testScope.runTest { + val session by collectLastValue(mediaOutputInteractor.defaultActiveMediaSession) + runCurrent() + val state by collectLastValue(underTest.playbackState(session!!)) + runCurrent() + + assertThat(state).isEqualTo(localMediaController.playbackState) + } + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt index dcf635e622f4..6f7f20b47199 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt @@ -29,9 +29,10 @@ import com.android.systemui.res.R import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.volume.localMediaController import com.android.systemui.volume.localMediaRepository -import com.android.systemui.volume.mediaController import com.android.systemui.volume.mediaControllerRepository +import com.android.systemui.volume.mediaDeviceSessionInteractor import com.android.systemui.volume.mediaOutputActionsInteractor import com.android.systemui.volume.mediaOutputInteractor import com.android.systemui.volume.panel.volumePanelViewModel @@ -63,6 +64,7 @@ class MediaOutputViewModelTest : SysuiTestCase() { testScope.backgroundScope, volumePanelViewModel, mediaOutputActionsInteractor, + mediaDeviceSessionInteractor, mediaOutputInteractor, ) @@ -74,11 +76,11 @@ class MediaOutputViewModelTest : SysuiTestCase() { ) } - whenever(mediaController.packageName).thenReturn("test.pkg") - whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {})) - whenever(mediaController.playbackState).then { playbackStateBuilder.build() } + whenever(localMediaController.packageName).thenReturn("test.pkg") + whenever(localMediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {})) + whenever(localMediaController.playbackState).then { playbackStateBuilder.build() } - mediaControllerRepository.setActiveLocalMediaController(mediaController) + mediaControllerRepository.setActiveSessions(listOf(localMediaController)) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt index 1ed7f5d04622..2f69942aa459 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt @@ -32,8 +32,8 @@ import com.android.systemui.media.spatializerRepository import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.volume.localMediaController import com.android.systemui.volume.localMediaRepository -import com.android.systemui.volume.mediaController import com.android.systemui.volume.mediaControllerRepository import com.android.systemui.volume.panel.component.spatial.spatialAudioComponentInteractor import com.google.common.truth.Truth.assertThat @@ -66,11 +66,11 @@ class SpatialAudioAvailabilityCriteriaTest : SysuiTestCase() { } ) - whenever(mediaController.packageName).thenReturn("test.pkg") - whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {})) - whenever(mediaController.playbackState).thenReturn(PlaybackState.Builder().build()) + whenever(localMediaController.packageName).thenReturn("test.pkg") + whenever(localMediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {})) + whenever(localMediaController.playbackState).thenReturn(PlaybackState.Builder().build()) - mediaControllerRepository.setActiveLocalMediaController(mediaController) + mediaControllerRepository.setActiveSessions(listOf(localMediaController)) underTest = SpatialAudioAvailabilityCriteria(spatialAudioComponentInteractor) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt index 281b03d69536..e36ae60ebe7d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt @@ -34,8 +34,8 @@ import com.android.systemui.media.spatializerRepository import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.volume.localMediaController import com.android.systemui.volume.localMediaRepository -import com.android.systemui.volume.mediaController import com.android.systemui.volume.mediaControllerRepository import com.android.systemui.volume.mediaOutputInteractor import com.android.systemui.volume.panel.component.spatial.domain.model.SpatialAudioAvailabilityModel @@ -70,11 +70,11 @@ class SpatialAudioComponentInteractorTest : SysuiTestCase() { } ) - whenever(mediaController.packageName).thenReturn("test.pkg") - whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {})) - whenever(mediaController.playbackState).thenReturn(PlaybackState.Builder().build()) + whenever(localMediaController.packageName).thenReturn("test.pkg") + whenever(localMediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {})) + whenever(localMediaController.playbackState).thenReturn(PlaybackState.Builder().build()) - mediaControllerRepository.setActiveLocalMediaController(mediaController) + mediaControllerRepository.setActiveSessions(listOf(localMediaController)) underTest = SpatialAudioComponentInteractor( 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/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index e621ffe4cbc4..86f64f809e42 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -631,7 +631,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS @Nullable public ClockController getClock() { if (migrateClocksToBlueprint()) { - return mKeyguardClockInteractor.getClock(); + return mKeyguardClockInteractor.getCurrentClock().getValue(); } else { return mClockEventController.getClock(); } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt index d849b3a44519..94e085479675 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt @@ -20,7 +20,6 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import javax.inject.Inject -import kotlinx.coroutines.flow.MutableStateFlow /** Provides access to bouncer-related application state. */ @SysUISingleton @@ -29,9 +28,6 @@ class BouncerRepository constructor( private val flags: FeatureFlagsClassic, ) { - /** The user-facing message to show in the bouncer. */ - val message = MutableStateFlow<String?>(null) - /** Whether the user switcher should be displayed within the bouncer UI on large screens. */ val isUserSwitcherVisible: Boolean get() = flags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER) diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt index d8be1afc4dd6..aeb564d53195 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt @@ -16,13 +16,8 @@ package com.android.systemui.bouncer.domain.interactor -import android.content.Context import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.domain.interactor.AuthenticationResult -import com.android.systemui.authentication.shared.model.AuthenticationMethodModel -import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Password -import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern -import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Sim import com.android.systemui.bouncer.data.repository.BouncerRepository import com.android.systemui.classifier.FalsingClassifier @@ -31,7 +26,6 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.res.R import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async @@ -41,7 +35,6 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch /** Encapsulates business logic and application state accessing use-cases. */ @SysUISingleton @@ -49,16 +42,14 @@ class BouncerInteractor @Inject constructor( @Application private val applicationScope: CoroutineScope, - @Application private val applicationContext: Context, private val repository: BouncerRepository, private val authenticationInteractor: AuthenticationInteractor, private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor, private val falsingInteractor: FalsingInteractor, private val powerInteractor: PowerInteractor, - private val simBouncerInteractor: SimBouncerInteractor, ) { - /** The user-facing message to show in the bouncer when lockout is not active. */ - val message: StateFlow<String?> = repository.message + private val _onIncorrectBouncerInput = MutableSharedFlow<Unit>() + val onIncorrectBouncerInput: SharedFlow<Unit> = _onIncorrectBouncerInput /** Whether the auto confirm feature is enabled for the currently-selected user. */ val isAutoConfirmEnabled: StateFlow<Boolean> = authenticationInteractor.isAutoConfirmEnabled @@ -119,25 +110,6 @@ constructor( ) } - fun setMessage(message: String?) { - repository.message.value = message - } - - /** - * Resets the user-facing message back to the default according to the current authentication - * method. - */ - fun resetMessage() { - applicationScope.launch { - setMessage(promptMessage(authenticationInteractor.getAuthenticationMethod())) - } - } - - /** Removes the user-facing message. */ - fun clearMessage() { - setMessage(null) - } - /** * Attempts to authenticate based on the given user input. * @@ -176,50 +148,17 @@ constructor( .async { authenticationInteractor.authenticate(input, tryAutoConfirm) } .await() - if (authenticationInteractor.lockoutEndTimestamp != null) { - clearMessage() - } else if ( + if ( authResult == AuthenticationResult.FAILED || (authResult == AuthenticationResult.SKIPPED && !tryAutoConfirm) ) { - showWrongInputMessage() + _onIncorrectBouncerInput.emit(Unit) } return authResult } - /** - * Shows the a message notifying the user that their credentials input is wrong. - * - * Callers should use this instead of [authenticate] when they know ahead of time that an auth - * attempt will fail but aren't interested in the other side effects like triggering lockout. - * For example, if the user entered a pattern that's too short, the system can show the error - * message without having the attempt trigger lockout. - */ - private suspend fun showWrongInputMessage() { - setMessage(wrongInputMessage(authenticationInteractor.getAuthenticationMethod())) - } - /** Notifies that the input method editor (software keyboard) has been hidden by the user. */ suspend fun onImeHiddenByUser() { _onImeHiddenByUser.emit(Unit) } - - private fun promptMessage(authMethod: AuthenticationMethodModel): String { - return when (authMethod) { - is Sim -> simBouncerInteractor.getDefaultMessage() - is Pin -> applicationContext.getString(R.string.keyguard_enter_your_pin) - is Password -> applicationContext.getString(R.string.keyguard_enter_your_password) - is Pattern -> applicationContext.getString(R.string.keyguard_enter_your_pattern) - else -> "" - } - } - - private fun wrongInputMessage(authMethod: AuthenticationMethodModel): String { - return when (authMethod) { - is Pin -> applicationContext.getString(R.string.kg_wrong_pin) - is Password -> applicationContext.getString(R.string.kg_wrong_password) - is Pattern -> applicationContext.getString(R.string.kg_wrong_pattern) - else -> "" - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt index 7f6fc914e92b..d20c60724822 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt @@ -33,15 +33,17 @@ import com.android.systemui.bouncer.shared.model.Message import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor import com.android.systemui.flags.SystemPropertiesHelper import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository -import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.TrustRepository import com.android.systemui.user.data.repository.UserRepository -import com.android.systemui.util.kotlin.Quint +import com.android.systemui.util.kotlin.Sextuple +import com.android.systemui.util.kotlin.combine import javax.inject.Inject import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -56,6 +58,7 @@ private const val REBOOT_MAINLINE_UPDATE = "reboot,mainline_update" private const val TAG = "BouncerMessageInteractor" /** Handles business logic for the primary bouncer message area. */ +@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class BouncerMessageInteractor @Inject @@ -63,23 +66,24 @@ constructor( private val repository: BouncerMessageRepository, private val userRepository: UserRepository, private val countDownTimerUtil: CountDownTimerUtil, - private val updateMonitor: KeyguardUpdateMonitor, + updateMonitor: KeyguardUpdateMonitor, trustRepository: TrustRepository, biometricSettingsRepository: BiometricSettingsRepository, private val systemPropertiesHelper: SystemPropertiesHelper, primaryBouncerInteractor: PrimaryBouncerInteractor, @Application private val applicationScope: CoroutineScope, private val facePropertyRepository: FacePropertyRepository, - deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository, + private val deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, faceAuthRepository: DeviceEntryFaceAuthRepository, private val securityModel: KeyguardSecurityModel, ) { - private val isFingerprintAuthCurrentlyAllowed = - deviceEntryFingerprintAuthRepository.isLockedOut - .isFalse() - .and(biometricSettingsRepository.isFingerprintAuthCurrentlyAllowed) - .stateIn(applicationScope, SharingStarted.Eagerly, false) + private val isFingerprintAuthCurrentlyAllowedOnBouncer = + deviceEntryFingerprintAuthInteractor.isFingerprintCurrentlyAllowedOnBouncer.stateIn( + applicationScope, + SharingStarted.Eagerly, + false + ) private val currentSecurityMode get() = securityModel.getSecurityMode(currentUserId) @@ -99,13 +103,13 @@ constructor( BiometricSourceType.FACE -> BouncerMessageStrings.incorrectFaceInput( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() else -> BouncerMessageStrings.defaultMessage( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() } @@ -144,11 +148,12 @@ constructor( biometricSettingsRepository.authenticationFlags, trustRepository.isCurrentUserTrustManaged, isAnyBiometricsEnabledAndEnrolled, - deviceEntryFingerprintAuthRepository.isLockedOut, + deviceEntryFingerprintAuthInteractor.isLockedOut, faceAuthRepository.isLockedOut, - ::Quint + isFingerprintAuthCurrentlyAllowedOnBouncer, + ::Sextuple ) - .map { (flags, _, biometricsEnrolledAndEnabled, fpLockedOut, faceLockedOut) -> + .map { (flags, _, biometricsEnrolledAndEnabled, fpLockedOut, faceLockedOut, _) -> val isTrustUsuallyManaged = trustRepository.isCurrentUserTrustUsuallyManaged.value val trustOrBiometricsAvailable = (isTrustUsuallyManaged || biometricsEnrolledAndEnabled) @@ -193,14 +198,14 @@ constructor( } else { BouncerMessageStrings.faceLockedOut( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() } } else if (flags.isSomeAuthRequiredAfterAdaptiveAuthRequest) { BouncerMessageStrings.authRequiredAfterAdaptiveAuthRequest( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() } else if ( @@ -209,19 +214,19 @@ constructor( ) { BouncerMessageStrings.nonStrongAuthTimeout( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() } else if (isTrustUsuallyManaged && flags.someAuthRequiredAfterUserRequest) { BouncerMessageStrings.trustAgentDisabled( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() } else if (isTrustUsuallyManaged && flags.someAuthRequiredAfterTrustAgentExpired) { BouncerMessageStrings.trustAgentDisabled( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() } else if (trustOrBiometricsAvailable && flags.isInUserLockdown) { @@ -265,7 +270,7 @@ constructor( repository.setMessage( BouncerMessageStrings.incorrectSecurityInput( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() ) @@ -274,14 +279,22 @@ constructor( fun setFingerprintAcquisitionMessage(value: String?) { if (!Flags.revampedBouncerMessages()) return repository.setMessage( - defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value) + defaultMessage( + currentSecurityMode, + value, + isFingerprintAuthCurrentlyAllowedOnBouncer.value + ) ) } fun setFaceAcquisitionMessage(value: String?) { if (!Flags.revampedBouncerMessages()) return repository.setMessage( - defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value) + defaultMessage( + currentSecurityMode, + value, + isFingerprintAuthCurrentlyAllowedOnBouncer.value + ) ) } @@ -289,7 +302,11 @@ constructor( if (!Flags.revampedBouncerMessages()) return repository.setMessage( - defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value) + defaultMessage( + currentSecurityMode, + value, + isFingerprintAuthCurrentlyAllowedOnBouncer.value + ) ) } @@ -297,7 +314,7 @@ constructor( get() = BouncerMessageStrings.defaultMessage( currentSecurityMode.toAuthModel(), - isFingerprintAuthCurrentlyAllowed.value + isFingerprintAuthCurrentlyAllowedOnBouncer.value ) .toMessage() @@ -355,11 +372,6 @@ open class CountDownTimerUtil @Inject constructor() { private fun Flow<Boolean>.or(anotherFlow: Flow<Boolean>) = this.combine(anotherFlow) { a, b -> a || b } -private fun Flow<Boolean>.and(anotherFlow: Flow<Boolean>) = - this.combine(anotherFlow) { a, b -> a && b } - -private fun Flow<Boolean>.isFalse() = this.map { !it } - private fun defaultMessage( securityMode: SecurityMode, secondaryMessage: String?, diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt index f3903ded7cf4..aebc50f92e8d 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt @@ -18,6 +18,7 @@ package com.android.systemui.bouncer.ui import android.app.AlertDialog import android.content.Context +import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModelModule import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModelModule import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -30,6 +31,7 @@ import dagger.Provides includes = [ BouncerViewModelModule::class, + BouncerMessageViewModelModule::class, ], ) interface BouncerViewModule { diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt index 0d7f6dcce1c7..4fbf735a62a2 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt @@ -57,17 +57,11 @@ sealed class AuthMethodBouncerViewModel( */ @get:StringRes abstract val lockoutMessageId: Int - /** Notifies that the UI has been shown to the user. */ - fun onShown() { - interactor.resetMessage() - } - /** * Notifies that the UI has been hidden from the user (after any transitions have completed). */ open fun onHidden() { clearInput() - interactor.resetMessage() } /** Notifies that the user has placed down a pointer. */ diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt new file mode 100644 index 000000000000..6cb9b16e2f9b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt @@ -0,0 +1,436 @@ +/* + * 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.bouncer.ui.viewmodel + +import android.content.Context +import android.util.PluralsMessageFormatter +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor +import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags +import com.android.systemui.bouncer.shared.model.BouncerMessagePair +import com.android.systemui.bouncer.shared.model.BouncerMessageStrings +import com.android.systemui.bouncer.shared.model.primaryMessage +import com.android.systemui.bouncer.shared.model.secondaryMessage +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.deviceentry.domain.interactor.BiometricMessageInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason +import com.android.systemui.deviceentry.shared.model.FaceFailureMessage +import com.android.systemui.deviceentry.shared.model.FaceLockoutMessage +import com.android.systemui.deviceentry.shared.model.FaceTimeoutMessage +import com.android.systemui.deviceentry.shared.model.FingerprintFailureMessage +import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage +import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown +import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel +import com.android.systemui.user.ui.viewmodel.UserViewModel +import com.android.systemui.util.kotlin.Utils.Companion.sample +import com.android.systemui.util.time.SystemClock +import dagger.Module +import dagger.Provides +import kotlin.math.ceil +import kotlin.math.max +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +/** Holds UI state for the 2-line status message shown on the bouncer. */ +@OptIn(ExperimentalCoroutinesApi::class) +class BouncerMessageViewModel( + @Application private val applicationContext: Context, + @Application private val applicationScope: CoroutineScope, + private val bouncerInteractor: BouncerInteractor, + private val simBouncerInteractor: SimBouncerInteractor, + private val authenticationInteractor: AuthenticationInteractor, + selectedUser: Flow<UserViewModel>, + private val clock: SystemClock, + private val biometricMessageInteractor: BiometricMessageInteractor, + private val faceAuthInteractor: DeviceEntryFaceAuthInteractor, + private val deviceEntryInteractor: DeviceEntryInteractor, + private val fingerprintInteractor: DeviceEntryFingerprintAuthInteractor, + flags: ComposeBouncerFlags, +) { + /** + * A message shown when the user has attempted the wrong credential too many times and now must + * wait a while before attempting to authenticate again. + * + * This is updated every second (countdown) during the lockout. When lockout is not active, this + * is `null` and no lockout message should be shown. + */ + private val lockoutMessage: MutableStateFlow<MessageViewModel?> = MutableStateFlow(null) + + /** Whether there is a lockout message that is available to be shown in the status message. */ + val isLockoutMessagePresent: Flow<Boolean> = lockoutMessage.map { it != null } + + /** The user-facing message to show in the bouncer. */ + val message: MutableStateFlow<MessageViewModel?> = MutableStateFlow(null) + + /** Initializes the bouncer message to default whenever it is shown. */ + fun onShown() { + showDefaultMessage() + } + + /** Reset the message shown on the bouncer to the default message. */ + fun showDefaultMessage() { + resetToDefault.tryEmit(Unit) + } + + private val resetToDefault = MutableSharedFlow<Unit>(replay = 1) + + private var lockoutCountdownJob: Job? = null + + private fun defaultBouncerMessageInitializer() { + applicationScope.launch { + resetToDefault.emit(Unit) + authenticationInteractor.authenticationMethod + .flatMapLatest { authMethod -> + if (authMethod == AuthenticationMethodModel.Sim) { + resetToDefault.map { + MessageViewModel(simBouncerInteractor.getDefaultMessage()) + } + } else if (authMethod.isSecure) { + combine( + deviceEntryInteractor.deviceEntryRestrictionReason, + lockoutMessage, + fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer, + resetToDefault, + ) { deviceEntryRestrictedReason, lockoutMsg, isFpAllowedInBouncer, _ -> + lockoutMsg + ?: deviceEntryRestrictedReason.toMessage( + authMethod, + isFpAllowedInBouncer + ) + } + } else { + emptyFlow() + } + } + .collectLatest { messageViewModel -> message.value = messageViewModel } + } + } + + private fun listenForSimBouncerEvents() { + // Listen for any events from the SIM bouncer and update the message shown on the bouncer. + applicationScope.launch { + authenticationInteractor.authenticationMethod + .flatMapLatest { authMethod -> + if (authMethod == AuthenticationMethodModel.Sim) { + simBouncerInteractor.bouncerMessageChanged.map { simMsg -> + simMsg?.let { MessageViewModel(it) } + } + } else { + emptyFlow() + } + } + .collectLatest { + if (it != null) { + message.value = it + } else { + resetToDefault.emit(Unit) + } + } + } + } + + private fun listenForFaceMessages() { + // Listen for any events from face authentication and update the message shown on the + // bouncer. + applicationScope.launch { + biometricMessageInteractor.faceMessage + .sample( + authenticationInteractor.authenticationMethod, + fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer, + ) + .collectLatest { (faceMessage, authMethod, fingerprintAllowedOnBouncer) -> + val isFaceAuthStrong = faceAuthInteractor.isFaceAuthStrong() + val defaultPrimaryMessage = + BouncerMessageStrings.defaultMessage( + authMethod, + fingerprintAllowedOnBouncer + ) + .primaryMessage + .toResString() + message.value = + when (faceMessage) { + is FaceTimeoutMessage -> + MessageViewModel( + text = defaultPrimaryMessage, + secondaryText = faceMessage.message, + isUpdateAnimated = true + ) + is FaceLockoutMessage -> + if (isFaceAuthStrong) + BouncerMessageStrings.class3AuthLockedOut(authMethod) + .toMessage() + else + BouncerMessageStrings.faceLockedOut( + authMethod, + fingerprintAllowedOnBouncer + ) + .toMessage() + is FaceFailureMessage -> + BouncerMessageStrings.incorrectFaceInput( + authMethod, + fingerprintAllowedOnBouncer + ) + .toMessage() + else -> + MessageViewModel( + text = defaultPrimaryMessage, + secondaryText = faceMessage.message, + isUpdateAnimated = false + ) + } + delay(MESSAGE_DURATION) + resetToDefault.emit(Unit) + } + } + } + + private fun listenForFingerprintMessages() { + applicationScope.launch { + // Listen for any events from fingerprint authentication and update the message shown + // on the bouncer. + biometricMessageInteractor.fingerprintMessage + .sample( + authenticationInteractor.authenticationMethod, + fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer + ) + .collectLatest { (fingerprintMessage, authMethod, isFingerprintAllowed) -> + val defaultPrimaryMessage = + BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowed) + .primaryMessage + .toResString() + message.value = + when (fingerprintMessage) { + is FingerprintLockoutMessage -> + BouncerMessageStrings.class3AuthLockedOut(authMethod).toMessage() + is FingerprintFailureMessage -> + BouncerMessageStrings.incorrectFingerprintInput(authMethod) + .toMessage() + else -> + MessageViewModel( + text = defaultPrimaryMessage, + secondaryText = fingerprintMessage.message, + isUpdateAnimated = false + ) + } + delay(MESSAGE_DURATION) + resetToDefault.emit(Unit) + } + } + } + + private fun listenForBouncerEvents() { + // Keeps the lockout message up-to-date. + applicationScope.launch { + bouncerInteractor.onLockoutStarted.collect { startLockoutCountdown() } + } + + // Listens to relevant bouncer events + applicationScope.launch { + bouncerInteractor.onIncorrectBouncerInput + .sample( + authenticationInteractor.authenticationMethod, + fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer + ) + .collectLatest { (_, authMethod, isFingerprintAllowed) -> + message.emit( + BouncerMessageStrings.incorrectSecurityInput( + authMethod, + isFingerprintAllowed + ) + .toMessage() + ) + delay(MESSAGE_DURATION) + resetToDefault.emit(Unit) + } + } + } + + private fun DeviceEntryRestrictionReason?.toMessage( + authMethod: AuthenticationMethodModel, + isFingerprintAllowedOnBouncer: Boolean, + ): MessageViewModel { + return when (this) { + DeviceEntryRestrictionReason.UserLockdown -> + BouncerMessageStrings.authRequiredAfterUserLockdown(authMethod) + DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot -> + BouncerMessageStrings.authRequiredAfterReboot(authMethod) + DeviceEntryRestrictionReason.PolicyLockdown -> + BouncerMessageStrings.authRequiredAfterAdminLockdown(authMethod) + DeviceEntryRestrictionReason.UnattendedUpdate -> + BouncerMessageStrings.authRequiredForUnattendedUpdate(authMethod) + DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate -> + BouncerMessageStrings.authRequiredForMainlineUpdate(authMethod) + DeviceEntryRestrictionReason.SecurityTimeout -> + BouncerMessageStrings.authRequiredAfterPrimaryAuthTimeout(authMethod) + DeviceEntryRestrictionReason.StrongBiometricsLockedOut -> + BouncerMessageStrings.class3AuthLockedOut(authMethod) + DeviceEntryRestrictionReason.NonStrongFaceLockedOut -> + BouncerMessageStrings.faceLockedOut(authMethod, isFingerprintAllowedOnBouncer) + DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout -> + BouncerMessageStrings.nonStrongAuthTimeout( + authMethod, + isFingerprintAllowedOnBouncer + ) + DeviceEntryRestrictionReason.TrustAgentDisabled -> + BouncerMessageStrings.trustAgentDisabled(authMethod, isFingerprintAllowedOnBouncer) + DeviceEntryRestrictionReason.AdaptiveAuthRequest -> + BouncerMessageStrings.authRequiredAfterAdaptiveAuthRequest( + authMethod, + isFingerprintAllowedOnBouncer + ) + else -> BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowedOnBouncer) + }.toMessage() + } + + private fun BouncerMessagePair.toMessage(): MessageViewModel { + val primaryMsg = this.primaryMessage.toResString() + val secondaryMsg = + if (this.secondaryMessage == 0) "" else this.secondaryMessage.toResString() + return MessageViewModel(primaryMsg, secondaryText = secondaryMsg, isUpdateAnimated = true) + } + + /** Shows the countdown message and refreshes it every second. */ + private fun startLockoutCountdown() { + lockoutCountdownJob?.cancel() + lockoutCountdownJob = + applicationScope.launch { + authenticationInteractor.authenticationMethod.collectLatest { authMethod -> + do { + val remainingSeconds = remainingLockoutSeconds() + val authLockedOutMsg = + BouncerMessageStrings.primaryAuthLockedOut(authMethod) + lockoutMessage.value = + if (remainingSeconds > 0) { + MessageViewModel( + text = + kg_too_many_failed_attempts_countdown.toPluralString( + mutableMapOf<String, Any>( + Pair("count", remainingSeconds) + ) + ), + secondaryText = authLockedOutMsg.secondaryMessage.toResString(), + isUpdateAnimated = false + ) + } else { + null + } + delay(1.seconds) + } while (remainingSeconds > 0) + lockoutCountdownJob = null + } + } + } + + private fun remainingLockoutSeconds(): Int { + val endTimestampMs = authenticationInteractor.lockoutEndTimestamp ?: 0 + val remainingMs = max(0, endTimestampMs - clock.elapsedRealtime()) + return ceil(remainingMs / 1000f).toInt() + } + + private fun Int.toPluralString(formatterArgs: Map<String, Any>): String = + PluralsMessageFormatter.format(applicationContext.resources, formatterArgs, this) + + private fun Int.toResString(): String = applicationContext.getString(this) + + init { + if (flags.isComposeBouncerOrSceneContainerEnabled()) { + applicationScope.launch { + // Update the lockout countdown whenever the selected user is switched. + selectedUser.collect { startLockoutCountdown() } + } + + defaultBouncerMessageInitializer() + + listenForSimBouncerEvents() + listenForBouncerEvents() + listenForFaceMessages() + listenForFingerprintMessages() + } + } + + companion object { + private const val MESSAGE_DURATION = 2000L + } +} + +/** Data class that represents the status message show on the bouncer. */ +data class MessageViewModel( + val text: String, + val secondaryText: String? = null, + /** + * Whether updates to the message should be cross-animated from one message to another. + * + * If `false`, no animation should be applied, the message text should just be replaced + * instantly. + */ + val isUpdateAnimated: Boolean = true, +) + +@OptIn(ExperimentalCoroutinesApi::class) +@Module +object BouncerMessageViewModelModule { + + @Provides + @SysUISingleton + fun viewModel( + @Application applicationContext: Context, + @Application applicationScope: CoroutineScope, + bouncerInteractor: BouncerInteractor, + simBouncerInteractor: SimBouncerInteractor, + authenticationInteractor: AuthenticationInteractor, + clock: SystemClock, + biometricMessageInteractor: BiometricMessageInteractor, + faceAuthInteractor: DeviceEntryFaceAuthInteractor, + deviceEntryInteractor: DeviceEntryInteractor, + fingerprintInteractor: DeviceEntryFingerprintAuthInteractor, + flags: ComposeBouncerFlags, + userSwitcherViewModel: UserSwitcherViewModel, + ): BouncerMessageViewModel { + return BouncerMessageViewModel( + applicationContext = applicationContext, + applicationScope = applicationScope, + bouncerInteractor = bouncerInteractor, + simBouncerInteractor = simBouncerInteractor, + authenticationInteractor = authenticationInteractor, + clock = clock, + biometricMessageInteractor = biometricMessageInteractor, + faceAuthInteractor = faceAuthInteractor, + deviceEntryInteractor = deviceEntryInteractor, + fingerprintInteractor = fingerprintInteractor, + flags = flags, + selectedUser = userSwitcherViewModel.selectedUser, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt index 62875783ef5f..5c07cc57c620 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt @@ -21,7 +21,6 @@ import android.app.admin.DevicePolicyResources import android.content.Context import android.graphics.Bitmap import androidx.core.graphics.drawable.toBitmap -import com.android.internal.R import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.authentication.shared.model.AuthenticationWipeModel @@ -40,18 +39,12 @@ import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.user.ui.viewmodel.UserActionViewModel import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel import com.android.systemui.user.ui.viewmodel.UserViewModel -import com.android.systemui.util.time.SystemClock import dagger.Module import dagger.Provides -import kotlin.math.ceil -import kotlin.math.max -import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -72,13 +65,13 @@ class BouncerViewModel( private val simBouncerInteractor: SimBouncerInteractor, private val authenticationInteractor: AuthenticationInteractor, private val selectedUserInteractor: SelectedUserInteractor, + private val devicePolicyManager: DevicePolicyManager, + bouncerMessageViewModel: BouncerMessageViewModel, flags: ComposeBouncerFlags, selectedUser: Flow<UserViewModel>, users: Flow<List<UserViewModel>>, userSwitcherMenu: Flow<List<UserActionViewModel>>, actionButton: Flow<BouncerActionButtonModel?>, - private val clock: SystemClock, - private val devicePolicyManager: DevicePolicyManager, ) { val selectedUserImage: StateFlow<Bitmap?> = selectedUser @@ -89,6 +82,8 @@ class BouncerViewModel( initialValue = null, ) + val message: BouncerMessageViewModel = bouncerMessageViewModel + val userSwitcherDropdown: StateFlow<List<UserSwitcherDropdownItemViewModel>> = combine( users, @@ -163,24 +158,6 @@ class BouncerViewModel( ) /** - * A message shown when the user has attempted the wrong credential too many times and now must - * wait a while before attempting to authenticate again. - * - * This is updated every second (countdown) during the lockout duration. When lockout is not - * active, this is `null` and no lockout message should be shown. - */ - private val lockoutMessage = MutableStateFlow<String?>(null) - - /** The user-facing message to show in the bouncer. */ - val message: StateFlow<MessageViewModel> = - combine(bouncerInteractor.message, lockoutMessage) { _, _ -> createMessageViewModel() } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = createMessageViewModel(), - ) - - /** * The bouncer action button (Return to Call / Emergency Call). If `null`, the button should not * be shown. */ @@ -222,31 +199,16 @@ class BouncerViewModel( ) private val isInputEnabled: StateFlow<Boolean> = - lockoutMessage - .map { it == null } + bouncerMessageViewModel.isLockoutMessagePresent + .map { lockoutMessagePresent -> !lockoutMessagePresent } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = authenticationInteractor.lockoutEndTimestamp == null, ) - private var lockoutCountdownJob: Job? = null - init { if (flags.isComposeBouncerOrSceneContainerEnabled()) { - // Keeps the lockout dialog up-to-date. - applicationScope.launch { - bouncerInteractor.onLockoutStarted.collect { - showLockoutDialog() - startLockoutCountdown() - } - } - - applicationScope.launch { - // Update the lockout countdown whenever the selected user is switched. - selectedUser.collect { startLockoutCountdown() } - } - // Keeps the upcoming wipe dialog up-to-date. applicationScope.launch { authenticationInteractor.upcomingWipe.collect { wipeModel -> @@ -256,48 +218,6 @@ class BouncerViewModel( } } - private fun showLockoutDialog() { - applicationScope.launch { - val failedAttempts = authenticationInteractor.failedAuthenticationAttempts.value - lockoutDialogMessage.value = - authMethodViewModel.value?.lockoutMessageId?.let { messageId -> - applicationContext.getString( - messageId, - failedAttempts, - remainingLockoutSeconds() - ) - } - } - } - - /** Shows the countdown message and refreshes it every second. */ - private fun startLockoutCountdown() { - lockoutCountdownJob?.cancel() - lockoutCountdownJob = - applicationScope.launch { - do { - val remainingSeconds = remainingLockoutSeconds() - lockoutMessage.value = - if (remainingSeconds > 0) { - applicationContext.getString( - R.string.lockscreen_too_many_failed_attempts_countdown, - remainingSeconds, - ) - } else { - null - } - delay(1.seconds) - } while (remainingSeconds > 0) - lockoutCountdownJob = null - } - } - - private fun remainingLockoutSeconds(): Int { - val endTimestampMs = authenticationInteractor.lockoutEndTimestamp ?: 0 - val remainingMs = max(0, endTimestampMs - clock.elapsedRealtime()) - return ceil(remainingMs / 1000f).toInt() - } - private fun isSideBySideSupported(authMethod: AuthMethodBouncerViewModel?): Boolean { return isUserSwitcherVisible || authMethod !is PasswordBouncerViewModel } @@ -306,15 +226,6 @@ class BouncerViewModel( return authMethod !is PasswordBouncerViewModel } - private fun createMessageViewModel(): MessageViewModel { - val isLockedOut = lockoutMessage.value != null - return MessageViewModel( - // A lockout message takes precedence over the non-lockout message. - text = lockoutMessage.value ?: bouncerInteractor.message.value ?: "", - isUpdateAnimated = !isLockedOut, - ) - } - private fun getChildViewModel( authenticationMethod: AuthenticationMethodModel, ): AuthMethodBouncerViewModel? { @@ -336,7 +247,8 @@ class BouncerViewModel( interactor = bouncerInteractor, isInputEnabled = isInputEnabled, simBouncerInteractor = simBouncerInteractor, - authenticationMethod = authenticationMethod + authenticationMethod = authenticationMethod, + onIntentionalUserInput = ::onIntentionalUserInput ) is AuthenticationMethodModel.Sim -> PinBouncerViewModel( @@ -346,6 +258,7 @@ class BouncerViewModel( isInputEnabled = isInputEnabled, simBouncerInteractor = simBouncerInteractor, authenticationMethod = authenticationMethod, + onIntentionalUserInput = ::onIntentionalUserInput ) is AuthenticationMethodModel.Password -> PasswordBouncerViewModel( @@ -354,6 +267,7 @@ class BouncerViewModel( interactor = bouncerInteractor, inputMethodInteractor = inputMethodInteractor, selectedUserInteractor = selectedUserInteractor, + onIntentionalUserInput = ::onIntentionalUserInput ) is AuthenticationMethodModel.Pattern -> PatternBouncerViewModel( @@ -361,11 +275,17 @@ class BouncerViewModel( viewModelScope = newViewModelScope, interactor = bouncerInteractor, isInputEnabled = isInputEnabled, + onIntentionalUserInput = ::onIntentionalUserInput ) else -> null } } + private fun onIntentionalUserInput() { + message.showDefaultMessage() + bouncerInteractor.onIntentionalUserInput() + } + private fun createChildCoroutineScope(parentScope: CoroutineScope): CoroutineScope { return CoroutineScope( SupervisorJob(parent = parentScope.coroutineContext.job) + mainDispatcher @@ -437,18 +357,6 @@ class BouncerViewModel( } } - data class MessageViewModel( - val text: String, - - /** - * Whether updates to the message should be cross-animated from one message to another. - * - * If `false`, no animation should be applied, the message text should just be replaced - * instantly. - */ - val isUpdateAnimated: Boolean, - ) - data class DialogViewModel( val text: String, @@ -480,8 +388,8 @@ object BouncerViewModelModule { selectedUserInteractor: SelectedUserInteractor, flags: ComposeBouncerFlags, userSwitcherViewModel: UserSwitcherViewModel, - clock: SystemClock, devicePolicyManager: DevicePolicyManager, + bouncerMessageViewModel: BouncerMessageViewModel, ): BouncerViewModel { return BouncerViewModel( applicationContext = applicationContext, @@ -497,8 +405,8 @@ object BouncerViewModelModule { users = userSwitcherViewModel.users, userSwitcherMenu = userSwitcherViewModel.menu, actionButton = actionButtonInteractor.actionButton, - clock = clock, devicePolicyManager = devicePolicyManager, + bouncerMessageViewModel = bouncerMessageViewModel, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt index b42eda108d54..052fb6b3c4d7 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt @@ -40,6 +40,7 @@ class PasswordBouncerViewModel( viewModelScope: CoroutineScope, isInputEnabled: StateFlow<Boolean>, interactor: BouncerInteractor, + private val onIntentionalUserInput: () -> Unit, private val inputMethodInteractor: InputMethodInteractor, private val selectedUserInteractor: SelectedUserInteractor, ) : @@ -96,12 +97,8 @@ class PasswordBouncerViewModel( /** Notifies that the user has changed the password input. */ fun onPasswordInputChanged(newPassword: String) { - if (this.password.value.isEmpty() && newPassword.isNotEmpty()) { - interactor.clearMessage() - } - if (newPassword.isNotEmpty()) { - interactor.onIntentionalUserInput() + onIntentionalUserInput() } _password.value = newPassword diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt index 69f8032ef4f2..a4016005a756 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt @@ -40,6 +40,7 @@ class PatternBouncerViewModel( viewModelScope: CoroutineScope, interactor: BouncerInteractor, isInputEnabled: StateFlow<Boolean>, + private val onIntentionalUserInput: () -> Unit, ) : AuthMethodBouncerViewModel( viewModelScope = viewModelScope, @@ -84,7 +85,7 @@ class PatternBouncerViewModel( /** Notifies that the user has started a drag gesture across the dot grid. */ fun onDragStart() { - interactor.clearMessage() + onIntentionalUserInput() } /** diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt index e910a9271ee2..62da5c0e5675 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt @@ -41,6 +41,7 @@ class PinBouncerViewModel( viewModelScope: CoroutineScope, interactor: BouncerInteractor, isInputEnabled: StateFlow<Boolean>, + private val onIntentionalUserInput: () -> Unit, private val simBouncerInteractor: SimBouncerInteractor, authenticationMethod: AuthenticationMethodModel, ) : @@ -131,11 +132,8 @@ class PinBouncerViewModel( /** Notifies that the user clicked on a PIN button with the given digit value. */ fun onPinButtonClicked(input: Int) { val pinInput = mutablePinInput.value - if (pinInput.isEmpty()) { - interactor.clearMessage() - } - interactor.onIntentionalUserInput() + onIntentionalUserInput() mutablePinInput.value = pinInput.append(input) tryAuthenticate(useAutoConfirm = true) @@ -149,7 +147,6 @@ class PinBouncerViewModel( /** Notifies that the user long-pressed the backspace button. */ fun onBackspaceButtonLongPressed() { clearInput() - interactor.clearMessage() } /** Notifies that the user clicked the "enter" button. */ @@ -173,7 +170,6 @@ class PinBouncerViewModel( /** Resets the sim screen and shows a default message. */ private fun onResetSimFlow() { simBouncerInteractor.resetSimPukUserInput() - interactor.resetMessage() clearInput() } diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt index 3063ebd60b0c..fdd98bec0a2d 100644 --- a/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt +++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt @@ -18,12 +18,8 @@ package com.android.systemui.common.shared.model /** Models the bounds of the notification container. */ data class NotificationContainerBounds( - /** The position of the left of the container in its window coordinate system, in pixels. */ - val left: Float = 0f, /** The position of the top of the container in its window coordinate system, in pixels. */ val top: Float = 0f, - /** The position of the right of the container in its window coordinate system, in pixels. */ - val right: Float = 0f, /** The position of the bottom of the container in its window coordinate system, in pixels. */ val bottom: Float = 0f, /** Whether any modifications to top/bottom should be smoothly animated. */ 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/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt index 805999397282..c4e0ef7d082d 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt @@ -29,6 +29,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @OptIn(ExperimentalCoroutinesApi::class) @@ -72,4 +74,14 @@ constructor( */ val isSensorUnderDisplay = fingerprintPropertyRepository.sensorType.map(FingerprintSensorType::isUdfps) + + /** Whether fingerprint authentication is currently allowed while on the bouncer. */ + val isFingerprintCurrentlyAllowedOnBouncer = + isSensorUnderDisplay.flatMapLatest { sensorBelowDisplay -> + if (sensorBelowDisplay) { + flowOf(false) + } else { + isFingerprintAuthCurrentlyAllowed + } + } } 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/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt index 4812e03ec3f6..89148b09b3ed 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 @@ -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..fa1fe5ec1fe8 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 @@ -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,12 +57,11 @@ object KeyguardClockViewBinder { } } keyguardRootView.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { + repeatOnLifecycle(Lifecycle.State.CREATED) { launch { if (!migrateClocksToBlueprint()) 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) @@ -76,7 +77,7 @@ object KeyguardClockViewBinder { launch { if (!migrateClocksToBlueprint()) 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 ( @@ -93,7 +94,7 @@ object KeyguardClockViewBinder { launch { if (!migrateClocksToBlueprint()) 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/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt index b77f0c5a1e60..4d0a25fb7cd3 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 @@ -41,7 +41,7 @@ object KeyguardSmartspaceViewBinder { blueprintInteractor: KeyguardBlueprintInteractor, ) { keyguardRootView.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { + repeatOnLifecycle(Lifecycle.State.CREATED) { launch { if (!migrateClocksToBlueprint()) return@launch clockViewModel.hasCustomWeatherDataDisplay.collect { hasCustomWeatherDataDisplay 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..f60da0e842e8 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 @@ -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,7 +184,7 @@ constructor( init { coroutineScope = CoroutineScope(applicationScope.coroutineContext + Job()) - disposables.add(DisposableHandle { coroutineScope.cancel() }) + disposables += DisposableHandle { coroutineScope.cancel() } if (keyguardBottomAreaRefactor()) { quickAffordancesCombinedViewModel.enablePreviewMode( @@ -214,7 +215,7 @@ constructor( if (hostToken == null) null else InputTransferToken(hostToken), "KeyguardPreviewRenderer" ) - disposables.add(DisposableHandle { host.release() }) + disposables += DisposableHandle { host.release() } } } @@ -284,7 +285,7 @@ constructor( fun destroy() { isDestroyed = true lockscreenSmartspaceController.disconnect() - disposables.forEach { it.dispose() } + disposables.dispose() if (keyguardBottomAreaRefactor()) { shortcutsBindings.forEach { it.destroy() } } @@ -372,7 +373,7 @@ constructor( private fun setupKeyguardRootView(previewContext: Context, rootView: FrameLayout) { val keyguardRootView = KeyguardRootView(previewContext, null) if (!keyguardBottomAreaRefactor()) { - disposables.add( + 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, @@ -555,14 +555,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,7 +579,7 @@ constructor( addAction(Intent.ACTION_TIME_CHANGED) }, ) - disposables.add(DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) }) + disposables += DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) } if (!migrateClocksToBlueprint()) { val layoutChangeListener = @@ -602,9 +600,9 @@ constructor( } } parentView.addOnLayoutChangeListener(layoutChangeListener) - disposables.add( - DisposableHandle { parentView.removeOnLayoutChangeListener(layoutChangeListener) } - ) + disposables += DisposableHandle { + parentView.removeOnLayoutChangeListener(layoutChangeListener) + } } onClockChanged() 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/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index a183b720c087..7847c1ce3968 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 @@ -86,7 +86,7 @@ constructor( if (!Flags.migrateClocksToBlueprint()) { 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/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt index 6a3b920f9692..c1b0cc6b6db9 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 @@ -26,20 +26,16 @@ 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.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.NotificationStackViewBinder +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 @@ -50,12 +46,9 @@ constructor( notificationPanelView: NotificationPanelView, sharedNotificationContainer: SharedNotificationContainer, sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, - notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, - ambientState: AmbientState, - controller: NotificationStackScrollLayoutController, - notificationStackSizeCalculator: NotificationStackSizeCalculator, + sharedNotificationContainerBinder: SharedNotificationContainerBinder, + notificationStackViewBinder: NotificationStackViewBinder, private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>, - @Main mainDispatcher: CoroutineDispatcher, ) : NotificationStackScrollLayoutSection( context, @@ -63,11 +56,8 @@ constructor( notificationPanelView, sharedNotificationContainer, sharedNotificationContainerViewModel, - notificationStackAppearanceViewModel, - ambientState, - controller, - notificationStackSizeCalculator, - mainDispatcher, + sharedNotificationContainerBinder, + notificationStackViewBinder, ) { override fun applyConstraints(constraintSet: ConstraintSet) { if (!migrateClocksToBlueprint()) { 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..83235020b416 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 @@ -31,16 +31,11 @@ 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.NotificationStackViewBinder 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 +import com.android.systemui.util.kotlin.DisposableHandles abstract class NotificationStackScrollLayoutSection constructor( @@ -49,14 +44,11 @@ constructor( 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, + private val notificationStackViewBinder: NotificationStackViewBinder, ) : KeyguardSection() { private val placeHolderId = R.id.nssl_placeholder - private val disposableHandles: MutableList<DisposableHandle> = mutableListOf() + private val disposableHandles = DisposableHandles() /** * Align the notification placeholder bottom to the top of either the lock icon or the ambient @@ -102,39 +94,20 @@ constructor( return } - disposeHandles() - disposableHandles.add( - SharedNotificationContainerBinder.bind( + disposableHandles.dispose() + disposableHandles += + sharedNotificationContainerBinder.bind( sharedNotificationContainer, sharedNotificationContainerViewModel, - sceneContainerFlags, - controller, - notificationStackSizeCalculator, - mainImmediateDispatcher = mainDispatcher, ) - ) if (sceneContainerFlags.isEnabled()) { - disposableHandles.add( - NotificationStackAppearanceViewBinder.bind( - context, - sharedNotificationContainer, - notificationStackAppearanceViewModel, - ambientState, - controller, - mainImmediateDispatcher = mainDispatcher, - ) - ) + disposableHandles += notificationStackViewBinder.bindWhileAttached() } } override fun removeViews(constraintLayout: ConstraintLayout) { - disposeHandles() + disposableHandles.dispose() 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/SplitShadeNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt index 2545302ccaa1..4a705a7f849d 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 @@ -24,19 +24,14 @@ 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.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.NotificationStackViewBinder +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 @@ -47,12 +42,8 @@ constructor( notificationPanelView: NotificationPanelView, sharedNotificationContainer: SharedNotificationContainer, sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, - notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, - ambientState: AmbientState, - controller: NotificationStackScrollLayoutController, - notificationStackSizeCalculator: NotificationStackSizeCalculator, - private val smartspaceViewModel: KeyguardSmartspaceViewModel, - @Main mainDispatcher: CoroutineDispatcher, + sharedNotificationContainerBinder: SharedNotificationContainerBinder, + notificationStackViewBinder: NotificationStackViewBinder, ) : NotificationStackScrollLayoutSection( context, @@ -60,11 +51,8 @@ constructor( notificationPanelView, sharedNotificationContainer, sharedNotificationContainerViewModel, - notificationStackAppearanceViewModel, - ambientState, - controller, - notificationStackSizeCalculator, - mainDispatcher, + sharedNotificationContainerBinder, + notificationStackViewBinder, ) { override fun applyConstraints(constraintSet: ConstraintSet) { if (!migrateClocksToBlueprint()) { 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/AodBurnInViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt index f961e083e64f..9c1f0770708c 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 @@ -169,7 +169,7 @@ constructor( provider: Provider<ClockController>?, ): Provider<ClockController>? { return if (Flags.migrateClocksToBlueprint()) { - Provider { keyguardClockViewModel.clock } + Provider { keyguardClockViewModel.currentClock.value } } else { provider } 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/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index 301f00ee38db..b662109ba4ad 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 @@ -205,7 +205,7 @@ constructor( merge( alphaOnShadeExpansion, keyguardInteractor.dismissAlpha.filterNotNull(), - alternateBouncerToGoneTransitionViewModel.lockscreenAlpha, + alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState), aodToGoneTransitionViewModel.lockscreenAlpha(viewState), aodToLockscreenTransitionViewModel.lockscreenAlpha(viewState), aodToOccludedTransitionViewModel.lockscreenAlpha(viewState), 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/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/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt index 34f66b85def1..c695d4c98308 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt @@ -48,6 +48,8 @@ constructor( qsSceneAdapter.isCustomizing.map { customizing -> if (customizing) { mapOf<UserAction, UserActionResult>(Back to UserActionResult(Scenes.QuickSettings)) + // TODO(b/330200163) Add an Up from Bottom to be able to collapse the shade + // while customizing } else { mapOf( Back to UserActionResult(Scenes.Shade), 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/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/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java index 8ba0544d5b7a..8dbceadbb7a8 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java @@ -1280,18 +1280,20 @@ public class QuickSettingsControllerImpl implements QuickSettingsController, Dum mScrimController.setScrimCornerRadius(radius); - // Convert global clipping coordinates to local ones, - // relative to NotificationStackScrollLayout - int nsslLeft = calculateNsslLeft(left); - int nsslRight = calculateNsslRight(right); - int nsslTop = getNotificationsClippingTopBounds(top); - int nsslBottom = bottom - mNotificationStackScrollLayoutController.getTop(); - int bottomRadius = mSplitShadeEnabled ? radius : 0; - // TODO (b/265193930): remove dependency on NPVC - int topRadius = mSplitShadeEnabled - && mPanelViewControllerLazy.get().isExpandingFromHeadsUp() ? 0 : radius; - mNotificationStackScrollLayoutController.setRoundedClippingBounds( - nsslLeft, nsslTop, nsslRight, nsslBottom, topRadius, bottomRadius); + if (!SceneContainerFlag.isEnabled()) { + // Convert global clipping coordinates to local ones, + // relative to NotificationStackScrollLayout + int nsslLeft = calculateNsslLeft(left); + int nsslRight = calculateNsslRight(right); + int nsslTop = getNotificationsClippingTopBounds(top); + int nsslBottom = bottom - mNotificationStackScrollLayoutController.getTop(); + int bottomRadius = mSplitShadeEnabled ? radius : 0; + // TODO (b/265193930): remove dependency on NPVC + int topRadius = mSplitShadeEnabled + && mPanelViewControllerLazy.get().isExpandingFromHeadsUp() ? 0 : radius; + mNotificationStackScrollLayoutController.setRoundedClippingBounds( + nsslLeft, nsslTop, nsslRight, nsslBottom, topRadius, bottomRadius); + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt index ea549f2b7e53..24b7533d6c26 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt @@ -66,11 +66,13 @@ constructor( deviceEntryInteractor.isUnlocked, deviceEntryInteractor.canSwipeToEnter, shadeInteractor.shadeMode, - ) { isUnlocked, canSwipeToDismiss, shadeMode -> + qsSceneAdapter.isCustomizing + ) { isUnlocked, canSwipeToDismiss, shadeMode, isCustomizing -> destinationScenes( isUnlocked = isUnlocked, canSwipeToDismiss = canSwipeToDismiss, shadeMode = shadeMode, + isCustomizing = isCustomizing ) } .stateIn( @@ -81,6 +83,7 @@ constructor( isUnlocked = deviceEntryInteractor.isUnlocked.value, canSwipeToDismiss = deviceEntryInteractor.canSwipeToEnter.value, shadeMode = shadeInteractor.shadeMode.value, + isCustomizing = qsSceneAdapter.isCustomizing.value, ), ) @@ -120,6 +123,7 @@ constructor( isUnlocked: Boolean, canSwipeToDismiss: Boolean?, shadeMode: ShadeMode, + isCustomizing: Boolean, ): Map<UserAction, UserActionResult> { val up = when { @@ -131,7 +135,9 @@ constructor( val down = Scenes.QuickSettings.takeIf { shadeMode is ShadeMode.Single } return buildMap { - this[Swipe(SwipeDirection.Up)] = UserActionResult(up) + if (!isCustomizing) { + this[Swipe(SwipeDirection.Up)] = UserActionResult(up) + } // TODO(b/330200163) Add an else to be able to collapse the shade while customizing down?.let { this[Swipe(SwipeDirection.Down)] = UserActionResult(down) } } } 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/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/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/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 947976299f8e..f2c593d7ffdb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -812,6 +812,10 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } else { mDebugTextUsedYPositions.clear(); } + + mDebugPaint.setColor(Color.DKGRAY); + canvas.drawPath(mRoundedClipPath, mDebugPaint); + int y = 0; drawDebugInfo(canvas, y, Color.RED, /* label= */ "y = " + y); @@ -843,14 +847,14 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable drawDebugInfo(canvas, y, Color.LTGRAY, /* label= */ "mAmbientState.getStackY() + mAmbientState.getStackHeight() = " + y); - y = (int) mAmbientState.getStackY() + mContentHeight; - drawDebugInfo(canvas, y, Color.MAGENTA, - /* label= */ "mAmbientState.getStackY() + mContentHeight = " + y); - y = (int) (mAmbientState.getStackY() + mIntrinsicContentHeight); drawDebugInfo(canvas, y, Color.YELLOW, /* label= */ "mAmbientState.getStackY() + mIntrinsicContentHeight = " + y); + y = mContentHeight; + drawDebugInfo(canvas, y, Color.MAGENTA, + /* label= */ "mContentHeight = " + y); + drawDebugInfo(canvas, mRoundedRectClippingBottom, Color.DKGRAY, /* label= */ "mRoundedRectClippingBottom) = " + y); } @@ -4940,6 +4944,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable println(pw, "intrinsicPadding", mIntrinsicPadding); println(pw, "topPadding", mTopPadding); println(pw, "bottomPadding", mBottomPadding); + dumpRoundedRectClipping(pw); + println(pw, "requestedClipBounds", mRequestedClipBounds); + println(pw, "isClipped", mIsClipped); println(pw, "translationX", getTranslationX()); println(pw, "translationY", getTranslationY()); println(pw, "translationZ", getTranslationZ()); @@ -4994,6 +5001,15 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable }); } + private void dumpRoundedRectClipping(IndentingPrintWriter pw) { + pw.append("roundedRectClipping{l=").print(mRoundedRectClippingLeft); + pw.append(" t=").print(mRoundedRectClippingTop); + pw.append(" r=").print(mRoundedRectClippingRight); + pw.append(" b=").print(mRoundedRectClippingBottom); + pw.append("} topRadius=").print(mBgCornerRadii[0]); + pw.append(" bottomRadius=").println(mBgCornerRadii[4]); + } + private void dumpFooterViewVisibility(IndentingPrintWriter pw) { FooterViewRefactor.assertInLegacyMode(); final boolean showDismissView = shouldShowDismissView(); 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 9efe632f5dbb..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 @@ -17,8 +17,8 @@ package com.android.systemui.statusbar.notification.stack.data.repository -import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow @@ -26,7 +26,7 @@ import kotlinx.coroutines.flow.MutableStateFlow @SysUISingleton class NotificationStackAppearanceRepository @Inject constructor() { /** The bounds of the notification stack in the current scene. */ - val stackBounds = MutableStateFlow(NotificationContainerBounds()) + val stackBounds = MutableStateFlow(StackBounds()) /** * 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/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt index 08df47388556..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 @@ -17,13 +17,19 @@ package com.android.systemui.statusbar.notification.stack.domain.interactor -import com.android.systemui.common.shared.model.NotificationContainerBounds 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 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 @@ -31,9 +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<NotificationContainerBounds> = repository.stackBounds.asStateFlow() + 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: 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 @@ -59,7 +86,7 @@ constructor( val syntheticScroll: Flow<Float> = repository.syntheticScroll.asStateFlow() /** Sets the position of the notification stack in the current scene. */ - fun setStackBounds(bounds: NotificationContainerBounds) { + fun setStackBounds(bounds: StackBounds) { check(bounds.top <= bounds.bottom) { "Invalid bounds: $bounds" } repository.stackBounds.value = bounds } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackBounds.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackBounds.kt new file mode 100644 index 000000000000..1fc9a182a10c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackBounds.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.statusbar.notification.stack.shared.model + +/** Models the bounds of the notification stack. */ +data class StackBounds( + /** The position of the left of the stack in its window coordinate system, in pixels. */ + val left: Float = 0f, + /** The position of the top of the stack in its window coordinate system, in pixels. */ + val top: Float = 0f, + /** The position of the right of the stack in its window coordinate system, in pixels. */ + val right: Float = 0f, + /** The position of the bottom of the stack in its window coordinate system, in pixels. */ + val bottom: Float = 0f, +) { + /** The current height of the notification container. */ + val height: Float = bottom - top +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackClipping.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackClipping.kt new file mode 100644 index 000000000000..0c92b5023d1d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackClipping.kt @@ -0,0 +1,20 @@ +/* + * 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.stack.shared.model + +/** Models the clipping rounded rectangle of the notification stack */ +data class StackClipping(val bounds: StackBounds, val rounding: StackRounding) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackRounding.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackRounding.kt new file mode 100644 index 000000000000..ddc5d7ea0d7f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackRounding.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.statusbar.notification.stack.shared.model + +/** Models the corner rounds of the notification stack. */ +data class StackRounding( + /** Whether the top corners of the notification stack should be rounded. */ + val roundTop: Boolean = false, + /** Whether the bottom corners of the notification stack should be rounded. */ + val roundBottom: Boolean = false, +) 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 f10e5f1ab022..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt +++ /dev/null @@ -1,99 +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.stackBounds.collect { bounds -> - 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, - SCRIM_CORNER_RADIUS.dpToPx(context), - 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..6db6719c76c7 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,23 @@ 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, + @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 +80,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 +90,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 +157,8 @@ object SharedNotificationContainerBinder { } } - controller.setOnHeightChangedRunnable(Runnable { viewModel.notificationStackChanged() }) + 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 +167,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/NotificationStackAppearanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt index b6167e1ef0fb..a7cbc3374a0e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import com.android.compose.animation.scene.ObservableTransitionState -import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dump.DumpManager @@ -27,6 +26,7 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.Scenes.Shade import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor +import com.android.systemui.statusbar.notification.stack.shared.model.StackClipping import com.android.systemui.util.kotlin.FlowDumperImpl import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -83,8 +83,13 @@ constructor( .dumpWhileCollecting("expandFraction") /** The bounds of the notification stack in the current scene. */ - val stackBounds: Flow<NotificationContainerBounds> = - stackAppearanceInteractor.stackBounds.dumpValue("stackBounds") + val stackClipping: Flow<StackClipping> = + combine( + stackAppearanceInteractor.stackBounds, + stackAppearanceInteractor.stackRounding, + ::StackClipping + ) + .dumpWhileCollecting("stackClipping") /** The y-coordinate in px of top of the contents of the notification stack. */ val contentTop: StateFlow<Float> = stackAppearanceInteractor.contentTop.dumpValue("contentTop") 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 9e2497d5bb41..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 @@ -24,6 +24,8 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor +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.Flow @@ -61,12 +63,17 @@ constructor( right: Float, bottom: Float, ) { - val notificationContainerBounds = - NotificationContainerBounds(top = top, bottom = bottom, left = left, right = right) - keyguardInteractor.setNotificationContainerBounds(notificationContainerBounds) - interactor.setStackBounds(notificationContainerBounds) + keyguardInteractor.setNotificationContainerBounds( + NotificationContainerBounds(top = top, bottom = bottom) + ) + interactor.setStackBounds( + StackBounds(top = top, bottom = bottom, left = left, right = right) + ) } + /** Corner rounding of the stack */ + val stackRounding: Flow<StackRounding> = interactor.stackRounding + /** * 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/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/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/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/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/volume/dagger/MediaDevicesModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt index d134e60ef72f..155102c9b9a7 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt @@ -21,7 +21,6 @@ import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.volume.data.repository.LocalMediaRepository import com.android.settingslib.volume.data.repository.MediaControllerRepository import com.android.settingslib.volume.data.repository.MediaControllerRepositoryImpl -import com.android.settingslib.volume.domain.interactor.LocalMediaInteractor import com.android.settingslib.volume.shared.AudioManagerEventsReceiver import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -52,13 +51,6 @@ interface MediaDevicesModule { @Provides @SysUISingleton - fun provideLocalMediaInteractor( - repository: LocalMediaRepository, - @Application scope: CoroutineScope, - ): LocalMediaInteractor = LocalMediaInteractor(repository, scope) - - @Provides - @SysUISingleton fun provideMediaDeviceSessionRepository( intentsReceiver: AudioManagerEventsReceiver, mediaSessionManager: MediaSessionManager, diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt index 11b4690e59ee..e052f243f7ea 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt @@ -15,15 +15,12 @@ */ package com.android.systemui.volume.panel.component.mediaoutput.data.repository -import android.media.MediaRouter2Manager import com.android.settingslib.volume.data.repository.LocalMediaRepository import com.android.settingslib.volume.data.repository.LocalMediaRepositoryImpl import com.android.settingslib.volume.shared.AudioManagerEventsReceiver import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.media.controls.util.LocalMediaManagerFactory import javax.inject.Inject -import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope interface LocalMediaRepositoryFactory { @@ -35,18 +32,14 @@ class LocalMediaRepositoryFactoryImpl @Inject constructor( private val eventsReceiver: AudioManagerEventsReceiver, - private val mediaRouter2Manager: MediaRouter2Manager, private val localMediaManagerFactory: LocalMediaManagerFactory, @Application private val coroutineScope: CoroutineScope, - @Background private val backgroundCoroutineContext: CoroutineContext, ) : LocalMediaRepositoryFactory { override fun create(packageName: String?): LocalMediaRepository = LocalMediaRepositoryImpl( eventsReceiver, localMediaManagerFactory.create(packageName), - mediaRouter2Manager, coroutineScope, - backgroundCoroutineContext, ) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt new file mode 100644 index 000000000000..b0c8a4a2d478 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt @@ -0,0 +1,108 @@ +/* + * 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.volume.panel.component.mediaoutput.domain.interactor + +import android.media.session.MediaController +import android.media.session.PlaybackState +import android.os.Handler +import com.android.settingslib.volume.data.repository.MediaControllerChange +import com.android.settingslib.volume.data.repository.MediaControllerRepository +import com.android.settingslib.volume.data.repository.stateChanges +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession +import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.withContext + +/** Allows to observe and change [MediaDeviceSession] state. */ +@OptIn(ExperimentalCoroutinesApi::class) +@VolumePanelScope +class MediaDeviceSessionInteractor +@Inject +constructor( + @Background private val backgroundCoroutineContext: CoroutineContext, + @Background private val backgroundHandler: Handler, + private val mediaControllerRepository: MediaControllerRepository, +) { + + /** [PlaybackState] changes for the [MediaDeviceSession]. */ + fun playbackState(session: MediaDeviceSession): Flow<PlaybackState?> { + return stateChanges(session) { + emit(MediaControllerChange.PlaybackStateChanged(it.playbackState)) + } + .filterIsInstance(MediaControllerChange.PlaybackStateChanged::class) + .map { it.state } + } + + /** [MediaController.PlaybackInfo] changes for the [MediaDeviceSession]. */ + fun playbackInfo(session: MediaDeviceSession): Flow<MediaController.PlaybackInfo?> { + return stateChanges(session) { + emit(MediaControllerChange.AudioInfoChanged(it.playbackInfo)) + } + .filterIsInstance(MediaControllerChange.AudioInfoChanged::class) + .map { it.info } + } + + private fun stateChanges( + session: MediaDeviceSession, + onStart: suspend FlowCollector<MediaControllerChange>.(controller: MediaController) -> Unit, + ): Flow<MediaControllerChange?> = + mediaControllerRepository.activeSessions + .flatMapLatest { controllers -> + val controller: MediaController = + findControllerForSession(controllers, session) + ?: return@flatMapLatest flowOf(null) + controller.stateChanges(backgroundHandler).onStart { onStart(controller) } + } + .flowOn(backgroundCoroutineContext) + + /** Set [MediaDeviceSession] volume to [volume]. */ + suspend fun setSessionVolume(mediaDeviceSession: MediaDeviceSession, volume: Int): Boolean { + if (!mediaDeviceSession.canAdjustVolume) { + return false + } + return withContext(backgroundCoroutineContext) { + val controller = + findControllerForSession( + mediaControllerRepository.activeSessions.value, + mediaDeviceSession, + ) + if (controller == null) { + false + } else { + controller.setVolumeTo(volume, 0) + true + } + } + } + + private fun findControllerForSession( + controllers: Collection<MediaController>, + mediaDeviceSession: MediaDeviceSession, + ): MediaController? = + controllers.firstOrNull { it.sessionToken == mediaDeviceSession.sessionToken } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt index cb16abe7e575..ea4c082f4660 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt @@ -33,23 +33,15 @@ constructor( private val mediaOutputDialogManager: MediaOutputDialogManager, ) { - fun onBarClick(session: MediaDeviceSession, expandable: Expandable) { - when (session) { - is MediaDeviceSession.Active -> { - mediaOutputDialogManager.createAndShowWithController( - session.packageName, - false, - expandable.dialogController() - ) - } - is MediaDeviceSession.Inactive -> { - mediaOutputDialogManager.createAndShowForSystemRouting( - expandable.dialogController() - ) - } - else -> { - /* do nothing */ - } + fun onBarClick(session: MediaDeviceSession, isPlaybackActive: Boolean, expandable: Expandable) { + if (isPlaybackActive) { + mediaOutputDialogManager.createAndShowWithController( + session.packageName, + false, + expandable.dialogController() + ) + } else { + mediaOutputDialogManager.createAndShowForSystemRouting(expandable.dialogController()) } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt index 0f5343701ac6..e60139ecf9cc 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt @@ -17,17 +17,16 @@ package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor import android.content.pm.PackageManager +import android.media.VolumeProvider import android.media.session.MediaController -import android.os.Handler import android.util.Log import com.android.settingslib.media.MediaDevice import com.android.settingslib.volume.data.repository.LocalMediaRepository -import com.android.settingslib.volume.data.repository.MediaControllerChange import com.android.settingslib.volume.data.repository.MediaControllerRepository -import com.android.settingslib.volume.data.repository.stateChanges import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession +import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSessions import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import javax.inject.Inject import kotlin.coroutines.CoroutineContext @@ -38,12 +37,9 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext @@ -58,35 +54,40 @@ constructor( private val packageManager: PackageManager, @VolumePanelScope private val coroutineScope: CoroutineScope, @Background private val backgroundCoroutineContext: CoroutineContext, - @Background private val backgroundHandler: Handler, - mediaControllerRepository: MediaControllerRepository + mediaControllerRepository: MediaControllerRepository, ) { - /** Current [MediaDeviceSession]. Emits when the session playback changes. */ - val mediaDeviceSession: StateFlow<MediaDeviceSession> = - mediaControllerRepository.activeLocalMediaController - .flatMapLatest { it?.mediaDeviceSession() ?: flowOf(MediaDeviceSession.Inactive) } - .flowOn(backgroundCoroutineContext) - .stateIn(coroutineScope, SharingStarted.Eagerly, MediaDeviceSession.Inactive) + private val activeMediaControllers: Flow<MediaControllers> = + mediaControllerRepository.activeSessions + .map { getMediaControllers(it) } + .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1) + + /** [MediaDeviceSessions] that contains currently active sessions. */ + val activeMediaDeviceSessions: Flow<MediaDeviceSessions> = + activeMediaControllers.map { + MediaDeviceSessions( + local = it.local?.mediaDeviceSession(), + remote = it.remote?.mediaDeviceSession() + ) + } - private fun MediaController.mediaDeviceSession(): Flow<MediaDeviceSession> { - return stateChanges(backgroundHandler) - .onStart { emit(MediaControllerChange.PlaybackStateChanged(playbackState)) } - .filterIsInstance<MediaControllerChange.PlaybackStateChanged>() + /** Returns the default [MediaDeviceSession] from [activeMediaDeviceSessions] */ + val defaultActiveMediaSession: StateFlow<MediaDeviceSession?> = + activeMediaControllers .map { - MediaDeviceSession.Active( - appLabel = getApplicationLabel(packageName) - ?: return@map MediaDeviceSession.Inactive, - packageName = packageName, - sessionToken = sessionToken, - playbackState = playbackState, - ) + when { + it.local?.playbackState?.isActive == true -> it.local.mediaDeviceSession() + it.remote?.playbackState?.isActive == true -> it.remote.mediaDeviceSession() + it.local != null -> it.local.mediaDeviceSession() + else -> null + } } - } + .flowOn(backgroundCoroutineContext) + .stateIn(coroutineScope, SharingStarted.Eagerly, null) private val localMediaRepository: SharedFlow<LocalMediaRepository> = - mediaDeviceSession - .map { (it as? MediaDeviceSession.Active)?.packageName } + defaultActiveMediaSession + .map { it?.packageName } .distinctUntilChanged() .map { localMediaRepositoryFactory.create(it) } .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1) @@ -111,6 +112,54 @@ constructor( } } + /** Finds local and remote media controllers. */ + private fun getMediaControllers( + controllers: Collection<MediaController>, + ): MediaControllers { + var localController: MediaController? = null + var remoteController: MediaController? = null + val remoteMediaSessions: MutableSet<String> = mutableSetOf() + for (controller in controllers) { + val playbackInfo: MediaController.PlaybackInfo = controller.playbackInfo ?: continue + when (playbackInfo.playbackType) { + MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> { + // MediaController can't be local if there is a remote one for the same package + if (localController?.packageName.equals(controller.packageName)) { + localController = null + } + if (!remoteMediaSessions.contains(controller.packageName)) { + remoteMediaSessions.add(controller.packageName) + if (remoteController == null) { + remoteController = controller + } + } + } + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> { + if (controller.packageName in remoteMediaSessions) continue + if (localController != null) continue + localController = controller + } + } + } + return MediaControllers(local = localController, remote = remoteController) + } + + private suspend fun MediaController.mediaDeviceSession(): MediaDeviceSession? { + return MediaDeviceSession( + packageName = packageName, + sessionToken = sessionToken, + canAdjustVolume = + playbackInfo != null && + playbackInfo?.volumeControl != VolumeProvider.VOLUME_CONTROL_FIXED, + appLabel = getApplicationLabel(packageName) ?: return null + ) + } + + private data class MediaControllers( + val local: MediaController?, + val remote: MediaController?, + ) + private companion object { const val TAG = "MediaOutputInteractor" } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt index 1bceee9b2d34..2a2ce796a2b7 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt @@ -17,26 +17,15 @@ package com.android.systemui.volume.panel.component.mediaoutput.domain.model import android.media.session.MediaSession -import android.media.session.PlaybackState /** Represents media playing on the connected device. */ -sealed interface MediaDeviceSession { +data class MediaDeviceSession( + val appLabel: CharSequence, + val packageName: String, + val sessionToken: MediaSession.Token, + val canAdjustVolume: Boolean, +) - /** Media is playing. */ - data class Active( - val appLabel: CharSequence, - val packageName: String, - val sessionToken: MediaSession.Token, - val playbackState: PlaybackState?, - ) : MediaDeviceSession - - /** Media is not playing. */ - data object Inactive : MediaDeviceSession - - /** Current media state is unknown yet. */ - data object Unknown : MediaDeviceSession -} - -/** Returns true when the audio is playing for the [MediaDeviceSession]. */ -fun MediaDeviceSession.isPlaying(): Boolean = - this is MediaDeviceSession.Active && playbackState?.isActive == true +/** Returns true when [other] controls the same sessions as [this]. */ +fun MediaDeviceSession.isTheSameSession(other: MediaDeviceSession?): Boolean = + sessionToken == other?.sessionToken diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.kt new file mode 100644 index 000000000000..ddc078421b9a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.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.volume.panel.component.mediaoutput.domain.model + +/** Models a pair of local and remote [MediaDeviceSession]s. */ +data class MediaDeviceSessions( + val local: MediaDeviceSession?, + val remote: MediaDeviceSession?, +) { + + companion object { + /** Returns [MediaDeviceSessions.local]. */ + val Local: (MediaDeviceSessions) -> MediaDeviceSession? = { it.local } + /** Returns [MediaDeviceSessions.remote]. */ + val Remote: (MediaDeviceSessions) -> MediaDeviceSession? = { it.remote } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt index d49cb1ea6958..2530a3a46384 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt @@ -17,24 +17,30 @@ package com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel import android.content.Context +import android.media.session.PlaybackState import com.android.systemui.animation.Expandable import com.android.systemui.common.shared.model.Color import com.android.systemui.common.shared.model.Icon import com.android.systemui.res.R +import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession -import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isPlaying import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel 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.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** Models the UI of the Media Output Volume Panel component. */ +@OptIn(ExperimentalCoroutinesApi::class) @VolumePanelScope class MediaOutputViewModel @Inject @@ -43,25 +49,36 @@ constructor( @VolumePanelScope private val coroutineScope: CoroutineScope, private val volumePanelViewModel: VolumePanelViewModel, private val actionsInteractor: MediaOutputActionsInteractor, + private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor, interactor: MediaOutputInteractor, ) { - private val mediaDeviceSession: StateFlow<MediaDeviceSession> = - interactor.mediaDeviceSession.stateIn( - coroutineScope, - SharingStarted.Eagerly, - MediaDeviceSession.Unknown, - ) + private val sessionWithPlayback: StateFlow<SessionWithPlayback?> = + interactor.defaultActiveMediaSession + .flatMapLatest { session -> + if (session == null) { + flowOf(null) + } else { + mediaDeviceSessionInteractor.playbackState(session).map { playback -> + playback?.let { SessionWithPlayback(session, it) } + } + } + } + .stateIn( + coroutineScope, + SharingStarted.Eagerly, + null, + ) val connectedDeviceViewModel: StateFlow<ConnectedDeviceViewModel?> = - combine(mediaDeviceSession, interactor.currentConnectedDevice) { + combine(sessionWithPlayback, interactor.currentConnectedDevice) { mediaDeviceSession, currentConnectedDevice -> ConnectedDeviceViewModel( - if (mediaDeviceSession.isPlaying()) { + if (mediaDeviceSession?.playback?.isActive == true) { context.getString( R.string.media_output_label_title, - (mediaDeviceSession as MediaDeviceSession.Active).appLabel + mediaDeviceSession.session.appLabel ) } else { context.getString(R.string.media_output_title_without_playing) @@ -76,10 +93,10 @@ constructor( ) val deviceIconViewModel: StateFlow<DeviceIconViewModel?> = - combine(mediaDeviceSession, interactor.currentConnectedDevice) { + combine(sessionWithPlayback, interactor.currentConnectedDevice) { mediaDeviceSession, currentConnectedDevice -> - if (mediaDeviceSession.isPlaying()) { + if (mediaDeviceSession?.playback?.isActive == true) { val icon = currentConnectedDevice?.icon?.let { Icon.Loaded(it, null) } ?: Icon.Resource( @@ -112,7 +129,14 @@ constructor( ) fun onBarClick(expandable: Expandable) { - actionsInteractor.onBarClick(mediaDeviceSession.value, expandable) + sessionWithPlayback.value?.let { + actionsInteractor.onBarClick(it.session, it.playback.isActive, expandable) + } volumePanelViewModel.dismissPanel() } + + private data class SessionWithPlayback( + val session: MediaDeviceSession, + val playback: PlaybackState, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt deleted file mode 100644 index 6b62074e023d..000000000000 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.volume.panel.component.volume.domain.interactor - -import com.android.settingslib.volume.domain.interactor.LocalMediaInteractor -import com.android.settingslib.volume.domain.model.RoutingSession -import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn - -/** Provides a remote media casting state. */ -@VolumePanelScope -class CastVolumeInteractor -@Inject -constructor( - @VolumePanelScope private val coroutineScope: CoroutineScope, - private val localMediaInteractor: LocalMediaInteractor, -) { - - /** Returns a list of [RoutingSession] to show in the UI. */ - val remoteRoutingSessions: StateFlow<List<RoutingSession>> = - localMediaInteractor.remoteRoutingSessions - .map { it.filter { routingSession -> routingSession.isVolumeSeekBarEnabled } } - .stateIn(coroutineScope, SharingStarted.Eagerly, emptyList()) - - /** Sets [routingSession] volume to [volume]. */ - suspend fun setVolume(routingSession: RoutingSession, volume: Int) { - localMediaInteractor.adjustSessionVolume(routingSession.routingSessionInfo.id, volume) - } -} 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 1b732081a12a..d49442c149ee 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 @@ -80,7 +80,7 @@ constructor( ) { model, isEnabled, ringerMode -> model.toState(isEnabled, ringerMode) } - .stateIn(coroutineScope, SharingStarted.Eagerly, EmptyState) + .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty) override fun onValueChanged(state: SliderState, newValue: Float) { val audioViewModel = state as? State @@ -163,17 +163,6 @@ constructor( val audioStreamModel: AudioStreamModel, ) : SliderState - private data object EmptyState : SliderState { - override val value: Float = 0f - override val valueRange: ClosedFloatingPointRange<Float> = 0f..1f - override val icon: Icon? = null - override val valueText: String = "" - override val label: String = "" - override val disabledMessage: String? = null - override val a11yStep: Int = 0 - override val isEnabled: Boolean = true - } - @AssistedFactory interface Factory { 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 86b2d73de3e3..0f240b37f02e 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 @@ -17,11 +17,11 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel import android.content.Context -import com.android.settingslib.volume.domain.model.RoutingSession +import android.media.session.MediaController.PlaybackInfo import com.android.systemui.common.shared.model.Icon import com.android.systemui.res.R -import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor -import com.android.systemui.volume.panel.component.volume.domain.interactor.CastVolumeInteractor +import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor +import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession import com.android.systemui.volume.panel.component.volume.domain.interactor.VolumeSliderInteractor import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -30,30 +30,29 @@ import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch class CastVolumeSliderViewModel @AssistedInject constructor( - @Assisted private val routingSession: RoutingSession, + @Assisted private val session: MediaDeviceSession, @Assisted private val coroutineScope: CoroutineScope, private val context: Context, - mediaOutputInteractor: MediaOutputInteractor, + private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor, private val volumeSliderInteractor: VolumeSliderInteractor, - private val castVolumeInteractor: CastVolumeInteractor, ) : SliderViewModel { - private val volumeRange = 0..routingSession.routingSessionInfo.volumeMax - override val slider: StateFlow<SliderState> = - combine(mediaOutputInteractor.currentConnectedDevice) { _ -> getCurrentState() } - .stateIn(coroutineScope, SharingStarted.Eagerly, getCurrentState()) + mediaDeviceSessionInteractor + .playbackInfo(session) + .mapNotNull { it?.getCurrentState() } + .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty) override fun onValueChanged(state: SliderState, newValue: Float) { coroutineScope.launch { - castVolumeInteractor.setVolume(routingSession, newValue.roundToInt()) + mediaDeviceSessionInteractor.setSessionVolume(session, newValue.roundToInt()) } } @@ -61,15 +60,16 @@ constructor( // do nothing because this action isn't supported for Cast sliders. } - private fun getCurrentState(): State = - State( - value = routingSession.routingSessionInfo.volume.toFloat(), + private fun PlaybackInfo.getCurrentState(): State { + val volumeRange = 0..maxVolume + return State( + value = currentVolume.toFloat(), valueRange = volumeRange.first.toFloat()..volumeRange.last.toFloat(), icon = Icon.Resource(R.drawable.ic_cast, null), valueText = SliderViewModel.formatValue( volumeSliderInteractor.processVolumeToValue( - volume = routingSession.routingSessionInfo.volume, + volume = currentVolume, volumeRange = volumeRange, ) ), @@ -77,6 +77,7 @@ constructor( isEnabled = true, a11yStep = 1 ) + } private data class State( override val value: Float, @@ -95,7 +96,7 @@ constructor( interface Factory { fun create( - routingSession: RoutingSession, + session: MediaDeviceSession, coroutineScope: CoroutineScope, ): CastVolumeSliderViewModel } 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 b87d0a786740..3dca2724b095 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,4 +36,15 @@ sealed interface SliderState { */ val a11yStep: Int val disabledMessage: String? + + data object Empty : SliderState { + override val value: Float = 0f + override val valueRange: ClosedFloatingPointRange<Float> = 0f..1f + override val icon: Icon? = null + override val valueText: String = "" + override val label: String = "" + override val disabledMessage: String? = null + override val a11yStep: Int = 0 + override val isEnabled: Boolean = true + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt index aaee24b9357f..4e9a45635f7b 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt @@ -18,9 +18,10 @@ package com.android.systemui.volume.panel.component.volume.ui.viewmodel import android.media.AudioManager import com.android.settingslib.volume.shared.model.AudioStream +import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor -import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isPlaying -import com.android.systemui.volume.panel.component.volume.domain.interactor.CastVolumeInteractor +import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession +import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isTheSameSession import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.AudioStreamSliderViewModel import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.CastVolumeSliderViewModel import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderViewModel @@ -29,17 +30,15 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.combineTransform +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge -import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.transformLatest import kotlinx.coroutines.launch /** @@ -52,50 +51,34 @@ class AudioVolumeComponentViewModel @Inject constructor( @VolumePanelScope private val scope: CoroutineScope, - castVolumeInteractor: CastVolumeInteractor, mediaOutputInteractor: MediaOutputInteractor, + private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor, private val streamSliderViewModelFactory: AudioStreamSliderViewModel.Factory, private val castVolumeSliderViewModelFactory: CastVolumeSliderViewModel.Factory, ) { - private val remoteSessionsViewModels: Flow<List<SliderViewModel>> = - castVolumeInteractor.remoteRoutingSessions.transformLatest { routingSessions -> - coroutineScope { - emit( - routingSessions.map { routingSession -> - castVolumeSliderViewModelFactory.create(routingSession, this) - } - ) - } - } - private val streamViewModels: Flow<List<SliderViewModel>> = - flowOf( - listOf( - AudioStream(AudioManager.STREAM_MUSIC), - AudioStream(AudioManager.STREAM_VOICE_CALL), - AudioStream(AudioManager.STREAM_RING), - AudioStream(AudioManager.STREAM_NOTIFICATION), - AudioStream(AudioManager.STREAM_ALARM), - ) - ) - .transformLatest { streams -> + val sliderViewModels: StateFlow<List<SliderViewModel>> = + combineTransform( + mediaOutputInteractor.activeMediaDeviceSessions, + mediaOutputInteractor.defaultActiveMediaSession, + ) { activeSessions, defaultSession -> coroutineScope { - emit( - streams.map { stream -> - streamSliderViewModelFactory.create( - AudioStreamSliderViewModel.FactoryAudioStreamWrapper(stream), - this, - ) + val viewModels = buildList { + if (defaultSession?.isTheSameSession(activeSessions.remote) == true) { + addRemoteViewModelIfNeeded(this, activeSessions.remote) + addStreamViewModel(this, AudioManager.STREAM_MUSIC) + } else { + addStreamViewModel(this, AudioManager.STREAM_MUSIC) + addRemoteViewModelIfNeeded(this, activeSessions.remote) } - ) - } - } - val sliderViewModels: StateFlow<List<SliderViewModel>> = - combine(remoteSessionsViewModels, streamViewModels) { - remoteSessionsViewModels, - streamViewModels -> - remoteSessionsViewModels + streamViewModels + addStreamViewModel(this, AudioManager.STREAM_VOICE_CALL) + addStreamViewModel(this, AudioManager.STREAM_RING) + addStreamViewModel(this, AudioManager.STREAM_NOTIFICATION) + addStreamViewModel(this, AudioManager.STREAM_ALARM) + } + emit(viewModels) + } } .stateIn(scope, SharingStarted.Eagerly, emptyList()) @@ -103,12 +86,41 @@ constructor( val isExpanded: StateFlow<Boolean> = merge( - mutableIsExpanded.onStart { emit(false) }, - mediaOutputInteractor.mediaDeviceSession.map { !it.isPlaying() }, + mutableIsExpanded, + mediaOutputInteractor.defaultActiveMediaSession.flatMapLatest { + if (it == null) flowOf(true) + else mediaDeviceSessionInteractor.playbackState(it).map { it?.isActive != true } + }, ) .stateIn(scope, SharingStarted.Eagerly, false) fun onExpandedChanged(isExpanded: Boolean) { scope.launch { mutableIsExpanded.emit(isExpanded) } } + + private fun CoroutineScope.addRemoteViewModelIfNeeded( + list: MutableList<SliderViewModel>, + remoteMediaDeviceSession: MediaDeviceSession? + ) { + if (remoteMediaDeviceSession?.canAdjustVolume == true) { + val viewModel = + castVolumeSliderViewModelFactory.create( + remoteMediaDeviceSession, + this, + ) + list.add(viewModel) + } + } + + private fun CoroutineScope.addStreamViewModel( + list: MutableList<SliderViewModel>, + stream: Int, + ) { + val viewModel = + streamSliderViewModelFactory.create( + AudioStreamSliderViewModel.FactoryAudioStreamWrapper(AudioStream(stream)), + this, + ) + list.add(viewModel) + } } 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/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/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/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt index 6ef74194fd85..ba07a849469d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt @@ -19,4 +19,5 @@ package com.android.systemui.biometrics.data.repository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -val Kosmos.facePropertyRepository by Fixture { FakeFacePropertyRepository() } +val Kosmos.fakeFacePropertyRepository by Fixture { FakeFacePropertyRepository() } +val Kosmos.facePropertyRepository by Fixture { fakeFacePropertyRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt index 27803b22de29..c06554573bd7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt @@ -16,7 +16,6 @@ package com.android.systemui.bouncer.domain.interactor -import android.content.applicationContext import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.bouncer.data.repository.bouncerRepository import com.android.systemui.classifier.domain.interactor.falsingInteractor @@ -29,12 +28,10 @@ import com.android.systemui.power.domain.interactor.powerInteractor val Kosmos.bouncerInteractor by Fixture { BouncerInteractor( applicationScope = testScope.backgroundScope, - applicationContext = applicationContext, repository = bouncerRepository, authenticationInteractor = authenticationInteractor, deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor, falsingInteractor = falsingInteractor, powerInteractor = powerInteractor, - simBouncerInteractor = simBouncerInteractor, ) } 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/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt new file mode 100644 index 000000000000..4b6441628500 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.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.bouncer.ui.viewmodel + +import android.content.applicationContext +import com.android.systemui.authentication.domain.interactor.authenticationInteractor +import com.android.systemui.bouncer.domain.interactor.bouncerInteractor +import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor +import com.android.systemui.bouncer.shared.flag.composeBouncerFlags +import com.android.systemui.deviceentry.domain.interactor.biometricMessageInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel +import com.android.systemui.util.time.systemClock +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@ExperimentalCoroutinesApi +val Kosmos.bouncerMessageViewModel by + Kosmos.Fixture { + BouncerMessageViewModel( + applicationContext = applicationContext, + applicationScope = testScope.backgroundScope, + bouncerInteractor = bouncerInteractor, + simBouncerInteractor = simBouncerInteractor, + authenticationInteractor = authenticationInteractor, + selectedUser = userSwitcherViewModel.selectedUser, + clock = systemClock, + biometricMessageInteractor = biometricMessageInteractor, + faceAuthInteractor = deviceEntryFaceAuthInteractor, + deviceEntryInteractor = deviceEntryInteractor, + fingerprintInteractor = deviceEntryFingerprintAuthInteractor, + flags = composeBouncerFlags, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt index 6d97238ba48b..0f6c7cf13211 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.bouncer.ui.viewmodel import android.content.applicationContext @@ -30,7 +32,7 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.user.domain.interactor.selectedUserInteractor import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel import com.android.systemui.util.mockito.mock -import com.android.systemui.util.time.systemClock +import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.bouncerViewModel by Fixture { BouncerViewModel( @@ -47,7 +49,7 @@ val Kosmos.bouncerViewModel by Fixture { users = userSwitcherViewModel.users, userSwitcherMenu = userSwitcherViewModel.menu, actionButton = bouncerActionButtonInteractor.actionButton, - clock = systemClock, devicePolicyManager = mock(), + bouncerMessageViewModel = bouncerMessageViewModel, ) } 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/volume/MediaControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt new file mode 100644 index 000000000000..5db17243c4e3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt @@ -0,0 +1,82 @@ +/* + * 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.volume + +import android.content.packageManager +import android.content.pm.ApplicationInfo +import android.media.AudioAttributes +import android.media.session.MediaController +import android.media.session.MediaSession +import com.android.systemui.kosmos.Kosmos +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 + +private const val LOCAL_PACKAGE = "local.test.pkg" +var Kosmos.localMediaController: MediaController by + Kosmos.Fixture { + val appInfo: ApplicationInfo = mock { + whenever(loadLabel(any())).thenReturn("local_media_controller_label") + } + whenever(packageManager.getApplicationInfo(eq(LOCAL_PACKAGE), any<Int>())) + .thenReturn(appInfo) + + val localSessionToken: MediaSession.Token = MediaSession.Token(0, mock {}) + mock { + whenever(packageName).thenReturn(LOCAL_PACKAGE) + whenever(playbackInfo) + .thenReturn( + MediaController.PlaybackInfo( + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL, + 0, + 0, + 0, + AudioAttributes.Builder().build(), + "", + ) + ) + whenever(sessionToken).thenReturn(localSessionToken) + } + } + +private const val REMOTE_PACKAGE = "remote.test.pkg" +var Kosmos.remoteMediaController: MediaController by + Kosmos.Fixture { + val appInfo: ApplicationInfo = mock { + whenever(loadLabel(any())).thenReturn("remote_media_controller_label") + } + whenever(packageManager.getApplicationInfo(eq(REMOTE_PACKAGE), any<Int>())) + .thenReturn(appInfo) + + val remoteSessionToken: MediaSession.Token = MediaSession.Token(0, mock {}) + mock { + whenever(packageName).thenReturn(REMOTE_PACKAGE) + whenever(playbackInfo) + .thenReturn( + MediaController.PlaybackInfo( + MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE, + 0, + 0, + 0, + AudioAttributes.Builder().build(), + "", + ) + ) + whenever(sessionToken).thenReturn(remoteSessionToken) + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt index 3938f77b9c54..fa3a19bae655 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt @@ -18,7 +18,6 @@ package com.android.systemui.volume import android.content.packageManager import android.content.pm.ApplicationInfo -import android.media.session.MediaController import android.os.Handler import android.testing.TestableLooper import com.android.systemui.kosmos.Kosmos @@ -32,11 +31,10 @@ import com.android.systemui.volume.data.repository.FakeLocalMediaRepository import com.android.systemui.volume.data.repository.FakeMediaControllerRepository import com.android.systemui.volume.panel.component.mediaoutput.data.repository.FakeLocalMediaRepositoryFactory import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory +import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor -var Kosmos.mediaController: MediaController by Kosmos.Fixture { mock {} } - val Kosmos.localMediaRepository by Kosmos.Fixture { FakeLocalMediaRepository() } val Kosmos.localMediaRepositoryFactory: LocalMediaRepositoryFactory by Kosmos.Fixture { FakeLocalMediaRepositoryFactory { localMediaRepository } } @@ -56,6 +54,14 @@ val Kosmos.mediaOutputInteractor by }, testScope.backgroundScope, testScope.testScheduler, + mediaControllerRepository, + ) + } + +val Kosmos.mediaDeviceSessionInteractor by + Kosmos.Fixture { + MediaDeviceSessionInteractor( + testScope.testScheduler, Handler(TestableLooper.get(testCase).looper), mediaControllerRepository, ) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt index 284bd55f15d7..909be7507d34 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt @@ -17,7 +17,6 @@ package com.android.systemui.volume.data.repository import com.android.settingslib.media.MediaDevice -import com.android.settingslib.volume.data.model.RoutingSession import com.android.settingslib.volume.data.repository.LocalMediaRepository import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -25,35 +24,11 @@ import kotlinx.coroutines.flow.asStateFlow class FakeLocalMediaRepository : LocalMediaRepository { - private val volumeBySession: MutableMap<String?, Int> = mutableMapOf() - - private val mutableMediaDevices = MutableStateFlow<List<MediaDevice>>(emptyList()) - override val mediaDevices: StateFlow<List<MediaDevice>> - get() = mutableMediaDevices.asStateFlow() - private val mutableCurrentConnectedDevice = MutableStateFlow<MediaDevice?>(null) override val currentConnectedDevice: StateFlow<MediaDevice?> get() = mutableCurrentConnectedDevice.asStateFlow() - private val mutableRemoteRoutingSessions = MutableStateFlow<List<RoutingSession>>(emptyList()) - override val remoteRoutingSessions: StateFlow<List<RoutingSession>> - get() = mutableRemoteRoutingSessions.asStateFlow() - - fun updateMediaDevices(devices: List<MediaDevice>) { - mutableMediaDevices.value = devices - } - fun updateCurrentConnectedDevice(device: MediaDevice?) { mutableCurrentConnectedDevice.value = device } - - fun updateRemoteRoutingSessions(sessions: List<RoutingSession>) { - mutableRemoteRoutingSessions.value = sessions - } - - fun getSessionVolume(sessionId: String?): Int = volumeBySession.getOrDefault(sessionId, 0) - - override suspend fun adjustSessionVolume(sessionId: String?, volume: Int) { - volumeBySession[sessionId] = volume - } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt index 6d52e525d238..8ab5bd903fdf 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt @@ -24,11 +24,11 @@ import kotlinx.coroutines.flow.asStateFlow class FakeMediaControllerRepository : MediaControllerRepository { - private val mutableActiveLocalMediaController = MutableStateFlow<MediaController?>(null) - override val activeLocalMediaController: StateFlow<MediaController?> = - mutableActiveLocalMediaController.asStateFlow() + private val mutableActiveSessions = MutableStateFlow<List<MediaController>>(emptyList()) + override val activeSessions: StateFlow<List<MediaController>> + get() = mutableActiveSessions.asStateFlow() - fun setActiveLocalMediaController(controller: MediaController?) { - mutableActiveLocalMediaController.value = controller + fun setActiveSessions(sessions: List<MediaController>) { + mutableActiveSessions.value = sessions } } 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/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/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/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..de000bf64c38 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; @@ -12207,7 +12208,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 +12231,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 +13204,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 +13244,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 +13377,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 +13401,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 +13417,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/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..136ab42cd0e8 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 { 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..f655455c5a6b 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; @@ -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/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..60848a787c95 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; @@ -2213,7 +2214,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 +2545,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 +2581,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 +2590,7 @@ public class WindowManagerService extends IWindowManager.Stub } else { maybeSyncSeqId = -1; } - outSyncIdBundle.putInt("seqid", maybeSyncSeqId); + outBundle.putInt(IWindowSession.KEY_RELAYOUT_BUNDLE_SEQID, maybeSyncSeqId); } if (configChanged) { 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/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/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/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/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..07e0e7319f4d --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/AndroidSafetyLabel.java @@ -0,0 +1,102 @@ +/* + * 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 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 { + + public enum Format { + NULL, HUMAN_READABLE, ON_DEVICE; + } + + private final SafetyLabels mSafetyLabels; + + public SafetyLabels getSafetyLabels() { + return mSafetyLabels; + } + + private AndroidSafetyLabel(SafetyLabels safetyLabels) { + this.mSafetyLabels = safetyLabels; + } + + /** Reads a {@link AndroidSafetyLabel} from an {@link InputStream}. */ + // TODO(b/329902686): Support conversion in both directions, specified by format. + 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); + + Element appMetadataBundles = + XmlUtils.getSingleElement(document, XmlUtils.HR_TAG_APP_METADATA_BUNDLES); + + return AndroidSafetyLabel.createFromHrElement(appMetadataBundles); + } + + /** Write the content of the {@link AndroidSafetyLabel} to a {@link OutputStream}. */ + // TODO(b/329902686): Support conversion in both directions, specified by format. + public void writeToStream(OutputStream out, Format format) + throws IOException, ParserConfigurationException, TransformerException { + var docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + var document = docBuilder.newDocument(); + document.appendChild(this.toOdDomElement(document)); + + 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 {@link AndroidSafetyLabel} from human-readable DOM element */ + public static AndroidSafetyLabel createFromHrElement(Element appMetadataBundlesEle) { + Element safetyLabelsEle = + XmlUtils.getSingleElement(appMetadataBundlesEle, XmlUtils.HR_TAG_SAFETY_LABELS); + SafetyLabels safetyLabels = SafetyLabels.createFromHrElement(safetyLabelsEle); + return new AndroidSafetyLabel(safetyLabels); + } + + /** Creates an on-device DOM element from an {@link AndroidSafetyLabel} */ + public Element toOdDomElement(Document doc) { + Element aslEle = doc.createElement(XmlUtils.OD_TAG_BUNDLE); + aslEle.appendChild(mSafetyLabels.toOdDomElement(doc)); + return aslEle; + } + + public static void test() { + // TODO(b/329902686): Add tests. + } +} 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..efdaa4062bdb --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataCategory.java @@ -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.asllib; + +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 { + private final Map<String, DataType> mDataTypes; + + private DataCategory(Map<String, DataType> dataTypes) { + this.mDataTypes = dataTypes; + } + + /** Return the type {@link Map} of String type key to {@link DataType} */ + + public Map<String, DataType> getDataTypes() { + return mDataTypes; + } + + /** Creates a {@link DataCategory} given map of {@param dataTypes}. */ + public static DataCategory create(Map<String, DataType> dataTypes) { + return new DataCategory(dataTypes); + } +} 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/DataLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.java new file mode 100644 index 000000000000..d2c3d75b1d9c --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataLabels.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 org.w3c.dom.NodeList; + +import java.util.HashMap; +import java.util.Map; + +/** + * Data label representation with data shared and data collected maps containing zero or more {@link + * DataCategory} + */ +public class DataLabels { + 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; + } + + /** Creates a {@link DataLabels} from the human-readable DOM element. */ + public static DataLabels createFromHrElement(Element ele) { + 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 dataSharedNodeList = dataLabelsEle.getElementsByTagName(dataCategoryUsageTypeTag); + + for (int i = 0; i < dataSharedNodeList.getLength(); i++) { + Element dataSharedEle = (Element) dataSharedNodeList.item(i); + String dataCategoryName = dataSharedEle.getAttribute(XmlUtils.HR_ATTR_DATA_CATEGORY); + String dataTypeName = dataSharedEle.getAttribute(XmlUtils.HR_ATTR_DATA_TYPE); + + if (!dataTypeMap.containsKey((dataCategoryName))) { + dataTypeMap.put(dataCategoryName, new HashMap<String, DataType>()); + } + dataTypeMap + .get(dataCategoryName) + .put(dataTypeName, DataType.createFromHrElement(dataSharedEle)); + } + + Map<String, DataCategory> dataCategoryMap = new HashMap<String, DataCategory>(); + for (String dataCategoryName : dataTypeMap.keySet()) { + Map<String, DataType> dataTypes = dataTypeMap.get(dataCategoryName); + dataCategoryMap.put(dataCategoryName, DataCategory.create(dataTypes)); + } + return dataCategoryMap; + } + + /** Gets the on-device DOM element for the {@link DataLabels}. */ + public Element toOdDomElement(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 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); + Element dataTypeEle = XmlUtils.createPbundleEleWithName(doc, dataTypeName); + if (!dataType.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(dataType.getPurposeSet().size())); + for (DataType.Purpose purpose : dataType.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, + dataType.getIsCollectionOptional(), + XmlUtils.OD_NAME_IS_COLLECTION_OPTIONAL); + maybeAddBoolToOdElement( + doc, + dataTypeEle, + dataType.getIsSharingOptional(), + XmlUtils.OD_NAME_IS_SHARING_OPTIONAL); + maybeAddBoolToOdElement( + doc, dataTypeEle, dataType.getEphemeral(), XmlUtils.OD_NAME_EPHEMERAL); + + dataCategoryEle.appendChild(dataTypeEle); + } + dataUsageEle.appendChild(dataCategoryEle); + } + dataLabelsEle.appendChild(dataUsageEle); + } + + 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/DataType.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java new file mode 100644 index 000000000000..7451c6923113 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/DataType.java @@ -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.asllib; + +import org.w3c.dom.Element; + +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Data usage type representation. Types are specific to a {@link DataCategory} and contains + * metadata related to the data usage purpose. + */ +public class DataType { + 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 Set<Purpose> mPurposeSet; + private final Boolean mIsCollectionOptional; + private final Boolean mIsSharingOptional; + private final Boolean mEphemeral; + + private DataType( + Set<Purpose> purposeSet, + Boolean isCollectionOptional, + Boolean isSharingOptional, + Boolean ephemeral) { + this.mPurposeSet = purposeSet; + this.mIsCollectionOptional = isCollectionOptional; + this.mIsSharingOptional = isSharingOptional; + this.mEphemeral = ephemeral; + } + + /** + * 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; + } + + /** Creates a {@link DataType} from the human-readable DOM element. */ + public static DataType createFromHrElement(Element hrDataTypeEle) { + Set<Purpose> purposeSet = + Arrays.stream(hrDataTypeEle.getAttribute(XmlUtils.HR_ATTR_PURPOSES).split("\\|")) + .map(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(purposeSet, isCollectionOptional, isSharingOptional, ephemeral); + } +} 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/SafetyLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java new file mode 100644 index 000000000000..6ba15e1ec4db --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/SafetyLabels.java @@ -0,0 +1,65 @@ +/* + * 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; + +/** Safety Label representation containing zero or more {@link DataCategory} for data shared */ +public class SafetyLabels { + + private final Long mVersion; + private final DataLabels mDataLabels; + + private 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 a {@link SafetyLabels} from the human-readable DOM element. */ + public static SafetyLabels createFromHrElement(Element safetyLabelsEle) { + 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."); + } + Element dataLabelsEle = + XmlUtils.getSingleElement(safetyLabelsEle, XmlUtils.HR_TAG_DATA_LABELS); + DataLabels dataLabels = DataLabels.createFromHrElement(dataLabelsEle); + return new SafetyLabels(version, dataLabels); + } + + /** Creates an on-device DOM element from the {@link SafetyLabels}. */ + public Element toOdDomElement(Document doc) { + Element safetyLabelsEle = + XmlUtils.createPbundleEleWithName(doc, XmlUtils.OD_NAME_SAFETY_LABELS); + safetyLabelsEle.appendChild(mDataLabels.toOdDomElement(doc)); + return safetyLabelsEle; + } +} 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..4392c2c220c4 --- /dev/null +++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/XmlUtils.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.asllib; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +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, tagName); + } + + /** + * Gets the single {@link Element} within {@param parentEle} and having the {@param tagName}. + */ + public static Element getSingleElement(Element parentEle, String tagName) { + var elements = parentEle.getElementsByTagName(tagName); + return getSingleElement(elements, tagName); + } + + /** Gets the single {@link Element} from {@param elements} and having the {@param tagName}. */ + public static Element getSingleElement(NodeList elements, String tagName) { + if (elements.getLength() != 1) { + throw new IllegalArgumentException( + String.format("Expected 1 %s but got %s.", tagName, elements.getLength())); + } + var elementAsNode = elements.item(0); + if (!(elementAsNode instanceof Element)) { + throw new IllegalStateException(String.format("%s was not an element.", tagName)); + } + return ((Element) elementAsNode); + } + + /** 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(); + } +} |