diff options
403 files changed, 13195 insertions, 9239 deletions
diff --git a/cmds/uinput/README.md b/cmds/uinput/README.md index 47e1dad9ccd6..1ce8f9f33a9c 100644 --- a/cmds/uinput/README.md +++ b/cmds/uinput/README.md @@ -1,87 +1,96 @@ # Usage -## Two options to use the uinput command: -### 1. Interactive through stdin: -type `uinput -` into the terminal, then type/paste commands to send to the binary. -Use Ctrl+D to signal end of stream to the binary (EOF). -This mode can be also used from an app to send uinput events. -For an example, see the cts test case at: [InputTestCase.java][2] +There are two ways to use the `uinput` command: -When using another program to control uinput in interactive mode, registering a -new input device (for example, a bluetooth joystick) should be the first step. -After the device is added, you need to wait for the _onInputDeviceAdded_ -(see [InputDeviceListener][1]) notification before issuing commands -to the device. -Failure to do so will cause missed events and inconsistent behavior. +* **Recommended:** `uinput -` reads commands from standard input until End-of-File (Ctrl+D) is sent. + This mode can be used interactively from a terminal or used to control uinput from another program + or app (such as the CTS tests via [`UinputDevice`][UinputDevice]). +* `uinput <filename>` reads commands from a file instead of standard input. -### 2. Using a file as an input: -type `uinput <filename>`, and the file will be used an an input to the binary. -You must add a sufficient delay after a "register" command to ensure device -is ready. The interactive mode is the recommended method of communicating -with the uinput binary. +[UinputDevice]: https://cs.android.com/android/platform/superproject/main/+/main:cts/libs/input/src/com/android/cts/input/UinputDevice.java -All of the input commands should be in pseudo-JSON format as documented below. -See examples [here][3]. +## Command format -The file can have multiple commands one after the other (which is not strictly -legal JSON format, as this would imply multiple root elements). +Input commands should be in JSON format, though the parser is in [lenient mode] to allow comments, +and integers can be specified in hexadecimal (e.g. `0xABCD`). The input file (or standard input) can +contain multiple commands, which will be executed in sequence. Simply add multiple JSON objects to +the file, one after the other without separators: -## Command description +```json5 +{ + "id": 1, + "command": "register", + // ... +} +{ + "id": 1, + "command": "delay", + // ... +} +``` + +Many examples of command files can be found [in the CTS tests][cts-example-jsons]. + +[lenient mode]: https://developer.android.com/reference/android/util/JsonReader#setLenient(boolean) +[cts-example-jsons]: https://cs.android.com/android/platform/superproject/main/+/main:cts/tests/tests/hardware/res/raw/ + +## Command reference + +### `register` -1. `register` Register a new uinput device -| Field | Type | Description | +| Field | Type | Description | +|:----------------:|:--------------:|:-------------------------- | +| `id` | integer | Device ID | +| `command` | string | Must be set to "register" | +| `name` | string | Device name | +| `vid` | 16-bit integer | Vendor ID | +| `pid` | 16-bit integer | Product ID | +| `bus` | string | Bus that device should use | +| `configuration` | object array | uinput device configuration| +| `ff_effects_max` | integer | `ff_effects_max` value | +| `abs_info` | array | Absolute axes information | + +`id` is used for matching the subsequent commands to a specific device to avoid ambiguity when +multiple devices are registered. + +`bus` is used to determine how the uinput device is connected to the host. The options are `"usb"` +and `"bluetooth"`. + +Device configuration is used to configure the uinput device. The `type` field provides a `UI_SET_*` +control code, and data is a vector of control values to be sent to the uinput device, which depends +on the control code. + +| Field | Type | Description | |:-------------:|:-------------:|:-------------------------- | -| id | integer | Device id | -| command | string | Must be set to "register" | -| name | string | Device name | -| vid | 16-bit integer| Vendor id | -| pid | 16-bit integer| Product id | -| bus | string | Bus that device should use | -| configuration | int array | uinput device configuration| -| ff_effects_max| integer | ff_effects_max value | -| abs_info | array | ABS axes information | - -Device ID is used for matching the subsequent commands to a specific device -to avoid ambiguity when multiple devices are registered. - -Device bus is used to determine how the uinput device is connected to the host. -The options are "usb" and "bluetooth". - -Device configuration is used to configure uinput device. "type" field provides the UI_SET_* -control code, and data is a vector of control values to be sent to uinput device, depends on -the control code. +| `type` | integer | `UI_SET_` control type | +| `data` | integer array | control values | -| Field | Type | Description | -|:-------------:|:-------------:|:-------------------------- | -| type | integer | UI_SET_ control type | -| data | int array | control values | +`ff_effects_max` must be provided if `UI_SET_FFBIT` is used in `configuration`. -Device ff_effects_max must be provided if FFBIT is set. +`abs_info` fields are provided to set the device axes information. It is an array of below objects: -Device abs_info fields are provided to set the device axes information. It is an array of below -objects: | Field | Type | Description | |:-------------:|:-------------:|:-------------------------- | -| code | integer | Axis code | -| info | object | ABS information object | +| `code` | integer | Axis code | +| `info` | object | Axis information object | + +The axis information object is defined as below, with the fields having the same meaning as those +Linux's [`struct input_absinfo`][struct input_absinfo]: -ABS information object is defined as below: | Field | Type | Description | |:-------------:|:-------------:|:-------------------------- | -| value | integer | Latest reported value | -| minimum | integer | Minimum value for the axis | -| maximum | integer | Maximum value for the axis | -| fuzz | integer | fuzz value for noise filter| -| flat | integer | values to be discarded | -| resolution | integer | resolution of axis | - -See [struct input_absinfo][4]) definitions. +| `value` | integer | Latest reported value | +| `minimum` | integer | Minimum value for the axis | +| `maximum` | integer | Maximum value for the axis | +| `fuzz` | integer | fuzz value for noise filter| +| `flat` | integer | values to be discarded | +| `resolution` | integer | resolution of axis | Example: -```json +```json5 { "id": 1, "command": "register", @@ -90,9 +99,9 @@ Example: "pid": 0x2c42, "bus": "usb", "configuration":[ - {"type":100, "data":[1, 21]}, // UI_SET_EVBIT : EV_KEY and EV_FF + {"type":100, "data":[1, 21]}, // UI_SET_EVBIT : EV_KEY and EV_FF {"type":101, "data":[11, 2, 3, 4]}, // UI_SET_KEYBIT : KEY_0 KEY_1 KEY_2 KEY_3 - {"type":107, "data":[80]} // UI_SET_FFBIT : FF_RUMBLE + {"type":107, "data":[80]} // UI_SET_FFBIT : FF_RUMBLE ], "ff_effects_max" : 1, "abs_info": [ @@ -104,19 +113,39 @@ Example: } ] } - ``` -2. `delay` + +[struct input_absinfo]: https://cs.android.com/android/platform/superproject/main/+/main:bionic/libc/kernel/uapi/linux/input.h?q=%22struct%20input_absinfo%22 + +#### Waiting for registration + +After the command is sent, there will be a delay before the device is set up by the Android input +stack, and `uinput` does not wait for that process to finish. Any commands sent to the device during +that time will be dropped. If you are controlling `uinput` by sending commands through standard +input from an app, you need to wait for [`onInputDeviceAdded`][onInputDeviceAdded] to be called on +an `InputDeviceListener` before issuing commands to the device. If you are passing a file to +`uinput`, add a `delay` after the `register` command to let registration complete. + +[onInputDeviceAdded]: https://developer.android.com/reference/android/hardware/input/InputManager.InputDeviceListener.html + +#### Unregistering the device + +As soon as EOF is reached (either in interactive mode, or in file mode), the device that was created +will be unregistered. There is no explicit command for unregistering a device. + +### `delay` + Add a delay to command processing | Field | Type | Description | |:-------------:|:-------------:|:-------------------------- | -| id | integer | Device id | -| command | string | Must be set to "delay" | -| duration | integer | Delay in milliseconds | +| `id` | integer | Device ID | +| `command` | string | Must be set to "delay" | +| `duration` | integer | Delay in milliseconds | Example: -```json + +```json5 { "id": 1, "command": "delay", @@ -124,20 +153,21 @@ Example: } ``` -3. `inject` -Send an array of uinput event packets [type, code, value] to the uinput device +### `inject` + +Send an array of uinput event packets to the uinput device | Field | Type | Description | |:-------------:|:-------------:|:-------------------------- | -| id | integer | Device id | -| command | string | Must be set to "inject" | -| events | integer array | events to inject | +| `id` | integer | Device ID | +| `command` | string | Must be set to "inject" | +| `events` | integer array | events to inject | -The "events" parameter is an array of integers, encapsulates evdev input_event type, code and value, -see the example below. +The `events` parameter is an array of integers in sets of three: a type, an axis code, and an axis +value, like you'd find in Linux's `struct input_event`. For example, sending presses of the 0 and 1 +keys would look like this: -Example: -```json +```json5 { "id": 1, "command": "inject", @@ -153,14 +183,6 @@ Example: } ``` -### Notes -1. As soon as EOF is reached (either in interactive mode, or in file mode), -the device that was created will be unregistered. There is no -explicit command for unregistering a device. -2. The `getevent` utility can used to print out the key events -for debugging purposes. - -[1]: https://developer.android.com/reference/android/hardware/input/InputManager.InputDeviceListener.html -[2]: ../../../../cts/tests/tests/hardware/src/android/hardware/input/cts/tests/InputTestCase.java -[3]: ../../../../cts/tests/tests/hardware/res/raw/ -[4]: ../../../../bionic/libc/kernel/uapi/linux/input.h +## Notes + +The `getevent` utility can used to print out the key events for debugging purposes. diff --git a/core/api/current.txt b/core/api/current.txt index a03abd83f230..1e2676c86b7a 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -12819,6 +12819,7 @@ package android.content.pm { field public static final String FEATURE_TELEPHONY_RADIO_ACCESS = "android.hardware.telephony.radio.access"; field public static final String FEATURE_TELEPHONY_SUBSCRIPTION = "android.hardware.telephony.subscription"; field @Deprecated public static final String FEATURE_TELEVISION = "android.hardware.type.television"; + field public static final String FEATURE_THREADNETWORK = "android.hardware.threadnetwork"; field public static final String FEATURE_TOUCHSCREEN = "android.hardware.touchscreen"; field public static final String FEATURE_TOUCHSCREEN_MULTITOUCH = "android.hardware.touchscreen.multitouch"; field public static final String FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT = "android.hardware.touchscreen.multitouch.distinct"; diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 60cdb846ee95..fbc759f790f3 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -17179,6 +17179,154 @@ package android.telephony.mbms.vendor { } +package android.telephony.satellite { + + public final class AntennaDirection implements android.os.Parcelable { + method public int describeContents(); + method public float getX(); + method public float getY(); + method public float getZ(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.telephony.satellite.AntennaDirection> CREATOR; + } + + public final class AntennaPosition implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public android.telephony.satellite.AntennaDirection getAntennaDirection(); + method public int getSuggestedHoldPosition(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.telephony.satellite.AntennaPosition> CREATOR; + } + + public final class PointingInfo implements android.os.Parcelable { + method public int describeContents(); + method public float getSatelliteAzimuthDegrees(); + method public float getSatelliteElevationDegrees(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.telephony.satellite.PointingInfo> CREATOR; + } + + public final class SatelliteCapabilities implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public java.util.Map<java.lang.Integer,android.telephony.satellite.AntennaPosition> getAntennaPositionMap(); + method public int getMaxBytesPerOutgoingDatagram(); + method @NonNull public java.util.Set<java.lang.Integer> getSupportedRadioTechnologies(); + method public boolean isPointingRequired(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.telephony.satellite.SatelliteCapabilities> CREATOR; + } + + public final class SatelliteDatagram implements android.os.Parcelable { + method public int describeContents(); + method @NonNull public byte[] getSatelliteDatagram(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.telephony.satellite.SatelliteDatagram> CREATOR; + } + + public interface SatelliteDatagramCallback { + method public void onSatelliteDatagramReceived(long, @NonNull android.telephony.satellite.SatelliteDatagram, int, @NonNull java.util.function.Consumer<java.lang.Void>); + } + + public class SatelliteManager { + method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionSatelliteService(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); + method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void onDeviceAlignedWithSatellite(boolean); + method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void pollPendingSatelliteDatagrams(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); + method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionSatelliteService(@NonNull String, @NonNull byte[], @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); + method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteDatagram(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteDatagramCallback); + method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteModemStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteStateCallback); + method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteProvisionStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteProvisionStateCallback); + method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsDemoModeEnabled(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); + method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsSatelliteCommunicationAllowedForCurrentLocation(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); + method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsSatelliteEnabled(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); + method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestIsSatelliteProvisioned(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); + method public void requestIsSatelliteSupported(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,android.telephony.satellite.SatelliteManager.SatelliteException>); + method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestSatelliteCapabilities(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<android.telephony.satellite.SatelliteCapabilities,android.telephony.satellite.SatelliteManager.SatelliteException>); + method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestSatelliteEnabled(boolean, boolean, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); + method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void requestTimeForNextSatelliteVisibility(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.time.Duration,android.telephony.satellite.SatelliteManager.SatelliteException>); + method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void sendSatelliteDatagram(int, @NonNull android.telephony.satellite.SatelliteDatagram, boolean, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); + method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void startSatelliteTransmissionUpdates(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>, @NonNull android.telephony.satellite.SatelliteTransmissionUpdateCallback); + method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void stopSatelliteTransmissionUpdates(@NonNull android.telephony.satellite.SatelliteTransmissionUpdateCallback, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); + method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForSatelliteDatagram(@NonNull android.telephony.satellite.SatelliteDatagramCallback); + method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForSatelliteModemStateChanged(@NonNull android.telephony.satellite.SatelliteStateCallback); + method @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void unregisterForSatelliteProvisionStateChanged(@NonNull android.telephony.satellite.SatelliteProvisionStateCallback); + field public static final int DATAGRAM_TYPE_LOCATION_SHARING = 2; // 0x2 + field public static final int DATAGRAM_TYPE_SOS_MESSAGE = 1; // 0x1 + field public static final int DATAGRAM_TYPE_UNKNOWN = 0; // 0x0 + field public static final int DEVICE_HOLD_POSITION_LANDSCAPE_LEFT = 2; // 0x2 + field public static final int DEVICE_HOLD_POSITION_LANDSCAPE_RIGHT = 3; // 0x3 + field public static final int DEVICE_HOLD_POSITION_PORTRAIT = 1; // 0x1 + field public static final int DEVICE_HOLD_POSITION_UNKNOWN = 0; // 0x0 + field public static final int DISPLAY_MODE_CLOSED = 3; // 0x3 + field public static final int DISPLAY_MODE_FIXED = 1; // 0x1 + field public static final int DISPLAY_MODE_OPENED = 2; // 0x2 + field public static final int DISPLAY_MODE_UNKNOWN = 0; // 0x0 + field public static final int NT_RADIO_TECHNOLOGY_EMTC_NTN = 3; // 0x3 + field public static final int NT_RADIO_TECHNOLOGY_NB_IOT_NTN = 1; // 0x1 + field public static final int NT_RADIO_TECHNOLOGY_NR_NTN = 2; // 0x2 + field public static final int NT_RADIO_TECHNOLOGY_PROPRIETARY = 4; // 0x4 + field public static final int NT_RADIO_TECHNOLOGY_UNKNOWN = 0; // 0x0 + field public static final int SATELLITE_ACCESS_BARRED = 16; // 0x10 + field public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_IDLE = 0; // 0x0 + field public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVE_FAILED = 7; // 0x7 + field public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVE_NONE = 6; // 0x6 + field public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVE_SUCCESS = 5; // 0x5 + field public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_RECEIVING = 4; // 0x4 + field public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_SENDING = 1; // 0x1 + field public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_SEND_FAILED = 3; // 0x3 + field public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_SEND_SUCCESS = 2; // 0x2 + field public static final int SATELLITE_DATAGRAM_TRANSFER_STATE_UNKNOWN = -1; // 0xffffffff + field public static final int SATELLITE_ERROR = 1; // 0x1 + field public static final int SATELLITE_ERROR_NONE = 0; // 0x0 + field public static final int SATELLITE_INVALID_ARGUMENTS = 8; // 0x8 + field public static final int SATELLITE_INVALID_MODEM_STATE = 7; // 0x7 + field public static final int SATELLITE_INVALID_TELEPHONY_STATE = 6; // 0x6 + field public static final int SATELLITE_MODEM_BUSY = 22; // 0x16 + field public static final int SATELLITE_MODEM_ERROR = 4; // 0x4 + field public static final int SATELLITE_MODEM_STATE_DATAGRAM_RETRYING = 3; // 0x3 + field public static final int SATELLITE_MODEM_STATE_DATAGRAM_TRANSFERRING = 2; // 0x2 + field public static final int SATELLITE_MODEM_STATE_IDLE = 0; // 0x0 + field public static final int SATELLITE_MODEM_STATE_LISTENING = 1; // 0x1 + field public static final int SATELLITE_MODEM_STATE_OFF = 4; // 0x4 + field public static final int SATELLITE_MODEM_STATE_UNAVAILABLE = 5; // 0x5 + field public static final int SATELLITE_MODEM_STATE_UNKNOWN = -1; // 0xffffffff + field public static final int SATELLITE_NETWORK_ERROR = 5; // 0x5 + field public static final int SATELLITE_NETWORK_TIMEOUT = 17; // 0x11 + field public static final int SATELLITE_NOT_AUTHORIZED = 19; // 0x13 + field public static final int SATELLITE_NOT_REACHABLE = 18; // 0x12 + field public static final int SATELLITE_NOT_SUPPORTED = 20; // 0x14 + field public static final int SATELLITE_NO_RESOURCES = 12; // 0xc + field public static final int SATELLITE_RADIO_NOT_AVAILABLE = 10; // 0xa + field public static final int SATELLITE_REQUEST_ABORTED = 15; // 0xf + field public static final int SATELLITE_REQUEST_FAILED = 9; // 0x9 + field public static final int SATELLITE_REQUEST_IN_PROGRESS = 21; // 0x15 + field public static final int SATELLITE_REQUEST_NOT_SUPPORTED = 11; // 0xb + field public static final int SATELLITE_SERVER_ERROR = 2; // 0x2 + field public static final int SATELLITE_SERVICE_ERROR = 3; // 0x3 + field public static final int SATELLITE_SERVICE_NOT_PROVISIONED = 13; // 0xd + field public static final int SATELLITE_SERVICE_PROVISION_IN_PROGRESS = 14; // 0xe + } + + public static class SatelliteManager.SatelliteException extends java.lang.Exception { + ctor public SatelliteManager.SatelliteException(int); + method public int getErrorCode(); + } + + public interface SatelliteProvisionStateCallback { + method public void onSatelliteProvisionStateChanged(boolean); + } + + public interface SatelliteStateCallback { + method public void onSatelliteModemStateChanged(int); + } + + public interface SatelliteTransmissionUpdateCallback { + method public void onReceiveDatagramStateChanged(int, int, int); + method public void onSatellitePositionChanged(@NonNull android.telephony.satellite.PointingInfo); + method public void onSendDatagramStateChanged(int, int, int); + } + +} + package android.text { public final class FontConfig implements android.os.Parcelable { diff --git a/core/api/system-lint-baseline.txt b/core/api/system-lint-baseline.txt index 0100f0e76285..6c233270bad5 100644 --- a/core/api/system-lint-baseline.txt +++ b/core/api/system-lint-baseline.txt @@ -15,6 +15,12 @@ KotlinKeyword: android.app.Notification#when: Avoid field names that are Kotlin hard keywords ("when"); see https://android.github.io/kotlin-guides/interop.html#no-hard-keywords +ListenerLast: android.telephony.satellite.SatelliteManager#stopSatelliteTransmissionUpdates(android.telephony.satellite.SatelliteTransmissionUpdateCallback, java.util.concurrent.Executor, java.util.function.Consumer<java.lang.Integer>) parameter #1: + Listeners should always be at end of argument list (method `stopSatelliteTransmissionUpdates`) +ListenerLast: android.telephony.satellite.SatelliteManager#stopSatelliteTransmissionUpdates(android.telephony.satellite.SatelliteTransmissionUpdateCallback, java.util.concurrent.Executor, java.util.function.Consumer<java.lang.Integer>) parameter #2: + Listeners should always be at end of argument list (method `stopSatelliteTransmissionUpdates`) + + MissingGetterMatchingBuilder: android.telecom.CallScreeningService.CallResponse.Builder#setShouldScreenCallViaAudioProcessing(boolean): android.telecom.CallScreeningService.CallResponse does not declare a `shouldScreenCallViaAudioProcessing()` method matching method android.telecom.CallScreeningService.CallResponse.Builder.setShouldScreenCallViaAudioProcessing(boolean) MissingGetterMatchingBuilder: android.telephony.mbms.DownloadRequest.Builder#setServiceId(String): @@ -211,6 +217,8 @@ SamShouldBeLast: android.security.KeyChain#choosePrivateKeyAlias(android.app.Act SAM-compatible parameters (such as parameter 2, "response", in android.security.KeyChain.choosePrivateKeyAlias) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions SamShouldBeLast: android.security.KeyChain#choosePrivateKeyAlias(android.app.Activity, android.security.KeyChainAliasCallback, String[], java.security.Principal[], android.net.Uri, String): SAM-compatible parameters (such as parameter 2, "response", in android.security.KeyChain.choosePrivateKeyAlias) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions +SamShouldBeLast: android.telephony.satellite.SatelliteManager#startSatelliteTransmissionUpdates(java.util.concurrent.Executor, java.util.function.Consumer<java.lang.Integer>, android.telephony.satellite.SatelliteTransmissionUpdateCallback): + SAM-compatible parameters (such as parameter 2, "resultListener", in android.telephony.satellite.SatelliteManager.startSatelliteTransmissionUpdates) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions SamShouldBeLast: android.view.View#postDelayed(Runnable, long): SAM-compatible parameters (such as parameter 1, "action", in android.view.View.postDelayed) should be last to improve Kotlin interoperability; see https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions SamShouldBeLast: android.view.View#postOnAnimationDelayed(Runnable, long): diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 083c445407dc..050667b166bc 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -3527,6 +3527,8 @@ package android.view { method @RequiresPermission("android.permission.DISABLE_INPUT_DEVICE") public void disable(); method @RequiresPermission("android.permission.DISABLE_INPUT_DEVICE") public void enable(); method @NonNull public android.hardware.input.InputDeviceIdentifier getIdentifier(); + method @Nullable public String getKeyboardLanguageTag(); + method @Nullable public String getKeyboardLayoutType(); } public abstract class InputEvent implements android.os.Parcelable { diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 12ffdb37f106..58c25489431a 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -65,7 +65,6 @@ import android.app.servertransaction.PendingTransactionActions.StopInfo; import android.app.servertransaction.ResumeActivityItem; import android.app.servertransaction.TransactionExecutor; import android.app.servertransaction.TransactionExecutorHelper; -import android.app.servertransaction.WindowTokenClientController; import android.bluetooth.BluetoothFrameworkInitializer; import android.companion.virtual.VirtualDeviceManager; import android.compat.annotation.UnsupportedAppUsage; @@ -205,6 +204,7 @@ import android.window.SizeConfigurationBuckets; import android.window.SplashScreen; import android.window.SplashScreenView; import android.window.WindowProviderService; +import android.window.WindowTokenClientController; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -1111,6 +1111,10 @@ public final class ActivityThread extends ClientTransactionHandler s.token = token; s.info = info; + if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { + Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER, "scheduleCreateService. token=" + + token); + } sendMessage(H.CREATE_SERVICE, s); } @@ -1126,6 +1130,11 @@ public final class ActivityThread extends ClientTransactionHandler if (DEBUG_SERVICE) Slog.v(TAG, "scheduleBindService token=" + token + " intent=" + intent + " uid=" + Binder.getCallingUid() + " pid=" + Binder.getCallingPid()); + + if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { + Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER, "scheduleBindService. token=" + + token + " bindSeq=" + bindSeq); + } sendMessage(H.BIND_SERVICE, s); } @@ -1135,6 +1144,10 @@ public final class ActivityThread extends ClientTransactionHandler s.intent = intent; s.bindSeq = -1; + if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { + Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER, "scheduleUnbindService. token=" + + token); + } sendMessage(H.UNBIND_SERVICE, s); } @@ -1150,16 +1163,28 @@ public final class ActivityThread extends ClientTransactionHandler s.flags = ssa.flags; s.args = ssa.args; + if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { + Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER, "scheduleServiceArgs. token=" + + token + " startId=" + s.startId); + } sendMessage(H.SERVICE_ARGS, s); } } public final void scheduleStopService(IBinder token) { + if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { + Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER, "scheduleStopService. token=" + + token); + } sendMessage(H.STOP_SERVICE, token); } @Override public final void scheduleTimeoutService(IBinder token, int startId) { + if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { + Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER, "scheduleTimeoutService. token=" + + token); + } sendMessage(H.TIMEOUT_SERVICE, token, startId); } diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 1f95497a7dba..5feafbed148c 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -27,7 +27,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.UiContext; -import android.app.servertransaction.WindowTokenClientController; import android.companion.virtual.VirtualDeviceManager; import android.compat.annotation.UnsupportedAppUsage; import android.content.AttributionSource; @@ -97,6 +96,7 @@ import android.view.DisplayAdjustments; import android.view.autofill.AutofillManager.AutofillClient; import android.window.WindowContext; import android.window.WindowTokenClient; +import android.window.WindowTokenClientController; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.Preconditions; diff --git a/core/java/android/app/HomeVisibilityListener.java b/core/java/android/app/HomeVisibilityListener.java index ca20648a871a..0b5a5ed100c9 100644 --- a/core/java/android/app/HomeVisibilityListener.java +++ b/core/java/android/app/HomeVisibilityListener.java @@ -25,6 +25,7 @@ import android.annotation.SystemApi; import android.annotation.TestApi; import android.content.Context; import android.os.Binder; +import android.util.Log; import java.util.List; import java.util.concurrent.Executor; @@ -40,6 +41,8 @@ import java.util.concurrent.Executor; @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @TestApi public abstract class HomeVisibilityListener { + private static final String TAG = HomeVisibilityListener.class.getSimpleName(); + private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); private ActivityTaskManager mActivityTaskManager; private Executor mExecutor; private int mMaxScanTasksForHomeVisibility; @@ -102,6 +105,11 @@ public abstract class HomeVisibilityListener { for (int i = 0, taskSize = tasksTopToBottom.size(); i < taskSize; ++i) { ActivityManager.RunningTaskInfo task = tasksTopToBottom.get(i); + if (DBG) { + Log.d(TAG, "Task#" + i + ": activity=" + task.topActivity + + ", visible=" + task.isVisible() + + ", flg=" + Integer.toHexString(task.baseIntent.getFlags())); + } if (!task.isVisible() || (task.baseIntent.getFlags() & FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) != 0) { continue; diff --git a/core/java/android/app/LocaleConfig.java b/core/java/android/app/LocaleConfig.java index 729e555509a6..0857c9655e8d 100644 --- a/core/java/android/app/LocaleConfig.java +++ b/core/java/android/app/LocaleConfig.java @@ -40,7 +40,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Set; @@ -195,7 +195,8 @@ public class LocaleConfig implements Parcelable { XmlUtils.beginDocument(parser, TAG_LOCALE_CONFIG); int outerDepth = parser.getDepth(); AttributeSet attrs = Xml.asAttributeSet(parser); - Set<String> localeNames = new HashSet<String>(); + // LinkedHashSet to preserve insertion order + Set<String> localeNames = new LinkedHashSet<>(); while (XmlUtils.nextElementWithin(parser, outerDepth)) { if (TAG_LOCALE.equals(parser.getName())) { final TypedArray attributes = res.obtainAttributes( diff --git a/core/java/android/app/TEST_MAPPING b/core/java/android/app/TEST_MAPPING index 0f66e93f7b3c..8da84426c041 100644 --- a/core/java/android/app/TEST_MAPPING +++ b/core/java/android/app/TEST_MAPPING @@ -16,7 +16,12 @@ }, { "file_patterns": ["(/|^)AppOpsManager.java"], - "name": "CtsAppOpsTestCases" + "name": "CtsAppOpsTestCases", + "options": [ + { + "exclude-annotation": "android.platform.test.annotations.FlakyTest" + } + ] }, { "file_patterns": ["(/|^)AppOpsManager.java"], @@ -57,7 +62,7 @@ "name": "CtsWindowManagerDeviceTestCases", "options": [ { - "include-filter": "android.server.wm.ToastWindowTest" + "include-filter": "android.server.wm.window.ToastWindowTest" } ], "file_patterns": ["INotificationManager\\.aidl"] @@ -263,6 +268,10 @@ { "file_patterns": ["(/|^)ActivityThreadTest.java"], "name": "FrameworksCoreTests" + }, + { + "file_patterns": ["(/|^)AppOpsManager.java"], + "name": "CtsAppOpsTestCases" } ] } diff --git a/core/java/android/app/usage/IUsageStatsManager.aidl b/core/java/android/app/usage/IUsageStatsManager.aidl index 429551722bfd..49543a1e3d99 100644 --- a/core/java/android/app/usage/IUsageStatsManager.aidl +++ b/core/java/android/app/usage/IUsageStatsManager.aidl @@ -87,6 +87,8 @@ interface IUsageStatsManager { int userId); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RESPONSE_STATS)") void clearBroadcastEvents(String callingPackage, int userId); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.DUMP)") + boolean isPackageExemptedFromBroadcastResponseStats(String packageName, int userId); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG)") String getAppStandbyConstant(String key); } diff --git a/core/java/android/app/usage/UsageStatsManager.java b/core/java/android/app/usage/UsageStatsManager.java index 4b0ca2826656..ecf164394c31 100644 --- a/core/java/android/app/usage/UsageStatsManager.java +++ b/core/java/android/app/usage/UsageStatsManager.java @@ -1528,6 +1528,22 @@ public final class UsageStatsManager { } } + /** + * Checks whether the given {@code packageName} is exempted from broadcast response tracking. + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.DUMP) + @UserHandleAware + public boolean isPackageExemptedFromBroadcastResponseStats(@NonNull String packageName) { + try { + return mService.isPackageExemptedFromBroadcastResponseStats(packageName, + mContext.getUserId()); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + /** @hide */ @RequiresPermission(Manifest.permission.READ_DEVICE_CONFIG) @Nullable diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java index c10e8984da77..060a5c85a713 100644 --- a/core/java/android/companion/virtual/VirtualDeviceManager.java +++ b/core/java/android/companion/virtual/VirtualDeviceManager.java @@ -102,13 +102,6 @@ public final class VirtualDeviceManager { public static final String EXTRA_VIRTUAL_DEVICE_ID = "android.companion.virtual.extra.VIRTUAL_DEVICE_ID"; - /** - * A representation of an invalid CDM association ID. Association IDs must be positive. - * - * @hide - */ - public static final int ASSOCIATION_ID_INVALID = -1; - /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef( diff --git a/core/java/android/content/pm/IShortcutService.aidl b/core/java/android/content/pm/IShortcutService.aidl index c9735b05cba4..86087cb0c0ef 100644 --- a/core/java/android/content/pm/IShortcutService.aidl +++ b/core/java/android/content/pm/IShortcutService.aidl @@ -61,7 +61,7 @@ interface IShortcutService { void resetThrottling(); // system only API for developer opsions - void onApplicationActive(String packageName, int userId); // system only API for sysUI + oneway void onApplicationActive(String packageName, int userId); // system only API for sysUI byte[] getBackupPayload(int user); diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 37917771bd74..d9a61aec01cb 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -2373,6 +2373,7 @@ public abstract class PackageManager { USER_MIN_ASPECT_RATIO_4_3, USER_MIN_ASPECT_RATIO_16_9, USER_MIN_ASPECT_RATIO_3_2, + USER_MIN_ASPECT_RATIO_FULLSCREEN, }) @Retention(RetentionPolicy.SOURCE) public @interface UserMinAspectRatio {} @@ -2394,8 +2395,9 @@ public abstract class PackageManager { /** * Aspect ratio override code: user forces app to the aspect ratio of the device display size. - * This will be the portrait aspect ratio of the device if the app is portrait or the landscape - * aspect ratio of the device if the app is landscape. + * This will be the portrait aspect ratio of the device if the app has fixed portrait + * orientation or the landscape aspect ratio of the device if the app has fixed landscape + * orientation. * * @hide */ @@ -2419,6 +2421,12 @@ public abstract class PackageManager { */ public static final int USER_MIN_ASPECT_RATIO_3_2 = 5; + /** + * Aspect ratio override code: user forces app to fullscreen + * @hide + */ + public static final int USER_MIN_ASPECT_RATIO_FULLSCREEN = 6; + /** @hide */ @IntDef(flag = true, prefix = { "DELETE_" }, value = { DELETE_KEEP_DATA, @@ -3684,6 +3692,14 @@ public abstract class PackageManager { /** * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature}: The device is capable of communicating with + * other devices via Thread network. + */ + @SdkConstant(SdkConstantType.FEATURE) + public static final String FEATURE_THREADNETWORK = "android.hardware.threadnetwork"; + + /** + * Feature for {@link #getSystemAvailableFeatures} and + * {@link #hasSystemFeature}: The device is capable of communicating with * other devices via ultra wideband. */ @SdkConstant(SdkConstantType.FEATURE) diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index 048289f56a0c..508eeed9335b 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -1456,8 +1456,8 @@ public class PackageParser { private static AssetManager newConfiguredAssetManager() { AssetManager assetManager = new AssetManager(); - assetManager.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - Build.VERSION.RESOURCES_SDK_INT); + assetManager.setConfiguration(0, 0, null, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, Build.VERSION.RESOURCES_SDK_INT); return assetManager; } @@ -9011,8 +9011,8 @@ public class PackageParser { } AssetManager assets = new AssetManager(); - assets.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - Build.VERSION.RESOURCES_SDK_INT); + assets.setConfiguration(0, 0, null, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, Build.VERSION.RESOURCES_SDK_INT); assets.setApkAssets(apkAssets, false /*invalidateCaches*/); mCachedAssetManager = assets; @@ -9086,8 +9086,8 @@ public class PackageParser { private static AssetManager createAssetManagerWithAssets(ApkAssets[] apkAssets) { final AssetManager assets = new AssetManager(); - assets.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - Build.VERSION.RESOURCES_SDK_INT); + assets.setConfiguration(0, 0, null, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, Build.VERSION.RESOURCES_SDK_INT); assets.setApkAssets(apkAssets, false /*invalidateCaches*/); return assets; } diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index b225de402f17..23b9d0b7c9a7 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -1480,9 +1480,13 @@ public final class AssetManager implements AutoCloseable { int screenWidth, int screenHeight, int smallestScreenWidthDp, int screenWidthDp, int screenHeightDp, int screenLayout, int uiMode, int colorMode, int grammaticalGender, int majorVersion) { - synchronized (this) { - ensureValidLocked(); - nativeSetConfiguration(mObject, mcc, mnc, locale, orientation, touchscreen, density, + if (locale != null) { + setConfiguration(mcc, mnc, null, new String[]{locale}, orientation, touchscreen, + density, keyboard, keyboardHidden, navigation, screenWidth, screenHeight, + smallestScreenWidthDp, screenWidthDp, screenHeightDp, screenLayout, uiMode, + colorMode, grammaticalGender, majorVersion); + } else { + setConfiguration(mcc, mnc, null, null, orientation, touchscreen, density, keyboard, keyboardHidden, navigation, screenWidth, screenHeight, smallestScreenWidthDp, screenWidthDp, screenHeightDp, screenLayout, uiMode, colorMode, grammaticalGender, majorVersion); @@ -1490,6 +1494,25 @@ public final class AssetManager implements AutoCloseable { } /** + * Change the configuration used when retrieving resources. Not for use by + * applications. + * @hide + */ + public void setConfiguration(int mcc, int mnc, String defaultLocale, String[] locales, + int orientation, int touchscreen, int density, int keyboard, int keyboardHidden, + int navigation, int screenWidth, int screenHeight, int smallestScreenWidthDp, + int screenWidthDp, int screenHeightDp, int screenLayout, int uiMode, int colorMode, + int grammaticalGender, int majorVersion) { + synchronized (this) { + ensureValidLocked(); + nativeSetConfiguration(mObject, mcc, mnc, defaultLocale, locales, orientation, + touchscreen, density, keyboard, keyboardHidden, navigation, screenWidth, + screenHeight, smallestScreenWidthDp, screenWidthDp, screenHeightDp, + screenLayout, uiMode, colorMode, grammaticalGender, majorVersion); + } + } + + /** * @hide */ @UnsupportedAppUsage @@ -1572,10 +1595,11 @@ public final class AssetManager implements AutoCloseable { private static native void nativeSetApkAssets(long ptr, @NonNull ApkAssets[] apkAssets, boolean invalidateCaches); private static native void nativeSetConfiguration(long ptr, int mcc, int mnc, - @Nullable String locale, int orientation, int touchscreen, int density, int keyboard, - int keyboardHidden, int navigation, int screenWidth, int screenHeight, - int smallestScreenWidthDp, int screenWidthDp, int screenHeightDp, int screenLayout, - int uiMode, int colorMode, int grammaticalGender, int majorVersion); + @Nullable String defaultLocale, @NonNull String[] locales, int orientation, + int touchscreen, int density, int keyboard, int keyboardHidden, int navigation, + int screenWidth, int screenHeight, int smallestScreenWidthDp, int screenWidthDp, + int screenHeightDp, int screenLayout, int uiMode, int colorMode, int grammaticalGender, + int majorVersion); private static native @NonNull SparseArray<String> nativeGetAssignedPackageIdentifiers( long ptr, boolean includeOverlays, boolean includeLoaders); diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index 1c8276cb8276..62630c8909d3 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -154,7 +154,6 @@ public final class Configuration implements Parcelable, Comparable<Configuration /** * Current user preference for the grammatical gender. */ - @GrammaticalGender private int mGrammaticalGender; /** @hide */ @@ -167,6 +166,13 @@ public final class Configuration implements Parcelable, Comparable<Configuration public @interface GrammaticalGender {} /** + * Constant for grammatical gender: to indicate that the grammatical gender is undefined. + * Only for internal usage. + * @hide + */ + public static final int GRAMMATICAL_GENDER_UNDEFINED = -1; + + /** * Constant for grammatical gender: to indicate the user has not specified the terms * of address for the application. */ @@ -1120,12 +1126,12 @@ public final class Configuration implements Parcelable, Comparable<Configuration } else { sb.append(" ?localeList"); } - if (mGrammaticalGender != 0) { + if (mGrammaticalGender > 0) { switch (mGrammaticalGender) { case GRAMMATICAL_GENDER_NEUTRAL: sb.append(" neuter"); break; case GRAMMATICAL_GENDER_FEMININE: sb.append(" feminine"); break; case GRAMMATICAL_GENDER_MASCULINE: sb.append(" masculine"); break; - case GRAMMATICAL_GENDER_NOT_SPECIFIED: sb.append(" ?grgend"); break; + default: sb.append(" ?grgend"); break; } } int layoutDir = (screenLayout&SCREENLAYOUT_LAYOUTDIR_MASK); @@ -1570,7 +1576,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration seq = 0; windowConfiguration.setToDefaults(); fontWeightAdjustment = FONT_WEIGHT_ADJUSTMENT_UNDEFINED; - mGrammaticalGender = GRAMMATICAL_GENDER_NOT_SPECIFIED; + mGrammaticalGender = GRAMMATICAL_GENDER_UNDEFINED; } /** @@ -1773,7 +1779,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration changed |= ActivityInfo.CONFIG_FONT_WEIGHT_ADJUSTMENT; fontWeightAdjustment = delta.fontWeightAdjustment; } - if (delta.mGrammaticalGender != mGrammaticalGender) { + if (delta.mGrammaticalGender != GRAMMATICAL_GENDER_UNDEFINED + && delta.mGrammaticalGender != mGrammaticalGender) { changed |= ActivityInfo.CONFIG_GRAMMATICAL_GENDER; mGrammaticalGender = delta.mGrammaticalGender; } @@ -1998,7 +2005,8 @@ public final class Configuration implements Parcelable, Comparable<Configuration changed |= ActivityInfo.CONFIG_FONT_WEIGHT_ADJUSTMENT; } - if (mGrammaticalGender != delta.mGrammaticalGender) { + if ((compareUndefined || delta.mGrammaticalGender != GRAMMATICAL_GENDER_UNDEFINED) + && mGrammaticalGender != delta.mGrammaticalGender) { changed |= ActivityInfo.CONFIG_GRAMMATICAL_GENDER; } return changed; @@ -2284,6 +2292,17 @@ public final class Configuration implements Parcelable, Comparable<Configuration */ @GrammaticalGender public int getGrammaticalGender() { + return mGrammaticalGender == GRAMMATICAL_GENDER_UNDEFINED + ? GRAMMATICAL_GENDER_NOT_SPECIFIED : mGrammaticalGender; + } + + /** + * Internal getter of grammatical gender, to get the raw value of grammatical gender, + * which include {@link #GRAMMATICAL_GENDER_UNDEFINED}. + * @hide + */ + + public int getGrammaticalGenderRaw() { return mGrammaticalGender; } @@ -2972,7 +2991,7 @@ public final class Configuration implements Parcelable, Comparable<Configuration configOut.fontWeightAdjustment = XmlUtils.readIntAttribute(parser, XML_ATTR_FONT_WEIGHT_ADJUSTMENT, FONT_WEIGHT_ADJUSTMENT_UNDEFINED); configOut.mGrammaticalGender = XmlUtils.readIntAttribute(parser, - XML_ATTR_GRAMMATICAL_GENDER, GRAMMATICAL_GENDER_NOT_SPECIFIED); + XML_ATTR_GRAMMATICAL_GENDER, GRAMMATICAL_GENDER_UNDEFINED); // For persistence, we don't care about assetsSeq and WindowConfiguration, so do not read it // out. diff --git a/core/java/android/content/res/Element.java b/core/java/android/content/res/Element.java index 62a46b6b7152..3e0ab90f99d2 100644 --- a/core/java/android/content/res/Element.java +++ b/core/java/android/content/res/Element.java @@ -16,6 +16,8 @@ package android.content.res; +import static android.os.SystemProperties.PROP_VALUE_MAX; + import android.annotation.NonNull; import android.util.Pools.SimplePool; @@ -23,9 +25,6 @@ import androidx.annotation.StyleableRes; import com.android.internal.R; -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - /** * Defines the string attribute length and child tag count restrictions for a xml element. * @@ -34,8 +33,13 @@ import org.xmlpull.v1.XmlPullParserException; public class Element { private static final int DEFAULT_MAX_STRING_ATTR_LENGTH = 32_768; private static final int MAX_POOL_SIZE = 128; - - private static final String ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android"; + private static final int MAX_ATTR_LEN_URL_COMPONENT = 256; + private static final int MAX_ATTR_LEN_PERMISSION_GROUP = 256; + private static final int MAX_ATTR_LEN_PACKAGE = 256; + private static final int MAX_ATTR_LEN_MIMETYPE = 512; + public static final int MAX_ATTR_LEN_NAME = 1024; + public static final int MAX_ATTR_LEN_PATH = 4000; + public static final int MAX_ATTR_LEN_DATA_VALUE = 4000; protected static final String TAG_ACTION = "action"; protected static final String TAG_ACTIVITY = "activity"; @@ -123,43 +127,9 @@ public class Element { protected static final String TAG_ATTR_VERSION_NAME = "versionName"; protected static final String TAG_ATTR_WRITE_PERMISSION = "writePermission"; - private static final String[] ACTIVITY_STR_ATTR_NAMES = {TAG_ATTR_NAME, - TAG_ATTR_PARENT_ACTIVITY_NAME, TAG_ATTR_PERMISSION, TAG_ATTR_PROCESS, - TAG_ATTR_TASK_AFFINITY}; - private static final String[] ACTIVITY_ALIAS_STR_ATTR_NAMES = {TAG_ATTR_NAME, - TAG_ATTR_PERMISSION, TAG_ATTR_TARGET_ACTIVITY}; - private static final String[] APPLICATION_STR_ATTR_NAMES = {TAG_ATTR_BACKUP_AGENT, - TAG_ATTR_MANAGE_SPACE_ACTIVITY, TAG_ATTR_NAME, TAG_ATTR_PERMISSION, TAG_ATTR_PROCESS, - TAG_ATTR_REQUIRED_ACCOUNT_TYPE, TAG_ATTR_RESTRICTED_ACCOUNT_TYPE, - TAG_ATTR_TASK_AFFINITY}; - private static final String[] DATA_STR_ATTR_NAMES = {TAG_ATTR_SCHEME, TAG_ATTR_HOST, - TAG_ATTR_PORT, TAG_ATTR_PATH, TAG_ATTR_PATH_PATTERN, TAG_ATTR_PATH_PREFIX, - TAG_ATTR_PATH_SUFFIX, TAG_ATTR_PATH_ADVANCED_PATTERN, TAG_ATTR_MIMETYPE}; - private static final String[] GRANT_URI_PERMISSION_STR_ATTR_NAMES = {TAG_ATTR_PATH, - TAG_ATTR_PATH_PATTERN, TAG_ATTR_PATH_PREFIX}; - private static final String[] INSTRUMENTATION_STR_ATTR_NAMES = {TAG_ATTR_NAME, - TAG_ATTR_TARGET_PACKAGE, TAG_ATTR_TARGET_PROCESSES}; - private static final String[] MANIFEST_STR_ATTR_NAMES = {TAG_ATTR_PACKAGE, - TAG_ATTR_SHARED_USER_ID, TAG_ATTR_VERSION_NAME}; - private static final String[] OVERLAY_STR_ATTR_NAMES = {TAG_ATTR_CATEGORY, - TAG_ATTR_REQUIRED_SYSTEM_PROPERTY_NAME, TAG_ATTR_REQUIRED_SYSTEM_PROPERTY_VALUE, - TAG_ATTR_TARGET_PACKAGE, TAG_ATTR_TARGET_NAME}; - private static final String[] PATH_PERMISSION_STR_ATTR_NAMES = {TAG_ATTR_PATH, - TAG_ATTR_PATH_PREFIX, TAG_ATTR_PATH_PATTERN, TAG_ATTR_PERMISSION, - TAG_ATTR_READ_PERMISSION, TAG_ATTR_WRITE_PERMISSION}; - private static final String[] PERMISSION_STR_ATTR_NAMES = {TAG_ATTR_NAME, - TAG_ATTR_PERMISSION_GROUP}; - private static final String[] PROVIDER_STR_ATTR_NAMES = {TAG_ATTR_NAME, TAG_ATTR_PERMISSION, - TAG_ATTR_PROCESS, TAG_ATTR_READ_PERMISSION, TAG_ATTR_WRITE_PERMISSION}; - private static final String[] RECEIVER_SERVICE_STR_ATTR_NAMES = {TAG_ATTR_NAME, - TAG_ATTR_PERMISSION, TAG_ATTR_PROCESS}; - private static final String[] NAME_ATTR = {TAG_ATTR_NAME}; - private static final String[] NAME_VALUE_ATTRS = {TAG_ATTR_NAME, TAG_ATTR_VALUE}; - - private String[] mStringAttrNames = new String[0]; // The length of mTagCounters corresponds to the number of tags defined in getCounterIdx. If new // tags are added then the size here should be increased to match. - private final TagCounter[] mTagCounters = new TagCounter[35]; + private final TagCounter[] mTagCounters = new TagCounter[34]; String mTag; @@ -177,7 +147,6 @@ public class Element { } void recycle() { - mStringAttrNames = new String[0]; mTag = null; sPool.get().release(this); } @@ -230,89 +199,111 @@ public class Element { return 20; case TAG_USES_CONFIGURATION: return 21; - case TAG_USES_PERMISSION_SDK_23: - return 22; case TAG_USES_SDK: - return 23; + return 22; case TAG_COMPATIBLE_SCREENS: - return 24; + return 23; case TAG_QUERIES: - return 25; + return 24; case TAG_ATTRIBUTION: - return 26; + return 25; case TAG_USES_FEATURE: - return 27; + return 26; case TAG_PERMISSION: - return 28; + return 27; case TAG_USES_PERMISSION: - return 29; + case TAG_USES_PERMISSION_SDK_23: + case TAG_USES_PERMISSION_SDK_M: + return 28; case TAG_GRANT_URI_PERMISSION: - return 30; + return 29; case TAG_PATH_PERMISSION: - return 31; + return 30; case TAG_PACKAGE: - return 32; + return 31; case TAG_INTENT: - return 33; + return 32; default: // The size of the mTagCounters array should be equal to this value+1 - return 34; + return 33; } } - private void init(String tag) { - this.mTag = tag; - mChildTagMask = 0; + static boolean shouldValidate(String tag) { switch (tag) { case TAG_ACTION: + case TAG_ACTIVITY: + case TAG_ACTIVITY_ALIAS: + case TAG_APPLICATION: + case TAG_ATTRIBUTION: case TAG_CATEGORY: + case TAG_COMPATIBLE_SCREENS: + case TAG_DATA: + case TAG_GRANT_URI_PERMISSION: + case TAG_INSTRUMENTATION: + case TAG_INTENT: + case TAG_INTENT_FILTER: + case TAG_LAYOUT: + case TAG_MANIFEST: + case TAG_META_DATA: + case TAG_OVERLAY: case TAG_PACKAGE: + case TAG_PATH_PERMISSION: + case TAG_PERMISSION: case TAG_PERMISSION_GROUP: case TAG_PERMISSION_TREE: + case TAG_PROFILEABLE: + case TAG_PROPERTY: + case TAG_PROVIDER: + case TAG_QUERIES: + case TAG_RECEIVER: + case TAG_SCREEN: + case TAG_SERVICE: case TAG_SUPPORTS_GL_TEXTURE: + case TAG_SUPPORTS_SCREENS: + case TAG_USES_CONFIGURATION: case TAG_USES_FEATURE: case TAG_USES_LIBRARY: case TAG_USES_NATIVE_LIBRARY: case TAG_USES_PERMISSION: case TAG_USES_PERMISSION_SDK_23: + case TAG_USES_PERMISSION_SDK_M: case TAG_USES_SDK: - setStringAttrNames(NAME_ATTR); - break; + return true; + default: + return false; + } + } + + private void init(String tag) { + this.mTag = tag; + mChildTagMask = 0; + switch (tag) { case TAG_ACTIVITY: - setStringAttrNames(ACTIVITY_STR_ATTR_NAMES); initializeCounter(TAG_LAYOUT, 1000); - initializeCounter(TAG_META_DATA, 8000); + initializeCounter(TAG_META_DATA, 1000); initializeCounter(TAG_INTENT_FILTER, 20000); break; case TAG_ACTIVITY_ALIAS: - setStringAttrNames(ACTIVITY_ALIAS_STR_ATTR_NAMES); - initializeCounter(TAG_META_DATA, 8000); + case TAG_RECEIVER: + case TAG_SERVICE: + initializeCounter(TAG_META_DATA, 1000); initializeCounter(TAG_INTENT_FILTER, 20000); break; case TAG_APPLICATION: - setStringAttrNames(APPLICATION_STR_ATTR_NAMES); initializeCounter(TAG_PROFILEABLE, 100); initializeCounter(TAG_USES_NATIVE_LIBRARY, 100); initializeCounter(TAG_RECEIVER, 1000); initializeCounter(TAG_SERVICE, 1000); + initializeCounter(TAG_META_DATA, 1000); + initializeCounter(TAG_USES_LIBRARY, 1000); initializeCounter(TAG_ACTIVITY_ALIAS, 4000); - initializeCounter(TAG_USES_LIBRARY, 4000); initializeCounter(TAG_PROVIDER, 8000); - initializeCounter(TAG_META_DATA, 8000); initializeCounter(TAG_ACTIVITY, 40000); break; case TAG_COMPATIBLE_SCREENS: initializeCounter(TAG_SCREEN, 4000); break; - case TAG_DATA: - setStringAttrNames(DATA_STR_ATTR_NAMES); - break; - case TAG_GRANT_URI_PERMISSION: - setStringAttrNames(GRANT_URI_PERMISSION_STR_ATTR_NAMES); - break; - case TAG_INSTRUMENTATION: - setStringAttrNames(INSTRUMENTATION_STR_ATTR_NAMES); - break; case TAG_INTENT: case TAG_INTENT_FILTER: initializeCounter(TAG_ACTION, 20000); @@ -320,7 +311,6 @@ public class Element { initializeCounter(TAG_DATA, 40000); break; case TAG_MANIFEST: - setStringAttrNames(MANIFEST_STR_ATTR_NAMES); initializeCounter(TAG_APPLICATION, 100); initializeCounter(TAG_OVERLAY, 100); initializeCounter(TAG_INSTRUMENTATION, 100); @@ -329,7 +319,6 @@ public class Element { initializeCounter(TAG_SUPPORTS_GL_TEXTURE, 100); initializeCounter(TAG_SUPPORTS_SCREENS, 100); initializeCounter(TAG_USES_CONFIGURATION, 100); - initializeCounter(TAG_USES_PERMISSION_SDK_23, 100); initializeCounter(TAG_USES_SDK, 100); initializeCounter(TAG_COMPATIBLE_SCREENS, 200); initializeCounter(TAG_QUERIES, 200); @@ -338,24 +327,10 @@ public class Element { initializeCounter(TAG_PERMISSION, 2000); initializeCounter(TAG_USES_PERMISSION, 20000); break; - case TAG_META_DATA: - case TAG_PROPERTY: - setStringAttrNames(NAME_VALUE_ATTRS); - break; - case TAG_OVERLAY: - setStringAttrNames(OVERLAY_STR_ATTR_NAMES); - break; - case TAG_PATH_PERMISSION: - setStringAttrNames(PATH_PERMISSION_STR_ATTR_NAMES); - break; - case TAG_PERMISSION: - setStringAttrNames(PERMISSION_STR_ATTR_NAMES); - break; case TAG_PROVIDER: - setStringAttrNames(PROVIDER_STR_ATTR_NAMES); initializeCounter(TAG_GRANT_URI_PERMISSION, 100); initializeCounter(TAG_PATH_PERMISSION, 100); - initializeCounter(TAG_META_DATA, 8000); + initializeCounter(TAG_META_DATA, 1000); initializeCounter(TAG_INTENT_FILTER, 20000); break; case TAG_QUERIES: @@ -363,39 +338,23 @@ public class Element { initializeCounter(TAG_INTENT, 2000); initializeCounter(TAG_PROVIDER, 8000); break; - case TAG_RECEIVER: - case TAG_SERVICE: - setStringAttrNames(RECEIVER_SERVICE_STR_ATTR_NAMES); - initializeCounter(TAG_META_DATA, 8000); - initializeCounter(TAG_INTENT_FILTER, 20000); - break; - } - } - - private void setStringAttrNames(String[] attrNames) { - mStringAttrNames = attrNames; - } - - private static String getAttrNamespace(String attrName) { - if (attrName.equals(TAG_ATTR_PACKAGE)) { - return null; } - return ANDROID_NAMESPACE; } - private static int getAttrStringMaxLength(String attrName) { + private static int getAttrStrMaxLen(String attrName) { switch (attrName) { case TAG_ATTR_HOST: - case TAG_ATTR_PACKAGE: - case TAG_ATTR_PERMISSION_GROUP: case TAG_ATTR_PORT: - case TAG_ATTR_REQUIRED_SYSTEM_PROPERTY_VALUE: case TAG_ATTR_SCHEME: + return MAX_ATTR_LEN_URL_COMPONENT; + case TAG_ATTR_PERMISSION_GROUP: + return MAX_ATTR_LEN_PERMISSION_GROUP; case TAG_ATTR_SHARED_USER_ID: + case TAG_ATTR_PACKAGE: case TAG_ATTR_TARGET_PACKAGE: - return 256; + return MAX_ATTR_LEN_PACKAGE; case TAG_ATTR_MIMETYPE: - return 512; + return MAX_ATTR_LEN_MIMETYPE; case TAG_ATTR_BACKUP_AGENT: case TAG_ATTR_CATEGORY: case TAG_ATTR_MANAGE_SPACE_ACTIVITY: @@ -405,33 +364,343 @@ public class Element { case TAG_ATTR_PROCESS: case TAG_ATTR_READ_PERMISSION: case TAG_ATTR_REQUIRED_ACCOUNT_TYPE: + case TAG_ATTR_REQUIRED_SYSTEM_PROPERTY_NAME: case TAG_ATTR_RESTRICTED_ACCOUNT_TYPE: case TAG_ATTR_TARGET_ACTIVITY: case TAG_ATTR_TARGET_NAME: case TAG_ATTR_TARGET_PROCESSES: case TAG_ATTR_TASK_AFFINITY: case TAG_ATTR_WRITE_PERMISSION: - return 1024; + case TAG_ATTR_VERSION_NAME: + return MAX_ATTR_LEN_NAME; case TAG_ATTR_PATH: case TAG_ATTR_PATH_ADVANCED_PATTERN: case TAG_ATTR_PATH_PATTERN: case TAG_ATTR_PATH_PREFIX: case TAG_ATTR_PATH_SUFFIX: - case TAG_ATTR_VERSION_NAME: - return 4000; + return MAX_ATTR_LEN_PATH; + case TAG_ATTR_VALUE: + return MAX_ATTR_LEN_DATA_VALUE; + case TAG_ATTR_REQUIRED_SYSTEM_PROPERTY_VALUE: + return PROP_VALUE_MAX; default: return DEFAULT_MAX_STRING_ATTR_LENGTH; } } - private static int getResStringMaxLength(@StyleableRes int index) { + private int getResStrMaxLen(@StyleableRes int index) { + switch (mTag) { + case TAG_ACTION: + return getActionResStrMaxLen(index); + case TAG_ACTIVITY: + return getActivityResStrMaxLen(index); + case TAG_ACTIVITY_ALIAS: + return getActivityAliasResStrMaxLen(index); + case TAG_APPLICATION: + return getApplicationResStrMaxLen(index); + case TAG_DATA: + return getDataResStrMaxLen(index); + case TAG_CATEGORY: + return getCategoryResStrMaxLen(index); + case TAG_GRANT_URI_PERMISSION: + return getGrantUriPermissionResStrMaxLen(index); + case TAG_INSTRUMENTATION: + return getInstrumentationResStrMaxLen(index); + case TAG_MANIFEST: + return getManifestResStrMaxLen(index); + case TAG_META_DATA: + return getMetaDataResStrMaxLen(index); + case TAG_OVERLAY: + return getOverlayResStrMaxLen(index); + case TAG_PATH_PERMISSION: + return getPathPermissionResStrMaxLen(index); + case TAG_PERMISSION: + return getPermissionResStrMaxLen(index); + case TAG_PERMISSION_GROUP: + return getPermissionGroupResStrMaxLen(index); + case TAG_PERMISSION_TREE: + return getPermissionTreeResStrMaxLen(index); + case TAG_PROPERTY: + return getPropertyResStrMaxLen(index); + case TAG_PROVIDER: + return getProviderResStrMaxLen(index); + case TAG_RECEIVER: + return getReceiverResStrMaxLen(index); + case TAG_SERVICE: + return getServiceResStrMaxLen(index); + case TAG_USES_FEATURE: + return getUsesFeatureResStrMaxLen(index); + case TAG_USES_LIBRARY: + return getUsesLibraryResStrMaxLen(index); + case TAG_USES_NATIVE_LIBRARY: + return getUsesNativeLibraryResStrMaxLen(index); + case TAG_USES_PERMISSION: + case TAG_USES_PERMISSION_SDK_23: + case TAG_USES_PERMISSION_SDK_M: + return getUsesPermissionResStrMaxLen(index); + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getActionResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestAction_name: + return MAX_ATTR_LEN_NAME; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getActivityResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestActivity_name: + case R.styleable.AndroidManifestActivity_parentActivityName: + case R.styleable.AndroidManifestActivity_permission: + case R.styleable.AndroidManifestActivity_process: + case R.styleable.AndroidManifestActivity_taskAffinity: + return MAX_ATTR_LEN_NAME; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getActivityAliasResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestActivityAlias_name: + case R.styleable.AndroidManifestActivityAlias_permission: + case R.styleable.AndroidManifestActivityAlias_targetActivity: + return MAX_ATTR_LEN_NAME; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getApplicationResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestApplication_backupAgent: + case R.styleable.AndroidManifestApplication_manageSpaceActivity: + case R.styleable.AndroidManifestApplication_name: + case R.styleable.AndroidManifestApplication_permission: + case R.styleable.AndroidManifestApplication_process: + case R.styleable.AndroidManifestApplication_requiredAccountType: + case R.styleable.AndroidManifestApplication_restrictedAccountType: + case R.styleable.AndroidManifestApplication_taskAffinity: + return MAX_ATTR_LEN_NAME; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getCategoryResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestCategory_name: + return MAX_ATTR_LEN_NAME; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getDataResStrMaxLen(@StyleableRes int index) { switch (index) { case R.styleable.AndroidManifestData_host: case R.styleable.AndroidManifestData_port: case R.styleable.AndroidManifestData_scheme: - return 255; + return MAX_ATTR_LEN_URL_COMPONENT; case R.styleable.AndroidManifestData_mimeType: - return 512; + return MAX_ATTR_LEN_MIMETYPE; + case R.styleable.AndroidManifestData_path: + case R.styleable.AndroidManifestData_pathPattern: + case R.styleable.AndroidManifestData_pathPrefix: + case R.styleable.AndroidManifestData_pathSuffix: + case R.styleable.AndroidManifestData_pathAdvancedPattern: + return MAX_ATTR_LEN_PATH; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getGrantUriPermissionResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestGrantUriPermission_path: + case R.styleable.AndroidManifestGrantUriPermission_pathPattern: + case R.styleable.AndroidManifestGrantUriPermission_pathPrefix: + return MAX_ATTR_LEN_PATH; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getInstrumentationResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestInstrumentation_targetPackage: + return MAX_ATTR_LEN_PACKAGE; + case R.styleable.AndroidManifestInstrumentation_name: + case R.styleable.AndroidManifestInstrumentation_targetProcesses: + return MAX_ATTR_LEN_NAME; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getManifestResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifest_sharedUserId: + return MAX_ATTR_LEN_PACKAGE; + case R.styleable.AndroidManifest_versionName: + return MAX_ATTR_LEN_NAME; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getMetaDataResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestMetaData_name: + return MAX_ATTR_LEN_NAME; + case R.styleable.AndroidManifestMetaData_value: + return MAX_ATTR_LEN_DATA_VALUE; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getOverlayResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestResourceOverlay_targetPackage: + return MAX_ATTR_LEN_PACKAGE; + case R.styleable.AndroidManifestResourceOverlay_category: + case R.styleable.AndroidManifestResourceOverlay_requiredSystemPropertyName: + case R.styleable.AndroidManifestResourceOverlay_targetName: + return MAX_ATTR_LEN_NAME; + case R.styleable.AndroidManifestResourceOverlay_requiredSystemPropertyValue: + return PROP_VALUE_MAX; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getPathPermissionResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestPathPermission_permission: + case R.styleable.AndroidManifestPathPermission_readPermission: + case R.styleable.AndroidManifestPathPermission_writePermission: + return MAX_ATTR_LEN_NAME; + case R.styleable.AndroidManifestPathPermission_path: + case R.styleable.AndroidManifestPathPermission_pathPattern: + case R.styleable.AndroidManifestPathPermission_pathPrefix: + return MAX_ATTR_LEN_PATH; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getPermissionResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestPermission_permissionGroup: + return MAX_ATTR_LEN_PERMISSION_GROUP; + case R.styleable.AndroidManifestPermission_name: + return MAX_ATTR_LEN_NAME; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getPermissionGroupResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestPermissionGroup_name: + return MAX_ATTR_LEN_NAME; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getPermissionTreeResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestPermissionTree_name: + return MAX_ATTR_LEN_NAME; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getPropertyResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestProperty_name: + return MAX_ATTR_LEN_NAME; + case R.styleable.AndroidManifestProperty_value: + return MAX_ATTR_LEN_DATA_VALUE; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getProviderResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestProvider_name: + case R.styleable.AndroidManifestProvider_permission: + case R.styleable.AndroidManifestProvider_process: + case R.styleable.AndroidManifestProvider_readPermission: + case R.styleable.AndroidManifestProvider_writePermission: + return MAX_ATTR_LEN_NAME; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getReceiverResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestReceiver_name: + case R.styleable.AndroidManifestReceiver_permission: + case R.styleable.AndroidManifestReceiver_process: + return MAX_ATTR_LEN_NAME; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getServiceResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestReceiver_name: + case R.styleable.AndroidManifestReceiver_permission: + case R.styleable.AndroidManifestReceiver_process: + return MAX_ATTR_LEN_NAME; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getUsesFeatureResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestUsesFeature_name: + return MAX_ATTR_LEN_NAME; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getUsesLibraryResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestUsesLibrary_name: + return MAX_ATTR_LEN_NAME; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getUsesNativeLibraryResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestUsesNativeLibrary_name: + return MAX_ATTR_LEN_NAME; + default: + return DEFAULT_MAX_STRING_ATTR_LENGTH; + } + } + + private static int getUsesPermissionResStrMaxLen(@StyleableRes int index) { + switch (index) { + case R.styleable.AndroidManifestUsesPermission_name: + return MAX_ATTR_LEN_NAME; default: return DEFAULT_MAX_STRING_ATTR_LENGTH; } @@ -450,31 +719,25 @@ public class Element { return (mChildTagMask & (1 << getCounterIdx(tag))) != 0; } - void validateStringAttrs(@NonNull XmlPullParser attrs) throws XmlPullParserException { - for (int i = 0; i < mStringAttrNames.length; i++) { - String attrName = mStringAttrNames[i]; - String val = attrs.getAttributeValue(getAttrNamespace(attrName), attrName); - if (val != null && val.length() > getAttrStringMaxLength(attrName)) { - throw new XmlPullParserException("String length limit exceeded for " - + "attribute " + attrName + " in " + mTag); - } + void validateStrAttr(String attrName, String attrValue) { + if (attrValue != null && attrValue.length() > getAttrStrMaxLen(attrName)) { + throw new SecurityException("String length limit exceeded for attribute " + attrName + + " in " + mTag); } } - void validateResStringAttr(@StyleableRes int index, CharSequence stringValue) - throws XmlPullParserException { - if (stringValue != null && stringValue.length() > getResStringMaxLength(index)) { - throw new XmlPullParserException("String length limit exceeded for " - + "attribute in " + mTag); + void validateResStrAttr(@StyleableRes int index, CharSequence stringValue) { + if (stringValue != null && stringValue.length() > getResStrMaxLen(index)) { + throw new SecurityException("String length limit exceeded for attribute in " + mTag); } } - void seen(@NonNull Element element) throws XmlPullParserException { + void seen(@NonNull Element element) { TagCounter counter = mTagCounters[getCounterIdx(element.mTag)]; if (counter != null) { counter.increment(); if (!counter.isValid()) { - throw new XmlPullParserException("The number of child " + element.mTag + throw new SecurityException("The number of child " + element.mTag + " elements exceeded the max allowed in " + this.mTag); } } diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java index 61d5aa600195..f2468b5f0653 100644 --- a/core/java/android/content/res/ResourcesImpl.java +++ b/core/java/android/content/res/ResourcesImpl.java @@ -407,14 +407,12 @@ public class ResourcesImpl { mConfiguration.setLocales(locales); } + String[] selectedLocales = null; + String defaultLocale = null; if ((configChanges & ActivityInfo.CONFIG_LOCALE) != 0) { if (locales.size() > 1) { String[] availableLocales; - - LocaleList localeList = ResourcesManager.getInstance().getLocaleList(); - if (!localeList.isEmpty()) { - availableLocales = localeList.toLanguageTags().split(","); - } else { + if (ResourcesManager.getInstance().getLocaleList().isEmpty()) { // The LocaleList has changed. We must query the AssetManager's // available Locales and figure out the best matching Locale in the new // LocaleList. @@ -426,16 +424,32 @@ public class ResourcesImpl { availableLocales = null; } } - } - if (availableLocales != null) { - final Locale bestLocale = locales.getFirstMatchWithEnglishSupported( - availableLocales); - if (bestLocale != null && bestLocale != locales.get(0)) { - mConfiguration.setLocales(new LocaleList(bestLocale, locales)); + if (availableLocales != null) { + final Locale bestLocale = locales.getFirstMatchWithEnglishSupported( + availableLocales); + if (bestLocale != null) { + selectedLocales = new String[]{ + adjustLanguageTag(bestLocale.toLanguageTag())}; + if (!bestLocale.equals(locales.get(0))) { + mConfiguration.setLocales( + new LocaleList(bestLocale, locales)); + } + } } + } else { + selectedLocales = locales.getIntersection( + ResourcesManager.getInstance().getLocaleList()); + defaultLocale = ResourcesManager.getInstance() + .getLocaleList().get(0).toLanguageTag(); } } } + if (selectedLocales == null) { + selectedLocales = new String[locales.size()]; + for (int i = 0; i < locales.size(); i++) { + selectedLocales[i] = adjustLanguageTag(locales.get(i).toLanguageTag()); + } + } if (mConfiguration.densityDpi != Configuration.DENSITY_DPI_UNDEFINED) { mMetrics.densityDpi = mConfiguration.densityDpi; @@ -470,7 +484,8 @@ public class ResourcesImpl { } mAssets.setConfiguration(mConfiguration.mcc, mConfiguration.mnc, - adjustLanguageTag(mConfiguration.getLocales().get(0).toLanguageTag()), + defaultLocale, + selectedLocales, mConfiguration.orientation, mConfiguration.touchscreen, mConfiguration.densityDpi, mConfiguration.keyboard, diff --git a/core/java/android/content/res/TypedArray.java b/core/java/android/content/res/TypedArray.java index 2e84636202be..48adfb907ab4 100644 --- a/core/java/android/content/res/TypedArray.java +++ b/core/java/android/content/res/TypedArray.java @@ -1393,16 +1393,17 @@ public class TypedArray implements AutoCloseable { private CharSequence loadStringValueAt(int index) { final int[] data = mData; final int cookie = data[index + STYLE_ASSET_COOKIE]; + CharSequence value = null; if (cookie < 0) { if (mXml != null) { - return mXml.getPooledString(data[index + STYLE_DATA]); + value = mXml.getPooledString(data[index + STYLE_DATA]); } - return null; + } else { + value = mAssets.getPooledStringForCookie(cookie, data[index + STYLE_DATA]); } - CharSequence value = mAssets.getPooledStringForCookie(cookie, data[index + STYLE_DATA]); - if (mXml != null && mXml.mValidator != null) { + if (value != null && mXml != null && mXml.mValidator != null) { try { - mXml.mValidator.validateAttr(mXml, index, value); + mXml.mValidator.validateResStrAttr(mXml, index / STYLE_NUM_ENTRIES, value); } catch (XmlPullParserException e) { throw new RuntimeException("Failed to validate resource string: " + e.getMessage()); } diff --git a/core/java/android/content/res/Validator.java b/core/java/android/content/res/Validator.java index 8b5e6c61d304..cae353b3bd5a 100644 --- a/core/java/android/content/res/Validator.java +++ b/core/java/android/content/res/Validator.java @@ -16,9 +16,8 @@ package android.content.res; -import static android.content.res.Element.TAG_MANIFEST; - import android.annotation.NonNull; +import android.annotation.StyleableRes; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -55,24 +54,19 @@ public class Validator { return; } if (eventType == XmlPullParser.START_TAG) { - try { - String tag = parser.getName(); - // only validate manifests - if (depth == 0 && mElements.size() == 0 && !TAG_MANIFEST.equals(tag)) { - return; - } + String tag = parser.getName(); + if (Element.shouldValidate(tag)) { + Element element = Element.obtain(tag); Element parent = mElements.peek(); - if (parent == null || parent.hasChild(tag)) { - Element element = Element.obtain(tag); - element.validateStringAttrs(parser); - if (parent != null) { + if (parent != null && parent.hasChild(tag)) { + try { parent.seen(element); + } catch (SecurityException e) { + cleanUp(); + throw e; } - mElements.push(element); } - } catch (XmlPullParserException e) { - cleanUp(); - throw e; + mElements.push(element); } } else if (eventType == XmlPullParser.END_TAG && depth == mElements.size()) { mElements.pop().recycle(); @@ -84,11 +78,21 @@ public class Validator { /** * Validates the resource string of a manifest tag attribute. */ - public void validateAttr(@NonNull XmlPullParser parser, int index, CharSequence stringValue) - throws XmlPullParserException { + public void validateResStrAttr(@NonNull XmlPullParser parser, @StyleableRes int index, + CharSequence stringValue) throws XmlPullParserException { + if (parser.getDepth() > mElements.size()) { + return; + } + mElements.peek().validateResStrAttr(index, stringValue); + } + + /** + * Validates the string of a manifest tag attribute by name. + */ + public void validateStrAttr(@NonNull XmlPullParser parser, String attrName, String attrValue) { if (parser.getDepth() > mElements.size()) { return; } - mElements.peek().validateResStringAttr(index, stringValue); + mElements.peek().validateStrAttr(attrName, attrValue); } } diff --git a/core/java/android/content/res/XmlBlock.java b/core/java/android/content/res/XmlBlock.java index 3afc830fa14d..7649b32a6c7a 100644 --- a/core/java/android/content/res/XmlBlock.java +++ b/core/java/android/content/res/XmlBlock.java @@ -319,7 +319,11 @@ public final class XmlBlock implements AutoCloseable { "Namespace=" + getAttributeNamespace(idx) + "Name=" + getAttributeName(idx) + ", Value=" + getAttributeValue(idx)); - return getAttributeValue(idx); + String value = getAttributeValue(idx); + if (mValidator != null) { + mValidator.validateStrAttr(this, name, value); + } + return value; } return null; } diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java index b7489229a424..706e75e4c64c 100644 --- a/core/java/android/database/sqlite/SQLiteConnection.java +++ b/core/java/android/database/sqlite/SQLiteConnection.java @@ -113,9 +113,6 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen private final boolean mIsReadOnlyConnection; private PreparedStatement mPreparedStatementPool; - // A lock access to the statement cache. - private final Object mCacheLock = new Object(); - @GuardedBy("mCacheLock") private final PreparedStatementCache mPreparedStatementCache; // The recent operations log. @@ -596,9 +593,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen mConfiguration.updateParametersFrom(configuration); // Update prepared statement cache size. - synchronized (mCacheLock) { - mPreparedStatementCache.resize(configuration.maxSqlCacheSize); - } + mPreparedStatementCache.resize(configuration.maxSqlCacheSize); if (foreignKeyModeChanged) { setForeignKeyModeFromConfiguration(); @@ -630,12 +625,12 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen mOnlyAllowReadOnlyOperations = readOnly; } - // Called by SQLiteConnectionPool only. - // Returns true if the prepared statement cache contains the specified SQL. + // Called by SQLiteConnectionPool only to decide if this connection has the desired statement + // already prepared. Returns true if the prepared statement cache contains the specified SQL. + // The statement may be stale, but that will be a rare occurrence and affects performance only + // a tiny bit, and only when database schema changes. boolean isPreparedStatementInCache(String sql) { - synchronized (mCacheLock) { - return mPreparedStatementCache.get(sql) != null; - } + return mPreparedStatementCache.get(sql) != null; } /** @@ -1070,28 +1065,41 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen /** * Return a {@link #PreparedStatement}, possibly from the cache. */ - @GuardedBy("mCacheLock") private PreparedStatement acquirePreparedStatementLI(String sql) { ++mPool.mTotalPrepareStatements; - PreparedStatement statement = mPreparedStatementCache.get(sql); + PreparedStatement statement = mPreparedStatementCache.getStatement(sql); + long seqNum = mPreparedStatementCache.getLastSeqNum(); + boolean skipCache = false; if (statement != null) { if (!statement.mInUse) { - statement.mInUse = true; - return statement; + if (statement.mSeqNum == seqNum) { + // This is a valid statement. Claim it and return it. + statement.mInUse = true; + return statement; + } else { + // This is a stale statement. Remove it from the cache. Treat this as if the + // statement was never found, which means we should not skip the cache. + mPreparedStatementCache.remove(sql); + statement = null; + // Leave skipCache == false. + } + } else { + // The statement is already in the cache but is in use (this statement appears to + // be not only re-entrant but recursive!). So prepare a new copy of the statement + // but do not cache it. + skipCache = true; } - // The statement is already in the cache but is in use (this statement appears - // to be not only re-entrant but recursive!). So prepare a new copy of the - // statement but do not cache it. - skipCache = true; } ++mPool.mTotalPrepareStatementCacheMiss; - final long statementPtr = nativePrepareStatement(mConnectionPtr, sql); + final long statementPtr = mPreparedStatementCache.createStatement(sql); + seqNum = mPreparedStatementCache.getLastSeqNum(); try { final int numParameters = nativeGetParameterCount(mConnectionPtr, statementPtr); final int type = DatabaseUtils.getSqlStatementType(sql); final boolean readOnly = nativeIsReadOnly(mConnectionPtr, statementPtr); - statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly); + statement = obtainPreparedStatement(sql, statementPtr, numParameters, type, readOnly, + seqNum); if (!skipCache && isCacheable(type)) { mPreparedStatementCache.put(sql, statement); statement.mInCache = true; @@ -1112,15 +1120,12 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen * Return a {@link #PreparedStatement}, possibly from the cache. */ PreparedStatement acquirePreparedStatement(String sql) { - synchronized (mCacheLock) { - return acquirePreparedStatementLI(sql); - } + return acquirePreparedStatementLI(sql); } /** * Release a {@link #PreparedStatement} that was originally supplied by this connection. */ - @GuardedBy("mCacheLock") private void releasePreparedStatementLI(PreparedStatement statement) { statement.mInUse = false; if (statement.mInCache) { @@ -1148,9 +1153,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen * Release a {@link #PreparedStatement} that was originally supplied by this connection. */ void releasePreparedStatement(PreparedStatement statement) { - synchronized (mCacheLock) { - releasePreparedStatementLI(statement); - } + releasePreparedStatementLI(statement); } private void finalizePreparedStatement(PreparedStatement statement) { @@ -1327,9 +1330,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen mRecentOperations.dump(printer); if (verbose) { - synchronized (mCacheLock) { - mPreparedStatementCache.dump(printer); - } + mPreparedStatementCache.dump(printer); } } @@ -1430,7 +1431,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen } private PreparedStatement obtainPreparedStatement(String sql, long statementPtr, - int numParameters, int type, boolean readOnly) { + int numParameters, int type, boolean readOnly, long seqNum) { PreparedStatement statement = mPreparedStatementPool; if (statement != null) { mPreparedStatementPool = statement.mPoolNext; @@ -1444,6 +1445,7 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen statement.mNumParameters = numParameters; statement.mType = type; statement.mReadOnly = readOnly; + statement.mSeqNum = seqNum; return statement; } @@ -1461,10 +1463,10 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen return sql.replaceAll("[\\s]*\\n+[\\s]*", " "); } - void clearPreparedStatementCache() { - synchronized (mCacheLock) { - mPreparedStatementCache.evictAll(); - } + // Update the database sequence number. This number is stored in the prepared statement + // cache. + void setDatabaseSeqNum(long n) { + mPreparedStatementCache.setDatabaseSeqNum(n); } /** @@ -1502,6 +1504,10 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen // True if the statement is in the cache. public boolean mInCache; + // The database schema ID at the time this statement was created. The ID is left zero for + // statements that are not cached. This value is meaningful only if mInCache is true. + public long mSeqNum; + // True if the statement is in use (currently executing). // We need this flag because due to the use of custom functions in triggers, it's // possible for SQLite calls to be re-entrant. Consequently we need to prevent @@ -1510,10 +1516,41 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen } private final class PreparedStatementCache extends LruCache<String, PreparedStatement> { + // The database sequence number. This changes every time the database schema changes. + private long mDatabaseSeqNum = 0; + + // The database sequence number from the last getStatement() or createStatement() + // call. The proper use of this variable depends on the caller being single threaded. + private long mLastSeqNum = 0; + public PreparedStatementCache(int size) { super(size); } + public synchronized void setDatabaseSeqNum(long n) { + mDatabaseSeqNum = n; + } + + // Return the last database sequence number. + public long getLastSeqNum() { + return mLastSeqNum; + } + + // Return a statement from the cache. Save the database sequence number for the caller. + public synchronized PreparedStatement getStatement(String sql) { + mLastSeqNum = mDatabaseSeqNum; + return get(sql); + } + + // Return a new native prepared statement and save the database sequence number for the + // caller. This does not modify the cache in any way. However, by being synchronized, + // callers are guaranteed that the sequence number did not change across the native + // preparation step. + public synchronized long createStatement(String sql) { + mLastSeqNum = mDatabaseSeqNum; + return nativePrepareStatement(mConnectionPtr, sql); + } + @Override protected void entryRemoved(boolean evicted, String key, PreparedStatement oldValue, PreparedStatement newValue) { diff --git a/core/java/android/database/sqlite/SQLiteConnectionPool.java b/core/java/android/database/sqlite/SQLiteConnectionPool.java index b35a2e4eb1b5..ad335b62f05e 100644 --- a/core/java/android/database/sqlite/SQLiteConnectionPool.java +++ b/core/java/android/database/sqlite/SQLiteConnectionPool.java @@ -111,6 +111,13 @@ public final class SQLiteConnectionPool implements Closeable { @GuardedBy("mLock") private IdleConnectionHandler mIdleConnectionHandler; + // The database schema sequence number. This counter is incremented every time a schema + // change is detected. Every prepared statement records its schema sequence when the + // statement is created. The prepared statement is not put back in the cache if the sequence + // number has changed. The counter starts at 1, which allows clients to use 0 as a + // distinguished value. + private long mDatabaseSeqNum = 1; + // whole execution time for this connection in milliseconds. private final AtomicLong mTotalStatementsTime = new AtomicLong(0); @@ -1127,10 +1134,12 @@ public final class SQLiteConnectionPool implements Closeable { } void clearAcquiredConnectionsPreparedStatementCache() { + // Invalidate prepared statements that have an earlier schema sequence number. synchronized (mLock) { + mDatabaseSeqNum++; if (!mAcquiredConnections.isEmpty()) { for (SQLiteConnection connection : mAcquiredConnections.keySet()) { - connection.clearPreparedStatementCache(); + connection.setDatabaseSeqNum(mDatabaseSeqNum); } } } diff --git a/core/java/android/os/LocaleList.java b/core/java/android/os/LocaleList.java index b74bb333deae..82cdd280a0f3 100644 --- a/core/java/android/os/LocaleList.java +++ b/core/java/android/os/LocaleList.java @@ -30,6 +30,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Locale; /** @@ -151,6 +152,25 @@ public final class LocaleList implements Parcelable { } /** + * Find the intersection between this LocaleList and another + * @return a String array of the Locales in both LocaleLists + * {@hide} + */ + @NonNull + public String[] getIntersection(@NonNull LocaleList other) { + List<String> intersection = new ArrayList<>(); + for (Locale l1 : mList) { + for (Locale l2 : other.mList) { + if (matchesLanguageAndScript(l2, l1)) { + intersection.add(l1.toLanguageTag()); + break; + } + } + } + return intersection.toArray(new String[0]); + } + + /** * Creates a new {@link LocaleList}. * * If two or more same locales are passed, the repeated locales will be dropped. diff --git a/core/java/android/os/TEST_MAPPING b/core/java/android/os/TEST_MAPPING index ea5499f4d5cd..954ee3c99346 100644 --- a/core/java/android/os/TEST_MAPPING +++ b/core/java/android/os/TEST_MAPPING @@ -14,6 +14,19 @@ ] }, { + "file_patterns": [ + "[^/]*(Vibrator|Vibration)[^/]*\\.java", + "vibrator/.*" + ], + "name": "FrameworksVibratorServicesTests", + "options": [ + {"exclude-annotation": "android.platform.test.annotations.LargeTest"}, + {"exclude-annotation": "android.platform.test.annotations.FlakyTest"}, + {"exclude-annotation": "androidx.test.filters.FlakyTest"}, + {"exclude-annotation": "org.junit.Ignore"} + ] + }, + { "file_patterns": ["Bugreport[^/]*\\.java"], "name": "BugreportManagerTestCases", "options": [ diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java index 654fa1da9d35..ca33c5e05944 100644 --- a/core/java/android/view/Choreographer.java +++ b/core/java/android/view/Choreographer.java @@ -1260,10 +1260,10 @@ public final class Choreographer { DisplayEventReceiver.VsyncEventData latestVsyncEventData = displayEventReceiver.getLatestVsyncEventData(); if (latestVsyncEventData == null) { - throw new IllegalArgumentException( - "Could not get VsyncEventData. Did SurfaceFlinger crash?"); + Log.w(TAG, "Could not get latest VsyncEventData. Did SurfaceFlinger crash?"); + } else { + update(frameTimeNanos, latestVsyncEventData); } - update(frameTimeNanos, latestVsyncEventData); } else { update(frameTimeNanos, newPreferredIndex); } diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index 48fb719279d5..8b23f19f1067 100644 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -958,6 +958,7 @@ public final class InputDevice implements Parcelable { * @hide */ @Nullable + @TestApi public String getKeyboardLanguageTag() { return mKeyboardLanguageTag; } @@ -968,6 +969,7 @@ public final class InputDevice implements Parcelable { * @hide */ @Nullable + @TestApi public String getKeyboardLayoutType() { return mKeyboardLayoutType; } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 379d1d8769ac..cf71d69d0141 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -310,6 +310,16 @@ public final class ViewRootImpl implements ViewParent, SystemProperties.getBoolean("persist.wm.debug.client_transient", false); /** + * Whether the client (system UI) is handling the immersive confirmation window. If + * {@link CLIENT_TRANSIENT} is set to true, the immersive confirmation window will always be the + * client instance and this flag will be ignored. Otherwise, the immersive confirmation window + * can be switched freely by this flag. + * @hide + */ + public static final boolean CLIENT_IMMERSIVE_CONFIRMATION = + SystemProperties.getBoolean("persist.wm.debug.client_immersive_confirmation", false); + + /** * Whether the client should compute the window frame on its own. * @hide */ diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index f1537ca950d6..355448353179 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -3099,6 +3099,16 @@ public interface WindowManager extends ViewManager { public static final int PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE = 1 << 16; /** + * Flag to indicate that this window is a immersive mode confirmation window. The window + * should be ignored when calculating insets control. This is used for prompt window + * triggered by insets visibility changes. If it can take over the insets control, the + * visibility will change unexpectedly and the window may dismiss itself. Power button panic + * handling will be disabled when this window exists. + * @hide + */ + public static final int PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW = 1 << 17; + + /** * Flag to indicate that any window added by an application process that is of type * {@link #TYPE_TOAST} or that requires * {@link android.app.AppOpsManager#OP_SYSTEM_ALERT_WINDOW} permission should be hidden when @@ -3242,6 +3252,7 @@ public interface WindowManager extends ViewManager { PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME, PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS, PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE, + PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW, SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY, PRIVATE_FLAG_EXCLUDE_FROM_SCREEN_MAGNIFICATION, @@ -3326,6 +3337,10 @@ public interface WindowManager extends ViewManager { equals = PRIVATE_FLAG_SUSTAINED_PERFORMANCE_MODE, name = "SUSTAINED_PERFORMANCE_MODE"), @ViewDebug.FlagToString( + mask = PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW, + equals = PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW, + name = "IMMERSIVE_CONFIRMATION_WINDOW"), + @ViewDebug.FlagToString( mask = SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, equals = SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS, name = "HIDE_NON_SYSTEM_OVERLAY_WINDOWS"), diff --git a/core/java/android/view/animation/AnimationUtils.java b/core/java/android/view/animation/AnimationUtils.java index 0699bc1cb734..8ba8b8cca5ed 100644 --- a/core/java/android/view/animation/AnimationUtils.java +++ b/core/java/android/view/animation/AnimationUtils.java @@ -32,7 +32,6 @@ import android.os.SystemClock; import android.util.AttributeSet; import android.util.TimeUtils; import android.util.Xml; -import android.view.Choreographer; import android.view.InflateException; import org.xmlpull.v1.XmlPullParser; @@ -154,13 +153,7 @@ public class AnimationUtils { */ public static long getExpectedPresentationTimeNanos() { AnimationState state = sAnimationState.get(); - if (state.animationClockLocked) { - return state.mExpectedPresentationTimeNanos; - } - // When this methoed is called outside of a Choreographer callback, - // we obtain the value of expectedPresentTimeNanos from the Choreographer. - // This helps avoid returning a time that could potentially be earlier than current time. - return Choreographer.getInstance().getLatestExpectedPresentTimeNanos(); + return state.mExpectedPresentationTimeNanos; } /** diff --git a/core/java/android/widget/TEST_MAPPING b/core/java/android/widget/TEST_MAPPING index 49c409368448..107cac2825ac 100644 --- a/core/java/android/widget/TEST_MAPPING +++ b/core/java/android/widget/TEST_MAPPING @@ -10,10 +10,10 @@ "file_patterns": ["Toast\\.java"] }, { - "name": "CtsWindowManagerDeviceTestCases", + "name": "CtsWindowManagerDeviceWindow", "options": [ { - "include-filter": "android.server.wm.ToastWindowTest" + "include-filter": "android.server.wm.window.ToastWindowTest" } ], "file_patterns": ["Toast\\.java"] diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java index e153bb70a7ca..43fa0be6c1b7 100644 --- a/core/java/android/window/TaskFragmentOperation.java +++ b/core/java/android/window/TaskFragmentOperation.java @@ -80,6 +80,14 @@ public final class TaskFragmentOperation implements Parcelable { */ public static final int OP_TYPE_REORDER_TO_FRONT = 10; + /** + * Sets the activity navigation to be isolated, where the activity navigation on the + * TaskFragment is separated from the rest activities in the Task. Activities cannot be + * started on an isolated TaskFragment unless the activities are launched from the same + * TaskFragment or explicitly requested to. + */ + public static final int OP_TYPE_SET_ISOLATED_NAVIGATION = 11; + @IntDef(prefix = { "OP_TYPE_" }, value = { OP_TYPE_UNKNOWN, OP_TYPE_CREATE_TASK_FRAGMENT, @@ -92,7 +100,8 @@ public final class TaskFragmentOperation implements Parcelable { OP_TYPE_SET_COMPANION_TASK_FRAGMENT, OP_TYPE_SET_ANIMATION_PARAMS, OP_TYPE_SET_RELATIVE_BOUNDS, - OP_TYPE_REORDER_TO_FRONT + OP_TYPE_REORDER_TO_FRONT, + OP_TYPE_SET_ISOLATED_NAVIGATION }) @Retention(RetentionPolicy.SOURCE) public @interface OperationType {} @@ -118,11 +127,14 @@ public final class TaskFragmentOperation implements Parcelable { @Nullable private final TaskFragmentAnimationParams mAnimationParams; + private final boolean mIsolatedNav; + private TaskFragmentOperation(@OperationType int opType, @Nullable TaskFragmentCreationParams taskFragmentCreationParams, @Nullable IBinder activityToken, @Nullable Intent activityIntent, @Nullable Bundle bundle, @Nullable IBinder secondaryFragmentToken, - @Nullable TaskFragmentAnimationParams animationParams) { + @Nullable TaskFragmentAnimationParams animationParams, + boolean isolatedNav) { mOpType = opType; mTaskFragmentCreationParams = taskFragmentCreationParams; mActivityToken = activityToken; @@ -130,6 +142,7 @@ public final class TaskFragmentOperation implements Parcelable { mBundle = bundle; mSecondaryFragmentToken = secondaryFragmentToken; mAnimationParams = animationParams; + mIsolatedNav = isolatedNav; } private TaskFragmentOperation(Parcel in) { @@ -140,6 +153,7 @@ public final class TaskFragmentOperation implements Parcelable { mBundle = in.readBundle(getClass().getClassLoader()); mSecondaryFragmentToken = in.readStrongBinder(); mAnimationParams = in.readTypedObject(TaskFragmentAnimationParams.CREATOR); + mIsolatedNav = in.readBoolean(); } @Override @@ -151,6 +165,7 @@ public final class TaskFragmentOperation implements Parcelable { dest.writeBundle(mBundle); dest.writeStrongBinder(mSecondaryFragmentToken); dest.writeTypedObject(mAnimationParams, flags); + dest.writeBoolean(mIsolatedNav); } @NonNull @@ -223,6 +238,14 @@ public final class TaskFragmentOperation implements Parcelable { return mAnimationParams; } + /** + * Returns whether the activity navigation on this TaskFragment is isolated. This is only + * useful when the op type is {@link OP_TYPE_SET_ISOLATED_NAVIGATION}. + */ + public boolean isIsolatedNav() { + return mIsolatedNav; + } + @Override public String toString() { final StringBuilder sb = new StringBuilder(); @@ -245,6 +268,7 @@ public final class TaskFragmentOperation implements Parcelable { if (mAnimationParams != null) { sb.append(", animationParams=").append(mAnimationParams); } + sb.append(", isolatedNav=").append(mIsolatedNav); sb.append('}'); return sb.toString(); @@ -253,7 +277,7 @@ public final class TaskFragmentOperation implements Parcelable { @Override public int hashCode() { return Objects.hash(mOpType, mTaskFragmentCreationParams, mActivityToken, mActivityIntent, - mBundle, mSecondaryFragmentToken, mAnimationParams); + mBundle, mSecondaryFragmentToken, mAnimationParams, mIsolatedNav); } @Override @@ -268,7 +292,8 @@ public final class TaskFragmentOperation implements Parcelable { && Objects.equals(mActivityIntent, other.mActivityIntent) && Objects.equals(mBundle, other.mBundle) && Objects.equals(mSecondaryFragmentToken, other.mSecondaryFragmentToken) - && Objects.equals(mAnimationParams, other.mAnimationParams); + && Objects.equals(mAnimationParams, other.mAnimationParams) + && mIsolatedNav == other.mIsolatedNav; } @Override @@ -300,6 +325,8 @@ public final class TaskFragmentOperation implements Parcelable { @Nullable private TaskFragmentAnimationParams mAnimationParams; + private boolean mIsolatedNav; + /** * @param opType the {@link OperationType} of this {@link TaskFragmentOperation}. */ @@ -363,12 +390,22 @@ public final class TaskFragmentOperation implements Parcelable { } /** + * Sets the activity navigation of this TaskFragment to be isolated. + */ + @NonNull + public Builder setIsolatedNav(boolean isolatedNav) { + mIsolatedNav = isolatedNav; + return this; + } + + /** * Constructs the {@link TaskFragmentOperation}. */ @NonNull public TaskFragmentOperation build() { return new TaskFragmentOperation(mOpType, mTaskFragmentCreationParams, mActivityToken, - mActivityIntent, mBundle, mSecondaryFragmentToken, mAnimationParams); + mActivityIntent, mBundle, mSecondaryFragmentToken, mAnimationParams, + mIsolatedNav); } } } diff --git a/core/java/android/window/WindowContextController.java b/core/java/android/window/WindowContextController.java index e78056e93568..99e63ec7fe02 100644 --- a/core/java/android/window/WindowContextController.java +++ b/core/java/android/window/WindowContextController.java @@ -21,7 +21,6 @@ import static java.lang.annotation.RetentionPolicy.SOURCE; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.servertransaction.WindowTokenClientController; import android.content.Context; import android.os.Bundle; import android.os.IBinder; diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java index c7af25bf943f..74585638df6f 100644 --- a/core/java/android/window/WindowTokenClient.java +++ b/core/java/android/window/WindowTokenClient.java @@ -25,7 +25,6 @@ import android.annotation.NonNull; import android.app.ActivityThread; import android.app.IWindowToken; import android.app.ResourcesManager; -import android.app.servertransaction.WindowTokenClientController; import android.content.Context; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; diff --git a/core/java/android/app/servertransaction/WindowTokenClientController.java b/core/java/android/window/WindowTokenClientController.java index 3060ab8d7cb2..2f05f830fe09 100644 --- a/core/java/android/app/servertransaction/WindowTokenClientController.java +++ b/core/java/android/window/WindowTokenClientController.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.app.servertransaction; +package android.window; import static android.view.WindowManager.LayoutParams.WindowType; import static android.view.WindowManagerGlobal.getWindowManagerService; @@ -23,6 +23,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityThread; import android.app.IApplicationThread; +import android.app.servertransaction.WindowContextConfigurationChangeItem; +import android.app.servertransaction.WindowContextWindowRemovalItem; import android.content.Context; import android.content.res.Configuration; import android.os.Bundle; @@ -31,8 +33,6 @@ import android.os.RemoteException; import android.util.ArrayMap; import android.util.Log; import android.view.IWindowManager; -import android.window.WindowContext; -import android.window.WindowTokenClient; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 50f393b53277..2445daf89b64 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -31,6 +31,7 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERS import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PROFILE_NOT_SUPPORTED; import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB; import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB_ACCESSIBILITY; +import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.PermissionChecker.PID_UNKNOWN; import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL; @@ -356,6 +357,12 @@ public class ResolverActivity extends Activity implements // flag set, we are now losing it. That should be a very rare case // and we can live with this. intent.setFlags(intent.getFlags()&~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + + // If FLAG_ACTIVITY_LAUNCH_ADJACENT was set, ResolverActivity was opened in the alternate + // side, which means we want to open the target app on the same side as ResolverActivity. + if ((intent.getFlags() & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0) { + intent.setFlags(intent.getFlags() & ~FLAG_ACTIVITY_LAUNCH_ADJACENT); + } return intent; } diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index d2564fb9c268..c6f5086b8346 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -63,6 +63,19 @@ oneway interface IStatusBar void cancelPreloadRecentApps(); void showScreenPinningRequest(int taskId); + /** + * Notify system UI the immersive prompt should be dismissed as confirmed, and the confirmed + * status should be saved without user clicking on the button. This could happen when a user + * swipe on the edge with the confirmation prompt showing. + */ + void confirmImmersivePrompt(); + + /** + * Notify system UI the immersive mode changed. This shall be removed when client immersive is + * enabled. + */ + void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode); + void dismissKeyboardShortcutsMenu(); void toggleKeyboardShortcutsMenu(int deviceId); diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp index 979c9e3ea7a3..1afae2944178 100644 --- a/core/jni/android_util_AssetManager.cpp +++ b/core/jni/android_util_AssetManager.cpp @@ -347,14 +347,23 @@ static void NativeSetApkAssets(JNIEnv* env, jclass /*clazz*/, jlong ptr, } static void NativeSetConfiguration(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint mcc, jint mnc, - jstring locale, jint orientation, jint touchscreen, jint density, - jint keyboard, jint keyboard_hidden, jint navigation, - jint screen_width, jint screen_height, - jint smallest_screen_width_dp, jint screen_width_dp, - jint screen_height_dp, jint screen_layout, jint ui_mode, - jint color_mode, jint grammatical_gender, jint major_version) { + jstring default_locale, jobjectArray locales, jint orientation, + jint touchscreen, jint density, jint keyboard, + jint keyboard_hidden, jint navigation, jint screen_width, + jint screen_height, jint smallest_screen_width_dp, + jint screen_width_dp, jint screen_height_dp, jint screen_layout, + jint ui_mode, jint color_mode, jint grammatical_gender, + jint major_version) { ATRACE_NAME("AssetManager::SetConfiguration"); + const jsize locale_count = (locales == NULL) ? 0 : env->GetArrayLength(locales); + + // Constants duplicated from Java class android.content.res.Configuration. + static const jint kScreenLayoutRoundMask = 0x300; + static const jint kScreenLayoutRoundShift = 8; + + std::vector<ResTable_config> configs; + ResTable_config configuration; memset(&configuration, 0, sizeof(configuration)); configuration.mcc = static_cast<uint16_t>(mcc); @@ -375,25 +384,37 @@ static void NativeSetConfiguration(JNIEnv* env, jclass /*clazz*/, jlong ptr, jin configuration.colorMode = static_cast<uint8_t>(color_mode); configuration.grammaticalInflection = static_cast<uint8_t>(grammatical_gender); configuration.sdkVersion = static_cast<uint16_t>(major_version); - - if (locale != nullptr) { - ScopedUtfChars locale_utf8(env, locale); - CHECK(locale_utf8.c_str() != nullptr); - configuration.setBcp47Locale(locale_utf8.c_str()); - } - - // Constants duplicated from Java class android.content.res.Configuration. - static const jint kScreenLayoutRoundMask = 0x300; - static const jint kScreenLayoutRoundShift = 8; - // In Java, we use a 32bit integer for screenLayout, while we only use an 8bit integer // in C++. We must extract the round qualifier out of the Java screenLayout and put it // into screenLayout2. configuration.screenLayout2 = - static_cast<uint8_t>((screen_layout & kScreenLayoutRoundMask) >> kScreenLayoutRoundShift); + static_cast<uint8_t>((screen_layout & kScreenLayoutRoundMask) >> kScreenLayoutRoundShift); + + if (locale_count > 0) { + configs.resize(locale_count, configuration); + for (int i = 0; i < locale_count; i++) { + jstring locale = (jstring)(env->GetObjectArrayElement(locales, i)); + ScopedUtfChars locale_utf8(env, locale); + CHECK(locale_utf8.c_str() != nullptr); + configs[i].setBcp47Locale(locale_utf8.c_str()); + } + } else { + configs.push_back(configuration); + } + + uint32_t default_locale_int = 0; + if (default_locale != nullptr) { + ResTable_config config; + static_assert(std::is_same_v<decltype(config.locale), decltype(default_locale_int)>); + ScopedUtfChars locale_utf8(env, default_locale); + CHECK(locale_utf8.c_str() != nullptr); + config.setBcp47Locale(locale_utf8.c_str()); + default_locale_int = config.locale; + } auto assetmanager = LockAndStartAssetManager(ptr); - assetmanager->SetConfiguration(configuration); + assetmanager->SetConfigurations(configs); + assetmanager->SetDefaultLocale(default_locale_int); } static jobject NativeGetAssignedPackageIdentifiers(JNIEnv* env, jclass /*clazz*/, jlong ptr, @@ -1498,94 +1519,97 @@ static jlong NativeAssetGetRemainingLength(JNIEnv* /*env*/, jclass /*clazz*/, jl // JNI registration. static const JNINativeMethod gAssetManagerMethods[] = { - // AssetManager setup methods. - {"nativeCreate", "()J", (void*)NativeCreate}, - {"nativeDestroy", "(J)V", (void*)NativeDestroy}, - {"nativeSetApkAssets", "(J[Landroid/content/res/ApkAssets;Z)V", (void*)NativeSetApkAssets}, - {"nativeSetConfiguration", "(JIILjava/lang/String;IIIIIIIIIIIIIIII)V", - (void*)NativeSetConfiguration}, - {"nativeGetAssignedPackageIdentifiers", "(JZZ)Landroid/util/SparseArray;", - (void*)NativeGetAssignedPackageIdentifiers}, - - // AssetManager file methods. - {"nativeContainsAllocatedTable", "(J)Z", (void*)ContainsAllocatedTable}, - {"nativeList", "(JLjava/lang/String;)[Ljava/lang/String;", (void*)NativeList}, - {"nativeOpenAsset", "(JLjava/lang/String;I)J", (void*)NativeOpenAsset}, - {"nativeOpenAssetFd", "(JLjava/lang/String;[J)Landroid/os/ParcelFileDescriptor;", - (void*)NativeOpenAssetFd}, - {"nativeOpenNonAsset", "(JILjava/lang/String;I)J", (void*)NativeOpenNonAsset}, - {"nativeOpenNonAssetFd", "(JILjava/lang/String;[J)Landroid/os/ParcelFileDescriptor;", - (void*)NativeOpenNonAssetFd}, - {"nativeOpenXmlAsset", "(JILjava/lang/String;)J", (void*)NativeOpenXmlAsset}, - {"nativeOpenXmlAssetFd", "(JILjava/io/FileDescriptor;)J", (void*)NativeOpenXmlAssetFd}, - - // AssetManager resource methods. - {"nativeGetResourceValue", "(JISLandroid/util/TypedValue;Z)I", (void*)NativeGetResourceValue}, - {"nativeGetResourceBagValue", "(JIILandroid/util/TypedValue;)I", - (void*)NativeGetResourceBagValue}, - {"nativeGetStyleAttributes", "(JI)[I", (void*)NativeGetStyleAttributes}, - {"nativeGetResourceStringArray", "(JI)[Ljava/lang/String;", - (void*)NativeGetResourceStringArray}, - {"nativeGetResourceStringArrayInfo", "(JI)[I", (void*)NativeGetResourceStringArrayInfo}, - {"nativeGetResourceIntArray", "(JI)[I", (void*)NativeGetResourceIntArray}, - {"nativeGetResourceArraySize", "(JI)I", (void*)NativeGetResourceArraySize}, - {"nativeGetResourceArray", "(JI[I)I", (void*)NativeGetResourceArray}, - {"nativeGetParentThemeIdentifier", "(JI)I", - (void*)NativeGetParentThemeIdentifier}, - - // AssetManager resource name/ID methods. - {"nativeGetResourceIdentifier", "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)I", - (void*)NativeGetResourceIdentifier}, - {"nativeGetResourceName", "(JI)Ljava/lang/String;", (void*)NativeGetResourceName}, - {"nativeGetResourcePackageName", "(JI)Ljava/lang/String;", (void*)NativeGetResourcePackageName}, - {"nativeGetResourceTypeName", "(JI)Ljava/lang/String;", (void*)NativeGetResourceTypeName}, - {"nativeGetResourceEntryName", "(JI)Ljava/lang/String;", (void*)NativeGetResourceEntryName}, - {"nativeSetResourceResolutionLoggingEnabled", "(JZ)V", - (void*) NativeSetResourceResolutionLoggingEnabled}, - {"nativeGetLastResourceResolution", "(J)Ljava/lang/String;", - (void*) NativeGetLastResourceResolution}, - {"nativeGetLocales", "(JZ)[Ljava/lang/String;", (void*)NativeGetLocales}, - {"nativeGetSizeConfigurations", "(J)[Landroid/content/res/Configuration;", - (void*)NativeGetSizeConfigurations}, - {"nativeGetSizeAndUiModeConfigurations", "(J)[Landroid/content/res/Configuration;", - (void*)NativeGetSizeAndUiModeConfigurations}, - - // Style attribute related methods. - {"nativeAttributeResolutionStack", "(JJIII)[I", (void*)NativeAttributeResolutionStack}, - {"nativeApplyStyle", "(JJIIJ[IJJ)V", (void*)NativeApplyStyle}, - {"nativeResolveAttrs", "(JJII[I[I[I[I)Z", (void*)NativeResolveAttrs}, - {"nativeRetrieveAttributes", "(JJ[I[I[I)Z", (void*)NativeRetrieveAttributes}, - - // Theme related methods. - {"nativeThemeCreate", "(J)J", (void*)NativeThemeCreate}, - {"nativeGetThemeFreeFunction", "()J", (void*)NativeGetThemeFreeFunction}, - {"nativeThemeApplyStyle", "(JJIZ)V", (void*)NativeThemeApplyStyle}, - {"nativeThemeRebase", "(JJ[I[ZI)V", (void*)NativeThemeRebase}, - - {"nativeThemeCopy", "(JJJJ)V", (void*)NativeThemeCopy}, - {"nativeThemeGetAttributeValue", "(JJILandroid/util/TypedValue;Z)I", - (void*)NativeThemeGetAttributeValue}, - {"nativeThemeDump", "(JJILjava/lang/String;Ljava/lang/String;)V", (void*)NativeThemeDump}, - {"nativeThemeGetChangingConfigurations", "(J)I", (void*)NativeThemeGetChangingConfigurations}, - - // AssetInputStream methods. - {"nativeAssetDestroy", "(J)V", (void*)NativeAssetDestroy}, - {"nativeAssetReadChar", "(J)I", (void*)NativeAssetReadChar}, - {"nativeAssetRead", "(J[BII)I", (void*)NativeAssetRead}, - {"nativeAssetSeek", "(JJI)J", (void*)NativeAssetSeek}, - {"nativeAssetGetLength", "(J)J", (void*)NativeAssetGetLength}, - {"nativeAssetGetRemainingLength", "(J)J", (void*)NativeAssetGetRemainingLength}, - - // System/idmap related methods. - {"nativeGetOverlayableMap", "(JLjava/lang/String;)Ljava/util/Map;", - (void*)NativeGetOverlayableMap}, - {"nativeGetOverlayablesToString", "(JLjava/lang/String;)Ljava/lang/String;", - (void*)NativeGetOverlayablesToString}, - - // Global management/debug methods. - {"getGlobalAssetCount", "()I", (void*)NativeGetGlobalAssetCount}, - {"getAssetAllocations", "()Ljava/lang/String;", (void*)NativeGetAssetAllocations}, - {"getGlobalAssetManagerCount", "()I", (void*)NativeGetGlobalAssetManagerCount}, + // AssetManager setup methods. + {"nativeCreate", "()J", (void*)NativeCreate}, + {"nativeDestroy", "(J)V", (void*)NativeDestroy}, + {"nativeSetApkAssets", "(J[Landroid/content/res/ApkAssets;Z)V", (void*)NativeSetApkAssets}, + {"nativeSetConfiguration", "(JIILjava/lang/String;[Ljava/lang/String;IIIIIIIIIIIIIIII)V", + (void*)NativeSetConfiguration}, + {"nativeGetAssignedPackageIdentifiers", "(JZZ)Landroid/util/SparseArray;", + (void*)NativeGetAssignedPackageIdentifiers}, + + // AssetManager file methods. + {"nativeContainsAllocatedTable", "(J)Z", (void*)ContainsAllocatedTable}, + {"nativeList", "(JLjava/lang/String;)[Ljava/lang/String;", (void*)NativeList}, + {"nativeOpenAsset", "(JLjava/lang/String;I)J", (void*)NativeOpenAsset}, + {"nativeOpenAssetFd", "(JLjava/lang/String;[J)Landroid/os/ParcelFileDescriptor;", + (void*)NativeOpenAssetFd}, + {"nativeOpenNonAsset", "(JILjava/lang/String;I)J", (void*)NativeOpenNonAsset}, + {"nativeOpenNonAssetFd", "(JILjava/lang/String;[J)Landroid/os/ParcelFileDescriptor;", + (void*)NativeOpenNonAssetFd}, + {"nativeOpenXmlAsset", "(JILjava/lang/String;)J", (void*)NativeOpenXmlAsset}, + {"nativeOpenXmlAssetFd", "(JILjava/io/FileDescriptor;)J", (void*)NativeOpenXmlAssetFd}, + + // AssetManager resource methods. + {"nativeGetResourceValue", "(JISLandroid/util/TypedValue;Z)I", + (void*)NativeGetResourceValue}, + {"nativeGetResourceBagValue", "(JIILandroid/util/TypedValue;)I", + (void*)NativeGetResourceBagValue}, + {"nativeGetStyleAttributes", "(JI)[I", (void*)NativeGetStyleAttributes}, + {"nativeGetResourceStringArray", "(JI)[Ljava/lang/String;", + (void*)NativeGetResourceStringArray}, + {"nativeGetResourceStringArrayInfo", "(JI)[I", (void*)NativeGetResourceStringArrayInfo}, + {"nativeGetResourceIntArray", "(JI)[I", (void*)NativeGetResourceIntArray}, + {"nativeGetResourceArraySize", "(JI)I", (void*)NativeGetResourceArraySize}, + {"nativeGetResourceArray", "(JI[I)I", (void*)NativeGetResourceArray}, + {"nativeGetParentThemeIdentifier", "(JI)I", (void*)NativeGetParentThemeIdentifier}, + + // AssetManager resource name/ID methods. + {"nativeGetResourceIdentifier", + "(JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;)I", + (void*)NativeGetResourceIdentifier}, + {"nativeGetResourceName", "(JI)Ljava/lang/String;", (void*)NativeGetResourceName}, + {"nativeGetResourcePackageName", "(JI)Ljava/lang/String;", + (void*)NativeGetResourcePackageName}, + {"nativeGetResourceTypeName", "(JI)Ljava/lang/String;", (void*)NativeGetResourceTypeName}, + {"nativeGetResourceEntryName", "(JI)Ljava/lang/String;", (void*)NativeGetResourceEntryName}, + {"nativeSetResourceResolutionLoggingEnabled", "(JZ)V", + (void*)NativeSetResourceResolutionLoggingEnabled}, + {"nativeGetLastResourceResolution", "(J)Ljava/lang/String;", + (void*)NativeGetLastResourceResolution}, + {"nativeGetLocales", "(JZ)[Ljava/lang/String;", (void*)NativeGetLocales}, + {"nativeGetSizeConfigurations", "(J)[Landroid/content/res/Configuration;", + (void*)NativeGetSizeConfigurations}, + {"nativeGetSizeAndUiModeConfigurations", "(J)[Landroid/content/res/Configuration;", + (void*)NativeGetSizeAndUiModeConfigurations}, + + // Style attribute related methods. + {"nativeAttributeResolutionStack", "(JJIII)[I", (void*)NativeAttributeResolutionStack}, + {"nativeApplyStyle", "(JJIIJ[IJJ)V", (void*)NativeApplyStyle}, + {"nativeResolveAttrs", "(JJII[I[I[I[I)Z", (void*)NativeResolveAttrs}, + {"nativeRetrieveAttributes", "(JJ[I[I[I)Z", (void*)NativeRetrieveAttributes}, + + // Theme related methods. + {"nativeThemeCreate", "(J)J", (void*)NativeThemeCreate}, + {"nativeGetThemeFreeFunction", "()J", (void*)NativeGetThemeFreeFunction}, + {"nativeThemeApplyStyle", "(JJIZ)V", (void*)NativeThemeApplyStyle}, + {"nativeThemeRebase", "(JJ[I[ZI)V", (void*)NativeThemeRebase}, + + {"nativeThemeCopy", "(JJJJ)V", (void*)NativeThemeCopy}, + {"nativeThemeGetAttributeValue", "(JJILandroid/util/TypedValue;Z)I", + (void*)NativeThemeGetAttributeValue}, + {"nativeThemeDump", "(JJILjava/lang/String;Ljava/lang/String;)V", (void*)NativeThemeDump}, + {"nativeThemeGetChangingConfigurations", "(J)I", + (void*)NativeThemeGetChangingConfigurations}, + + // AssetInputStream methods. + {"nativeAssetDestroy", "(J)V", (void*)NativeAssetDestroy}, + {"nativeAssetReadChar", "(J)I", (void*)NativeAssetReadChar}, + {"nativeAssetRead", "(J[BII)I", (void*)NativeAssetRead}, + {"nativeAssetSeek", "(JJI)J", (void*)NativeAssetSeek}, + {"nativeAssetGetLength", "(J)J", (void*)NativeAssetGetLength}, + {"nativeAssetGetRemainingLength", "(J)J", (void*)NativeAssetGetRemainingLength}, + + // System/idmap related methods. + {"nativeGetOverlayableMap", "(JLjava/lang/String;)Ljava/util/Map;", + (void*)NativeGetOverlayableMap}, + {"nativeGetOverlayablesToString", "(JLjava/lang/String;)Ljava/lang/String;", + (void*)NativeGetOverlayablesToString}, + + // Global management/debug methods. + {"getGlobalAssetCount", "()I", (void*)NativeGetGlobalAssetCount}, + {"getAssetAllocations", "()Ljava/lang/String;", (void*)NativeGetAssetAllocations}, + {"getGlobalAssetManagerCount", "()I", (void*)NativeGetGlobalAssetManagerCount}, }; int register_android_content_AssetManager(JNIEnv* env) { diff --git a/core/res/res/drawable-round-watch/progress_indeterminate_horizontal_material.xml b/core/res/res/drawable-round-watch/progress_indeterminate_horizontal_material.xml new file mode 100644 index 000000000000..bd77e595af08 --- /dev/null +++ b/core/res/res/drawable-round-watch/progress_indeterminate_horizontal_material.xml @@ -0,0 +1,944 @@ +<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2014 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <target android:name="_R_G_L_1_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="983" + android:propertyName="fillAlpha" + android:startOffset="0" + android:valueFrom="0.1" + android:valueTo="0.1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="fillAlpha" + android:startOffset="983" + android:valueFrom="0.1" + android:valueTo="0.1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_1_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="983" + android:pathData="M 50,50C 50,50 50,50 50,50" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="0"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:pathData="M 50,50C 50,50 50,50 50,50" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="983"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_1_G_N_1_T_1"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="983" + android:pathData="M 32,12C 32,12 32,12 32,12" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="0"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:pathData="M 32,12C 32,12 32,12 32,12" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="983"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="83" + android:propertyName="fillAlpha" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="0" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.6,0 0.833,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="500" + android:propertyName="fillAlpha" + android:startOffset="83" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.6,0 0.999,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="400" + android:propertyName="fillAlpha" + android:startOffset="583" + android:valueFrom="1" + android:valueTo="0.0008500000000000001" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.001,0 0.201,0.967 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="fillAlpha" + android:startOffset="983" + android:valueFrom="0.0008500000000000001" + android:valueTo="0" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.329,0.663 0.662,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M-2053 -413 C-2064.2,-412.04 -2074.66,-410.29 -2084.48,-407.87 C-2094.29,-405.46 -2103.47,-402.36 -2112.09,-398.71 C-2120.71,-395.06 -2128.79,-390.85 -2136.41,-386.19 C-2144.02,-381.54 -2151.19,-376.43 -2158,-371 C-2168.19,-362.86 -2177.05,-354.51 -2184.8,-345.79 C-2192.55,-337.07 -2199.2,-327.99 -2204.98,-318.41 C-2210.77,-308.83 -2215.7,-298.74 -2220,-288 C-2237.22,-245.04 -2237.06,-202.14 -2226.93,-164.3 C-2216.8,-126.46 -2196.69,-93.69 -2174,-71 C-2150.2,-47.2 -2115.92,-28.38 -2077.59,-19.79 C-2039.27,-11.2 -1996.92,-12.85 -1957,-30 C-1925.35,-43.59 -1896.01,-64.58 -1873.88,-92.68 C-1851.74,-120.77 -1836.82,-155.98 -1834,-198 C-1831.53,-234.86 -1837.92,-266 -1849.1,-292.1 C-1860.29,-318.21 -1876.28,-339.28 -1893,-356 C-1910.36,-373.36 -1932.3,-389.15 -1958.94,-399.84 C-1985.57,-410.52 -2016.89,-416.09 -2053,-413c " + android:valueTo="M-2053 -413 C-2072.13,-411.36 -2090.2,-406.86 -2106.82,-400.38 C-2123.44,-393.91 -2138.61,-385.46 -2151.93,-375.94 C-2165.26,-366.41 -2176.74,-355.8 -2186,-345 C-2200.2,-328.44 -2213.75,-307.38 -2222.86,-282.11 C-2231.96,-256.85 -2236.61,-227.38 -2233,-194 C-2229.85,-164.84 -2221.85,-140.07 -2210.02,-118.6 C-2198.19,-97.13 -2182.51,-78.96 -2164,-63 C-2146.03,-47.52 -2123.67,-34.03 -2098.23,-25.16 C-2072.79,-16.29 -2044.27,-12.03 -2014,-15 C-1985.05,-17.84 -1959.94,-25.74 -1938.13,-37.35 C-1916.32,-48.97 -1897.79,-64.3 -1882,-82 C-1866.83,-98.99 -1853.27,-120.85 -1844.28,-146.28 C-1835.29,-171.72 -1830.87,-200.72 -1834,-232 C-1836.98,-261.8 -1845.16,-287.41 -1856.88,-309.38 C-1868.6,-331.36 -1883.86,-349.71 -1901,-365 C-1917.51,-379.74 -1939.59,-393.1 -1965.5,-402.1 C-1991.42,-411.09 -2021.16,-415.73 -2053,-413c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="17" + android:valueFrom="M-2053 -413 C-2072.13,-411.36 -2090.2,-406.86 -2106.82,-400.38 C-2123.44,-393.91 -2138.61,-385.46 -2151.93,-375.94 C-2165.26,-366.41 -2176.74,-355.8 -2186,-345 C-2200.2,-328.44 -2213.75,-307.38 -2222.86,-282.11 C-2231.96,-256.85 -2236.61,-227.38 -2233,-194 C-2229.85,-164.84 -2221.85,-140.07 -2210.02,-118.6 C-2198.19,-97.13 -2182.51,-78.96 -2164,-63 C-2146.03,-47.52 -2123.67,-34.03 -2098.23,-25.16 C-2072.79,-16.29 -2044.27,-12.03 -2014,-15 C-1985.05,-17.84 -1959.94,-25.74 -1938.13,-37.35 C-1916.32,-48.97 -1897.79,-64.3 -1882,-82 C-1866.83,-98.99 -1853.27,-120.85 -1844.28,-146.28 C-1835.29,-171.72 -1830.87,-200.72 -1834,-232 C-1836.98,-261.8 -1845.16,-287.41 -1856.88,-309.38 C-1868.6,-331.36 -1883.86,-349.71 -1901,-365 C-1917.51,-379.74 -1939.59,-393.1 -1965.5,-402.1 C-1991.42,-411.09 -2021.16,-415.73 -2053,-413c " + android:valueTo="M-2052 -413 C-2071.28,-411.42 -2089.49,-406.96 -2106.24,-400.51 C-2122.98,-394.05 -2138.26,-385.61 -2151.69,-376.07 C-2165.12,-366.52 -2176.68,-355.87 -2186,-345 C-2200.23,-328.39 -2213.89,-307.21 -2223.04,-281.77 C-2232.19,-256.33 -2236.82,-226.63 -2233,-193 C-2229.75,-164.4 -2221.68,-139.52 -2209.83,-117.85 C-2197.98,-96.18 -2182.35,-77.73 -2164,-62 C-2146.19,-46.74 -2123.18,-33.34 -2097.25,-24.47 C-2071.32,-15.59 -2042.48,-11.22 -2013,-14 C-1983.8,-16.75 -1958.61,-24.73 -1936.82,-36.53 C-1915.02,-48.33 -1896.62,-63.95 -1881,-82 C-1865.71,-99.67 -1851.88,-121.33 -1842.66,-146.49 C-1833.45,-171.65 -1828.84,-200.32 -1832,-232 C-1835.01,-262.26 -1843.42,-287.88 -1855.37,-309.62 C-1867.32,-331.36 -1882.81,-349.22 -1900,-364 C-1917.39,-378.94 -1938.96,-392.54 -1964.39,-401.73 C-1989.83,-410.92 -2019.14,-415.7 -2052,-413c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="33" + android:valueFrom="M-2052 -413 C-2071.28,-411.42 -2089.49,-406.96 -2106.24,-400.51 C-2122.98,-394.05 -2138.26,-385.61 -2151.69,-376.07 C-2165.12,-366.52 -2176.68,-355.87 -2186,-345 C-2200.23,-328.39 -2213.89,-307.21 -2223.04,-281.77 C-2232.19,-256.33 -2236.82,-226.63 -2233,-193 C-2229.75,-164.4 -2221.68,-139.52 -2209.83,-117.85 C-2197.98,-96.18 -2182.35,-77.73 -2164,-62 C-2146.19,-46.74 -2123.18,-33.34 -2097.25,-24.47 C-2071.32,-15.59 -2042.48,-11.22 -2013,-14 C-1983.8,-16.75 -1958.61,-24.73 -1936.82,-36.53 C-1915.02,-48.33 -1896.62,-63.95 -1881,-82 C-1865.71,-99.67 -1851.88,-121.33 -1842.66,-146.49 C-1833.45,-171.65 -1828.84,-200.32 -1832,-232 C-1835.01,-262.26 -1843.42,-287.88 -1855.37,-309.62 C-1867.32,-331.36 -1882.81,-349.22 -1900,-364 C-1917.39,-378.94 -1938.96,-392.54 -1964.39,-401.73 C-1989.83,-410.92 -2019.14,-415.7 -2052,-413c " + android:valueTo="M-2052 -413 C-2071.66,-411.39 -2090,-406.88 -2106.76,-400.35 C-2123.51,-393.82 -2138.67,-385.28 -2151.97,-375.59 C-2165.26,-365.91 -2176.7,-355.08 -2186,-344 C-2200.29,-326.96 -2214.12,-305.92 -2223.33,-280.54 C-2232.55,-255.16 -2237.15,-225.42 -2233,-191 C-2229.56,-162.51 -2221.15,-137.78 -2208.93,-116.29 C-2196.71,-94.8 -2180.68,-76.54 -2162,-61 C-2143.85,-45.9 -2120.72,-32.5 -2094.69,-23.57 C-2068.65,-14.65 -2039.73,-10.19 -2010,-13 C-1981.12,-15.72 -1955.63,-23.67 -1933.48,-35.47 C-1911.33,-47.26 -1892.52,-62.9 -1877,-81 C-1861.92,-98.59 -1848.2,-120.71 -1839.11,-146.38 C-1830.02,-172.04 -1825.57,-201.24 -1829,-233 C-1832.23,-262.84 -1840.89,-288.53 -1853.04,-310.41 C-1865.19,-332.3 -1880.82,-350.37 -1898,-365 C-1915.02,-379.5 -1937.52,-392.81 -1963.76,-401.84 C-1990.01,-410.88 -2020,-415.63 -2052,-413c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="50" + android:valueFrom="M-2052 -413 C-2071.66,-411.39 -2090,-406.88 -2106.76,-400.35 C-2123.51,-393.82 -2138.67,-385.28 -2151.97,-375.59 C-2165.26,-365.91 -2176.7,-355.08 -2186,-344 C-2200.29,-326.96 -2214.12,-305.92 -2223.33,-280.54 C-2232.55,-255.16 -2237.15,-225.42 -2233,-191 C-2229.56,-162.51 -2221.15,-137.78 -2208.93,-116.29 C-2196.71,-94.8 -2180.68,-76.54 -2162,-61 C-2143.85,-45.9 -2120.72,-32.5 -2094.69,-23.57 C-2068.65,-14.65 -2039.73,-10.19 -2010,-13 C-1981.12,-15.72 -1955.63,-23.67 -1933.48,-35.47 C-1911.33,-47.26 -1892.52,-62.9 -1877,-81 C-1861.92,-98.59 -1848.2,-120.71 -1839.11,-146.38 C-1830.02,-172.04 -1825.57,-201.24 -1829,-233 C-1832.23,-262.84 -1840.89,-288.53 -1853.04,-310.41 C-1865.19,-332.3 -1880.82,-350.37 -1898,-365 C-1915.02,-379.5 -1937.52,-392.81 -1963.76,-401.84 C-1990.01,-410.88 -2020,-415.63 -2052,-413c " + android:valueTo="M-2052 -413 C-2061.59,-412.21 -2070.55,-410.81 -2079.03,-408.89 C-2087.5,-406.97 -2095.48,-404.54 -2103.1,-401.69 C-2110.72,-398.84 -2117.98,-395.58 -2125,-392 C-2154.86,-376.79 -2181.38,-355.21 -2200.81,-326.98 C-2220.23,-298.75 -2232.55,-263.85 -2234,-222 C-2235.06,-191.38 -2230.21,-165.23 -2221.52,-142.55 C-2212.83,-119.88 -2200.3,-100.69 -2186,-84 C-2171.48,-67.06 -2154.35,-52.83 -2134.66,-41.45 C-2114.98,-30.08 -2092.74,-21.55 -2068,-16 C-2009.13,-2.8 -1958.21,-14.57 -1918.33,-39.75 C-1878.46,-64.93 -1849.64,-103.53 -1835,-144 C-1825.51,-170.22 -1822.73,-198.65 -1824.83,-225.69 C-1826.94,-252.72 -1833.93,-278.36 -1844,-299 C-1853.43,-318.34 -1865.87,-336.49 -1881.35,-352.36 C-1896.84,-368.23 -1915.37,-381.81 -1937,-392 C-1951.85,-398.99 -1969.2,-405.18 -1988.54,-409.15 C-2007.87,-413.12 -2029.2,-414.87 -2052,-413c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="67" + android:valueFrom="M-2052 -413 C-2061.59,-412.21 -2070.55,-410.81 -2079.03,-408.89 C-2087.5,-406.97 -2095.48,-404.54 -2103.1,-401.69 C-2110.72,-398.84 -2117.98,-395.58 -2125,-392 C-2154.86,-376.79 -2181.38,-355.21 -2200.81,-326.98 C-2220.23,-298.75 -2232.55,-263.85 -2234,-222 C-2235.06,-191.38 -2230.21,-165.23 -2221.52,-142.55 C-2212.83,-119.88 -2200.3,-100.69 -2186,-84 C-2171.48,-67.06 -2154.35,-52.83 -2134.66,-41.45 C-2114.98,-30.08 -2092.74,-21.55 -2068,-16 C-2009.13,-2.8 -1958.21,-14.57 -1918.33,-39.75 C-1878.46,-64.93 -1849.64,-103.53 -1835,-144 C-1825.51,-170.22 -1822.73,-198.65 -1824.83,-225.69 C-1826.94,-252.72 -1833.93,-278.36 -1844,-299 C-1853.43,-318.34 -1865.87,-336.49 -1881.35,-352.36 C-1896.84,-368.23 -1915.37,-381.81 -1937,-392 C-1951.85,-398.99 -1969.2,-405.18 -1988.54,-409.15 C-2007.87,-413.12 -2029.2,-414.87 -2052,-413c " + android:valueTo="M-2052 -413 C-2063.16,-412.08 -2073.74,-410.37 -2083.77,-407.96 C-2093.79,-405.56 -2103.26,-402.47 -2112.17,-398.81 C-2121.07,-395.16 -2129.43,-390.93 -2137.23,-386.26 C-2145.03,-381.58 -2152.29,-376.46 -2159,-371 C-2169.4,-362.54 -2178.31,-353.88 -2186.05,-344.85 C-2193.79,-335.82 -2200.37,-326.42 -2206.08,-316.5 C-2211.8,-306.57 -2216.67,-296.13 -2221,-285 C-2238.37,-240.34 -2237.17,-196.9 -2225.73,-159.1 C-2214.28,-121.3 -2192.59,-89.13 -2169,-67 C-2142.29,-41.95 -2105.22,-23.59 -2065.35,-15.17 C-2025.49,-6.75 -1982.85,-8.27 -1945,-23 C-1912.4,-35.68 -1882.18,-56.87 -1859.35,-85.41 C-1836.52,-113.96 -1821.07,-149.86 -1818,-192 C-1815.28,-229.38 -1822.17,-262.27 -1834.17,-289.87 C-1846.17,-317.47 -1863.28,-339.78 -1881,-356 C-1899.69,-373.11 -1924.59,-388.83 -1953.74,-399.53 C-1982.89,-410.24 -2016.29,-415.93 -2052,-413c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="83" + android:valueFrom="M-2052 -413 C-2063.16,-412.08 -2073.74,-410.37 -2083.77,-407.96 C-2093.79,-405.56 -2103.26,-402.47 -2112.17,-398.81 C-2121.07,-395.16 -2129.43,-390.93 -2137.23,-386.26 C-2145.03,-381.58 -2152.29,-376.46 -2159,-371 C-2169.4,-362.54 -2178.31,-353.88 -2186.05,-344.85 C-2193.79,-335.82 -2200.37,-326.42 -2206.08,-316.5 C-2211.8,-306.57 -2216.67,-296.13 -2221,-285 C-2238.37,-240.34 -2237.17,-196.9 -2225.73,-159.1 C-2214.28,-121.3 -2192.59,-89.13 -2169,-67 C-2142.29,-41.95 -2105.22,-23.59 -2065.35,-15.17 C-2025.49,-6.75 -1982.85,-8.27 -1945,-23 C-1912.4,-35.68 -1882.18,-56.87 -1859.35,-85.41 C-1836.52,-113.96 -1821.07,-149.86 -1818,-192 C-1815.28,-229.38 -1822.17,-262.27 -1834.17,-289.87 C-1846.17,-317.47 -1863.28,-339.78 -1881,-356 C-1899.69,-373.11 -1924.59,-388.83 -1953.74,-399.53 C-1982.89,-410.24 -2016.29,-415.93 -2052,-413c " + android:valueTo="M-2052 -413 C-2072.22,-411.34 -2091,-406.66 -2108.1,-399.87 C-2125.21,-393.08 -2140.66,-384.18 -2154.21,-374.07 C-2167.77,-363.95 -2179.44,-352.63 -2189,-341 C-2203.41,-323.47 -2216.63,-300.89 -2224.89,-274.11 C-2233.14,-247.32 -2236.44,-216.33 -2231,-182 C-2226.51,-153.63 -2217.1,-128.79 -2203.78,-107.33 C-2190.45,-85.88 -2173.19,-67.81 -2153,-53 C-2133.35,-38.58 -2108.08,-26.05 -2080.23,-17.76 C-2052.38,-9.48 -2021.96,-5.43 -1992,-8 C-1961.34,-10.63 -1935.39,-18.87 -1913.09,-31.22 C-1890.78,-43.56 -1872.12,-60 -1856,-79 C-1840.68,-97.06 -1827.09,-119.88 -1818.63,-146.65 C-1810.16,-173.42 -1806.82,-204.14 -1812,-238 C-1816.36,-266.51 -1826.56,-291.74 -1840.31,-313.35 C-1854.07,-334.96 -1871.39,-352.95 -1890,-367 C-1908.07,-380.64 -1932.92,-393.43 -1961.07,-402.16 C-1989.23,-410.89 -2020.69,-415.57 -2052,-413c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="100" + android:valueFrom="M-2052 -413 C-2072.22,-411.34 -2091,-406.66 -2108.1,-399.87 C-2125.21,-393.08 -2140.66,-384.18 -2154.21,-374.07 C-2167.77,-363.95 -2179.44,-352.63 -2189,-341 C-2203.41,-323.47 -2216.63,-300.89 -2224.89,-274.11 C-2233.14,-247.32 -2236.44,-216.33 -2231,-182 C-2226.51,-153.63 -2217.1,-128.79 -2203.78,-107.33 C-2190.45,-85.88 -2173.19,-67.81 -2153,-53 C-2133.35,-38.58 -2108.08,-26.05 -2080.23,-17.76 C-2052.38,-9.48 -2021.96,-5.43 -1992,-8 C-1961.34,-10.63 -1935.39,-18.87 -1913.09,-31.22 C-1890.78,-43.56 -1872.12,-60 -1856,-79 C-1840.68,-97.06 -1827.09,-119.88 -1818.63,-146.65 C-1810.16,-173.42 -1806.82,-204.14 -1812,-238 C-1816.36,-266.51 -1826.56,-291.74 -1840.31,-313.35 C-1854.07,-334.96 -1871.39,-352.95 -1890,-367 C-1908.07,-380.64 -1932.92,-393.43 -1961.07,-402.16 C-1989.23,-410.89 -2020.69,-415.57 -2052,-413c " + android:valueTo="M-2052 -413 C-2083.49,-410.41 -2110.62,-400.99 -2133.57,-387.67 C-2156.51,-374.35 -2175.26,-357.14 -2190,-339 C-2204.11,-321.63 -2217.58,-298.34 -2225.92,-270.66 C-2234.26,-242.97 -2237.46,-210.91 -2231,-176 C-2223.12,-133.42 -2203.82,-99.93 -2176.63,-73.96 C-2149.45,-47.99 -2114.39,-29.53 -2075,-17 C-2060.09,-12.26 -2044.98,-8.59 -2029.65,-6.43 C-2014.33,-4.28 -1998.78,-3.65 -1983,-5 C-1951.72,-7.67 -1925.53,-16 -1903.13,-28.62 C-1880.73,-41.24 -1862.11,-58.16 -1846,-78 C-1830.98,-96.49 -1817.15,-119.48 -1808.74,-146.6 C-1800.33,-173.72 -1797.34,-204.98 -1804,-240 C-1809.39,-268.37 -1820.04,-293.46 -1834.27,-314.92 C-1848.5,-336.37 -1866.3,-354.19 -1886,-368 C-1896.13,-375.1 -1907.27,-380.86 -1919.42,-385.96 C-1931.58,-391.07 -1944.76,-395.51 -1959,-400 C-1987.83,-409.09 -2018.76,-415.73 -2052,-413c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="117" + android:valueFrom="M-2052 -413 C-2083.49,-410.41 -2110.62,-400.99 -2133.57,-387.67 C-2156.51,-374.35 -2175.26,-357.14 -2190,-339 C-2204.11,-321.63 -2217.58,-298.34 -2225.92,-270.66 C-2234.26,-242.97 -2237.46,-210.91 -2231,-176 C-2223.12,-133.42 -2203.82,-99.93 -2176.63,-73.96 C-2149.45,-47.99 -2114.39,-29.53 -2075,-17 C-2060.09,-12.26 -2044.98,-8.59 -2029.65,-6.43 C-2014.33,-4.28 -1998.78,-3.65 -1983,-5 C-1951.72,-7.67 -1925.53,-16 -1903.13,-28.62 C-1880.73,-41.24 -1862.11,-58.16 -1846,-78 C-1830.98,-96.49 -1817.15,-119.48 -1808.74,-146.6 C-1800.33,-173.72 -1797.34,-204.98 -1804,-240 C-1809.39,-268.37 -1820.04,-293.46 -1834.27,-314.92 C-1848.5,-336.37 -1866.3,-354.19 -1886,-368 C-1896.13,-375.1 -1907.27,-380.86 -1919.42,-385.96 C-1931.58,-391.07 -1944.76,-395.51 -1959,-400 C-1987.83,-409.09 -2018.76,-415.73 -2052,-413c " + android:valueTo="M-2052 -413 C-2068.16,-411.67 -2083.44,-408.39 -2097.42,-403.83 C-2111.4,-399.27 -2124.07,-393.43 -2135,-387 C-2147.19,-379.82 -2157.4,-372.54 -2166.58,-364.44 C-2175.76,-356.34 -2183.92,-347.44 -2192,-337 C-2206.01,-318.91 -2219.24,-294.36 -2226.98,-265.73 C-2234.71,-237.1 -2236.95,-204.4 -2229,-170 C-2222.57,-142.18 -2211.58,-117.82 -2196.58,-96.96 C-2181.57,-76.11 -2162.54,-58.77 -2140,-45 C-2129.22,-38.41 -2117.43,-32.95 -2104.72,-27.98 C-2092.02,-23.02 -2078.4,-18.56 -2064,-14 C-2048.86,-9.2 -2033.94,-5.15 -2018.76,-2.7 C-2003.58,-0.26 -1988.15,0.6 -1972,-1 C-1909.86,-7.14 -1864.77,-35.48 -1833,-76 C-1803.02,-114.23 -1778.89,-172.33 -1794,-243 C-1806.16,-299.87 -1840.21,-341.67 -1882,-369 C-1902.73,-382.55 -1929.67,-390.89 -1958,-400 C-1986.85,-409.28 -2018.94,-415.71 -2052,-413c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="133" + android:valueFrom="M-2052 -413 C-2068.16,-411.67 -2083.44,-408.39 -2097.42,-403.83 C-2111.4,-399.27 -2124.07,-393.43 -2135,-387 C-2147.19,-379.82 -2157.4,-372.54 -2166.58,-364.44 C-2175.76,-356.34 -2183.92,-347.44 -2192,-337 C-2206.01,-318.91 -2219.24,-294.36 -2226.98,-265.73 C-2234.71,-237.1 -2236.95,-204.4 -2229,-170 C-2222.57,-142.18 -2211.58,-117.82 -2196.58,-96.96 C-2181.57,-76.11 -2162.54,-58.77 -2140,-45 C-2129.22,-38.41 -2117.43,-32.95 -2104.72,-27.98 C-2092.02,-23.02 -2078.4,-18.56 -2064,-14 C-2048.86,-9.2 -2033.94,-5.15 -2018.76,-2.7 C-2003.58,-0.26 -1988.15,0.6 -1972,-1 C-1909.86,-7.14 -1864.77,-35.48 -1833,-76 C-1803.02,-114.23 -1778.89,-172.33 -1794,-243 C-1806.16,-299.87 -1840.21,-341.67 -1882,-369 C-1902.73,-382.55 -1929.67,-390.89 -1958,-400 C-1986.85,-409.28 -2018.94,-415.71 -2052,-413c " + android:valueTo="M-2052 -413 C-2068.7,-411.63 -2084.13,-408.26 -2098.16,-403.54 C-2112.2,-398.82 -2124.85,-392.76 -2136,-386 C-2147.98,-378.74 -2158.36,-371.57 -2167.81,-363.43 C-2177.26,-355.28 -2185.77,-346.16 -2194,-335 C-2207.5,-316.7 -2220.59,-290.87 -2228.01,-261.03 C-2235.43,-231.2 -2237.18,-197.35 -2228,-163 C-2220.91,-136.45 -2208.67,-112.16 -2192.46,-91.28 C-2176.24,-70.4 -2156.04,-52.92 -2133,-40 C-2121.53,-33.57 -2108.74,-28.52 -2095.21,-23.98 C-2081.68,-19.44 -2067.42,-15.41 -2053,-11 C-2036.87,-6.07 -2022.2,-1.69 -2007.14,1.02 C-1992.09,3.72 -1976.65,4.76 -1959,3 C-1895.61,-3.31 -1849.91,-32.53 -1818,-74 C-1787.82,-113.22 -1765.01,-174.6 -1783,-245 C-1797.41,-301.38 -1831.8,-344.78 -1876,-370 C-1898.81,-383.01 -1927.34,-389.73 -1956,-399 C-1985.38,-408.5 -2017.46,-415.84 -2052,-413c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="150" + android:valueFrom="M-2052 -413 C-2068.7,-411.63 -2084.13,-408.26 -2098.16,-403.54 C-2112.2,-398.82 -2124.85,-392.76 -2136,-386 C-2147.98,-378.74 -2158.36,-371.57 -2167.81,-363.43 C-2177.26,-355.28 -2185.77,-346.16 -2194,-335 C-2207.5,-316.7 -2220.59,-290.87 -2228.01,-261.03 C-2235.43,-231.2 -2237.18,-197.35 -2228,-163 C-2220.91,-136.45 -2208.67,-112.16 -2192.46,-91.28 C-2176.24,-70.4 -2156.04,-52.92 -2133,-40 C-2121.53,-33.57 -2108.74,-28.52 -2095.21,-23.98 C-2081.68,-19.44 -2067.42,-15.41 -2053,-11 C-2036.87,-6.07 -2022.2,-1.69 -2007.14,1.02 C-1992.09,3.72 -1976.65,4.76 -1959,3 C-1895.61,-3.31 -1849.91,-32.53 -1818,-74 C-1787.82,-113.22 -1765.01,-174.6 -1783,-245 C-1797.41,-301.38 -1831.8,-344.78 -1876,-370 C-1898.81,-383.01 -1927.34,-389.73 -1956,-399 C-1985.38,-408.5 -2017.46,-415.84 -2052,-413c " + android:valueTo="M-2052 -413 C-2081.79,-410.55 -2107.78,-401.91 -2129.84,-389.82 C-2151.89,-377.73 -2169.99,-362.21 -2184,-346 C-2197.87,-329.95 -2211.62,-310.23 -2221.28,-286 C-2230.94,-261.78 -2236.5,-233.06 -2234,-199 C-2231.84,-169.67 -2223.83,-144.4 -2212.24,-122.59 C-2200.65,-100.79 -2185.48,-82.45 -2169,-67 C-2151.26,-50.36 -2130.31,-38.88 -2107.26,-29.54 C-2084.22,-20.21 -2059.09,-13.03 -2033,-5 C-2017.74,-0.3 -2004.84,3.33 -1991.59,5.6 C-1978.33,7.86 -1964.71,8.77 -1948,8 C-1905.51,6.05 -1868.9,-9.21 -1839.73,-31.86 C-1810.55,-54.51 -1788.79,-84.54 -1776,-116 C-1771.1,-128.04 -1767.01,-141.61 -1764.31,-155.76 C-1761.61,-169.9 -1760.31,-184.63 -1761,-199 C-1765.31,-289.18 -1818.08,-347.02 -1882,-375 C-1905.42,-385.25 -1933.09,-392.49 -1960,-401 C-1987.46,-409.69 -2016.78,-415.89 -2052,-413c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="167" + android:valueFrom="M-2052 -413 C-2081.79,-410.55 -2107.78,-401.91 -2129.84,-389.82 C-2151.89,-377.73 -2169.99,-362.21 -2184,-346 C-2197.87,-329.95 -2211.62,-310.23 -2221.28,-286 C-2230.94,-261.78 -2236.5,-233.06 -2234,-199 C-2231.84,-169.67 -2223.83,-144.4 -2212.24,-122.59 C-2200.65,-100.79 -2185.48,-82.45 -2169,-67 C-2151.26,-50.36 -2130.31,-38.88 -2107.26,-29.54 C-2084.22,-20.21 -2059.09,-13.03 -2033,-5 C-2017.74,-0.3 -2004.84,3.33 -1991.59,5.6 C-1978.33,7.86 -1964.71,8.77 -1948,8 C-1905.51,6.05 -1868.9,-9.21 -1839.73,-31.86 C-1810.55,-54.51 -1788.79,-84.54 -1776,-116 C-1771.1,-128.04 -1767.01,-141.61 -1764.31,-155.76 C-1761.61,-169.9 -1760.31,-184.63 -1761,-199 C-1765.31,-289.18 -1818.08,-347.02 -1882,-375 C-1905.42,-385.25 -1933.09,-392.49 -1960,-401 C-1987.46,-409.69 -2016.78,-415.89 -2052,-413c " + android:valueTo="M-2052 -413 C-2069.88,-411.53 -2085.89,-407.88 -2100.3,-402.79 C-2114.71,-397.7 -2127.52,-391.19 -2139,-384 C-2162.8,-369.1 -2183.13,-350.89 -2198.84,-328.39 C-2214.56,-305.89 -2225.66,-279.09 -2231,-247 C-2234.03,-228.79 -2234.51,-211.38 -2232.87,-194.73 C-2231.22,-178.08 -2227.46,-162.19 -2222,-147 C-2212.49,-120.53 -2198.25,-96.52 -2179.64,-76.38 C-2161.02,-56.24 -2138.03,-39.97 -2111,-29 C-2097.39,-23.47 -2083.02,-18.98 -2068.36,-14.74 C-2053.7,-10.5 -2038.75,-6.51 -2024,-2 C-2009.05,2.58 -1994.19,7.3 -1978.25,10.39 C-1962.31,13.48 -1945.29,14.94 -1926,13 C-1858.97,6.24 -1812.71,-25.63 -1781,-71 C-1751.71,-112.9 -1731.01,-182.53 -1755,-252 C-1774.66,-308.93 -1813.12,-347.39 -1866,-370 C-1891.58,-380.93 -1922.29,-388.48 -1952,-398 C-1982.5,-407.77 -2015.77,-415.97 -2052,-413c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="183" + android:valueFrom="M-2052 -413 C-2069.88,-411.53 -2085.89,-407.88 -2100.3,-402.79 C-2114.71,-397.7 -2127.52,-391.19 -2139,-384 C-2162.8,-369.1 -2183.13,-350.89 -2198.84,-328.39 C-2214.56,-305.89 -2225.66,-279.09 -2231,-247 C-2234.03,-228.79 -2234.51,-211.38 -2232.87,-194.73 C-2231.22,-178.08 -2227.46,-162.19 -2222,-147 C-2212.49,-120.53 -2198.25,-96.52 -2179.64,-76.38 C-2161.02,-56.24 -2138.03,-39.97 -2111,-29 C-2097.39,-23.47 -2083.02,-18.98 -2068.36,-14.74 C-2053.7,-10.5 -2038.75,-6.51 -2024,-2 C-2009.05,2.58 -1994.19,7.3 -1978.25,10.39 C-1962.31,13.48 -1945.29,14.94 -1926,13 C-1858.97,6.24 -1812.71,-25.63 -1781,-71 C-1751.71,-112.9 -1731.01,-182.53 -1755,-252 C-1774.66,-308.93 -1813.12,-347.39 -1866,-370 C-1891.58,-380.93 -1922.29,-388.48 -1952,-398 C-1982.5,-407.77 -2015.77,-415.97 -2052,-413c " + android:valueTo="M-2052 -413 C-2070.21,-411.5 -2086.7,-407.63 -2101.51,-402.32 C-2116.33,-397.01 -2129.47,-390.26 -2141,-383 C-2164.73,-368.06 -2185.58,-349.25 -2201.57,-325.58 C-2217.56,-301.91 -2228.69,-273.38 -2233,-239 C-2235.24,-221.1 -2234.79,-202.88 -2232.26,-185.49 C-2229.72,-168.1 -2225.1,-151.55 -2219,-137 C-2208.09,-110.99 -2192.21,-87.72 -2171.75,-68.46 C-2151.29,-49.2 -2126.25,-33.96 -2097,-24 C-2082.29,-18.99 -2067.22,-14.21 -2052.11,-9.58 C-2037.01,-4.94 -2021.86,-0.45 -2007,4 C-1992.53,8.33 -1976.87,12.99 -1960.04,16.12 C-1943.21,19.24 -1925.19,20.83 -1906,19 C-1837.8,12.51 -1789.31,-21.44 -1758,-68 C-1727.51,-113.34 -1711.25,-189.69 -1738,-256 C-1760.5,-311.77 -1803.61,-350.74 -1859,-370 C-1888.1,-380.12 -1918.72,-387.3 -1950,-397 C-1981.82,-406.86 -2013.69,-416.14 -2052,-413c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="200" + android:valueFrom="M-2052 -413 C-2070.21,-411.5 -2086.7,-407.63 -2101.51,-402.32 C-2116.33,-397.01 -2129.47,-390.26 -2141,-383 C-2164.73,-368.06 -2185.58,-349.25 -2201.57,-325.58 C-2217.56,-301.91 -2228.69,-273.38 -2233,-239 C-2235.24,-221.1 -2234.79,-202.88 -2232.26,-185.49 C-2229.72,-168.1 -2225.1,-151.55 -2219,-137 C-2208.09,-110.99 -2192.21,-87.72 -2171.75,-68.46 C-2151.29,-49.2 -2126.25,-33.96 -2097,-24 C-2082.29,-18.99 -2067.22,-14.21 -2052.11,-9.58 C-2037.01,-4.94 -2021.86,-0.45 -2007,4 C-1992.53,8.33 -1976.87,12.99 -1960.04,16.12 C-1943.21,19.24 -1925.19,20.83 -1906,19 C-1837.8,12.51 -1789.31,-21.44 -1758,-68 C-1727.51,-113.34 -1711.25,-189.69 -1738,-256 C-1760.5,-311.77 -1803.61,-350.74 -1859,-370 C-1888.1,-380.12 -1918.72,-387.3 -1950,-397 C-1981.82,-406.86 -2013.69,-416.14 -2052,-413c " + android:valueTo="M-2048 -413 C-2066.96,-411.52 -2083.65,-407.87 -2098.63,-402.66 C-2113.62,-397.46 -2126.89,-390.7 -2139,-383 C-2163.5,-367.42 -2184.61,-348.88 -2200.66,-325.26 C-2216.71,-301.64 -2227.71,-272.93 -2232,-237 C-2234.19,-218.68 -2233.63,-200.16 -2230.93,-182.61 C-2228.24,-165.06 -2223.39,-148.47 -2217,-134 C-2205.27,-107.47 -2188.65,-84.32 -2167.4,-65.37 C-2146.15,-46.43 -2120.26,-31.69 -2090,-22 C-2075,-17.19 -2059.6,-12.47 -2044.27,-7.81 C-2028.95,-3.14 -2013.69,1.45 -1999,6 C-1983.7,10.73 -1967.89,16.01 -1951.43,20.09 C-1934.97,24.17 -1917.86,27.05 -1900,27 C-1823.07,26.78 -1770.47,-11.8 -1738,-57 C-1720.71,-81.07 -1708.33,-109.31 -1703,-145 C-1691.18,-224.09 -1728.76,-287.55 -1768,-323 C-1810.65,-361.52 -1879.25,-375.58 -1942,-395 C-1973.74,-404.82 -2008.85,-416.06 -2048,-413c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="217" + android:valueFrom="M-2048 -413 C-2066.96,-411.52 -2083.65,-407.87 -2098.63,-402.66 C-2113.62,-397.46 -2126.89,-390.7 -2139,-383 C-2163.5,-367.42 -2184.61,-348.88 -2200.66,-325.26 C-2216.71,-301.64 -2227.71,-272.93 -2232,-237 C-2234.19,-218.68 -2233.63,-200.16 -2230.93,-182.61 C-2228.24,-165.06 -2223.39,-148.47 -2217,-134 C-2205.27,-107.47 -2188.65,-84.32 -2167.4,-65.37 C-2146.15,-46.43 -2120.26,-31.69 -2090,-22 C-2075,-17.19 -2059.6,-12.47 -2044.27,-7.81 C-2028.95,-3.14 -2013.69,1.45 -1999,6 C-1983.7,10.73 -1967.89,16.01 -1951.43,20.09 C-1934.97,24.17 -1917.86,27.05 -1900,27 C-1823.07,26.78 -1770.47,-11.8 -1738,-57 C-1720.71,-81.07 -1708.33,-109.31 -1703,-145 C-1691.18,-224.09 -1728.76,-287.55 -1768,-323 C-1810.65,-361.52 -1879.25,-375.58 -1942,-395 C-1973.74,-404.82 -2008.85,-416.06 -2048,-413c " + android:valueTo="M-2044 -412 C-2063.62,-410.55 -2080.88,-406.72 -2096.49,-401.17 C-2112.09,-395.62 -2126.03,-388.34 -2139,-380 C-2164.03,-363.91 -2185.97,-343.3 -2202.06,-317.15 C-2218.15,-290.99 -2228.39,-259.29 -2230,-221 C-2231.65,-181.81 -2222.43,-147.58 -2207.28,-119.14 C-2192.13,-90.7 -2171.06,-68.05 -2149,-52 C-2136.68,-43.04 -2123.52,-36.2 -2109.14,-30.27 C-2094.77,-24.34 -2079.18,-19.32 -2062,-14 C-2046.23,-9.11 -2030.66,-4.02 -2014.94,1.05 C-1999.21,6.13 -1983.35,11.18 -1967,16 C-1950.6,20.83 -1933.88,26.02 -1916.12,29.66 C-1898.36,33.3 -1879.56,35.38 -1859,34 C-1785.73,29.09 -1734.33,-12.76 -1704,-61 C-1687.14,-87.82 -1677.54,-119.8 -1675,-157 C-1669.82,-232.93 -1712.56,-294.98 -1754,-326 C-1800.38,-360.72 -1874.96,-373.95 -1937,-393 C-1970.55,-403.3 -2003.97,-414.96 -2044,-412c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="233" + android:valueFrom="M-2044 -412 C-2063.62,-410.55 -2080.88,-406.72 -2096.49,-401.17 C-2112.09,-395.62 -2126.03,-388.34 -2139,-380 C-2164.03,-363.91 -2185.97,-343.3 -2202.06,-317.15 C-2218.15,-290.99 -2228.39,-259.29 -2230,-221 C-2231.65,-181.81 -2222.43,-147.58 -2207.28,-119.14 C-2192.13,-90.7 -2171.06,-68.05 -2149,-52 C-2136.68,-43.04 -2123.52,-36.2 -2109.14,-30.27 C-2094.77,-24.34 -2079.18,-19.32 -2062,-14 C-2046.23,-9.11 -2030.66,-4.02 -2014.94,1.05 C-1999.21,6.13 -1983.35,11.18 -1967,16 C-1950.6,20.83 -1933.88,26.02 -1916.12,29.66 C-1898.36,33.3 -1879.56,35.38 -1859,34 C-1785.73,29.09 -1734.33,-12.76 -1704,-61 C-1687.14,-87.82 -1677.54,-119.8 -1675,-157 C-1669.82,-232.93 -1712.56,-294.98 -1754,-326 C-1800.38,-360.72 -1874.96,-373.95 -1937,-393 C-1970.55,-403.3 -2003.97,-414.96 -2044,-412c " + android:valueTo="M-2043 -410 C-2062.47,-408.33 -2080.12,-404.05 -2096.08,-397.95 C-2112.04,-391.85 -2126.3,-383.94 -2139,-375 C-2163.84,-357.52 -2185.95,-334.8 -2201.41,-306.44 C-2216.88,-278.08 -2225.71,-244.07 -2224,-204 C-2222.32,-164.72 -2211.27,-131.77 -2194.29,-104.74 C-2177.32,-77.71 -2154.41,-56.59 -2129,-41 C-2115.9,-32.96 -2101.29,-26.6 -2085.64,-20.93 C-2069.99,-15.26 -2053.29,-10.28 -2036,-5 C-2019.94,-0.1 -2004.01,4.86 -1987.69,9.87 C-1971.37,14.87 -1954.65,19.92 -1937,25 C-1905.8,33.99 -1865.86,45.81 -1827,42 C-1752.62,34.71 -1698.64,-7.41 -1670,-59 C-1654,-87.82 -1644.06,-123.59 -1645,-163 C-1645.93,-201.81 -1658.75,-235.41 -1675,-262 C-1714,-325.81 -1760.88,-340.83 -1833,-362 C-1865.62,-371.58 -1898.36,-381.3 -1932,-392 C-1966.74,-403.05 -2001.4,-413.56 -2043,-410c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="250" + android:valueFrom="M-2043 -410 C-2062.47,-408.33 -2080.12,-404.05 -2096.08,-397.95 C-2112.04,-391.85 -2126.3,-383.94 -2139,-375 C-2163.84,-357.52 -2185.95,-334.8 -2201.41,-306.44 C-2216.88,-278.08 -2225.71,-244.07 -2224,-204 C-2222.32,-164.72 -2211.27,-131.77 -2194.29,-104.74 C-2177.32,-77.71 -2154.41,-56.59 -2129,-41 C-2115.9,-32.96 -2101.29,-26.6 -2085.64,-20.93 C-2069.99,-15.26 -2053.29,-10.28 -2036,-5 C-2019.94,-0.1 -2004.01,4.86 -1987.69,9.87 C-1971.37,14.87 -1954.65,19.92 -1937,25 C-1905.8,33.99 -1865.86,45.81 -1827,42 C-1752.62,34.71 -1698.64,-7.41 -1670,-59 C-1654,-87.82 -1644.06,-123.59 -1645,-163 C-1645.93,-201.81 -1658.75,-235.41 -1675,-262 C-1714,-325.81 -1760.88,-340.83 -1833,-362 C-1865.62,-371.58 -1898.36,-381.3 -1932,-392 C-1966.74,-403.05 -2001.4,-413.56 -2043,-410c " + android:valueTo="M-2028 -408 C-2048.69,-406.56 -2067.62,-402.42 -2084.53,-396.46 C-2101.45,-390.49 -2116.35,-382.71 -2129,-374 C-2153.83,-356.9 -2177.14,-333.29 -2193.47,-303.62 C-2209.79,-273.96 -2219.13,-238.26 -2216,-197 C-2212.99,-157.39 -2201.12,-124.14 -2183.05,-97.04 C-2164.99,-69.95 -2140.74,-49 -2113,-34 C-2099.07,-26.47 -2082.86,-20.28 -2065.99,-14.7 C-2049.12,-9.11 -2031.58,-4.13 -2015,1 C-1998.21,6.19 -1981.3,11.15 -1964.16,16.08 C-1947.03,21.01 -1929.67,25.91 -1912,31 C-1876.63,41.19 -1842.13,53.72 -1799,52 C-1720.79,48.87 -1665.28,1.83 -1636,-51 C-1619.77,-80.28 -1609.29,-118.72 -1612,-158 C-1617.51,-237.84 -1661.13,-292.87 -1714,-322 C-1743.39,-338.2 -1774.63,-344.74 -1813,-356 C-1847.11,-366.01 -1880.41,-376.12 -1915,-387 C-1949.7,-397.91 -1987.46,-410.81 -2028,-408c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="267" + android:valueFrom="M-2028 -408 C-2048.69,-406.56 -2067.62,-402.42 -2084.53,-396.46 C-2101.45,-390.49 -2116.35,-382.71 -2129,-374 C-2153.83,-356.9 -2177.14,-333.29 -2193.47,-303.62 C-2209.79,-273.96 -2219.13,-238.26 -2216,-197 C-2212.99,-157.39 -2201.12,-124.14 -2183.05,-97.04 C-2164.99,-69.95 -2140.74,-49 -2113,-34 C-2099.07,-26.47 -2082.86,-20.28 -2065.99,-14.7 C-2049.12,-9.11 -2031.58,-4.13 -2015,1 C-1998.21,6.19 -1981.3,11.15 -1964.16,16.08 C-1947.03,21.01 -1929.67,25.91 -1912,31 C-1876.63,41.19 -1842.13,53.72 -1799,52 C-1720.79,48.87 -1665.28,1.83 -1636,-51 C-1619.77,-80.28 -1609.29,-118.72 -1612,-158 C-1617.51,-237.84 -1661.13,-292.87 -1714,-322 C-1743.39,-338.2 -1774.63,-344.74 -1813,-356 C-1847.11,-366.01 -1880.41,-376.12 -1915,-387 C-1949.7,-397.91 -1987.46,-410.81 -2028,-408c " + android:valueTo="M-2009 -405 C-2051.87,-404.67 -2087.83,-392.14 -2116.71,-372.95 C-2145.6,-353.77 -2167.42,-327.93 -2182,-301 C-2190.39,-285.5 -2196.97,-267.54 -2201.03,-248.43 C-2205.1,-229.32 -2206.65,-209.07 -2205,-189 C-2201.67,-148.43 -2188.4,-114.91 -2168.6,-87.92 C-2148.8,-60.93 -2122.46,-40.46 -2093,-26 C-2077.81,-18.55 -2060.68,-12.37 -2043.04,-6.78 C-2025.39,-1.18 -2007.24,3.84 -1990,9 C-1972.83,14.14 -1955.59,19.23 -1938.01,24.37 C-1920.43,29.51 -1902.51,34.69 -1884,40 C-1848.91,50.07 -1808.39,64.36 -1769,63 C-1685.56,60.11 -1628.04,14.47 -1598,-41 C-1581.7,-71.11 -1570.85,-111.53 -1574,-152 C-1580.19,-231.59 -1628.54,-288.29 -1685,-316 C-1717.08,-331.74 -1751.19,-337.82 -1790,-349 C-1827.01,-359.66 -1860.79,-370.76 -1895,-381 C-1930.29,-391.56 -1967.46,-405.32 -2009,-405c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="283" + android:valueFrom="M-2009 -405 C-2051.87,-404.67 -2087.83,-392.14 -2116.71,-372.95 C-2145.6,-353.77 -2167.42,-327.93 -2182,-301 C-2190.39,-285.5 -2196.97,-267.54 -2201.03,-248.43 C-2205.1,-229.32 -2206.65,-209.07 -2205,-189 C-2201.67,-148.43 -2188.4,-114.91 -2168.6,-87.92 C-2148.8,-60.93 -2122.46,-40.46 -2093,-26 C-2077.81,-18.55 -2060.68,-12.37 -2043.04,-6.78 C-2025.39,-1.18 -2007.24,3.84 -1990,9 C-1972.83,14.14 -1955.59,19.23 -1938.01,24.37 C-1920.43,29.51 -1902.51,34.69 -1884,40 C-1848.91,50.07 -1808.39,64.36 -1769,63 C-1685.56,60.11 -1628.04,14.47 -1598,-41 C-1581.7,-71.11 -1570.85,-111.53 -1574,-152 C-1580.19,-231.59 -1628.54,-288.29 -1685,-316 C-1717.08,-331.74 -1751.19,-337.82 -1790,-349 C-1827.01,-359.66 -1860.79,-370.76 -1895,-381 C-1930.29,-391.56 -1967.46,-405.32 -2009,-405c " + android:valueTo="M-2011 -400 C-2053.42,-396.52 -2087.52,-381.94 -2114.34,-361.15 C-2141.17,-340.36 -2160.71,-313.35 -2174,-285 C-2181.72,-268.54 -2187.51,-250.33 -2190.47,-230.84 C-2193.44,-211.34 -2193.58,-190.57 -2190,-169 C-2183.7,-131.08 -2168.49,-98.81 -2147.08,-72.91 C-2125.67,-47.01 -2098.06,-27.47 -2067,-15 C-2050.55,-8.4 -2033.29,-3.15 -2015.51,1.83 C-1997.74,6.81 -1979.46,11.51 -1961,17 C-1926.67,27.21 -1890.41,37.86 -1854,48 C-1817.75,58.1 -1779.46,72.79 -1741,74 C-1651.6,76.8 -1593.42,34.23 -1560,-21 C-1542.31,-50.24 -1529.38,-88.39 -1530,-129 C-1531.26,-212.05 -1581.77,-272.22 -1635,-301 C-1665.71,-317.6 -1706.86,-326.2 -1739,-335 C-1775.31,-344.94 -1811.03,-355.54 -1846,-366 C-1882.13,-376.81 -1917.72,-389.72 -1954,-397 C-1969.82,-400.17 -1990.77,-401.66 -2011,-400c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="300" + android:valueFrom="M-2011 -400 C-2053.42,-396.52 -2087.52,-381.94 -2114.34,-361.15 C-2141.17,-340.36 -2160.71,-313.35 -2174,-285 C-2181.72,-268.54 -2187.51,-250.33 -2190.47,-230.84 C-2193.44,-211.34 -2193.58,-190.57 -2190,-169 C-2183.7,-131.08 -2168.49,-98.81 -2147.08,-72.91 C-2125.67,-47.01 -2098.06,-27.47 -2067,-15 C-2050.55,-8.4 -2033.29,-3.15 -2015.51,1.83 C-1997.74,6.81 -1979.46,11.51 -1961,17 C-1926.67,27.21 -1890.41,37.86 -1854,48 C-1817.75,58.1 -1779.46,72.79 -1741,74 C-1651.6,76.8 -1593.42,34.23 -1560,-21 C-1542.31,-50.24 -1529.38,-88.39 -1530,-129 C-1531.26,-212.05 -1581.77,-272.22 -1635,-301 C-1665.71,-317.6 -1706.86,-326.2 -1739,-335 C-1775.31,-344.94 -1811.03,-355.54 -1846,-366 C-1882.13,-376.81 -1917.72,-389.72 -1954,-397 C-1969.82,-400.17 -1990.77,-401.66 -2011,-400c " + android:valueTo="M-1993 -395 C-2035.52,-391.51 -2071.23,-376.75 -2099.59,-354.5 C-2127.94,-332.24 -2148.93,-302.49 -2162,-269 C-2168.66,-251.95 -2173.35,-230.87 -2174.9,-208.84 C-2176.46,-186.81 -2174.88,-163.83 -2169,-143 C-2158.02,-104.12 -2139.35,-74.22 -2113.89,-51.11 C-2088.42,-28.01 -2056.16,-11.7 -2018,0 C-1979.52,11.8 -1942.29,22.92 -1904.79,33.78 C-1867.3,44.64 -1829.54,55.24 -1790,66 C-1770.25,71.37 -1750.38,77.31 -1729.76,81.43 C-1709.14,85.56 -1687.76,87.87 -1665,86 C-1622.2,82.49 -1586.43,67.71 -1558.12,45.7 C-1529.81,23.68 -1508.96,-5.56 -1496,-38 C-1488.42,-56.98 -1484.03,-78.42 -1482.85,-100.14 C-1481.68,-121.87 -1483.72,-143.88 -1489,-164 C-1508.99,-240.16 -1563.51,-287.94 -1639,-308 C-1716.8,-328.67 -1792.02,-350.18 -1868,-373 C-1906.44,-384.54 -1946.52,-398.82 -1993,-395c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="317" + android:valueFrom="M-1993 -395 C-2035.52,-391.51 -2071.23,-376.75 -2099.59,-354.5 C-2127.94,-332.24 -2148.93,-302.49 -2162,-269 C-2168.66,-251.95 -2173.35,-230.87 -2174.9,-208.84 C-2176.46,-186.81 -2174.88,-163.83 -2169,-143 C-2158.02,-104.12 -2139.35,-74.22 -2113.89,-51.11 C-2088.42,-28.01 -2056.16,-11.7 -2018,0 C-1979.52,11.8 -1942.29,22.92 -1904.79,33.78 C-1867.3,44.64 -1829.54,55.24 -1790,66 C-1770.25,71.37 -1750.38,77.31 -1729.76,81.43 C-1709.14,85.56 -1687.76,87.87 -1665,86 C-1622.2,82.49 -1586.43,67.71 -1558.12,45.7 C-1529.81,23.68 -1508.96,-5.56 -1496,-38 C-1488.42,-56.98 -1484.03,-78.42 -1482.85,-100.14 C-1481.68,-121.87 -1483.72,-143.88 -1489,-164 C-1508.99,-240.16 -1563.51,-287.94 -1639,-308 C-1716.8,-328.67 -1792.02,-350.18 -1868,-373 C-1906.44,-384.54 -1946.52,-398.82 -1993,-395c " + android:valueTo="M-1972 -389 C-2015.53,-385.6 -2052.71,-370.1 -2082.05,-346.58 C-2111.38,-323.06 -2132.86,-291.51 -2145,-256 C-2148.29,-246.37 -2151.04,-236.62 -2152.97,-226.03 C-2154.89,-215.43 -2156,-204 -2156,-191 C-2156,-154.4 -2147.68,-123.61 -2134.39,-97.87 C-2121.1,-72.13 -2102.85,-51.43 -2083,-35 C-2068.94,-23.36 -2053.07,-14.7 -2035.52,-7.4 C-2017.98,-0.11 -1998.76,5.83 -1978,12 C-1938.06,23.88 -1899,35.2 -1859.7,46.27 C-1820.4,57.33 -1780.87,68.14 -1740,79 C-1699.56,89.74 -1657.14,104.77 -1609,101 C-1522.04,94.2 -1462.2,36.39 -1438,-32 C-1423.14,-74 -1423.94,-121.55 -1437,-161 C-1449.47,-198.68 -1467.69,-227.4 -1498,-253 C-1526.28,-276.89 -1561.4,-288.27 -1604,-299 C-1684.97,-319.39 -1764.33,-341.91 -1843,-365 C-1883.1,-376.77 -1923.46,-392.79 -1972,-389c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="333" + android:valueFrom="M-1972 -389 C-2015.53,-385.6 -2052.71,-370.1 -2082.05,-346.58 C-2111.38,-323.06 -2132.86,-291.51 -2145,-256 C-2148.29,-246.37 -2151.04,-236.62 -2152.97,-226.03 C-2154.89,-215.43 -2156,-204 -2156,-191 C-2156,-154.4 -2147.68,-123.61 -2134.39,-97.87 C-2121.1,-72.13 -2102.85,-51.43 -2083,-35 C-2068.94,-23.36 -2053.07,-14.7 -2035.52,-7.4 C-2017.98,-0.11 -1998.76,5.83 -1978,12 C-1938.06,23.88 -1899,35.2 -1859.7,46.27 C-1820.4,57.33 -1780.87,68.14 -1740,79 C-1699.56,89.74 -1657.14,104.77 -1609,101 C-1522.04,94.2 -1462.2,36.39 -1438,-32 C-1423.14,-74 -1423.94,-121.55 -1437,-161 C-1449.47,-198.68 -1467.69,-227.4 -1498,-253 C-1526.28,-276.89 -1561.4,-288.27 -1604,-299 C-1684.97,-319.39 -1764.33,-341.91 -1843,-365 C-1883.1,-376.77 -1923.46,-392.79 -1972,-389c " + android:valueTo="M-1947 -382 C-1992.63,-378.43 -2031.23,-362.21 -2061.25,-337.24 C-2091.27,-312.27 -2112.7,-278.55 -2124,-240 C-2138.21,-191.5 -2133.14,-145.62 -2117.43,-107.06 C-2101.71,-68.5 -2075.36,-37.25 -2047,-18 C-2031.95,-7.78 -2013.76,0.26 -1994.19,7.23 C-1974.61,14.19 -1953.63,20.08 -1933,26 C-1891.74,37.84 -1850.85,49.22 -1809.47,60.33 C-1768.09,71.44 -1726.22,82.27 -1683,93 C-1662.04,98.2 -1640.37,104.9 -1618.02,109.82 C-1595.68,114.74 -1572.65,117.89 -1549,116 C-1503.84,112.4 -1465.18,96.01 -1435.01,71.21 C-1404.85,46.41 -1383.17,13.21 -1372,-24 C-1358.2,-69.97 -1362.29,-119.5 -1378,-158 C-1393.11,-195.04 -1414.99,-223.76 -1448,-247 C-1479.65,-269.28 -1518.77,-277.61 -1564,-289 C-1647.07,-309.92 -1731.94,-331.77 -1814,-356 C-1855.02,-368.11 -1897.6,-385.86 -1947,-382c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="350" + android:valueFrom="M-1947 -382 C-1992.63,-378.43 -2031.23,-362.21 -2061.25,-337.24 C-2091.27,-312.27 -2112.7,-278.55 -2124,-240 C-2138.21,-191.5 -2133.14,-145.62 -2117.43,-107.06 C-2101.71,-68.5 -2075.36,-37.25 -2047,-18 C-2031.95,-7.78 -2013.76,0.26 -1994.19,7.23 C-1974.61,14.19 -1953.63,20.08 -1933,26 C-1891.74,37.84 -1850.85,49.22 -1809.47,60.33 C-1768.09,71.44 -1726.22,82.27 -1683,93 C-1662.04,98.2 -1640.37,104.9 -1618.02,109.82 C-1595.68,114.74 -1572.65,117.89 -1549,116 C-1503.84,112.4 -1465.18,96.01 -1435.01,71.21 C-1404.85,46.41 -1383.17,13.21 -1372,-24 C-1358.2,-69.97 -1362.29,-119.5 -1378,-158 C-1393.11,-195.04 -1414.99,-223.76 -1448,-247 C-1479.65,-269.28 -1518.77,-277.61 -1564,-289 C-1647.07,-309.92 -1731.94,-331.77 -1814,-356 C-1855.02,-368.11 -1897.6,-385.86 -1947,-382c " + android:valueTo="M-1915 -374 C-1963.11,-370.66 -2003.62,-353.89 -2034.73,-327.71 C-2065.84,-301.53 -2087.53,-265.95 -2098,-225 C-2111.65,-171.6 -2104.14,-125.11 -2085.35,-87.34 C-2066.56,-49.57 -2036.48,-20.52 -2005,-2 C-1987.6,8.23 -1967.69,15.77 -1946.7,22.23 C-1925.72,28.68 -1903.67,34.07 -1882,40 C-1828.45,54.66 -1774.32,69.41 -1719.51,83.62 C-1664.71,97.83 -1609.24,111.5 -1553,124 C-1540.64,126.75 -1528.83,129.45 -1517.02,131.24 C-1505.2,133.02 -1493.38,133.9 -1481,133 C-1431.84,129.42 -1391.55,112.94 -1360.82,87.22 C-1330.09,61.5 -1308.92,26.54 -1298,-14 C-1284.79,-63.02 -1292.08,-115.37 -1310,-153 C-1326.77,-188.23 -1354.33,-219.1 -1389,-239 C-1423.4,-258.74 -1470.69,-267.55 -1515,-278 C-1603.39,-298.85 -1690.13,-322.03 -1777,-346 C-1817.41,-357.15 -1865.58,-377.43 -1915,-374c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="367" + android:valueFrom="M-1915 -374 C-1963.11,-370.66 -2003.62,-353.89 -2034.73,-327.71 C-2065.84,-301.53 -2087.53,-265.95 -2098,-225 C-2111.65,-171.6 -2104.14,-125.11 -2085.35,-87.34 C-2066.56,-49.57 -2036.48,-20.52 -2005,-2 C-1987.6,8.23 -1967.69,15.77 -1946.7,22.23 C-1925.72,28.68 -1903.67,34.07 -1882,40 C-1828.45,54.66 -1774.32,69.41 -1719.51,83.62 C-1664.71,97.83 -1609.24,111.5 -1553,124 C-1540.64,126.75 -1528.83,129.45 -1517.02,131.24 C-1505.2,133.02 -1493.38,133.9 -1481,133 C-1431.84,129.42 -1391.55,112.94 -1360.82,87.22 C-1330.09,61.5 -1308.92,26.54 -1298,-14 C-1284.79,-63.02 -1292.08,-115.37 -1310,-153 C-1326.77,-188.23 -1354.33,-219.1 -1389,-239 C-1423.4,-258.74 -1470.69,-267.55 -1515,-278 C-1603.39,-298.85 -1690.13,-322.03 -1777,-346 C-1817.41,-357.15 -1865.58,-377.43 -1915,-374c " + android:valueTo="M-1890 -364 C-1915.49,-361.91 -1938.32,-355.57 -1958.37,-346.18 C-1978.42,-336.78 -1995.67,-324.33 -2010,-310 C-2024.52,-295.48 -2037.55,-279.77 -2047.93,-261.42 C-2058.31,-243.07 -2066.05,-222.08 -2070,-197 C-2074.59,-167.83 -2073.02,-142.02 -2067.58,-119.16 C-2062.15,-96.3 -2052.85,-76.38 -2042,-59 C-2030.6,-40.74 -2017.28,-25.07 -2001.72,-11.92 C-1986.16,1.22 -1968.36,11.84 -1948,20 C-1905.62,36.98 -1860,46.68 -1815,59 C-1725.92,83.38 -1631.46,106.29 -1538,128 C-1491.98,138.69 -1441.13,155.58 -1390,151 C-1292.44,142.26 -1225.72,75.29 -1210,-14 C-1200.46,-68.19 -1215.31,-117.16 -1237,-153 C-1258.29,-188.17 -1290.09,-216.37 -1330,-233 C-1350.41,-241.5 -1374.56,-245.91 -1398,-251 C-1516.15,-276.67 -1631.32,-305.3 -1744,-337 C-1787.58,-349.26 -1835.91,-368.44 -1890,-364c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="383" + android:valueFrom="M-1890 -364 C-1915.49,-361.91 -1938.32,-355.57 -1958.37,-346.18 C-1978.42,-336.78 -1995.67,-324.33 -2010,-310 C-2024.52,-295.48 -2037.55,-279.77 -2047.93,-261.42 C-2058.31,-243.07 -2066.05,-222.08 -2070,-197 C-2074.59,-167.83 -2073.02,-142.02 -2067.58,-119.16 C-2062.15,-96.3 -2052.85,-76.38 -2042,-59 C-2030.6,-40.74 -2017.28,-25.07 -2001.72,-11.92 C-1986.16,1.22 -1968.36,11.84 -1948,20 C-1905.62,36.98 -1860,46.68 -1815,59 C-1725.92,83.38 -1631.46,106.29 -1538,128 C-1491.98,138.69 -1441.13,155.58 -1390,151 C-1292.44,142.26 -1225.72,75.29 -1210,-14 C-1200.46,-68.19 -1215.31,-117.16 -1237,-153 C-1258.29,-188.17 -1290.09,-216.37 -1330,-233 C-1350.41,-241.5 -1374.56,-245.91 -1398,-251 C-1516.15,-276.67 -1631.32,-305.3 -1744,-337 C-1787.58,-349.26 -1835.91,-368.44 -1890,-364c " + android:valueTo="M-1841 -354 C-1869.44,-353.16 -1894.26,-347.31 -1915.8,-337.97 C-1937.35,-328.63 -1955.63,-315.8 -1971,-301 C-1986.17,-286.39 -1999.54,-271.29 -2010.18,-252.51 C-2020.82,-233.74 -2028.74,-211.3 -2033,-182 C-2041.16,-125.91 -2026.91,-77.42 -2000.49,-39.83 C-1974.07,-2.23 -1935.49,24.47 -1895,37 C-1871.46,44.28 -1847.67,50.61 -1823.78,56.71 C-1799.89,62.8 -1775.91,68.66 -1752,75 C-1632.18,106.77 -1509.29,135.8 -1384,161 C-1357.24,166.38 -1331.35,172.76 -1304,172 C-1248.4,170.45 -1205.4,147.31 -1175,120 C-1143.38,91.59 -1119.42,51.91 -1112,2 C-1103.47,-55.42 -1120.14,-106.53 -1145,-142 C-1169.17,-176.48 -1205.94,-205.29 -1250,-219 C-1273.89,-226.43 -1299.44,-229.88 -1324,-235 C-1449.37,-261.13 -1570.6,-289.56 -1691,-322 C-1735.34,-333.94 -1789.57,-355.52 -1841,-354c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="400" + android:valueFrom="M-1841 -354 C-1869.44,-353.16 -1894.26,-347.31 -1915.8,-337.97 C-1937.35,-328.63 -1955.63,-315.8 -1971,-301 C-1986.17,-286.39 -1999.54,-271.29 -2010.18,-252.51 C-2020.82,-233.74 -2028.74,-211.3 -2033,-182 C-2041.16,-125.91 -2026.91,-77.42 -2000.49,-39.83 C-1974.07,-2.23 -1935.49,24.47 -1895,37 C-1871.46,44.28 -1847.67,50.61 -1823.78,56.71 C-1799.89,62.8 -1775.91,68.66 -1752,75 C-1632.18,106.77 -1509.29,135.8 -1384,161 C-1357.24,166.38 -1331.35,172.76 -1304,172 C-1248.4,170.45 -1205.4,147.31 -1175,120 C-1143.38,91.59 -1119.42,51.91 -1112,2 C-1103.47,-55.42 -1120.14,-106.53 -1145,-142 C-1169.17,-176.48 -1205.94,-205.29 -1250,-219 C-1273.89,-226.43 -1299.44,-229.88 -1324,-235 C-1449.37,-261.13 -1570.6,-289.56 -1691,-322 C-1735.34,-333.94 -1789.57,-355.52 -1841,-354c " + android:valueTo="M-1796 -342 C-1811.94,-341.79 -1825.94,-339.83 -1838.82,-336.66 C-1851.71,-333.48 -1863.49,-329.09 -1875,-324 C-1907.54,-309.62 -1935.19,-288.4 -1955.51,-260 C-1975.82,-231.61 -1988.8,-196.05 -1992,-153 C-1994.25,-122.72 -1989.36,-95.65 -1980.35,-72.07 C-1971.34,-48.48 -1958.22,-28.37 -1944,-12 C-1928.17,6.22 -1910.48,19.99 -1890.22,30.95 C-1869.96,41.91 -1847.13,50.05 -1821,57 C-1732.48,80.56 -1643.53,102.75 -1552.93,123.58 C-1462.34,144.4 -1370.1,163.87 -1275,182 C-1247.01,187.34 -1218.58,194.81 -1191,194 C-1131.03,192.25 -1087.63,168.15 -1056,138 C-1024.96,108.42 -997.59,63.62 -995,7 C-992.15,-55.28 -1013.07,-100.38 -1043,-136 C-1072.69,-171.34 -1113.24,-194.03 -1167,-204 C-1328.4,-233.93 -1484.44,-268.19 -1638,-308 C-1687.14,-320.74 -1741.73,-342.71 -1796,-342c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="417" + android:valueFrom="M-1796 -342 C-1811.94,-341.79 -1825.94,-339.83 -1838.82,-336.66 C-1851.71,-333.48 -1863.49,-329.09 -1875,-324 C-1907.54,-309.62 -1935.19,-288.4 -1955.51,-260 C-1975.82,-231.61 -1988.8,-196.05 -1992,-153 C-1994.25,-122.72 -1989.36,-95.65 -1980.35,-72.07 C-1971.34,-48.48 -1958.22,-28.37 -1944,-12 C-1928.17,6.22 -1910.48,19.99 -1890.22,30.95 C-1869.96,41.91 -1847.13,50.05 -1821,57 C-1732.48,80.56 -1643.53,102.75 -1552.93,123.58 C-1462.34,144.4 -1370.1,163.87 -1275,182 C-1247.01,187.34 -1218.58,194.81 -1191,194 C-1131.03,192.25 -1087.63,168.15 -1056,138 C-1024.96,108.42 -997.59,63.62 -995,7 C-992.15,-55.28 -1013.07,-100.38 -1043,-136 C-1072.69,-171.34 -1113.24,-194.03 -1167,-204 C-1328.4,-233.93 -1484.44,-268.19 -1638,-308 C-1687.14,-320.74 -1741.73,-342.71 -1796,-342c " + android:valueTo="M-1758 -328 C-1773.72,-326.77 -1788.05,-324.1 -1801.28,-320.2 C-1814.52,-316.3 -1826.65,-311.16 -1838,-305 C-1867.83,-288.8 -1896.87,-263.63 -1916.93,-230.09 C-1936.99,-196.55 -1948.07,-154.65 -1942,-105 C-1938.26,-74.41 -1929.05,-49.04 -1916.25,-27.63 C-1903.46,-6.22 -1887.08,11.24 -1869,26 C-1849.22,42.16 -1826.74,52.66 -1802.07,61 C-1777.41,69.34 -1750.55,75.51 -1722,83 C-1627.76,107.73 -1531.68,130.26 -1433.57,150.84 C-1335.47,171.42 -1235.33,190.06 -1133,207 C-1102.48,212.05 -1071.97,219.42 -1041,217 C-980.71,212.29 -935.73,182.99 -906,149 C-876.19,114.92 -850.64,62 -858,-5 C-867.57,-92.16 -925.05,-149.77 -996,-173 C-1020.84,-181.13 -1049.84,-184.7 -1081,-190 C-1283.95,-224.53 -1479.06,-266.92 -1668,-316 C-1696.45,-323.39 -1726.55,-330.46 -1758,-328c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="433" + android:valueFrom="M-1758 -328 C-1773.72,-326.77 -1788.05,-324.1 -1801.28,-320.2 C-1814.52,-316.3 -1826.65,-311.16 -1838,-305 C-1867.83,-288.8 -1896.87,-263.63 -1916.93,-230.09 C-1936.99,-196.55 -1948.07,-154.65 -1942,-105 C-1938.26,-74.41 -1929.05,-49.04 -1916.25,-27.63 C-1903.46,-6.22 -1887.08,11.24 -1869,26 C-1849.22,42.16 -1826.74,52.66 -1802.07,61 C-1777.41,69.34 -1750.55,75.51 -1722,83 C-1627.76,107.73 -1531.68,130.26 -1433.57,150.84 C-1335.47,171.42 -1235.33,190.06 -1133,207 C-1102.48,212.05 -1071.97,219.42 -1041,217 C-980.71,212.29 -935.73,182.99 -906,149 C-876.19,114.92 -850.64,62 -858,-5 C-867.57,-92.16 -925.05,-149.77 -996,-173 C-1020.84,-181.13 -1049.84,-184.7 -1081,-190 C-1283.95,-224.53 -1479.06,-266.92 -1668,-316 C-1696.45,-323.39 -1726.55,-330.46 -1758,-328c " + android:valueTo="M-1702 -313 C-1718.97,-311.67 -1734.27,-308.63 -1748.31,-304.2 C-1762.34,-299.76 -1775.11,-293.92 -1787,-287 C-1818.73,-268.52 -1848.63,-238.43 -1867.4,-200.25 C-1886.16,-162.07 -1893.8,-115.81 -1881,-65 C-1873.66,-35.86 -1861.23,-11.42 -1844.97,9 C-1828.71,29.43 -1808.63,45.86 -1786,59 C-1763.78,71.9 -1735.76,80.93 -1706.18,88.48 C-1676.6,96.02 -1645.45,102.07 -1617,109 C-1514.25,134.02 -1409.51,156.86 -1302.58,177.39 C-1195.65,197.93 -1086.52,216.17 -975,232 C-941.5,236.76 -907.26,243.23 -874,241 C-807.88,236.57 -764.02,203.88 -733,164 C-703.96,126.67 -678.96,62.64 -696,-6 C-710.57,-64.7 -744.81,-104.43 -790,-131 C-838.34,-159.43 -903.19,-162.4 -968,-172 C-1188.55,-204.67 -1401.48,-249.48 -1605,-300 C-1635.42,-307.55 -1668.51,-315.62 -1702,-313c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="450" + android:valueFrom="M-1702 -313 C-1718.97,-311.67 -1734.27,-308.63 -1748.31,-304.2 C-1762.34,-299.76 -1775.11,-293.92 -1787,-287 C-1818.73,-268.52 -1848.63,-238.43 -1867.4,-200.25 C-1886.16,-162.07 -1893.8,-115.81 -1881,-65 C-1873.66,-35.86 -1861.23,-11.42 -1844.97,9 C-1828.71,29.43 -1808.63,45.86 -1786,59 C-1763.78,71.9 -1735.76,80.93 -1706.18,88.48 C-1676.6,96.02 -1645.45,102.07 -1617,109 C-1514.25,134.02 -1409.51,156.86 -1302.58,177.39 C-1195.65,197.93 -1086.52,216.17 -975,232 C-941.5,236.76 -907.26,243.23 -874,241 C-807.88,236.57 -764.02,203.88 -733,164 C-703.96,126.67 -678.96,62.64 -696,-6 C-710.57,-64.7 -744.81,-104.43 -790,-131 C-838.34,-159.43 -903.19,-162.4 -968,-172 C-1188.55,-204.67 -1401.48,-249.48 -1605,-300 C-1635.42,-307.55 -1668.51,-315.62 -1702,-313c " + android:valueTo="M-1629 -297 C-1647.68,-296 -1664.53,-292.94 -1679.91,-288.29 C-1695.29,-283.64 -1709.2,-277.39 -1722,-270 C-1735.13,-262.42 -1746.45,-254.66 -1756.69,-245.68 C-1766.94,-236.7 -1776.13,-226.49 -1785,-214 C-1801.02,-191.45 -1813.31,-160.96 -1818.44,-127.98 C-1823.58,-95 -1821.57,-59.52 -1809,-27 C-1798.02,1.4 -1782.25,25.85 -1762.24,45.72 C-1742.22,65.59 -1717.96,80.88 -1690,91 C-1661.64,101.26 -1627.93,107.27 -1595,115 C-1338.91,175.14 -1069.51,224.52 -786,256 C-749.41,260.06 -712.09,266.16 -678,265 C-602.8,262.44 -555.29,227.14 -523,183 C-491.08,139.36 -471.68,63.07 -497,-4 C-518,-59.64 -556.44,-100.48 -616,-122 C-645.9,-132.8 -681.94,-135 -718,-139 C-999.71,-170.28 -1271.16,-219.92 -1527,-280 C-1559.85,-287.71 -1594.62,-298.85 -1629,-297c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="467" + android:valueFrom="M-1629 -297 C-1647.68,-296 -1664.53,-292.94 -1679.91,-288.29 C-1695.29,-283.64 -1709.2,-277.39 -1722,-270 C-1735.13,-262.42 -1746.45,-254.66 -1756.69,-245.68 C-1766.94,-236.7 -1776.13,-226.49 -1785,-214 C-1801.02,-191.45 -1813.31,-160.96 -1818.44,-127.98 C-1823.58,-95 -1821.57,-59.52 -1809,-27 C-1798.02,1.4 -1782.25,25.85 -1762.24,45.72 C-1742.22,65.59 -1717.96,80.88 -1690,91 C-1661.64,101.26 -1627.93,107.27 -1595,115 C-1338.91,175.14 -1069.51,224.52 -786,256 C-749.41,260.06 -712.09,266.16 -678,265 C-602.8,262.44 -555.29,227.14 -523,183 C-491.08,139.36 -471.68,63.07 -497,-4 C-518,-59.64 -556.44,-100.48 -616,-122 C-645.9,-132.8 -681.94,-135 -718,-139 C-999.71,-170.28 -1271.16,-219.92 -1527,-280 C-1559.85,-287.71 -1594.62,-298.85 -1629,-297c " + android:valueTo="M-1554 -279 C-1573.95,-278.03 -1592.16,-274.76 -1608.9,-269.49 C-1625.63,-264.23 -1640.91,-256.96 -1655,-248 C-1681.11,-231.4 -1704.11,-209.28 -1720.58,-181.38 C-1737.06,-153.48 -1747,-119.78 -1747,-80 C-1747,-40.36 -1736.63,-6.11 -1720.07,22.22 C-1703.51,50.56 -1680.76,72.99 -1656,89 C-1642.79,97.54 -1627.94,103.93 -1611.81,109.23 C-1595.69,114.54 -1578.3,118.77 -1560,123 C-1243.1,196.29 -905.52,251.65 -545,279 C-503.41,282.15 -461.51,286.97 -421,286 C-342.32,284.12 -289.14,239.96 -258,189 C-241.23,161.57 -229.41,126.9 -229,88 C-228.57,48.09 -239.97,14.95 -257,-14 C-288.07,-66.84 -336.69,-106.64 -418,-113 C-495.07,-119.03 -576.44,-125.73 -659,-133 C-929.35,-156.79 -1195.73,-207.11 -1442,-260 C-1478.55,-267.85 -1516.39,-280.83 -1554,-279c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="483" + android:valueFrom="M-1554 -279 C-1573.95,-278.03 -1592.16,-274.76 -1608.9,-269.49 C-1625.63,-264.23 -1640.91,-256.96 -1655,-248 C-1681.11,-231.4 -1704.11,-209.28 -1720.58,-181.38 C-1737.06,-153.48 -1747,-119.78 -1747,-80 C-1747,-40.36 -1736.63,-6.11 -1720.07,22.22 C-1703.51,50.56 -1680.76,72.99 -1656,89 C-1642.79,97.54 -1627.94,103.93 -1611.81,109.23 C-1595.69,114.54 -1578.3,118.77 -1560,123 C-1243.1,196.29 -905.52,251.65 -545,279 C-503.41,282.15 -461.51,286.97 -421,286 C-342.32,284.12 -289.14,239.96 -258,189 C-241.23,161.57 -229.41,126.9 -229,88 C-228.57,48.09 -239.97,14.95 -257,-14 C-288.07,-66.84 -336.69,-106.64 -418,-113 C-495.07,-119.03 -576.44,-125.73 -659,-133 C-929.35,-156.79 -1195.73,-207.11 -1442,-260 C-1478.55,-267.85 -1516.39,-280.83 -1554,-279c " + android:valueTo="M-1475 -259 C-1497.94,-257.3 -1517.43,-252.76 -1534.68,-246.04 C-1551.94,-239.32 -1566.97,-230.42 -1581,-220 C-1607.62,-200.23 -1631.26,-172.32 -1645.92,-138.18 C-1660.58,-104.04 -1666.27,-63.68 -1657,-19 C-1649.07,19.22 -1632.37,51.34 -1608.87,76.59 C-1585.37,101.84 -1555.09,120.22 -1520,131 C-1502.34,136.42 -1483.19,140.77 -1463.28,144.83 C-1443.37,148.9 -1422.7,152.69 -1402,157 C-1023.46,235.88 -622.54,284.09 -179,298 C-155.11,298.75 -130.67,300.57 -108,299 C-25.73,293.3 33.98,241.56 61,181 C76.53,146.2 82.53,100.81 74,59 C58.4,-17.45 8.72,-69.55 -62,-92 C-97.88,-103.39 -148.34,-103 -195,-103 C-282.94,-103 -387.12,-111.21 -468,-117 C-774.59,-138.96 -1075.42,-182.91 -1350,-240 C-1389.82,-248.28 -1432.72,-262.13 -1475,-259c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="500" + android:valueFrom="M-1475 -259 C-1497.94,-257.3 -1517.43,-252.76 -1534.68,-246.04 C-1551.94,-239.32 -1566.97,-230.42 -1581,-220 C-1607.62,-200.23 -1631.26,-172.32 -1645.92,-138.18 C-1660.58,-104.04 -1666.27,-63.68 -1657,-19 C-1649.07,19.22 -1632.37,51.34 -1608.87,76.59 C-1585.37,101.84 -1555.09,120.22 -1520,131 C-1502.34,136.42 -1483.19,140.77 -1463.28,144.83 C-1443.37,148.9 -1422.7,152.69 -1402,157 C-1023.46,235.88 -622.54,284.09 -179,298 C-155.11,298.75 -130.67,300.57 -108,299 C-25.73,293.3 33.98,241.56 61,181 C76.53,146.2 82.53,100.81 74,59 C58.4,-17.45 8.72,-69.55 -62,-92 C-97.88,-103.39 -148.34,-103 -195,-103 C-282.94,-103 -387.12,-111.21 -468,-117 C-774.59,-138.96 -1075.42,-182.91 -1350,-240 C-1389.82,-248.28 -1432.72,-262.13 -1475,-259c " + android:valueTo="M-1373 -238 C-1397.37,-236.31 -1418.62,-231.33 -1437.53,-223.76 C-1456.44,-216.19 -1473,-206.04 -1488,-194 C-1503.21,-181.79 -1516.33,-168.45 -1527.22,-153.33 C-1538.1,-138.21 -1546.75,-121.31 -1553,-102 C-1568.58,-53.91 -1564.76,-8.07 -1550.21,30.82 C-1535.66,69.72 -1510.38,101.67 -1483,122 C-1452.82,144.41 -1414.34,154.7 -1368,164 C-1237.54,190.18 -1096.12,213.69 -965,233 C-643.35,280.36 -299.33,300 68,300 C120,300 172,299.45 222,296 C316.17,289.51 375.99,234.85 401,161 C433.46,65.17 388.09,-21.48 333,-63 C304.2,-84.71 265.24,-102.48 216,-103 C161,-103.58 109.46,-100 62,-100 C-42.22,-100 -151.92,-100.95 -248,-105 C-597.18,-119.72 -929.37,-158.41 -1236,-217 C-1278.46,-225.11 -1327.89,-241.13 -1373,-238c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="517" + android:valueFrom="M-1373 -238 C-1397.37,-236.31 -1418.62,-231.33 -1437.53,-223.76 C-1456.44,-216.19 -1473,-206.04 -1488,-194 C-1503.21,-181.79 -1516.33,-168.45 -1527.22,-153.33 C-1538.1,-138.21 -1546.75,-121.31 -1553,-102 C-1568.58,-53.91 -1564.76,-8.07 -1550.21,30.82 C-1535.66,69.72 -1510.38,101.67 -1483,122 C-1452.82,144.41 -1414.34,154.7 -1368,164 C-1237.54,190.18 -1096.12,213.69 -965,233 C-643.35,280.36 -299.33,300 68,300 C120,300 172,299.45 222,296 C316.17,289.51 375.99,234.85 401,161 C433.46,65.17 388.09,-21.48 333,-63 C304.2,-84.71 265.24,-102.48 216,-103 C161,-103.58 109.46,-100 62,-100 C-42.22,-100 -151.92,-100.95 -248,-105 C-597.18,-119.72 -929.37,-158.41 -1236,-217 C-1278.46,-225.11 -1327.89,-241.13 -1373,-238c " + android:valueTo="M-1265 -215 C-1290.3,-212.92 -1313.06,-206.47 -1333,-197.26 C-1352.93,-188.06 -1370.03,-176.1 -1384,-163 C-1398.71,-149.21 -1411.64,-133.82 -1422.06,-116.08 C-1432.48,-98.34 -1440.37,-78.24 -1445,-55 C-1455.6,-1.79 -1444.84,45.36 -1423.06,83.13 C-1401.29,120.91 -1368.49,149.3 -1335,165 C-1316.23,173.8 -1293.86,179.41 -1270.12,183.89 C-1246.37,188.37 -1221.26,191.72 -1197,196 C-1052.86,221.45 -899.36,243.11 -753,259 C-353.75,302.34 120.02,314.63 538,279 C633.75,270.84 700.5,206.17 718,121 C740.05,13.71 679.18,-65.56 609,-100 C589.87,-109.39 569.5,-116.07 544,-119 C520.02,-121.75 489.94,-117.74 464,-116 C409.15,-112.32 351.08,-109.29 304,-107 C-196.98,-82.59 -686.9,-120.79 -1115,-195 C-1161.65,-203.09 -1214.46,-219.15 -1265,-215c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="533" + android:valueFrom="M-1265 -215 C-1290.3,-212.92 -1313.06,-206.47 -1333,-197.26 C-1352.93,-188.06 -1370.03,-176.1 -1384,-163 C-1398.71,-149.21 -1411.64,-133.82 -1422.06,-116.08 C-1432.48,-98.34 -1440.37,-78.24 -1445,-55 C-1455.6,-1.79 -1444.84,45.36 -1423.06,83.13 C-1401.29,120.91 -1368.49,149.3 -1335,165 C-1316.23,173.8 -1293.86,179.41 -1270.12,183.89 C-1246.37,188.37 -1221.26,191.72 -1197,196 C-1052.86,221.45 -899.36,243.11 -753,259 C-353.75,302.34 120.02,314.63 538,279 C633.75,270.84 700.5,206.17 718,121 C740.05,13.71 679.18,-65.56 609,-100 C589.87,-109.39 569.5,-116.07 544,-119 C520.02,-121.75 489.94,-117.74 464,-116 C409.15,-112.32 351.08,-109.29 304,-107 C-196.98,-82.59 -686.9,-120.79 -1115,-195 C-1161.65,-203.09 -1214.46,-219.15 -1265,-215c " + android:valueTo="M-1118 -192 C-1145.42,-191.79 -1170.54,-186.13 -1192.5,-177.22 C-1214.45,-168.32 -1233.24,-156.17 -1248,-143 C-1263.27,-129.37 -1277,-113.05 -1288.05,-94.18 C-1299.1,-75.3 -1307.46,-53.87 -1312,-30 C-1332.86,79.71 -1267.54,162.8 -1189,193 C-1147.25,209.05 -1092.45,214.06 -1041,222 C-887.29,245.72 -722.44,264.69 -565,277 C-240.57,302.37 128.06,309.73 459,285 C570.39,276.68 679.51,267.39 784,256 C887.81,244.69 957.02,188.83 976,94 C987.3,37.57 972.22,-12.23 950,-49 C917.49,-102.8 862.33,-144.29 781,-145 C750.06,-145.27 727.56,-139.91 700,-137 C541.6,-120.26 360.16,-106.76 203,-103 C92.15,-100.34 -28.22,-98.3 -146,-102 C-465.5,-112.02 -757.79,-137.12 -1038,-183 C-1064.09,-187.27 -1095.31,-192.17 -1118,-192c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="550" + android:valueFrom="M-1118 -192 C-1145.42,-191.79 -1170.54,-186.13 -1192.5,-177.22 C-1214.45,-168.32 -1233.24,-156.17 -1248,-143 C-1263.27,-129.37 -1277,-113.05 -1288.05,-94.18 C-1299.1,-75.3 -1307.46,-53.87 -1312,-30 C-1332.86,79.71 -1267.54,162.8 -1189,193 C-1147.25,209.05 -1092.45,214.06 -1041,222 C-887.29,245.72 -722.44,264.69 -565,277 C-240.57,302.37 128.06,309.73 459,285 C570.39,276.68 679.51,267.39 784,256 C887.81,244.69 957.02,188.83 976,94 C987.3,37.57 972.22,-12.23 950,-49 C917.49,-102.8 862.33,-144.29 781,-145 C750.06,-145.27 727.56,-139.91 700,-137 C541.6,-120.26 360.16,-106.76 203,-103 C92.15,-100.34 -28.22,-98.3 -146,-102 C-465.5,-112.02 -757.79,-137.12 -1038,-183 C-1064.09,-187.27 -1095.31,-192.17 -1118,-192c " + android:valueTo="M970 -171 C943.29,-168.81 916.34,-163.76 889,-160 C702.71,-134.36 510.97,-116.08 311,-107 C84.92,-96.73 -172.89,-100.57 -391,-112 C-497.92,-117.6 -602.84,-127.1 -723,-140 C-773.04,-145.37 -825.42,-152.52 -883,-160 C-909.72,-163.47 -935.68,-168.58 -965,-168 C-996.16,-167.38 -1018.31,-161.2 -1040,-152 C-1115.75,-119.87 -1181.79,-30.18 -1153,82 C-1140.72,129.86 -1115.71,165.17 -1082,191 C-1045.71,218.81 -1002.91,227.92 -946,236 C-630.61,280.78 -288.29,300 71,300 C188.01,300 304.89,292.44 419,287 C585.2,279.08 754.78,261.17 912,240 C964.06,232.99 1024.03,228.96 1065,212 C1142.61,179.87 1208.86,90.77 1181,-21 C1169.65,-66.53 1143.86,-103.88 1110,-130 C1077.04,-155.43 1028.35,-175.79 970,-171c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="567" + android:valueFrom="M970 -171 C943.29,-168.81 916.34,-163.76 889,-160 C702.71,-134.36 510.97,-116.08 311,-107 C84.92,-96.73 -172.89,-100.57 -391,-112 C-497.92,-117.6 -602.84,-127.1 -723,-140 C-773.04,-145.37 -825.42,-152.52 -883,-160 C-909.72,-163.47 -935.68,-168.58 -965,-168 C-996.16,-167.38 -1018.31,-161.2 -1040,-152 C-1115.75,-119.87 -1181.79,-30.18 -1153,82 C-1140.72,129.86 -1115.71,165.17 -1082,191 C-1045.71,218.81 -1002.91,227.92 -946,236 C-630.61,280.78 -288.29,300 71,300 C188.01,300 304.89,292.44 419,287 C585.2,279.08 754.78,261.17 912,240 C964.06,232.99 1024.03,228.96 1065,212 C1142.61,179.87 1208.86,90.77 1181,-21 C1169.65,-66.53 1143.86,-103.88 1110,-130 C1077.04,-155.43 1028.35,-175.79 970,-171c " + android:valueTo="M1139 -198 C1112.96,-195.96 1086.75,-190.39 1060,-186 C724.42,-130.9 368.62,-100 -21,-100 C-206.52,-100 -352.82,-106.58 -538,-122 C-587.36,-126.11 -643.08,-130.48 -702,-137 C-729.04,-139.99 -755.08,-146.24 -784,-145 C-814,-143.72 -835.74,-137.28 -857,-128 C-930.63,-95.87 -1001.24,-8.83 -971,105 C-958.3,152.82 -933.55,187.66 -900,214 C-863.65,242.54 -822.66,251.49 -764,258 C-604.41,275.72 -434.69,289.25 -263,295 C-87.78,300.87 91.29,299.55 266,295 C381.34,291.99 494.86,283.26 604,274 C762.94,260.51 929.9,239.45 1083,215 C1134.03,206.85 1192.04,202.57 1233,185 C1309.97,151.99 1375.09,64.69 1348,-47 C1336.52,-94.34 1310.62,-129.78 1277,-156 C1244.09,-181.67 1197.01,-202.54 1139,-198c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="583" + android:valueFrom="M1139 -198 C1112.96,-195.96 1086.75,-190.39 1060,-186 C724.42,-130.9 368.62,-100 -21,-100 C-206.52,-100 -352.82,-106.58 -538,-122 C-587.36,-126.11 -643.08,-130.48 -702,-137 C-729.04,-139.99 -755.08,-146.24 -784,-145 C-814,-143.72 -835.74,-137.28 -857,-128 C-930.63,-95.87 -1001.24,-8.83 -971,105 C-958.3,152.82 -933.55,187.66 -900,214 C-863.65,242.54 -822.66,251.49 -764,258 C-604.41,275.72 -434.69,289.25 -263,295 C-87.78,300.87 91.29,299.55 266,295 C381.34,291.99 494.86,283.26 604,274 C762.94,260.51 929.9,239.45 1083,215 C1134.03,206.85 1192.04,202.57 1233,185 C1309.97,151.99 1375.09,64.69 1348,-47 C1336.52,-94.34 1310.62,-129.78 1277,-156 C1244.09,-181.67 1197.01,-202.54 1139,-198c " + android:valueTo="M1273 -223 C1260.98,-222.01 1248.39,-219.92 1235.6,-217.5 C1222.81,-215.07 1209.81,-212.32 1197,-210 C966.02,-168.25 716.41,-134.99 456.35,-116.17 C196.29,-97.35 -74.22,-92.98 -347,-109 C-371.45,-110.44 -398.42,-112.07 -426.29,-113.9 C-454.16,-115.73 -482.93,-117.76 -511,-120 C-538.24,-122.17 -566.8,-124.89 -593,-121 C-620.44,-116.92 -639.32,-109.31 -659,-99 C-728.07,-62.79 -787.77,29.83 -754,136 C-739.63,181.19 -715.06,213.22 -681,238 C-645.58,263.77 -604.66,274.8 -547,279 C-330.87,294.73 -102.21,301.44 131,299 C529.86,294.82 898.81,248.97 1245,188 C1293.44,179.47 1348.93,172.3 1385,153 C1454.2,115.97 1514.13,26.39 1482,-82 C1468.82,-126.48 1443.86,-160.23 1410,-185 C1376.3,-209.65 1329.97,-227.68 1273,-223c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="600" + android:valueFrom="M1273 -223 C1260.98,-222.01 1248.39,-219.92 1235.6,-217.5 C1222.81,-215.07 1209.81,-212.32 1197,-210 C966.02,-168.25 716.41,-134.99 456.35,-116.17 C196.29,-97.35 -74.22,-92.98 -347,-109 C-371.45,-110.44 -398.42,-112.07 -426.29,-113.9 C-454.16,-115.73 -482.93,-117.76 -511,-120 C-538.24,-122.17 -566.8,-124.89 -593,-121 C-620.44,-116.92 -639.32,-109.31 -659,-99 C-728.07,-62.79 -787.77,29.83 -754,136 C-739.63,181.19 -715.06,213.22 -681,238 C-645.58,263.77 -604.66,274.8 -547,279 C-330.87,294.73 -102.21,301.44 131,299 C529.86,294.82 898.81,248.97 1245,188 C1293.44,179.47 1348.93,172.3 1385,153 C1454.2,115.97 1514.13,26.39 1482,-82 C1468.82,-126.48 1443.86,-160.23 1410,-185 C1376.3,-209.65 1329.97,-227.68 1273,-223c " + android:valueTo="M1396 -247 C1384.29,-246.25 1372.12,-244.54 1359.84,-242.37 C1347.55,-240.21 1335.16,-237.59 1323,-235 C1242.35,-217.84 1159.48,-202.2 1075.58,-188.17 C991.68,-174.14 906.76,-161.72 822,-151 C647.45,-128.92 465.42,-113.12 278.92,-105.12 C92.42,-97.11 -98.55,-96.91 -291,-106 C-319.6,-107.35 -344.62,-105.15 -366.73,-99.83 C-388.84,-94.52 -408.04,-86.09 -425,-75 C-456,-54.72 -484.76,-22.13 -501.39,18.1 C-518.02,58.32 -522.52,106.19 -505,157 C-491.11,197.3 -467.19,230.65 -435,254 C-401.08,278.61 -361.34,290.67 -306,293 C-92.09,302.01 134.66,300.83 346,291 C720.7,273.57 1058.02,226.02 1386,161 C1433.15,151.65 1480.38,141.86 1513,121 C1574.49,81.68 1629.52,-8.18 1595,-111 C1569.5,-186.95 1497.99,-253.57 1396,-247c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="617" + android:valueFrom="M1396 -247 C1384.29,-246.25 1372.12,-244.54 1359.84,-242.37 C1347.55,-240.21 1335.16,-237.59 1323,-235 C1242.35,-217.84 1159.48,-202.2 1075.58,-188.17 C991.68,-174.14 906.76,-161.72 822,-151 C647.45,-128.92 465.42,-113.12 278.92,-105.12 C92.42,-97.11 -98.55,-96.91 -291,-106 C-319.6,-107.35 -344.62,-105.15 -366.73,-99.83 C-388.84,-94.52 -408.04,-86.09 -425,-75 C-456,-54.72 -484.76,-22.13 -501.39,18.1 C-518.02,58.32 -522.52,106.19 -505,157 C-491.11,197.3 -467.19,230.65 -435,254 C-401.08,278.61 -361.34,290.67 -306,293 C-92.09,302.01 134.66,300.83 346,291 C720.7,273.57 1058.02,226.02 1386,161 C1433.15,151.65 1480.38,141.86 1513,121 C1574.49,81.68 1629.52,-8.18 1595,-111 C1569.5,-186.95 1497.99,-253.57 1396,-247c " + android:valueTo="M1487 -268 C1475.75,-267.12 1464.42,-265.34 1453.07,-263.17 C1441.72,-261 1430.34,-258.44 1419,-256 C1408.2,-253.68 1397.48,-251.27 1386.82,-248.89 C1376.16,-246.52 1365.56,-244.17 1355,-242 C1158.69,-201.57 958.13,-168.1 748.31,-143.85 C538.48,-119.59 319.39,-104.56 86,-101 C64.01,-100.66 37.31,-101.63 10.2,-101.81 C-16.9,-102 -44.41,-101.42 -68,-98 C-92.39,-94.46 -112.22,-87.84 -129.29,-79.04 C-146.36,-70.24 -160.66,-59.26 -174,-47 C-201.16,-22.03 -221.68,10.56 -232,51 C-243.2,94.93 -237.31,146.32 -220,184 C-188.2,253.2 -124.98,300 -22,300 C554.96,300 1059.34,235.05 1531,130 C1615.03,111.29 1675.73,61.99 1696,-18 C1707.22,-62.26 1701.41,-113.05 1684,-151 C1653.53,-217.42 1586.39,-275.77 1487,-268c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="633" + android:valueFrom="M1487 -268 C1475.75,-267.12 1464.42,-265.34 1453.07,-263.17 C1441.72,-261 1430.34,-258.44 1419,-256 C1408.2,-253.68 1397.48,-251.27 1386.82,-248.89 C1376.16,-246.52 1365.56,-244.17 1355,-242 C1158.69,-201.57 958.13,-168.1 748.31,-143.85 C538.48,-119.59 319.39,-104.56 86,-101 C64.01,-100.66 37.31,-101.63 10.2,-101.81 C-16.9,-102 -44.41,-101.42 -68,-98 C-92.39,-94.46 -112.22,-87.84 -129.29,-79.04 C-146.36,-70.24 -160.66,-59.26 -174,-47 C-201.16,-22.03 -221.68,10.56 -232,51 C-243.2,94.93 -237.31,146.32 -220,184 C-188.2,253.2 -124.98,300 -22,300 C554.96,300 1059.34,235.05 1531,130 C1615.03,111.29 1675.73,61.99 1696,-18 C1707.22,-62.26 1701.41,-113.05 1684,-151 C1653.53,-217.42 1586.39,-275.77 1487,-268c " + android:valueTo="M1580 -288 C1569.02,-287.82 1558.33,-286.59 1547.73,-284.76 C1537.13,-282.94 1526.62,-280.53 1516,-278 C1329.96,-233.65 1137.53,-196.27 937.45,-167.41 C737.37,-138.56 529.64,-118.24 313,-108 C289.85,-106.91 266.93,-106.34 245.25,-104.34 C223.58,-102.33 203.15,-98.87 185,-92 C151.24,-79.21 121.91,-58.96 99.69,-31.9 C77.47,-4.85 62.35,29.01 57,69 C54.11,90.57 54.97,112.34 58.75,132.84 C62.53,153.33 69.22,172.54 78,189 C109.79,248.58 169.07,295 261,295 C352.67,295 445.29,286.74 535,280 C894.48,253 1223.58,200.63 1539,128 C1578.68,118.86 1619.85,111.36 1654,99 C1720.76,74.84 1771.83,17.2 1783,-61 C1789.05,-103.34 1779.11,-148.02 1762,-181 C1731.69,-239.43 1668.35,-289.44 1580,-288c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="650" + android:valueFrom="M1580 -288 C1569.02,-287.82 1558.33,-286.59 1547.73,-284.76 C1537.13,-282.94 1526.62,-280.53 1516,-278 C1329.96,-233.65 1137.53,-196.27 937.45,-167.41 C737.37,-138.56 529.64,-118.24 313,-108 C289.85,-106.91 266.93,-106.34 245.25,-104.34 C223.58,-102.33 203.15,-98.87 185,-92 C151.24,-79.21 121.91,-58.96 99.69,-31.9 C77.47,-4.85 62.35,29.01 57,69 C54.11,90.57 54.97,112.34 58.75,132.84 C62.53,153.33 69.22,172.54 78,189 C109.79,248.58 169.07,295 261,295 C352.67,295 445.29,286.74 535,280 C894.48,253 1223.58,200.63 1539,128 C1578.68,118.86 1619.85,111.36 1654,99 C1720.76,74.84 1771.83,17.2 1783,-61 C1789.05,-103.34 1779.11,-148.02 1762,-181 C1731.69,-239.43 1668.35,-289.44 1580,-288c " + android:valueTo="M1640 -305 C1621.47,-303.55 1603.27,-299.67 1585.17,-295.09 C1567.08,-290.52 1549.09,-285.24 1531,-281 C1477.75,-268.52 1423.74,-256.18 1369.55,-244.5 C1315.36,-232.82 1260.99,-221.81 1207,-212 C1111.62,-194.67 1018.32,-179.75 923.77,-166.49 C829.22,-153.24 733.41,-141.65 633,-131 C612.82,-128.86 592.3,-127.43 572.08,-125.88 C551.87,-124.33 531.96,-122.65 513,-120 C438.98,-109.65 388.59,-64.25 362,-9 C329.07,59.43 340.86,143.66 378,195 C413.52,244.1 470.86,281.96 558,278 C637.88,274.37 717.93,263.28 796,254 C1110.58,216.59 1403.92,162.28 1683,93 C1720.1,83.79 1748.74,73.39 1775,54 C1800,35.54 1820.18,12.52 1835,-17 C1869.4,-85.51 1855.22,-170.32 1819,-221 C1784.97,-268.62 1722.8,-311.47 1640,-305c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="667" + android:valueFrom="M1640 -305 C1621.47,-303.55 1603.27,-299.67 1585.17,-295.09 C1567.08,-290.52 1549.09,-285.24 1531,-281 C1477.75,-268.52 1423.74,-256.18 1369.55,-244.5 C1315.36,-232.82 1260.99,-221.81 1207,-212 C1111.62,-194.67 1018.32,-179.75 923.77,-166.49 C829.22,-153.24 733.41,-141.65 633,-131 C612.82,-128.86 592.3,-127.43 572.08,-125.88 C551.87,-124.33 531.96,-122.65 513,-120 C438.98,-109.65 388.59,-64.25 362,-9 C329.07,59.43 340.86,143.66 378,195 C413.52,244.1 470.86,281.96 558,278 C637.88,274.37 717.93,263.28 796,254 C1110.58,216.59 1403.92,162.28 1683,93 C1720.1,83.79 1748.74,73.39 1775,54 C1800,35.54 1820.18,12.52 1835,-17 C1869.4,-85.51 1855.22,-170.32 1819,-221 C1784.97,-268.62 1722.8,-311.47 1640,-305c " + android:valueTo="M1701 -321 C1684.31,-319.63 1667.92,-316.14 1651.65,-311.99 C1635.38,-307.84 1619.22,-303.03 1603,-299 C1476.28,-267.54 1347.03,-238.76 1214.58,-213.61 C1082.13,-188.46 946.5,-166.94 807,-150 C789.03,-147.82 771.91,-145.33 755.97,-141.75 C740.03,-138.18 725.27,-133.52 712,-127 C685.66,-114.05 663.18,-96.41 645.39,-74.45 C627.61,-52.5 614.53,-26.22 607,4 C597.72,41.22 599.94,76.18 608.87,106.85 C617.8,137.53 633.44,163.92 651,184 C686.76,224.89 741.19,258.64 818,252 C887.92,245.96 957.59,233.95 1026,224 C1266.28,189.04 1496.64,142.4 1714,85 C1747,76.29 1778.9,70.6 1805,58 C1856.52,33.13 1894.52,-10.44 1911,-72 C1930.34,-144.21 1903.92,-211.1 1869,-252 C1830.94,-296.58 1772.68,-326.88 1701,-321c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="683" + android:valueFrom="M1701 -321 C1684.31,-319.63 1667.92,-316.14 1651.65,-311.99 C1635.38,-307.84 1619.22,-303.03 1603,-299 C1476.28,-267.54 1347.03,-238.76 1214.58,-213.61 C1082.13,-188.46 946.5,-166.94 807,-150 C789.03,-147.82 771.91,-145.33 755.97,-141.75 C740.03,-138.18 725.27,-133.52 712,-127 C685.66,-114.05 663.18,-96.41 645.39,-74.45 C627.61,-52.5 614.53,-26.22 607,4 C597.72,41.22 599.94,76.18 608.87,106.85 C617.8,137.53 633.44,163.92 651,184 C686.76,224.89 741.19,258.64 818,252 C887.92,245.96 957.59,233.95 1026,224 C1266.28,189.04 1496.64,142.4 1714,85 C1747,76.29 1778.9,70.6 1805,58 C1856.52,33.13 1894.52,-10.44 1911,-72 C1930.34,-144.21 1903.92,-211.1 1869,-252 C1830.94,-296.58 1772.68,-326.88 1701,-321c " + android:valueTo="M1762 -336 C1746.19,-334.9 1731.2,-332.08 1716.53,-328.57 C1701.87,-325.06 1687.52,-320.86 1673,-317 C1574.29,-290.78 1473.48,-266.39 1370.87,-244.37 C1268.27,-222.35 1163.87,-202.72 1058,-186 C1025.18,-180.82 995.89,-176.47 969.45,-168.96 C943.01,-161.46 919.42,-150.8 898,-133 C879.29,-117.45 862.09,-98.59 849.04,-75.55 C836,-52.51 827.1,-25.29 825,7 C822.78,41.02 828.46,71.1 838.84,97.01 C849.23,122.92 864.34,144.66 881,162 C915.41,197.81 964.38,224.82 1035,222 C1064.82,220.81 1096.18,213.17 1127,208 C1342.04,171.91 1545.18,128.66 1743,78 C1802.17,62.85 1856.59,51.3 1897,19 C1934.73,-11.15 1965.34,-55.38 1971,-120 C1976.73,-185.43 1950.08,-240.04 1916,-276 C1883.45,-310.34 1827.33,-340.53 1762,-336c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="700" + android:valueFrom="M1762 -336 C1746.19,-334.9 1731.2,-332.08 1716.53,-328.57 C1701.87,-325.06 1687.52,-320.86 1673,-317 C1574.29,-290.78 1473.48,-266.39 1370.87,-244.37 C1268.27,-222.35 1163.87,-202.72 1058,-186 C1025.18,-180.82 995.89,-176.47 969.45,-168.96 C943.01,-161.46 919.42,-150.8 898,-133 C879.29,-117.45 862.09,-98.59 849.04,-75.55 C836,-52.51 827.1,-25.29 825,7 C822.78,41.02 828.46,71.1 838.84,97.01 C849.23,122.92 864.34,144.66 881,162 C915.41,197.81 964.38,224.82 1035,222 C1064.82,220.81 1096.18,213.17 1127,208 C1342.04,171.91 1545.18,128.66 1743,78 C1802.17,62.85 1856.59,51.3 1897,19 C1934.73,-11.15 1965.34,-55.38 1971,-120 C1976.73,-185.43 1950.08,-240.04 1916,-276 C1883.45,-310.34 1827.33,-340.53 1762,-336c " + android:valueTo="M1808 -349 C1793.7,-348.01 1780.06,-345.52 1766.69,-342.4 C1753.32,-339.27 1740.22,-335.52 1727,-332 C1713.99,-328.54 1700.95,-324.87 1688.18,-321.27 C1675.41,-317.68 1662.91,-314.16 1651,-311 C1572.35,-290.15 1494.41,-272.11 1415.73,-255.17 C1337.05,-238.24 1257.62,-222.42 1176,-206 C1149.31,-200.63 1126.51,-191.65 1106.78,-179.38 C1087.04,-167.11 1070.39,-151.55 1056,-133 C1027.49,-96.25 1007.2,-47.97 1014,14 C1019.89,67.66 1045.55,110.83 1080,141 C1113.35,170.21 1161.55,194.66 1223,190 C1277.44,185.87 1332.96,171.84 1387,161 C1548.03,128.69 1702.89,88.79 1854,48 C1905.15,34.19 1945.33,13.53 1975,-24 C2002.3,-58.53 2024.95,-108.71 2018,-172 C2012.18,-225.03 1984.89,-270 1952,-299 C1918.54,-328.51 1867.48,-353.12 1808,-349c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="717" + android:valueFrom="M1808 -349 C1793.7,-348.01 1780.06,-345.52 1766.69,-342.4 C1753.32,-339.27 1740.22,-335.52 1727,-332 C1713.99,-328.54 1700.95,-324.87 1688.18,-321.27 C1675.41,-317.68 1662.91,-314.16 1651,-311 C1572.35,-290.15 1494.41,-272.11 1415.73,-255.17 C1337.05,-238.24 1257.62,-222.42 1176,-206 C1149.31,-200.63 1126.51,-191.65 1106.78,-179.38 C1087.04,-167.11 1070.39,-151.55 1056,-133 C1027.49,-96.25 1007.2,-47.97 1014,14 C1019.89,67.66 1045.55,110.83 1080,141 C1113.35,170.21 1161.55,194.66 1223,190 C1277.44,185.87 1332.96,171.84 1387,161 C1548.03,128.69 1702.89,88.79 1854,48 C1905.15,34.19 1945.33,13.53 1975,-24 C2002.3,-58.53 2024.95,-108.71 2018,-172 C2012.18,-225.03 1984.89,-270 1952,-299 C1918.54,-328.51 1867.48,-353.12 1808,-349c " + android:valueTo="M1853 -361 C1826.42,-359.57 1802.2,-354.21 1778.74,-347.63 C1755.28,-341.06 1732.57,-333.27 1709,-327 C1663.45,-314.88 1616.15,-302.72 1568.63,-290.92 C1521.12,-279.12 1473.39,-267.68 1427,-257 C1404.39,-251.79 1378.86,-247.68 1354,-242.54 C1329.14,-237.41 1304.95,-231.27 1285,-222 C1248.81,-205.19 1214.39,-175.2 1192.48,-135.53 C1170.57,-95.86 1161.16,-46.51 1175,9 C1186.23,54.06 1211.95,90.39 1244,116 C1275.98,141.56 1320.49,162.26 1377,160 C1401.44,159.02 1426.8,152.54 1452,147 C1573.59,120.28 1692.02,92.2 1806,61 C1851.55,48.53 1902.21,39.28 1943,21 C1981.01,3.96 2014.53,-25.54 2035,-63 C2054.98,-99.57 2068.68,-155.45 2055,-210 C2043.49,-255.87 2017.77,-291.27 1986,-317 C1955.82,-341.45 1906.33,-363.87 1853,-361c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="733" + android:valueFrom="M1853 -361 C1826.42,-359.57 1802.2,-354.21 1778.74,-347.63 C1755.28,-341.06 1732.57,-333.27 1709,-327 C1663.45,-314.88 1616.15,-302.72 1568.63,-290.92 C1521.12,-279.12 1473.39,-267.68 1427,-257 C1404.39,-251.79 1378.86,-247.68 1354,-242.54 C1329.14,-237.41 1304.95,-231.27 1285,-222 C1248.81,-205.19 1214.39,-175.2 1192.48,-135.53 C1170.57,-95.86 1161.16,-46.51 1175,9 C1186.23,54.06 1211.95,90.39 1244,116 C1275.98,141.56 1320.49,162.26 1377,160 C1401.44,159.02 1426.8,152.54 1452,147 C1573.59,120.28 1692.02,92.2 1806,61 C1851.55,48.53 1902.21,39.28 1943,21 C1981.01,3.96 2014.53,-25.54 2035,-63 C2054.98,-99.57 2068.68,-155.45 2055,-210 C2043.49,-255.87 2017.77,-291.27 1986,-317 C1955.82,-341.45 1906.33,-363.87 1853,-361c " + android:valueTo="M1879 -371 C1855.84,-369.1 1833.88,-363.8 1812.31,-357.5 C1790.74,-351.2 1769.57,-343.9 1748,-338 C1705.55,-326.38 1662.66,-314.62 1619.58,-303.26 C1576.5,-291.91 1533.22,-280.97 1490,-271 C1477.96,-268.22 1466.77,-265.89 1456.19,-263.3 C1445.61,-260.7 1435.63,-257.84 1426,-254 C1397.93,-242.8 1372.88,-225.68 1352.62,-204.17 C1332.36,-182.66 1316.9,-156.75 1308,-128 C1301.29,-106.31 1298.77,-82.16 1300.06,-58.26 C1301.35,-34.35 1306.46,-10.7 1315,10 C1328.97,43.86 1354.44,76.47 1388.92,99.44 C1423.41,122.41 1466.93,135.73 1517,131 C1538.99,128.93 1561.43,122.33 1584,117 C1694.61,90.86 1801.5,62.81 1906,33 C1950.61,20.28 1990.32,9.42 2021,-15 C2077.17,-59.7 2121.6,-152.41 2082,-249 C2052.82,-320.16 1979.29,-379.23 1879,-371c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="750" + android:valueFrom="M1879 -371 C1855.84,-369.1 1833.88,-363.8 1812.31,-357.5 C1790.74,-351.2 1769.57,-343.9 1748,-338 C1705.55,-326.38 1662.66,-314.62 1619.58,-303.26 C1576.5,-291.91 1533.22,-280.97 1490,-271 C1477.96,-268.22 1466.77,-265.89 1456.19,-263.3 C1445.61,-260.7 1435.63,-257.84 1426,-254 C1397.93,-242.8 1372.88,-225.68 1352.62,-204.17 C1332.36,-182.66 1316.9,-156.75 1308,-128 C1301.29,-106.31 1298.77,-82.16 1300.06,-58.26 C1301.35,-34.35 1306.46,-10.7 1315,10 C1328.97,43.86 1354.44,76.47 1388.92,99.44 C1423.41,122.41 1466.93,135.73 1517,131 C1538.99,128.93 1561.43,122.33 1584,117 C1694.61,90.86 1801.5,62.81 1906,33 C1950.61,20.28 1990.32,9.42 2021,-15 C2077.17,-59.7 2121.6,-152.41 2082,-249 C2052.82,-320.16 1979.29,-379.23 1879,-371c " + android:valueTo="M1917 -381 C1894.47,-379.33 1874.18,-374.77 1854.43,-369.16 C1834.67,-363.54 1815.44,-356.87 1795,-351 C1775.41,-345.37 1755.69,-339.8 1735.98,-334.3 C1716.28,-328.8 1696.57,-323.37 1677,-318 C1655.71,-312.16 1635.71,-307.53 1615.91,-302.93 C1596.11,-298.33 1576.51,-293.75 1556,-288 C1518.95,-277.61 1486.57,-257.44 1461.65,-230.32 C1436.73,-203.2 1419.25,-169.14 1412,-131 C1407.72,-108.49 1407.85,-85.43 1411.23,-63.65 C1414.61,-41.87 1421.25,-21.38 1430,-4 C1445.1,25.99 1469.59,55.09 1501.94,75.77 C1534.28,96.45 1574.48,108.71 1621,105 C1662.49,101.69 1703.3,88.68 1744,78 C1824.94,56.76 1901.6,35.95 1980,12 C2055.14,-10.95 2109.6,-63.03 2125,-144 C2134.11,-191.88 2123.42,-237.96 2108,-270 C2078.07,-332.19 2008.91,-387.8 1917,-381c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="767" + android:valueFrom="M1917 -381 C1894.47,-379.33 1874.18,-374.77 1854.43,-369.16 C1834.67,-363.54 1815.44,-356.87 1795,-351 C1775.41,-345.37 1755.69,-339.8 1735.98,-334.3 C1716.28,-328.8 1696.57,-323.37 1677,-318 C1655.71,-312.16 1635.71,-307.53 1615.91,-302.93 C1596.11,-298.33 1576.51,-293.75 1556,-288 C1518.95,-277.61 1486.57,-257.44 1461.65,-230.32 C1436.73,-203.2 1419.25,-169.14 1412,-131 C1407.72,-108.49 1407.85,-85.43 1411.23,-63.65 C1414.61,-41.87 1421.25,-21.38 1430,-4 C1445.1,25.99 1469.59,55.09 1501.94,75.77 C1534.28,96.45 1574.48,108.71 1621,105 C1662.49,101.69 1703.3,88.68 1744,78 C1824.94,56.76 1901.6,35.95 1980,12 C2055.14,-10.95 2109.6,-63.03 2125,-144 C2134.11,-191.88 2123.42,-237.96 2108,-270 C2078.07,-332.19 2008.91,-387.8 1917,-381c " + android:valueTo="M1938 -389 C1918.02,-387.29 1899.13,-382.71 1880.56,-377.22 C1861.99,-371.74 1843.73,-365.34 1825,-360 C1807.04,-354.88 1788.76,-349.64 1770.36,-344.43 C1751.95,-339.21 1733.43,-334.02 1715,-329 C1695.02,-323.55 1676.29,-319.2 1658.55,-314.13 C1640.81,-309.06 1624.04,-303.29 1608,-295 C1579.39,-280.22 1552.9,-257.61 1533.32,-228.25 C1513.73,-198.88 1501.03,-162.77 1500,-121 C1499.48,-99.88 1502.66,-79.69 1508.25,-61.13 C1513.84,-42.57 1521.86,-25.63 1531,-11 C1565.04,43.45 1626.35,88.61 1719,81 C1756.71,77.9 1795.59,64.05 1832,54 C1869.38,43.69 1905.63,33.72 1942,23 C1979,12.1 2017.29,3.77 2048,-12 C2106.27,-41.91 2155.33,-100.64 2157,-185 C2157.86,-228.59 2143.96,-268.45 2127,-296 C2092.42,-352.19 2025.63,-396.5 1938,-389c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="783" + android:valueFrom="M1938 -389 C1918.02,-387.29 1899.13,-382.71 1880.56,-377.22 C1861.99,-371.74 1843.73,-365.34 1825,-360 C1807.04,-354.88 1788.76,-349.64 1770.36,-344.43 C1751.95,-339.21 1733.43,-334.02 1715,-329 C1695.02,-323.55 1676.29,-319.2 1658.55,-314.13 C1640.81,-309.06 1624.04,-303.29 1608,-295 C1579.39,-280.22 1552.9,-257.61 1533.32,-228.25 C1513.73,-198.88 1501.03,-162.77 1500,-121 C1499.48,-99.88 1502.66,-79.69 1508.25,-61.13 C1513.84,-42.57 1521.86,-25.63 1531,-11 C1565.04,43.45 1626.35,88.61 1719,81 C1756.71,77.9 1795.59,64.05 1832,54 C1869.38,43.69 1905.63,33.72 1942,23 C1979,12.1 2017.29,3.77 2048,-12 C2106.27,-41.91 2155.33,-100.64 2157,-185 C2157.86,-228.59 2143.96,-268.45 2127,-296 C2092.42,-352.19 2025.63,-396.5 1938,-389c " + android:valueTo="M1968 -397 C1948.07,-395.44 1929.72,-391.19 1912.16,-386.08 C1894.59,-380.97 1877.8,-375 1861,-370 C1843.57,-364.82 1826.22,-359.67 1809.03,-354.64 C1791.83,-349.62 1774.8,-344.71 1758,-340 C1738.06,-334.41 1721.02,-329.65 1705.61,-323.91 C1690.2,-318.18 1676.43,-311.48 1663,-302 C1651,-293.53 1639.65,-284.24 1629.09,-272.96 C1618.54,-261.69 1608.79,-248.42 1600,-232 C1582.06,-198.48 1576.15,-159.81 1579.3,-122.98 C1582.45,-86.15 1594.66,-51.16 1613,-25 C1648.02,24.96 1705.28,67.84 1791,61 C1828.15,58.04 1863.89,45.73 1898,36 C1932.04,26.29 1967.24,16.14 2001,6 C2034.31,-4 2068.94,-15.13 2095,-33 C2143.84,-66.49 2187.78,-128.14 2182,-212 C2179.47,-248.64 2164.22,-286.95 2148,-311 C2114.12,-361.25 2048.55,-403.3 1968,-397c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="800" + android:valueFrom="M1968 -397 C1948.07,-395.44 1929.72,-391.19 1912.16,-386.08 C1894.59,-380.97 1877.8,-375 1861,-370 C1843.57,-364.82 1826.22,-359.67 1809.03,-354.64 C1791.83,-349.62 1774.8,-344.71 1758,-340 C1738.06,-334.41 1721.02,-329.65 1705.61,-323.91 C1690.2,-318.18 1676.43,-311.48 1663,-302 C1651,-293.53 1639.65,-284.24 1629.09,-272.96 C1618.54,-261.69 1608.79,-248.42 1600,-232 C1582.06,-198.48 1576.15,-159.81 1579.3,-122.98 C1582.45,-86.15 1594.66,-51.16 1613,-25 C1648.02,24.96 1705.28,67.84 1791,61 C1828.15,58.04 1863.89,45.73 1898,36 C1932.04,26.29 1967.24,16.14 2001,6 C2034.31,-4 2068.94,-15.13 2095,-33 C2143.84,-66.49 2187.78,-128.14 2182,-212 C2179.47,-248.64 2164.22,-286.95 2148,-311 C2114.12,-361.25 2048.55,-403.3 1968,-397c " + android:valueTo="M1994 -404 C1974.92,-402.77 1957.48,-399.23 1940.77,-394.77 C1924.07,-390.3 1908.1,-384.92 1892,-380 C1875.77,-375.04 1859.61,-370.12 1843.46,-365.27 C1827.3,-360.42 1811.17,-355.65 1795,-351 C1762.37,-341.62 1734.85,-328.32 1712.13,-309.52 C1689.4,-290.71 1671.47,-266.4 1658,-235 C1643.1,-200.25 1640.2,-162.74 1645.36,-128.05 C1650.51,-93.35 1663.71,-61.48 1681,-38 C1697.9,-15.06 1720.48,6.55 1748.8,21.74 C1777.12,36.94 1811.17,45.72 1851,43 C1869.38,41.75 1886.68,38.15 1903.48,33.67 C1920.29,29.18 1936.6,23.81 1953,19 C1986.59,9.16 2019.21,-0.58 2050,-10 C2116.98,-30.5 2161.4,-66.4 2187,-126 C2200.41,-157.22 2207.11,-195.75 2202,-233 C2196.75,-271.26 2182.39,-298.2 2165,-322 C2131.47,-367.9 2073.87,-409.14 1994,-404c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="817" + android:valueFrom="M1994 -404 C1974.92,-402.77 1957.48,-399.23 1940.77,-394.77 C1924.07,-390.3 1908.1,-384.92 1892,-380 C1875.77,-375.04 1859.61,-370.12 1843.46,-365.27 C1827.3,-360.42 1811.17,-355.65 1795,-351 C1762.37,-341.62 1734.85,-328.32 1712.13,-309.52 C1689.4,-290.71 1671.47,-266.4 1658,-235 C1643.1,-200.25 1640.2,-162.74 1645.36,-128.05 C1650.51,-93.35 1663.71,-61.48 1681,-38 C1697.9,-15.06 1720.48,6.55 1748.8,21.74 C1777.12,36.94 1811.17,45.72 1851,43 C1869.38,41.75 1886.68,38.15 1903.48,33.67 C1920.29,29.18 1936.6,23.81 1953,19 C1986.59,9.16 2019.21,-0.58 2050,-10 C2116.98,-30.5 2161.4,-66.4 2187,-126 C2200.41,-157.22 2207.11,-195.75 2202,-233 C2196.75,-271.26 2182.39,-298.2 2165,-322 C2131.47,-367.9 2073.87,-409.14 1994,-404c " + android:valueTo="M2013 -410 C2003.61,-409.39 1994.32,-408.36 1985.41,-406.87 C1976.49,-405.39 1967.94,-403.44 1960,-401 C1936.86,-393.89 1913.03,-387.2 1889.78,-380.25 C1866.53,-373.31 1843.85,-366.11 1823,-358 C1795.57,-347.33 1771.25,-331.83 1751.32,-311.26 C1731.4,-290.68 1715.87,-265.02 1706,-234 C1694.29,-197.19 1694.75,-160.83 1702.28,-128.48 C1709.8,-96.13 1724.41,-67.79 1741,-47 C1758.18,-25.47 1780.08,-5.59 1807.49,8.35 C1834.89,22.29 1867.8,30.27 1907,28 C1923.88,27.02 1940.1,23.7 1956.12,19.41 C1972.14,15.12 1987.95,9.86 2004,5 C2019.65,0.27 2035.34,-4.2 2050.65,-8.88 C2065.96,-13.55 2080.89,-18.44 2095,-24 C2153.39,-47.02 2192.43,-90.22 2212,-148 C2235.67,-217.89 2212.62,-293.74 2180,-335 C2149.08,-374.11 2090.43,-414.98 2013,-410c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="833" + android:valueFrom="M2013 -410 C2003.61,-409.39 1994.32,-408.36 1985.41,-406.87 C1976.49,-405.39 1967.94,-403.44 1960,-401 C1936.86,-393.89 1913.03,-387.2 1889.78,-380.25 C1866.53,-373.31 1843.85,-366.11 1823,-358 C1795.57,-347.33 1771.25,-331.83 1751.32,-311.26 C1731.4,-290.68 1715.87,-265.02 1706,-234 C1694.29,-197.19 1694.75,-160.83 1702.28,-128.48 C1709.8,-96.13 1724.41,-67.79 1741,-47 C1758.18,-25.47 1780.08,-5.59 1807.49,8.35 C1834.89,22.29 1867.8,30.27 1907,28 C1923.88,27.02 1940.1,23.7 1956.12,19.41 C1972.14,15.12 1987.95,9.86 2004,5 C2019.65,0.27 2035.34,-4.2 2050.65,-8.88 C2065.96,-13.55 2080.89,-18.44 2095,-24 C2153.39,-47.02 2192.43,-90.22 2212,-148 C2235.67,-217.89 2212.62,-293.74 2180,-335 C2149.08,-374.11 2090.43,-414.98 2013,-410c " + android:valueTo="M2026 -415 C2008.21,-413.68 1992.19,-410.31 1976.88,-406.07 C1961.57,-401.83 1946.96,-396.74 1932,-392 C1916.81,-387.18 1901.5,-382.75 1887.03,-377.86 C1872.55,-372.97 1858.89,-367.63 1847,-361 C1823.18,-347.72 1801.58,-330.64 1784.24,-309.04 C1766.9,-287.45 1753.81,-261.34 1747,-230 C1738.93,-192.85 1741.7,-158.46 1750.72,-128.55 C1759.75,-98.64 1775.03,-73.21 1792,-54 C1809.62,-34.04 1831.33,-15.84 1858.28,-3.23 C1885.23,9.38 1917.42,16.4 1956,14 C1972.29,12.99 1987.98,9.55 2003.26,5.22 C2018.53,0.88 2033.39,-4.37 2048,-9 C2062.62,-13.63 2077.58,-18.1 2092.03,-23.05 C2106.48,-28 2120.42,-33.44 2133,-40 C2183.73,-66.48 2220.06,-111.51 2234,-170 C2251.17,-242.03 2221.03,-311.29 2190,-347 C2155.44,-386.78 2098.94,-420.39 2026,-415c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="850" + android:valueFrom="M2026 -415 C2008.21,-413.68 1992.19,-410.31 1976.88,-406.07 C1961.57,-401.83 1946.96,-396.74 1932,-392 C1916.81,-387.18 1901.5,-382.75 1887.03,-377.86 C1872.55,-372.97 1858.89,-367.63 1847,-361 C1823.18,-347.72 1801.58,-330.64 1784.24,-309.04 C1766.9,-287.45 1753.81,-261.34 1747,-230 C1738.93,-192.85 1741.7,-158.46 1750.72,-128.55 C1759.75,-98.64 1775.03,-73.21 1792,-54 C1809.62,-34.04 1831.33,-15.84 1858.28,-3.23 C1885.23,9.38 1917.42,16.4 1956,14 C1972.29,12.99 1987.98,9.55 2003.26,5.22 C2018.53,0.88 2033.39,-4.37 2048,-9 C2062.62,-13.63 2077.58,-18.1 2092.03,-23.05 C2106.48,-28 2120.42,-33.44 2133,-40 C2183.73,-66.48 2220.06,-111.51 2234,-170 C2251.17,-242.03 2221.03,-311.29 2190,-347 C2155.44,-386.78 2098.94,-420.39 2026,-415c " + android:valueTo="M2035 -419 C2018.03,-417.61 2002.83,-414.17 1988.22,-409.92 C1973.61,-405.68 1959.6,-400.62 1945,-396 C1929.99,-391.24 1915.88,-386.54 1902.83,-381.12 C1889.77,-375.71 1877.78,-369.59 1867,-362 C1846.17,-347.34 1826.91,-328.8 1811.85,-305.82 C1796.8,-282.85 1785.97,-255.42 1782,-223 C1777.51,-186.32 1782.26,-154.15 1792.5,-126.62 C1802.75,-99.08 1818.49,-76.18 1836,-58 C1853.47,-39.86 1875.92,-23.08 1903.02,-11.72 C1930.13,-0.36 1961.89,5.57 1998,2 C2013.61,0.46 2028.33,-2.98 2042.78,-7.18 C2057.24,-11.39 2071.43,-16.37 2086,-21 C2099.99,-25.45 2114.29,-30 2127.82,-35.29 C2141.34,-40.57 2154.1,-46.6 2165,-54 C2209.24,-84.04 2242.69,-131.41 2251,-192 C2261.06,-265.31 2230.86,-322.96 2198,-358 C2164.79,-393.41 2109.99,-425.16 2035,-419c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="867" + android:valueFrom="M2035 -419 C2018.03,-417.61 2002.83,-414.17 1988.22,-409.92 C1973.61,-405.68 1959.6,-400.62 1945,-396 C1929.99,-391.24 1915.88,-386.54 1902.83,-381.12 C1889.77,-375.71 1877.78,-369.59 1867,-362 C1846.17,-347.34 1826.91,-328.8 1811.85,-305.82 C1796.8,-282.85 1785.97,-255.42 1782,-223 C1777.51,-186.32 1782.26,-154.15 1792.5,-126.62 C1802.75,-99.08 1818.49,-76.18 1836,-58 C1853.47,-39.86 1875.92,-23.08 1903.02,-11.72 C1930.13,-0.36 1961.89,5.57 1998,2 C2013.61,0.46 2028.33,-2.98 2042.78,-7.18 C2057.24,-11.39 2071.43,-16.37 2086,-21 C2099.99,-25.45 2114.29,-30 2127.82,-35.29 C2141.34,-40.57 2154.1,-46.6 2165,-54 C2209.24,-84.04 2242.69,-131.41 2251,-192 C2261.06,-265.31 2230.86,-322.96 2198,-358 C2164.79,-393.41 2109.99,-425.16 2035,-419c " + android:valueTo="M2048 -423 C2034.52,-421.89 2023.13,-419.7 2012.3,-416.87 C2001.47,-414.05 1991.22,-410.61 1980,-407 C1969.12,-403.5 1958.34,-400.34 1948.04,-396.88 C1937.74,-393.43 1927.93,-389.67 1919,-385 C1899.26,-374.66 1883.06,-362.52 1869.52,-348.78 C1855.98,-335.04 1845.1,-319.71 1836,-303 C1826.17,-284.95 1818.62,-262.83 1814.8,-239.16 C1810.98,-215.48 1810.9,-190.26 1816,-166 C1824.81,-124.13 1845.68,-87.42 1875.54,-59.91 C1905.39,-32.39 1944.24,-14.08 1989,-9 C2015.48,-5.99 2038.19,-8.51 2059.92,-13.64 C2081.65,-18.77 2102.41,-26.52 2125,-34 C2162.45,-46.39 2197.45,-69.04 2223.09,-101.01 C2248.73,-132.98 2265,-174.29 2265,-224 C2265,-257.55 2257.85,-285.51 2246.41,-309.23 C2234.96,-332.95 2219.21,-352.42 2202,-369 C2168.19,-401.57 2116.15,-428.59 2048,-423c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="883" + android:valueFrom="M2048 -423 C2034.52,-421.89 2023.13,-419.7 2012.3,-416.87 C2001.47,-414.05 1991.22,-410.61 1980,-407 C1969.12,-403.5 1958.34,-400.34 1948.04,-396.88 C1937.74,-393.43 1927.93,-389.67 1919,-385 C1899.26,-374.66 1883.06,-362.52 1869.52,-348.78 C1855.98,-335.04 1845.1,-319.71 1836,-303 C1826.17,-284.95 1818.62,-262.83 1814.8,-239.16 C1810.98,-215.48 1810.9,-190.26 1816,-166 C1824.81,-124.13 1845.68,-87.42 1875.54,-59.91 C1905.39,-32.39 1944.24,-14.08 1989,-9 C2015.48,-5.99 2038.19,-8.51 2059.92,-13.64 C2081.65,-18.77 2102.41,-26.52 2125,-34 C2162.45,-46.39 2197.45,-69.04 2223.09,-101.01 C2248.73,-132.98 2265,-174.29 2265,-224 C2265,-257.55 2257.85,-285.51 2246.41,-309.23 C2234.96,-332.95 2219.21,-352.42 2202,-369 C2168.19,-401.57 2116.15,-428.59 2048,-423c " + android:valueTo="M2056 -426 C2040.73,-424.75 2025.49,-421.51 2010.94,-417.46 C1996.39,-413.4 1982.52,-408.53 1970,-404 C1956.36,-399.06 1944.24,-393.17 1933.34,-386.46 C1922.45,-379.75 1912.77,-372.22 1904,-364 C1886.94,-348 1870.57,-328.74 1858.28,-304.99 C1845.98,-281.24 1837.76,-252.98 1837,-219 C1836.25,-185.71 1842.79,-156.92 1854.12,-132.22 C1865.45,-107.52 1881.57,-86.92 1900,-70 C1917.59,-53.86 1939.31,-38.58 1965.28,-28.23 C1991.24,-17.87 2021.45,-12.45 2056,-16 C2069.73,-17.41 2083.86,-20.52 2098,-24.48 C2112.14,-28.43 2126.27,-33.23 2140,-38 C2177.19,-50.93 2209.98,-73.51 2233.83,-104.56 C2257.68,-135.61 2272.58,-175.15 2274,-222 C2275.03,-256.11 2267.97,-285.17 2256.56,-309.79 C2245.15,-334.42 2229.38,-354.62 2213,-371 C2179.81,-404.19 2126.45,-431.78 2056,-426c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="900" + android:valueFrom="M2056 -426 C2040.73,-424.75 2025.49,-421.51 2010.94,-417.46 C1996.39,-413.4 1982.52,-408.53 1970,-404 C1956.36,-399.06 1944.24,-393.17 1933.34,-386.46 C1922.45,-379.75 1912.77,-372.22 1904,-364 C1886.94,-348 1870.57,-328.74 1858.28,-304.99 C1845.98,-281.24 1837.76,-252.98 1837,-219 C1836.25,-185.71 1842.79,-156.92 1854.12,-132.22 C1865.45,-107.52 1881.57,-86.92 1900,-70 C1917.59,-53.86 1939.31,-38.58 1965.28,-28.23 C1991.24,-17.87 2021.45,-12.45 2056,-16 C2069.73,-17.41 2083.86,-20.52 2098,-24.48 C2112.14,-28.43 2126.27,-33.23 2140,-38 C2177.19,-50.93 2209.98,-73.51 2233.83,-104.56 C2257.68,-135.61 2272.58,-175.15 2274,-222 C2275.03,-256.11 2267.97,-285.17 2256.56,-309.79 C2245.15,-334.42 2229.38,-354.62 2213,-371 C2179.81,-404.19 2126.45,-431.78 2056,-426c " + android:valueTo="M2070 -429 C2054.11,-427.83 2039.39,-424.99 2025.5,-421.24 C2011.62,-417.49 1998.56,-412.83 1986,-408 C1972.35,-402.75 1960.84,-396.75 1950.5,-390.06 C1940.16,-383.37 1930.98,-375.99 1922,-368 C1904.86,-352.74 1888.86,-333.04 1877.15,-309 C1865.43,-284.96 1858,-256.59 1858,-224 C1858,-191.48 1864.22,-163.11 1874.96,-138.68 C1885.69,-114.24 1900.94,-93.75 1919,-77 C1936.46,-60.8 1957.91,-45.91 1983.4,-35.64 C2008.89,-25.36 2038.42,-19.71 2072,-22 C2085.63,-22.93 2099.7,-25.74 2113.57,-29.54 C2127.45,-33.34 2141.13,-38.13 2154,-43 C2188.92,-56.23 2220.88,-79.24 2244.25,-110.32 C2267.62,-141.39 2282.41,-180.53 2283,-226 C2283.44,-259.42 2276.49,-288.29 2265.28,-312.75 C2254.07,-337.2 2238.61,-357.24 2222,-373 C2189.33,-404.01 2137.11,-433.96 2070,-429c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="917" + android:valueFrom="M2070 -429 C2054.11,-427.83 2039.39,-424.99 2025.5,-421.24 C2011.62,-417.49 1998.56,-412.83 1986,-408 C1972.35,-402.75 1960.84,-396.75 1950.5,-390.06 C1940.16,-383.37 1930.98,-375.99 1922,-368 C1904.86,-352.74 1888.86,-333.04 1877.15,-309 C1865.43,-284.96 1858,-256.59 1858,-224 C1858,-191.48 1864.22,-163.11 1874.96,-138.68 C1885.69,-114.24 1900.94,-93.75 1919,-77 C1936.46,-60.8 1957.91,-45.91 1983.4,-35.64 C2008.89,-25.36 2038.42,-19.71 2072,-22 C2085.63,-22.93 2099.7,-25.74 2113.57,-29.54 C2127.45,-33.34 2141.13,-38.13 2154,-43 C2188.92,-56.23 2220.88,-79.24 2244.25,-110.32 C2267.62,-141.39 2282.41,-180.53 2283,-226 C2283.44,-259.42 2276.49,-288.29 2265.28,-312.75 C2254.07,-337.2 2238.61,-357.24 2222,-373 C2189.33,-404.01 2137.11,-433.96 2070,-429c " + android:valueTo="M2074 -431 C2067.27,-430.47 2061.91,-429.95 2057.16,-429.32 C2052.4,-428.69 2048.27,-427.96 2044,-427 C2016.54,-420.84 1992.45,-411.46 1971.52,-399.05 C1950.6,-386.65 1932.83,-371.23 1918,-353 C1905.73,-337.91 1892.68,-315.95 1883.98,-289.37 C1875.27,-262.79 1870.91,-231.58 1876,-198 C1880.07,-171.13 1888.38,-147.32 1900.64,-126.25 C1912.9,-105.19 1929.11,-86.87 1949,-71 C1967.14,-56.52 1990.42,-43.76 2016.89,-35.75 C2043.37,-27.74 2073.05,-24.48 2104,-29 C2131.69,-33.04 2156.62,-41.35 2178.66,-53 C2200.71,-64.65 2219.86,-79.63 2236,-97 C2251.9,-114.12 2266.06,-135.28 2275.66,-159.96 C2285.26,-184.64 2290.3,-212.83 2288,-244 C2285.7,-275.1 2278.29,-301.18 2267.16,-323.44 C2256.03,-345.7 2241.18,-364.14 2224,-380 C2192.75,-408.84 2138.58,-436.05 2074,-431c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="933" + android:valueFrom="M2074 -431 C2067.27,-430.47 2061.91,-429.95 2057.16,-429.32 C2052.4,-428.69 2048.27,-427.96 2044,-427 C2016.54,-420.84 1992.45,-411.46 1971.52,-399.05 C1950.6,-386.65 1932.83,-371.23 1918,-353 C1905.73,-337.91 1892.68,-315.95 1883.98,-289.37 C1875.27,-262.79 1870.91,-231.58 1876,-198 C1880.07,-171.13 1888.38,-147.32 1900.64,-126.25 C1912.9,-105.19 1929.11,-86.87 1949,-71 C1967.14,-56.52 1990.42,-43.76 2016.89,-35.75 C2043.37,-27.74 2073.05,-24.48 2104,-29 C2131.69,-33.04 2156.62,-41.35 2178.66,-53 C2200.71,-64.65 2219.86,-79.63 2236,-97 C2251.9,-114.12 2266.06,-135.28 2275.66,-159.96 C2285.26,-184.64 2290.3,-212.83 2288,-244 C2285.7,-275.1 2278.29,-301.18 2267.16,-323.44 C2256.03,-345.7 2241.18,-364.14 2224,-380 C2192.75,-408.84 2138.58,-436.05 2074,-431c " + android:valueTo="M2090 -433 C2079.15,-432.82 2068.99,-431.84 2059.32,-430.22 C2049.66,-428.59 2040.5,-426.32 2031.67,-423.56 C2022.84,-420.81 2014.34,-417.57 2006,-414 C1972.49,-399.68 1942.32,-376.89 1920.49,-346.2 C1898.66,-315.52 1885.16,-276.93 1885,-231 C1884.89,-199.3 1891.54,-172.16 1902.39,-148.76 C1913.24,-125.35 1928.3,-105.7 1945,-89 C1961.99,-72.01 1982.49,-57.13 2006.61,-46.69 C2030.74,-36.25 2058.5,-30.24 2090,-31 C2118.98,-31.69 2146.38,-38.09 2170.71,-48.49 C2195.04,-58.89 2216.3,-73.3 2233,-90 C2249.92,-106.92 2265.03,-126.77 2275.99,-150.22 C2286.94,-173.67 2293.73,-200.71 2294,-232 C2294.27,-263.44 2287.4,-291.32 2276.52,-315.27 C2265.64,-339.22 2250.75,-359.25 2235,-375 C2218.75,-391.25 2198.15,-406 2173.79,-416.57 C2149.42,-427.13 2121.3,-433.51 2090,-433c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="950" + android:valueFrom="M2090 -433 C2079.15,-432.82 2068.99,-431.84 2059.32,-430.22 C2049.66,-428.59 2040.5,-426.32 2031.67,-423.56 C2022.84,-420.81 2014.34,-417.57 2006,-414 C1972.49,-399.68 1942.32,-376.89 1920.49,-346.2 C1898.66,-315.52 1885.16,-276.93 1885,-231 C1884.89,-199.3 1891.54,-172.16 1902.39,-148.76 C1913.24,-125.35 1928.3,-105.7 1945,-89 C1961.99,-72.01 1982.49,-57.13 2006.61,-46.69 C2030.74,-36.25 2058.5,-30.24 2090,-31 C2118.98,-31.69 2146.38,-38.09 2170.71,-48.49 C2195.04,-58.89 2216.3,-73.3 2233,-90 C2249.92,-106.92 2265.03,-126.77 2275.99,-150.22 C2286.94,-173.67 2293.73,-200.71 2294,-232 C2294.27,-263.44 2287.4,-291.32 2276.52,-315.27 C2265.64,-339.22 2250.75,-359.25 2235,-375 C2218.75,-391.25 2198.15,-406 2173.79,-416.57 C2149.42,-427.13 2121.3,-433.51 2090,-433c " + android:valueTo="M2088 -434 C2073.56,-433.07 2058.65,-430.77 2044.55,-427.34 C2030.45,-423.92 2017.17,-419.39 2006,-414 C1994.71,-408.55 1983.93,-402.05 1974.11,-394.92 C1964.29,-387.78 1955.43,-380.01 1948,-372 C1932.68,-355.48 1918.25,-334.46 1907.99,-309.83 C1897.73,-285.2 1891.64,-256.96 1893,-226 C1894.35,-195.33 1901.36,-168.86 1912.41,-146.04 C1923.45,-123.22 1938.53,-104.06 1956,-88 C1973.16,-72.23 1994.54,-57.77 2019.07,-47.74 C2043.6,-37.7 2071.27,-32.08 2101,-34 C2130.44,-35.9 2156.54,-42.38 2179.6,-52.91 C2202.66,-63.43 2222.69,-77.98 2240,-96 C2256.04,-112.7 2270.94,-132.52 2281.5,-156.25 C2292.07,-179.98 2298.29,-207.63 2297,-240 C2295.78,-270.62 2288.45,-297.7 2277.35,-320.96 C2266.24,-344.22 2251.35,-363.66 2235,-379 C2201.84,-410.09 2151.36,-438.08 2088,-434c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="pathData" + android:startOffset="967" + android:valueFrom="M2088 -434 C2073.56,-433.07 2058.65,-430.77 2044.55,-427.34 C2030.45,-423.92 2017.17,-419.39 2006,-414 C1994.71,-408.55 1983.93,-402.05 1974.11,-394.92 C1964.29,-387.78 1955.43,-380.01 1948,-372 C1932.68,-355.48 1918.25,-334.46 1907.99,-309.83 C1897.73,-285.2 1891.64,-256.96 1893,-226 C1894.35,-195.33 1901.36,-168.86 1912.41,-146.04 C1923.45,-123.22 1938.53,-104.06 1956,-88 C1973.16,-72.23 1994.54,-57.77 2019.07,-47.74 C2043.6,-37.7 2071.27,-32.08 2101,-34 C2130.44,-35.9 2156.54,-42.38 2179.6,-52.91 C2202.66,-63.43 2222.69,-77.98 2240,-96 C2256.04,-112.7 2270.94,-132.52 2281.5,-156.25 C2292.07,-179.98 2298.29,-207.63 2297,-240 C2295.78,-270.62 2288.45,-297.7 2277.35,-320.96 C2266.24,-344.22 2251.35,-363.66 2235,-379 C2201.84,-410.09 2151.36,-438.08 2088,-434c " + android:valueTo="M2081 -434 C2061.85,-432.43 2043.71,-428.03 2026.99,-421.68 C2010.26,-415.32 1994.96,-407.01 1981.49,-397.61 C1968.02,-388.21 1956.39,-377.72 1947,-367 C1932.41,-350.35 1918.99,-329.51 1909.93,-304.5 C1900.87,-279.49 1896.16,-250.32 1899,-217 C1901.47,-187.99 1909.18,-162.6 1920.78,-140.53 C1932.39,-118.47 1947.91,-99.72 1966,-84 C1983.82,-68.52 2006.42,-55.12 2032.02,-46.29 C2057.61,-37.46 2086.2,-33.2 2116,-36 C2145.54,-38.78 2170.61,-46.49 2192.26,-58.02 C2213.91,-69.56 2232.15,-84.92 2248,-103 C2263.25,-120.39 2277.16,-141.16 2286.55,-165.69 C2295.94,-190.23 2300.82,-218.54 2298,-251 C2295.43,-280.64 2287.42,-306.3 2275.82,-328.47 C2264.21,-350.64 2248.99,-369.32 2232,-385 C2215.77,-399.98 2193.85,-413.53 2168.07,-422.69 C2142.29,-431.84 2112.66,-436.6 2081,-434c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="983" + android:pathData="M 50,50C 50,50 50,50 50,50" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="0"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:pathData="M 50,50C 50,50 50,50 50,50" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="983"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_N_1_T_1"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="983" + android:pathData="M 32,12C 32,12 32,12 32,12" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="0"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:pathData="M 32,12C 32,12 32,12 32,12" + android:propertyName="translateXY" + android:propertyXName="translateX" + android:propertyYName="translateY" + android:startOffset="983"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="1000" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> + <aapt:attr name="android:drawable"> + <vector + android:width="64dp" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="64"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_2_G" + android:translateX="32" + android:translateY="12"> + <path + android:name="_R_G_L_2_G_D_0_P_0" + android:fillAlpha="0" + android:fillColor="?attr/colorControlNormal" + android:fillType="nonZero" + android:pathData=" M32 -12 C32,-12 32,12 32,12 C32,12 -32,12 -32,12 C-32,12 -32,-12 -32,-12 C-32,-12 32,-12 32,-12c " /> + </group> + <group + android:name="_R_G_L_1_G_N_1_T_1" + android:scaleX="0.01" + android:scaleY="0.01" + android:translateX="32" + android:translateY="12"> + <group + android:name="_R_G_L_1_G_N_1_T_0" + android:translateX="-50" + android:translateY="-50"> + <group + android:name="_R_G_L_1_G" + android:translateX="50" + android:translateY="50"> + <path + android:name="_R_G_L_1_G_D_0_P_0" + android:fillAlpha="0.2" + android:fillColor="?attr/colorProgressBackgroundNormal" + android:fillType="nonZero" + android:pathData=" M2080 -434 C2053.41,-431.82 2032.66,-423.65 2009,-416 C1677.83,-308.94 1328.18,-225.31 948,-169 C552.55,-110.43 82.93,-85.03 -368,-110 C-945.44,-141.98 -1453.99,-244.34 -1920,-388 C-1941.56,-394.65 -1959.08,-402.44 -1986,-409 C-2037.81,-421.62 -2094.99,-409.05 -2131,-389 C-2199,-351.14 -2259.29,-260.99 -2224,-153 C-2209.83,-109.65 -2185.04,-77.07 -2151,-52 C-2117.95,-27.66 -2071.89,-16.07 -2026,-2 C-1671.19,106.76 -1289.85,190.97 -878,245 C-470.97,298.4 -1.15,313.9 448,286 C879.56,259.19 1286.12,192.07 1657,100 C1789.03,67.23 1935.92,26.61 2065,-14 C2109.16,-27.89 2157.33,-39.88 2194,-60 C2261.75,-97.17 2324.86,-186 2290,-295 C2263.92,-376.54 2191.93,-443.19 2080,-434c " /> + </group> + </group> + </group> + <group + android:name="_R_G_L_0_G_N_1_T_1" + android:scaleX="0.01" + android:scaleY="0.01" + android:translateX="32" + android:translateY="12"> + <group + android:name="_R_G_L_0_G_N_1_T_0" + android:translateX="-50" + android:translateY="-50"> + <group + android:name="_R_G_L_0_G" + android:translateX="50" + android:translateY="50"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:fillAlpha="0" + android:fillColor="?attr/colorControlNormal" + android:fillType="nonZero" + android:pathData=" M-2053 -413 C-2064.2,-412.04 -2074.66,-410.29 -2084.48,-407.87 C-2094.29,-405.46 -2103.47,-402.36 -2112.09,-398.71 C-2120.71,-395.06 -2128.79,-390.85 -2136.41,-386.19 C-2144.02,-381.54 -2151.19,-376.43 -2158,-371 C-2168.19,-362.86 -2177.05,-354.51 -2184.8,-345.79 C-2192.55,-337.07 -2199.2,-327.99 -2204.98,-318.41 C-2210.77,-308.83 -2215.7,-298.74 -2220,-288 C-2237.22,-245.04 -2237.06,-202.14 -2226.93,-164.3 C-2216.8,-126.46 -2196.69,-93.69 -2174,-71 C-2150.2,-47.2 -2115.92,-28.38 -2077.59,-19.79 C-2039.27,-11.2 -1996.92,-12.85 -1957,-30 C-1925.35,-43.59 -1896.01,-64.58 -1873.88,-92.68 C-1851.74,-120.77 -1836.82,-155.98 -1834,-198 C-1831.53,-234.86 -1837.92,-266 -1849.1,-292.1 C-1860.29,-318.21 -1876.28,-339.28 -1893,-356 C-1910.36,-373.36 -1932.3,-389.15 -1958.94,-399.84 C-1985.57,-410.52 -2016.89,-416.09 -2053,-413c " /> + </group> + </group> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> +</animated-vector>
\ No newline at end of file diff --git a/core/res/res/drawable-watch/progress_horizontal_material.xml b/core/res/res/drawable-watch/progress_horizontal_material.xml new file mode 100644 index 000000000000..8c52a41c149d --- /dev/null +++ b/core/res/res/drawable-watch/progress_horizontal_material.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2014 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. +--> + +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@id/background" + android:gravity="center_vertical|fill_horizontal"> + <shape android:shape="rectangle"> + <corners android:radius="?attr/progressBarCornerRadius" /> + <size android:height="@dimen/progress_bar_height_material" /> + <solid android:color="@color/material_grey_900" /> + </shape> + </item> + <item android:id="@id/secondaryProgress" + android:gravity="center_vertical|fill_horizontal"> + <scale android:scaleWidth="100%"> + <shape android:shape="rectangle"> + <corners android:radius="?attr/progressBarCornerRadius" /> + <size android:height="@dimen/progress_bar_height_material" /> + <solid android:color="@color/material_grey_900" /> + </shape> + </scale> + </item> + <item android:id="@id/progress" + android:gravity="center_vertical|fill_horizontal"> + <scale android:scaleWidth="100%"> + <shape android:shape="rectangle"> + <corners android:radius="?attr/progressBarCornerRadius" /> + <size android:height="@dimen/progress_bar_height_material" /> + <solid android:color="@color/white" /> + </shape> + </scale> + </item> +</layer-list>
\ No newline at end of file diff --git a/core/res/res/values-watch/dimens_material.xml b/core/res/res/values-watch/dimens_material.xml index 51d401860f7f..40673c1aa584 100644 --- a/core/res/res/values-watch/dimens_material.xml +++ b/core/res/res/values-watch/dimens_material.xml @@ -44,6 +44,7 @@ <dimen name="progress_bar_size_small">16dip</dimen> <dimen name="progress_bar_size_medium">32dip</dimen> <dimen name="progress_bar_size_large">64dip</dimen> + <dimen name="progress_bar_height">24dp</dimen> <!-- Progress bar message dimens --> <dimen name="message_progress_dialog_text_size">18sp</dimen> diff --git a/core/res/res/values-watch/strings.xml b/core/res/res/values-watch/strings.xml index 6d6cfc775fd3..9fa4bef76d3a 100644 --- a/core/res/res/values-watch/strings.xml +++ b/core/res/res/values-watch/strings.xml @@ -26,6 +26,7 @@ <string name="global_action_emergency">Emergency SOS</string> <!-- Reboot to Recovery Progress Dialog. This is shown before it reboots to recovery. --> + <string name="reboot_to_update_prepare">Preparing to update</string> <string name="reboot_to_update_title">Wear OS system update</string> <!-- Title of the pop-up dialog in which the user switches keyboard, also known as input method. --> diff --git a/core/res/res/values-watch/styles_material.xml b/core/res/res/values-watch/styles_material.xml index 12bc406fc998..8698e86ba5f9 100644 --- a/core/res/res/values-watch/styles_material.xml +++ b/core/res/res/values-watch/styles_material.xml @@ -107,4 +107,14 @@ please see styles_device_defaults.xml. <item name="paddingStart">@dimen/message_progress_dialog_start_padding</item> <item name="paddingTop">@dimen/message_progress_dialog_top_padding</item> </style> + + <!-- Material progress part (indeterminate/horizontal) for Wear --> + <style name="Widget.Material.ProgressBar.Horizontal" parent="Widget.ProgressBar.Horizontal"> + <item name="progressDrawable">@drawable/progress_horizontal_material</item> + <item name="indeterminateDrawable"> + @drawable/progress_indeterminate_horizontal_material + </item> + <item name="minHeight">@dimen/progress_bar_height</item> + <item name="maxHeight">@dimen/progress_bar_height</item> + </style> </resources> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 7602f697fcf3..d5aaeefe1cfc 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -198,6 +198,9 @@ service. Off by default, since the service may not be available on some devices. --> <bool name="config_enableProximityService">false</bool> + <!-- Enable or disable android.companion.virtual.VirtualDeviceManager. Enabled by default. --> + <bool name="config_enableVirtualDeviceManager">true</bool> + <!-- Whether dialogs should close automatically when the user touches outside of them. This should not normally be modified. --> <bool name="config_closeDialogWhenTouchOutside">true</bool> @@ -6556,6 +6559,10 @@ serialization, a default vibration will be used. Note that, indefinitely repeating vibrations are not allowed as shutdown vibrations. --> <string name="config_defaultShutdownVibrationFile" /> + + <!-- Whether single finger panning is enabled when magnification is on --> + <bool name="config_enable_a11y_magnification_single_panning">false</bool> + <!-- The file path in which custom vibrations are provided for haptic feedbacks. If the device does not specify any such file path here, if the file path specified here does not exist, or if the contents of the file does not make up a valid customization diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 978849d8bde0..bd678cc06223 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -418,6 +418,7 @@ <java-symbol type="bool" name="config_localDisplaysMirrorContent" /> <java-symbol type="bool" name="config_ignoreUdfpsVote" /> <java-symbol type="bool" name="config_enableProximityService" /> + <java-symbol type="bool" name="config_enableVirtualDeviceManager" /> <java-symbol type="array" name="config_localPrivateDisplayPorts" /> <java-symbol type="integer" name="config_defaultDisplayDefaultColorMode" /> <java-symbol type="bool" name="config_enableAppWidgetService" /> @@ -5178,5 +5179,8 @@ <java-symbol type="drawable" name="focus_event_pressed_key_background" /> <java-symbol type="string" name="config_defaultShutdownVibrationFile" /> <java-symbol type="string" name="lockscreen_too_many_failed_attempts_countdown" /> + + <java-symbol type="bool" name="config_enable_a11y_magnification_single_panning" /> + <java-symbol type="string" name="config_hapticFeedbackCustomizationFile" /> </resources> diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java index c1b55cdfc6d7..6913bf9e3cfa 100644 --- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java +++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java @@ -49,7 +49,6 @@ import android.app.servertransaction.ConfigurationChangeItem; import android.app.servertransaction.NewIntentItem; import android.app.servertransaction.ResumeActivityItem; import android.app.servertransaction.StopActivityItem; -import android.app.servertransaction.WindowTokenClientController; import android.content.Context; import android.content.Intent; import android.content.res.CompatibilityInfo; @@ -65,6 +64,7 @@ import android.util.DisplayMetrics; import android.util.MergedConfiguration; import android.view.Display; import android.view.View; +import android.window.WindowTokenClientController; import androidx.test.filters.MediumTest; import androidx.test.platform.app.InstrumentationRegistry; diff --git a/core/tests/coretests/src/android/database/DatabaseErrorHandlerTest.java b/core/tests/coretests/src/android/database/DatabaseErrorHandlerTest.java index 91c7687176d4..96775846c66f 100644 --- a/core/tests/coretests/src/android/database/DatabaseErrorHandlerTest.java +++ b/core/tests/coretests/src/android/database/DatabaseErrorHandlerTest.java @@ -19,10 +19,13 @@ package android.database; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDiskIOException; +import android.database.sqlite.SQLiteDatabaseCorruptException; + import android.database.sqlite.SQLiteException; import android.test.AndroidTestCase; import android.util.Log; +import java.util.concurrent.atomic.AtomicBoolean; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; @@ -61,7 +64,6 @@ public class DatabaseErrorHandlerTest extends AndroidTestCase { assertTrue(mDatabaseFile.exists()); } - public void testDatabaseIsCorrupt() throws IOException { mDatabase.execSQL("create table t (i int);"); // write junk into the database file @@ -70,30 +72,23 @@ public class DatabaseErrorHandlerTest extends AndroidTestCase { writer.close(); assertTrue(mDatabaseFile.exists()); // since the database file is now corrupt, doing any sql on this database connection - // should trigger call to MyDatabaseCorruptionHandler.onCorruption + // should trigger call to MyDatabaseCorruptionHandler.onCorruption. A corruption + // exception will also be throws. This seems redundant. try { mDatabase.execSQL("select * from t;"); fail("expected exception"); - } catch (SQLiteDiskIOException e) { - /** - * this test used to produce a corrupted db. but with new sqlite it instead reports - * Disk I/O error. meh.. - * need to figure out how to cause corruption in db - */ - // expected - if (mDatabaseFile.exists()) { - mDatabaseFile.delete(); - } - } catch (SQLiteException e) { - + } catch (SQLiteDatabaseCorruptException e) { + // Expected result. } - // database file should be gone + + // The database file should be gone. assertFalse(mDatabaseFile.exists()); - // after corruption handler is called, the database file should be free of - // database corruption - SQLiteDatabase db = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null, + // After corruption handler is called, the database file should be free of + // database corruption. Reopen it. + mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile.getPath(), null, new MyDatabaseCorruptionHandler()); - assertTrue(db.isDatabaseIntegrityOk()); + assertTrue(mDatabase.isDatabaseIntegrityOk()); + // The teadDown() routine will close the database. } /** @@ -102,8 +97,21 @@ public class DatabaseErrorHandlerTest extends AndroidTestCase { * corrupt before deleting the file. */ public class MyDatabaseCorruptionHandler implements DatabaseErrorHandler { + private final AtomicBoolean mEntered = new AtomicBoolean(false); public void onCorruption(SQLiteDatabase dbObj) { - boolean databaseOk = dbObj.isDatabaseIntegrityOk(); + boolean databaseOk = false; + if (!mEntered.get()) { + // The integrity check can retrigger the corruption handler if the database is, + // indeed, corrupted. Use mEntered to detect recursion and to skip retrying the + // integrity check on recursion. + mEntered.set(true); + databaseOk = dbObj.isDatabaseIntegrityOk(); + } + // At this point the database state has been detected and there is no further danger + // of recursion. Setting mEntered to false allows this object to be reused, although + // it is not obvious how such reuse would work. + mEntered.set(false); + // close the database try { dbObj.close(); @@ -122,4 +130,4 @@ public class DatabaseErrorHandlerTest extends AndroidTestCase { } } } -}
\ No newline at end of file +} diff --git a/core/tests/coretests/src/android/database/DatabaseGeneralTest.java b/core/tests/coretests/src/android/database/DatabaseGeneralTest.java index 95b0e325c1fa..e8d90f552c3b 100644 --- a/core/tests/coretests/src/android/database/DatabaseGeneralTest.java +++ b/core/tests/coretests/src/android/database/DatabaseGeneralTest.java @@ -914,27 +914,11 @@ public class DatabaseGeneralTest extends AndroidTestCase implements PerformanceT verifyLookasideStats(true); } - @SmallTest - public void testOpenParamsSetLookasideConfigValidation() { - try { - SQLiteDatabase.OpenParams params = new SQLiteDatabase.OpenParams.Builder() - .setLookasideConfig(-1, 0).build(); - fail("Negative slot size should be rejected"); - } catch (IllegalArgumentException expected) { - } - try { - SQLiteDatabase.OpenParams params = new SQLiteDatabase.OpenParams.Builder() - .setLookasideConfig(0, -10).build(); - fail("Negative slot count should be rejected"); - } catch (IllegalArgumentException expected) { - } - } - void verifyLookasideStats(boolean expectDisabled) { boolean dbStatFound = false; SQLiteDebug.PagerStats info = SQLiteDebug.getDatabaseInfo(); for (SQLiteDebug.DbStats dbStat : info.dbStats) { - if (dbStat.dbName.endsWith(mDatabaseFile.getName())) { + if (dbStat.dbName.endsWith(mDatabaseFile.getName()) && !dbStat.arePoolStats) { dbStatFound = true; Log.i(TAG, "Lookaside for " + dbStat.dbName + " " + dbStat.lookaside); if (expectDisabled) { @@ -948,6 +932,22 @@ public class DatabaseGeneralTest extends AndroidTestCase implements PerformanceT assertTrue("No dbstat found for " + mDatabaseFile.getName(), dbStatFound); } + @SmallTest + public void testOpenParamsSetLookasideConfigValidation() { + try { + SQLiteDatabase.OpenParams params = new SQLiteDatabase.OpenParams.Builder() + .setLookasideConfig(-1, 0).build(); + fail("Negative slot size should be rejected"); + } catch (IllegalArgumentException expected) { + } + try { + SQLiteDatabase.OpenParams params = new SQLiteDatabase.OpenParams.Builder() + .setLookasideConfig(0, -10).build(); + fail("Negative slot count should be rejected"); + } catch (IllegalArgumentException expected) { + } + } + @LargeTest public void testDefaultDatabaseErrorHandler() { DefaultDatabaseErrorHandler errorHandler = new DefaultDatabaseErrorHandler(); diff --git a/core/tests/coretests/src/android/window/WindowContextControllerTest.java b/core/tests/coretests/src/android/window/WindowContextControllerTest.java index 5f2aecc40d16..940c06794b43 100644 --- a/core/tests/coretests/src/android/window/WindowContextControllerTest.java +++ b/core/tests/coretests/src/android/window/WindowContextControllerTest.java @@ -30,7 +30,6 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; -import android.app.servertransaction.WindowTokenClientController; import android.os.Binder; import android.platform.test.annotations.Presubmit; diff --git a/core/tests/coretests/src/android/app/servertransaction/WindowTokenClientControllerTest.java b/core/tests/coretests/src/android/window/WindowTokenClientControllerTest.java index 59a55bbb5c5a..9793dde0aaa5 100644 --- a/core/tests/coretests/src/android/app/servertransaction/WindowTokenClientControllerTest.java +++ b/core/tests/coretests/src/android/window/WindowTokenClientControllerTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.app.servertransaction; +package android.window; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; @@ -36,7 +36,6 @@ import android.os.RemoteException; import android.platform.test.annotations.Presubmit; import android.view.IWindowManager; import android.view.WindowManagerGlobal; -import android.window.WindowTokenClient; import androidx.test.filters.SmallTest; diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index 94e23e735d06..87e6b18be557 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -427,12 +427,6 @@ "group": "WM_DEBUG_BACK_PREVIEW", "at": "com\/android\/server\/wm\/BackNavigationController.java" }, - "-1715268616": { - "message": "Last window, removing starting window %s", - "level": "VERBOSE", - "group": "WM_DEBUG_STARTING_WINDOW", - "at": "com\/android\/server\/wm\/ActivityRecord.java" - }, "-1710206702": { "message": "Display id=%d is frozen while keyguard locked, return %d", "level": "VERBOSE", @@ -463,6 +457,12 @@ "group": "WM_DEBUG_TASKS", "at": "com\/android\/server\/wm\/ActivityTaskManagerService.java" }, + "-1671601441": { + "message": "attachWindowContextToDisplayContent: calling from non-existing process pid=%d uid=%d", + "level": "WARN", + "group": "WM_ERROR", + "at": "com\/android\/server\/wm\/WindowManagerService.java" + }, "-1670695197": { "message": "Attempted to add presentation window to a non-suitable display. Aborting.", "level": "WARN", @@ -1225,6 +1225,12 @@ "group": "WM_DEBUG_STARTING_WINDOW", "at": "com\/android\/server\/wm\/WindowState.java" }, + "-961053385": { + "message": "attachWindowContextToDisplayArea: calling from non-existing process pid=%d uid=%d", + "level": "WARN", + "group": "WM_ERROR", + "at": "com\/android\/server\/wm\/WindowManagerService.java" + }, "-957060823": { "message": "Moving to PAUSING: %s", "level": "VERBOSE", @@ -4057,12 +4063,6 @@ "group": "WM_DEBUG_WINDOW_TRANSITIONS", "at": "com\/android\/server\/wm\/Transition.java" }, - "1671994402": { - "message": "Nulling last startingData", - "level": "VERBOSE", - "group": "WM_DEBUG_STARTING_WINDOW", - "at": "com\/android\/server\/wm\/ActivityRecord.java" - }, "1674747211": { "message": "%s forcing orientation to %d for display id=%d", "level": "VERBOSE", @@ -4243,12 +4243,6 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/ActivityRecord.java" }, - "1853793312": { - "message": "Notify removed startingWindow %s", - "level": "VERBOSE", - "group": "WM_DEBUG_STARTING_WINDOW", - "at": "com\/android\/server\/wm\/ActivityRecord.java" - }, "1856783490": { "message": "resumeTopActivity: Restarting %s", "level": "DEBUG", @@ -4279,6 +4273,12 @@ "group": "WM_DEBUG_ORIENTATION", "at": "com\/android\/server\/wm\/DisplayContent.java" }, + "1879463933": { + "message": "attachWindowContextToWindowToken: calling from non-existing process pid=%d uid=%d", + "level": "WARN", + "group": "WM_ERROR", + "at": "com\/android\/server\/wm\/WindowManagerService.java" + }, "1891501279": { "message": "cancelAnimation(): reason=%s", "level": "DEBUG", diff --git a/data/keyboards/Vendor_0957_Product_0001.kl b/data/keyboards/Vendor_0957_Product_0001.kl index 1da3ba6efc2b..893c87d7fe1b 100644 --- a/data/keyboards/Vendor_0957_Product_0001.kl +++ b/data/keyboards/Vendor_0957_Product_0001.kl @@ -16,7 +16,7 @@ # Key Layout file for Google Reference RCU Remote. # -key 116 TV_POWER WAKE +key 116 POWER WAKE key 217 ASSIST WAKE key 103 DPAD_UP 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 d94e8e426c4b..4d73c20fe39f 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/JetpackTaskFragmentOrganizer.java @@ -17,7 +17,9 @@ package androidx.window.extensions.embedding; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; +import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_FRONT; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION; import static androidx.window.extensions.embedding.SplitContainer.getFinishPrimaryWithSecondaryBehavior; import static androidx.window.extensions.embedding.SplitContainer.getFinishSecondaryWithPrimaryBehavior; @@ -340,6 +342,20 @@ class JetpackTaskFragmentOrganizer extends TaskFragmentOrganizer { wct.deleteTaskFragment(fragmentToken); } + void reorderTaskFragmentToFront(@NonNull WindowContainerTransaction wct, + @NonNull IBinder fragmentToken) { + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_REORDER_TO_FRONT).build(); + wct.addTaskFragmentOperation(fragmentToken, operation); + } + + void setTaskFragmentIsolatedNavigation(@NonNull WindowContainerTransaction wct, + @NonNull IBinder fragmentToken, boolean isolatedNav) { + final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( + OP_TYPE_SET_ISOLATED_NAVIGATION).setIsolatedNav(isolatedNav).build(); + wct.addTaskFragmentOperation(fragmentToken, operation); + } + void updateTaskFragmentInfo(@NonNull TaskFragmentInfo taskFragmentInfo) { mFragmentInfos.put(taskFragmentInfo.getFragmentToken(), taskFragmentInfo); } 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 a2f75e099465..f95f3ffb4df3 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -250,6 +250,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Updates the Split final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); final WindowContainerTransaction wct = transactionRecord.getTransaction(); + + mPresenter.setTaskFragmentIsolatedNavigation(wct, + splitPinContainer.getSecondaryContainer().getTaskFragmentToken(), + true /* isolatedNav */); mPresenter.updateSplitContainer(splitPinContainer, wct); transactionRecord.apply(false /* shouldApplyIndependently */); updateCallbackIfNecessary(); 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 4dafbd17f379..5de6acfcc9db 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -17,7 +17,6 @@ package androidx.window.extensions.embedding; import static android.content.pm.PackageManager.MATCH_ALL; -import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_FRONT; import android.app.Activity; import android.app.ActivityThread; @@ -40,7 +39,6 @@ import android.view.WindowInsets; import android.view.WindowMetrics; import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentCreationParams; -import android.window.TaskFragmentOperation; import android.window.WindowContainerTransaction; import androidx.annotation.IntDef; @@ -427,10 +425,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { final SplitPinContainer pinnedContainer = container.getTaskContainer().getSplitPinContainer(); if (pinnedContainer != null) { - final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( - OP_TYPE_REORDER_TO_FRONT).build(); - wct.addTaskFragmentOperation( - pinnedContainer.getSecondaryContainer().getTaskFragmentToken(), operation); + reorderTaskFragmentToFront(wct, + pinnedContainer.getSecondaryContainer().getTaskFragmentToken()); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java index 39f861de1ba0..5cf9175073c0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationAdapter.java @@ -50,7 +50,7 @@ class ActivityEmbeddingAnimationAdapter { final SurfaceControl mLeash; /** Area in absolute coordinate that the animation surface shouldn't go beyond. */ @NonNull - private final Rect mWholeAnimationBounds = new Rect(); + final Rect mWholeAnimationBounds = new Rect(); /** * Area in absolute coordinate that should represent all the content to show for this window. * This should be the end bounds for opening window, and start bounds for closing window in case @@ -229,20 +229,7 @@ class ActivityEmbeddingAnimationAdapter { mTransformation.getMatrix().postTranslate(mContentRelOffset.x, mContentRelOffset.y); t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); t.setAlpha(mLeash, mTransformation.getAlpha()); - - // The following applies an inverse scale to the clip-rect so that it crops "after" the - // scale instead of before. - mVecs[1] = mVecs[2] = 0; - mVecs[0] = mVecs[3] = 1; - mTransformation.getMatrix().mapVectors(mVecs); - mVecs[0] = 1.f / mVecs[0]; - mVecs[3] = 1.f / mVecs[3]; - final Rect clipRect = mTransformation.getClipRect(); - mRect.left = (int) (clipRect.left * mVecs[0] + 0.5f); - mRect.right = (int) (clipRect.right * mVecs[0] + 0.5f); - mRect.top = (int) (clipRect.top * mVecs[3] + 0.5f); - mRect.bottom = (int) (clipRect.bottom * mVecs[3] + 0.5f); - t.setCrop(mLeash, mRect); + t.setWindowCrop(mLeash, mWholeAnimationBounds.width(), mWholeAnimationBounds.height()); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java index 1793a3d0feb4..4640106b5f1c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationSpec.java @@ -26,7 +26,6 @@ import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.view.animation.AnimationSet; import android.view.animation.AnimationUtils; -import android.view.animation.ClipRectAnimation; import android.view.animation.Interpolator; import android.view.animation.LinearInterpolator; import android.view.animation.ScaleAnimation; @@ -189,14 +188,6 @@ class ActivityEmbeddingAnimationSpec { startBounds.top - endBounds.top, 0); endTranslate.setDuration(CHANGE_ANIMATION_DURATION); endSet.addAnimation(endTranslate); - // The end leash is resizing, we should update the window crop based on the clip rect. - final Rect startClip = new Rect(startBounds); - final Rect endClip = new Rect(endBounds); - startClip.offsetTo(0, 0); - endClip.offsetTo(0, 0); - final Animation clipAnim = new ClipRectAnimation(startClip, endClip); - clipAnim.setDuration(CHANGE_ANIMATION_DURATION); - endSet.addAnimation(clipAnim); endSet.initialize(startBounds.width(), startBounds.height(), parentBounds.width(), parentBounds.height()); endSet.scaleCurrentDuration(mTransitionAnimationScaleSetting); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java index 65727b6145e4..51e7be0f8a24 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java @@ -939,6 +939,11 @@ public class PipController implements PipTransitionController.PipTransitionCallb * Sets both shelf visibility and its height. */ private void setShelfHeight(boolean visible, int height) { + if (mEnablePipKeepClearAlgorithm) { + // turn this into Launcher keep clear area registration instead + setLauncherKeepClearAreaHeight(visible, height); + return; + } if (!mIsKeyguardShowingOrAnimating) { setShelfHeightLocked(visible, height); } diff --git a/libs/WindowManager/Shell/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp index b062fbd510ca..09477232c9e0 100644 --- a/libs/WindowManager/Shell/tests/flicker/Android.bp +++ b/libs/WindowManager/Shell/tests/flicker/Android.bp @@ -44,14 +44,27 @@ filegroup { } filegroup { - name: "WMShellFlickerTestsSplitScreen-src", + name: "WMShellFlickerTestsSplitScreenBase-src", srcs: [ - "src/com/android/wm/shell/flicker/splitscreen/*.kt", "src/com/android/wm/shell/flicker/splitscreen/benchmark/*.kt", ], } filegroup { + name: "WMShellFlickerTestsSplitScreenEnter-src", + srcs: [ + "src/com/android/wm/shell/flicker/splitscreen/Enter*.kt", + ], +} + +filegroup { + name: "WMShellFlickerTestsSplitScreenOther-src", + srcs: [ + "src/com/android/wm/shell/flicker/splitscreen/*.kt", + ], +} + +filegroup { name: "WMShellFlickerServiceTests-src", srcs: [ "src/com/android/wm/shell/flicker/service/**/*.kt", @@ -122,7 +135,9 @@ android_test { exclude_srcs: [ ":WMShellFlickerTestsBubbles-src", ":WMShellFlickerTestsPip-src", - ":WMShellFlickerTestsSplitScreen-src", + ":WMShellFlickerTestsSplitScreenEnter-src", + ":WMShellFlickerTestsSplitScreenOther-src", + ":WMShellFlickerTestsSplitScreenBase-src", ":WMShellFlickerServiceTests-src", ], } @@ -152,14 +167,31 @@ android_test { } android_test { - name: "WMShellFlickerTestsSplitScreen", + name: "WMShellFlickerTestsSplitScreenEnter", defaults: ["WMShellFlickerTestsDefault"], additional_manifests: ["manifests/AndroidManifestSplitScreen.xml"], package_name: "com.android.wm.shell.flicker.splitscreen", instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen", srcs: [ ":WMShellFlickerTestsBase-src", - ":WMShellFlickerTestsSplitScreen-src", + ":WMShellFlickerTestsSplitScreenBase-src", + ":WMShellFlickerTestsSplitScreenEnter-src", + ], +} + +android_test { + name: "WMShellFlickerTestsSplitScreenOther", + defaults: ["WMShellFlickerTestsDefault"], + additional_manifests: ["manifests/AndroidManifestSplitScreen.xml"], + package_name: "com.android.wm.shell.flicker.splitscreen", + instrumentation_target_package: "com.android.wm.shell.flicker.splitscreen", + srcs: [ + ":WMShellFlickerTestsBase-src", + ":WMShellFlickerTestsSplitScreenBase-src", + ":WMShellFlickerTestsSplitScreenOther-src", + ], + exclude_srcs: [ + ":WMShellFlickerTestsSplitScreenEnter-src", ], } diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/Utils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/Utils.kt index 610cedefe594..a0023c0d969e 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/Utils.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/Utils.kt @@ -22,9 +22,7 @@ import android.platform.test.rule.PressHomeRule import android.platform.test.rule.UnlockScreenRule import android.tools.common.NavBar import android.tools.common.Rotation -import android.tools.device.apphelpers.MessagingAppHelper import android.tools.device.flicker.rules.ChangeDisplayOrientationRule -import android.tools.device.flicker.rules.LaunchAppRule import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule import androidx.test.platform.app.InstrumentationRegistry import org.junit.rules.RuleChain @@ -37,9 +35,6 @@ object Utils { .around( NavigationModeRule(navigationMode.value, /* changeNavigationModeAfterTest */ false) ) - .around( - LaunchAppRule(MessagingAppHelper(instrumentation), clearCacheAfterParsing = false) - ) .around(RemoveAllTasksButHomeRule()) .around( ChangeDisplayOrientationRule( diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt index e5c1e75a75f4..4d9007093cea 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/CopyContentInSplitBenchmark.kt @@ -22,7 +22,6 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import androidx.test.filters.RequiresDevice -import com.android.wm.shell.flicker.splitscreen.SplitScreenBase import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.FixMethodOrder import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt index e4e1af9d24ce..8360e94a6c3e 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByDividerBenchmark.kt @@ -21,7 +21,6 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import androidx.test.filters.RequiresDevice -import com.android.wm.shell.flicker.splitscreen.SplitScreenBase import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.FixMethodOrder import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt index b2dd02bf2c41..e74587843a72 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DismissSplitScreenByGoHomeBenchmark.kt @@ -21,7 +21,6 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import androidx.test.filters.RequiresDevice -import com.android.wm.shell.flicker.splitscreen.SplitScreenBase import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.FixMethodOrder import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt index 078859166dbc..c3beb366cc66 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/DragDividerToResizeBenchmark.kt @@ -21,7 +21,6 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import androidx.test.filters.RequiresDevice -import com.android.wm.shell.flicker.splitscreen.SplitScreenBase import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.Assume import org.junit.Before diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt index 884e4513e893..80ccaa144c58 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromAllAppsBenchmark.kt @@ -22,7 +22,6 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import androidx.test.filters.RequiresDevice -import com.android.wm.shell.flicker.splitscreen.SplitScreenBase import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.Assume import org.junit.Before diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt index e5c40b69726c..cd3fbab1497b 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromNotificationBenchmark.kt @@ -22,7 +22,6 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import androidx.test.filters.RequiresDevice -import com.android.wm.shell.flicker.splitscreen.SplitScreenBase import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.Assume import org.junit.Before diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt index 04510014c437..a06ae6bc63a1 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromShortcutBenchmark.kt @@ -22,7 +22,6 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import androidx.test.filters.RequiresDevice -import com.android.wm.shell.flicker.splitscreen.SplitScreenBase import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.Assume import org.junit.Before diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt index 9e0ca1b20f09..de4ec6d12657 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenByDragFromTaskbarBenchmark.kt @@ -22,7 +22,6 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import androidx.test.filters.RequiresDevice -import com.android.wm.shell.flicker.splitscreen.SplitScreenBase import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.Assume import org.junit.Before diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt index 06b4fe7e0eb4..be507d833986 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/EnterSplitScreenFromOverviewBenchmark.kt @@ -21,7 +21,6 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import androidx.test.filters.RequiresDevice -import com.android.wm.shell.flicker.splitscreen.SplitScreenBase import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.FixMethodOrder import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt index 540c11fba05d..2a07369062c8 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SplitScreenBase.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SplitScreenBase.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.flicker.splitscreen +package com.android.wm.shell.flicker.splitscreen.benchmark import android.content.Context import android.tools.device.flicker.legacy.FlickerBuilder diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt index 007b7518b16e..ed0debd01408 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchAppByDoubleTapDividerBenchmark.kt @@ -25,7 +25,6 @@ import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import android.tools.device.helpers.WindowUtils import android.tools.device.traces.parsers.WindowManagerStateHelper import androidx.test.filters.RequiresDevice -import com.android.wm.shell.flicker.splitscreen.SplitScreenBase import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.FixMethodOrder import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt index 10c8eebf7d1d..9b7939a3a006 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromAnotherAppBenchmark.kt @@ -22,7 +22,6 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import androidx.test.filters.RequiresDevice -import com.android.wm.shell.flicker.splitscreen.SplitScreenBase import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.FixMethodOrder import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt index a6e750fed70e..9326ef3024a4 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromHomeBenchmark.kt @@ -22,7 +22,6 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import androidx.test.filters.RequiresDevice -import com.android.wm.shell.flicker.splitscreen.SplitScreenBase import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.FixMethodOrder import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt index 7e8d5fb83157..b928e40108bf 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBackToSplitFromRecentBenchmark.kt @@ -22,7 +22,6 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import androidx.test.filters.RequiresDevice -import com.android.wm.shell.flicker.splitscreen.SplitScreenBase import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.FixMethodOrder import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt index 56edad1ded19..f314995fa947 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/SwitchBetweenSplitPairsBenchmark.kt @@ -21,7 +21,6 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import androidx.test.filters.RequiresDevice -import com.android.wm.shell.flicker.splitscreen.SplitScreenBase import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.FixMethodOrder import org.junit.runner.RunWith diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.kt index 065d4d62be42..e71834de7123 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/benchmark/UnlockKeyguardToSplitScreenBenchmark.kt @@ -22,7 +22,6 @@ import android.tools.device.flicker.legacy.FlickerBuilder import android.tools.device.flicker.legacy.LegacyFlickerTest import android.tools.device.flicker.legacy.LegacyFlickerTestFactory import androidx.test.filters.RequiresDevice -import com.android.wm.shell.flicker.splitscreen.SplitScreenBase import com.android.wm.shell.flicker.utils.SplitScreenUtils import org.junit.FixMethodOrder import org.junit.runner.RunWith diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index dcbaaec1e342..ad09067e6309 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -91,12 +91,17 @@ struct FindEntryResult { StringPoolRef entry_string_ref; }; -AssetManager2::AssetManager2(ApkAssetsList apk_assets, const ResTable_config& configuration) - : configuration_(configuration) { +AssetManager2::AssetManager2(ApkAssetsList apk_assets, const ResTable_config& configuration) { + configurations_.push_back(configuration); + // Don't invalidate caches here as there's nothing cached yet. SetApkAssets(apk_assets, false); } +AssetManager2::AssetManager2() { + configurations_.resize(1); +} + bool AssetManager2::SetApkAssets(ApkAssetsList apk_assets, bool invalidate_caches) { BuildDynamicRefTable(apk_assets); RebuildFilterList(); @@ -421,9 +426,16 @@ bool AssetManager2::ContainsAllocatedTable() const { return false; } -void AssetManager2::SetConfiguration(const ResTable_config& configuration) { - const int diff = configuration_.diff(configuration); - configuration_ = configuration; +void AssetManager2::SetConfigurations(std::vector<ResTable_config> configurations) { + int diff = 0; + if (configurations_.size() != configurations.size()) { + diff = -1; + } else { + for (int i = 0; i < configurations_.size(); i++) { + diff |= configurations_[i].diff(configurations[i]); + } + } + configurations_ = std::move(configurations); if (diff) { RebuildFilterList(); @@ -620,16 +632,6 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry( auto op = StartOperation(); - // Might use this if density_override != 0. - ResTable_config density_override_config; - - // Select our configuration or generate a density override configuration. - const ResTable_config* desired_config = &configuration_; - if (density_override != 0 && density_override != configuration_.density) { - density_override_config = configuration_; - density_override_config.density = density_override; - desired_config = &density_override_config; - } // Retrieve the package group from the package id of the resource id. if (UNLIKELY(!is_valid_resid(resid))) { @@ -648,119 +650,160 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry( } const PackageGroup& package_group = package_groups_[package_idx]; - auto result = FindEntryInternal(package_group, type_idx, entry_idx, *desired_config, - stop_at_first_match, ignore_configuration); - if (UNLIKELY(!result.has_value())) { - return base::unexpected(result.error()); - } + std::optional<FindEntryResult> final_result; + bool final_has_locale = false; + bool final_overlaid = false; + for (auto & config : configurations_) { + // Might use this if density_override != 0. + ResTable_config density_override_config; + + // Select our configuration or generate a density override configuration. + const ResTable_config* desired_config = &config; + if (density_override != 0 && density_override != config.density) { + density_override_config = config; + density_override_config.density = density_override; + desired_config = &density_override_config; + } - bool overlaid = false; - if (!stop_at_first_match && !ignore_configuration) { - const auto& assets = GetApkAssets(result->cookie); - if (!assets) { - ALOGE("Found expired ApkAssets #%d for resource ID 0x%08x.", result->cookie, resid); - return base::unexpected(std::nullopt); + auto result = FindEntryInternal(package_group, type_idx, entry_idx, *desired_config, + stop_at_first_match, ignore_configuration); + if (UNLIKELY(!result.has_value())) { + return base::unexpected(result.error()); } - if (!assets->IsLoader()) { - for (const auto& id_map : package_group.overlays_) { - auto overlay_entry = id_map.overlay_res_maps_.Lookup(resid); - if (!overlay_entry) { - // No id map entry exists for this target resource. - continue; - } - if (overlay_entry.IsInlineValue()) { - // The target resource is overlaid by an inline value not represented by a resource. - ConfigDescription best_frro_config; - Res_value best_frro_value; - bool frro_found = false; - for( const auto& [config, value] : overlay_entry.GetInlineValue()) { - if ((!frro_found || config.isBetterThan(best_frro_config, desired_config)) - && config.match(*desired_config)) { - frro_found = true; - best_frro_config = config; - best_frro_value = value; + bool overlaid = false; + if (!stop_at_first_match && !ignore_configuration) { + const auto& assets = GetApkAssets(result->cookie); + if (!assets) { + ALOGE("Found expired ApkAssets #%d for resource ID 0x%08x.", result->cookie, resid); + return base::unexpected(std::nullopt); + } + if (!assets->IsLoader()) { + for (const auto& id_map : package_group.overlays_) { + auto overlay_entry = id_map.overlay_res_maps_.Lookup(resid); + if (!overlay_entry) { + // No id map entry exists for this target resource. + continue; + } + if (overlay_entry.IsInlineValue()) { + // The target resource is overlaid by an inline value not represented by a resource. + ConfigDescription best_frro_config; + Res_value best_frro_value; + bool frro_found = false; + for( const auto& [config, value] : overlay_entry.GetInlineValue()) { + if ((!frro_found || config.isBetterThan(best_frro_config, desired_config)) + && config.match(*desired_config)) { + frro_found = true; + best_frro_config = config; + best_frro_value = value; + } + } + if (!frro_found) { + continue; } + result->entry = best_frro_value; + result->dynamic_ref_table = id_map.overlay_res_maps_.GetOverlayDynamicRefTable(); + result->cookie = id_map.cookie; + + if (UNLIKELY(logging_enabled)) { + last_resolution_.steps.push_back(Resolution::Step{ + Resolution::Step::Type::OVERLAID_INLINE, result->cookie, String8()}); + if (auto path = assets->GetPath()) { + const std::string overlay_path = path->data(); + if (IsFabricatedOverlay(overlay_path)) { + // FRRO don't have package name so we use the creating package here. + String8 frro_name = String8("FRRO"); + // Get the first part of it since the expected one should be like + // {overlayPackageName}-{overlayName}-{4 alphanumeric chars}.frro + // under /data/resource-cache/. + const std::string name = overlay_path.substr(overlay_path.rfind('/') + 1); + const size_t end = name.find('-'); + if (frro_name.size() != overlay_path.size() && end != std::string::npos) { + frro_name.append(base::StringPrintf(" created by %s", + name.substr(0 /* pos */, + end).c_str()).c_str()); + } + last_resolution_.best_package_name = frro_name; + } else { + last_resolution_.best_package_name = result->package_name->c_str(); + } + } + overlaid = true; + } + continue; + } + + auto overlay_result = FindEntry(overlay_entry.GetResourceId(), density_override, + false /* stop_at_first_match */, + false /* ignore_configuration */); + if (UNLIKELY(IsIOError(overlay_result))) { + return base::unexpected(overlay_result.error()); + } + if (!overlay_result.has_value()) { + continue; } - if (!frro_found) { + + if (!overlay_result->config.isBetterThan(result->config, desired_config) + && overlay_result->config.compare(result->config) != 0) { + // The configuration of the entry for the overlay must be equal to or better than the + // target configuration to be chosen as the better value. continue; } - result->entry = best_frro_value; + + result->cookie = overlay_result->cookie; + result->entry = overlay_result->entry; + result->config = overlay_result->config; result->dynamic_ref_table = id_map.overlay_res_maps_.GetOverlayDynamicRefTable(); - result->cookie = id_map.cookie; if (UNLIKELY(logging_enabled)) { last_resolution_.steps.push_back( - Resolution::Step{Resolution::Step::Type::OVERLAID_INLINE, result->cookie, String8()}); - if (auto path = assets->GetPath()) { - const std::string overlay_path = path->data(); - if (IsFabricatedOverlay(overlay_path)) { - // FRRO don't have package name so we use the creating package here. - String8 frro_name = String8("FRRO"); - // Get the first part of it since the expected one should be like - // {overlayPackageName}-{overlayName}-{4 alphanumeric chars}.frro - // under /data/resource-cache/. - const std::string name = overlay_path.substr(overlay_path.rfind('/') + 1); - const size_t end = name.find('-'); - if (frro_name.size() != overlay_path.size() && end != std::string::npos) { - frro_name.append(base::StringPrintf(" created by %s", - name.substr(0 /* pos */, - end).c_str()).c_str()); - } - last_resolution_.best_package_name = frro_name; - } else { - last_resolution_.best_package_name = result->package_name->c_str(); - } - } + Resolution::Step{Resolution::Step::Type::OVERLAID, overlay_result->cookie, + overlay_result->config.toString()}); + last_resolution_.best_package_name = + overlay_result->package_name->c_str(); overlaid = true; } - continue; } + } + } - auto overlay_result = FindEntry(overlay_entry.GetResourceId(), density_override, - false /* stop_at_first_match */, - false /* ignore_configuration */); - if (UNLIKELY(IsIOError(overlay_result))) { - return base::unexpected(overlay_result.error()); - } - if (!overlay_result.has_value()) { - continue; - } - - if (!overlay_result->config.isBetterThan(result->config, desired_config) - && overlay_result->config.compare(result->config) != 0) { - // The configuration of the entry for the overlay must be equal to or better than the target - // configuration to be chosen as the better value. - continue; - } - - result->cookie = overlay_result->cookie; - result->entry = overlay_result->entry; - result->config = overlay_result->config; - result->dynamic_ref_table = id_map.overlay_res_maps_.GetOverlayDynamicRefTable(); - - if (UNLIKELY(logging_enabled)) { - last_resolution_.steps.push_back( - Resolution::Step{Resolution::Step::Type::OVERLAID, overlay_result->cookie, - overlay_result->config.toString()}); - last_resolution_.best_package_name = - overlay_result->package_name->c_str(); - overlaid = true; - } + bool has_locale = false; + if (result->config.locale == 0) { + if (default_locale_ != 0) { + ResTable_config conf; + conf.locale = default_locale_; + // Since we know conf has a locale and only a locale, match will tell us if that locale + // matches + has_locale = conf.match(config); } + } else { + has_locale = true; + } + + // if we don't have a result yet + if (!final_result || + // or this config is better before the locale than the existing result + result->config.isBetterThanBeforeLocale(final_result->config, desired_config) || + // or the existing config isn't better before locale and this one specifies a locale + // whereas the existing one doesn't + (!final_result->config.isBetterThanBeforeLocale(result->config, desired_config) + && has_locale && !final_has_locale)) { + final_result = result.value(); + final_overlaid = overlaid; + final_has_locale = has_locale; } } if (UNLIKELY(logging_enabled)) { - last_resolution_.cookie = result->cookie; - last_resolution_.type_string_ref = result->type_string_ref; - last_resolution_.entry_string_ref = result->entry_string_ref; - last_resolution_.best_config_name = result->config.toString(); - if (!overlaid) { - last_resolution_.best_package_name = result->package_name->c_str(); + last_resolution_.cookie = final_result->cookie; + last_resolution_.type_string_ref = final_result->type_string_ref; + last_resolution_.entry_string_ref = final_result->entry_string_ref; + last_resolution_.best_config_name = final_result->config.toString(); + if (!final_overlaid) { + last_resolution_.best_package_name = final_result->package_name->c_str(); } } - return result; + return *final_result; } base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntryInternal( @@ -778,8 +821,10 @@ base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntryInternal( // If `desired_config` is not the same as the set configuration or the caller will accept a value // from any configuration, then we cannot use our filtered list of types since it only it contains // types matched to the set configuration. - const bool use_filtered = !ignore_configuration && &desired_config == &configuration_; - + const bool use_filtered = !ignore_configuration && std::find_if( + configurations_.begin(), configurations_.end(), + [&desired_config](auto& value) { return &desired_config == &value; }) + != configurations_.end(); const size_t package_count = package_group.packages_.size(); for (size_t pi = 0; pi < package_count; pi++) { const ConfiguredPackage& loaded_package_impl = package_group.packages_[pi]; @@ -934,10 +979,22 @@ std::string AssetManager2::GetLastResourceResolution() const { } std::stringstream log_stream; - log_stream << base::StringPrintf("Resolution for 0x%08x %s\n" - "\tFor config - %s", resid, resource_name_string.c_str(), - configuration_.toString().c_str()); - + if (configurations_.size() == 1) { + log_stream << base::StringPrintf("Resolution for 0x%08x %s\n" + "\tFor config - %s", resid, resource_name_string.c_str(), + configurations_[0].toString().c_str()); + } else { + ResTable_config conf = configurations_[0]; + conf.clearLocale(); + log_stream << base::StringPrintf("Resolution for 0x%08x %s\n\tFor config - %s and locales", + resid, resource_name_string.c_str(), conf.toString().c_str()); + char str[40]; + str[0] = '\0'; + for(auto iter = configurations_.begin(); iter < configurations_.end(); iter++) { + iter->getBcp47Locale(str); + log_stream << base::StringPrintf(" %s%s", str, iter < configurations_.end() ? "," : ""); + } + } for (const Resolution::Step& step : last_resolution_.steps) { constexpr static std::array kStepStrings = { "Found initial", @@ -1427,11 +1484,14 @@ void AssetManager2::RebuildFilterList() { package.loaded_package_->ForEachTypeSpec([&](const TypeSpec& type_spec, uint8_t type_id) { FilteredConfigGroup* group = nullptr; for (const auto& type_entry : type_spec.type_entries) { - if (type_entry.config.match(configuration_)) { - if (!group) { - group = &package.filtered_configs_.editItemAt(type_id - 1); + for (auto & config : configurations_) { + if (type_entry.config.match(config)) { + if (!group) { + group = &package.filtered_configs_.editItemAt(type_id - 1); + } + group->type_entries.push_back(&type_entry); + break; } - group->type_entries.push_back(&type_entry); } } }); diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp index 5a636128e076..06d19e064c91 100644 --- a/libs/androidfw/ResourceTypes.cpp +++ b/libs/androidfw/ResourceTypes.cpp @@ -2568,6 +2568,22 @@ bool ResTable_config::isLocaleBetterThan(const ResTable_config& o, return false; } +bool ResTable_config::isBetterThanBeforeLocale(const ResTable_config& o, + const ResTable_config* requested) const { + if (requested) { + if (imsi || o.imsi) { + if ((mcc != o.mcc) && requested->mcc) { + return (mcc); + } + + if ((mnc != o.mnc) && requested->mnc) { + return (mnc); + } + } + } + return false; +} + bool ResTable_config::isBetterThan(const ResTable_config& o, const ResTable_config* requested) const { if (requested) { diff --git a/libs/androidfw/include/androidfw/AssetManager2.h b/libs/androidfw/include/androidfw/AssetManager2.h index f611d0d8566a..d9ff35b49e0a 100644 --- a/libs/androidfw/include/androidfw/AssetManager2.h +++ b/libs/androidfw/include/androidfw/AssetManager2.h @@ -100,7 +100,7 @@ class AssetManager2 { using ApkAssetsWPtr = wp<const ApkAssets>; using ApkAssetsList = std::span<const ApkAssetsPtr>; - AssetManager2() = default; + AssetManager2(); explicit AssetManager2(AssetManager2&& other) = default; AssetManager2(ApkAssetsList apk_assets, const ResTable_config& configuration); @@ -156,10 +156,14 @@ class AssetManager2 { // Sets/resets the configuration for this AssetManager. This will cause all // caches that are related to the configuration change to be invalidated. - void SetConfiguration(const ResTable_config& configuration); + void SetConfigurations(std::vector<ResTable_config> configurations); - inline const ResTable_config& GetConfiguration() const { - return configuration_; + inline const std::vector<ResTable_config>& GetConfigurations() const { + return configurations_; + } + + inline void SetDefaultLocale(uint32_t default_locale) { + default_locale_ = default_locale; } // Returns all configurations for which there are resources defined, or an I/O error if reading @@ -465,9 +469,11 @@ class AssetManager2 { // without taking too much memory. std::array<uint8_t, std::numeric_limits<uint8_t>::max() + 1> package_ids_; - // The current configuration set for this AssetManager. When this changes, cached resources + uint32_t default_locale_; + + // The current configurations set for this AssetManager. When this changes, cached resources // may need to be purged. - ResTable_config configuration_ = {}; + std::vector<ResTable_config> configurations_; // Cached set of bags. These are cached because they can inherit keys from parent bags, // which involves some calculation. diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index 4eb1d7a2d9ae..52666ab8d1d5 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -1375,6 +1375,8 @@ struct ResTable_config // match the requested configuration at all. bool isLocaleBetterThan(const ResTable_config& o, const ResTable_config* requested) const; + bool isBetterThanBeforeLocale(const ResTable_config& o, const ResTable_config* requested) const; + String8 toString() const; }; diff --git a/libs/androidfw/tests/AssetManager2_bench.cpp b/libs/androidfw/tests/AssetManager2_bench.cpp index 6fae72a6d10e..2caa98c35971 100644 --- a/libs/androidfw/tests/AssetManager2_bench.cpp +++ b/libs/androidfw/tests/AssetManager2_bench.cpp @@ -228,10 +228,12 @@ static void BM_AssetManagerSetConfigurationFramework(benchmark::State& state) { ResTable_config config; memset(&config, 0, sizeof(config)); + std::vector<ResTable_config> configs; + configs.push_back(config); while (state.KeepRunning()) { - config.sdkVersion = ~config.sdkVersion; - assets.SetConfiguration(config); + configs[0].sdkVersion = ~configs[0].sdkVersion; + assets.SetConfigurations(configs); } } BENCHMARK(BM_AssetManagerSetConfigurationFramework); diff --git a/libs/androidfw/tests/AssetManager2_test.cpp b/libs/androidfw/tests/AssetManager2_test.cpp index df3fa02ce44c..c62f095e9dac 100644 --- a/libs/androidfw/tests/AssetManager2_test.cpp +++ b/libs/androidfw/tests/AssetManager2_test.cpp @@ -113,7 +113,7 @@ TEST_F(AssetManager2Test, FindsResourceFromSingleApkAssets) { desired_config.language[1] = 'e'; AssetManager2 assetmanager; - assetmanager.SetConfiguration(desired_config); + assetmanager.SetConfigurations({desired_config}); assetmanager.SetApkAssets({basic_assets_}); auto value = assetmanager.GetResource(basic::R::string::test1); @@ -137,7 +137,7 @@ TEST_F(AssetManager2Test, FindsResourceFromMultipleApkAssets) { desired_config.language[1] = 'e'; AssetManager2 assetmanager; - assetmanager.SetConfiguration(desired_config); + assetmanager.SetConfigurations({desired_config}); assetmanager.SetApkAssets({basic_assets_, basic_de_fr_assets_}); auto value = assetmanager.GetResource(basic::R::string::test1); @@ -466,10 +466,10 @@ TEST_F(AssetManager2Test, ResolveDeepIdReference) { TEST_F(AssetManager2Test, DensityOverride) { AssetManager2 assetmanager; assetmanager.SetApkAssets({basic_assets_, basic_xhdpi_assets_, basic_xxhdpi_assets_}); - assetmanager.SetConfiguration({ + assetmanager.SetConfigurations({{ .density = ResTable_config::DENSITY_XHIGH, .sdkVersion = 21, - }); + }}); auto value = assetmanager.GetResource(basic::R::string::density, false /*may_be_bag*/); ASSERT_TRUE(value.has_value()); @@ -721,7 +721,7 @@ TEST_F(AssetManager2Test, GetLastPathWithoutEnablingReturnsEmpty) { ResTable_config desired_config; AssetManager2 assetmanager; - assetmanager.SetConfiguration(desired_config); + assetmanager.SetConfigurations({desired_config}); assetmanager.SetApkAssets({basic_assets_}); assetmanager.SetResourceResolutionLoggingEnabled(false); @@ -736,7 +736,7 @@ TEST_F(AssetManager2Test, GetLastPathWithoutResolutionReturnsEmpty) { ResTable_config desired_config; AssetManager2 assetmanager; - assetmanager.SetConfiguration(desired_config); + assetmanager.SetConfigurations({desired_config}); assetmanager.SetApkAssets({basic_assets_}); auto result = assetmanager.GetLastResourceResolution(); @@ -751,7 +751,7 @@ TEST_F(AssetManager2Test, GetLastPathWithSingleApkAssets) { AssetManager2 assetmanager; assetmanager.SetResourceResolutionLoggingEnabled(true); - assetmanager.SetConfiguration(desired_config); + assetmanager.SetConfigurations({desired_config}); assetmanager.SetApkAssets({basic_assets_}); auto value = assetmanager.GetResource(basic::R::string::test1); @@ -774,7 +774,7 @@ TEST_F(AssetManager2Test, GetLastPathWithMultipleApkAssets) { AssetManager2 assetmanager; assetmanager.SetResourceResolutionLoggingEnabled(true); - assetmanager.SetConfiguration(desired_config); + assetmanager.SetConfigurations({desired_config}); assetmanager.SetApkAssets({basic_assets_, basic_de_fr_assets_}); auto value = assetmanager.GetResource(basic::R::string::test1); @@ -796,7 +796,7 @@ TEST_F(AssetManager2Test, GetLastPathAfterDisablingReturnsEmpty) { AssetManager2 assetmanager; assetmanager.SetResourceResolutionLoggingEnabled(true); - assetmanager.SetConfiguration(desired_config); + assetmanager.SetConfigurations({desired_config}); assetmanager.SetApkAssets({basic_assets_}); auto value = assetmanager.GetResource(basic::R::string::test1); @@ -817,7 +817,7 @@ TEST_F(AssetManager2Test, GetOverlayablesToString) { AssetManager2 assetmanager; assetmanager.SetResourceResolutionLoggingEnabled(true); - assetmanager.SetConfiguration(desired_config); + assetmanager.SetConfigurations({desired_config}); assetmanager.SetApkAssets({overlayable_assets_}); const auto map = assetmanager.GetOverlayableMapForPackage(0x7f); diff --git a/libs/androidfw/tests/BenchmarkHelpers.cpp b/libs/androidfw/tests/BenchmarkHelpers.cpp index b97dd96f8934..8b883f4ed1df 100644 --- a/libs/androidfw/tests/BenchmarkHelpers.cpp +++ b/libs/androidfw/tests/BenchmarkHelpers.cpp @@ -66,7 +66,7 @@ void GetResourceBenchmark(const std::vector<std::string>& paths, const ResTable_ AssetManager2 assetmanager; assetmanager.SetApkAssets(apk_assets); if (config != nullptr) { - assetmanager.SetConfiguration(*config); + assetmanager.SetConfigurations({*config}); } while (state.KeepRunning()) { diff --git a/libs/androidfw/tests/Theme_test.cpp b/libs/androidfw/tests/Theme_test.cpp index e08a6a7f277d..181d1411fb91 100644 --- a/libs/androidfw/tests/Theme_test.cpp +++ b/libs/androidfw/tests/Theme_test.cpp @@ -260,7 +260,7 @@ TEST_F(ThemeTest, ThemeRebase) { ResTable_config night{}; night.uiMode = ResTable_config::UI_MODE_NIGHT_YES; night.version = 8u; - am_night.SetConfiguration(night); + am_night.SetConfigurations({night}); auto theme = am.NewTheme(); { diff --git a/libs/hwui/Mesh.h b/libs/hwui/Mesh.h index 13e3c8e7bf77..764d1efcc8f4 100644 --- a/libs/hwui/Mesh.h +++ b/libs/hwui/Mesh.h @@ -19,6 +19,7 @@ #include <GrDirectContext.h> #include <SkMesh.h> +#include <include/gpu/ganesh/SkMeshGanesh.h> #include <jni.h> #include <log/log.h> @@ -143,14 +144,26 @@ public: } if (mIsDirty || genId != mGenerationId) { - auto vb = SkMesh::MakeVertexBuffer( - context, reinterpret_cast<const void*>(mVertexBufferData.data()), - mVertexBufferData.size()); + auto vertexData = reinterpret_cast<const void*>(mVertexBufferData.data()); +#ifdef __ANDROID__ + auto vb = SkMeshes::MakeVertexBuffer(context, + vertexData, + mVertexBufferData.size()); +#else + auto vb = SkMeshes::MakeVertexBuffer(vertexData, + mVertexBufferData.size()); +#endif auto meshMode = SkMesh::Mode(mMode); if (!mIndexBufferData.empty()) { - auto ib = SkMesh::MakeIndexBuffer( - context, reinterpret_cast<const void*>(mIndexBufferData.data()), - mIndexBufferData.size()); + auto indexData = reinterpret_cast<const void*>(mIndexBufferData.data()); +#ifdef __ANDROID__ + auto ib = SkMeshes::MakeIndexBuffer(context, + indexData, + mIndexBufferData.size()); +#else + auto ib = SkMeshes::MakeIndexBuffer(indexData, + mIndexBufferData.size()); +#endif mMesh = SkMesh::MakeIndexed(mMeshSpec, meshMode, vb, mVertexCount, mVertexOffset, ib, mIndexCount, mIndexOffset, mBuilder->fUniforms, mBounds) diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp index 3cd0e75c17c2..71f47e92e055 100644 --- a/libs/hwui/RecordingCanvas.cpp +++ b/libs/hwui/RecordingCanvas.cpp @@ -36,6 +36,7 @@ #include "SkImageFilter.h" #include "SkImageInfo.h" #include "SkLatticeIter.h" +#include "SkMesh.h" #include "SkPaint.h" #include "SkPicture.h" #include "SkRRect.h" @@ -49,6 +50,7 @@ #include "effects/GainmapRenderer.h" #include "include/gpu/GpuTypes.h" // from Skia #include "include/gpu/GrDirectContext.h" +#include "include/gpu/ganesh/SkMeshGanesh.h" #include "pipeline/skia/AnimatedDrawables.h" #include "pipeline/skia/FunctorDrawable.h" #ifdef __ANDROID__ @@ -527,12 +529,13 @@ struct DrawSkMesh final : Op { mutable bool isGpuBased; mutable GrDirectContext::DirectContextID contextId; void draw(SkCanvas* c, const SkMatrix&) const { +#ifdef __ANDROID__ GrDirectContext* directContext = c->recordingContext()->asDirectContext(); GrDirectContext::DirectContextID id = directContext->directContextID(); if (!isGpuBased || contextId != id) { sk_sp<SkMesh::VertexBuffer> vb = - SkMesh::CopyVertexBuffer(directContext, cpuMesh.refVertexBuffer()); + SkMeshes::CopyVertexBuffer(directContext, cpuMesh.refVertexBuffer()); if (!cpuMesh.indexBuffer()) { gpuMesh = SkMesh::Make(cpuMesh.refSpec(), cpuMesh.mode(), vb, cpuMesh.vertexCount(), cpuMesh.vertexOffset(), cpuMesh.refUniforms(), @@ -540,7 +543,7 @@ struct DrawSkMesh final : Op { .mesh; } else { sk_sp<SkMesh::IndexBuffer> ib = - SkMesh::CopyIndexBuffer(directContext, cpuMesh.refIndexBuffer()); + SkMeshes::CopyIndexBuffer(directContext, cpuMesh.refIndexBuffer()); gpuMesh = SkMesh::MakeIndexed(cpuMesh.refSpec(), cpuMesh.mode(), vb, cpuMesh.vertexCount(), cpuMesh.vertexOffset(), ib, cpuMesh.indexCount(), cpuMesh.indexOffset(), @@ -553,6 +556,9 @@ struct DrawSkMesh final : Op { } c->drawMesh(gpuMesh, blender, paint); +#else + c->drawMesh(cpuMesh, blender, paint); +#endif } }; diff --git a/libs/hwui/SkiaCanvas.h b/libs/hwui/SkiaCanvas.h index b785989f35cb..ced02241ffe2 100644 --- a/libs/hwui/SkiaCanvas.h +++ b/libs/hwui/SkiaCanvas.h @@ -175,7 +175,7 @@ protected: const Paint& paint, const SkPath& path, size_t start, size_t end) override; - void onFilterPaint(Paint& paint); + virtual void onFilterPaint(Paint& paint); Paint filterPaint(const Paint& src) { Paint dst(src); diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp index 58c14c1fabbd..e917f9a66917 100644 --- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp +++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp @@ -236,6 +236,17 @@ void SkiaRecordingCanvas::handleMutableImages(Bitmap& bitmap, DrawImagePayload& } } +void SkiaRecordingCanvas::onFilterPaint(android::Paint& paint) { + INHERITED::onFilterPaint(paint); + SkShader* shader = paint.getShader(); + // TODO(b/264559422): This only works for very specifically a BitmapShader. + // It's better than nothing, though + SkImage* image = shader ? shader->isAImage(nullptr, nullptr) : nullptr; + if (image) { + mDisplayList->mMutableImages.push_back(image); + } +} + void SkiaRecordingCanvas::drawBitmap(Bitmap& bitmap, float left, float top, const Paint* paint) { auto payload = DrawImagePayload(bitmap); diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h index a8e4580dc200..3bd091df1ece 100644 --- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h +++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h @@ -105,6 +105,8 @@ private: void handleMutableImages(Bitmap& bitmap, DrawImagePayload& payload); + void onFilterPaint(Paint& paint) override; + using INHERITED = SkiaCanvas; }; diff --git a/libs/hwui/renderthread/RenderProxy.cpp b/libs/hwui/renderthread/RenderProxy.cpp index 224c878bf43d..f949dddd8b44 100644 --- a/libs/hwui/renderthread/RenderProxy.cpp +++ b/libs/hwui/renderthread/RenderProxy.cpp @@ -16,7 +16,13 @@ #include "RenderProxy.h" +#include <SkBitmap.h> +#include <SkImage.h> +#include <SkPicture.h> #include <gui/TraceUtils.h> +#include <pthread.h> +#include <ui/GraphicBufferAllocator.h> + #include "DeferredLayerUpdater.h" #include "DisplayList.h" #include "Properties.h" @@ -29,12 +35,6 @@ #include "utils/Macros.h" #include "utils/TimeUtils.h" -#include <SkBitmap.h> -#include <SkImage.h> -#include <SkPicture.h> - -#include <pthread.h> - namespace android { namespace uirenderer { namespace renderthread { @@ -323,6 +323,9 @@ void RenderProxy::dumpGraphicsMemory(int fd, bool includeProfileData, bool reset } }); } + std::string grallocInfo; + GraphicBufferAllocator::getInstance().dump(grallocInfo); + dprintf(fd, "%s\n", grallocInfo.c_str()); } void RenderProxy::getMemoryUsage(size_t* cpuUsage, size_t* gpuUsage) { diff --git a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp index f6be7b20a9e2..064d42ec8941 100644 --- a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp +++ b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp @@ -129,6 +129,33 @@ TEST(SkiaDisplayList, syncContexts) { EXPECT_EQ(counts.destroyed, 1); } +TEST(SkiaDisplayList, recordMutableBitmap) { + SkiaRecordingCanvas canvas{nullptr, 100, 100}; + auto bitmap = Bitmap::allocateHeapBitmap(SkImageInfo::Make( + 10, 20, SkColorType::kN32_SkColorType, SkAlphaType::kPremul_SkAlphaType)); + EXPECT_FALSE(bitmap->isImmutable()); + canvas.drawBitmap(*bitmap, 0, 0, nullptr); + auto displayList = canvas.finishRecording(); + ASSERT_EQ(1, displayList->mMutableImages.size()); + EXPECT_EQ(10, displayList->mMutableImages[0]->width()); + EXPECT_EQ(20, displayList->mMutableImages[0]->height()); +} + +TEST(SkiaDisplayList, recordMutableBitmapInShader) { + SkiaRecordingCanvas canvas{nullptr, 100, 100}; + auto bitmap = Bitmap::allocateHeapBitmap(SkImageInfo::Make( + 10, 20, SkColorType::kN32_SkColorType, SkAlphaType::kPremul_SkAlphaType)); + EXPECT_FALSE(bitmap->isImmutable()); + SkSamplingOptions sampling(SkFilterMode::kLinear, SkMipmapMode::kNone); + Paint paint; + paint.setShader(bitmap->makeImage()->makeShader(sampling)); + canvas.drawPaint(paint); + auto displayList = canvas.finishRecording(); + ASSERT_EQ(1, displayList->mMutableImages.size()); + EXPECT_EQ(10, displayList->mMutableImages[0]->width()); + EXPECT_EQ(20, displayList->mMutableImages[0]->height()); +} + class ContextFactory : public IContextFactory { public: virtual AnimationContext* createAnimationContext(renderthread::TimeLord& clock) override { diff --git a/media/java/android/media/midi/MidiDeviceService.java b/media/java/android/media/midi/MidiDeviceService.java index 388d95bba16b..96540a22c978 100644 --- a/media/java/android/media/midi/MidiDeviceService.java +++ b/media/java/android/media/midi/MidiDeviceService.java @@ -34,15 +34,15 @@ import android.util.Log; * <p>To extend this class, you must declare the service in your manifest file with * an intent filter with the {@link #SERVICE_INTERFACE} action * and meta-data to describe the virtual device. - For example:</p> + * For example:</p> * <pre> * <service android:name=".VirtualDeviceService" - * android:label="@string/service_name"> + * android:label="@string/service_name"> * <intent-filter> - * <action android:name="android.media.midi.MidiDeviceService" /> + * <action android:name="android.media.midi.MidiDeviceService" /> * </intent-filter> - * <meta-data android:name="android.media.midi.MidiDeviceService" - android:resource="@xml/device_info" /> + * <meta-data android:name="android.media.midi.MidiDeviceService" + * android:resource="@xml/device_info" /> * </service></pre> */ abstract public class MidiDeviceService extends Service { @@ -114,8 +114,8 @@ abstract public class MidiDeviceService extends Service { } /** - * returns the {@link MidiDeviceInfo} instance for this service - * @return our MidiDeviceInfo + * Returns the {@link MidiDeviceInfo} instance for this service + * @return the MidiDeviceInfo of the virtual MIDI device */ public final MidiDeviceInfo getDeviceInfo() { return mDeviceInfo; @@ -123,13 +123,14 @@ abstract public class MidiDeviceService extends Service { /** * Called to notify when an our {@link MidiDeviceStatus} has changed - * @param status the number of the port that was opened + * @param status the current status of the MIDI device */ public void onDeviceStatusChanged(MidiDeviceStatus status) { } /** - * Called to notify when our device has been closed by all its clients + * Called to notify when the virtual MIDI device running in this service has been closed by + * all its clients */ public void onClose() { } diff --git a/media/java/android/media/midi/MidiUmpDeviceService.java b/media/java/android/media/midi/MidiUmpDeviceService.java index d189e3a88f03..0c6096ecad4d 100644 --- a/media/java/android/media/midi/MidiUmpDeviceService.java +++ b/media/java/android/media/midi/MidiUmpDeviceService.java @@ -48,9 +48,9 @@ import java.util.List; * <service android:name=".VirtualDeviceService" * android:label="@string/service_name"> * <intent-filter> - * <action android:name="android.media.midi.MidiUmpDeviceService" /> + * <action android:name="android.media.midi.MidiUmpDeviceService" /> * </intent-filter> - * <property android:name="android.media.midi.MidiUmpDeviceService" + * <property android:name="android.media.midi.MidiUmpDeviceService" * android:resource="@xml/device_info" /> * </service></pre> */ diff --git a/native/android/configuration.cpp b/native/android/configuration.cpp index b50514d27bac..283445fc8a9a 100644 --- a/native/android/configuration.cpp +++ b/native/android/configuration.cpp @@ -36,7 +36,7 @@ void AConfiguration_delete(AConfiguration* config) { void AConfiguration_fromAssetManager(AConfiguration* out, AAssetManager* am) { ScopedLock<AssetManager2> locked_mgr(*AssetManagerForNdkAssetManager(am)); - ResTable_config config = locked_mgr->GetConfiguration(); + ResTable_config config = locked_mgr->GetConfigurations()[0]; // AConfiguration is not a virtual subclass, so we can memcpy. memcpy(out, &config, sizeof(config)); diff --git a/packages/SettingsLib/res/values/arrays.xml b/packages/SettingsLib/res/values/arrays.xml index 3adb882bc1b1..3252aad6262f 100644 --- a/packages/SettingsLib/res/values/arrays.xml +++ b/packages/SettingsLib/res/values/arrays.xml @@ -660,4 +660,20 @@ <item>2.0</item> </string-array> + <!-- Grammatical genders --> + <array name="grammatical_gender_entries"> + <item>@string/not_specified</item> + <item>@string/neuter</item> + <item>@string/feminine</item> + <item>@string/masculine</item> + </array> + + <!-- Values for grammatical genders --> + <string-array name="grammatical_gender_values"> + <item>0</item> + <item>1</item> + <item>2</item> + <item>3</item> + </string-array> + </resources> diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index 60bc226b5af5..dab3bcb9ed9c 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -1678,4 +1678,13 @@ <!-- Formatting states for the scale of font size, in percent. Double "%" is required to represent the symbol "%". [CHAR LIMIT=20] --> <string name="font_scale_percentage"> <xliff:g id="percentage">%1$d</xliff:g> %%</string> + + <!-- List entry in developer settings to set the grammatical gender to Not specified [CHAR LIMIT=30]--> + <string name="not_specified">Not specified</string> + <!-- List entry in developer settings to set the grammatical gender to Neuter [CHAR LIMIT=30]--> + <string name="neuter">Neuter</string> + <!-- List entry in developer settings to set the grammatical gender to Feminine [CHAR LIMIT=30]--> + <string name="feminine">Feminine</string> + <!-- List entry in developer settings to set the grammatical gender to Masculine [CHAR LIMIT=30]--> + <string name="masculine">Masculine</string> </resources> diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 0f16d930dcf7..78d9361272d2 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -228,6 +228,17 @@ filegroup { } filegroup { + name: "SystemUI-test-fakes", + srcs: [ + /* Status bar fakes */ + "tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt", + "tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt", + "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt", + ], + path: "tests/src", +} + +filegroup { name: "SystemUI-tests-robolectric-pilots", srcs: [ /* Keyguard converted tests */ @@ -291,6 +302,11 @@ filegroup { "tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewLegacyControllerWithCoroutinesTest.kt", "tests/src/com/android/systemui/biometrics/UdfpsShellTest.kt", "tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt", + + /* Status bar wifi converted tests */ + "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt", + "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt", + "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt", ], path: "tests/src", } @@ -449,6 +465,7 @@ android_robolectric_test { "tests/robolectric/src/**/*.kt", "tests/robolectric/src/**/*.java", ":SystemUI-tests-utils", + ":SystemUI-test-fakes", ":SystemUI-tests-robolectric-pilots", ], static_libs: [ diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING index 7a5a3823d466..c59b0f9c8e9a 100644 --- a/packages/SystemUI/TEST_MAPPING +++ b/packages/SystemUI/TEST_MAPPING @@ -72,7 +72,7 @@ "exclude-annotation": "org.junit.Ignore" }, { - "exclude-annotation": "androidx.test.filters.FlakyTest" + "exclude-annotation": "android.platform.test.annotations.FlakyTest" }, { "include-filter": "android.permissionui.cts.CameraMicIndicatorsPermissionTest" @@ -110,6 +110,17 @@ ] } ], + "postsubmit": [ + { + // Permission indicators + "name": "CtsPermissionUiTestCases", + "options": [ + { + "include-filter": "android.permissionui.cts.CameraMicIndicatorsPermissionTest" + } + ] + } + ], "silver-sysui": [ { "name": "PlatformScenarioTests", diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt index dbfa192f5ec4..37b1ee543e46 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/DialogLaunchAnimator.kt @@ -32,11 +32,10 @@ import android.view.ViewRootImpl import android.view.WindowInsets import android.view.WindowManager import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS -import android.widget.FrameLayout import com.android.app.animation.Interpolators import com.android.internal.jank.InteractionJankMonitor import com.android.internal.jank.InteractionJankMonitor.CujType -import com.android.systemui.animation.view.LaunchableFrameLayout +import com.android.systemui.util.maybeForceFullscreen import com.android.systemui.util.registerAnimationOnBackInvoked import kotlin.math.roundToInt @@ -622,96 +621,12 @@ private class AnimatedDialog( viewGroupWithBackground } else { - // We will make the dialog window (and therefore its DecorView) fullscreen to make - // it possible to animate outside its bounds. - // - // Before that, we add a new View as a child of the DecorView with the same size and - // gravity as that DecorView, then we add all original children of the DecorView to - // that new View. Finally we remove the background of the DecorView and add it to - // the new View, then we make the DecorView fullscreen. This new View now acts as a - // fake (non fullscreen) window. - // - // On top of that, we also add a fullscreen transparent background between the - // DecorView and the view that we added so that we can dismiss the dialog when this - // view is clicked. This is necessary because DecorView overrides onTouchEvent and - // therefore we can't set the click listener directly on the (now fullscreen) - // DecorView. - val fullscreenTransparentBackground = FrameLayout(dialog.context) - decorView.addView( - fullscreenTransparentBackground, - 0 /* index */, - FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) - ) - - val dialogContentWithBackground = LaunchableFrameLayout(dialog.context) - dialogContentWithBackground.background = decorView.background - - // Make the window background transparent. Note that setting the window (or - // DecorView) background drawable to null leads to issues with background color (not - // being transparent) or with insets that are not refreshed. Therefore we need to - // set it to something not null, hence we are using android.R.color.transparent - // here. - window.setBackgroundDrawableResource(android.R.color.transparent) - - // Close the dialog when clicking outside of it. - fullscreenTransparentBackground.setOnClickListener { dialog.dismiss() } - dialogContentWithBackground.isClickable = true - - // Make sure the transparent and dialog backgrounds are not focusable by - // accessibility - // features. - fullscreenTransparentBackground.importantForAccessibility = - View.IMPORTANT_FOR_ACCESSIBILITY_NO - dialogContentWithBackground.importantForAccessibility = - View.IMPORTANT_FOR_ACCESSIBILITY_NO - - fullscreenTransparentBackground.addView( - dialogContentWithBackground, - FrameLayout.LayoutParams( - window.attributes.width, - window.attributes.height, - window.attributes.gravity - ) - ) - - // Move all original children of the DecorView to the new View we just added. - for (i in 1 until decorView.childCount) { - val view = decorView.getChildAt(1) - decorView.removeViewAt(1) - dialogContentWithBackground.addView(view) - } - - // Make the window fullscreen and add a layout listener to ensure it stays - // fullscreen. - window.setLayout(MATCH_PARENT, MATCH_PARENT) - decorViewLayoutListener = - View.OnLayoutChangeListener { - v, - left, - top, - right, - bottom, - oldLeft, - oldTop, - oldRight, - oldBottom -> - if ( - window.attributes.width != MATCH_PARENT || - window.attributes.height != MATCH_PARENT - ) { - // The dialog size changed, copy its size to dialogContentWithBackground - // and make the dialog window full screen again. - val layoutParams = dialogContentWithBackground.layoutParams - layoutParams.width = window.attributes.width - layoutParams.height = window.attributes.height - dialogContentWithBackground.layoutParams = layoutParams - window.setLayout(MATCH_PARENT, MATCH_PARENT) - } - } - decorView.addOnLayoutChangeListener(decorViewLayoutListener) - + val (dialogContentWithBackground, decorViewLayoutListener) = + dialog.maybeForceFullscreen()!! + this.decorViewLayoutListener = decorViewLayoutListener dialogContentWithBackground } + this.dialogContentWithBackground = dialogContentWithBackground dialogContentWithBackground.setTag(R.id.tag_dialog_background, true) diff --git a/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt b/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt index 428856dc5f30..0f63548b6f0c 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/util/Dialog.kt @@ -18,6 +18,9 @@ package com.android.systemui.util import android.app.Dialog import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.widget.FrameLayout import android.window.OnBackInvokedDispatcher import com.android.systemui.animation.back.BackAnimationSpec import com.android.systemui.animation.back.BackTransformation @@ -25,6 +28,7 @@ import com.android.systemui.animation.back.applyTo import com.android.systemui.animation.back.floatingSystemSurfacesForSysUi import com.android.systemui.animation.back.onBackAnimationCallbackFrom import com.android.systemui.animation.back.registerOnBackInvokedCallbackOnViewAttached +import com.android.systemui.animation.view.LaunchableFrameLayout /** * Register on the Dialog's [OnBackInvokedDispatcher] an animation using the [BackAnimationSpec]. @@ -49,3 +53,110 @@ fun Dialog.registerAnimationOnBackInvoked( ), ) } + +/** + * Make the dialog window (and therefore its DecorView) fullscreen to make it possible to animate + * outside its bounds. No-op if the dialog is already fullscreen. + * + * <p>Returns null if the dialog is already fullscreen. Otherwise, returns a pair containing a view + * and a layout listener. The new view matches the original dialog DecorView in size, position, and + * background. This new view will be a child of the modified, transparent, fullscreen DecorView. The + * layout listener is listening to changes to the modified DecorView. It is the responsibility of + * the caller to deregister the listener when the dialog is dismissed. + */ +fun Dialog.maybeForceFullscreen(): Pair<LaunchableFrameLayout, View.OnLayoutChangeListener>? { + // Create the dialog so that its onCreate() method is called, which usually sets the dialog + // content. + create() + + val window = window!! + val decorView = window.decorView as ViewGroup + + val isWindowFullscreen = + window.attributes.width == MATCH_PARENT && window.attributes.height == MATCH_PARENT + if (isWindowFullscreen) { + return null + } + + // We will make the dialog window (and therefore its DecorView) fullscreen to make it possible + // to animate outside its bounds. + // + // Before that, we add a new View as a child of the DecorView with the same size and gravity as + // that DecorView, then we add all original children of the DecorView to that new View. Finally + // we remove the background of the DecorView and add it to the new View, then we make the + // DecorView fullscreen. This new View now acts as a fake (non fullscreen) window. + // + // On top of that, we also add a fullscreen transparent background between the DecorView and the + // view that we added so that we can dismiss the dialog when this view is clicked. This is + // necessary because DecorView overrides onTouchEvent and therefore we can't set the click + // listener directly on the (now fullscreen) DecorView. + val fullscreenTransparentBackground = FrameLayout(context) + decorView.addView( + fullscreenTransparentBackground, + 0 /* index */, + FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + ) + + val dialogContentWithBackground = LaunchableFrameLayout(context) + dialogContentWithBackground.background = decorView.background + + // Make the window background transparent. Note that setting the window (or DecorView) + // background drawable to null leads to issues with background color (not being transparent) or + // with insets that are not refreshed. Therefore we need to set it to something not null, hence + // we are using android.R.color.transparent here. + window.setBackgroundDrawableResource(android.R.color.transparent) + + // Close the dialog when clicking outside of it. + fullscreenTransparentBackground.setOnClickListener { dismiss() } + dialogContentWithBackground.isClickable = true + + // Make sure the transparent and dialog backgrounds are not focusable by accessibility + // features. + fullscreenTransparentBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO + dialogContentWithBackground.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO + + fullscreenTransparentBackground.addView( + dialogContentWithBackground, + FrameLayout.LayoutParams( + window.attributes.width, + window.attributes.height, + window.attributes.gravity + ) + ) + + // Move all original children of the DecorView to the new View we just added. + for (i in 1 until decorView.childCount) { + val view = decorView.getChildAt(1) + decorView.removeViewAt(1) + dialogContentWithBackground.addView(view) + } + + // Make the window fullscreen and add a layout listener to ensure it stays fullscreen. + window.setLayout(MATCH_PARENT, MATCH_PARENT) + val decorViewLayoutListener = + View.OnLayoutChangeListener { + v, + left, + top, + right, + bottom, + oldLeft, + oldTop, + oldRight, + oldBottom -> + if ( + window.attributes.width != MATCH_PARENT || window.attributes.height != MATCH_PARENT + ) { + // The dialog size changed, copy its size to dialogContentWithBackground and make + // the dialog window full screen again. + val layoutParams = dialogContentWithBackground.layoutParams + layoutParams.width = window.attributes.width + layoutParams.height = window.attributes.height + dialogContentWithBackground.layoutParams = layoutParams + window.setLayout(MATCH_PARENT, MATCH_PARENT) + } + } + decorView.addOnLayoutChangeListener(decorViewLayoutListener) + + return dialogContentWithBackground to decorViewLayoutListener +} diff --git a/packages/SystemUI/compose/core/src/com/android/compose/pager/Pager.kt b/packages/SystemUI/compose/core/src/com/android/compose/pager/Pager.kt deleted file mode 100644 index a80a1f934dab..000000000000 --- a/packages/SystemUI/compose/core/src/com/android/compose/pager/Pager.kt +++ /dev/null @@ -1,357 +0,0 @@ -/* - * Copyright (C) 2022 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.compose.pager - -import androidx.compose.animation.core.AnimationSpec -import androidx.compose.animation.core.DecayAnimationSpec -import androidx.compose.animation.rememberSplineBasedDecay -import androidx.compose.foundation.gestures.FlingBehavior -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.Stable -import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.Velocity -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.flow.filter - -/** Library-wide switch to turn on debug logging. */ -internal const val DebugLog = false - -@RequiresOptIn(message = "Accompanist Pager is experimental. The API may be changed in the future.") -@Retention(AnnotationRetention.BINARY) -annotation class ExperimentalPagerApi - -/** Contains the default values used by [HorizontalPager] and [VerticalPager]. */ -@ExperimentalPagerApi -object PagerDefaults { - /** - * Remember the default [FlingBehavior] that represents the scroll curve. - * - * @param state The [PagerState] to update. - * @param decayAnimationSpec The decay animation spec to use for decayed flings. - * @param snapAnimationSpec The animation spec to use when snapping. - */ - @Composable - fun flingBehavior( - state: PagerState, - decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(), - snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec, - ): FlingBehavior = - rememberSnappingFlingBehavior( - lazyListState = state.lazyListState, - decayAnimationSpec = decayAnimationSpec, - snapAnimationSpec = snapAnimationSpec, - ) - - @Deprecated( - "Replaced with PagerDefaults.flingBehavior()", - ReplaceWith("PagerDefaults.flingBehavior(state, decayAnimationSpec, snapAnimationSpec)") - ) - @Composable - fun rememberPagerFlingConfig( - state: PagerState, - decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(), - snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec, - ): FlingBehavior = flingBehavior(state, decayAnimationSpec, snapAnimationSpec) -} - -/** - * A horizontally scrolling layout that allows users to flip between items to the left and right. - * - * @param count the number of pages. - * @param modifier the modifier to apply to this layout. - * @param state the state object to be used to control or observe the pager's state. - * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be - * composed from the end to the start and [PagerState.currentPage] == 0 will mean the first item - * is located at the end. - * @param itemSpacing horizontal spacing to add between items. - * @param flingBehavior logic describing fling behavior. - * @param key the scroll position will be maintained based on the key, which means if you add/remove - * items before the current visible item the item with the given key will be kept as the first - * visible one. - * @param content a block which describes the content. Inside this block you can reference - * [PagerScope.currentPage] and other properties in [PagerScope]. - * @sample com.google.accompanist.sample.pager.HorizontalPagerSample - */ -@ExperimentalPagerApi -@Composable -fun HorizontalPager( - count: Int, - modifier: Modifier = Modifier, - state: PagerState = rememberPagerState(), - reverseLayout: Boolean = false, - itemSpacing: Dp = 0.dp, - flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(state), - verticalAlignment: Alignment.Vertical = Alignment.CenterVertically, - key: ((page: Int) -> Any)? = null, - contentPadding: PaddingValues = PaddingValues(0.dp), - content: @Composable PagerScope.(page: Int) -> Unit, -) { - Pager( - count = count, - state = state, - modifier = modifier, - isVertical = false, - reverseLayout = reverseLayout, - itemSpacing = itemSpacing, - verticalAlignment = verticalAlignment, - flingBehavior = flingBehavior, - key = key, - contentPadding = contentPadding, - content = content - ) -} - -/** - * A vertically scrolling layout that allows users to flip between items to the top and bottom. - * - * @param count the number of pages. - * @param modifier the modifier to apply to this layout. - * @param state the state object to be used to control or observe the pager's state. - * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be - * composed from the bottom to the top and [PagerState.currentPage] == 0 will mean the first item - * is located at the bottom. - * @param itemSpacing vertical spacing to add between items. - * @param flingBehavior logic describing fling behavior. - * @param key the scroll position will be maintained based on the key, which means if you add/remove - * items before the current visible item the item with the given key will be kept as the first - * visible one. - * @param content a block which describes the content. Inside this block you can reference - * [PagerScope.currentPage] and other properties in [PagerScope]. - * @sample com.google.accompanist.sample.pager.VerticalPagerSample - */ -@ExperimentalPagerApi -@Composable -fun VerticalPager( - count: Int, - modifier: Modifier = Modifier, - state: PagerState = rememberPagerState(), - reverseLayout: Boolean = false, - itemSpacing: Dp = 0.dp, - flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(state), - horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, - key: ((page: Int) -> Any)? = null, - contentPadding: PaddingValues = PaddingValues(0.dp), - content: @Composable PagerScope.(page: Int) -> Unit, -) { - Pager( - count = count, - state = state, - modifier = modifier, - isVertical = true, - reverseLayout = reverseLayout, - itemSpacing = itemSpacing, - horizontalAlignment = horizontalAlignment, - flingBehavior = flingBehavior, - key = key, - contentPadding = contentPadding, - content = content - ) -} - -@ExperimentalPagerApi -@Composable -internal fun Pager( - count: Int, - modifier: Modifier, - state: PagerState, - reverseLayout: Boolean, - itemSpacing: Dp, - isVertical: Boolean, - flingBehavior: FlingBehavior, - key: ((page: Int) -> Any)?, - contentPadding: PaddingValues, - verticalAlignment: Alignment.Vertical = Alignment.CenterVertically, - horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, - content: @Composable PagerScope.(page: Int) -> Unit, -) { - require(count >= 0) { "pageCount must be >= 0" } - - // Provide our PagerState with access to the SnappingFlingBehavior animation target - // TODO: can this be done in a better way? - state.flingAnimationTarget = { (flingBehavior as? SnappingFlingBehavior)?.animationTarget } - - LaunchedEffect(count) { - state.currentPage = minOf(count - 1, state.currentPage).coerceAtLeast(0) - } - - // Once a fling (scroll) has finished, notify the state - LaunchedEffect(state) { - // When a 'scroll' has finished, notify the state - snapshotFlow { state.isScrollInProgress } - .filter { !it } - .collect { state.onScrollFinished() } - } - - val pagerScope = remember(state) { PagerScopeImpl(state) } - - // We only consume nested flings in the main-axis, allowing cross-axis flings to propagate - // as normal - val consumeFlingNestedScrollConnection = - ConsumeFlingNestedScrollConnection( - consumeHorizontal = !isVertical, - consumeVertical = isVertical, - ) - - if (isVertical) { - LazyColumn( - state = state.lazyListState, - verticalArrangement = Arrangement.spacedBy(itemSpacing, verticalAlignment), - horizontalAlignment = horizontalAlignment, - flingBehavior = flingBehavior, - reverseLayout = reverseLayout, - contentPadding = contentPadding, - modifier = modifier, - ) { - items( - count = count, - key = key, - ) { page -> - Box( - Modifier - // We don't any nested flings to continue in the pager, so we add a - // connection which consumes them. - // See: https://github.com/google/accompanist/issues/347 - .nestedScroll(connection = consumeFlingNestedScrollConnection) - // Constraint the content to be <= than the size of the pager. - .fillParentMaxHeight() - .wrapContentSize() - ) { - pagerScope.content(page) - } - } - } - } else { - LazyRow( - state = state.lazyListState, - verticalAlignment = verticalAlignment, - horizontalArrangement = Arrangement.spacedBy(itemSpacing, horizontalAlignment), - flingBehavior = flingBehavior, - reverseLayout = reverseLayout, - contentPadding = contentPadding, - modifier = modifier, - ) { - items( - count = count, - key = key, - ) { page -> - Box( - Modifier - // We don't any nested flings to continue in the pager, so we add a - // connection which consumes them. - // See: https://github.com/google/accompanist/issues/347 - .nestedScroll(connection = consumeFlingNestedScrollConnection) - // Constraint the content to be <= than the size of the pager. - .fillParentMaxWidth() - .wrapContentSize() - ) { - pagerScope.content(page) - } - } - } - } -} - -private class ConsumeFlingNestedScrollConnection( - private val consumeHorizontal: Boolean, - private val consumeVertical: Boolean, -) : NestedScrollConnection { - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource - ): Offset = - when (source) { - // We can consume all resting fling scrolls so that they don't propagate up to the - // Pager - NestedScrollSource.Fling -> available.consume(consumeHorizontal, consumeVertical) - else -> Offset.Zero - } - - override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - // We can consume all post fling velocity on the main-axis - // so that it doesn't propagate up to the Pager - return available.consume(consumeHorizontal, consumeVertical) - } -} - -private fun Offset.consume( - consumeHorizontal: Boolean, - consumeVertical: Boolean, -): Offset = - Offset( - x = if (consumeHorizontal) this.x else 0f, - y = if (consumeVertical) this.y else 0f, - ) - -private fun Velocity.consume( - consumeHorizontal: Boolean, - consumeVertical: Boolean, -): Velocity = - Velocity( - x = if (consumeHorizontal) this.x else 0f, - y = if (consumeVertical) this.y else 0f, - ) - -/** Scope for [HorizontalPager] content. */ -@ExperimentalPagerApi -@Stable -interface PagerScope { - /** Returns the current selected page */ - val currentPage: Int - - /** The current offset from the start of [currentPage], as a ratio of the page width. */ - val currentPageOffset: Float -} - -@ExperimentalPagerApi -private class PagerScopeImpl( - private val state: PagerState, -) : PagerScope { - override val currentPage: Int - get() = state.currentPage - override val currentPageOffset: Float - get() = state.currentPageOffset -} - -/** - * Calculate the offset for the given [page] from the current scroll position. This is useful when - * using the scroll position to apply effects or animations to items. - * - * The returned offset can positive or negative, depending on whether which direction the [page] is - * compared to the current scroll position. - * - * @sample com.google.accompanist.sample.pager.HorizontalPagerWithOffsetTransition - */ -@ExperimentalPagerApi -fun PagerScope.calculateCurrentOffsetForPage(page: Int): Float { - return (currentPage + currentPageOffset) - page -} diff --git a/packages/SystemUI/compose/core/src/com/android/compose/pager/PagerState.kt b/packages/SystemUI/compose/core/src/com/android/compose/pager/PagerState.kt deleted file mode 100644 index 1822a68f1e77..000000000000 --- a/packages/SystemUI/compose/core/src/com/android/compose/pager/PagerState.kt +++ /dev/null @@ -1,348 +0,0 @@ -/* - * Copyright (C) 2022 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.compose.pager - -import androidx.annotation.FloatRange -import androidx.annotation.IntRange -import androidx.compose.animation.core.AnimationSpec -import androidx.compose.animation.core.spring -import androidx.compose.foundation.MutatePriority -import androidx.compose.foundation.gestures.ScrollScope -import androidx.compose.foundation.gestures.ScrollableState -import androidx.compose.foundation.interaction.InteractionSource -import androidx.compose.foundation.lazy.LazyListItemInfo -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.listSaver -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import kotlin.math.absoluteValue -import kotlin.math.roundToInt - -@Deprecated( - "Replaced with rememberPagerState(initialPage) and count parameter on Pager composables", - ReplaceWith("rememberPagerState(initialPage)"), - level = DeprecationLevel.ERROR, -) -@Suppress("UNUSED_PARAMETER", "NOTHING_TO_INLINE") -@ExperimentalPagerApi -@Composable -inline fun rememberPagerState( - @IntRange(from = 0) pageCount: Int, - @IntRange(from = 0) initialPage: Int = 0, - @FloatRange(from = 0.0, to = 1.0) initialPageOffset: Float = 0f, - @IntRange(from = 1) initialOffscreenLimit: Int = 1, - infiniteLoop: Boolean = false -): PagerState { - return rememberPagerState(initialPage = initialPage) -} - -/** - * Creates a [PagerState] that is remembered across compositions. - * - * Changes to the provided values for [initialPage] will **not** result in the state being recreated - * or changed in any way if it has already been created. - * - * @param initialPage the initial value for [PagerState.currentPage] - */ -@ExperimentalPagerApi -@Composable -fun rememberPagerState( - @IntRange(from = 0) initialPage: Int = 0, -): PagerState = - rememberSaveable(saver = PagerState.Saver) { - PagerState( - currentPage = initialPage, - ) - } - -/** - * A state object that can be hoisted to control and observe scrolling for [HorizontalPager]. - * - * In most cases, this will be created via [rememberPagerState]. - * - * @param currentPage the initial value for [PagerState.currentPage] - */ -@ExperimentalPagerApi -@Stable -class PagerState( - @IntRange(from = 0) currentPage: Int = 0, -) : ScrollableState { - // Should this be public? - internal val lazyListState = LazyListState(firstVisibleItemIndex = currentPage) - - private var _currentPage by mutableStateOf(currentPage) - - private val currentLayoutPageInfo: LazyListItemInfo? - get() = - lazyListState.layoutInfo.visibleItemsInfo - .asSequence() - .filter { it.offset <= 0 && it.offset + it.size > 0 } - .lastOrNull() - - private val currentLayoutPageOffset: Float - get() = - currentLayoutPageInfo?.let { current -> - // We coerce since itemSpacing can make the offset > 1f. - // We don't want to count spacing in the offset so cap it to 1f - (-current.offset / current.size.toFloat()).coerceIn(0f, 1f) - } - ?: 0f - - /** - * [InteractionSource] that will be used to dispatch drag events when this list is being - * dragged. If you want to know whether the fling (or animated scroll) is in progress, use - * [isScrollInProgress]. - */ - val interactionSource: InteractionSource - get() = lazyListState.interactionSource - - /** The number of pages to display. */ - @get:IntRange(from = 0) - val pageCount: Int by derivedStateOf { lazyListState.layoutInfo.totalItemsCount } - - /** - * The index of the currently selected page. This may not be the page which is currently - * displayed on screen. - * - * To update the scroll position, use [scrollToPage] or [animateScrollToPage]. - */ - @get:IntRange(from = 0) - var currentPage: Int - get() = _currentPage - internal set(value) { - if (value != _currentPage) { - _currentPage = value - } - } - - /** - * The current offset from the start of [currentPage], as a ratio of the page width. - * - * To update the scroll position, use [scrollToPage] or [animateScrollToPage]. - */ - val currentPageOffset: Float by derivedStateOf { - currentLayoutPageInfo?.let { - // The current page offset is the current layout page delta from `currentPage` - // (which is only updated after a scroll/animation). - // We calculate this by looking at the current layout page + it's offset, - // then subtracting the 'current page'. - it.index + currentLayoutPageOffset - _currentPage - } - ?: 0f - } - - /** The target page for any on-going animations. */ - private var animationTargetPage: Int? by mutableStateOf(null) - - internal var flingAnimationTarget: (() -> Int?)? by mutableStateOf(null) - - /** - * The target page for any on-going animations or scrolls by the user. Returns the current page - * if a scroll or animation is not currently in progress. - */ - val targetPage: Int - get() = - animationTargetPage - ?: flingAnimationTarget?.invoke() - ?: when { - // If a scroll isn't in progress, return the current page - !isScrollInProgress -> currentPage - // If the offset is 0f (or very close), return the current page - currentPageOffset.absoluteValue < 0.001f -> currentPage - // If we're offset towards the start, guess the previous page - currentPageOffset < -0.5f -> (currentPage - 1).coerceAtLeast(0) - // If we're offset towards the end, guess the next page - else -> (currentPage + 1).coerceAtMost(pageCount - 1) - } - - @Deprecated( - "Replaced with animateScrollToPage(page, pageOffset)", - ReplaceWith("animateScrollToPage(page = page, pageOffset = pageOffset)") - ) - @Suppress("UNUSED_PARAMETER") - suspend fun animateScrollToPage( - @IntRange(from = 0) page: Int, - @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f, - animationSpec: AnimationSpec<Float> = spring(), - initialVelocity: Float = 0f, - skipPages: Boolean = true, - ) { - animateScrollToPage(page = page, pageOffset = pageOffset) - } - - /** - * Animate (smooth scroll) to the given page to the middle of the viewport. - * - * Cancels the currently running scroll, if any, and suspends until the cancellation is - * complete. - * - * @param page the page to animate to. Must be between 0 and [pageCount] (inclusive). - * @param pageOffset the percentage of the page width to offset, from the start of [page]. Must - * be in the range 0f..1f. - */ - suspend fun animateScrollToPage( - @IntRange(from = 0) page: Int, - @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f, - ) { - requireCurrentPage(page, "page") - requireCurrentPageOffset(pageOffset, "pageOffset") - try { - animationTargetPage = page - - if (pageOffset <= 0.005f) { - // If the offset is (close to) zero, just call animateScrollToItem and we're done - lazyListState.animateScrollToItem(index = page) - } else { - // Else we need to figure out what the offset is in pixels... - - var target = - lazyListState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == page } - - if (target != null) { - // If we have access to the target page layout, we can calculate the pixel - // offset from the size - lazyListState.animateScrollToItem( - index = page, - scrollOffset = (target.size * pageOffset).roundToInt() - ) - } else { - // If we don't, we use the current page size as a guide - val currentSize = currentLayoutPageInfo!!.size - lazyListState.animateScrollToItem( - index = page, - scrollOffset = (currentSize * pageOffset).roundToInt() - ) - - // The target should be visible now - target = lazyListState.layoutInfo.visibleItemsInfo.first { it.index == page } - - if (target.size != currentSize) { - // If the size we used for calculating the offset differs from the actual - // target page size, we need to scroll again. This doesn't look great, - // but there's not much else we can do. - lazyListState.animateScrollToItem( - index = page, - scrollOffset = (target.size * pageOffset).roundToInt() - ) - } - } - } - } finally { - // We need to manually call this, as the `animateScrollToItem` call above will happen - // in 1 frame, which is usually too fast for the LaunchedEffect in Pager to detect - // the change. This is especially true when running unit tests. - onScrollFinished() - } - } - - /** - * Instantly brings the item at [page] to the middle of the viewport. - * - * Cancels the currently running scroll, if any, and suspends until the cancellation is - * complete. - * - * @param page the page to snap to. Must be between 0 and [pageCount] (inclusive). - */ - suspend fun scrollToPage( - @IntRange(from = 0) page: Int, - @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f, - ) { - requireCurrentPage(page, "page") - requireCurrentPageOffset(pageOffset, "pageOffset") - try { - animationTargetPage = page - - // First scroll to the given page. It will now be laid out at offset 0 - lazyListState.scrollToItem(index = page) - - // If we have a start spacing, we need to offset (scroll) by that too - if (pageOffset > 0.0001f) { - scroll { currentLayoutPageInfo?.let { scrollBy(it.size * pageOffset) } } - } - } finally { - // We need to manually call this, as the `scroll` call above will happen in 1 frame, - // which is usually too fast for the LaunchedEffect in Pager to detect the change. - // This is especially true when running unit tests. - onScrollFinished() - } - } - - internal fun onScrollFinished() { - // Then update the current page to our layout page - currentPage = currentLayoutPageInfo?.index ?: 0 - // Clear the animation target page - animationTargetPage = null - } - - override suspend fun scroll( - scrollPriority: MutatePriority, - block: suspend ScrollScope.() -> Unit - ) = lazyListState.scroll(scrollPriority, block) - - override fun dispatchRawDelta(delta: Float): Float { - return lazyListState.dispatchRawDelta(delta) - } - - override val isScrollInProgress: Boolean - get() = lazyListState.isScrollInProgress - - override fun toString(): String = - "PagerState(" + - "pageCount=$pageCount, " + - "currentPage=$currentPage, " + - "currentPageOffset=$currentPageOffset" + - ")" - - private fun requireCurrentPage(value: Int, name: String) { - if (pageCount == 0) { - require(value == 0) { "$name must be 0 when pageCount is 0" } - } else { - require(value in 0 until pageCount) { "$name[$value] must be >= 0 and < pageCount" } - } - } - - private fun requireCurrentPageOffset(value: Float, name: String) { - if (pageCount == 0) { - require(value == 0f) { "$name must be 0f when pageCount is 0" } - } else { - require(value in 0f..1f) { "$name must be >= 0 and <= 1" } - } - } - - companion object { - /** The default [Saver] implementation for [PagerState]. */ - val Saver: Saver<PagerState, *> = - listSaver( - save = { - listOf<Any>( - it.currentPage, - ) - }, - restore = { - PagerState( - currentPage = it[0] as Int, - ) - } - ) - } -} diff --git a/packages/SystemUI/compose/core/src/com/android/compose/pager/SnappingFlingBehavior.kt b/packages/SystemUI/compose/core/src/com/android/compose/pager/SnappingFlingBehavior.kt deleted file mode 100644 index 98140295306a..000000000000 --- a/packages/SystemUI/compose/core/src/com/android/compose/pager/SnappingFlingBehavior.kt +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Copyright (C) 2022 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.compose.pager - -import androidx.compose.animation.core.AnimationSpec -import androidx.compose.animation.core.AnimationState -import androidx.compose.animation.core.DecayAnimationSpec -import androidx.compose.animation.core.animateDecay -import androidx.compose.animation.core.animateTo -import androidx.compose.animation.core.calculateTargetValue -import androidx.compose.animation.core.spring -import androidx.compose.animation.rememberSplineBasedDecay -import androidx.compose.foundation.gestures.FlingBehavior -import androidx.compose.foundation.gestures.ScrollScope -import androidx.compose.foundation.lazy.LazyListItemInfo -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import kotlin.math.abs - -/** Default values used for [SnappingFlingBehavior] & [rememberSnappingFlingBehavior]. */ -internal object SnappingFlingBehaviorDefaults { - /** TODO */ - val snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = 600f) -} - -/** - * Create and remember a snapping [FlingBehavior] to be used with [LazyListState]. - * - * @param lazyListState The [LazyListState] to update. - * @param decayAnimationSpec The decay animation spec to use for decayed flings. - * @param snapAnimationSpec The animation spec to use when snapping. - * - * TODO: move this to a new module and make it public - */ -@Composable -internal fun rememberSnappingFlingBehavior( - lazyListState: LazyListState, - decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(), - snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec, -): SnappingFlingBehavior = - remember(lazyListState, decayAnimationSpec, snapAnimationSpec) { - SnappingFlingBehavior( - lazyListState = lazyListState, - decayAnimationSpec = decayAnimationSpec, - snapAnimationSpec = snapAnimationSpec, - ) - } - -/** - * A snapping [FlingBehavior] for [LazyListState]. Typically this would be created via - * [rememberSnappingFlingBehavior]. - * - * @param lazyListState The [LazyListState] to update. - * @param decayAnimationSpec The decay animation spec to use for decayed flings. - * @param snapAnimationSpec The animation spec to use when snapping. - */ -internal class SnappingFlingBehavior( - private val lazyListState: LazyListState, - private val decayAnimationSpec: DecayAnimationSpec<Float>, - private val snapAnimationSpec: AnimationSpec<Float>, -) : FlingBehavior { - /** The target item index for any on-going animations. */ - var animationTarget: Int? by mutableStateOf(null) - private set - - override suspend fun ScrollScope.performFling(initialVelocity: Float): Float { - val itemInfo = currentItemInfo ?: return initialVelocity - - // If the decay fling can scroll past the current item, fling with decay - return if (decayAnimationSpec.canFlingPastCurrentItem(itemInfo, initialVelocity)) { - performDecayFling(initialVelocity, itemInfo) - } else { - // Otherwise we 'spring' to current/next item - performSpringFling( - index = - when { - // If the velocity is greater than 1 item per second (velocity is px/s), - // spring - // in the relevant direction - initialVelocity > itemInfo.size -> { - (itemInfo.index + 1).coerceAtMost( - lazyListState.layoutInfo.totalItemsCount - 1 - ) - } - initialVelocity < -itemInfo.size -> itemInfo.index - // If the velocity is 0 (or less than the size of the item), spring to - // whichever item is closest to the snap point - itemInfo.offset < -itemInfo.size / 2 -> itemInfo.index + 1 - else -> itemInfo.index - }, - initialVelocity = initialVelocity, - ) - } - } - - private suspend fun ScrollScope.performDecayFling( - initialVelocity: Float, - startItem: LazyListItemInfo, - ): Float { - val index = - when { - initialVelocity > 0 -> startItem.index + 1 - else -> startItem.index - } - val forward = index > (currentItemInfo?.index ?: return initialVelocity) - - // Update the animationTarget - animationTarget = index - - var velocityLeft = initialVelocity - var lastValue = 0f - AnimationState( - initialValue = 0f, - initialVelocity = initialVelocity, - ) - .animateDecay(decayAnimationSpec) { - val delta = value - lastValue - val consumed = scrollBy(delta) - lastValue = value - velocityLeft = this.velocity - - val current = currentItemInfo - if (current == null) { - cancelAnimation() - return@animateDecay - } - - if ( - !forward && - (current.index < index || current.index == index && current.offset >= 0) - ) { - // 'snap back' to the item as we may have scrolled past it - scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat()) - cancelAnimation() - } else if ( - forward && - (current.index > index || current.index == index && current.offset <= 0) - ) { - // 'snap back' to the item as we may have scrolled past it - scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat()) - cancelAnimation() - } else if (abs(delta - consumed) > 0.5f) { - // avoid rounding errors and stop if anything is unconsumed - cancelAnimation() - } - } - animationTarget = null - return velocityLeft - } - - private suspend fun ScrollScope.performSpringFling( - index: Int, - scrollOffset: Int = 0, - initialVelocity: Float = 0f, - ): Float { - // If we don't have a current layout, we can't snap - val initialItem = currentItemInfo ?: return initialVelocity - - val forward = index > initialItem.index - // We add 10% on to the size of the current item, to compensate for any item spacing, etc - val target = (if (forward) initialItem.size else -initialItem.size) * 1.1f - - // Update the animationTarget - animationTarget = index - - var velocityLeft = initialVelocity - var lastValue = 0f - AnimationState( - initialValue = 0f, - initialVelocity = initialVelocity, - ) - .animateTo( - targetValue = target, - animationSpec = snapAnimationSpec, - ) { - // Springs can overshoot their target, clamp to the desired range - val coercedValue = - if (forward) { - value.coerceAtMost(target) - } else { - value.coerceAtLeast(target) - } - val delta = coercedValue - lastValue - val consumed = scrollBy(delta) - lastValue = coercedValue - velocityLeft = this.velocity - - val current = currentItemInfo - if (current == null) { - cancelAnimation() - return@animateTo - } - - if (scrolledPastItem(initialVelocity, current, index, scrollOffset)) { - // If we've scrolled to/past the item, stop the animation. We may also need to - // 'snap back' to the item as we may have scrolled past it - scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat()) - cancelAnimation() - } else if (abs(delta - consumed) > 0.5f) { - // avoid rounding errors and stop if anything is unconsumed - cancelAnimation() - } - } - animationTarget = null - return velocityLeft - } - - private fun LazyListState.calculateScrollOffsetToItem(index: Int): Int { - return layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }?.offset ?: 0 - } - - private val currentItemInfo: LazyListItemInfo? - get() = - lazyListState.layoutInfo.visibleItemsInfo - .asSequence() - .filter { it.offset <= 0 && it.offset + it.size > 0 } - .lastOrNull() -} - -private fun scrolledPastItem( - initialVelocity: Float, - currentItem: LazyListItemInfo, - targetIndex: Int, - targetScrollOffset: Int = 0, -): Boolean { - return if (initialVelocity > 0) { - // forward - currentItem.index > targetIndex || - (currentItem.index == targetIndex && currentItem.offset <= targetScrollOffset) - } else { - // backwards - currentItem.index < targetIndex || - (currentItem.index == targetIndex && currentItem.offset >= targetScrollOffset) - } -} - -private fun DecayAnimationSpec<Float>.canFlingPastCurrentItem( - currentItem: LazyListItemInfo, - initialVelocity: Float, -): Boolean { - val targetValue = - calculateTargetValue( - initialValue = currentItem.offset.toFloat(), - initialVelocity = initialVelocity, - ) - return when { - // forward. We add 10% onto the size to cater for any item spacing - initialVelocity > 0 -> targetValue <= -(currentItem.size * 1.1f) - // backwards. We add 10% onto the size to cater for any item spacing - else -> targetValue >= (currentItem.size * 0.1f) - } -} diff --git a/packages/SystemUI/compose/core/src/com/android/compose/swipeable/Swipeable.kt b/packages/SystemUI/compose/core/src/com/android/compose/swipeable/Swipeable.kt deleted file mode 100644 index 946e77959b1c..000000000000 --- a/packages/SystemUI/compose/core/src/com/android/compose/swipeable/Swipeable.kt +++ /dev/null @@ -1,849 +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.compose.swipeable - -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.AnimationSpec -import androidx.compose.animation.core.SpringSpec -import androidx.compose.foundation.gestures.DraggableState -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.draggable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.debugInspectorInfo -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.Velocity -import androidx.compose.ui.unit.dp -import com.android.compose.swipeable.SwipeableDefaults.AnimationSpec -import com.android.compose.swipeable.SwipeableDefaults.StandardResistanceFactor -import com.android.compose.swipeable.SwipeableDefaults.VelocityThreshold -import com.android.compose.swipeable.SwipeableDefaults.resistanceConfig -import com.android.compose.ui.util.lerp -import kotlin.math.PI -import kotlin.math.abs -import kotlin.math.sign -import kotlin.math.sin -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.launch - -/** - * State of the [swipeable] modifier. - * - * This contains necessary information about any ongoing swipe or animation and provides methods to - * change the state either immediately or by starting an animation. To create and remember a - * [SwipeableState] with the default animation clock, use [rememberSwipeableState]. - * - * @param initialValue The initial value of the state. - * @param animationSpec The default animation that will be used to animate to a new state. - * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change. - * - * TODO(b/272311106): this is a fork from material. Unfork it when Swipeable.kt reaches material3. - */ -@Stable -open class SwipeableState<T>( - initialValue: T, - internal val animationSpec: AnimationSpec<Float> = AnimationSpec, - internal val confirmStateChange: (newValue: T) -> Boolean = { true } -) { - /** - * The current value of the state. - * - * If no swipe or animation is in progress, this corresponds to the anchor at which the - * [swipeable] is currently settled. If a swipe or animation is in progress, this corresponds - * the last anchor at which the [swipeable] was settled before the swipe or animation started. - */ - var currentValue: T by mutableStateOf(initialValue) - private set - - /** Whether the state is currently animating. */ - var isAnimationRunning: Boolean by mutableStateOf(false) - private set - - /** - * The current position (in pixels) of the [swipeable]. - * - * You should use this state to offset your content accordingly. The recommended way is to use - * `Modifier.offsetPx`. This includes the resistance by default, if resistance is enabled. - */ - val offset: State<Float> - get() = offsetState - - /** The amount by which the [swipeable] has been swiped past its bounds. */ - val overflow: State<Float> - get() = overflowState - - // Use `Float.NaN` as a placeholder while the state is uninitialised. - private val offsetState = mutableStateOf(0f) - private val overflowState = mutableStateOf(0f) - - // the source of truth for the "real"(non ui) position - // basically position in bounds + overflow - private val absoluteOffset = mutableStateOf(0f) - - // current animation target, if animating, otherwise null - private val animationTarget = mutableStateOf<Float?>(null) - - internal var anchors by mutableStateOf(emptyMap<Float, T>()) - - private val latestNonEmptyAnchorsFlow: Flow<Map<Float, T>> = - snapshotFlow { anchors }.filter { it.isNotEmpty() }.take(1) - - internal var minBound = Float.NEGATIVE_INFINITY - internal var maxBound = Float.POSITIVE_INFINITY - - internal fun ensureInit(newAnchors: Map<Float, T>) { - if (anchors.isEmpty()) { - // need to do initial synchronization synchronously :( - val initialOffset = newAnchors.getOffset(currentValue) - requireNotNull(initialOffset) { "The initial value must have an associated anchor." } - offsetState.value = initialOffset - absoluteOffset.value = initialOffset - } - } - - internal suspend fun processNewAnchors(oldAnchors: Map<Float, T>, newAnchors: Map<Float, T>) { - if (oldAnchors.isEmpty()) { - // If this is the first time that we receive anchors, then we need to initialise - // the state so we snap to the offset associated to the initial value. - minBound = newAnchors.keys.minOrNull()!! - maxBound = newAnchors.keys.maxOrNull()!! - val initialOffset = newAnchors.getOffset(currentValue) - requireNotNull(initialOffset) { "The initial value must have an associated anchor." } - snapInternalToOffset(initialOffset) - } else if (newAnchors != oldAnchors) { - // If we have received new anchors, then the offset of the current value might - // have changed, so we need to animate to the new offset. If the current value - // has been removed from the anchors then we animate to the closest anchor - // instead. Note that this stops any ongoing animation. - minBound = Float.NEGATIVE_INFINITY - maxBound = Float.POSITIVE_INFINITY - val animationTargetValue = animationTarget.value - // if we're in the animation already, let's find it a new home - val targetOffset = - if (animationTargetValue != null) { - // first, try to map old state to the new state - val oldState = oldAnchors[animationTargetValue] - val newState = newAnchors.getOffset(oldState) - // return new state if exists, or find the closes one among new anchors - newState ?: newAnchors.keys.minByOrNull { abs(it - animationTargetValue) }!! - } else { - // we're not animating, proceed by finding the new anchors for an old value - val actualOldValue = oldAnchors[offset.value] - val value = if (actualOldValue == currentValue) currentValue else actualOldValue - newAnchors.getOffset(value) - ?: newAnchors.keys.minByOrNull { abs(it - offset.value) }!! - } - try { - animateInternalToOffset(targetOffset, animationSpec) - } catch (c: CancellationException) { - // If the animation was interrupted for any reason, snap as a last resort. - snapInternalToOffset(targetOffset) - } finally { - currentValue = newAnchors.getValue(targetOffset) - minBound = newAnchors.keys.minOrNull()!! - maxBound = newAnchors.keys.maxOrNull()!! - } - } - } - - internal var thresholds: (Float, Float) -> Float by mutableStateOf({ _, _ -> 0f }) - - internal var velocityThreshold by mutableStateOf(0f) - - internal var resistance: ResistanceConfig? by mutableStateOf(null) - - internal val draggableState = DraggableState { - val newAbsolute = absoluteOffset.value + it - val clamped = newAbsolute.coerceIn(minBound, maxBound) - val overflow = newAbsolute - clamped - val resistanceDelta = resistance?.computeResistance(overflow) ?: 0f - offsetState.value = clamped + resistanceDelta - overflowState.value = overflow - absoluteOffset.value = newAbsolute - } - - private suspend fun snapInternalToOffset(target: Float) { - draggableState.drag { dragBy(target - absoluteOffset.value) } - } - - private suspend fun animateInternalToOffset(target: Float, spec: AnimationSpec<Float>) { - draggableState.drag { - var prevValue = absoluteOffset.value - animationTarget.value = target - isAnimationRunning = true - try { - Animatable(prevValue).animateTo(target, spec) { - dragBy(this.value - prevValue) - prevValue = this.value - } - } finally { - animationTarget.value = null - isAnimationRunning = false - } - } - } - - /** - * The target value of the state. - * - * If a swipe is in progress, this is the value that the [swipeable] would animate to if the - * swipe finished. If an animation is running, this is the target value of that animation. - * Finally, if no swipe or animation is in progress, this is the same as the [currentValue]. - */ - val targetValue: T - get() { - // TODO(calintat): Track current velocity (b/149549482) and use that here. - val target = - animationTarget.value - ?: computeTarget( - offset = offset.value, - lastValue = anchors.getOffset(currentValue) ?: offset.value, - anchors = anchors.keys, - thresholds = thresholds, - velocity = 0f, - velocityThreshold = Float.POSITIVE_INFINITY - ) - return anchors[target] ?: currentValue - } - - /** - * Information about the ongoing swipe or animation, if any. See [SwipeProgress] for details. - * - * If no swipe or animation is in progress, this returns `SwipeProgress(value, value, 1f)`. - */ - val progress: SwipeProgress<T> - get() { - val bounds = findBounds(offset.value, anchors.keys) - val from: T - val to: T - val fraction: Float - when (bounds.size) { - 0 -> { - from = currentValue - to = currentValue - fraction = 1f - } - 1 -> { - from = anchors.getValue(bounds[0]) - to = anchors.getValue(bounds[0]) - fraction = 1f - } - else -> { - val (a, b) = - if (direction > 0f) { - bounds[0] to bounds[1] - } else { - bounds[1] to bounds[0] - } - from = anchors.getValue(a) - to = anchors.getValue(b) - fraction = (offset.value - a) / (b - a) - } - } - return SwipeProgress(from, to, fraction) - } - - /** - * The direction in which the [swipeable] is moving, relative to the current [currentValue]. - * - * This will be either 1f if it is is moving from left to right or top to bottom, -1f if it is - * moving from right to left or bottom to top, or 0f if no swipe or animation is in progress. - */ - val direction: Float - get() = anchors.getOffset(currentValue)?.let { sign(offset.value - it) } ?: 0f - - /** - * Set the state without any animation and suspend until it's set - * - * @param targetValue The new target value to set [currentValue] to. - */ - suspend fun snapTo(targetValue: T) { - latestNonEmptyAnchorsFlow.collect { anchors -> - val targetOffset = anchors.getOffset(targetValue) - requireNotNull(targetOffset) { "The target value must have an associated anchor." } - snapInternalToOffset(targetOffset) - currentValue = targetValue - } - } - - /** - * Set the state to the target value by starting an animation. - * - * @param targetValue The new value to animate to. - * @param anim The animation that will be used to animate to the new value. - */ - suspend fun animateTo(targetValue: T, anim: AnimationSpec<Float> = animationSpec) { - latestNonEmptyAnchorsFlow.collect { anchors -> - try { - val targetOffset = anchors.getOffset(targetValue) - requireNotNull(targetOffset) { "The target value must have an associated anchor." } - animateInternalToOffset(targetOffset, anim) - } finally { - val endOffset = absoluteOffset.value - val endValue = - anchors - // fighting rounding error once again, anchor should be as close as 0.5 - // pixels - .filterKeys { anchorOffset -> abs(anchorOffset - endOffset) < 0.5f } - .values - .firstOrNull() - ?: currentValue - currentValue = endValue - } - } - } - - /** - * Perform fling with settling to one of the anchors which is determined by the given - * [velocity]. Fling with settling [swipeable] will always consume all the velocity provided - * since it will settle at the anchor. - * - * In general cases, [swipeable] flings by itself when being swiped. This method is to be used - * for nested scroll logic that wraps the [swipeable]. In nested scroll developer may want to - * trigger settling fling when the child scroll container reaches the bound. - * - * @param velocity velocity to fling and settle with - * @return the reason fling ended - */ - suspend fun performFling(velocity: Float) { - latestNonEmptyAnchorsFlow.collect { anchors -> - val lastAnchor = anchors.getOffset(currentValue)!! - val targetValue = - computeTarget( - offset = offset.value, - lastValue = lastAnchor, - anchors = anchors.keys, - thresholds = thresholds, - velocity = velocity, - velocityThreshold = velocityThreshold - ) - val targetState = anchors[targetValue] - if (targetState != null && confirmStateChange(targetState)) animateTo(targetState) - // If the user vetoed the state change, rollback to the previous state. - else animateInternalToOffset(lastAnchor, animationSpec) - } - } - - /** - * Force [swipeable] to consume drag delta provided from outside of the regular [swipeable] - * gesture flow. - * - * Note: This method performs generic drag and it won't settle to any particular anchor, * - * leaving swipeable in between anchors. When done dragging, [performFling] must be called as - * well to ensure swipeable will settle at the anchor. - * - * In general cases, [swipeable] drags by itself when being swiped. This method is to be used - * for nested scroll logic that wraps the [swipeable]. In nested scroll developer may want to - * force drag when the child scroll container reaches the bound. - * - * @param delta delta in pixels to drag by - * @return the amount of [delta] consumed - */ - fun performDrag(delta: Float): Float { - val potentiallyConsumed = absoluteOffset.value + delta - val clamped = potentiallyConsumed.coerceIn(minBound, maxBound) - val deltaToConsume = clamped - absoluteOffset.value - if (abs(deltaToConsume) > 0) { - draggableState.dispatchRawDelta(deltaToConsume) - } - return deltaToConsume - } - - companion object { - /** The default [Saver] implementation for [SwipeableState]. */ - fun <T : Any> Saver( - animationSpec: AnimationSpec<Float>, - confirmStateChange: (T) -> Boolean - ) = - Saver<SwipeableState<T>, T>( - save = { it.currentValue }, - restore = { SwipeableState(it, animationSpec, confirmStateChange) } - ) - } -} - -/** - * Collects information about the ongoing swipe or animation in [swipeable]. - * - * To access this information, use [SwipeableState.progress]. - * - * @param from The state corresponding to the anchor we are moving away from. - * @param to The state corresponding to the anchor we are moving towards. - * @param fraction The fraction that the current position represents between [from] and [to]. Must - * be between `0` and `1`. - */ -@Immutable -class SwipeProgress<T>( - val from: T, - val to: T, - /*@FloatRange(from = 0.0, to = 1.0)*/ - val fraction: Float -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is SwipeProgress<*>) return false - - if (from != other.from) return false - if (to != other.to) return false - if (fraction != other.fraction) return false - - return true - } - - override fun hashCode(): Int { - var result = from?.hashCode() ?: 0 - result = 31 * result + (to?.hashCode() ?: 0) - result = 31 * result + fraction.hashCode() - return result - } - - override fun toString(): String { - return "SwipeProgress(from=$from, to=$to, fraction=$fraction)" - } -} - -/** - * Create and [remember] a [SwipeableState] with the default animation clock. - * - * @param initialValue The initial value of the state. - * @param animationSpec The default animation that will be used to animate to a new state. - * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change. - */ -@Composable -fun <T : Any> rememberSwipeableState( - initialValue: T, - animationSpec: AnimationSpec<Float> = AnimationSpec, - confirmStateChange: (newValue: T) -> Boolean = { true } -): SwipeableState<T> { - return rememberSaveable( - saver = - SwipeableState.Saver( - animationSpec = animationSpec, - confirmStateChange = confirmStateChange - ) - ) { - SwipeableState( - initialValue = initialValue, - animationSpec = animationSpec, - confirmStateChange = confirmStateChange - ) - } -} - -/** - * Create and [remember] a [SwipeableState] which is kept in sync with another state, i.e.: - * 1. Whenever the [value] changes, the [SwipeableState] will be animated to that new value. - * 2. Whenever the value of the [SwipeableState] changes (e.g. after a swipe), the owner of the - * [value] will be notified to update their state to the new value of the [SwipeableState] by - * invoking [onValueChange]. If the owner does not update their state to the provided value for - * some reason, then the [SwipeableState] will perform a rollback to the previous, correct value. - */ -@Composable -internal fun <T : Any> rememberSwipeableStateFor( - value: T, - onValueChange: (T) -> Unit, - animationSpec: AnimationSpec<Float> = AnimationSpec -): SwipeableState<T> { - val swipeableState = remember { - SwipeableState( - initialValue = value, - animationSpec = animationSpec, - confirmStateChange = { true } - ) - } - val forceAnimationCheck = remember { mutableStateOf(false) } - LaunchedEffect(value, forceAnimationCheck.value) { - if (value != swipeableState.currentValue) { - swipeableState.animateTo(value) - } - } - DisposableEffect(swipeableState.currentValue) { - if (value != swipeableState.currentValue) { - onValueChange(swipeableState.currentValue) - forceAnimationCheck.value = !forceAnimationCheck.value - } - onDispose {} - } - return swipeableState -} - -/** - * Enable swipe gestures between a set of predefined states. - * - * To use this, you must provide a map of anchors (in pixels) to states (of type [T]). Note that - * this map cannot be empty and cannot have two anchors mapped to the same state. - * - * When a swipe is detected, the offset of the [SwipeableState] will be updated with the swipe - * delta. You should use this offset to move your content accordingly (see `Modifier.offsetPx`). - * When the swipe ends, the offset will be animated to one of the anchors and when that anchor is - * reached, the value of the [SwipeableState] will also be updated to the state corresponding to the - * new anchor. The target anchor is calculated based on the provided positional [thresholds]. - * - * Swiping is constrained between the minimum and maximum anchors. If the user attempts to swipe - * past these bounds, a resistance effect will be applied by default. The amount of resistance at - * each edge is specified by the [resistance] config. To disable all resistance, set it to `null`. - * - * For an example of a [swipeable] with three states, see: - * - * @param T The type of the state. - * @param state The state of the [swipeable]. - * @param anchors Pairs of anchors and states, used to map anchors to states and vice versa. - * @param thresholds Specifies where the thresholds between the states are. The thresholds will be - * used to determine which state to animate to when swiping stops. This is represented as a lambda - * that takes two states and returns the threshold between them in the form of a - * [ThresholdConfig]. Note that the order of the states corresponds to the swipe direction. - * @param orientation The orientation in which the [swipeable] can be swiped. - * @param enabled Whether this [swipeable] is enabled and should react to the user's input. - * @param reverseDirection Whether to reverse the direction of the swipe, so a top to bottom swipe - * will behave like bottom to top, and a left to right swipe will behave like right to left. - * @param interactionSource Optional [MutableInteractionSource] that will passed on to the internal - * [Modifier.draggable]. - * @param resistance Controls how much resistance will be applied when swiping past the bounds. - * @param velocityThreshold The threshold (in dp per second) that the end velocity has to exceed in - * order to animate to the next state, even if the positional [thresholds] have not been reached. - * @sample androidx.compose.material.samples.SwipeableSample - */ -fun <T> Modifier.swipeable( - state: SwipeableState<T>, - anchors: Map<Float, T>, - orientation: Orientation, - enabled: Boolean = true, - reverseDirection: Boolean = false, - interactionSource: MutableInteractionSource? = null, - thresholds: (from: T, to: T) -> ThresholdConfig = { _, _ -> FixedThreshold(56.dp) }, - resistance: ResistanceConfig? = resistanceConfig(anchors.keys), - velocityThreshold: Dp = VelocityThreshold -) = - composed( - inspectorInfo = - debugInspectorInfo { - name = "swipeable" - properties["state"] = state - properties["anchors"] = anchors - properties["orientation"] = orientation - properties["enabled"] = enabled - properties["reverseDirection"] = reverseDirection - properties["interactionSource"] = interactionSource - properties["thresholds"] = thresholds - properties["resistance"] = resistance - properties["velocityThreshold"] = velocityThreshold - } - ) { - require(anchors.isNotEmpty()) { "You must have at least one anchor." } - require(anchors.values.distinct().count() == anchors.size) { - "You cannot have two anchors mapped to the same state." - } - val density = LocalDensity.current - state.ensureInit(anchors) - LaunchedEffect(anchors, state) { - val oldAnchors = state.anchors - state.anchors = anchors - state.resistance = resistance - state.thresholds = { a, b -> - val from = anchors.getValue(a) - val to = anchors.getValue(b) - with(thresholds(from, to)) { density.computeThreshold(a, b) } - } - with(density) { state.velocityThreshold = velocityThreshold.toPx() } - state.processNewAnchors(oldAnchors, anchors) - } - - Modifier.draggable( - orientation = orientation, - enabled = enabled, - reverseDirection = reverseDirection, - interactionSource = interactionSource, - startDragImmediately = state.isAnimationRunning, - onDragStopped = { velocity -> launch { state.performFling(velocity) } }, - state = state.draggableState - ) - } - -/** - * Interface to compute a threshold between two anchors/states in a [swipeable]. - * - * To define a [ThresholdConfig], consider using [FixedThreshold] and [FractionalThreshold]. - */ -@Stable -interface ThresholdConfig { - /** Compute the value of the threshold (in pixels), once the values of the anchors are known. */ - fun Density.computeThreshold(fromValue: Float, toValue: Float): Float -} - -/** - * A fixed threshold will be at an [offset] away from the first anchor. - * - * @param offset The offset (in dp) that the threshold will be at. - */ -@Immutable -data class FixedThreshold(private val offset: Dp) : ThresholdConfig { - override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float { - return fromValue + offset.toPx() * sign(toValue - fromValue) - } -} - -/** - * A fractional threshold will be at a [fraction] of the way between the two anchors. - * - * @param fraction The fraction (between 0 and 1) that the threshold will be at. - */ -@Immutable -data class FractionalThreshold( - /*@FloatRange(from = 0.0, to = 1.0)*/ - private val fraction: Float -) : ThresholdConfig { - override fun Density.computeThreshold(fromValue: Float, toValue: Float): Float { - return lerp(fromValue, toValue, fraction) - } -} - -/** - * Specifies how resistance is calculated in [swipeable]. - * - * There are two things needed to calculate resistance: the resistance basis determines how much - * overflow will be consumed to achieve maximum resistance, and the resistance factor determines the - * amount of resistance (the larger the resistance factor, the stronger the resistance). - * - * The resistance basis is usually either the size of the component which [swipeable] is applied to, - * or the distance between the minimum and maximum anchors. For a constructor in which the - * resistance basis defaults to the latter, consider using [resistanceConfig]. - * - * You may specify different resistance factors for each bound. Consider using one of the default - * resistance factors in [SwipeableDefaults]: `StandardResistanceFactor` to convey that the user has - * run out of things to see, and `StiffResistanceFactor` to convey that the user cannot swipe this - * right now. Also, you can set either factor to 0 to disable resistance at that bound. - * - * @param basis Specifies the maximum amount of overflow that will be consumed. Must be positive. - * @param factorAtMin The factor by which to scale the resistance at the minimum bound. Must not be - * negative. - * @param factorAtMax The factor by which to scale the resistance at the maximum bound. Must not be - * negative. - */ -@Immutable -class ResistanceConfig( - /*@FloatRange(from = 0.0, fromInclusive = false)*/ - val basis: Float, - /*@FloatRange(from = 0.0)*/ - val factorAtMin: Float = StandardResistanceFactor, - /*@FloatRange(from = 0.0)*/ - val factorAtMax: Float = StandardResistanceFactor -) { - fun computeResistance(overflow: Float): Float { - val factor = if (overflow < 0) factorAtMin else factorAtMax - if (factor == 0f) return 0f - val progress = (overflow / basis).coerceIn(-1f, 1f) - return basis / factor * sin(progress * PI.toFloat() / 2) - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is ResistanceConfig) return false - - if (basis != other.basis) return false - if (factorAtMin != other.factorAtMin) return false - if (factorAtMax != other.factorAtMax) return false - - return true - } - - override fun hashCode(): Int { - var result = basis.hashCode() - result = 31 * result + factorAtMin.hashCode() - result = 31 * result + factorAtMax.hashCode() - return result - } - - override fun toString(): String { - return "ResistanceConfig(basis=$basis, factorAtMin=$factorAtMin, factorAtMax=$factorAtMax)" - } -} - -/** - * Given an offset x and a set of anchors, return a list of anchors: - * 1. [ ] if the set of anchors is empty, - * 2. [ x' ] if x is equal to one of the anchors, accounting for a small rounding error, where x' is - * x rounded to the exact value of the matching anchor, - * 3. [ min ] if min is the minimum anchor and x < min, - * 4. [ max ] if max is the maximum anchor and x > max, or - * 5. [ a , b ] if a and b are anchors such that a < x < b and b - a is minimal. - */ -private fun findBounds(offset: Float, anchors: Set<Float>): List<Float> { - // Find the anchors the target lies between with a little bit of rounding error. - val a = anchors.filter { it <= offset + 0.001 }.maxOrNull() - val b = anchors.filter { it >= offset - 0.001 }.minOrNull() - - return when { - a == null -> - // case 1 or 3 - listOfNotNull(b) - b == null -> - // case 4 - listOf(a) - a == b -> - // case 2 - // Can't return offset itself here since it might not be exactly equal - // to the anchor, despite being considered an exact match. - listOf(a) - else -> - // case 5 - listOf(a, b) - } -} - -private fun computeTarget( - offset: Float, - lastValue: Float, - anchors: Set<Float>, - thresholds: (Float, Float) -> Float, - velocity: Float, - velocityThreshold: Float -): Float { - val bounds = findBounds(offset, anchors) - return when (bounds.size) { - 0 -> lastValue - 1 -> bounds[0] - else -> { - val lower = bounds[0] - val upper = bounds[1] - if (lastValue <= offset) { - // Swiping from lower to upper (positive). - if (velocity >= velocityThreshold) { - return upper - } else { - val threshold = thresholds(lower, upper) - if (offset < threshold) lower else upper - } - } else { - // Swiping from upper to lower (negative). - if (velocity <= -velocityThreshold) { - return lower - } else { - val threshold = thresholds(upper, lower) - if (offset > threshold) upper else lower - } - } - } - } -} - -private fun <T> Map<Float, T>.getOffset(state: T): Float? { - return entries.firstOrNull { it.value == state }?.key -} - -/** Contains useful defaults for [swipeable] and [SwipeableState]. */ -object SwipeableDefaults { - /** The default animation used by [SwipeableState]. */ - val AnimationSpec = SpringSpec<Float>() - - /** The default velocity threshold (1.8 dp per millisecond) used by [swipeable]. */ - val VelocityThreshold = 125.dp - - /** A stiff resistance factor which indicates that swiping isn't available right now. */ - const val StiffResistanceFactor = 20f - - /** A standard resistance factor which indicates that the user has run out of things to see. */ - const val StandardResistanceFactor = 10f - - /** - * The default resistance config used by [swipeable]. - * - * This returns `null` if there is one anchor. If there are at least two anchors, it returns a - * [ResistanceConfig] with the resistance basis equal to the distance between the two bounds. - */ - fun resistanceConfig( - anchors: Set<Float>, - factorAtMin: Float = StandardResistanceFactor, - factorAtMax: Float = StandardResistanceFactor - ): ResistanceConfig? { - return if (anchors.size <= 1) { - null - } else { - val basis = anchors.maxOrNull()!! - anchors.minOrNull()!! - ResistanceConfig(basis, factorAtMin, factorAtMax) - } - } -} - -// temp default nested scroll connection for swipeables which desire as an opt in -// revisit in b/174756744 as all types will have their own specific connection probably -internal val <T> SwipeableState<T>.PreUpPostDownNestedScrollConnection: NestedScrollConnection - get() = - object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - val delta = available.toFloat() - return if (delta < 0 && source == NestedScrollSource.Drag) { - performDrag(delta).toOffset() - } else { - Offset.Zero - } - } - - override fun onPostScroll( - consumed: Offset, - available: Offset, - source: NestedScrollSource - ): Offset { - return if (source == NestedScrollSource.Drag) { - performDrag(available.toFloat()).toOffset() - } else { - Offset.Zero - } - } - - override suspend fun onPreFling(available: Velocity): Velocity { - val toFling = Offset(available.x, available.y).toFloat() - return if (toFling < 0 && offset.value > minBound) { - performFling(velocity = toFling) - // since we go to the anchor with tween settling, consume all for the best UX - available - } else { - Velocity.Zero - } - } - - override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { - performFling(velocity = Offset(available.x, available.y).toFloat()) - return available - } - - private fun Float.toOffset(): Offset = Offset(0f, this) - - private fun Offset.toFloat(): Float = this.y - } diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt index 82fe3f265384..609ea90e9159 100644 --- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -21,13 +21,11 @@ import android.content.Context import android.view.View import androidx.activity.ComponentActivity import androidx.lifecycle.LifecycleOwner -import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel -import com.android.systemui.util.time.SystemClock /** The Compose facade, when Compose is *not* available. */ object ComposeFacade : BaseComposeFacade { @@ -53,14 +51,6 @@ object ComposeFacade : BaseComposeFacade { throwComposeUnavailableError() } - override fun createMultiShadeView( - context: Context, - viewModel: MultiShadeViewModel, - clock: SystemClock, - ): View { - throwComposeUnavailableError() - } - override fun createSceneContainerView( context: Context, viewModel: SceneContainerViewModel, diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt index 7926f9224347..0ee88b90bcc4 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -23,8 +23,6 @@ import androidx.activity.compose.setContent import androidx.compose.ui.platform.ComposeView import androidx.lifecycle.LifecycleOwner import com.android.compose.theme.PlatformTheme -import com.android.systemui.multishade.ui.composable.MultiShade -import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel import com.android.systemui.people.ui.compose.PeopleScreen import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.compose.FooterActions @@ -34,7 +32,6 @@ import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.composable.ComposableScene import com.android.systemui.scene.ui.composable.SceneContainer import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel -import com.android.systemui.util.time.SystemClock /** The Compose facade, when Compose is available. */ object ComposeFacade : BaseComposeFacade { @@ -60,23 +57,6 @@ object ComposeFacade : BaseComposeFacade { } } - override fun createMultiShadeView( - context: Context, - viewModel: MultiShadeViewModel, - clock: SystemClock, - ): View { - return ComposeView(context).apply { - setContent { - PlatformTheme { - MultiShade( - viewModel = viewModel, - clock = clock, - ) - } - } - } - } - override fun createSceneContainerView( context: Context, viewModel: SceneContainerViewModel, 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 63a3eca4695d..64227b8c5f2a 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 @@ -228,43 +228,45 @@ internal fun PatternBouncer( } } ) { - // Draw lines between dots. - selectedDots.forEachIndexed { index, dot -> - if (index > 0) { - val previousDot = selectedDots[index - 1] - val lineFadeOutAnimationProgress = lineFadeOutAnimatables[previousDot]!!.value - val startLerp = 1 - lineFadeOutAnimationProgress - val from = pixelOffset(previousDot, spacing, verticalOffset) - val to = pixelOffset(dot, spacing, verticalOffset) - val lerpedFrom = - Offset( - x = from.x + (to.x - from.x) * startLerp, - y = from.y + (to.y - from.y) * startLerp, + if (isAnimationEnabled) { + // Draw lines between dots. + selectedDots.forEachIndexed { index, dot -> + if (index > 0) { + val previousDot = selectedDots[index - 1] + val lineFadeOutAnimationProgress = lineFadeOutAnimatables[previousDot]!!.value + val startLerp = 1 - lineFadeOutAnimationProgress + val from = pixelOffset(previousDot, spacing, verticalOffset) + val to = pixelOffset(dot, spacing, verticalOffset) + val lerpedFrom = + Offset( + x = from.x + (to.x - from.x) * startLerp, + y = from.y + (to.y - from.y) * startLerp, + ) + drawLine( + start = lerpedFrom, + end = to, + cap = StrokeCap.Round, + alpha = lineFadeOutAnimationProgress * lineAlpha(spacing), + color = lineColor, + strokeWidth = lineStrokeWidth, ) - drawLine( - start = lerpedFrom, - end = to, - cap = StrokeCap.Round, - alpha = lineFadeOutAnimationProgress * lineAlpha(spacing), - color = lineColor, - strokeWidth = lineStrokeWidth, - ) + } } - } - // Draw the line between the most recently-selected dot and the input pointer position. - inputPosition?.let { lineEnd -> - currentDot?.let { dot -> - val from = pixelOffset(dot, spacing, verticalOffset) - val lineLength = sqrt((from.y - lineEnd.y).pow(2) + (from.x - lineEnd.x).pow(2)) - drawLine( - start = from, - end = lineEnd, - cap = StrokeCap.Round, - alpha = lineAlpha(spacing, lineLength), - color = lineColor, - strokeWidth = lineStrokeWidth, - ) + // Draw the line between the most recently-selected dot and the input pointer position. + inputPosition?.let { lineEnd -> + currentDot?.let { dot -> + val from = pixelOffset(dot, spacing, verticalOffset) + val lineLength = sqrt((from.y - lineEnd.y).pow(2) + (from.x - lineEnd.x).pow(2)) + drawLine( + start = from, + end = lineEnd, + cap = StrokeCap.Round, + alpha = lineAlpha(spacing, lineLength), + color = lineColor, + strokeWidth = lineStrokeWidth, + ) + } } } 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 bef0b3df36c2..ec6e5eda264e 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 @@ -14,63 +14,40 @@ * limitations under the License. */ -@file:OptIn(ExperimentalAnimationApi::class, ExperimentalAnimationGraphicsApi::class) - package com.android.systemui.bouncer.ui.composable import android.view.HapticFeedbackConstants -import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.AnimationVector1D -import androidx.compose.animation.core.MutableTransitionState -import androidx.compose.animation.core.Transition -import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween -import androidx.compose.animation.core.updateTransition -import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi -import androidx.compose.animation.graphics.res.animatedVectorResource -import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter -import androidx.compose.animation.graphics.vector.AnimatedImageVector -import androidx.compose.foundation.Image import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.Layout import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.compose.animation.Easings @@ -78,7 +55,6 @@ import com.android.compose.grid.VerticalGrid import com.android.compose.modifiers.thenIf import com.android.systemui.R import com.android.systemui.bouncer.ui.viewmodel.ActionButtonAppearance -import com.android.systemui.bouncer.ui.viewmodel.EnteredKey import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon @@ -109,147 +85,6 @@ internal fun PinBouncer( } @Composable -private fun PinInputDisplay(viewModel: PinBouncerViewModel) { - val currentPinEntries: List<EnteredKey> by viewModel.pinEntries.collectAsState() - - // visiblePinEntries keeps pins removed from currentPinEntries in the composition until their - // disappear-animation completed. The list is sorted by the natural ordering of EnteredKey, - // which is guaranteed to produce the original edit order, since the model only modifies entries - // at the end. - val visiblePinEntries = remember { SnapshotStateList<EnteredKey>() } - currentPinEntries.forEach { - val index = visiblePinEntries.binarySearch(it) - if (index < 0) { - val insertionPoint = -(index + 1) - visiblePinEntries.add(insertionPoint, it) - } - } - - Row( - modifier = - Modifier.heightIn(min = entryShapeSize) - // Pins overflowing horizontally should still be shown as scrolling. - .wrapContentSize(unbounded = true), - ) { - visiblePinEntries.forEachIndexed { index, entry -> - key(entry) { - val visibility = remember { - MutableTransitionState<EntryVisibility>(EntryVisibility.Hidden) - } - visibility.targetState = - when { - currentPinEntries.isEmpty() && visiblePinEntries.size > 1 -> - EntryVisibility.BulkHidden(index, visiblePinEntries.size) - currentPinEntries.contains(entry) -> EntryVisibility.Shown - else -> EntryVisibility.Hidden - } - - val shape = viewModel.pinShapes.getShape(entry.sequenceNumber) - PinInputEntry(shape, updateTransition(visibility, label = "Pin Entry $entry")) - - LaunchedEffect(entry) { - // Remove entry from visiblePinEntries once the hide transition completed. - snapshotFlow { - visibility.currentState == visibility.targetState && - visibility.targetState != EntryVisibility.Shown - } - .collect { isRemoved -> - if (isRemoved) { - visiblePinEntries.remove(entry) - } - } - } - } - } - } -} - -private sealed class EntryVisibility { - object Shown : EntryVisibility() - - object Hidden : EntryVisibility() - - /** - * Same as [Hidden], but applies when multiple entries are hidden simultaneously, without - * collapsing during the hide. - */ - data class BulkHidden(val staggerIndex: Int, val totalEntryCount: Int) : EntryVisibility() -} - -@Composable -private fun PinInputEntry(shapeResourceId: Int, transition: Transition<EntryVisibility>) { - // spec: http://shortn/_DEhE3Xl2bi - val dismissStaggerDelayMs = 33 - val dismissDurationMs = 450 - val expansionDurationMs = 250 - val shapeCollapseDurationMs = 200 - - val animatedEntryWidth by - transition.animateDp( - transitionSpec = { - when (val target = targetState) { - is EntryVisibility.BulkHidden -> - // only collapse horizontal space once all entries are removed - snap(dismissDurationMs + dismissStaggerDelayMs * target.totalEntryCount) - else -> tween(expansionDurationMs, easing = Easings.Standard) - } - }, - label = "entry space" - ) { state -> - if (state == EntryVisibility.Shown) entryShapeSize else 0.dp - } - - val animatedShapeSize by - transition.animateDp( - transitionSpec = { - when { - EntryVisibility.Hidden isTransitioningTo EntryVisibility.Shown -> { - // The AVD contains the entry transition. - snap() - } - targetState is EntryVisibility.BulkHidden -> { - val target = targetState as EntryVisibility.BulkHidden - tween( - dismissDurationMs, - delayMillis = target.staggerIndex * dismissStaggerDelayMs, - easing = Easings.Legacy, - ) - } - else -> tween(shapeCollapseDurationMs, easing = Easings.StandardDecelerate) - } - }, - label = "shape size" - ) { state -> - if (state == EntryVisibility.Shown) entryShapeSize else 0.dp - } - - val dotColor = MaterialTheme.colorScheme.onSurfaceVariant - Layout( - content = { - val image = AnimatedImageVector.animatedVectorResource(shapeResourceId) - var atEnd by remember { mutableStateOf(false) } - Image( - painter = rememberAnimatedVectorPainter(image, atEnd), - contentDescription = null, - contentScale = ContentScale.Crop, - colorFilter = ColorFilter.tint(dotColor), - ) - LaunchedEffect(Unit) { atEnd = true } - } - ) { measurables, _ -> - val shapeSizePx = animatedShapeSize.roundToPx() - val placeable = measurables.single().measure(Constraints.fixed(shapeSizePx, shapeSizePx)) - - layout(animatedEntryWidth.roundToPx(), entryShapeSize.roundToPx()) { - placeable.place( - ((animatedEntryWidth - animatedShapeSize) / 2f).roundToPx(), - ((entryShapeSize - animatedShapeSize) / 2f).roundToPx() - ) - } - } -} - -@Composable private fun PinPad(viewModel: PinBouncerViewModel) { val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState() val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState() @@ -511,8 +346,6 @@ private suspend fun showFailureAnimation( } } -private val entryShapeSize = 30.dp - private val pinButtonSize = 84.dp private val pinButtonErrorShrinkFactor = 67.dp / pinButtonSize private const val pinButtonErrorShrinkMs = 50 diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt new file mode 100644 index 000000000000..77065cfdeb76 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt @@ -0,0 +1,437 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalAnimationGraphicsApi::class) + +package com.android.systemui.bouncer.ui.composable + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.core.tween +import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi +import androidx.compose.animation.graphics.res.animatedVectorResource +import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter +import androidx.compose.animation.graphics.vector.AnimatedImageVector +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.layout +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.android.compose.animation.Easings +import com.android.keyguard.PinShapeAdapter +import com.android.systemui.R +import com.android.systemui.bouncer.ui.viewmodel.EntryToken.Digit +import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel +import com.android.systemui.bouncer.ui.viewmodel.PinInputViewModel +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch + +@Composable +fun PinInputDisplay(viewModel: PinBouncerViewModel) { + val hintedPinLength: Int? by viewModel.hintedPinLength.collectAsState() + val shapeAnimations = rememberShapeAnimations(viewModel.pinShapes) + + // The display comes in two different flavors: + // 1) hinting: shows a circle (◦) per expected pin input, and dot (●) per entered digit. + // This has a fixed width, and uses two distinct types of AVDs to animate the addition and + // removal of digits. + // 2) regular, shows a dot (●) per entered digit. + // This grows/shrinks as digits are added deleted. Uses the same type of AVDs to animate the + // addition of digits, but simply center-shrinks the dot (●) shape to zero to animate the + // removal. + // Because of all these differences, there are two separate implementations, rather than + // unifying into a single, more complex implementation. + + when (val length = hintedPinLength) { + null -> RegularPinInputDisplay(viewModel, shapeAnimations) + else -> HintingPinInputDisplay(viewModel, shapeAnimations, length) + } +} + +/** + * A pin input display that shows a placeholder circle (◦) for every digit in the pin not yet + * entered. + * + * Used for auto-confirmed pins of a specific length, see design: http://shortn/_jS8kPzQ7QV + */ +@Composable +private fun HintingPinInputDisplay( + viewModel: PinBouncerViewModel, + shapeAnimations: ShapeAnimations, + hintedPinLength: Int, +) { + val pinInput: PinInputViewModel by viewModel.pinInput.collectAsState() + // [ClearAll] marker pointing at the beginning of the current pin input. + // When a new [ClearAll] token is added to the [pinInput], the clear-all animation is played + // and the marker is advanced manually to the most recent marker. See LaunchedEffect below. + var currentClearAll by remember { mutableStateOf(pinInput.mostRecentClearAll()) } + // The length of the pin currently entered by the user. + val currentPinLength = pinInput.getDigits(currentClearAll).size + + // The animated vector drawables for each of the [hintedPinLength] slots. + // The first [currentPinLength] drawables end in a dot (●) shape, the remaining drawables up to + // [hintedPinLength] end in the circle (◦) shape. + // This list is re-generated upon each pin entry, it is modelled as a [MutableStateList] to + // allow the clear-all animation to replace the shapes asynchronously, see LaunchedEffect below. + // Note that when a [ClearAll] token is added to the input (and the clear-all animation plays) + // the [currentPinLength] does not change; the [pinEntryDrawable] is remembered until the + // clear-all animation finishes and the [currentClearAll] state is manually advanced. + val pinEntryDrawable = + remember(currentPinLength) { + buildList { + repeat(currentPinLength) { add(shapeAnimations.getShapeToDot(it)) } + repeat(hintedPinLength - currentPinLength) { add(shapeAnimations.dotToCircle) } + } + .toMutableStateList() + } + + val mostRecentClearAll = pinInput.mostRecentClearAll() + // Whenever a new [ClearAll] marker is added to the input, the clear-all animation needs to + // be played. + LaunchedEffect(mostRecentClearAll) { + if (currentClearAll == mostRecentClearAll) { + // Except during the initial composition. + return@LaunchedEffect + } + + // Staggered replace of all dot (●) shapes with an animation from dot (●) to circle (◦). + for (index in 0 until hintedPinLength) { + if (!shapeAnimations.isDotShape(pinEntryDrawable[index])) break + + pinEntryDrawable[index] = shapeAnimations.dotToCircle + delay(shapeAnimations.dismissStaggerDelay) + } + + // Once the animation is done, start processing the next pin input again. + currentClearAll = mostRecentClearAll + } + + // During the initial composition, do not play the [pinEntryDrawable] animations. This prevents + // the dot (●) to circle (◦) animation when the empty display becomes first visible, and a + // superfluous shape to dot (●) animation after for example device rotation. + var playAnimation by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { playAnimation = true } + + val dotColor = MaterialTheme.colorScheme.onSurfaceVariant + Row(modifier = Modifier.heightIn(min = shapeAnimations.shapeSize)) { + pinEntryDrawable.forEachIndexed { index, drawable -> + // Key the loop by [index] and [drawable], so that updating a shape drawable at the same + // index will play the new animation (by remembering a new [atEnd]). + key(index, drawable) { + // [rememberAnimatedVectorPainter] requires a `atEnd` boolean to switch from `false` + // to `true` for the animation to play. This animation is suppressed when + // playAnimation is false, always rendering the end-state of the animation. + var atEnd by remember { mutableStateOf(!playAnimation) } + LaunchedEffect(Unit) { atEnd = true } + + Image( + painter = rememberAnimatedVectorPainter(drawable, atEnd), + contentDescription = null, + contentScale = ContentScale.Crop, + colorFilter = ColorFilter.tint(dotColor), + ) + } + } + } +} + +/** + * A pin input that shows a dot (●) for each entered pin, horizontally centered and growing / + * shrinking as more digits are entered and deleted. + * + * Used for pin input when the pin length is not hinted, see design http://shortn/_wNP7SrBD78 + */ +@Composable +private fun RegularPinInputDisplay( + viewModel: PinBouncerViewModel, + shapeAnimations: ShapeAnimations, +) { + // Holds all currently [VisiblePinEntry] composables. This cannot be simply derived from + // `viewModel.pinInput` at composition, since deleting a pin entry needs to play a remove + // animation, thus the composable to be removed has to remain in the composition until fully + // disappeared (see `prune` launched effect below) + val pinInputRow = remember(shapeAnimations) { PinInputRow(shapeAnimations) } + + // Processed `viewModel.pinInput` updates and applies them to [pinDigitShapes] + LaunchedEffect(viewModel.pinInput, pinInputRow) { + // Initial setup: capture the most recent [ClearAll] marker and create the visuals for the + // existing digits (if any) without animation.. + var currentClearAll = + with(viewModel.pinInput.value) { + val initialClearAll = mostRecentClearAll() + pinInputRow.setDigits(getDigits(initialClearAll)) + initialClearAll + } + + viewModel.pinInput.collect { input -> + // Process additions and removals of pins within the current input block. + pinInputRow.updateDigits(input.getDigits(currentClearAll), scope = this@LaunchedEffect) + + val mostRecentClearAll = input.mostRecentClearAll() + if (currentClearAll != mostRecentClearAll) { + // A new [ClearAll] token is added to the [input], play the clear-all animation + pinInputRow.playClearAllAnimation() + + // Animation finished, advance manually to the next marker. + currentClearAll = mostRecentClearAll + } + } + } + + LaunchedEffect(pinInputRow) { + // Prunes unused VisiblePinEntries once they are no longer visible. + snapshotFlow { pinInputRow.hasUnusedEntries() } + .collect { hasUnusedEntries -> + if (hasUnusedEntries) { + pinInputRow.prune() + } + } + } + + pinInputRow.Content() +} + +private class PinInputRow( + val shapeAnimations: ShapeAnimations, +) { + private val entries = mutableStateListOf<PinInputEntry>() + + @Composable + fun Content() { + Row( + modifier = + Modifier.heightIn(min = shapeAnimations.shapeSize) + // Pins overflowing horizontally should still be shown as scrolling. + .wrapContentSize(unbounded = true), + ) { + entries.forEach { entry -> key(entry.digit) { entry.Content() } } + } + } + + /** + * Replaces all current [PinInputEntry] composables with new instances for each digit. + * + * Does not play the entry expansion animation. + */ + fun setDigits(digits: List<Digit>) { + entries.clear() + entries.addAll(digits.map { PinInputEntry(it, shapeAnimations) }) + } + + /** + * Adds [PinInputEntry] composables for new digits and plays an entry animation, and starts the + * exit animation for digits not in [updated] anymore. + * + * The function return immediately, playing the animations in the background. + * + * Removed entries have to be [prune]d once the exit animation completes, [hasUnusedEntries] can + * be used in a [SnapshotFlow] to discover when its time to do so. + */ + fun updateDigits(updated: List<Digit>, scope: CoroutineScope) { + val incoming = updated.minus(entries.map { it.digit }.toSet()).toList() + val outgoing = entries.filterNot { entry -> updated.any { entry.digit == it } }.toList() + + entries.addAll( + incoming.map { + PinInputEntry(it, shapeAnimations).apply { scope.launch { animateAppearance() } } + } + ) + + outgoing.forEach { entry -> scope.launch { entry.animateRemoval() } } + + entries.sortWith(compareBy { it.digit }) + } + + /** + * Plays a staggered remove animation, and upon completion removes the [PinInputEntry] + * composables. + * + * This function returns once the animation finished playing and the entries are removed. + */ + suspend fun playClearAllAnimation() = coroutineScope { + val entriesToRemove = entries.toList() + entriesToRemove + .mapIndexed { index, entry -> + launch { + delay(shapeAnimations.dismissStaggerDelay * index) + entry.animateClearAllCollapse() + } + } + .joinAll() + + // Remove all [PinInputEntry] composables for which the staggered remove animation was + // played. Note that up to now, each PinInputEntry still occupied the full width. + entries.removeAll(entriesToRemove) + } + + /** + * Whether there are [PinInputEntry] that can be removed from the composition since they were + * fully animated out. + */ + fun hasUnusedEntries(): Boolean { + return entries.any { it.isUnused } + } + + /** Remove all no longer visible [PinInputEntry]s from the composition. */ + fun prune() { + entries.removeAll { it.isUnused } + } +} + +private class PinInputEntry( + val digit: Digit, + val shapeAnimations: ShapeAnimations, +) { + private val shape = shapeAnimations.getShapeToDot(digit.sequenceNumber) + // horizontal space occupied, used to shift contents as individual digits are animated in/out + private val entryWidth = + Animatable(shapeAnimations.shapeSize, Dp.VectorConverter, label = "Width of pin ($digit)") + // intrinsic width and height of the shape, used to collapse the shape during exit animations. + private val shapeSize = + Animatable(shapeAnimations.shapeSize, Dp.VectorConverter, label = "Size of pin ($digit)") + + /** + * Whether the is fully animated out. When `true`, removing this from the composable won't have + * visual effects. + */ + val isUnused: Boolean + get() { + return entryWidth.targetValue == 0.dp && !entryWidth.isRunning + } + + /** Animate the shape appearance by growing the entry width from 0.dp to the intrinsic width. */ + suspend fun animateAppearance() = coroutineScope { + entryWidth.snapTo(0.dp) + entryWidth.animateTo(shapeAnimations.shapeSize, shapeAnimations.inputShiftAnimationSpec) + } + + /** + * Animates shape disappearance by collapsing the shape and occupied horizontal space. + * + * Once complete, [isUnused] will return `true`. + */ + suspend fun animateRemoval() = coroutineScope { + awaitAll( + async { entryWidth.animateTo(0.dp, shapeAnimations.inputShiftAnimationSpec) }, + async { shapeSize.animateTo(0.dp, shapeAnimations.deleteShapeSizeAnimationSpec) } + ) + } + + /** Collapses the shape in place, while still holding on to the horizontal space. */ + suspend fun animateClearAllCollapse() = coroutineScope { + shapeSize.animateTo(0.dp, shapeAnimations.clearAllShapeSizeAnimationSpec) + } + + @Composable + fun Content() { + val animatedShapeSize by shapeSize.asState() + val animatedEntryWidth by entryWidth.asState() + + val dotColor = MaterialTheme.colorScheme.onSurfaceVariant + val shapeHeight = shapeAnimations.shapeSize + var atEnd by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { atEnd = true } + Image( + painter = rememberAnimatedVectorPainter(shape, atEnd), + contentDescription = null, + contentScale = ContentScale.Crop, + colorFilter = ColorFilter.tint(dotColor), + modifier = + Modifier.layout { measurable, _ -> + val shapeSizePx = animatedShapeSize.roundToPx() + val placeable = measurable.measure(Constraints.fixed(shapeSizePx, shapeSizePx)) + + layout(animatedEntryWidth.roundToPx(), shapeHeight.roundToPx()) { + placeable.place( + ((animatedEntryWidth - animatedShapeSize) / 2f).roundToPx(), + ((shapeHeight - animatedShapeSize) / 2f).roundToPx() + ) + } + }, + ) + } +} + +/** Animated Vector Drawables used to render the pin input. */ +private class ShapeAnimations( + /** Width and height for all the animation images listed here. */ + val shapeSize: Dp, + /** Transitions from the dot (●) to the circle (◦). Used for the hinting pin input only. */ + val dotToCircle: AnimatedImageVector, + /** Each of the animations transition from nothing via a shape to the dot (●). */ + private val shapesToDot: List<AnimatedImageVector>, +) { + /** + * Returns a transition from nothing via shape to the dot (●)., specific to the input position. + */ + fun getShapeToDot(position: Int): AnimatedImageVector { + return shapesToDot[position.mod(shapesToDot.size)] + } + + /** + * Whether the [shapeAnimation] is a image returned by [getShapeToDot], and thus is ending in + * the dot (●) shape. + * + * `false` if the shape's end state is the circle (◦). + */ + fun isDotShape(shapeAnimation: AnimatedImageVector): Boolean { + return shapeAnimation != dotToCircle + } + + // spec: http://shortn/_DEhE3Xl2bi + val dismissStaggerDelay = 33.milliseconds + val inputShiftAnimationSpec = tween<Dp>(durationMillis = 250, easing = Easings.Standard) + val deleteShapeSizeAnimationSpec = + tween<Dp>(durationMillis = 200, easing = Easings.StandardDecelerate) + val clearAllShapeSizeAnimationSpec = tween<Dp>(durationMillis = 450, easing = Easings.Legacy) +} + +@Composable +private fun rememberShapeAnimations(pinShapes: PinShapeAdapter): ShapeAnimations { + // NOTE: `animatedVectorResource` does remember the returned AnimatedImageVector. + val dotToCircle = AnimatedImageVector.animatedVectorResource(R.drawable.pin_dot_delete_avd) + val shapesToDot = pinShapes.shapes.map { AnimatedImageVector.animatedVectorResource(it) } + val shapeSize = dimensionResource(R.dimen.password_shape_size) + + return remember(dotToCircle, shapesToDot, shapeSize) { + ShapeAnimations(shapeSize, dotToCircle, shapesToDot) + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt deleted file mode 100644 index 99fe26ce1f3b..000000000000 --- a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/MultiShade.kt +++ /dev/null @@ -1,145 +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.multishade.ui.composable - -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.gestures.detectVerticalDragGestures -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.unit.IntSize -import com.android.systemui.R -import com.android.systemui.multishade.shared.model.ProxiedInputModel -import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel -import com.android.systemui.notifications.ui.composable.Notifications -import com.android.systemui.qs.footer.ui.compose.QuickSettings -import com.android.systemui.statusbar.ui.composable.StatusBar -import com.android.systemui.util.time.SystemClock - -@Composable -fun MultiShade( - viewModel: MultiShadeViewModel, - clock: SystemClock, - modifier: Modifier = Modifier, -) { - val isScrimEnabled: Boolean by viewModel.isScrimEnabled.collectAsState() - val scrimAlpha: Float by viewModel.scrimAlpha.collectAsState() - - // TODO(b/273298030): find a different way to get the height constraint from its parent. - BoxWithConstraints(modifier = modifier) { - val maxHeightPx = with(LocalDensity.current) { maxHeight.toPx() } - - Scrim( - modifier = Modifier.fillMaxSize(), - remoteTouch = viewModel::onScrimTouched, - alpha = { scrimAlpha }, - isScrimEnabled = isScrimEnabled, - ) - Shade( - viewModel = viewModel.leftShade, - currentTimeMillis = clock::elapsedRealtime, - containerHeightPx = maxHeightPx, - modifier = Modifier.align(Alignment.TopStart), - ) { - Column { - StatusBar() - Notifications() - } - } - Shade( - viewModel = viewModel.rightShade, - currentTimeMillis = clock::elapsedRealtime, - containerHeightPx = maxHeightPx, - modifier = Modifier.align(Alignment.TopEnd), - ) { - Column { - StatusBar() - QuickSettings() - } - } - Shade( - viewModel = viewModel.singleShade, - currentTimeMillis = clock::elapsedRealtime, - containerHeightPx = maxHeightPx, - modifier = Modifier, - ) { - Column { - StatusBar() - Notifications() - QuickSettings() - } - } - } -} - -@Composable -private fun Scrim( - remoteTouch: (ProxiedInputModel) -> Unit, - alpha: () -> Float, - isScrimEnabled: Boolean, - modifier: Modifier = Modifier, -) { - var size by remember { mutableStateOf(IntSize.Zero) } - - Box( - modifier = - modifier - .graphicsLayer { this.alpha = alpha() } - .background(colorResource(R.color.opaque_scrim)) - .fillMaxSize() - .onSizeChanged { size = it } - .then( - if (isScrimEnabled) { - Modifier.pointerInput(Unit) { - detectTapGestures(onTap = { remoteTouch(ProxiedInputModel.OnTap) }) - } - .pointerInput(Unit) { - detectVerticalDragGestures( - onVerticalDrag = { change, dragAmount -> - remoteTouch( - ProxiedInputModel.OnDrag( - xFraction = change.position.x / size.width, - yDragAmountPx = dragAmount, - ) - ) - }, - onDragEnd = { remoteTouch(ProxiedInputModel.OnDragEnd) }, - onDragCancel = { remoteTouch(ProxiedInputModel.OnDragCancel) } - ) - } - } else { - Modifier - } - ) - ) -} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt deleted file mode 100644 index cfcc2fb251fd..000000000000 --- a/packages/SystemUI/compose/features/src/com/android/systemui/multishade/ui/composable/Shade.kt +++ /dev/null @@ -1,336 +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.multishade.ui.composable - -import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.interaction.DragInteraction -import androidx.compose.foundation.interaction.InteractionSource -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Surface -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.util.VelocityTracker -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.android.compose.modifiers.height -import com.android.compose.modifiers.padding -import com.android.compose.swipeable.FixedThreshold -import com.android.compose.swipeable.SwipeableState -import com.android.compose.swipeable.ThresholdConfig -import com.android.compose.swipeable.rememberSwipeableState -import com.android.compose.swipeable.swipeable -import com.android.systemui.multishade.shared.model.ProxiedInputModel -import com.android.systemui.multishade.ui.viewmodel.ShadeViewModel -import kotlin.math.min -import kotlin.math.roundToInt -import kotlinx.coroutines.launch - -/** - * Renders a shade (container and content). - * - * This should be allowed to grow to fill the width and height of its container. - * - * @param viewModel The view-model for this shade. - * @param currentTimeMillis A provider for the current time, in milliseconds. - * @param containerHeightPx The height of the container that this shade is being shown in, in - * pixels. - * @param modifier The Modifier. - * @param content The content of the shade. - */ -@Composable -fun Shade( - viewModel: ShadeViewModel, - currentTimeMillis: () -> Long, - containerHeightPx: Float, - modifier: Modifier = Modifier, - content: @Composable () -> Unit = {}, -) { - val isVisible: Boolean by viewModel.isVisible.collectAsState() - if (!isVisible) { - return - } - - val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } - ReportNonProxiedInput(viewModel, interactionSource) - - val swipeableState = rememberSwipeableState(initialValue = ShadeState.FullyCollapsed) - HandleForcedCollapse(viewModel, swipeableState) - HandleProxiedInput(viewModel, swipeableState, currentTimeMillis) - ReportShadeExpansion(viewModel, swipeableState, containerHeightPx) - - val isSwipingEnabled: Boolean by viewModel.isSwipingEnabled.collectAsState() - val collapseThreshold: Float by viewModel.swipeCollapseThreshold.collectAsState() - val expandThreshold: Float by viewModel.swipeExpandThreshold.collectAsState() - - val width: ShadeViewModel.Size by viewModel.width.collectAsState() - val density = LocalDensity.current - - val anchors: Map<Float, ShadeState> = - remember(containerHeightPx) { swipeableAnchors(containerHeightPx) } - - ShadeContent( - shadeHeightPx = { swipeableState.offset.value }, - overstretch = { swipeableState.overflow.value / containerHeightPx }, - isSwipingEnabled = isSwipingEnabled, - swipeableState = swipeableState, - interactionSource = interactionSource, - anchors = anchors, - thresholds = { _, to -> - swipeableThresholds( - to = to, - swipeCollapseThreshold = collapseThreshold.fractionToDp(density, containerHeightPx), - swipeExpandThreshold = expandThreshold.fractionToDp(density, containerHeightPx), - ) - }, - modifier = modifier.shadeWidth(width, density), - content = content, - ) -} - -/** - * Draws the content of the shade. - * - * @param shadeHeightPx Provider for the current expansion of the shade, in pixels, where `0` is - * fully collapsed. - * @param overstretch Provider for the current amount of vertical "overstretch" that the shade - * should be rendered with. This is `0` or a positive number that is a percentage of the total - * height of the shade when fully expanded. A value of `0` means that the shade is not stretched - * at all. - * @param isSwipingEnabled Whether swiping inside the shade is enabled or not. - * @param swipeableState The state to use for the [swipeable] modifier, allowing external control in - * addition to direct control (proxied user input in addition to non-proxied/direct user input). - * @param anchors A map of [ShadeState] keyed by the vertical position, in pixels, where that state - * occurs; this is used to configure the [swipeable] modifier. - * @param thresholds Function that returns the [ThresholdConfig] for going from one [ShadeState] to - * another. This controls how the [swipeable] decides which [ShadeState] to animate to once the - * user lets go of the shade; e.g. does it animate to fully collapsed or fully expanded. - * @param content The content to render inside the shade. - * @param modifier The [Modifier]. - */ -@Composable -private fun ShadeContent( - shadeHeightPx: () -> Float, - overstretch: () -> Float, - isSwipingEnabled: Boolean, - swipeableState: SwipeableState<ShadeState>, - interactionSource: MutableInteractionSource, - anchors: Map<Float, ShadeState>, - thresholds: (from: ShadeState, to: ShadeState) -> ThresholdConfig, - modifier: Modifier = Modifier, - content: @Composable () -> Unit = {}, -) { - /** - * Returns a function that takes in [Density] and returns the current padding around the shade - * content. - */ - fun padding( - shadeHeightPx: () -> Float, - ): Density.() -> Int { - return { - min( - 12.dp.toPx().roundToInt(), - shadeHeightPx().roundToInt(), - ) - } - } - - Surface( - shape = RoundedCornerShape(32.dp), - modifier = - modifier - .fillMaxWidth() - .height { shadeHeightPx().roundToInt() } - .padding( - horizontal = padding(shadeHeightPx), - vertical = padding(shadeHeightPx), - ) - .graphicsLayer { - // Applies the vertical over-stretching of the shade content that may happen if - // the user keep dragging down when the shade is already fully-expanded. - transformOrigin = transformOrigin.copy(pivotFractionY = 0f) - this.scaleY = 1 + overstretch().coerceAtLeast(0f) - } - .swipeable( - enabled = isSwipingEnabled, - state = swipeableState, - interactionSource = interactionSource, - anchors = anchors, - thresholds = thresholds, - orientation = Orientation.Vertical, - ), - content = content, - ) -} - -/** Funnels current shade expansion values into the view-model. */ -@Composable -private fun ReportShadeExpansion( - viewModel: ShadeViewModel, - swipeableState: SwipeableState<ShadeState>, - containerHeightPx: Float, -) { - LaunchedEffect(swipeableState.offset, containerHeightPx) { - snapshotFlow { swipeableState.offset.value / containerHeightPx } - .collect { expansion -> viewModel.onExpansionChanged(expansion) } - } -} - -/** Funnels drag gesture start and end events into the view-model. */ -@Composable -private fun ReportNonProxiedInput( - viewModel: ShadeViewModel, - interactionSource: InteractionSource, -) { - LaunchedEffect(interactionSource) { - interactionSource.interactions.collect { - when (it) { - is DragInteraction.Start -> { - viewModel.onDragStarted() - } - is DragInteraction.Stop -> { - viewModel.onDragEnded() - } - } - } - } -} - -/** When told to force collapse, collapses the shade. */ -@Composable -private fun HandleForcedCollapse( - viewModel: ShadeViewModel, - swipeableState: SwipeableState<ShadeState>, -) { - LaunchedEffect(viewModel) { - viewModel.isForceCollapsed.collect { - launch { swipeableState.animateTo(ShadeState.FullyCollapsed) } - } - } -} - -/** - * Handles proxied input (input originating outside of the UI of the shade) by driving the - * [SwipeableState] accordingly. - */ -@Composable -private fun HandleProxiedInput( - viewModel: ShadeViewModel, - swipeableState: SwipeableState<ShadeState>, - currentTimeMillis: () -> Long, -) { - val velocityTracker: VelocityTracker = remember { VelocityTracker() } - LaunchedEffect(viewModel) { - viewModel.proxiedInput.collect { - when (it) { - is ProxiedInputModel.OnDrag -> { - velocityTracker.addPosition( - timeMillis = currentTimeMillis.invoke(), - position = Offset(0f, it.yDragAmountPx), - ) - swipeableState.performDrag(it.yDragAmountPx) - } - is ProxiedInputModel.OnDragEnd -> { - launch { - val velocity = velocityTracker.calculateVelocity().y - velocityTracker.resetTracking() - // We use a VelocityTracker to keep a record of how fast the pointer was - // moving such that we know how far to fling the shade when the gesture - // ends. Flinging the SwipeableState using performFling is required after - // one or more calls to performDrag such that the swipeable settles into one - // of the states. Without doing that, the shade would remain unmoving in an - // in-between state on the screen. - swipeableState.performFling(velocity) - } - } - is ProxiedInputModel.OnDragCancel -> { - launch { - velocityTracker.resetTracking() - swipeableState.animateTo(swipeableState.progress.from) - } - } - else -> Unit - } - } - } -} - -/** - * Converts the [Float] (which is assumed to be a fraction between `0` and `1`) to a value in dp. - * - * @param density The [Density] of the display. - * @param wholePx The whole amount that the given [Float] is a fraction of. - * @return The dp size that's a fraction of the whole amount. - */ -private fun Float.fractionToDp(density: Density, wholePx: Float): Dp { - return with(density) { (this@fractionToDp * wholePx).toDp() } -} - -private fun Modifier.shadeWidth( - size: ShadeViewModel.Size, - density: Density, -): Modifier { - return then( - when (size) { - is ShadeViewModel.Size.Fraction -> Modifier.fillMaxWidth(size.fraction) - is ShadeViewModel.Size.Pixels -> Modifier.width(with(density) { size.pixels.toDp() }) - } - ) -} - -/** Returns the pixel positions for each of the supported shade states. */ -private fun swipeableAnchors(containerHeightPx: Float): Map<Float, ShadeState> { - return mapOf( - 0f to ShadeState.FullyCollapsed, - containerHeightPx to ShadeState.FullyExpanded, - ) -} - -/** - * Returns the [ThresholdConfig] for how far the shade should be expanded or collapsed such that it - * actually completes the expansion or collapse after the user lifts their pointer. - */ -private fun swipeableThresholds( - to: ShadeState, - swipeExpandThreshold: Dp, - swipeCollapseThreshold: Dp, -): ThresholdConfig { - return FixedThreshold( - when (to) { - ShadeState.FullyExpanded -> swipeExpandThreshold - ShadeState.FullyCollapsed -> swipeCollapseThreshold - } - ) -} - -/** Enumerates the shade UI states for [SwipeableState]. */ -private enum class ShadeState { - FullyCollapsed, - FullyExpanded, -} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt index 5e0761063af2..32986649388d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt @@ -92,6 +92,14 @@ private fun Scene( onSceneChanged: (SceneModel) -> Unit, modifier: Modifier = Modifier, ) { + val destinationScenes: Map<UserAction, SceneModel> by + scene.destinationScenes(containerName).collectAsState() + val swipeLeftDestinationScene = destinationScenes[UserAction.Swipe(Direction.LEFT)] + val swipeUpDestinationScene = destinationScenes[UserAction.Swipe(Direction.UP)] + val swipeRightDestinationScene = destinationScenes[UserAction.Swipe(Direction.RIGHT)] + val swipeDownDestinationScene = destinationScenes[UserAction.Swipe(Direction.DOWN)] + val backDestinationScene = destinationScenes[UserAction.Back] + // TODO(b/280880714): replace with the real UI and make sure to call onTransitionProgress. Box(modifier) { Column( @@ -103,14 +111,6 @@ private fun Scene( modifier = Modifier, ) - val destinationScenes: Map<UserAction, SceneModel> by - scene.destinationScenes(containerName).collectAsState() - val swipeLeftDestinationScene = destinationScenes[UserAction.Swipe(Direction.LEFT)] - val swipeUpDestinationScene = destinationScenes[UserAction.Swipe(Direction.UP)] - val swipeRightDestinationScene = destinationScenes[UserAction.Swipe(Direction.RIGHT)] - val swipeDownDestinationScene = destinationScenes[UserAction.Swipe(Direction.DOWN)] - val backDestinationScene = destinationScenes[UserAction.Back] - Row( horizontalArrangement = Arrangement.spacedBy(8.dp), ) { diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt index 95a9ce960dcd..d43276c00f87 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderContract.kt @@ -187,6 +187,9 @@ object CustomizationProviderContract { /** Flag denoting transit clock are enabled in wallpaper picker. */ const val FLAG_NAME_TRANSIT_CLOCK = "lockscreen_custom_transit_clock" + /** Flag denoting transit clock are enabled in wallpaper picker. */ + const val FLAG_NAME_PAGE_TRANSITIONS = "wallpaper_picker_page_transitions" + object Columns { /** String. Unique ID for the flag. */ const val NAME = "name" diff --git a/packages/SystemUI/res/color/qs_dialog_btn_filled_background.xml b/packages/SystemUI/res/color/qs_dialog_btn_filled_background.xml new file mode 100644 index 000000000000..40bab5ed08f2 --- /dev/null +++ b/packages/SystemUI/res/color/qs_dialog_btn_filled_background.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <item android:state_enabled="false" + android:color="?androidprv:attr/materialColorPrimary" + android:alpha="0.30"/> + <item android:color="?androidprv:attr/materialColorPrimary"/> +</selector>
\ No newline at end of file diff --git a/packages/SystemUI/res/color/qs_dialog_btn_filled_text_color.xml b/packages/SystemUI/res/color/qs_dialog_btn_filled_text_color.xml new file mode 100644 index 000000000000..e76ad991a92c --- /dev/null +++ b/packages/SystemUI/res/color/qs_dialog_btn_filled_text_color.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <item android:state_enabled="false" + android:color="?androidprv:attr/materialColorOnPrimary" + android:alpha="0.30"/> + <item android:color="?androidprv:attr/materialColorOnPrimary"/> +</selector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_down.xml b/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_down.xml new file mode 100644 index 000000000000..16076b17a6e5 --- /dev/null +++ b/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_down.xml @@ -0,0 +1,31 @@ +<!-- + ~ 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. + --> + +<!-- TODO(b/273761935): This drawable night variant is identical to the standard drawable. Delete once the drawable cache correctly invalidates for attributes that reference colors that change when the UI mode changes. --> +<vector + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M0,12C0,5.373 5.373,0 12,0C18.627,0 24,5.373 24,12C24,18.627 18.627,24 12,24C5.373,24 0,18.627 0,12Z" + android:fillColor="?androidprv:attr/materialColorSurfaceContainerHigh"/> + <path + android:pathData="M7.607,9.059L6.667,9.999L12,15.332L17.333,9.999L16.393,9.059L12,13.445" + android:fillColor="?androidprv:attr/materialColorOnSurface"/> +</vector> diff --git a/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_up.xml b/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_up.xml new file mode 100644 index 000000000000..309770ddd76d --- /dev/null +++ b/packages/SystemUI/res/drawable-night/privacy_dialog_expand_toggle_up.xml @@ -0,0 +1,31 @@ +<!-- + ~ 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. + --> + +<!-- TODO(b/273761935): This drawable night variant is identical to the standard drawable. Delete once the drawable cache correctly invalidates for attributes that reference colors that change when the UI mode changes. --> +<vector + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M0,12C0,5.3726 5.3726,0 12,0C18.6274,0 24,5.3726 24,12C24,18.6274 18.6274,24 12,24C5.3726,24 0,18.6274 0,12Z" + android:fillColor="?androidprv:attr/materialColorSurfaceContainerHigh"/> + <path + android:pathData="M16.3934,14.9393L17.3334,13.9993L12.0001,8.666L6.6667,13.9993L7.6068,14.9393L12.0001,10.5527" + android:fillColor="?androidprv:attr/materialColorOnSurface"/> +</vector> diff --git a/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml b/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml index 54bdf18e3076..bc1775ee64ae 100644 --- a/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml +++ b/packages/SystemUI/res/drawable/dream_overlay_assistant_attention_indicator.xml @@ -1,3 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> <!-- ~ Copyright (C) 2023 The Android Open Source Project ~ @@ -13,30 +14,22 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="56dp" - android:height="24dp" - android:viewportWidth="56" - android:viewportHeight="24"> - <path - android:pathData="M12,0L44,0A12,12 0,0 1,56 12L56,12A12,12 0,0 1,44 24L12,24A12,12 0,0 1,0 12L0,12A12,12 0,0 1,12 0z" - android:fillColor="#ffffff"/> - <group - android:scaleX="0.8" - android:scaleY="0.8" - android:translateY="2" - android:translateX="18"> - <path - android:pathData="M21.5,9C22.3284,9 23,8.3284 23,7.5C23,6.6716 22.3284,6 21.5,6C20.6716,6 20,6.6716 20,7.5C20,8.3284 20.6716,9 21.5,9Z" - android:fillColor="#000000"/> - <path - android:pathData="M17,14C18.6569,14 20,12.6569 20,11C20,9.3432 18.6569,8 17,8C15.3431,8 14,9.3432 14,11C14,12.6569 15.3431,14 17,14Z" - android:fillColor="#000000"/> - <path - android:pathData="M17,22C18.933,22 20.5,20.433 20.5,18.5C20.5,16.567 18.933,15 17,15C15.067,15 13.5,16.567 13.5,18.5C13.5,20.433 15.067,22 17,22Z" - android:fillColor="#000000"/> - <path - android:pathData="M7,14C10.3137,14 13,11.3137 13,8C13,4.6863 10.3137,2 7,2C3.6863,2 1,4.6863 1,8C1,11.3137 3.6863,14 7,14Z" - android:fillColor="#000000"/> - </group> -</vector>
\ No newline at end of file + +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:id="@id/background" + android:gravity="center"> + <shape android:shape="oval"> + <size + android:height="24px" + android:width="24px" + /> + <solid android:color="#FFFFFFFF" /> + </shape> + </item> + <item android:id="@id/icon" + android:gravity="center" + android:width="20px" + android:height="20px" + android:drawable="@drawable/ic_person_outline" + /> +</layer-list>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_person_outline.xml b/packages/SystemUI/res/drawable/ic_person_outline.xml new file mode 100644 index 000000000000..d94714e0d51a --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_person_outline.xml @@ -0,0 +1,26 @@ +<!-- + ~ 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/black" + android:pathData="M480,480Q414,480 367,433Q320,386 320,320Q320,254 367,207Q414,160 480,160Q546,160 593,207Q640,254 640,320Q640,386 593,433Q546,480 480,480ZM160,800L160,688Q160,654 177.5,625.5Q195,597 224,582Q286,551 350,535.5Q414,520 480,520Q546,520 610,535.5Q674,551 736,582Q765,597 782.5,625.5Q800,654 800,688L800,800L160,800ZM240,720L720,720L720,688Q720,677 714.5,668Q709,659 700,654Q646,627 591,613.5Q536,600 480,600Q424,600 369,613.5Q314,627 260,654Q251,659 245.5,668Q240,677 240,688L240,720ZM480,400Q513,400 536.5,376.5Q560,353 560,320Q560,287 536.5,263.5Q513,240 480,240Q447,240 423.5,263.5Q400,287 400,320Q400,353 423.5,376.5Q447,400 480,400ZM480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320Q480,320 480,320ZM480,720L480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720Q480,720 480,720L480,720L480,720Z"/> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/immersive_cling_bg_circ.xml b/packages/SystemUI/res/drawable/immersive_cling_bg_circ.xml index 4029702ec6b4..32e88ab22b91 100644 --- a/packages/SystemUI/res/drawable/immersive_cling_bg_circ.xml +++ b/packages/SystemUI/res/drawable/immersive_cling_bg_circ.xml @@ -17,7 +17,7 @@ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval" > - <solid android:color="@android:color/white" /> + <solid android:color="?android:attr/colorBackground" /> <size android:height="56dp" diff --git a/packages/SystemUI/res/drawable/immersive_cling_light_bg_circ.xml b/packages/SystemUI/res/drawable/immersive_cling_light_bg_circ.xml index e3c7d0ce89aa..12c3e23bf0a0 100644 --- a/packages/SystemUI/res/drawable/immersive_cling_light_bg_circ.xml +++ b/packages/SystemUI/res/drawable/immersive_cling_light_bg_circ.xml @@ -17,7 +17,7 @@ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval" > - <solid android:color="#80ffffff" /> + <solid android:color="?android:attr/colorBackground" /> <size android:height="76dp" diff --git a/packages/SystemUI/res/drawable/media_output_icon_volume.xml b/packages/SystemUI/res/drawable/media_output_icon_volume.xml index fce4e0022c7a..85d608fa736f 100644 --- a/packages/SystemUI/res/drawable/media_output_icon_volume.xml +++ b/packages/SystemUI/res/drawable/media_output_icon_volume.xml @@ -3,7 +3,8 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24" - android:tint="?attr/colorControlNormal"> + android:tint="?attr/colorControlNormal" + android:autoMirrored="true"> <path android:fillColor="@color/media_dialog_item_main_content" android:pathData="M14,20.725V18.675Q16.25,18.025 17.625,16.175Q19,14.325 19,11.975Q19,9.625 17.625,7.775Q16.25,5.925 14,5.275V3.225Q17.1,3.925 19.05,6.362Q21,8.8 21,11.975Q21,15.15 19.05,17.587Q17.1,20.025 14,20.725ZM3,15V9H7L12,4V20L7,15ZM14,16V7.95Q15.125,8.475 15.812,9.575Q16.5,10.675 16.5,12Q16.5,13.325 15.812,14.4Q15.125,15.475 14,16ZM10,8.85 L7.85,11H5V13H7.85L10,15.15ZM7.5,12Z"/> diff --git a/packages/SystemUI/res/drawable/media_output_title_icon_area.xml b/packages/SystemUI/res/drawable/media_output_title_icon_area.xml index b93793773179..a8779002c1b3 100644 --- a/packages/SystemUI/res/drawable/media_output_title_icon_area.xml +++ b/packages/SystemUI/res/drawable/media_output_title_icon_area.xml @@ -17,9 +17,9 @@ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <corners - android:bottomLeftRadius="28dp" - android:topLeftRadius="28dp" - android:bottomRightRadius="0dp" - android:topRightRadius="0dp"/> + android:bottomLeftRadius="@dimen/media_output_dialog_icon_left_radius" + android:topLeftRadius="@dimen/media_output_dialog_icon_left_radius" + android:bottomRightRadius="@dimen/media_output_dialog_icon_right_radius" + android:topRightRadius="@dimen/media_output_dialog_icon_right_radius"/> <solid android:color="@color/media_dialog_item_background" /> </shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/privacy_dialog_background_circle.xml b/packages/SystemUI/res/drawable/privacy_dialog_background_circle.xml new file mode 100644 index 000000000000..f63c2ffbfdad --- /dev/null +++ b/packages/SystemUI/res/drawable/privacy_dialog_background_circle.xml @@ -0,0 +1,29 @@ +<!-- + ~ Copyright (C) 2022 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?android:colorControlHighlight"> + <item android:id="@android:id/background"> + <shape + android:shape="oval" + android:id="@id/background" + android:gravity="center"> + <size + android:height="24dp" + android:width="24dp"/> + <solid android:color="@android:color/white"/> + </shape> + </item> +</ripple>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_large_bottom.xml b/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_large_bottom.xml new file mode 100644 index 000000000000..5d5529ff1a50 --- /dev/null +++ b/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_large_bottom.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?android:colorControlHighlight"> + <item android:id="@android:id/mask"> + <shape android:shape="rectangle"> + <corners + android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_large" + android:bottomRightRadius="@dimen/privacy_dialog_background_radius_large" + android:topLeftRadius="@dimen/privacy_dialog_background_radius_large" + android:topRightRadius="@dimen/privacy_dialog_background_radius_large" /> + <solid android:color="@android:color/white" /> + </shape> + </item> + <item> + <shape android:shape="rectangle"> + <corners + android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_large" + android:bottomRightRadius="@dimen/privacy_dialog_background_radius_large" + android:topLeftRadius="@dimen/privacy_dialog_background_radius_large" + android:topRightRadius="@dimen/privacy_dialog_background_radius_large" /> + <solid android:color="@android:color/white" /> + </shape> + </item> +</ripple> diff --git a/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_small_bottom.xml b/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_small_bottom.xml new file mode 100644 index 000000000000..310b0becf10c --- /dev/null +++ b/packages/SystemUI/res/drawable/privacy_dialog_background_large_top_small_bottom.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?android:colorControlHighlight"> + <item android:id="@android:id/mask"> + <shape android:shape="rectangle"> + <corners + android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_small" + android:bottomRightRadius="@dimen/privacy_dialog_background_radius_small" + android:topLeftRadius="@dimen/privacy_dialog_background_radius_large" + android:topRightRadius="@dimen/privacy_dialog_background_radius_large" /> + <solid android:color="@android:color/white" /> + </shape> + </item> + <item> + <shape android:shape="rectangle"> + <corners + android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_small" + android:bottomRightRadius="@dimen/privacy_dialog_background_radius_small" + android:topLeftRadius="@dimen/privacy_dialog_background_radius_large" + android:topRightRadius="@dimen/privacy_dialog_background_radius_large" /> + <solid android:color="@android:color/white" /> + </shape> + </item> +</ripple> diff --git a/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_large_bottom.xml b/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_large_bottom.xml new file mode 100644 index 000000000000..e89bdd31ca17 --- /dev/null +++ b/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_large_bottom.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?android:colorControlHighlight"> + <item android:id="@android:id/mask"> + <shape android:shape="rectangle"> + <corners + android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_large" + android:bottomRightRadius="@dimen/privacy_dialog_background_radius_large" + android:topLeftRadius="@dimen/privacy_dialog_background_radius_small" + android:topRightRadius="@dimen/privacy_dialog_background_radius_small" /> + <solid android:color="@android:color/white" /> + </shape> + </item> + <item> + <shape android:shape="rectangle"> + <corners + android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_large" + android:bottomRightRadius="@dimen/privacy_dialog_background_radius_large" + android:topLeftRadius="@dimen/privacy_dialog_background_radius_small" + android:topRightRadius="@dimen/privacy_dialog_background_radius_small" /> + <solid android:color="@android:color/white" /> + </shape> + </item> +</ripple> diff --git a/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_small_bottom.xml b/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_small_bottom.xml new file mode 100644 index 000000000000..fcf0b1c5091f --- /dev/null +++ b/packages/SystemUI/res/drawable/privacy_dialog_background_small_top_small_bottom.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="?android:colorControlHighlight"> + <item android:id="@android:id/mask"> + <shape android:shape="rectangle"> + <corners + android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_small" + android:bottomRightRadius="@dimen/privacy_dialog_background_radius_small" + android:topLeftRadius="@dimen/privacy_dialog_background_radius_small" + android:topRightRadius="@dimen/privacy_dialog_background_radius_small" /> + <solid android:color="@android:color/white" /> + </shape> + </item> + <item> + <shape android:shape="rectangle"> + <corners + android:bottomLeftRadius="@dimen/privacy_dialog_background_radius_small" + android:bottomRightRadius="@dimen/privacy_dialog_background_radius_small" + android:topLeftRadius="@dimen/privacy_dialog_background_radius_small" + android:topRightRadius="@dimen/privacy_dialog_background_radius_small" /> + <solid android:color="@android:color/white" /> + </shape> + </item> +</ripple> diff --git a/packages/SystemUI/res/drawable/privacy_dialog_check_icon.xml b/packages/SystemUI/res/drawable/privacy_dialog_check_icon.xml new file mode 100644 index 000000000000..b9f5d60abd8b --- /dev/null +++ b/packages/SystemUI/res/drawable/privacy_dialog_check_icon.xml @@ -0,0 +1,25 @@ +<!-- + ~ 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="#FF000000" + android:pathData="M9.55,18l-5.7,-5.7 1.425,-1.425L9.55,15.15l9.175,-9.175L20.15,7.4z"/> +</vector>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/privacy_dialog_default_permission_icon.xml b/packages/SystemUI/res/drawable/privacy_dialog_default_permission_icon.xml new file mode 100644 index 000000000000..ea8f9c2839ec --- /dev/null +++ b/packages/SystemUI/res/drawable/privacy_dialog_default_permission_icon.xml @@ -0,0 +1,25 @@ +<!-- + ~ 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. + --> + +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24.0dp" + android:height="24.0dp" + android:viewportWidth="48.0" + android:viewportHeight="48.0"> + <path + android:fillColor="#FF000000" + android:pathData="M26.0,14.0l-4.0,0.0l0.0,4.0l4.0,0.0l0.0,-4.0zm0.0,8.0l-4.0,0.0l0.0,12.0l4.0,0.0L26.0,22.0zm8.0,-19.98L14.0,2.0c-2.21,0.0 -4.0,1.79 -4.0,4.0l0.0,36.0c0.0,2.21 1.79,4.0 4.0,4.0l20.0,0.0c2.21,0.0 4.0,-1.79 4.0,-4.0L38.0,6.0c0.0,-2.21 -1.79,-3.98 -4.0,-3.98zM34.0,38.0L14.0,38.0L14.0,10.0l20.0,0.0l0.0,28.0z"/> +</vector> diff --git a/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_down.xml b/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_down.xml new file mode 100644 index 000000000000..f8b99f4a0ee4 --- /dev/null +++ b/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_down.xml @@ -0,0 +1,30 @@ +<!-- + ~ 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. + --> + +<vector + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M0,12C0,5.373 5.373,0 12,0C18.627,0 24,5.373 24,12C24,18.627 18.627,24 12,24C5.373,24 0,18.627 0,12Z" + android:fillColor="?androidprv:attr/materialColorSurfaceContainerHigh"/> + <path + android:pathData="M7.607,9.059L6.667,9.999L12,15.332L17.333,9.999L16.393,9.059L12,13.445" + android:fillColor="?androidprv:attr/materialColorOnSurface"/> +</vector> diff --git a/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_up.xml b/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_up.xml new file mode 100644 index 000000000000..ae60d517ceb4 --- /dev/null +++ b/packages/SystemUI/res/drawable/privacy_dialog_expand_toggle_up.xml @@ -0,0 +1,30 @@ +<!-- + ~ 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. + --> + +<vector + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M0,12C0,5.3726 5.3726,0 12,0C18.6274,0 24,5.3726 24,12C24,18.6274 18.6274,24 12,24C5.3726,24 0,18.6274 0,12Z" + android:fillColor="?androidprv:attr/materialColorSurfaceContainerHigh"/> + <path + android:pathData="M16.3934,14.9393L17.3334,13.9993L12.0001,8.666L6.6667,13.9993L7.6068,14.9393L12.0001,10.5527" + android:fillColor="?androidprv:attr/materialColorOnSurface"/> +</vector> diff --git a/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml b/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml index c4e45bf2c223..9bc8b53b308e 100644 --- a/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml +++ b/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml @@ -15,7 +15,6 @@ ~ limitations under the License. --> <inset xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:insetTop="@dimen/dialog_button_vertical_inset" android:insetBottom="@dimen/dialog_button_vertical_inset"> <ripple android:color="?android:attr/colorControlHighlight"> @@ -28,7 +27,7 @@ <item> <shape android:shape="rectangle"> <corners android:radius="?android:attr/buttonCornerRadius"/> - <solid android:color="?androidprv:attr/materialColorPrimary"/> + <solid android:color="@color/qs_dialog_btn_filled_background"/> <padding android:left="@dimen/dialog_button_horizontal_padding" android:top="@dimen/dialog_button_vertical_padding" android:right="@dimen/dialog_button_horizontal_padding" diff --git a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml index bb32022a0b5f..82410703c9e6 100644 --- a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml +++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml @@ -110,7 +110,7 @@ <ImageView android:id="@+id/dream_overlay_assistant_attention_indicator" - android:layout_width="@dimen/dream_overlay_grey_chip_width" + android:layout_width="@dimen/dream_overlay_status_bar_icon_size" android:layout_height="match_parent" android:layout_marginStart="@dimen/dream_overlay_status_icon_margin" android:src="@drawable/dream_overlay_assistant_attention_indicator" diff --git a/packages/SystemUI/res/layout/immersive_mode_cling.xml b/packages/SystemUI/res/layout/immersive_mode_cling.xml index bfb8184ee044..e6529b9aa9a1 100644 --- a/packages/SystemUI/res/layout/immersive_mode_cling.xml +++ b/packages/SystemUI/res/layout/immersive_mode_cling.xml @@ -58,7 +58,7 @@ android:paddingStart="48dp" android:paddingTop="40dp" android:text="@string/immersive_cling_title" - android:textColor="@android:color/white" + android:textColor="?android:attr/textColorPrimaryInverse" android:textSize="24sp" /> <TextView @@ -70,7 +70,7 @@ android:paddingStart="48dp" android:paddingTop="12.6dp" android:text="@string/immersive_cling_description" - android:textColor="@android:color/white" + android:textColor="?android:attr/textColorPrimaryInverse" android:textSize="16sp" /> <Button @@ -85,7 +85,7 @@ android:paddingEnd="8dp" android:paddingStart="8dp" android:text="@string/immersive_cling_positive" - android:textColor="@android:color/white" + android:textColor="?android:attr/textColorPrimaryInverse" android:textSize="14sp" /> </RelativeLayout> diff --git a/packages/SystemUI/res/layout/privacy_dialog_card_button.xml b/packages/SystemUI/res/layout/privacy_dialog_card_button.xml new file mode 100644 index 000000000000..e297b939e2b8 --- /dev/null +++ b/packages/SystemUI/res/layout/privacy_dialog_card_button.xml @@ -0,0 +1,26 @@ +<!-- + ~ 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. + --> +<Button + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="56dp" + android:layout_marginBottom="4dp" + android:ellipsize="end" + android:maxLines="1" + android:clickable="true" + android:focusable="true" + android:gravity="center" + style="@style/Widget.Dialog.Button.BorderButton"/>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/privacy_dialog_item_v2.xml b/packages/SystemUI/res/layout/privacy_dialog_item_v2.xml new file mode 100644 index 000000000000..b84f3a9794be --- /dev/null +++ b/packages/SystemUI/res/layout/privacy_dialog_item_v2.xml @@ -0,0 +1,89 @@ +<!-- + ~ 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. + --> +<androidx.cardview.widget.CardView + 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:id="@+id/privacy_dialog_item_card" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" + android:foreground="?android:attr/selectableItemBackground" + app:cardCornerRadius="28dp" + app:cardElevation="0dp" + app:cardBackgroundColor="?androidprv:attr/materialColorSurfaceBright"> + <LinearLayout + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + <LinearLayout + android:id="@+id/privacy_dialog_item_header" + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="20dp" + android:paddingBottom="20dp" + android:paddingStart="24dp" + android:paddingEnd="24dp"> + <ImageView + android:id="@+id/privacy_dialog_item_header_icon" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_centerVertical="true" + android:importantForAccessibility="no" /> + <LinearLayout + android:orientation="vertical" + android:layout_width="0dp" + android:layout_weight="1" + android:layout_height="match_parent" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_centerVertical="true"> + <TextView + android:id="@+id/privacy_dialog_item_header_title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="2dp" + android:hyphenationFrequency="normalFast" + android:textAlignment="viewStart" + android:textAppearance="@style/TextAppearance.PrivacyDialog.Item.Title" /> + <TextView + android:id="@+id/privacy_dialog_item_header_summary" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAlignment="viewStart" + android:textAppearance="@style/TextAppearance.PrivacyDialog.Item.Summary" /> + </LinearLayout> + <ImageView + android:id="@+id/privacy_dialog_item_header_expand_toggle" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_centerVertical="true" + android:visibility="gone" /> + </LinearLayout> + <LinearLayout + android:id="@+id/privacy_dialog_item_header_expanded_layout" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingBottom="16dp" + android:paddingStart="24dp" + android:paddingEnd="24dp" + android:visibility="gone"> + </LinearLayout> + </LinearLayout> +</androidx.cardview.widget.CardView>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/privacy_dialog_v2.xml b/packages/SystemUI/res/layout/privacy_dialog_v2.xml new file mode 100644 index 000000000000..843dad03bca4 --- /dev/null +++ b/packages/SystemUI/res/layout/privacy_dialog_v2.xml @@ -0,0 +1,109 @@ +<!-- + ~ 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. + --> +<androidx.core.widget.NestedScrollView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:layout_width="@dimen/large_dialog_width" + android:layout_height="wrap_content"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:orientation="vertical"> + + <!-- Header --> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:gravity="center"> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceLarge" + android:fontFamily="@*android:string/config_headlineFontFamily" + android:text="@string/privacy_dialog_title" + android:layout_marginBottom="12dp"/> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/privacy_dialog_summary" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?androidprv:attr/materialColorOnSurfaceVariant" + android:gravity="center" + android:layout_marginBottom="20dp"/> + </LinearLayout> + + <!-- Items --> + <LinearLayout + android:id="@+id/privacy_dialog_items_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="20dp" + android:orientation="vertical" + /> + + <!-- Buttons --> + <LinearLayout + android:id="@+id/button_layout" + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="18dp" + android:clickable="false" + android:focusable="false"> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_gravity="start|center_vertical" + android:orientation="vertical"> + <Button + android:id="@+id/privacy_dialog_more_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/privacy_dialog_more_button" + android:ellipsize="end" + android:maxLines="1" + style="@style/Widget.Dialog.Button.BorderButton" + android:clickable="true" + android:focusable="true" + android:gravity="center"/> + </LinearLayout> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end|center_vertical"> + <Button + android:id="@+id/privacy_dialog_close_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/privacy_dialog_done_button" + android:ellipsize="end" + android:maxLines="1" + style="@style/Widget.Dialog.Button.BorderButton" + android:clickable="true" + android:focusable="true" + android:gravity="center"/> + </LinearLayout> + </LinearLayout> + </LinearLayout> +</androidx.core.widget.NestedScrollView>
\ No newline at end of file diff --git a/packages/SystemUI/res/values-ldrtl/dimens.xml b/packages/SystemUI/res/values-ldrtl/dimens.xml new file mode 100644 index 000000000000..0d99b617819b --- /dev/null +++ b/packages/SystemUI/res/values-ldrtl/dimens.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> +<resources> + <dimen name="media_output_dialog_icon_left_radius">0dp</dimen> + <dimen name="media_output_dialog_icon_right_radius">28dp</dimen> +</resources>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index db7eb7a049e7..ab754985e11d 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -16,7 +16,8 @@ * limitations under the License. */ --> -<resources xmlns:android="http://schemas.android.com/apk/res/android"> +<resources xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> <drawable name="notification_number_text_color">#ffffffff</drawable> <drawable name="system_bar_background">@color/system_bar_background_opaque</drawable> <color name="system_bar_background_opaque">#ff000000</color> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 3366f4f6d443..5a15dcec5223 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1097,6 +1097,9 @@ <dimen name="ongoing_appops_dialog_side_padding">16dp</dimen> + <dimen name="privacy_dialog_background_radius_large">12dp</dimen> + <dimen name="privacy_dialog_background_radius_small">4dp</dimen> + <!-- Size of media cards in the QSPanel carousel --> <dimen name="qs_media_padding">16dp</dimen> <dimen name="qs_media_album_radius">14dp</dimen> @@ -1346,6 +1349,8 @@ <dimen name="media_output_dialog_default_margin_end">16dp</dimen> <dimen name="media_output_dialog_selectable_margin_end">80dp</dimen> <dimen name="media_output_dialog_list_padding_top">8dp</dimen> + <dimen name="media_output_dialog_icon_left_radius">28dp</dimen> + <dimen name="media_output_dialog_icon_right_radius">0dp</dimen> <!-- Distance that the full shade transition takes in order to complete by tapping on a button like "expand". --> diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 3a2177a0045c..15ca9d48c62a 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -214,4 +214,8 @@ <item type="id" name="nssl_guideline" /> <item type="id" name="lock_icon" /> <item type="id" name="lock_icon_bg" /> + + <!-- Privacy dialog --> + <item type="id" name="privacy_dialog_close_app_button" /> + <item type="id" name="privacy_dialog_manage_app_button" /> </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index f8c13b008fd9..c11e7ba207ae 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2582,6 +2582,9 @@ <!-- Tooltip to show in management screen when there are multiple structures [CHAR_LIMIT=50] --> <string name="controls_structure_tooltip">Swipe to see more</string> + <!-- Accessibility action informing the user how they can retry face authentication [CHAR LIMIT=NONE] --> + <string name="retry_face">Retry face authentication</string> + <!-- Message to tell the user to wait while systemui attempts to load a set of recommended controls [CHAR_LIMIT=60] --> <string name="controls_seeding_in_progress">Loading recommendations</string> @@ -3167,10 +3170,43 @@ <!--- Content of toast triggered when the notes app entry point is triggered without setting a default notes app. [CHAR LIMIT=NONE] --> <string name="set_default_notes_app_toast_content">Set default notes app in Settings</string> - <!-- - Label for a button that, when clicked, sends the user to the app store to install an app. - - [CHAR LIMIT=64]. - --> + <!-- Label for a button that, when clicked, sends the user to the app store to install an app. [CHAR LIMIT=64]. --> <string name="install_app">Install app</string> + + <!-- Title of the privacy dialog, shown for active / recent app usage of some phone sensors [CHAR LIMIT=30] --> + <string name="privacy_dialog_title">Microphone & Camera</string> + <!-- Subtitle of the privacy dialog, shown for active / recent app usage of some phone sensors [CHAR LIMIT=NONE] --> + <string name="privacy_dialog_summary">Recent app use</string> + <!-- Label of the secondary button of the privacy dialog, used to check recent app usage of phone sensors [CHAR LIMIT=30] --> + <string name="privacy_dialog_more_button">See recent access</string> + <!-- Label of the primary button to dismiss the privacy dialog [CHAR LIMIT=20] --> + <string name="privacy_dialog_done_button">Done</string> + <!-- Description for expanding a collapsible widget in the privacy dialog [CHAR LIMIT=NONE] --> + <string name="privacy_dialog_expand_action">Expand and show options</string> + <!-- Description for collapsing a collapsible widget in the privacy dialog [CHAR LIMIT=NONE] --> + <string name="privacy_dialog_collapse_action">Collapse</string> + <!-- Label of a button of the privacy dialog to close an app actively using a phone sensor [CHAR LIMIT=50] --> + <string name="privacy_dialog_close_app_button">Close this app</string> + <!-- Message shown in the privacy dialog when an app actively using a phone sensor is closed [CHAR LIMIT=NONE] --> + <string name="privacy_dialog_close_app_message"><xliff:g id="app_name" example="Gmail">%1$s</xliff:g> closed</string> + <!-- Label of a button of the privacy dialog to learn more of a service actively or recently using a phone sensor [CHAR LIMIT=50] --> + <string name="privacy_dialog_manage_service">Manage service</string> + <!-- Label of a button of the privacy dialog to manage permissions of an app actively or recently using a phone sensor [CHAR LIMIT=50] --> + <string name="privacy_dialog_manage_permissions">Manage access</string> + <!-- Label for active usage of a phone sensor by phone call in the privacy dialog [CHAR LIMIT=NONE] --> + <string name="privacy_dialog_active_call_usage">In use by phone call</string> + <!-- Label for recent usage of a phone sensor by phone call in the privacy dialog [CHAR LIMIT=NONE] --> + <string name="privacy_dialog_recent_call_usage">Recently used in phone call</string> + <!-- Label for active app usage of a phone sensor in the privacy dialog [CHAR LIMIT=NONE] --> + <string name="privacy_dialog_active_app_usage">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g></string> + <!-- Label for recent app usage of a phone sensor in the privacy dialog [CHAR LIMIT=NONE] --> + <string name="privacy_dialog_recent_app_usage">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g></string> + <!-- Label for active app usage of a phone sensor with sub-attribution or proxy label in the privacy dialog [CHAR LIMIT=NONE] --> + <string name="privacy_dialog_active_app_usage_1">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g>)</string> + <!-- Label for recent app usage of a phone sensor with sub-attribution or proxy label in the privacy dialog [CHAR LIMIT=NONE] --> + <string name="privacy_dialog_recent_app_usage_1">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g>)</string> + <!-- Label for active app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] --> + <string name="privacy_dialog_active_app_usage_2">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string> + <!-- Label for recent app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] --> + <string name="privacy_dialog_recent_app_usage_2">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string> </resources> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 31f40e99e91c..d520670ec012 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -1116,7 +1116,7 @@ <style name="Widget.Dialog.Button"> <item name="android:buttonCornerRadius">28dp</item> <item name="android:background">@drawable/qs_dialog_btn_filled</item> - <item name="android:textColor">?androidprv:attr/materialColorOnPrimary</item> + <item name="android:textColor">@color/qs_dialog_btn_filled_text_color</item> <item name="android:textSize">14sp</item> <item name="android:lineHeight">20sp</item> <item name="android:fontFamily">@*android:string/config_bodyFontFamilyMedium</item> @@ -1439,4 +1439,22 @@ <item name="android:windowEnterAnimation">@anim/long_press_lock_screen_popup_enter</item> <item name="android:windowExitAnimation">@anim/long_press_lock_screen_popup_exit</item> </style> + + <style name="TextAppearance.PrivacyDialog.Item.Title" + parent="@android:style/TextAppearance.DeviceDefault.Medium"> + <item name="android:textSize">14sp</item> + <item name="android:lineHeight">20sp</item> + <item name="android:textColor">?androidprv:attr/materialColorOnSurface</item> + </style> + + <style name="TextAppearance.PrivacyDialog.Item.Summary" + parent="@android:style/TextAppearance.DeviceDefault.Small"> + <item name="android:textSize">14sp</item> + <item name="android:lineHeight">20sp</item> + <item name="android:textColor">?androidprv:attr/materialColorOnSurfaceVariant</item> + </style> + + <style name="Theme.PrivacyDialog" parent="@style/Theme.SystemUI.Dialog"> + <item name="android:colorBackground">?androidprv:attr/materialColorSurfaceContainer</item> + </style> </resources> diff --git a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt index d8085b9f9f2e..22cdb30376d0 100644 --- a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt +++ b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt @@ -20,6 +20,7 @@ import android.annotation.StringDef import android.os.PowerManager import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger +import com.android.keyguard.FaceAuthApiRequestReason.Companion.ACCESSIBILITY_ACTION import com.android.keyguard.FaceAuthApiRequestReason.Companion.NOTIFICATION_PANEL_CLICKED import com.android.keyguard.FaceAuthApiRequestReason.Companion.PICK_UP_GESTURE_TRIGGERED import com.android.keyguard.FaceAuthApiRequestReason.Companion.QS_EXPANDED @@ -71,6 +72,7 @@ import com.android.keyguard.InternalFaceAuthReasons.USER_SWITCHING NOTIFICATION_PANEL_CLICKED, QS_EXPANDED, PICK_UP_GESTURE_TRIGGERED, + ACCESSIBILITY_ACTION, ) annotation class FaceAuthApiRequestReason { companion object { @@ -80,6 +82,7 @@ annotation class FaceAuthApiRequestReason { const val QS_EXPANDED = "Face auth due to QS expansion." const val PICK_UP_GESTURE_TRIGGERED = "Face auth due to pickup gesture triggered when the device is awake and not from AOD." + const val ACCESSIBILITY_ACTION = "Face auth due to an accessibility action." } } @@ -217,7 +220,8 @@ constructor(private val id: Int, val reason: String, var extraInfo: Int = 0) : @UiEvent(doc = STRONG_AUTH_ALLOWED_CHANGED) FACE_AUTH_UPDATED_STRONG_AUTH_CHANGED(1255, STRONG_AUTH_ALLOWED_CHANGED), @UiEvent(doc = NON_STRONG_BIOMETRIC_ALLOWED_CHANGED) - FACE_AUTH_NON_STRONG_BIOMETRIC_ALLOWED_CHANGED(1256, NON_STRONG_BIOMETRIC_ALLOWED_CHANGED); + FACE_AUTH_NON_STRONG_BIOMETRIC_ALLOWED_CHANGED(1256, NON_STRONG_BIOMETRIC_ALLOWED_CHANGED), + @UiEvent(doc = ACCESSIBILITY_ACTION) FACE_AUTH_ACCESSIBILITY_ACTION(1454, ACCESSIBILITY_ACTION); override fun getId(): Int = this.id @@ -233,6 +237,8 @@ private val apiRequestReasonToUiEvent = FaceAuthUiEvent.FACE_AUTH_TRIGGERED_NOTIFICATION_PANEL_CLICKED, QS_EXPANDED to FaceAuthUiEvent.FACE_AUTH_TRIGGERED_QS_EXPANDED, PICK_UP_GESTURE_TRIGGERED to FaceAuthUiEvent.FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED, + PICK_UP_GESTURE_TRIGGERED to FaceAuthUiEvent.FACE_AUTH_TRIGGERED_PICK_UP_GESTURE_TRIGGERED, + ACCESSIBILITY_ACTION to FaceAuthUiEvent.FACE_AUTH_ACCESSIBILITY_ACTION, ) /** Converts the [reason] to the corresponding [FaceAuthUiEvent]. */ diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index f9523370adb1..bc24249b23c9 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -68,6 +68,7 @@ import com.android.keyguard.dagger.KeyguardBouncerScope; import com.android.settingslib.utils.ThreadUtils; import com.android.systemui.Gefingerpoken; import com.android.systemui.R; +import com.android.systemui.biometrics.FaceAuthAccessibilityDelegate; import com.android.systemui.biometrics.SideFpsController; import com.android.systemui.biometrics.SideFpsUiRequestSource; import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor; @@ -417,9 +418,11 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard BouncerMessageInteractor bouncerMessageInteractor, Provider<JavaAdapter> javaAdapter, UserInteractor userInteractor, + FaceAuthAccessibilityDelegate faceAuthAccessibilityDelegate, Provider<SceneInteractor> sceneInteractor ) { super(view); + view.setAccessibilityDelegate(faceAuthAccessibilityDelegate); mLockPatternUtils = lockPatternUtils; mUpdateMonitor = keyguardUpdateMonitor; mSecurityModel = keyguardSecurityModel; diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 6bd9df0245e7..5833d98f04b2 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -3158,6 +3158,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return false; } + if (isFaceAuthInteractorEnabled()) { + return mFaceAuthInteractor.canFaceAuthRun(); + } + final boolean statusBarShadeLocked = mStatusBarState == StatusBarState.SHADE_LOCKED; final boolean awakeKeyguard = isKeyguardVisible() && mDeviceInteractive && !statusBarShadeLocked; diff --git a/packages/SystemUI/src/com/android/systemui/CoreStartable.java b/packages/SystemUI/src/com/android/systemui/CoreStartable.java index becf5b39e9df..c07a4d26f476 100644 --- a/packages/SystemUI/src/com/android/systemui/CoreStartable.java +++ b/packages/SystemUI/src/com/android/systemui/CoreStartable.java @@ -53,6 +53,11 @@ public interface CoreStartable extends Dumpable { default void dump(@NonNull PrintWriter pw, @NonNull String[] args) { } + /** Called to determine if the dumpable should be registered as critical or normal priority */ + default boolean isDumpCritical() { + return true; + } + /** Called immediately after the system broadcasts * {@link android.content.Intent#ACTION_LOCKED_BOOT_COMPLETED} or during SysUI startup if the * property {@code sys.boot_completed} is already set to 1. The latter typically occurs when diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java index 522b3976f76e..38298cfac49a 100644 --- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java +++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java @@ -254,11 +254,16 @@ public class SystemUIApplication extends Application implements } for (i = 0; i < mServices.length; i++) { + final CoreStartable service = mServices[i]; if (mBootCompleteCache.isBootComplete()) { - notifyBootCompleted(mServices[i]); + notifyBootCompleted(service); } - dumpManager.registerDumpable(mServices[i].getClass().getSimpleName(), mServices[i]); + if (service.isDumpCritical()) { + dumpManager.registerCriticalDumpable(service); + } else { + dumpManager.registerNormalDumpable(service); + } } mSysUIComponent.getInitController().executePostInitTasks(); log.traceEnd(); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java index 2f6a68c3ff8d..03ad132c452c 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java @@ -54,6 +54,7 @@ import com.android.systemui.recents.Recents; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeController; +import com.android.systemui.shade.ShadeViewController; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.CentralSurfaces; @@ -188,6 +189,7 @@ public class SystemActions implements CoreStartable { private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy; private final NotificationShadeWindowController mNotificationShadeController; private final ShadeController mShadeController; + private final Lazy<ShadeViewController> mShadeViewController; private final StatusBarWindowCallback mNotificationShadeCallback; private boolean mDismissNotificationShadeActionRegistered; @@ -196,12 +198,14 @@ public class SystemActions implements CoreStartable { UserTracker userTracker, NotificationShadeWindowController notificationShadeController, ShadeController shadeController, + Lazy<ShadeViewController> shadeViewController, Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy, Optional<Recents> recentsOptional, DisplayTracker displayTracker) { mContext = context; mUserTracker = userTracker; mShadeController = shadeController; + mShadeViewController = shadeViewController; mRecentsOptional = recentsOptional; mDisplayTracker = displayTracker; mReceiver = new SystemActionsBroadcastReceiver(); @@ -330,8 +334,7 @@ public class SystemActions implements CoreStartable { final Optional<CentralSurfaces> centralSurfacesOptional = mCentralSurfacesOptionalLazy.get(); if (centralSurfacesOptional.isPresent() - && centralSurfacesOptional.get().getShadeViewController() != null - && centralSurfacesOptional.get().getShadeViewController().isPanelExpanded() + && mShadeViewController.get().isPanelExpanded() && !centralSurfacesOptional.get().isKeyguardShowing()) { if (!mDismissNotificationShadeActionRegistered) { mA11yManager.registerSystemAction( diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java index 602f817f826b..6c8f8f39646e 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java @@ -262,6 +262,9 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, mResources.getInteger(R.integer.magnification_default_scale), UserHandle.USER_CURRENT); + mAllowDiagonalScrolling = secureSettings.getIntForUser( + Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING, 1, + UserHandle.USER_CURRENT) == 1; setupMagnificationSizeScaleOptions(); @@ -1225,6 +1228,12 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold return isActivated() ? mMagnificationFrame.exactCenterY() : Float.NaN; } + + @VisibleForTesting + boolean isDiagonalScrollingEnabled() { + return mAllowDiagonalScrolling; + } + private CharSequence formatStateDescription(float scale) { // Cache the locale-appropriate NumberFormat. Configuration locale is guaranteed // non-null, so the first time this is called we will always get the appropriate diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java index bb29d52cf160..7c11311373ab 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java @@ -101,7 +101,9 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest private Button mEditButton; private ImageButton mFullScreenButton; private int mLastSelectedButtonIndex = MagnificationSize.NONE; + private boolean mAllowDiagonalScrolling = false; + /** * Amount by which magnification scale changes compared to seekbar in settings. * magnitude = 10 means, for every 1 scale increase, 10 progress increase in seekbar. @@ -141,7 +143,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest mSecureSettings = secureSettings; mAllowDiagonalScrolling = mSecureSettings.getIntForUser( - Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING, 0, + Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING, 1, UserHandle.USER_CURRENT) == 1; mParams = createLayoutParams(context); @@ -420,6 +422,11 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest UserHandle.USER_CURRENT); } + @VisibleForTesting + boolean isDiagonalScrollingEnabled() { + return mAllowDiagonalScrolling; + } + /** * Only called from outside to notify the controlling magnifier scale changed * @@ -632,7 +639,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest private void toggleDiagonalScrolling() { boolean enabled = mSecureSettings.getIntForUser( - Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING, 0, + Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING, 1, UserHandle.USER_CURRENT) == 1; setDiagonalScrolling(!enabled); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt index 783460c325fa..0ef256d41157 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt @@ -145,6 +145,8 @@ class FontScalingDialog( */ @MainThread fun updateFontScaleDelayed(delayMsFromSource: Long) { + doneButton.isEnabled = false + var delayMs = delayMsFromSource if (systemClock.elapsedRealtime() - lastUpdateTime < MIN_UPDATE_INTERVAL_MS) { delayMs += MIN_UPDATE_INTERVAL_MS @@ -197,17 +199,22 @@ class FontScalingDialog( title.post { title.setTextAppearance(R.style.TextAppearance_Dialog_Title) doneButton.setTextAppearance(R.style.Widget_Dialog_Button) + doneButton.isEnabled = true } } } @WorkerThread fun updateFontScale() { - systemSettings.putStringForUser( - Settings.System.FONT_SCALE, - strEntryValues[lastProgress.get()], - userTracker.userId - ) + if ( + !systemSettings.putStringForUser( + Settings.System.FONT_SCALE, + strEntryValues[lastProgress.get()], + userTracker.userId + ) + ) { + title.post { doneButton.isEnabled = true } + } } @WorkerThread diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt index a9779663cc7c..deb3d035d753 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt @@ -14,8 +14,6 @@ * limitations under the License. */ -@file:OptIn(ExperimentalCoroutinesApi::class) - package com.android.systemui.authentication.data.repository import com.android.internal.widget.LockPatternChecker @@ -29,6 +27,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.time.SystemClock import dagger.Binds import dagger.Module @@ -38,16 +37,14 @@ import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** Defines interface for classes that can access authentication-related application state. */ @@ -156,32 +153,18 @@ constructor( } override val isAutoConfirmEnabled: StateFlow<Boolean> = - userRepository.selectedUserInfo - .map { it.id } - .flatMapLatest { userId -> - flow { emit(lockPatternUtils.isAutoPinConfirmEnabled(userId)) } - .flowOn(backgroundDispatcher) - } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = false, - ) + refreshingFlow( + initialValue = false, + getFreshValue = lockPatternUtils::isAutoPinConfirmEnabled, + ) override val hintedPinLength: Int = 6 override val isPatternVisible: StateFlow<Boolean> = - userRepository.selectedUserInfo - .map { it.id } - .flatMapLatest { userId -> - flow { emit(lockPatternUtils.isVisiblePatternEnabled(userId)) } - .flowOn(backgroundDispatcher) - } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = true, - ) + refreshingFlow( + initialValue = true, + getFreshValue = lockPatternUtils::isVisiblePatternEnabled, + ) private val _throttling = MutableStateFlow(AuthenticationThrottlingModel()) override val throttling: StateFlow<AuthenticationThrottlingModel> = _throttling.asStateFlow() @@ -276,6 +259,48 @@ constructor( ) } } + + /** + * Returns a [StateFlow] that's automatically kept fresh. The passed-in [getFreshValue] is + * invoked on a background thread every time the selected user is changed and every time a new + * downstream subscriber is added to the flow. + * + * Initially, the flow will emit [initialValue] while it refreshes itself in the background by + * invoking the [getFreshValue] function and emitting the fresh value when that's done. + * + * Every time the selected user is changed, the flow will re-invoke [getFreshValue] and emit the + * new value. + * + * Every time a new downstream subscriber is added to the flow it first receives the latest + * cached value that's either the [initialValue] or the latest previously fetched value. In + * addition, adding a new downstream subscriber also triggers another [getFreshValue] call and a + * subsequent emission of that newest value. + */ + private fun <T> refreshingFlow( + initialValue: T, + getFreshValue: suspend (selectedUserId: Int) -> T, + ): StateFlow<T> { + val flow = MutableStateFlow(initialValue) + applicationScope.launch { + combine( + // Emits a value initially and every time the selected user is changed. + userRepository.selectedUserInfo.map { it.id }.distinctUntilChanged(), + // Emits a value only when the number of downstream subscribers of this flow + // increases. + flow.subscriptionCount.pairwise(initialValue = 0).filter { (previous, current) + -> + current > previous + }, + ) { selectedUserId, _ -> + selectedUserId + } + .collect { selectedUserId -> + flow.value = withContext(backgroundDispatcher) { getFreshValue(selectedUserId) } + } + } + + return flow.asStateFlow() + } } @Module diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt index b482977bde67..d4371bf30e0e 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt @@ -104,7 +104,9 @@ constructor( } .stateIn( scope = applicationScope, - started = SharingStarted.Eagerly, + // Make sure this is kept as WhileSubscribed or we can run into a bug where the + // downstream continues to receive old/stale/cached values. + started = SharingStarted.WhileSubscribed(), initialValue = null, ) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt b/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt new file mode 100644 index 000000000000..b9fa24022ad5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegate.kt @@ -0,0 +1,62 @@ +/* + * 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.biometrics + +import android.content.res.Resources +import android.os.Bundle +import android.view.View +import android.view.accessibility.AccessibilityNodeInfo +import com.android.keyguard.FaceAuthApiRequestReason +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.R +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor +import javax.inject.Inject + +/** + * Accessibility delegate that will add a click accessibility action to a view when face auth can + * run. When the click a11y action is triggered, face auth will retry. + */ +@SysUISingleton +class FaceAuthAccessibilityDelegate +@Inject +constructor( + @Main private val resources: Resources, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val faceAuthInteractor: KeyguardFaceAuthInteractor, +) : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfo) { + super.onInitializeAccessibilityNodeInfo(host, info) + if (keyguardUpdateMonitor.shouldListenForFace()) { + val clickActionToRetryFace = + AccessibilityNodeInfo.AccessibilityAction( + AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id, + resources.getString(R.string.retry_face) + ) + info.addAction(clickActionToRetryFace) + } + } + + override fun performAccessibilityAction(host: View?, action: Int, args: Bundle?): Boolean { + return if (action == AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id) { + keyguardUpdateMonitor.requestFaceAuth(FaceAuthApiRequestReason.ACCESSIBILITY_ACTION) + faceAuthInteractor.onAccessibilityAction() + true + } else super.performAccessibilityAction(host, action, args) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt index 083e21fbdfba..37ce44488346 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt @@ -17,7 +17,12 @@ package com.android.systemui.biometrics import android.app.ActivityTaskManager import android.content.Context +import android.content.res.Configuration +import android.graphics.Color import android.graphics.PixelFormat +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.Rect import android.hardware.biometrics.BiometricOverlayConstants import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_KEYGUARD import android.hardware.biometrics.BiometricOverlayConstants.REASON_AUTH_SETTINGS @@ -28,23 +33,27 @@ import android.hardware.fingerprint.FingerprintSensorPropertiesInternal import android.hardware.fingerprint.ISidefpsController import android.os.Handler import android.util.Log +import android.util.RotationUtils import android.view.Display import android.view.DisplayInfo import android.view.Gravity import android.view.LayoutInflater import android.view.Surface import android.view.View +import android.view.View.AccessibilityDelegate import android.view.ViewPropertyAnimator import android.view.WindowManager import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY +import android.view.accessibility.AccessibilityEvent +import androidx.annotation.RawRes import com.airbnb.lottie.LottieAnimationView +import com.airbnb.lottie.LottieProperty +import com.airbnb.lottie.model.KeyPath import com.android.internal.annotations.VisibleForTesting import com.android.systemui.Dumpable import com.android.systemui.R import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor -import com.android.systemui.biometrics.ui.binder.SideFpsOverlayViewBinder -import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -55,7 +64,6 @@ import com.android.systemui.util.concurrency.DelayableExecutor import com.android.systemui.util.traceSection import java.io.PrintWriter import javax.inject.Inject -import javax.inject.Provider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -78,7 +86,6 @@ constructor( @Main private val mainExecutor: DelayableExecutor, @Main private val handler: Handler, private val alternateBouncerInteractor: AlternateBouncerInteractor, - private val sideFpsOverlayViewModelFactory: Provider<SideFpsOverlayViewModel>, @Application private val scope: CoroutineScope, dumpManager: DumpManager ) : Dumpable { @@ -243,15 +250,105 @@ constructor( private fun createOverlayForDisplay(@BiometricOverlayConstants.ShowReason reason: Int) { val view = layoutInflater.inflate(R.layout.sidefps_view, null, false) overlayView = view - SideFpsOverlayViewBinder.bind( - view = view, - viewModel = sideFpsOverlayViewModelFactory.get(), - overlayViewParams = overlayViewParams, - reason = reason, - context = context, + val display = context.display!! + // b/284098873 `context.display.rotation` may not up-to-date, we use displayInfo.rotation + display.getDisplayInfo(displayInfo) + val offsets = + sensorProps.getLocation(display.uniqueId).let { location -> + if (location == null) { + Log.w(TAG, "No location specified for display: ${display.uniqueId}") + } + location ?: sensorProps.location + } + overlayOffsets = offsets + + val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView + view.rotation = + display.asSideFpsAnimationRotation( + offsets.isYAligned(), + getRotationFromDefault(displayInfo.rotation) + ) + lottie.setAnimation( + display.asSideFpsAnimation( + offsets.isYAligned(), + getRotationFromDefault(displayInfo.rotation) + ) ) + lottie.addLottieOnCompositionLoadedListener { + // Check that view is not stale, and that overlayView has not been hidden/removed + if (overlayView != null && overlayView == view) { + updateOverlayParams(display, it.bounds) + } + } orientationReasonListener.reason = reason + lottie.addOverlayDynamicColor(context, reason) + + /** + * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback from + * speaking @string/accessibility_fingerprint_label twice when sensor location indicator is + * in focus + */ + view.setAccessibilityDelegate( + object : AccessibilityDelegate() { + override fun dispatchPopulateAccessibilityEvent( + host: View, + event: AccessibilityEvent + ): Boolean { + return if ( + event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED + ) { + true + } else { + super.dispatchPopulateAccessibilityEvent(host, event) + } + } + } + ) } + + @VisibleForTesting + fun updateOverlayParams(display: Display, bounds: Rect) { + val isNaturalOrientation = display.isNaturalOrientation() + val isDefaultOrientation = + if (isReverseDefaultRotation) !isNaturalOrientation else isNaturalOrientation + val size = windowManager.maximumWindowMetrics.bounds + + val displayWidth = if (isDefaultOrientation) size.width() else size.height() + val displayHeight = if (isDefaultOrientation) size.height() else size.width() + val boundsWidth = if (isDefaultOrientation) bounds.width() else bounds.height() + val boundsHeight = if (isDefaultOrientation) bounds.height() else bounds.width() + + val sensorBounds = + if (overlayOffsets.isYAligned()) { + Rect( + displayWidth - boundsWidth, + overlayOffsets.sensorLocationY, + displayWidth, + overlayOffsets.sensorLocationY + boundsHeight + ) + } else { + Rect( + overlayOffsets.sensorLocationX, + 0, + overlayOffsets.sensorLocationX + boundsWidth, + boundsHeight + ) + } + + RotationUtils.rotateBounds( + sensorBounds, + Rect(0, 0, displayWidth, displayHeight), + getRotationFromDefault(display.rotation) + ) + + overlayViewParams.x = sensorBounds.left + overlayViewParams.y = sensorBounds.top + + windowManager.updateViewLayout(overlayView, overlayViewParams) + } + + private fun getRotationFromDefault(rotation: Int): Int = + if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation } private val FingerprintManager?.sideFpsSensorProperties: FingerprintSensorPropertiesInternal? @@ -276,12 +373,89 @@ private fun Int.isReasonToAutoShow(activityTaskManager: ActivityTaskManager): Bo private fun ActivityTaskManager.topClass(): String = getTasks(1).firstOrNull()?.topActivity?.className ?: "" +@RawRes +private fun Display.asSideFpsAnimation(yAligned: Boolean, rotationFromDefault: Int): Int = + when (rotationFromDefault) { + Surface.ROTATION_0 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape + Surface.ROTATION_180 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape + else -> if (yAligned) R.raw.sfps_pulse_landscape else R.raw.sfps_pulse + } + +private fun Display.asSideFpsAnimationRotation(yAligned: Boolean, rotationFromDefault: Int): Float = + when (rotationFromDefault) { + Surface.ROTATION_90 -> if (yAligned) 0f else 180f + Surface.ROTATION_180 -> 180f + Surface.ROTATION_270 -> if (yAligned) 180f else 0f + else -> 0f + } + private fun SensorLocationInternal.isYAligned(): Boolean = sensorLocationY != 0 private fun Display.isNaturalOrientation(): Boolean = rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180 -public class OrientationReasonListener( +private fun LottieAnimationView.addOverlayDynamicColor( + context: Context, + @BiometricOverlayConstants.ShowReason reason: Int +) { + fun update() { + val isKeyguard = reason == REASON_AUTH_KEYGUARD + if (isKeyguard) { + val color = + com.android.settingslib.Utils.getColorAttrDefaultColor( + context, + com.android.internal.R.attr.materialColorPrimaryFixed + ) + val outerRimColor = + com.android.settingslib.Utils.getColorAttrDefaultColor( + context, + com.android.internal.R.attr.materialColorPrimaryFixedDim + ) + val chevronFill = + com.android.settingslib.Utils.getColorAttrDefaultColor( + context, + com.android.internal.R.attr.materialColorOnPrimaryFixed + ) + addValueCallback(KeyPath(".blue600", "**"), LottieProperty.COLOR_FILTER) { + PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP) + } + addValueCallback(KeyPath(".blue400", "**"), LottieProperty.COLOR_FILTER) { + PorterDuffColorFilter(outerRimColor, PorterDuff.Mode.SRC_ATOP) + } + addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) { + PorterDuffColorFilter(chevronFill, PorterDuff.Mode.SRC_ATOP) + } + } else { + if (!isDarkMode(context)) { + addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) { + PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP) + } + } + for (key in listOf(".blue600", ".blue400")) { + addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) { + PorterDuffColorFilter( + context.getColor(R.color.settingslib_color_blue400), + PorterDuff.Mode.SRC_ATOP + ) + } + } + } + } + + if (composition != null) { + update() + } else { + addLottieOnCompositionLoadedListener { update() } + } +} + +private fun isDarkMode(context: Context): Boolean { + val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + return darkMode == Configuration.UI_MODE_NIGHT_YES +} + +@VisibleForTesting +class OrientationReasonListener( context: Context, displayManager: DisplayManager, handler: Handler, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt index efbde4c5985b..efc92ad3b4c8 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepository.kt @@ -16,11 +16,8 @@ package com.android.systemui.biometrics.data.repository -import android.hardware.biometrics.ComponentInfoInternal import android.hardware.biometrics.SensorLocationInternal -import android.hardware.biometrics.SensorProperties import android.hardware.fingerprint.FingerprintManager -import android.hardware.fingerprint.FingerprintSensorProperties import android.hardware.fingerprint.FingerprintSensorPropertiesInternal import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback import com.android.systemui.biometrics.shared.model.FingerprintSensorType @@ -33,8 +30,10 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.shareIn /** @@ -44,17 +43,22 @@ import kotlinx.coroutines.flow.shareIn */ interface FingerprintPropertyRepository { + /** + * If the repository is initialized or not. Other properties are defaults until this is true. + */ + val isInitialized: Flow<Boolean> + /** The id of fingerprint sensor. */ - val sensorId: Flow<Int> + val sensorId: StateFlow<Int> /** The security strength of sensor (convenience, weak, strong). */ - val strength: Flow<SensorStrength> + val strength: StateFlow<SensorStrength> /** The types of fingerprint sensor (rear, ultrasonic, optical, etc.). */ - val sensorType: Flow<FingerprintSensorType> + val sensorType: StateFlow<FingerprintSensorType> /** The sensor location relative to each physical display. */ - val sensorLocations: Flow<Map<String, SensorLocationInternal>> + val sensorLocations: StateFlow<Map<String, SensorLocationInternal>> } @SysUISingleton @@ -62,10 +66,10 @@ class FingerprintPropertyRepositoryImpl @Inject constructor( @Application private val applicationScope: CoroutineScope, - private val fingerprintManager: FingerprintManager? + private val fingerprintManager: FingerprintManager?, ) : FingerprintPropertyRepository { - private val props: Flow<FingerprintSensorPropertiesInternal> = + override val isInitialized: Flow<Boolean> = conflatedCallbackFlow { val callback = object : IFingerprintAuthenticatorsRegisteredCallback.Stub() { @@ -73,47 +77,45 @@ constructor( sensors: List<FingerprintSensorPropertiesInternal> ) { if (sensors.isNotEmpty()) { - trySendWithFailureLogging(sensors[0], TAG, "initialize properties") - } else { - trySendWithFailureLogging( - DEFAULT_PROPS, - TAG, - "initialize with default properties" - ) + setProperties(sensors[0]) + trySendWithFailureLogging(true, TAG, "initialize properties") } } } fingerprintManager?.addAuthenticatorsRegisteredCallback(callback) - trySendWithFailureLogging(DEFAULT_PROPS, TAG, "initialize with default properties") + trySendWithFailureLogging(false, TAG, "initial value defaulting to false") awaitClose {} } .shareIn(scope = applicationScope, started = SharingStarted.Eagerly, replay = 1) - override val sensorId: Flow<Int> = props.map { it.sensorId } - override val strength: Flow<SensorStrength> = - props.map { sensorStrengthIntToObject(it.sensorStrength) } - override val sensorType: Flow<FingerprintSensorType> = - props.map { sensorTypeIntToObject(it.sensorType) } - override val sensorLocations: Flow<Map<String, SensorLocationInternal>> = - props.map { - it.allLocations.associateBy { sensorLocationInternal -> + private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1) + override val sensorId: StateFlow<Int> = _sensorId.asStateFlow() + + private val _strength: MutableStateFlow<SensorStrength> = + MutableStateFlow(SensorStrength.CONVENIENCE) + override val strength = _strength.asStateFlow() + + private val _sensorType: MutableStateFlow<FingerprintSensorType> = + MutableStateFlow(FingerprintSensorType.UNKNOWN) + override val sensorType = _sensorType.asStateFlow() + + private val _sensorLocations: MutableStateFlow<Map<String, SensorLocationInternal>> = + MutableStateFlow(mapOf("" to SensorLocationInternal.DEFAULT)) + override val sensorLocations: StateFlow<Map<String, SensorLocationInternal>> = + _sensorLocations.asStateFlow() + + private fun setProperties(prop: FingerprintSensorPropertiesInternal) { + _sensorId.value = prop.sensorId + _strength.value = sensorStrengthIntToObject(prop.sensorStrength) + _sensorType.value = sensorTypeIntToObject(prop.sensorType) + _sensorLocations.value = + prop.allLocations.associateBy { sensorLocationInternal -> sensorLocationInternal.displayId } - } + } companion object { private const val TAG = "FingerprintPropertyRepositoryImpl" - private val DEFAULT_PROPS = - FingerprintSensorPropertiesInternal( - -1 /* sensorId */, - SensorProperties.STRENGTH_CONVENIENCE, - 0 /* maxEnrollmentsPerUser */, - listOf<ComponentInfoInternal>(), - FingerprintSensorProperties.TYPE_UNKNOWN, - false /* halControlsIllumination */, - true /* resetLockoutRequiresHardwareAuthToken */, - listOf<SensorLocationInternal>(SensorLocationInternal.DEFAULT) - ) } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt index 37f39cb5fe0e..aa85e5f3b21a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractor.kt @@ -17,24 +17,16 @@ package com.android.systemui.biometrics.domain.interactor import android.hardware.biometrics.SensorLocationInternal +import android.util.Log import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository import com.android.systemui.dagger.SysUISingleton import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine /** Business logic for SideFps overlay offsets. */ interface SideFpsOverlayInteractor { - /** The displayId of the current display. */ - val displayId: Flow<String> - /** The corresponding offsets based on different displayId. */ - val overlayOffsets: Flow<SensorLocationInternal> - - /** Update the displayId. */ - fun changeDisplay(displayId: String?) + /** Get the corresponding offsets based on different displayId. */ + fun getOverlayOffsets(displayId: String): SensorLocationInternal } @SysUISingleton @@ -43,16 +35,14 @@ class SideFpsOverlayInteractorImpl constructor(private val fingerprintPropertyRepository: FingerprintPropertyRepository) : SideFpsOverlayInteractor { - private val _displayId: MutableStateFlow<String> = MutableStateFlow("") - override val displayId: Flow<String> = _displayId.asStateFlow() - - override val overlayOffsets: Flow<SensorLocationInternal> = - combine(displayId, fingerprintPropertyRepository.sensorLocations) { displayId, offsets -> - offsets[displayId] ?: SensorLocationInternal.DEFAULT + override fun getOverlayOffsets(displayId: String): SensorLocationInternal { + val offsets = fingerprintPropertyRepository.sensorLocations.value + return if (offsets.containsKey(displayId)) { + offsets[displayId]!! + } else { + Log.w(TAG, "No location specified for display: $displayId") + offsets[""]!! } - - override fun changeDisplay(displayId: String?) { - _displayId.value = displayId ?: "" } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt deleted file mode 100644 index 0409519c9816..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt +++ /dev/null @@ -1,168 +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.biometrics.ui.binder - -import android.content.Context -import android.content.res.Configuration -import android.graphics.Color -import android.graphics.PorterDuff -import android.graphics.PorterDuffColorFilter -import android.hardware.biometrics.BiometricOverlayConstants -import android.view.View -import android.view.WindowManager -import android.view.accessibility.AccessibilityEvent -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle -import com.airbnb.lottie.LottieAnimationView -import com.airbnb.lottie.LottieProperty -import com.airbnb.lottie.model.KeyPath -import com.android.systemui.R -import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.lifecycle.repeatWhenAttached -import kotlinx.coroutines.launch - -/** Sub-binder for SideFpsOverlayView. */ -object SideFpsOverlayViewBinder { - - /** Bind the view. */ - @JvmStatic - fun bind( - view: View, - viewModel: SideFpsOverlayViewModel, - overlayViewParams: WindowManager.LayoutParams, - @BiometricOverlayConstants.ShowReason reason: Int, - @Application context: Context - ) { - val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager - - val lottie = view.findViewById(R.id.sidefps_animation) as LottieAnimationView - - viewModel.changeDisplay() - - view.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.STARTED) { - launch { - viewModel.sideFpsAnimationRotation.collect { rotation -> - view.rotation = rotation - } - } - - launch { - // TODO(b/221037350, wenhuiy): Create a separate ViewBinder for sideFpsAnimation - // in order to add scuba tests in the future. - viewModel.sideFpsAnimation.collect { animation -> - lottie.setAnimation(animation) - } - } - - launch { - viewModel.sensorBounds.collect { sensorBounds -> - overlayViewParams.x = sensorBounds.left - overlayViewParams.y = sensorBounds.top - - windowManager.updateViewLayout(view, overlayViewParams) - } - } - - launch { - viewModel.overlayOffsets.collect { overlayOffsets -> - lottie.addLottieOnCompositionLoadedListener { - viewModel.updateSensorBounds( - it.bounds, - windowManager.maximumWindowMetrics.bounds, - overlayOffsets - ) - } - } - } - } - } - - lottie.addOverlayDynamicColor(context, reason) - - /** - * Intercepts TYPE_WINDOW_STATE_CHANGED accessibility event, preventing Talkback from - * speaking @string/accessibility_fingerprint_label twice when sensor location indicator is - * in focus - */ - view.accessibilityDelegate = - object : View.AccessibilityDelegate() { - override fun dispatchPopulateAccessibilityEvent( - host: View, - event: AccessibilityEvent - ): Boolean { - return if ( - event.getEventType() === AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED - ) { - true - } else { - super.dispatchPopulateAccessibilityEvent(host, event) - } - } - } - } -} - -private fun LottieAnimationView.addOverlayDynamicColor( - context: Context, - @BiometricOverlayConstants.ShowReason reason: Int -) { - fun update() { - val isKeyguard = reason == BiometricOverlayConstants.REASON_AUTH_KEYGUARD - if (isKeyguard) { - val color = context.getColor(R.color.numpad_key_color_secondary) // match bouncer color - val chevronFill = - com.android.settingslib.Utils.getColorAttrDefaultColor( - context, - android.R.attr.textColorPrimaryInverse - ) - for (key in listOf(".blue600", ".blue400")) { - addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) { - PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP) - } - } - addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) { - PorterDuffColorFilter(chevronFill, PorterDuff.Mode.SRC_ATOP) - } - } else if (!isDarkMode(context)) { - addValueCallback(KeyPath(".black", "**"), LottieProperty.COLOR_FILTER) { - PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_ATOP) - } - } else if (isDarkMode(context)) { - for (key in listOf(".blue600", ".blue400")) { - addValueCallback(KeyPath(key, "**"), LottieProperty.COLOR_FILTER) { - PorterDuffColorFilter( - context.getColor(R.color.settingslib_color_blue400), - PorterDuff.Mode.SRC_ATOP - ) - } - } - } - } - - if (composition != null) { - update() - } else { - addLottieOnCompositionLoadedListener { update() } - } -} - -private fun isDarkMode(context: Context): Boolean { - val darkMode = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK - return darkMode == Configuration.UI_MODE_NIGHT_YES -} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt deleted file mode 100644 index e938b4efb68c..000000000000 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt +++ /dev/null @@ -1,148 +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.biometrics.ui.viewmodel - -import android.content.Context -import android.graphics.Rect -import android.hardware.biometrics.SensorLocationInternal -import android.util.RotationUtils -import android.view.Display -import android.view.DisplayInfo -import android.view.Surface -import androidx.annotation.RawRes -import com.android.systemui.R -import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractor -import com.android.systemui.dagger.qualifiers.Application -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map - -/** View-model for SideFpsOverlayView. */ -class SideFpsOverlayViewModel -@Inject -constructor( - @Application private val context: Context, - private val sideFpsOverlayInteractor: SideFpsOverlayInteractor, -) { - - private val isReverseDefaultRotation = - context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation) - - private val _sensorBounds: MutableStateFlow<Rect> = MutableStateFlow(Rect()) - val sensorBounds = _sensorBounds.asStateFlow() - - val overlayOffsets: Flow<SensorLocationInternal> = sideFpsOverlayInteractor.overlayOffsets - - /** Update the displayId. */ - fun changeDisplay() { - sideFpsOverlayInteractor.changeDisplay(context.display!!.uniqueId) - } - - /** Determine the rotation of the sideFps animation given the overlay offsets. */ - val sideFpsAnimationRotation: Flow<Float> = - overlayOffsets.map { overlayOffsets -> - val display = context.display!! - val displayInfo: DisplayInfo = DisplayInfo() - // b/284098873 `context.display.rotation` may not up-to-date, we use - // displayInfo.rotation - display.getDisplayInfo(displayInfo) - val yAligned: Boolean = overlayOffsets.isYAligned() - when (getRotationFromDefault(displayInfo.rotation)) { - Surface.ROTATION_90 -> if (yAligned) 0f else 180f - Surface.ROTATION_180 -> 180f - Surface.ROTATION_270 -> if (yAligned) 180f else 0f - else -> 0f - } - } - - /** Populate the sideFps animation from the overlay offsets. */ - @RawRes - val sideFpsAnimation: Flow<Int> = - overlayOffsets.map { overlayOffsets -> - val display = context.display!! - val displayInfo: DisplayInfo = DisplayInfo() - // b/284098873 `context.display.rotation` may not up-to-date, we use - // displayInfo.rotation - display.getDisplayInfo(displayInfo) - val yAligned: Boolean = overlayOffsets.isYAligned() - when (getRotationFromDefault(displayInfo.rotation)) { - Surface.ROTATION_0 -> if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape - Surface.ROTATION_180 -> - if (yAligned) R.raw.sfps_pulse else R.raw.sfps_pulse_landscape - else -> if (yAligned) R.raw.sfps_pulse_landscape else R.raw.sfps_pulse - } - } - - /** - * Calculate and update the bounds of the sensor based on the bounds of the overlay view, the - * maximum bounds of the window, and the offsets of the sensor location. - */ - fun updateSensorBounds( - bounds: Rect, - maximumWindowBounds: Rect, - offsets: SensorLocationInternal - ) { - val isNaturalOrientation = context.display!!.isNaturalOrientation() - val isDefaultOrientation = - if (isReverseDefaultRotation) !isNaturalOrientation else isNaturalOrientation - - val displayWidth = - if (isDefaultOrientation) maximumWindowBounds.width() else maximumWindowBounds.height() - val displayHeight = - if (isDefaultOrientation) maximumWindowBounds.height() else maximumWindowBounds.width() - val boundsWidth = if (isDefaultOrientation) bounds.width() else bounds.height() - val boundsHeight = if (isDefaultOrientation) bounds.height() else bounds.width() - - val sensorBounds = - if (offsets.isYAligned()) { - Rect( - displayWidth - boundsWidth, - offsets.sensorLocationY, - displayWidth, - offsets.sensorLocationY + boundsHeight - ) - } else { - Rect( - offsets.sensorLocationX, - 0, - offsets.sensorLocationX + boundsWidth, - boundsHeight - ) - } - - val displayInfo: DisplayInfo = DisplayInfo() - context.display!!.getDisplayInfo(displayInfo) - - RotationUtils.rotateBounds( - sensorBounds, - Rect(0, 0, displayWidth, displayHeight), - getRotationFromDefault(displayInfo.rotation) - ) - - _sensorBounds.value = sensorBounds - } - - private fun getRotationFromDefault(rotation: Int): Int = - if (isReverseDefaultRotation) (rotation + 1) % 4 else rotation -} - -private fun Display.isNaturalOrientation(): Boolean = - rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180 - -private fun SensorLocationInternal.isYAligned(): Boolean = sensorLocationY != 0 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 1b14acc7fabc..844cf024ef71 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 @@ -40,9 +40,10 @@ class PinBouncerViewModel( ) { val pinShapes = PinShapeAdapter(applicationContext) + private val mutablePinInput = MutableStateFlow(PinInputViewModel.empty()) - private val mutablePinEntries = MutableStateFlow<List<EnteredKey>>(emptyList()) - val pinEntries: StateFlow<List<EnteredKey>> = mutablePinEntries + /** Currently entered pin keys. */ + val pinInput: StateFlow<PinInputViewModel> = mutablePinInput /** The length of the PIN for which we should show a hint. */ val hintedPinLength: StateFlow<Int?> = interactor.hintedPinLength @@ -50,17 +51,19 @@ class PinBouncerViewModel( /** Appearance of the backspace button. */ val backspaceButtonAppearance: StateFlow<ActionButtonAppearance> = combine( - mutablePinEntries, + mutablePinInput, interactor.isAutoConfirmEnabled, ) { mutablePinEntries, isAutoConfirmEnabled -> computeBackspaceButtonAppearance( - enteredPin = mutablePinEntries, + pinInput = mutablePinEntries, isAutoConfirmEnabled = isAutoConfirmEnabled, ) } .stateIn( scope = applicationScope, - started = SharingStarted.Eagerly, + // Make sure this is kept as WhileSubscribed or we can run into a bug where the + // downstream continues to receive old/stale/cached values. + started = SharingStarted.WhileSubscribed(), initialValue = ActionButtonAppearance.Hidden, ) @@ -87,26 +90,23 @@ class PinBouncerViewModel( /** Notifies that the user clicked on a PIN button with the given digit value. */ fun onPinButtonClicked(input: Int) { - if (mutablePinEntries.value.isEmpty()) { + val pinInput = mutablePinInput.value + if (pinInput.isEmpty()) { interactor.clearMessage() } - mutablePinEntries.value += EnteredKey(input) - + mutablePinInput.value = pinInput.append(input) tryAuthenticate(useAutoConfirm = true) } /** Notifies that the user clicked the backspace button. */ fun onBackspaceButtonClicked() { - if (mutablePinEntries.value.isEmpty()) { - return - } - mutablePinEntries.value = mutablePinEntries.value.toMutableList().apply { removeLast() } + mutablePinInput.value = mutablePinInput.value.deleteLast() } /** Notifies that the user long-pressed the backspace button. */ fun onBackspaceButtonLongPressed() { - mutablePinEntries.value = emptyList() + mutablePinInput.value = mutablePinInput.value.clearAll() } /** Notifies that the user clicked the "enter" button. */ @@ -115,7 +115,7 @@ class PinBouncerViewModel( } private fun tryAuthenticate(useAutoConfirm: Boolean) { - val pinCode = mutablePinEntries.value.map { it.input } + val pinCode = mutablePinInput.value.getPin() applicationScope.launch { val isSuccess = interactor.authenticate(pinCode, useAutoConfirm) ?: return@launch @@ -124,15 +124,17 @@ class PinBouncerViewModel( showFailureAnimation() } - mutablePinEntries.value = emptyList() + // TODO(b/291528545): this should not be cleared on success (at least until the view + // is animated away). + mutablePinInput.value = mutablePinInput.value.clearAll() } } private fun computeBackspaceButtonAppearance( - enteredPin: List<EnteredKey>, + pinInput: PinInputViewModel, isAutoConfirmEnabled: Boolean, ): ActionButtonAppearance { - val isEmpty = enteredPin.isEmpty() + val isEmpty = pinInput.isEmpty() return when { isAutoConfirmEnabled && isEmpty -> ActionButtonAppearance.Hidden @@ -151,19 +153,3 @@ enum class ActionButtonAppearance { /** Button is shown. */ Shown, } - -private var nextSequenceNumber = 1 - -/** - * The pin bouncer [input] as digits 0-9, together with a [sequenceNumber] to indicate the ordering. - * - * Since the model only allows appending/removing [EnteredKey]s from the end, the [sequenceNumber] - * is strictly increasing in input order of the pin, but not guaranteed to be monotonic or start at - * a specific number. - */ -data class EnteredKey -internal constructor(val input: Int, val sequenceNumber: Int = nextSequenceNumber++) : - Comparable<EnteredKey> { - override fun compareTo(other: EnteredKey): Int = - compareValuesBy(this, other, EnteredKey::sequenceNumber) -} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModel.kt new file mode 100644 index 000000000000..4efc21b41e6a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModel.kt @@ -0,0 +1,177 @@ +/* + * 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.bouncer.ui.viewmodel + +import androidx.annotation.VisibleForTesting +import com.android.systemui.bouncer.ui.viewmodel.EntryToken.ClearAll +import com.android.systemui.bouncer.ui.viewmodel.EntryToken.Digit + +/** + * Immutable pin input state. + * + * The input is a hybrid of state ([Digit]) and event ([ClearAll]) tokens. The [ClearAll] token can + * be interpreted as a watermark, indicating that the current input up to that point is deleted + * (after a auth failure or when long-pressing the delete button). Therefore, [Digit]s following a + * [ClearAll] make up the next pin input entry. Up to two complete pin inputs are memoized. + * + * This is required when auto-confirm rejects the input, and the last digit will be animated-in at + * the end of the input, concurrently with the staggered clear-all animation starting to play at the + * beginning of the input. + * + * The input is guaranteed to always contain a initial [ClearAll] token as a sentinel, thus clients + * can always assume there is a 'ClearAll' watermark available. + */ +data class PinInputViewModel +internal constructor( + @get:VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal val input: List<EntryToken> +) { + init { + require(input.firstOrNull() is ClearAll) { "input does not begin with a ClearAll token" } + require(input.zipWithNext().all { it.first < it.second }) { + "EntryTokens are not sorted by their sequenceNumber" + } + } + /** + * [PinInputViewModel] with [previousInput] and appended [newToken]. + * + * [previousInput] is trimmed so that the new [PinBouncerViewModel] contains at most two pin + * inputs. + */ + private constructor( + previousInput: List<EntryToken>, + newToken: EntryToken + ) : this( + buildList { + addAll( + previousInput.subList(previousInput.indexOfLastClearAllToKeep(), previousInput.size) + ) + add(newToken) + } + ) + + fun append(digit: Int): PinInputViewModel { + return PinInputViewModel(input, Digit(digit)) + } + + /** + * Delete last digit. + * + * This removes the last digit from the input. Returns `this` if the last token is [ClearAll]. + */ + fun deleteLast(): PinInputViewModel { + if (isEmpty()) return this + return PinInputViewModel(input.take(input.size - 1)) + } + + /** + * Appends a [ClearAll] watermark, completing the current pin. + * + * Returns `this` if the last token is [ClearAll]. + */ + fun clearAll(): PinInputViewModel { + if (isEmpty()) return this + return PinInputViewModel(input, ClearAll()) + } + + /** Whether the current pin is empty. */ + fun isEmpty(): Boolean { + return input.last() is ClearAll + } + + /** The current pin, or an empty list if [isEmpty]. */ + fun getPin(): List<Int> { + return getDigits(mostRecentClearAll()).map { it.input } + } + + /** + * The digits following the specified [ClearAll] marker, up to the next marker or the end of the + * input. + * + * Returns an empty list if the [ClearAll] is not in the input. + */ + fun getDigits(clearAllMarker: ClearAll): List<Digit> { + val startIndex = input.indexOf(clearAllMarker) + 1 + if (startIndex == 0 || startIndex == input.size) return emptyList() + + return input.subList(startIndex, input.size).takeWhile { it is Digit }.map { it as Digit } + } + + /** The most recent [ClearAll] marker. */ + fun mostRecentClearAll(): ClearAll { + return input.last { it is ClearAll } as ClearAll + } + + companion object { + fun empty() = PinInputViewModel(listOf(ClearAll())) + } +} + +/** + * Pin bouncer entry token with a [sequenceNumber] to indicate input event ordering. + * + * Since the model only allows appending/removing [Digit]s from the end, the [sequenceNumber] is + * strictly increasing in input order of the pin, but not guaranteed to be monotonic or start at a + * specific number. + */ +sealed interface EntryToken : Comparable<EntryToken> { + val sequenceNumber: Int + + /** The pin bouncer [input] as digits 0-9. */ + data class Digit + internal constructor(val input: Int, override val sequenceNumber: Int = nextSequenceNumber++) : + EntryToken { + init { + check(input in 0..9) + } + } + + /** + * Marker to indicate the input is completely cleared, and subsequent [EntryToken]s mark a new + * pin entry. + */ + data class ClearAll + internal constructor(override val sequenceNumber: Int = nextSequenceNumber++) : EntryToken + + override fun compareTo(other: EntryToken): Int = + compareValuesBy(this, other, EntryToken::sequenceNumber) + + companion object { + private var nextSequenceNumber = 1 + } +} + +/** + * Index of the last [ClearAll] token to keep for a new [PinInputViewModel], so that after appending + * another [EntryToken], there are at most two pin inputs in the [PinInputViewModel]. + */ +private fun List<EntryToken>.indexOfLastClearAllToKeep(): Int { + require(isNotEmpty() && first() is ClearAll) + + var seenClearAll = 0 + for (i in size - 1 downTo 0) { + if (get(i) is ClearAll) { + seenClearAll++ + if (seenClearAll == 2) { + return i + } + } + } + + // The first element is guaranteed to be a ClearAll marker. + return 0 +} diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java index e342ac2f320d..566a74ae3e07 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/IntentCreator.java @@ -17,6 +17,7 @@ package com.android.systemui.clipboardoverlay; import android.content.ClipData; +import android.content.ClipDescription; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -41,10 +42,16 @@ class IntentCreator { // From the ACTION_SEND docs: // "If using EXTRA_TEXT, the MIME type should be "text/plain"; otherwise it should be the // MIME type of the data in EXTRA_STREAM" - if (clipData.getItemAt(0).getUri() != null) { - shareIntent.setDataAndType( - clipData.getItemAt(0).getUri(), clipData.getDescription().getMimeType(0)); - shareIntent.putExtra(Intent.EXTRA_STREAM, clipData.getItemAt(0).getUri()); + Uri uri = clipData.getItemAt(0).getUri(); + if (uri != null) { + // We don't use setData here because some apps interpret this as "to:". + shareIntent.setType(clipData.getDescription().getMimeType(0)); + // Include URI in ClipData also, so that grantPermission picks it up. + shareIntent.setClipData(new ClipData( + new ClipDescription( + "content", new String[]{clipData.getDescription().getMimeType(0)}), + new ClipData.Item(uri))); + shareIntent.putExtra(Intent.EXTRA_STREAM, uri); shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } else { shareIntent.putExtra( diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt index b15c60e62ead..85f31e5e6b5a 100644 --- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt +++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt @@ -21,13 +21,11 @@ import android.content.Context import android.view.View import androidx.activity.ComponentActivity import androidx.lifecycle.LifecycleOwner -import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel -import com.android.systemui.util.time.SystemClock /** * A facade to interact with Compose, when it is available. @@ -64,13 +62,6 @@ interface BaseComposeFacade { ): View /** Create a [View] to represent [viewModel] on screen. */ - fun createMultiShadeView( - context: Context, - viewModel: MultiShadeViewModel, - clock: SystemClock, - ): View - - /** Create a [View] to represent [viewModel] on screen. */ fun createSceneContainerView( context: Context, viewModel: SceneContainerViewModel, diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index a560accfff68..d9665c5b5047 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -48,6 +48,7 @@ import com.android.systemui.reardisplay.RearDisplayDialogController import com.android.systemui.recents.Recents import com.android.systemui.settings.dagger.MultiUserUtilsModule import com.android.systemui.shortcut.ShortcutKeyDispatcher +import com.android.systemui.statusbar.ImmersiveModeConfirmation import com.android.systemui.statusbar.notification.InstantAppNotifier import com.android.systemui.statusbar.phone.KeyguardLiftController import com.android.systemui.statusbar.phone.LockscreenWallpaper @@ -162,6 +163,12 @@ abstract class SystemUICoreStartableModule { @ClassKey(Recents::class) abstract fun bindRecents(sysui: Recents): CoreStartable + /** Inject into ImmersiveModeConfirmation. */ + @Binds + @IntoMap + @ClassKey(ImmersiveModeConfirmation::class) + abstract fun bindImmersiveModeConfirmation(sysui: ImmersiveModeConfirmation): CoreStartable + /** Inject into RingtonePlayer. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java index 99451f2ee1b9..6f05e83b22ba 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java @@ -37,12 +37,15 @@ import javax.inject.Named; */ public class ShadeTouchHandler implements DreamTouchHandler { private final Optional<CentralSurfaces> mSurfaces; + private final ShadeViewController mShadeViewController; private final int mInitiationHeight; @Inject ShadeTouchHandler(Optional<CentralSurfaces> centralSurfaces, + ShadeViewController shadeViewController, @Named(NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT) int initiationHeight) { mSurfaces = centralSurfaces; + mShadeViewController = shadeViewController; mInitiationHeight = initiationHeight; } @@ -54,12 +57,7 @@ public class ShadeTouchHandler implements DreamTouchHandler { } session.registerInputListener(ev -> { - final ShadeViewController viewController = - mSurfaces.map(CentralSurfaces::getShadeViewController).orElse(null); - - if (viewController != null) { - viewController.handleExternalTouch((MotionEvent) ev); - } + mShadeViewController.handleExternalTouch((MotionEvent) ev); if (ev instanceof MotionEvent) { if (((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) { diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index efa5981ef7b2..c11e48574497 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -77,7 +77,7 @@ object Flags { // TODO(b/278873737): Tracking Bug @JvmField val LOAD_NOTIFICATIONS_BEFORE_THE_USER_SWITCH_IS_COMPLETE = - releasedFlag(278873737, "load_notifications_before_the_user_switch_is_complete") + releasedFlag(278873737, "load_notifications_before_the_user_switch_is_complete") // TODO(b/277338665): Tracking Bug @JvmField @@ -92,11 +92,7 @@ object Flags { // TODO(b/288326013): Tracking Bug @JvmField val NOTIFICATION_ASYNC_HYBRID_VIEW_INFLATION = - unreleasedFlag( - 288326013, - "notification_async_hybrid_view_inflation", - teamfood = false - ) + unreleasedFlag(288326013, "notification_async_hybrid_view_inflation", teamfood = false) @JvmField val ANIMATED_NOTIFICATION_SHADE_INSETS = @@ -104,18 +100,17 @@ object Flags { // TODO(b/268005230): Tracking Bug @JvmField - val SENSITIVE_REVEAL_ANIM = - unreleasedFlag(268005230, "sensitive_reveal_anim", teamfood = true) + val SENSITIVE_REVEAL_ANIM = unreleasedFlag(268005230, "sensitive_reveal_anim", teamfood = true) // TODO(b/280783617): Tracking Bug @Keep @JvmField val BUILDER_EXTRAS_OVERRIDE = - sysPropBooleanFlag( - 128, - "persist.sysui.notification.builder_extras_override", - default = false - ) + sysPropBooleanFlag( + 128, + "persist.sysui.notification.builder_extras_override", + default = true + ) // 200 - keyguard/lockscreen // ** Flag retired ** @@ -133,16 +128,17 @@ object Flags { // TODO(b/254512676): Tracking Bug @JvmField - val LOCKSCREEN_CUSTOM_CLOCKS = resourceBooleanFlag( - 207, - R.bool.config_enableLockScreenCustomClocks, - "lockscreen_custom_clocks" - ) + val LOCKSCREEN_CUSTOM_CLOCKS = + resourceBooleanFlag( + 207, + R.bool.config_enableLockScreenCustomClocks, + "lockscreen_custom_clocks" + ) // TODO(b/275694445): Tracking Bug @JvmField - val LOCKSCREEN_WITHOUT_SECURE_LOCK_WHEN_DREAMING = releasedFlag(208, - "lockscreen_without_secure_lock_when_dreaming") + val LOCKSCREEN_WITHOUT_SECURE_LOCK_WHEN_DREAMING = + releasedFlag(208, "lockscreen_without_secure_lock_when_dreaming") // TODO(b/286092087): Tracking Bug @JvmField @@ -156,8 +152,7 @@ object Flags { * Whether the clock on a wide lock screen should use the new "stepping" animation for moving * the digits when the clock moves. */ - @JvmField - val STEP_CLOCK_ANIMATION = releasedFlag(212, "step_clock_animation") + @JvmField val STEP_CLOCK_ANIMATION = releasedFlag(212, "step_clock_animation") /** * Migration from the legacy isDozing/dozeAmount paths to the new KeyguardTransitionRepository @@ -183,13 +178,11 @@ object Flags { @JvmField val BIOMETRICS_ANIMATION_REVAMP = unreleasedFlag(221, "biometrics_animation_revamp") // TODO(b/262780002): Tracking Bug - @JvmField - val REVAMPED_WALLPAPER_UI = releasedFlag(222, "revamped_wallpaper_ui") + @JvmField val REVAMPED_WALLPAPER_UI = releasedFlag(222, "revamped_wallpaper_ui") // flag for controlling auto pin confirmation and material u shapes in bouncer @JvmField - val AUTO_PIN_CONFIRMATION = - releasedFlag(224, "auto_pin_confirmation", "auto_pin_confirmation") + val AUTO_PIN_CONFIRMATION = releasedFlag(224, "auto_pin_confirmation", "auto_pin_confirmation") // TODO(b/262859270): Tracking Bug @JvmField val FALSING_OFF_FOR_UNFOLDED = releasedFlag(225, "falsing_off_for_unfolded") @@ -206,20 +199,11 @@ object Flags { /** Whether the long-press gesture to open wallpaper picker is enabled. */ // TODO(b/266242192): Tracking Bug @JvmField - val LOCK_SCREEN_LONG_PRESS_ENABLED = - releasedFlag( - 228, - "lock_screen_long_press_enabled" - ) + val LOCK_SCREEN_LONG_PRESS_ENABLED = releasedFlag(228, "lock_screen_long_press_enabled") /** Enables UI updates for AI wallpapers in the wallpaper picker. */ // TODO(b/267722622): Tracking Bug - @JvmField - val WALLPAPER_PICKER_UI_FOR_AIWP = - releasedFlag( - 229, - "wallpaper_picker_ui_for_aiwp" - ) + @JvmField val WALLPAPER_PICKER_UI_FOR_AIWP = releasedFlag(229, "wallpaper_picker_ui_for_aiwp") /** Whether to use a new data source for intents to run on keyguard dismissal. */ // TODO(b/275069969): Tracking bug. @@ -232,6 +216,12 @@ object Flags { val LOCK_SCREEN_LONG_PRESS_DIRECT_TO_WPP = unreleasedFlag(232, "lock_screen_long_press_directly_opens_wallpaper_picker") + /** Whether page transition animations in the wallpaper picker are enabled */ + // TODO(b/291710220): Tracking bug. + @JvmField + val WALLPAPER_PICKER_PAGE_TRANSITIONS = + unreleasedFlag(291710220, "wallpaper_picker_page_transitions") + /** Whether to run the new udfps keyguard refactor code. */ // TODO(b/279440316): Tracking bug. @JvmField @@ -239,27 +229,21 @@ object Flags { /** Provide new auth messages on the bouncer. */ // TODO(b/277961132): Tracking bug. - @JvmField - val REVAMPED_BOUNCER_MESSAGES = - unreleasedFlag(234, "revamped_bouncer_messages") + @JvmField val REVAMPED_BOUNCER_MESSAGES = unreleasedFlag(234, "revamped_bouncer_messages") /** Whether to delay showing bouncer UI when face auth or active unlock are enrolled. */ // TODO(b/279794160): Tracking bug. - @JvmField - val DELAY_BOUNCER = unreleasedFlag(235, "delay_bouncer", teamfood = true) - + @JvmField val DELAY_BOUNCER = unreleasedFlag(235, "delay_bouncer", teamfood = true) /** Keyguard Migration */ /** Migrate the indication area to the new keyguard root view. */ // TODO(b/280067944): Tracking bug. - @JvmField - val MIGRATE_INDICATION_AREA = releasedFlag(236, "migrate_indication_area") + @JvmField val MIGRATE_INDICATION_AREA = releasedFlag(236, "migrate_indication_area") /** - * Migrate the bottom area to the new keyguard root view. - * Because there is no such thing as a "bottom area" after this, this also breaks it up into - * many smaller, modular pieces. + * Migrate the bottom area to the new keyguard root view. Because there is no such thing as a + * "bottom area" after this, this also breaks it up into many smaller, modular pieces. */ // TODO(b/290652751): Tracking bug. @JvmField @@ -268,36 +252,29 @@ object Flags { /** Whether to listen for fingerprint authentication over keyguard occluding activities. */ // TODO(b/283260512): Tracking bug. - @JvmField - val FP_LISTEN_OCCLUDING_APPS = unreleasedFlag(237, "fp_listen_occluding_apps") + @JvmField val FP_LISTEN_OCCLUDING_APPS = unreleasedFlag(237, "fp_listen_occluding_apps") /** Flag meant to guard the talkback fix for the KeyguardIndicationTextView */ // TODO(b/286563884): Tracking bug - @JvmField - val KEYGUARD_TALKBACK_FIX = releasedFlag(238, "keyguard_talkback_fix") + @JvmField val KEYGUARD_TALKBACK_FIX = releasedFlag(238, "keyguard_talkback_fix") // TODO(b/287268101): Tracking bug. - @JvmField - val TRANSIT_CLOCK = unreleasedFlag(239, "lockscreen_custom_transit_clock") + @JvmField val TRANSIT_CLOCK = unreleasedFlag(239, "lockscreen_custom_transit_clock") /** Migrate the lock icon view to the new keyguard root view. */ // TODO(b/286552209): Tracking bug. - @JvmField - val MIGRATE_LOCK_ICON = unreleasedFlag(240, "migrate_lock_icon", teamfood = true) + @JvmField val MIGRATE_LOCK_ICON = unreleasedFlag(240, "migrate_lock_icon", teamfood = true) // TODO(b/288276738): Tracking bug. - @JvmField - val WIDGET_ON_KEYGUARD = unreleasedFlag(241, "widget_on_keyguard") + @JvmField val WIDGET_ON_KEYGUARD = unreleasedFlag(241, "widget_on_keyguard") /** Migrate the NSSL to the a sibling to both the panel and keyguard root view. */ // TODO(b/288074305): Tracking bug. - @JvmField - val MIGRATE_NSSL = unreleasedFlag(242, "migrate_nssl") + @JvmField val MIGRATE_NSSL = unreleasedFlag(242, "migrate_nssl") /** Migrate the status view from the notification panel to keyguard root view. */ // TODO(b/291767565): Tracking bug. - @JvmField - val MIGRATE_KEYGUARD_STATUS_VIEW = unreleasedFlag(243, "migrate_keyguard_status_view") + @JvmField val MIGRATE_KEYGUARD_STATUS_VIEW = unreleasedFlag(243, "migrate_keyguard_status_view") // 300 - power menu // TODO(b/254512600): Tracking Bug @@ -316,8 +293,7 @@ object Flags { // TODO(b/270223352): Tracking Bug @JvmField - val HIDE_SMARTSPACE_ON_DREAM_OVERLAY = - releasedFlag(404, "hide_smartspace_on_dream_overlay") + val HIDE_SMARTSPACE_ON_DREAM_OVERLAY = releasedFlag(404, "hide_smartspace_on_dream_overlay") // TODO(b/271460958): Tracking Bug @JvmField @@ -357,8 +333,7 @@ object Flags { /** Enables Font Scaling Quick Settings tile */ // TODO(b/269341316): Tracking Bug - @JvmField - val ENABLE_FONT_SCALING_TILE = releasedFlag(509, "enable_font_scaling_tile") + @JvmField val ENABLE_FONT_SCALING_TILE = releasedFlag(509, "enable_font_scaling_tile") /** Enables new QS Edit Mode visual refresh */ // TODO(b/269787742): Tracking Bug @@ -367,13 +342,11 @@ object Flags { // 600- status bar - // TODO(b/265892345): Tracking Bug val PLUG_IN_STATUS_BAR_CHIP = releasedFlag(265892345, "plug_in_status_bar_chip") // TODO(b/280426085): Tracking Bug - @JvmField - val NEW_BLUETOOTH_REPOSITORY = releasedFlag(612, "new_bluetooth_repository") + @JvmField val NEW_BLUETOOTH_REPOSITORY = releasedFlag(612, "new_bluetooth_repository") // 700 - dialer/calls // TODO(b/254512734): Tracking Bug @@ -441,8 +414,8 @@ object Flags { // TODO(b/273509374): Tracking Bug @JvmField - val ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS = releasedFlag(1006, - "always_show_home_controls_on_dreams") + val ALWAYS_SHOW_HOME_CONTROLS_ON_DREAMS = + releasedFlag(1006, "always_show_home_controls_on_dreams") // 1100 - windowing @Keep @@ -490,11 +463,7 @@ object Flags { @Keep @JvmField val ENABLE_PIP_KEEP_CLEAR_ALGORITHM = - sysPropBooleanFlag( - 1110, - "persist.wm.debug.enable_pip_keep_clear_algorithm", - default = true - ) + sysPropBooleanFlag(1110, "persist.wm.debug.enable_pip_keep_clear_algorithm", default = true) // TODO(b/256873975): Tracking Bug @JvmField @@ -532,7 +501,8 @@ object Flags { // TODO(b/273443374): Tracking Bug @Keep - @JvmField val LOCKSCREEN_LIVE_WALLPAPER = + @JvmField + val LOCKSCREEN_LIVE_WALLPAPER = sysPropBooleanFlag(1117, "persist.wm.debug.lockscreen_live_wallpaper", default = true) // TODO(b/281648899): Tracking bug @@ -547,7 +517,6 @@ object Flags { val ENABLE_PIP2_IMPLEMENTATION = sysPropBooleanFlag(1119, "persist.wm.debug.enable_pip2_implementation", default = false) - // 1200 - predictive back @Keep @JvmField @@ -573,8 +542,7 @@ object Flags { unreleasedFlag(1204, "persist.wm.debug.predictive_back_sysui_enable", teamfood = true) // TODO(b/270987164): Tracking Bug - @JvmField - val TRACKPAD_GESTURE_FEATURES = releasedFlag(1205, "trackpad_gesture_features") + @JvmField val TRACKPAD_GESTURE_FEATURES = releasedFlag(1205, "trackpad_gesture_features") // TODO(b/263826204): Tracking Bug @JvmField @@ -597,8 +565,7 @@ object Flags { unreleasedFlag(1209, "persist.wm.debug.predictive_back_qs_dialog_anim", teamfood = true) // TODO(b/273800936): Tracking Bug - @JvmField - val TRACKPAD_GESTURE_COMMON = releasedFlag(1210, "trackpad_gesture_common") + @JvmField val TRACKPAD_GESTURE_COMMON = releasedFlag(1210, "trackpad_gesture_common") // 1300 - screenshots // TODO(b/264916608): Tracking Bug @@ -623,16 +590,12 @@ object Flags { // 1700 - clipboard @JvmField val CLIPBOARD_REMOTE_BEHAVIOR = releasedFlag(1701, "clipboard_remote_behavior") // TODO(b/278714186) Tracking Bug - @JvmField val CLIPBOARD_IMAGE_TIMEOUT = - unreleasedFlag(1702, "clipboard_image_timeout", teamfood = true) + @JvmField + val CLIPBOARD_IMAGE_TIMEOUT = unreleasedFlag(1702, "clipboard_image_timeout", teamfood = true) // TODO(b/279405451): Tracking Bug @JvmField val CLIPBOARD_SHARED_TRANSITIONS = unreleasedFlag(1703, "clipboard_shared_transitions") - // 1800 - shade container - // TODO(b/265944639): Tracking Bug - @JvmField val DUAL_SHADE = unreleasedFlag(1801, "dual_shade") - // TODO(b/283300105): Tracking Bug @JvmField val SCENE_CONTAINER = unreleasedFlag(1802, "scene_container") @@ -642,19 +605,15 @@ object Flags { // 2000 - device controls @Keep @JvmField val USE_APP_PANELS = releasedFlag(2000, "use_app_panels") - @JvmField - val APP_PANELS_ALL_APPS_ALLOWED = - releasedFlag(2001, "app_panels_all_apps_allowed") + @JvmField val APP_PANELS_ALL_APPS_ALLOWED = releasedFlag(2001, "app_panels_all_apps_allowed") @JvmField - val CONTROLS_MANAGEMENT_NEW_FLOWS = - releasedFlag(2002, "controls_management_new_flows") + val CONTROLS_MANAGEMENT_NEW_FLOWS = releasedFlag(2002, "controls_management_new_flows") // Enables removing app from Home control panel as a part of a new flow // TODO(b/269132640): Tracking Bug @JvmField - val APP_PANELS_REMOVE_APPS_ALLOWED = - releasedFlag(2003, "app_panels_remove_apps_allowed") + val APP_PANELS_REMOVE_APPS_ALLOWED = releasedFlag(2003, "app_panels_remove_apps_allowed") // 2200 - biometrics (udfps, sfps, BiometricPrompt, etc.) // TODO(b/259264861): Tracking Bug @@ -665,11 +624,9 @@ object Flags { // 2300 - stylus @JvmField val TRACK_STYLUS_EVER_USED = releasedFlag(2300, "track_stylus_ever_used") + @JvmField val ENABLE_STYLUS_CHARGING_UI = releasedFlag(2301, "enable_stylus_charging_ui") @JvmField - val ENABLE_STYLUS_CHARGING_UI = releasedFlag(2301, "enable_stylus_charging_ui") - @JvmField - val ENABLE_USI_BATTERY_NOTIFICATIONS = - releasedFlag(2302, "enable_usi_battery_notifications") + val ENABLE_USI_BATTERY_NOTIFICATIONS = releasedFlag(2302, "enable_usi_battery_notifications") @JvmField val ENABLE_STYLUS_EDUCATION = releasedFlag(2303, "enable_stylus_education") // 2400 - performance tools and debugging info @@ -681,11 +638,10 @@ object Flags { // TODO(b/283071711): Tracking bug @JvmField val TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK = - unreleasedFlag(2401, "trim_resources_with_background_trim_on_lock") + unreleasedFlag(2401, "trim_resources_with_background_trim_on_lock") // TODO:(b/283203305): Tracking bug - @JvmField - val TRIM_FONT_CACHES_AT_UNLOCK = unreleasedFlag(2402, "trim_font_caches_on_unlock") + @JvmField val TRIM_FONT_CACHES_AT_UNLOCK = unreleasedFlag(2402, "trim_font_caches_on_unlock") // 2700 - unfold transitions // TODO(b/265764985): Tracking Bug @@ -708,27 +664,21 @@ object Flags { @JvmField val SHORTCUT_LIST_SEARCH_LAYOUT = releasedFlag(2600, "shortcut_list_search_layout") // TODO(b/259428678): Tracking Bug - @JvmField - val KEYBOARD_BACKLIGHT_INDICATOR = releasedFlag(2601, "keyboard_backlight_indicator") + @JvmField val KEYBOARD_BACKLIGHT_INDICATOR = releasedFlag(2601, "keyboard_backlight_indicator") // TODO(b/277192623): Tracking Bug - @JvmField - val KEYBOARD_EDUCATION = - unreleasedFlag(2603, "keyboard_education", teamfood = false) + @JvmField val KEYBOARD_EDUCATION = unreleasedFlag(2603, "keyboard_education", teamfood = false) // TODO(b/277201412): Tracking Bug @JvmField - val SPLIT_SHADE_SUBPIXEL_OPTIMIZATION = - releasedFlag(2805, "split_shade_subpixel_optimization") + val SPLIT_SHADE_SUBPIXEL_OPTIMIZATION = releasedFlag(2805, "split_shade_subpixel_optimization") // TODO(b/288868056): Tracking Bug @JvmField - val PARTIAL_SCREEN_SHARING_TASK_SWITCHER = - unreleasedFlag(288868056, "pss_task_switcher") + val PARTIAL_SCREEN_SHARING_TASK_SWITCHER = unreleasedFlag(288868056, "pss_task_switcher") // TODO(b/278761837): Tracking Bug - @JvmField - val USE_NEW_ACTIVITY_STARTER = releasedFlag(2801, name = "use_new_activity_starter") + @JvmField val USE_NEW_ACTIVITY_STARTER = releasedFlag(2801, name = "use_new_activity_starter") // 2900 - Zero Jank fixes. Naming convention is: zj_<bug number>_<cuj name> @@ -744,23 +694,20 @@ object Flags { unreleasedFlag(3000, name = "enable_lockscreen_wallpaper_dream") // TODO(b/283084712): Tracking Bug - @JvmField - val IMPROVED_HUN_ANIMATIONS = unreleasedFlag(283084712, "improved_hun_animations") + @JvmField val IMPROVED_HUN_ANIMATIONS = unreleasedFlag(283084712, "improved_hun_animations") // TODO(b/283447257): Tracking bug @JvmField val BIGPICTURE_NOTIFICATION_LAZY_LOADING = - unreleasedFlag(283447257, "bigpicture_notification_lazy_loading") + unreleasedFlag(283447257, "bigpicture_notification_lazy_loading") // TODO(b/283740863): Tracking Bug @JvmField val ENABLE_NEW_PRIVACY_DIALOG = - unreleasedFlag(283740863, "enable_new_privacy_dialog", teamfood = false) + unreleasedFlag(283740863, "enable_new_privacy_dialog", teamfood = true) // TODO(b/289573946): Tracking Bug - @JvmField - val PRECOMPUTED_TEXT = - unreleasedFlag(289573946, "precomputed_text") + @JvmField val PRECOMPUTED_TEXT = unreleasedFlag(289573946, "precomputed_text") // 2900 - CentralSurfaces-related flags @@ -773,6 +720,5 @@ object Flags { // TODO(b/290213663): Tracking Bug @JvmField - val ONE_WAY_HAPTICS_API_MIGRATION = - unreleasedFlag(3100, "oneway_haptics_api_migration") + val ONE_WAY_HAPTICS_API_MIGRATION = unreleasedFlag(3100, "oneway_haptics_api_migration") } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index e0834bb894b5..82d0c8bb0e31 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -444,6 +444,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private final LockPatternUtils mLockPatternUtils; private final BroadcastDispatcher mBroadcastDispatcher; private boolean mKeyguardDonePending = false; + private boolean mUnlockingAndWakingFromDream = false; private boolean mHideAnimationRun = false; private boolean mHideAnimationRunning = false; @@ -802,6 +803,25 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mKeyguardViewControllerLazy.get().setKeyguardGoingAwayState(false); mKeyguardDisplayManager.hide(); mUpdateMonitor.startBiometricWatchdog(); + + // It's possible that the device was unlocked (via BOUNCER or Fingerprint) while + // dreaming. It's time to wake up. + if (mUnlockingAndWakingFromDream) { + Log.d(TAG, "waking from dream after unlock"); + mUnlockingAndWakingFromDream = false; + + if (mKeyguardStateController.isShowing()) { + Log.d(TAG, "keyguard showing after keyguardGone, dismiss"); + mKeyguardViewControllerLazy.get() + .notifyKeyguardAuthenticated(!mWakeAndUnlocking); + } else { + Log.d(TAG, "keyguard gone, waking up from dream"); + mPM.wakeUp(mSystemClock.uptimeMillis(), + mWakeAndUnlocking ? PowerManager.WAKE_REASON_BIOMETRIC + : PowerManager.WAKE_REASON_GESTURE, + "com.android.systemui:UNLOCK_DREAMING"); + } + } Trace.endSection(); } @@ -2673,6 +2693,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mKeyguardExitAnimationRunner = null; mWakeAndUnlocking = false; + mUnlockingAndWakingFromDream = false; setPendingLock(false); // Force if we we're showing in the middle of hiding, to ensure we end up in the correct @@ -2795,7 +2816,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, synchronized (KeyguardViewMediator.this) { if (DEBUG) Log.d(TAG, "handleHide"); - if (mShowing && !mOccluded) { + mUnlockingAndWakingFromDream = mStatusBarStateController.isDreaming() + && !mStatusBarStateController.isDozing(); + + if ((mShowing && !mOccluded) || mUnlockingAndWakingFromDream) { + if (mUnlockingAndWakingFromDream) { + Log.d(TAG, "hiding keyguard before waking from dream"); + } mHiding = true; mKeyguardGoingAwayRunnable.run(); } else { @@ -2806,13 +2833,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mHideAnimation.getDuration()); onKeyguardExitFinished(); } - - // It's possible that the device was unlocked (via BOUNCER or Fingerprint) while - // dreaming. It's time to wake up. - if (mDreamOverlayShowing || mUpdateMonitor.isDreaming()) { - mPM.wakeUp(mSystemClock.uptimeMillis(), PowerManager.WAKE_REASON_GESTURE, - "com.android.systemui:UNLOCK_DREAMING"); - } } Trace.endSection(); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt index 1978b3d048b7..039460d8fdae 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ResourceTrimmer.kt @@ -61,28 +61,23 @@ constructor( override fun start() { Log.d(LOG_TAG, "Resource trimmer registered.") - if ( - !(featureFlags.isEnabled(Flags.TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK) || - featureFlags.isEnabled(Flags.TRIM_FONT_CACHES_AT_UNLOCK)) - ) { - return - } - - applicationScope.launch(bgDispatcher) { - // We need to wait for the AoD transition (and animation) to complete. - // This means we're waiting for isDreaming (== implies isDoze) and dozeAmount == 1f - // signal. This is to make sure we don't clear font caches during animation which - // would jank and leave stale data in memory. - val isDozingFully = - keyguardInteractor.dozeAmount.map { it == 1f }.distinctUntilChanged() - combine( - keyguardInteractor.wakefulnessModel.map { it.state }, - keyguardInteractor.isDreaming, - isDozingFully, - ::Triple - ) - .distinctUntilChanged() - .collect { onWakefulnessUpdated(it.first, it.second, it.third) } + if (featureFlags.isEnabled(Flags.TRIM_RESOURCES_WITH_BACKGROUND_TRIM_AT_LOCK)) { + applicationScope.launch(bgDispatcher) { + // We need to wait for the AoD transition (and animation) to complete. + // This means we're waiting for isDreaming (== implies isDoze) and dozeAmount == 1f + // signal. This is to make sure we don't clear font caches during animation which + // would jank and leave stale data in memory. + val isDozingFully = + keyguardInteractor.dozeAmount.map { it == 1f }.distinctUntilChanged() + combine( + keyguardInteractor.wakefulnessModel.map { it.state }, + keyguardInteractor.isDreaming, + isDozingFully, + ::Triple + ) + .distinctUntilChanged() + .collect { onWakefulnessUpdated(it.first, it.second, it.third) } + } } applicationScope.launch(bgDispatcher) { @@ -97,17 +92,16 @@ constructor( @WorkerThread private fun onKeyguardGone() { - if (!featureFlags.isEnabled(Flags.TRIM_FONT_CACHES_AT_UNLOCK)) { - return - } - - if (DEBUG) { - Log.d(LOG_TAG, "Trimming font caches since keyguard went away.") - } // We want to clear temporary caches we've created while rendering and animating // lockscreen elements, especially clocks. + Log.d(LOG_TAG, "Sending TRIM_MEMORY_UI_HIDDEN.") globalWindowManager.trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) - globalWindowManager.trimCaches(HardwareRenderer.CACHE_TRIM_FONT) + if (featureFlags.isEnabled(Flags.TRIM_FONT_CACHES_AT_UNLOCK)) { + if (DEBUG) { + Log.d(LOG_TAG, "Trimming font caches since keyguard went away.") + } + globalWindowManager.trimCaches(HardwareRenderer.CACHE_TRIM_FONT) + } } @WorkerThread diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt index d1f011ea4f9b..bf1e75b09bac 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt @@ -102,6 +102,9 @@ interface DeviceEntryFaceAuthRepository { /** Whether bypass is currently enabled */ val isBypassEnabled: Flow<Boolean> + /** Set whether face authentication should be locked out or not */ + fun lockoutFaceAuth() + /** * Trigger face authentication. * @@ -199,6 +202,10 @@ constructor( } ?: flowOf(false) + override fun lockoutFaceAuth() { + _isLockedOut.value = true + } + private val faceLockoutResetCallback = object : FaceManager.LockoutResetCallback() { override fun onLockoutReset(sensorId: Int) { @@ -396,7 +403,7 @@ constructor( private val faceAuthCallback = object : FaceManager.AuthenticationCallback() { override fun onAuthenticationFailed() { - _authenticationStatus.value = FailedFaceAuthenticationStatus + _authenticationStatus.value = FailedFaceAuthenticationStatus() _isAuthenticated.value = false faceAuthLogger.authenticationFailed() onFaceAuthRequestCompleted() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt index 482e9a3d09d7..6a2511fdb90e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt @@ -20,10 +20,13 @@ package com.android.systemui.keyguard.data.repository import android.content.Context import android.graphics.Point +import androidx.core.animation.Animator +import androidx.core.animation.ValueAnimator import com.android.systemui.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.BiometricUnlockSource +import com.android.systemui.keyguard.shared.model.WakeSleepReason.TAP import com.android.systemui.statusbar.CircleReveal import com.android.systemui.statusbar.LiftReveal import com.android.systemui.statusbar.LightRevealEffect @@ -31,9 +34,12 @@ import com.android.systemui.statusbar.PowerButtonReveal import javax.inject.Inject import kotlin.math.max import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @@ -52,6 +58,10 @@ interface LightRevealScrimRepository { * at the current screen position of the appropriate sensor. */ val revealEffect: Flow<LightRevealEffect> + + val revealAmount: Flow<Float> + + fun startRevealAmountAnimator(reveal: Boolean) } @SysUISingleton @@ -108,13 +118,30 @@ constructor( /** The reveal effect we'll use for the next non-biometric unlock (tap, power button, etc). */ private val nonBiometricRevealEffect: Flow<LightRevealEffect?> = - keyguardRepository.wakefulness.flatMapLatest { wakefulnessModel -> - when { - wakefulnessModel.isTransitioningFromPowerButton() -> powerButtonRevealEffect - wakefulnessModel.isAwakeFromTap() -> tapRevealEffect - else -> flowOf(LiftReveal) + keyguardRepository.wakefulness + .filter { it.isStartingToWake() || it.isStartingToSleep() } + .flatMapLatest { wakefulnessModel -> + when { + wakefulnessModel.isTransitioningFromPowerButton() -> powerButtonRevealEffect + wakefulnessModel.isWakingFrom(TAP) -> tapRevealEffect + else -> flowOf(LiftReveal) + } } - } + + private val revealAmountAnimator = ValueAnimator.ofFloat(0f, 1f).apply { duration = 500 } + + override val revealAmount: Flow<Float> = callbackFlow { + val updateListener = + Animator.AnimatorUpdateListener { + trySend((it as ValueAnimator).animatedValue as Float) + } + revealAmountAnimator.addUpdateListener(updateListener) + awaitClose { revealAmountAnimator.removeUpdateListener(updateListener) } + } + + override fun startRevealAmountAnimator(reveal: Boolean) { + if (reveal) revealAmountAnimator.start() else revealAmountAnimator.reverse() + } override val revealEffect = combine( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt index 27e3a749a6c0..5ef9a9e0482c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/NoopDeviceEntryFaceAuthRepository.kt @@ -55,6 +55,8 @@ class NoopDeviceEntryFaceAuthRepository @Inject constructor() : DeviceEntryFaceA override val isBypassEnabled: Flow<Boolean> get() = emptyFlow() + override fun lockoutFaceAuth() = Unit + /** * Trigger face authentication. * diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt index 141b13055889..e57c919a5b3e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractor.kt @@ -60,6 +60,7 @@ interface KeyguardFaceAuthInteractor { fun onNotificationPanelClicked() fun onSwipeUpOnBouncer() fun onPrimaryBouncerUserInput() + fun onAccessibilityAction() } /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index 324d443d974d..40e0604ae1b3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -409,6 +409,10 @@ constructor( KeyguardPickerFlag( name = Contract.FlagsTable.FLAG_NAME_TRANSIT_CLOCK, value = featureFlags.isEnabled(Flags.TRANSIT_CLOCK) + ), + KeyguardPickerFlag( + name = Contract.FlagsTable.FLAG_NAME_PAGE_TRANSITIONS, + value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_PAGE_TRANSITIONS) ) ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt index 833eda77108d..4244e5565a4b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractor.kt @@ -17,28 +17,44 @@ package com.android.systemui.keyguard.domain.interactor import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.LightRevealScrimRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.statusbar.LightRevealEffect import com.android.systemui.util.kotlin.sample import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch @ExperimentalCoroutinesApi @SysUISingleton class LightRevealScrimInteractor @Inject constructor( - transitionRepository: KeyguardTransitionRepository, - transitionInteractor: KeyguardTransitionInteractor, - lightRevealScrimRepository: LightRevealScrimRepository, + private val transitionInteractor: KeyguardTransitionInteractor, + private val lightRevealScrimRepository: LightRevealScrimRepository, + @Application private val scope: CoroutineScope, ) { + init { + listenForStartedKeyguardTransitionStep() + } + + private fun listenForStartedKeyguardTransitionStep() { + scope.launch { + transitionInteractor.startedKeyguardTransitionStep.collect { + if (willTransitionChangeEndState(it)) { + lightRevealScrimRepository.startRevealAmountAnimator( + willBeRevealedInState(it.to) + ) + } + } + } + } + /** * Whenever a keyguard transition starts, sample the latest reveal effect from the repository * and use that for the starting transition. @@ -54,17 +70,7 @@ constructor( lightRevealScrimRepository.revealEffect ) - /** - * The reveal amount to use for the light reveal scrim, which is derived from the keyguard - * transition steps. - */ - val revealAmount: Flow<Float> = - transitionRepository.transitions - // Only listen to transitions that change the reveal amount. - .filter { willTransitionAffectRevealAmount(it) } - // Use the transition amount as the reveal amount, inverting it if we're transitioning - // to a non-revealed (hidden) state. - .map { step -> if (willBeRevealedInState(step.to)) step.value else 1f - step.value } + val revealAmount = lightRevealScrimRepository.revealAmount companion object { @@ -72,7 +78,7 @@ constructor( * Whether the transition requires a change in the reveal amount of the light reveal scrim. * If not, we don't care about the transition and don't need to listen to it. */ - fun willTransitionAffectRevealAmount(transition: TransitionStep): Boolean { + fun willTransitionChangeEndState(transition: TransitionStep): Boolean { return willBeRevealedInState(transition.from) != willBeRevealedInState(transition.to) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt index 10dd900e437e..596a1c01ca42 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NoopKeyguardFaceAuthInteractor.kt @@ -60,4 +60,5 @@ class NoopKeyguardFaceAuthInteractor @Inject constructor() : KeyguardFaceAuthInt override fun onSwipeUpOnBouncer() {} override fun onPrimaryBouncerUserInput() {} + override fun onAccessibilityAction() {} } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt index 6e7a20b092f4..8f4776fa2ed3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SystemUIKeyguardFaceAuthInteractor.kt @@ -30,6 +30,7 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus import com.android.systemui.keyguard.shared.model.FaceAuthenticationStatus import com.android.systemui.keyguard.shared.model.TransitionState @@ -67,6 +68,7 @@ constructor( private val featureFlags: FeatureFlags, private val faceAuthenticationLogger: FaceAuthenticationLogger, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository, ) : CoreStartable, KeyguardFaceAuthInteractor { private val listeners: MutableList<FaceAuthenticationListener> = mutableListOf() @@ -117,6 +119,15 @@ constructor( ) } .launchIn(applicationScope) + + deviceEntryFingerprintAuthRepository.isLockedOut + .onEach { + if (it) { + faceAuthenticationLogger.faceLockedOut("Fingerprint locked out") + repository.lockoutFaceAuth() + } + } + .launchIn(applicationScope) } override fun onSwipeUpOnBouncer() { @@ -143,6 +154,10 @@ constructor( runFaceAuth(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_UDFPS_POINTER_DOWN, false) } + override fun onAccessibilityAction() { + runFaceAuth(FaceAuthUiEvent.FACE_AUTH_ACCESSIBILITY_ACTION, false) + } + override fun registerListener(listener: FaceAuthenticationListener) { listeners.add(listener) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt index d9792cf704c8..3de3666fdc3c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/FaceAuthenticationModels.kt @@ -23,29 +23,40 @@ import android.os.SystemClock.elapsedRealtime * Authentication status provided by * [com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthRepository] */ -sealed class FaceAuthenticationStatus( - // present to break equality check if the same error occurs repeatedly. - val createdAt: Long = elapsedRealtime() -) +sealed class FaceAuthenticationStatus /** Success authentication status. */ -data class SuccessFaceAuthenticationStatus(val successResult: FaceManager.AuthenticationResult) : - FaceAuthenticationStatus() +data class SuccessFaceAuthenticationStatus( + val successResult: FaceManager.AuthenticationResult, + // present to break equality check if the same error occurs repeatedly. + @JvmField val createdAt: Long = elapsedRealtime() +) : FaceAuthenticationStatus() /** Face authentication help message. */ -data class HelpFaceAuthenticationStatus(val msgId: Int, val msg: String?) : - FaceAuthenticationStatus() +data class HelpFaceAuthenticationStatus( + val msgId: Int, + val msg: String?, // present to break equality check if the same error occurs repeatedly. + @JvmField val createdAt: Long = elapsedRealtime() +) : FaceAuthenticationStatus() /** Face acquired message. */ -data class AcquiredFaceAuthenticationStatus(val acquiredInfo: Int) : FaceAuthenticationStatus() +data class AcquiredFaceAuthenticationStatus( + val acquiredInfo: Int, // present to break equality check if the same error occurs repeatedly. + @JvmField val createdAt: Long = elapsedRealtime() +) : FaceAuthenticationStatus() /** Face authentication failed message. */ -object FailedFaceAuthenticationStatus : FaceAuthenticationStatus() +data class FailedFaceAuthenticationStatus( + // present to break equality check if the same error occurs repeatedly. + @JvmField val createdAt: Long = elapsedRealtime() +) : FaceAuthenticationStatus() /** Face authentication error message */ data class ErrorFaceAuthenticationStatus( val msgId: Int, val msg: String? = null, + // present to break equality check if the same error occurs repeatedly. + @JvmField val createdAt: Long = elapsedRealtime() ) : FaceAuthenticationStatus() { /** * Method that checks if [msgId] is a lockout error. A lockout error means that face @@ -80,5 +91,5 @@ data class FaceDetectionStatus( val userId: Int, val isStrongBiometric: Boolean, // present to break equality check if the same error occurs repeatedly. - val createdAt: Long = elapsedRealtime() + @JvmField val createdAt: Long = elapsedRealtime() ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt index cfd9e0866c06..62f43ed82a96 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/WakefulnessModel.kt @@ -16,6 +16,13 @@ package com.android.systemui.keyguard.shared.model import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.keyguard.shared.model.WakeSleepReason.GESTURE +import com.android.systemui.keyguard.shared.model.WakeSleepReason.POWER_BUTTON +import com.android.systemui.keyguard.shared.model.WakeSleepReason.TAP +import com.android.systemui.keyguard.shared.model.WakefulnessState.ASLEEP +import com.android.systemui.keyguard.shared.model.WakefulnessState.AWAKE +import com.android.systemui.keyguard.shared.model.WakefulnessState.STARTING_TO_SLEEP +import com.android.systemui.keyguard.shared.model.WakefulnessState.STARTING_TO_WAKE /** Model device wakefulness states. */ data class WakefulnessModel( @@ -23,33 +30,31 @@ data class WakefulnessModel( val lastWakeReason: WakeSleepReason, val lastSleepReason: WakeSleepReason, ) { - fun isStartingToWake() = state == WakefulnessState.STARTING_TO_WAKE + fun isStartingToWake() = state == STARTING_TO_WAKE - fun isStartingToSleep() = state == WakefulnessState.STARTING_TO_SLEEP + fun isStartingToSleep() = state == STARTING_TO_SLEEP - private fun isAsleep() = state == WakefulnessState.ASLEEP + private fun isAsleep() = state == ASLEEP + + private fun isAwake() = state == AWAKE + + fun isStartingToWakeOrAwake() = isStartingToWake() || isAwake() fun isStartingToSleepOrAsleep() = isStartingToSleep() || isAsleep() fun isDeviceInteractive() = !isAsleep() - fun isStartingToWakeOrAwake() = isStartingToWake() || state == WakefulnessState.AWAKE + fun isWakingFrom(wakeSleepReason: WakeSleepReason) = + isStartingToWake() && lastWakeReason == wakeSleepReason - fun isStartingToSleepFromPowerButton() = - isStartingToSleep() && lastWakeReason == WakeSleepReason.POWER_BUTTON - - fun isWakingFromPowerButton() = - isStartingToWake() && lastWakeReason == WakeSleepReason.POWER_BUTTON + fun isStartingToSleepFrom(wakeSleepReason: WakeSleepReason) = + isStartingToSleep() && lastSleepReason == wakeSleepReason fun isTransitioningFromPowerButton() = - isStartingToSleepFromPowerButton() || isWakingFromPowerButton() - - fun isAwakeFromTap() = - state == WakefulnessState.STARTING_TO_WAKE && lastWakeReason == WakeSleepReason.TAP + isStartingToSleepFrom(POWER_BUTTON) || isWakingFrom(POWER_BUTTON) fun isDeviceInteractiveFromTapOrGesture(): Boolean { - return isDeviceInteractive() && - (lastWakeReason == WakeSleepReason.TAP || lastWakeReason == WakeSleepReason.GESTURE) + return isDeviceInteractive() && (lastWakeReason == TAP || lastWakeReason == GESTURE) } companion object { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt index 389cf76c47ac..f1ceaaa391f5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardIndicationAreaViewModel.kt @@ -20,20 +20,18 @@ import com.android.systemui.doze.util.BurnInHelperWrapper import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import javax.inject.Inject -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map /** View-model for the keyguard indication area view */ -@OptIn(ExperimentalCoroutinesApi::class) class KeyguardIndicationAreaViewModel @Inject constructor( private val keyguardInteractor: KeyguardInteractor, - private val bottomAreaInteractor: KeyguardBottomAreaInteractor, - private val keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel, + bottomAreaInteractor: KeyguardBottomAreaInteractor, + keyguardBottomAreaViewModel: KeyguardBottomAreaViewModel, private val burnInHelperWrapper: BurnInHelperWrapper, ) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt index a46d441613ac..82f40bf3a16a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LightRevealScrimViewModel.kt @@ -19,12 +19,14 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.keyguard.domain.interactor.LightRevealScrimInteractor import com.android.systemui.statusbar.LightRevealEffect import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow /** * Models UI state for the light reveal scrim, which is used during screen on and off animations to * draw a gradient that reveals/hides the contents of the screen. */ +@OptIn(ExperimentalCoroutinesApi::class) class LightRevealScrimViewModel @Inject constructor(interactor: LightRevealScrimInteractor) { val lightRevealEffect: Flow<LightRevealEffect> = interactor.lightRevealEffect val revealAmount: Flow<Float> = interactor.revealAmount diff --git a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt index 373f70582612..66067b11a18c 100644 --- a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt @@ -8,6 +8,7 @@ import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.log.core.LogLevel.DEBUG import com.android.systemui.log.dagger.FaceAuthLog +import com.google.errorprone.annotations.CompileTimeConstant import javax.inject.Inject private const val TAG = "DeviceEntryFaceAuthRepositoryLog" @@ -264,4 +265,8 @@ constructor( fun watchdogScheduled() { logBuffer.log(TAG, DEBUG, "FaceManager Biometric watchdog scheduled.") } + + fun faceLockedOut(@CompileTimeConstant reason: String) { + logBuffer.log(TAG, DEBUG, "Face auth has been locked out: $reason") + } } diff --git a/packages/SystemUI/src/com/android/systemui/multishade/data/model/MultiShadeInteractionModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/data/model/MultiShadeInteractionModel.kt deleted file mode 100644 index c48028c31cf0..000000000000 --- a/packages/SystemUI/src/com/android/systemui/multishade/data/model/MultiShadeInteractionModel.kt +++ /dev/null @@ -1,28 +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.multishade.data.model - -import com.android.systemui.multishade.shared.model.ShadeId - -/** Models the current interaction with one of the shades. */ -data class MultiShadeInteractionModel( - /** The ID of the shade that the user is currently interacting with. */ - val shadeId: ShadeId, - /** Whether the interaction is proxied (as in: coming from an external app or different UI). */ - val isProxied: Boolean, -) diff --git a/packages/SystemUI/src/com/android/systemui/multishade/data/remoteproxy/MultiShadeInputProxy.kt b/packages/SystemUI/src/com/android/systemui/multishade/data/remoteproxy/MultiShadeInputProxy.kt deleted file mode 100644 index 86f0c0d15b55..000000000000 --- a/packages/SystemUI/src/com/android/systemui/multishade/data/remoteproxy/MultiShadeInputProxy.kt +++ /dev/null @@ -1,47 +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.multishade.data.remoteproxy - -import com.android.systemui.multishade.shared.model.ProxiedInputModel -import javax.inject.Inject -import javax.inject.Singleton -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow - -/** - * Acts as a hub for routing proxied user input into the multi shade system. - * - * "Proxied" user input is coming through a proxy; typically from an external app or different UI. - * In other words: it's not user input that's occurring directly on the shade UI itself. This class - * is that proxy. - */ -@Singleton -class MultiShadeInputProxy @Inject constructor() { - private val _proxiedTouch = - MutableSharedFlow<ProxiedInputModel>( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - val proxiedInput: Flow<ProxiedInputModel> = _proxiedTouch.asSharedFlow() - - fun onProxiedInput(proxiedInput: ProxiedInputModel) { - _proxiedTouch.tryEmit(proxiedInput) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/multishade/data/repository/MultiShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/multishade/data/repository/MultiShadeRepository.kt deleted file mode 100644 index 117203012757..000000000000 --- a/packages/SystemUI/src/com/android/systemui/multishade/data/repository/MultiShadeRepository.kt +++ /dev/null @@ -1,157 +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.multishade.data.repository - -import android.content.Context -import androidx.annotation.FloatRange -import com.android.systemui.R -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.multishade.data.model.MultiShadeInteractionModel -import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy -import com.android.systemui.multishade.shared.model.ProxiedInputModel -import com.android.systemui.multishade.shared.model.ShadeConfig -import com.android.systemui.multishade.shared.model.ShadeId -import com.android.systemui.multishade.shared.model.ShadeModel -import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow - -/** Encapsulates application state for all shades. */ -@SysUISingleton -class MultiShadeRepository -@Inject -constructor( - @Application private val applicationContext: Context, - inputProxy: MultiShadeInputProxy, -) { - /** - * Remote input coming from sources outside of system UI (for example, swiping down on the - * Launcher or from the status bar). - */ - val proxiedInput: Flow<ProxiedInputModel> = inputProxy.proxiedInput - - /** Width of the left-hand side shade, in pixels. */ - private val leftShadeWidthPx = - applicationContext.resources.getDimensionPixelSize(R.dimen.left_shade_width) - - /** Width of the right-hand side shade, in pixels. */ - private val rightShadeWidthPx = - applicationContext.resources.getDimensionPixelSize(R.dimen.right_shade_width) - - /** - * The amount that the user must swipe up when the shade is fully expanded to automatically - * collapse once the user lets go of the shade. If the user swipes less than this amount, the - * shade will automatically revert back to fully expanded once the user stops swiping. - * - * This is a fraction between `0` and `1`. - */ - private val swipeCollapseThreshold = - checkInBounds(applicationContext.resources.getFloat(R.dimen.shade_swipe_collapse_threshold)) - - /** - * The amount that the user must swipe down when the shade is fully collapsed to automatically - * expand once the user lets go of the shade. If the user swipes less than this amount, the - * shade will automatically revert back to fully collapsed once the user stops swiping. - * - * This is a fraction between `0` and `1`. - */ - private val swipeExpandThreshold = - checkInBounds(applicationContext.resources.getFloat(R.dimen.shade_swipe_expand_threshold)) - - /** - * Maximum opacity when the scrim that shows up behind the dual shades is fully visible. - * - * This is a fraction between `0` and `1`. - */ - private val dualShadeScrimAlpha = - checkInBounds(applicationContext.resources.getFloat(R.dimen.dual_shade_scrim_alpha)) - - /** The current configuration of the shade system. */ - val shadeConfig: StateFlow<ShadeConfig> = - MutableStateFlow( - if (applicationContext.resources.getBoolean(R.bool.dual_shade_enabled)) { - ShadeConfig.DualShadeConfig( - leftShadeWidthPx = leftShadeWidthPx, - rightShadeWidthPx = rightShadeWidthPx, - swipeCollapseThreshold = swipeCollapseThreshold, - swipeExpandThreshold = swipeExpandThreshold, - splitFraction = - applicationContext.resources.getFloat( - R.dimen.dual_shade_split_fraction - ), - scrimAlpha = dualShadeScrimAlpha, - ) - } else { - ShadeConfig.SingleShadeConfig( - swipeCollapseThreshold = swipeCollapseThreshold, - swipeExpandThreshold = swipeExpandThreshold, - ) - } - ) - .asStateFlow() - - private val _forceCollapseAll = MutableStateFlow(false) - /** Whether all shades should be collapsed. */ - val forceCollapseAll: StateFlow<Boolean> = _forceCollapseAll.asStateFlow() - - private val _shadeInteraction = MutableStateFlow<MultiShadeInteractionModel?>(null) - /** The current shade interaction or `null` if no shade is interacted with currently. */ - val shadeInteraction: StateFlow<MultiShadeInteractionModel?> = _shadeInteraction.asStateFlow() - - private val stateByShade = mutableMapOf<ShadeId, MutableStateFlow<ShadeModel>>() - - /** The model for the shade with the given ID. */ - fun getShade( - shadeId: ShadeId, - ): StateFlow<ShadeModel> { - return getMutableShade(shadeId).asStateFlow() - } - - /** Sets the expansion amount for the shade with the given ID. */ - fun setExpansion( - shadeId: ShadeId, - @FloatRange(from = 0.0, to = 1.0) expansion: Float, - ) { - getMutableShade(shadeId).let { mutableState -> - mutableState.value = mutableState.value.copy(expansion = expansion) - } - } - - /** Sets whether all shades should be immediately forced to collapse. */ - fun setForceCollapseAll(isForced: Boolean) { - _forceCollapseAll.value = isForced - } - - /** Sets the current shade interaction; use `null` if no shade is interacted with currently. */ - fun setShadeInteraction(shadeInteraction: MultiShadeInteractionModel?) { - _shadeInteraction.value = shadeInteraction - } - - private fun getMutableShade(id: ShadeId): MutableStateFlow<ShadeModel> { - return stateByShade.getOrPut(id) { MutableStateFlow(ShadeModel(id)) } - } - - /** Asserts that the given [Float] is in the range of `0` and `1`, inclusive. */ - private fun checkInBounds(float: Float): Float { - check(float in 0f..1f) { "$float isn't between 0 and 1." } - return float - } -} diff --git a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt deleted file mode 100644 index ebb8639b8922..000000000000 --- a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractor.kt +++ /dev/null @@ -1,327 +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.multishade.domain.interactor - -import androidx.annotation.FloatRange -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.multishade.data.model.MultiShadeInteractionModel -import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy -import com.android.systemui.multishade.data.repository.MultiShadeRepository -import com.android.systemui.multishade.shared.math.isZero -import com.android.systemui.multishade.shared.model.ProxiedInputModel -import com.android.systemui.multishade.shared.model.ShadeConfig -import com.android.systemui.multishade.shared.model.ShadeId -import com.android.systemui.multishade.shared.model.ShadeModel -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.shareIn -import kotlinx.coroutines.yield - -/** Encapsulates business logic related to interactions with the multi-shade system. */ -@OptIn(ExperimentalCoroutinesApi::class) -@SysUISingleton -class MultiShadeInteractor -@Inject -constructor( - @Application private val applicationScope: CoroutineScope, - private val repository: MultiShadeRepository, - private val inputProxy: MultiShadeInputProxy, -) { - /** The current configuration of the shade system. */ - val shadeConfig: StateFlow<ShadeConfig> = repository.shadeConfig - - /** The expansion of the shade that's most expanded. */ - val maxShadeExpansion: Flow<Float> = - repository.shadeConfig.flatMapLatest { shadeConfig -> - combine(allShades(shadeConfig)) { shadeModels -> - shadeModels.maxOfOrNull { it.expansion } ?: 0f - } - } - - /** Whether any shade is expanded, even a little bit. */ - val isAnyShadeExpanded: Flow<Boolean> = - maxShadeExpansion.map { maxExpansion -> !maxExpansion.isZero() }.distinctUntilChanged() - - /** - * A _processed_ version of the proxied input flow. - * - * All internal dependencies on the proxied input flow *must* use this one for two reasons: - * 1. It's a [SharedFlow] so we only do the upstream work once, no matter how many usages we - * actually have. - * 2. It actually does some preprocessing as the proxied input events stream through, handling - * common things like recording the current state of the system based on incoming input - * events. - */ - private val processedProxiedInput: SharedFlow<ProxiedInputModel> = - combine( - repository.shadeConfig, - repository.proxiedInput.distinctUntilChanged(), - ::Pair, - ) - .map { (shadeConfig, proxiedInput) -> - if (proxiedInput !is ProxiedInputModel.OnTap) { - // If the user is interacting with any other gesture type (for instance, - // dragging), - // we no longer want to force collapse all shades. - repository.setForceCollapseAll(false) - } - - when (proxiedInput) { - is ProxiedInputModel.OnDrag -> { - val affectedShadeId = affectedShadeId(shadeConfig, proxiedInput.xFraction) - // This might be the start of a new drag gesture, let's update our - // application - // state to record that fact. - onUserInteractionStarted( - shadeId = affectedShadeId, - isProxied = true, - ) - } - is ProxiedInputModel.OnTap -> { - // Tapping outside any shade collapses all shades. This code path is not hit - // for - // taps that happen _inside_ a shade as that input event is directly applied - // through the UI and is, hence, not a proxied input. - collapseAll() - } - else -> Unit - } - - proxiedInput - } - .shareIn( - scope = applicationScope, - started = SharingStarted.Eagerly, - replay = 1, - ) - - /** Whether the shade with the given ID should be visible. */ - fun isVisible(shadeId: ShadeId): Flow<Boolean> { - return repository.shadeConfig.map { shadeConfig -> shadeConfig.shadeIds.contains(shadeId) } - } - - /** Whether direct user input is allowed on the shade with the given ID. */ - fun isNonProxiedInputAllowed(shadeId: ShadeId): Flow<Boolean> { - return combine( - isForceCollapsed(shadeId), - repository.shadeInteraction, - ::Pair, - ) - .map { (isForceCollapsed, shadeInteraction) -> - !isForceCollapsed && shadeInteraction?.isProxied != true - } - } - - /** Whether the shade with the given ID is forced to collapse. */ - fun isForceCollapsed(shadeId: ShadeId): Flow<Boolean> { - return combine( - repository.forceCollapseAll, - repository.shadeInteraction.map { it?.shadeId }, - ::Pair, - ) - .map { (collapseAll, userInteractedShadeIdOrNull) -> - val counterpartShadeIdOrNull = - when (shadeId) { - ShadeId.SINGLE -> null - ShadeId.LEFT -> ShadeId.RIGHT - ShadeId.RIGHT -> ShadeId.LEFT - } - - when { - // If all shades have been told to collapse (by a tap outside, for example), - // then this shade is collapsed. - collapseAll -> true - // A shade that doesn't have a counterpart shade cannot be force-collapsed by - // interactions on the counterpart shade. - counterpartShadeIdOrNull == null -> false - // If the current user interaction is on the counterpart shade, then this shade - // should be force-collapsed. - else -> userInteractedShadeIdOrNull == counterpartShadeIdOrNull - } - } - } - - /** - * Proxied input affecting the shade with the given ID. This is input coming from sources - * outside of system UI (for example, swiping down on the Launcher or from the status bar) or - * outside the UI of any shade (for example, the scrim that's shown behind the shades). - */ - fun proxiedInput(shadeId: ShadeId): Flow<ProxiedInputModel?> { - return combine( - processedProxiedInput, - isForceCollapsed(shadeId).distinctUntilChanged(), - repository.shadeInteraction, - ::Triple, - ) - .map { (proxiedInput, isForceCollapsed, shadeInteraction) -> - when { - // If the shade is force-collapsed, we ignored proxied input on it. - isForceCollapsed -> null - // If the proxied input does not belong to this shade, ignore it. - shadeInteraction?.shadeId != shadeId -> null - // If there is ongoing non-proxied user input on any shade, ignore the - // proxied input. - !shadeInteraction.isProxied -> null - // Otherwise, send the proxied input downstream. - else -> proxiedInput - } - } - .onEach { proxiedInput -> - // We use yield() to make sure that the following block of code happens _after_ - // downstream collectors had a chance to process the proxied input. Otherwise, we - // might change our state to clear the current UserInteraction _before_ those - // downstream collectors get a chance to process the proxied input, which will make - // them ignore it (since they ignore proxied input when the current user interaction - // doesn't match their shade). - yield() - - if ( - proxiedInput is ProxiedInputModel.OnDragEnd || - proxiedInput is ProxiedInputModel.OnDragCancel - ) { - onUserInteractionEnded(shadeId = shadeId, isProxied = true) - } - } - } - - /** Sets the expansion amount for the shade with the given ID. */ - fun setExpansion( - shadeId: ShadeId, - @FloatRange(from = 0.0, to = 1.0) expansion: Float, - ) { - repository.setExpansion(shadeId, expansion) - } - - /** Collapses all shades. */ - fun collapseAll() { - repository.setForceCollapseAll(true) - } - - /** - * Notifies that a new non-proxied interaction may have started. Safe to call multiple times for - * the same interaction as it won't overwrite an existing interaction. - * - * Existing interactions can be cleared by calling [onUserInteractionEnded]. - */ - fun onUserInteractionStarted(shadeId: ShadeId) { - onUserInteractionStarted( - shadeId = shadeId, - isProxied = false, - ) - } - - /** - * Notifies that the current non-proxied interaction has ended. - * - * Safe to call multiple times, even if there's no current interaction or even if the current - * interaction doesn't belong to the given shade or is proxied as the code is a no-op unless - * there's a match between the parameters and the current interaction. - */ - fun onUserInteractionEnded( - shadeId: ShadeId, - ) { - onUserInteractionEnded( - shadeId = shadeId, - isProxied = false, - ) - } - - fun sendProxiedInput(proxiedInput: ProxiedInputModel) { - inputProxy.onProxiedInput(proxiedInput) - } - - /** - * Notifies that a new interaction may have started. Safe to call multiple times for the same - * interaction as it won't overwrite an existing interaction. - * - * Existing interactions can be cleared by calling [onUserInteractionEnded]. - */ - private fun onUserInteractionStarted( - shadeId: ShadeId, - isProxied: Boolean, - ) { - if (repository.shadeInteraction.value != null) { - return - } - - repository.setShadeInteraction( - MultiShadeInteractionModel( - shadeId = shadeId, - isProxied = isProxied, - ) - ) - } - - /** - * Notifies that the current interaction has ended. - * - * Safe to call multiple times, even if there's no current interaction or even if the current - * interaction doesn't belong to the given shade or [isProxied] value as the code is a no-op - * unless there's a match between the parameters and the current interaction. - */ - private fun onUserInteractionEnded( - shadeId: ShadeId, - isProxied: Boolean, - ) { - repository.shadeInteraction.value?.let { (interactionShadeId, isInteractionProxied) -> - if (shadeId == interactionShadeId && isProxied == isInteractionProxied) { - repository.setShadeInteraction(null) - } - } - } - - /** - * Returns the ID of the shade that's affected by user input at a given coordinate. - * - * @param config The shade configuration being used. - * @param xFraction The horizontal position of the user input as a fraction along the width of - * its container where `0` is all the way to the left and `1` is all the way to the right. - */ - private fun affectedShadeId( - config: ShadeConfig, - @FloatRange(from = 0.0, to = 1.0) xFraction: Float, - ): ShadeId { - return if (config is ShadeConfig.DualShadeConfig) { - if (xFraction <= config.splitFraction) { - ShadeId.LEFT - } else { - ShadeId.RIGHT - } - } else { - ShadeId.SINGLE - } - } - - /** Returns the list of flows of all the shades in the given configuration. */ - private fun allShades( - config: ShadeConfig, - ): List<Flow<ShadeModel>> { - return config.shadeIds.map { shadeId -> repository.getShade(shadeId) } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt deleted file mode 100644 index 1894bc4cfeab..000000000000 --- a/packages/SystemUI/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractor.kt +++ /dev/null @@ -1,288 +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.multishade.domain.interactor - -import android.content.Context -import android.view.MotionEvent -import android.view.ViewConfiguration -import com.android.systemui.classifier.Classifier -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.shared.model.KeyguardState -import com.android.systemui.multishade.shared.math.isZero -import com.android.systemui.multishade.shared.model.ProxiedInputModel -import com.android.systemui.plugins.FalsingManager -import com.android.systemui.shade.ShadeController -import javax.inject.Inject -import kotlin.math.abs -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch - -/** - * Encapsulates business logic to handle [MotionEvent]-based user input. - * - * This class is meant purely for the legacy `View`-based system to be able to pass `MotionEvent`s - * into the newer multi-shade framework for processing. - */ -@SysUISingleton -class MultiShadeMotionEventInteractor -@Inject -constructor( - @Application private val applicationContext: Context, - @Application private val applicationScope: CoroutineScope, - private val multiShadeInteractor: MultiShadeInteractor, - featureFlags: FeatureFlags, - keyguardTransitionInteractor: KeyguardTransitionInteractor, - private val falsingManager: FalsingManager, - private val shadeController: ShadeController, -) { - init { - if (featureFlags.isEnabled(Flags.DUAL_SHADE)) { - applicationScope.launch { - multiShadeInteractor.isAnyShadeExpanded.collect { - if (!it && !shadeController.isKeyguard) { - shadeController.makeExpandedInvisible() - } else { - shadeController.makeExpandedVisible(false) - } - } - } - } - } - - private val isAnyShadeExpanded: StateFlow<Boolean> = - multiShadeInteractor.isAnyShadeExpanded.stateIn( - scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = false, - ) - - private val isBouncerShowing: StateFlow<Boolean> = - keyguardTransitionInteractor - .transitionValue(state = KeyguardState.PRIMARY_BOUNCER) - .map { !it.isZero() } - .stateIn( - scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = false, - ) - - private var interactionState: InteractionState? = null - - /** - * Returns `true` if the given [MotionEvent] and the rest of events in this gesture should be - * passed to this interactor's [onTouchEvent] method. - * - * Note: the caller should continue to pass [MotionEvent] instances into this method, even if it - * returns `false` as the gesture may be intercepted mid-stream. - */ - fun shouldIntercept(event: MotionEvent): Boolean { - if (isAnyShadeExpanded.value) { - // If any shade is expanded, we assume that touch handling outside the shades is handled - // by the scrim that appears behind the shades. No need to intercept anything here. - return false - } - - if (isBouncerShowing.value) { - return false - } - - return when (event.actionMasked) { - MotionEvent.ACTION_DOWN -> { - // Record where the pointer was placed and which pointer it was. - interactionState = - InteractionState( - initialX = event.x, - initialY = event.y, - currentY = event.y, - pointerId = event.getPointerId(0), - isDraggingHorizontally = false, - isDraggingShade = false, - ) - - false - } - MotionEvent.ACTION_MOVE -> { - onMove(event) - - // We want to intercept the rest of the gesture if we're dragging the shade. - isDraggingShade() - } - MotionEvent.ACTION_UP, - MotionEvent.ACTION_CANCEL -> - // Make sure that we intercept the up or cancel if we're dragging the shade, to - // handle drag end or cancel. - isDraggingShade() - else -> false - } - } - - /** - * Notifies that a [MotionEvent] in a series of events of a gesture that was intercepted due to - * the result of [shouldIntercept] has been received. - * - * @param event The [MotionEvent] to handle. - * @param viewWidthPx The width of the view, in pixels. - * @return `true` if the event was consumed, `false` otherwise. - */ - fun onTouchEvent(event: MotionEvent, viewWidthPx: Int): Boolean { - return when (event.actionMasked) { - MotionEvent.ACTION_MOVE -> { - interactionState?.let { - if (it.isDraggingShade) { - val pointerIndex = event.findPointerIndex(it.pointerId) - val previousY = it.currentY - val currentY = event.getY(pointerIndex) - interactionState = it.copy(currentY = currentY) - - val yDragAmountPx = currentY - previousY - - if (yDragAmountPx != 0f) { - multiShadeInteractor.sendProxiedInput( - ProxiedInputModel.OnDrag( - xFraction = event.x / viewWidthPx, - yDragAmountPx = yDragAmountPx, - ) - ) - } - true - } else { - onMove(event) - isDraggingShade() - } - } - ?: false - } - MotionEvent.ACTION_UP -> { - if (isDraggingShade()) { - // We finished dragging the shade. Record that so the multi-shade framework can - // issue a fling, if the velocity reached in the drag was high enough, for - // example. - multiShadeInteractor.sendProxiedInput(ProxiedInputModel.OnDragEnd) - - if (falsingManager.isFalseTouch(Classifier.SHADE_DRAG)) { - multiShadeInteractor.collapseAll() - } - } - - interactionState = null - true - } - MotionEvent.ACTION_POINTER_UP -> { - val removedPointerId = event.getPointerId(event.actionIndex) - if (removedPointerId == interactionState?.pointerId && event.pointerCount > 1) { - // We removed the original pointer but there must be another pointer because the - // gesture is still ongoing. Let's switch to that pointer. - interactionState = - event.firstUnremovedPointerId(removedPointerId)?.let { replacementPointerId - -> - interactionState?.copy( - pointerId = replacementPointerId, - // We want to update the currentY of our state so that the - // transition to the next pointer doesn't report a big jump between - // the Y coordinate of the removed pointer and the Y coordinate of - // the replacement pointer. - currentY = event.getY(replacementPointerId), - ) - } - } - true - } - MotionEvent.ACTION_CANCEL -> { - if (isDraggingShade()) { - // Our drag gesture was canceled by the system. This happens primarily in one of - // two occasions: (a) the parent view has decided to intercept the gesture - // itself and/or route it to a different child view or (b) the pointer has - // traveled beyond the bounds of our view and/or the touch display. Either way, - // we pass the cancellation event to the multi-shade framework to record it. - // Doing that allows the multi-shade framework to know that the gesture ended to - // allow new gestures to be accepted. - multiShadeInteractor.sendProxiedInput(ProxiedInputModel.OnDragCancel) - - if (falsingManager.isFalseTouch(Classifier.SHADE_DRAG)) { - multiShadeInteractor.collapseAll() - } - } - - interactionState = null - true - } - else -> false - } - } - - /** - * Handles [MotionEvent.ACTION_MOVE] and sets whether or not we are dragging shade in our - * current interaction - * - * @param event The [MotionEvent] to handle. - */ - private fun onMove(event: MotionEvent) { - interactionState?.let { - val pointerIndex = event.findPointerIndex(it.pointerId) - val currentX = event.getX(pointerIndex) - val currentY = event.getY(pointerIndex) - if (!it.isDraggingHorizontally && !it.isDraggingShade) { - val xDistanceTravelled = currentX - it.initialX - val yDistanceTravelled = currentY - it.initialY - val touchSlop = ViewConfiguration.get(applicationContext).scaledTouchSlop - interactionState = - when { - yDistanceTravelled > touchSlop -> it.copy(isDraggingShade = true) - abs(xDistanceTravelled) > touchSlop -> - it.copy(isDraggingHorizontally = true) - else -> interactionState - } - } - } - } - - private data class InteractionState( - val initialX: Float, - val initialY: Float, - val currentY: Float, - val pointerId: Int, - /** Whether the current gesture is dragging horizontally. */ - val isDraggingHorizontally: Boolean, - /** Whether the current gesture is dragging the shade vertically. */ - val isDraggingShade: Boolean, - ) - - private fun isDraggingShade(): Boolean { - return interactionState?.isDraggingShade ?: false - } - - /** - * Returns the index of the first pointer that is not [removedPointerId] or `null`, if there is - * no other pointer. - */ - private fun MotionEvent.firstUnremovedPointerId(removedPointerId: Int): Int? { - return (0 until pointerCount) - .firstOrNull { pointerIndex -> - val pointerId = getPointerId(pointerIndex) - pointerId != removedPointerId - } - ?.let { pointerIndex -> getPointerId(pointerIndex) } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/math/Math.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/math/Math.kt deleted file mode 100644 index c2eaf72a841a..000000000000 --- a/packages/SystemUI/src/com/android/systemui/multishade/shared/math/Math.kt +++ /dev/null @@ -1,27 +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.multishade.shared.math - -import androidx.annotation.VisibleForTesting -import kotlin.math.abs - -/** Returns `true` if this [Float] is within [epsilon] of `0`. */ -fun Float.isZero(epsilon: Float = EPSILON): Boolean { - return abs(this) < epsilon -} - -@VisibleForTesting private const val EPSILON = 0.0001f diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ProxiedInputModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ProxiedInputModel.kt deleted file mode 100644 index ee1dd65b867f..000000000000 --- a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ProxiedInputModel.kt +++ /dev/null @@ -1,50 +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.multishade.shared.model - -import androidx.annotation.FloatRange - -/** - * Models a part of an ongoing proxied user input gesture. - * - * "Proxied" user input is coming through a proxy; typically from an external app or different UI. - * In other words: it's not user input that's occurring directly on the shade UI itself. - */ -sealed class ProxiedInputModel { - /** The user is dragging their pointer. */ - data class OnDrag( - /** - * The relative position of the pointer as a fraction of its container width where `0` is - * all the way to the left and `1` is all the way to the right. - */ - @FloatRange(from = 0.0, to = 1.0) val xFraction: Float, - /** The amount that the pointer was dragged, in pixels. */ - val yDragAmountPx: Float, - ) : ProxiedInputModel() - - /** The user finished dragging by lifting up their pointer. */ - object OnDragEnd : ProxiedInputModel() - - /** - * The drag gesture has been canceled. Usually because the pointer exited the draggable area. - */ - object OnDragCancel : ProxiedInputModel() - - /** The user has tapped (clicked). */ - object OnTap : ProxiedInputModel() -} diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeConfig.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeConfig.kt deleted file mode 100644 index a4cd35c8a11a..000000000000 --- a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeConfig.kt +++ /dev/null @@ -1,79 +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.multishade.shared.model - -import androidx.annotation.FloatRange - -/** Enumerates the various possible configurations of the shade system. */ -sealed class ShadeConfig( - - /** IDs of the shade(s) in this configuration. */ - open val shadeIds: List<ShadeId>, - - /** - * The amount that the user must swipe up when the shade is fully expanded to automatically - * collapse once the user lets go of the shade. If the user swipes less than this amount, the - * shade will automatically revert back to fully expanded once the user stops swiping. - */ - @FloatRange(from = 0.0, to = 1.0) open val swipeCollapseThreshold: Float, - - /** - * The amount that the user must swipe down when the shade is fully collapsed to automatically - * expand once the user lets go of the shade. If the user swipes less than this amount, the - * shade will automatically revert back to fully collapsed once the user stops swiping. - */ - @FloatRange(from = 0.0, to = 1.0) open val swipeExpandThreshold: Float, -) { - - /** There is a single shade. */ - data class SingleShadeConfig( - @FloatRange(from = 0.0, to = 1.0) override val swipeCollapseThreshold: Float, - @FloatRange(from = 0.0, to = 1.0) override val swipeExpandThreshold: Float, - ) : - ShadeConfig( - shadeIds = listOf(ShadeId.SINGLE), - swipeCollapseThreshold = swipeCollapseThreshold, - swipeExpandThreshold = swipeExpandThreshold, - ) - - /** There are two shades arranged side-by-side. */ - data class DualShadeConfig( - /** Width of the left-hand side shade. */ - val leftShadeWidthPx: Int, - /** Width of the right-hand side shade. */ - val rightShadeWidthPx: Int, - @FloatRange(from = 0.0, to = 1.0) override val swipeCollapseThreshold: Float, - @FloatRange(from = 0.0, to = 1.0) override val swipeExpandThreshold: Float, - /** - * The position of the "split" between interaction areas for each of the shades, as a - * fraction of the width of the container. - * - * Interactions that occur on the start-side (left-hand side in left-to-right languages like - * English) affect the start-side shade. Interactions that occur on the end-side (right-hand - * side in left-to-right languages like English) affect the end-side shade. - */ - @FloatRange(from = 0.0, to = 1.0) val splitFraction: Float, - /** Maximum opacity when the scrim that shows up behind the dual shades is fully visible. */ - @FloatRange(from = 0.0, to = 1.0) val scrimAlpha: Float, - ) : - ShadeConfig( - shadeIds = listOf(ShadeId.LEFT, ShadeId.RIGHT), - swipeCollapseThreshold = swipeCollapseThreshold, - swipeExpandThreshold = swipeExpandThreshold, - ) -} diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeId.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeId.kt deleted file mode 100644 index 9e026576e842..000000000000 --- a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeId.kt +++ /dev/null @@ -1,28 +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.multishade.shared.model - -/** Enumerates all known shade IDs. */ -enum class ShadeId { - /** ID of the shade on the left in dual shade configurations. */ - LEFT, - /** ID of the shade on the right in dual shade configurations. */ - RIGHT, - /** ID of the single shade in single shade configurations. */ - SINGLE, -} diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt deleted file mode 100644 index 49ac64c58cb8..000000000000 --- a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt +++ /dev/null @@ -1,26 +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.multishade.shared.model - -import androidx.annotation.FloatRange - -/** Models the current state of a shade. */ -data class ShadeModel( - val id: ShadeId, - @FloatRange(from = 0.0, to = 1.0) val expansion: Float = 0f, -) diff --git a/packages/SystemUI/src/com/android/systemui/multishade/ui/view/MultiShadeView.kt b/packages/SystemUI/src/com/android/systemui/multishade/ui/view/MultiShadeView.kt deleted file mode 100644 index aecec39c5c07..000000000000 --- a/packages/SystemUI/src/com/android/systemui/multishade/ui/view/MultiShadeView.kt +++ /dev/null @@ -1,71 +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.multishade.ui.view - -import android.content.Context -import android.util.AttributeSet -import android.widget.FrameLayout -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.compose.ComposeFacade -import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor -import com.android.systemui.multishade.ui.viewmodel.MultiShadeViewModel -import com.android.systemui.util.time.SystemClock -import kotlinx.coroutines.launch - -/** - * View that hosts the multi-shade system and acts as glue between legacy code and the - * implementation. - */ -class MultiShadeView( - context: Context, - attrs: AttributeSet?, -) : - FrameLayout( - context, - attrs, - ) { - - fun init( - interactor: MultiShadeInteractor, - clock: SystemClock, - ) { - repeatWhenAttached { - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - addView( - ComposeFacade.createMultiShadeView( - context = context, - viewModel = - MultiShadeViewModel( - viewModelScope = this, - interactor = interactor, - ), - clock = clock, - ) - ) - } - - // Here when destroyed. - removeAllViews() - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt deleted file mode 100644 index ed92c5469d23..000000000000 --- a/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModel.kt +++ /dev/null @@ -1,108 +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.multishade.ui.viewmodel - -import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor -import com.android.systemui.multishade.shared.model.ProxiedInputModel -import com.android.systemui.multishade.shared.model.ShadeConfig -import com.android.systemui.multishade.shared.model.ShadeId -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 UI state for UI that supports multi (or single) shade. */ -@OptIn(ExperimentalCoroutinesApi::class) -class MultiShadeViewModel( - viewModelScope: CoroutineScope, - private val interactor: MultiShadeInteractor, -) { - /** Models UI state for the single shade. */ - val singleShade = - ShadeViewModel( - viewModelScope, - ShadeId.SINGLE, - interactor, - ) - - /** Models UI state for the shade on the left-hand side. */ - val leftShade = - ShadeViewModel( - viewModelScope, - ShadeId.LEFT, - interactor, - ) - - /** Models UI state for the shade on the right-hand side. */ - val rightShade = - ShadeViewModel( - viewModelScope, - ShadeId.RIGHT, - interactor, - ) - - /** The amount of alpha that the scrim should have. This is a value between `0` and `1`. */ - val scrimAlpha: StateFlow<Float> = - combine( - interactor.maxShadeExpansion, - interactor.shadeConfig - .map { it as? ShadeConfig.DualShadeConfig } - .map { dualShadeConfigOrNull -> dualShadeConfigOrNull?.scrimAlpha ?: 0f }, - ::Pair, - ) - .map { (anyShadeExpansion, scrimAlpha) -> - (anyShadeExpansion * scrimAlpha).coerceIn(0f, 1f) - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = 0f, - ) - - /** Whether the scrim should accept touch events. */ - val isScrimEnabled: StateFlow<Boolean> = - interactor.shadeConfig - .flatMapLatest { shadeConfig -> - when (shadeConfig) { - // In the dual shade configuration, the scrim is enabled when the expansion is - // greater than zero on any one of the shades. - is ShadeConfig.DualShadeConfig -> interactor.isAnyShadeExpanded - // No scrim in the single shade configuration. - is ShadeConfig.SingleShadeConfig -> flowOf(false) - } - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = false, - ) - - /** Notifies that the scrim has been touched. */ - fun onScrimTouched(proxiedInput: ProxiedInputModel) { - if (!isScrimEnabled.value) { - return - } - - interactor.sendProxiedInput(proxiedInput) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModel.kt deleted file mode 100644 index e828dbdc6c62..000000000000 --- a/packages/SystemUI/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModel.kt +++ /dev/null @@ -1,150 +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.multishade.ui.viewmodel - -import androidx.annotation.FloatRange -import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor -import com.android.systemui.multishade.shared.model.ProxiedInputModel -import com.android.systemui.multishade.shared.model.ShadeConfig -import com.android.systemui.multishade.shared.model.ShadeId -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn - -/** Models UI state for a single shade. */ -class ShadeViewModel( - viewModelScope: CoroutineScope, - private val shadeId: ShadeId, - private val interactor: MultiShadeInteractor, -) { - /** Whether the shade is visible. */ - val isVisible: StateFlow<Boolean> = - interactor - .isVisible(shadeId) - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = false, - ) - - /** Whether swiping on the shade UI is currently enabled. */ - val isSwipingEnabled: StateFlow<Boolean> = - interactor - .isNonProxiedInputAllowed(shadeId) - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = false, - ) - - /** Whether the shade must be collapsed immediately. */ - val isForceCollapsed: Flow<Boolean> = - interactor.isForceCollapsed(shadeId).distinctUntilChanged() - - /** The width of the shade. */ - val width: StateFlow<Size> = - interactor.shadeConfig - .map { shadeWidth(it) } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = shadeWidth(interactor.shadeConfig.value), - ) - - /** - * The amount that the user must swipe up when the shade is fully expanded to automatically - * collapse once the user lets go of the shade. If the user swipes less than this amount, the - * shade will automatically revert back to fully expanded once the user stops swiping. - */ - val swipeCollapseThreshold: StateFlow<Float> = - interactor.shadeConfig - .map { it.swipeCollapseThreshold } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = interactor.shadeConfig.value.swipeCollapseThreshold, - ) - - /** - * The amount that the user must swipe down when the shade is fully collapsed to automatically - * expand once the user lets go of the shade. If the user swipes less than this amount, the - * shade will automatically revert back to fully collapsed once the user stops swiping. - */ - val swipeExpandThreshold: StateFlow<Float> = - interactor.shadeConfig - .map { it.swipeExpandThreshold } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = interactor.shadeConfig.value.swipeExpandThreshold, - ) - - /** - * Proxied input affecting the shade. This is input coming from sources outside of system UI - * (for example, swiping down on the Launcher or from the status bar) or outside the UI of any - * shade (for example, the scrim that's shown behind the shades). - */ - val proxiedInput: Flow<ProxiedInputModel?> = - interactor - .proxiedInput(shadeId) - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - initialValue = null, - ) - - /** Notifies that the expansion amount for the shade has changed. */ - fun onExpansionChanged( - expansion: Float, - ) { - interactor.setExpansion(shadeId, expansion.coerceIn(0f, 1f)) - } - - /** Notifies that a drag gesture has started. */ - fun onDragStarted() { - interactor.onUserInteractionStarted(shadeId) - } - - /** Notifies that a drag gesture has ended. */ - fun onDragEnded() { - interactor.onUserInteractionEnded(shadeId = shadeId) - } - - private fun shadeWidth(shadeConfig: ShadeConfig): Size { - return when (shadeId) { - ShadeId.LEFT -> - Size.Pixels((shadeConfig as? ShadeConfig.DualShadeConfig)?.leftShadeWidthPx ?: 0) - ShadeId.RIGHT -> - Size.Pixels((shadeConfig as? ShadeConfig.DualShadeConfig)?.rightShadeWidthPx ?: 0) - ShadeId.SINGLE -> Size.Fraction(1f) - } - } - - sealed class Size { - data class Fraction( - @FloatRange(from = 0.0, to = 1.0) val fraction: Float, - ) : Size() - data class Pixels( - val pixels: Int, - ) : Size() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java index 682335e0b419..e134f7c10b9b 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java @@ -133,6 +133,7 @@ import com.android.systemui.settings.DisplayTracker; import com.android.systemui.settings.UserContextProvider; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeController; +import com.android.systemui.shade.ShadeViewController; import com.android.systemui.shared.navigationbar.RegionSamplingHelper; import com.android.systemui.shared.recents.utilities.Utilities; import com.android.systemui.shared.rotation.RotationButton; @@ -199,6 +200,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements private final SysUiState mSysUiFlagsContainer; private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy; private final ShadeController mShadeController; + private final ShadeViewController mShadeViewController; private final NotificationRemoteInputManager mNotificationRemoteInputManager; private final OverviewProxyService mOverviewProxyService; private final NavigationModeController mNavigationModeController; @@ -523,6 +525,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements @Inject NavigationBar( NavigationBarView navigationBarView, + ShadeController shadeController, NavigationBarFrame navigationBarFrame, @Nullable Bundle savedState, @DisplayId Context context, @@ -541,7 +544,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements Optional<Pip> pipOptional, Optional<Recents> recentsOptional, Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy, - ShadeController shadeController, + ShadeViewController shadeViewController, NotificationRemoteInputManager notificationRemoteInputManager, NotificationShadeDepthController notificationShadeDepthController, @Main Handler mainHandler, @@ -577,6 +580,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements mSysUiFlagsContainer = sysUiFlagsContainer; mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy; mShadeController = shadeController; + mShadeViewController = shadeViewController; mNotificationRemoteInputManager = notificationRemoteInputManager; mOverviewProxyService = overviewProxyService; mNavigationModeController = navigationModeController; @@ -739,8 +743,7 @@ public class NavigationBar extends ViewController<NavigationBarView> implements final Display display = mView.getDisplay(); mView.setComponents(mRecentsOptional); if (mCentralSurfacesOptionalLazy.get().isPresent()) { - mView.setComponents( - mCentralSurfacesOptionalLazy.get().get().getShadeViewController()); + mView.setComponents(mShadeViewController); } mView.setDisabledFlags(mDisabledFlags1, mSysUiFlagsContainer); mView.setOnVerticalChangedListener(this::onVerticalChanged); @@ -1341,9 +1344,10 @@ public class NavigationBar extends ViewController<NavigationBarView> implements } private void onVerticalChanged(boolean isVertical) { - Optional<CentralSurfaces> cs = mCentralSurfacesOptionalLazy.get(); - if (cs.isPresent() && cs.get().getShadeViewController() != null) { - cs.get().getShadeViewController().setQsScrimEnabled(!isVertical); + // This check can probably be safely removed. It only remained to reduce regression + // risk for a broad change that removed the CentralSurfaces reference in the if block + if (mCentralSurfacesOptionalLazy.get().isPresent()) { + mShadeViewController.setQsScrimEnabled(!isVertical); } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt index 77e2847cbe76..c4749e093854 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt @@ -26,6 +26,7 @@ import android.os.VibrationEffect import android.util.Log import android.util.MathUtils import android.view.Gravity +import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.VelocityTracker import android.view.ViewConfiguration @@ -36,6 +37,8 @@ import androidx.core.view.isVisible import androidx.dynamicanimation.animation.DynamicAnimation import com.android.internal.util.LatencyTracker import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION import com.android.systemui.plugins.NavigationEdgeBackPlugin import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController @@ -76,27 +79,24 @@ private const val POP_ON_INACTIVE_TO_ACTIVE_VELOCITY = 4.7f private const val POP_ON_INACTIVE_VELOCITY = -1.5f internal val VIBRATE_ACTIVATED_EFFECT = - VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK) + VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK) internal val VIBRATE_DEACTIVATED_EFFECT = - VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK) + VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK) private const val DEBUG = false -class BackPanelController internal constructor( - context: Context, - private val windowManager: WindowManager, - private val viewConfiguration: ViewConfiguration, - @Main private val mainHandler: Handler, - private val vibratorHelper: VibratorHelper, - private val configurationController: ConfigurationController, - private val latencyTracker: LatencyTracker -) : ViewController<BackPanel>( - BackPanel( - context, - latencyTracker - ) -), NavigationEdgeBackPlugin { +class BackPanelController +internal constructor( + context: Context, + private val windowManager: WindowManager, + private val viewConfiguration: ViewConfiguration, + @Main private val mainHandler: Handler, + private val vibratorHelper: VibratorHelper, + private val configurationController: ConfigurationController, + private val latencyTracker: LatencyTracker, + private val featureFlags: FeatureFlags +) : ViewController<BackPanel>(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin { /** * Injectable instance to create a new BackPanelController. @@ -104,34 +104,37 @@ class BackPanelController internal constructor( * Necessary because EdgeBackGestureHandler sometimes needs to create new instances of * BackPanelController, and we need to match EdgeBackGestureHandler's context. */ - class Factory @Inject constructor( - private val windowManager: WindowManager, - private val viewConfiguration: ViewConfiguration, - @Main private val mainHandler: Handler, - private val vibratorHelper: VibratorHelper, - private val configurationController: ConfigurationController, - private val latencyTracker: LatencyTracker + class Factory + @Inject + constructor( + private val windowManager: WindowManager, + private val viewConfiguration: ViewConfiguration, + @Main private val mainHandler: Handler, + private val vibratorHelper: VibratorHelper, + private val configurationController: ConfigurationController, + private val latencyTracker: LatencyTracker, + private val featureFlags: FeatureFlags ) { - /** Construct a [BackPanelController]. */ + /** Construct a [BackPanelController]. */ fun create(context: Context): BackPanelController { - val backPanelController = BackPanelController( + val backPanelController = + BackPanelController( context, windowManager, viewConfiguration, mainHandler, vibratorHelper, configurationController, - latencyTracker - ) + latencyTracker, + featureFlags + ) backPanelController.init() return backPanelController } } - @VisibleForTesting - internal var params: EdgePanelParams = EdgePanelParams(resources) - @VisibleForTesting - internal var currentState: GestureState = GestureState.GONE + @VisibleForTesting internal var params: EdgePanelParams = EdgePanelParams(resources) + @VisibleForTesting internal var currentState: GestureState = GestureState.GONE private var previousState: GestureState = GestureState.GONE // Screen attributes @@ -167,7 +170,6 @@ class BackPanelController internal constructor( private val elapsedTimeSinceEntry get() = SystemClock.uptimeMillis() - gestureEntryTime - private var pastThresholdWhileEntryOrInactiveTime = 0L private var entryToActiveDelay = 0F private val entryToActiveDelayCalculation = { @@ -206,24 +208,25 @@ class BackPanelController internal constructor( COMMITTED, /* back action currently cancelling, arrow soon to be GONE */ - CANCELLED; + CANCELLED } /** * Wrapper around OnAnimationEndListener which runs the given runnable after a delay. The * runnable is not called if the animation is cancelled */ - inner class DelayedOnAnimationEndListener internal constructor( - private val handler: Handler, - private val runnableDelay: Long, - val runnable: Runnable, + inner class DelayedOnAnimationEndListener + internal constructor( + private val handler: Handler, + private val runnableDelay: Long, + val runnable: Runnable, ) : DynamicAnimation.OnAnimationEndListener { override fun onAnimationEnd( - animation: DynamicAnimation<*>, - canceled: Boolean, - value: Float, - velocity: Float + animation: DynamicAnimation<*>, + canceled: Boolean, + value: Float, + velocity: Float ) { animation.removeEndListener(this) @@ -239,45 +242,43 @@ class BackPanelController internal constructor( internal fun run() = runnable.run() } - private val onEndSetCommittedStateListener = DelayedOnAnimationEndListener(mainHandler, 0L) { - updateArrowState(GestureState.COMMITTED) - } - + private val onEndSetCommittedStateListener = + DelayedOnAnimationEndListener(mainHandler, 0L) { updateArrowState(GestureState.COMMITTED) } private val onEndSetGoneStateListener = - DelayedOnAnimationEndListener(mainHandler, runnableDelay = 0L) { - cancelFailsafe() - updateArrowState(GestureState.GONE) - } + DelayedOnAnimationEndListener(mainHandler, runnableDelay = 0L) { + cancelFailsafe() + updateArrowState(GestureState.GONE) + } - private val onAlphaEndSetGoneStateListener = DelayedOnAnimationEndListener(mainHandler, 0L) { - updateRestingArrowDimens() - if (!mView.addAnimationEndListener(mView.backgroundAlpha, onEndSetGoneStateListener)) { - scheduleFailsafe() + private val onAlphaEndSetGoneStateListener = + DelayedOnAnimationEndListener(mainHandler, 0L) { + updateRestingArrowDimens() + if (!mView.addAnimationEndListener(mView.backgroundAlpha, onEndSetGoneStateListener)) { + scheduleFailsafe() + } } - } // Minimum of the screen's width or the predefined threshold private var fullyStretchedThreshold = 0f - /** - * Used for initialization and configuration changes - */ + /** Used for initialization and configuration changes */ private fun updateConfiguration() { params.update(resources) mView.updateArrowPaint(params.arrowThickness) minFlingDistance = viewConfiguration.scaledTouchSlop * 3 } - private val configurationListener = object : ConfigurationController.ConfigurationListener { - override fun onConfigChanged(newConfig: Configuration?) { - updateConfiguration() - } + private val configurationListener = + object : ConfigurationController.ConfigurationListener { + override fun onConfigChanged(newConfig: Configuration?) { + updateConfiguration() + } - override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) { - updateArrowDirection(isLayoutRtl) + override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) { + updateArrowDirection(isLayoutRtl) + } } - } override fun onViewAttached() { updateConfiguration() @@ -320,8 +321,9 @@ class BackPanelController internal constructor( MotionEvent.ACTION_UP -> { when (currentState) { GestureState.ENTRY -> { - if (isFlungAwayFromEdge(endX = event.x) || - previousXTranslation > params.staticTriggerThreshold + if ( + isFlungAwayFromEdge(endX = event.x) || + previousXTranslation > params.staticTriggerThreshold ) { updateArrowState(GestureState.FLUNG) } else { @@ -342,14 +344,16 @@ class BackPanelController internal constructor( } } GestureState.ACTIVE -> { - if (previousState == GestureState.ENTRY && - elapsedTimeSinceEntry - < MIN_DURATION_ENTRY_TO_ACTIVE_CONSIDERED_AS_FLING + if ( + previousState == GestureState.ENTRY && + elapsedTimeSinceEntry < + MIN_DURATION_ENTRY_TO_ACTIVE_CONSIDERED_AS_FLING ) { updateArrowState(GestureState.FLUNG) - } else if (previousState == GestureState.INACTIVE && - elapsedTimeSinceInactive - < MIN_DURATION_INACTIVE_TO_ACTIVE_CONSIDERED_AS_FLING + } else if ( + previousState == GestureState.INACTIVE && + elapsedTimeSinceInactive < + MIN_DURATION_INACTIVE_TO_ACTIVE_CONSIDERED_AS_FLING ) { // A delay is added to allow the background to transition back to ACTIVE // since it was briefly in INACTIVE. Without this delay, setting it @@ -390,10 +394,10 @@ class BackPanelController internal constructor( } /** - * Returns false until the current gesture exceeds the touch slop threshold, - * and returns true thereafter (we reset on the subsequent back gesture). - * The moment it switches from false -> true is important, - * because that's when we switch state, from GONE -> ENTRY. + * Returns false until the current gesture exceeds the touch slop threshold, and returns true + * thereafter (we reset on the subsequent back gesture). The moment it switches from false -> + * true is important, because that's when we switch state, from GONE -> ENTRY. + * * @return whether the current gesture has moved past a minimum threshold. */ private fun dragSlopExceeded(curX: Float, startX: Float): Boolean { @@ -416,7 +420,8 @@ class BackPanelController internal constructor( val isPastStaticThreshold = xTranslation > params.staticTriggerThreshold when (currentState) { GestureState.ENTRY -> { - if (isPastThresholdToActive( + if ( + isPastThresholdToActive( isPastThreshold = isPastStaticThreshold, dynamicDelay = entryToActiveDelayCalculation ) @@ -428,8 +433,10 @@ class BackPanelController internal constructor( val isPastDynamicReactivationThreshold = totalTouchDeltaInactive >= params.reactivationTriggerThreshold - if (isPastThresholdToActive( - isPastThreshold = isPastStaticThreshold && + if ( + isPastThresholdToActive( + isPastThreshold = + isPastStaticThreshold && isPastDynamicReactivationThreshold && isWithinYActivationThreshold, delay = MIN_DURATION_INACTIVE_BEFORE_ACTIVE_ANIMATION @@ -489,19 +496,19 @@ class BackPanelController internal constructor( // Add a slop to to prevent small jitters when arrow is at edge in // emitting small values that cause the arrow to poke out slightly val minimumDelta = -viewConfiguration.scaledTouchSlop.toFloat() - totalTouchDeltaInactive = totalTouchDeltaInactive - .plus(xDelta) - .coerceAtLeast(minimumDelta) + totalTouchDeltaInactive = + totalTouchDeltaInactive.plus(xDelta).coerceAtLeast(minimumDelta) } updateArrowStateOnMove(yTranslation, xTranslation) - val gestureProgress = when (currentState) { - GestureState.ACTIVE -> fullScreenProgress(xTranslation) - GestureState.ENTRY -> staticThresholdProgress(xTranslation) - GestureState.INACTIVE -> reactivationThresholdProgress(totalTouchDeltaInactive) - else -> null - } + val gestureProgress = + when (currentState) { + GestureState.ACTIVE -> fullScreenProgress(xTranslation) + GestureState.ENTRY -> staticThresholdProgress(xTranslation) + GestureState.INACTIVE -> reactivationThresholdProgress(totalTouchDeltaInactive) + else -> null + } gestureProgress?.let { when (currentState) { @@ -517,27 +524,30 @@ class BackPanelController internal constructor( } private fun setArrowStrokeAlpha(gestureProgress: Float?) { - val strokeAlphaProgress = when (currentState) { - GestureState.ENTRY -> gestureProgress - GestureState.INACTIVE -> gestureProgress - GestureState.ACTIVE, - GestureState.FLUNG, - GestureState.COMMITTED -> 1f - GestureState.CANCELLED, - GestureState.GONE -> 0f - } + val strokeAlphaProgress = + when (currentState) { + GestureState.ENTRY -> gestureProgress + GestureState.INACTIVE -> gestureProgress + GestureState.ACTIVE, + GestureState.FLUNG, + GestureState.COMMITTED -> 1f + GestureState.CANCELLED, + GestureState.GONE -> 0f + } - val indicator = when (currentState) { - GestureState.ENTRY -> params.entryIndicator - GestureState.INACTIVE -> params.preThresholdIndicator - GestureState.ACTIVE -> params.activeIndicator - else -> params.preThresholdIndicator - } + val indicator = + when (currentState) { + GestureState.ENTRY -> params.entryIndicator + GestureState.INACTIVE -> params.preThresholdIndicator + GestureState.ACTIVE -> params.activeIndicator + else -> params.preThresholdIndicator + } strokeAlphaProgress?.let { progress -> - indicator.arrowDimens.alphaSpring?.get(progress)?.takeIf { it.isNewState }?.let { - mView.popArrowAlpha(0f, it.value) - } + indicator.arrowDimens.alphaSpring + ?.get(progress) + ?.takeIf { it.isNewState } + ?.let { mView.popArrowAlpha(0f, it.value) } } } @@ -546,15 +556,16 @@ class BackPanelController internal constructor( val maxYOffset = (mView.height - params.entryIndicator.backgroundDimens.height) / 2f val rubberbandAmount = 15f val yProgress = MathUtils.saturate(yTranslation / (maxYOffset * rubberbandAmount)) - val yPosition = params.verticalTranslationInterpolator.getInterpolation(yProgress) * + val yPosition = + params.verticalTranslationInterpolator.getInterpolation(yProgress) * maxYOffset * sign(yOffset) mView.animateVertically(yPosition) } /** - * Tracks the relative position of the drag from the time after the arrow is activated until - * the arrow is fully stretched (between 0.0 - 1.0f) + * Tracks the relative position of the drag from the time after the arrow is activated until the + * arrow is fully stretched (between 0.0 - 1.0f) */ private fun fullScreenProgress(xTranslation: Float): Float { val progress = (xTranslation - previousXTranslationOnActiveOffset) / fullyStretchedThreshold @@ -575,35 +586,32 @@ class BackPanelController internal constructor( private fun stretchActiveBackIndicator(progress: Float) { mView.setStretch( - horizontalTranslationStretchAmount = params.horizontalTranslationInterpolator - .getInterpolation(progress), - arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress), - backgroundWidthStretchAmount = params.activeWidthInterpolator - .getInterpolation(progress), - backgroundAlphaStretchAmount = 1f, - backgroundHeightStretchAmount = 1f, - arrowAlphaStretchAmount = 1f, - edgeCornerStretchAmount = 1f, - farCornerStretchAmount = 1f, - fullyStretchedDimens = params.fullyStretchedIndicator + horizontalTranslationStretchAmount = + params.horizontalTranslationInterpolator.getInterpolation(progress), + arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress), + backgroundWidthStretchAmount = + params.activeWidthInterpolator.getInterpolation(progress), + backgroundAlphaStretchAmount = 1f, + backgroundHeightStretchAmount = 1f, + arrowAlphaStretchAmount = 1f, + edgeCornerStretchAmount = 1f, + farCornerStretchAmount = 1f, + fullyStretchedDimens = params.fullyStretchedIndicator ) } private fun stretchEntryBackIndicator(progress: Float) { mView.setStretch( - horizontalTranslationStretchAmount = 0f, - arrowStretchAmount = params.arrowAngleInterpolator - .getInterpolation(progress), - backgroundWidthStretchAmount = params.entryWidthInterpolator - .getInterpolation(progress), - backgroundHeightStretchAmount = params.heightInterpolator - .getInterpolation(progress), - backgroundAlphaStretchAmount = 1f, - arrowAlphaStretchAmount = params.entryIndicator.arrowDimens - .alphaInterpolator?.get(progress)?.value ?: 0f, - edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress), - farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress), - fullyStretchedDimens = params.preThresholdIndicator + horizontalTranslationStretchAmount = 0f, + arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress), + backgroundWidthStretchAmount = params.entryWidthInterpolator.getInterpolation(progress), + backgroundHeightStretchAmount = params.heightInterpolator.getInterpolation(progress), + backgroundAlphaStretchAmount = 1f, + arrowAlphaStretchAmount = + params.entryIndicator.arrowDimens.alphaInterpolator?.get(progress)?.value ?: 0f, + edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress), + farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress), + fullyStretchedDimens = params.preThresholdIndicator ) } @@ -612,31 +620,32 @@ class BackPanelController internal constructor( val interpolator = run { val isPastSlop = totalTouchDeltaInactive > viewConfiguration.scaledTouchSlop if (isPastSlop) { - if (totalTouchDeltaInactive > 0) { - params.entryWidthInterpolator + if (totalTouchDeltaInactive > 0) { + params.entryWidthInterpolator + } else { + params.entryWidthTowardsEdgeInterpolator + } } else { - params.entryWidthTowardsEdgeInterpolator + previousPreThresholdWidthInterpolator } - } else { - previousPreThresholdWidthInterpolator - }.also { previousPreThresholdWidthInterpolator = it } + .also { previousPreThresholdWidthInterpolator = it } } return interpolator.getInterpolation(progress).coerceAtLeast(0f) } private fun stretchInactiveBackIndicator(progress: Float) { mView.setStretch( - horizontalTranslationStretchAmount = 0f, - arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress), - backgroundWidthStretchAmount = preThresholdWidthStretchAmount(progress), - backgroundHeightStretchAmount = params.heightInterpolator - .getInterpolation(progress), - backgroundAlphaStretchAmount = 1f, - arrowAlphaStretchAmount = params.preThresholdIndicator.arrowDimens - .alphaInterpolator?.get(progress)?.value ?: 0f, - edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress), - farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress), - fullyStretchedDimens = params.preThresholdIndicator + horizontalTranslationStretchAmount = 0f, + arrowStretchAmount = params.arrowAngleInterpolator.getInterpolation(progress), + backgroundWidthStretchAmount = preThresholdWidthStretchAmount(progress), + backgroundHeightStretchAmount = params.heightInterpolator.getInterpolation(progress), + backgroundAlphaStretchAmount = 1f, + arrowAlphaStretchAmount = + params.preThresholdIndicator.arrowDimens.alphaInterpolator?.get(progress)?.value + ?: 0f, + edgeCornerStretchAmount = params.edgeCornerInterpolator.getInterpolation(progress), + farCornerStretchAmount = params.farCornerInterpolator.getInterpolation(progress), + fullyStretchedDimens = params.preThresholdIndicator ) } @@ -647,11 +656,12 @@ class BackPanelController internal constructor( override fun setIsLeftPanel(isLeftPanel: Boolean) { mView.isLeftPanel = isLeftPanel - layoutParams.gravity = if (isLeftPanel) { - Gravity.LEFT or Gravity.TOP - } else { - Gravity.RIGHT or Gravity.TOP - } + layoutParams.gravity = + if (isLeftPanel) { + Gravity.LEFT or Gravity.TOP + } else { + Gravity.RIGHT or Gravity.TOP + } } override fun setInsets(insetLeft: Int, insetRight: Int) = Unit @@ -667,12 +677,14 @@ class BackPanelController internal constructor( private fun isFlungAwayFromEdge(endX: Float, startX: Float = touchDeltaStartX): Boolean { val flingDistance = if (mView.isLeftPanel) endX - startX else startX - endX - val flingVelocity = velocityTracker?.run { - computeCurrentVelocity(PX_PER_SEC) - xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1) - } ?: 0f + val flingVelocity = + velocityTracker?.run { + computeCurrentVelocity(PX_PER_SEC) + xVelocity.takeIf { mView.isLeftPanel } ?: (xVelocity * -1) + } + ?: 0f val isPastFlingVelocityThreshold = - flingVelocity > viewConfiguration.scaledMinimumFlingVelocity + flingVelocity > viewConfiguration.scaledMinimumFlingVelocity return flingDistance > minFlingDistance && isPastFlingVelocityThreshold } @@ -699,8 +711,8 @@ class BackPanelController internal constructor( } private fun playWithBackgroundWidthAnimation( - onEnd: DelayedOnAnimationEndListener, - delay: Long = 0L + onEnd: DelayedOnAnimationEndListener, + delay: Long = 0L ) { if (delay == 0L) { updateRestingArrowDimens() @@ -724,104 +736,103 @@ class BackPanelController internal constructor( fullyStretchedThreshold = min(displaySize.x.toFloat(), params.swipeProgressThreshold) } - /** - * Updates resting arrow and background size not accounting for stretch - */ + /** Updates resting arrow and background size not accounting for stretch */ private fun updateRestingArrowDimens() { when (currentState) { GestureState.GONE, GestureState.ENTRY -> { mView.setSpring( - arrowLength = params.entryIndicator.arrowDimens.lengthSpring, - arrowHeight = params.entryIndicator.arrowDimens.heightSpring, - scale = params.entryIndicator.scaleSpring, - verticalTranslation = params.entryIndicator.verticalTranslationSpring, - horizontalTranslation = params.entryIndicator.horizontalTranslationSpring, - backgroundAlpha = params.entryIndicator.backgroundDimens.alphaSpring, - backgroundWidth = params.entryIndicator.backgroundDimens.widthSpring, - backgroundHeight = params.entryIndicator.backgroundDimens.heightSpring, - backgroundEdgeCornerRadius = params.entryIndicator.backgroundDimens - .edgeCornerRadiusSpring, - backgroundFarCornerRadius = params.entryIndicator.backgroundDimens - .farCornerRadiusSpring, + arrowLength = params.entryIndicator.arrowDimens.lengthSpring, + arrowHeight = params.entryIndicator.arrowDimens.heightSpring, + scale = params.entryIndicator.scaleSpring, + verticalTranslation = params.entryIndicator.verticalTranslationSpring, + horizontalTranslation = params.entryIndicator.horizontalTranslationSpring, + backgroundAlpha = params.entryIndicator.backgroundDimens.alphaSpring, + backgroundWidth = params.entryIndicator.backgroundDimens.widthSpring, + backgroundHeight = params.entryIndicator.backgroundDimens.heightSpring, + backgroundEdgeCornerRadius = + params.entryIndicator.backgroundDimens.edgeCornerRadiusSpring, + backgroundFarCornerRadius = + params.entryIndicator.backgroundDimens.farCornerRadiusSpring, ) } GestureState.INACTIVE -> { mView.setSpring( - arrowLength = params.preThresholdIndicator.arrowDimens.lengthSpring, - arrowHeight = params.preThresholdIndicator.arrowDimens.heightSpring, - horizontalTranslation = params.preThresholdIndicator - .horizontalTranslationSpring, - scale = params.preThresholdIndicator.scaleSpring, - backgroundWidth = params.preThresholdIndicator.backgroundDimens - .widthSpring, - backgroundHeight = params.preThresholdIndicator.backgroundDimens - .heightSpring, - backgroundEdgeCornerRadius = params.preThresholdIndicator.backgroundDimens - .edgeCornerRadiusSpring, - backgroundFarCornerRadius = params.preThresholdIndicator.backgroundDimens - .farCornerRadiusSpring, + arrowLength = params.preThresholdIndicator.arrowDimens.lengthSpring, + arrowHeight = params.preThresholdIndicator.arrowDimens.heightSpring, + horizontalTranslation = + params.preThresholdIndicator.horizontalTranslationSpring, + scale = params.preThresholdIndicator.scaleSpring, + backgroundWidth = params.preThresholdIndicator.backgroundDimens.widthSpring, + backgroundHeight = params.preThresholdIndicator.backgroundDimens.heightSpring, + backgroundEdgeCornerRadius = + params.preThresholdIndicator.backgroundDimens.edgeCornerRadiusSpring, + backgroundFarCornerRadius = + params.preThresholdIndicator.backgroundDimens.farCornerRadiusSpring, ) } GestureState.ACTIVE -> { mView.setSpring( - arrowLength = params.activeIndicator.arrowDimens.lengthSpring, - arrowHeight = params.activeIndicator.arrowDimens.heightSpring, - scale = params.activeIndicator.scaleSpring, - horizontalTranslation = params.activeIndicator.horizontalTranslationSpring, - backgroundWidth = params.activeIndicator.backgroundDimens.widthSpring, - backgroundHeight = params.activeIndicator.backgroundDimens.heightSpring, - backgroundEdgeCornerRadius = params.activeIndicator.backgroundDimens - .edgeCornerRadiusSpring, - backgroundFarCornerRadius = params.activeIndicator.backgroundDimens - .farCornerRadiusSpring, + arrowLength = params.activeIndicator.arrowDimens.lengthSpring, + arrowHeight = params.activeIndicator.arrowDimens.heightSpring, + scale = params.activeIndicator.scaleSpring, + horizontalTranslation = params.activeIndicator.horizontalTranslationSpring, + backgroundWidth = params.activeIndicator.backgroundDimens.widthSpring, + backgroundHeight = params.activeIndicator.backgroundDimens.heightSpring, + backgroundEdgeCornerRadius = + params.activeIndicator.backgroundDimens.edgeCornerRadiusSpring, + backgroundFarCornerRadius = + params.activeIndicator.backgroundDimens.farCornerRadiusSpring, ) } GestureState.FLUNG -> { mView.setSpring( - arrowLength = params.flungIndicator.arrowDimens.lengthSpring, - arrowHeight = params.flungIndicator.arrowDimens.heightSpring, - backgroundWidth = params.flungIndicator.backgroundDimens.widthSpring, - backgroundHeight = params.flungIndicator.backgroundDimens.heightSpring, - backgroundEdgeCornerRadius = params.flungIndicator.backgroundDimens - .edgeCornerRadiusSpring, - backgroundFarCornerRadius = params.flungIndicator.backgroundDimens - .farCornerRadiusSpring, + arrowLength = params.flungIndicator.arrowDimens.lengthSpring, + arrowHeight = params.flungIndicator.arrowDimens.heightSpring, + backgroundWidth = params.flungIndicator.backgroundDimens.widthSpring, + backgroundHeight = params.flungIndicator.backgroundDimens.heightSpring, + backgroundEdgeCornerRadius = + params.flungIndicator.backgroundDimens.edgeCornerRadiusSpring, + backgroundFarCornerRadius = + params.flungIndicator.backgroundDimens.farCornerRadiusSpring, ) } GestureState.COMMITTED -> { mView.setSpring( - arrowLength = params.committedIndicator.arrowDimens.lengthSpring, - arrowHeight = params.committedIndicator.arrowDimens.heightSpring, - scale = params.committedIndicator.scaleSpring, - backgroundAlpha = params.committedIndicator.backgroundDimens.alphaSpring, - backgroundWidth = params.committedIndicator.backgroundDimens.widthSpring, - backgroundHeight = params.committedIndicator.backgroundDimens.heightSpring, - backgroundEdgeCornerRadius = params.committedIndicator.backgroundDimens - .edgeCornerRadiusSpring, - backgroundFarCornerRadius = params.committedIndicator.backgroundDimens - .farCornerRadiusSpring, + arrowLength = params.committedIndicator.arrowDimens.lengthSpring, + arrowHeight = params.committedIndicator.arrowDimens.heightSpring, + scale = params.committedIndicator.scaleSpring, + backgroundAlpha = params.committedIndicator.backgroundDimens.alphaSpring, + backgroundWidth = params.committedIndicator.backgroundDimens.widthSpring, + backgroundHeight = params.committedIndicator.backgroundDimens.heightSpring, + backgroundEdgeCornerRadius = + params.committedIndicator.backgroundDimens.edgeCornerRadiusSpring, + backgroundFarCornerRadius = + params.committedIndicator.backgroundDimens.farCornerRadiusSpring, ) } GestureState.CANCELLED -> { mView.setSpring( - backgroundAlpha = params.cancelledIndicator.backgroundDimens.alphaSpring) + backgroundAlpha = params.cancelledIndicator.backgroundDimens.alphaSpring + ) } else -> {} } mView.setRestingDimens( - animate = !(currentState == GestureState.FLUNG || - currentState == GestureState.COMMITTED), - restingParams = EdgePanelParams.BackIndicatorDimens( - scale = when (currentState) { + animate = + !(currentState == GestureState.FLUNG || currentState == GestureState.COMMITTED), + restingParams = + EdgePanelParams.BackIndicatorDimens( + scale = + when (currentState) { GestureState.ACTIVE, - GestureState.FLUNG, - -> params.activeIndicator.scale + GestureState.FLUNG, -> params.activeIndicator.scale GestureState.COMMITTED -> params.committedIndicator.scale else -> params.preThresholdIndicator.scale }, - scalePivotX = when (currentState) { + scalePivotX = + when (currentState) { GestureState.GONE, GestureState.ENTRY, GestureState.INACTIVE, @@ -830,7 +841,8 @@ class BackPanelController internal constructor( GestureState.FLUNG, GestureState.COMMITTED -> params.committedIndicator.scalePivotX }, - horizontalTranslation = when (currentState) { + horizontalTranslation = + when (currentState) { GestureState.GONE -> { params.activeIndicator.backgroundDimens.width?.times(-1) } @@ -843,7 +855,8 @@ class BackPanelController internal constructor( } else -> null }, - arrowDimens = when (currentState) { + arrowDimens = + when (currentState) { GestureState.GONE, GestureState.ENTRY, GestureState.INACTIVE -> params.entryIndicator.arrowDimens @@ -852,7 +865,8 @@ class BackPanelController internal constructor( GestureState.COMMITTED -> params.committedIndicator.arrowDimens GestureState.CANCELLED -> params.cancelledIndicator.arrowDimens }, - backgroundDimens = when (currentState) { + backgroundDimens = + when (currentState) { GestureState.GONE, GestureState.ENTRY, GestureState.INACTIVE -> params.entryIndicator.backgroundDimens @@ -894,7 +908,7 @@ class BackPanelController internal constructor( GestureState.ACTIVE -> { backCallback.setTriggerBack(true) } - GestureState.GONE -> { } + GestureState.GONE -> {} } when (currentState) { @@ -913,18 +927,25 @@ class BackPanelController internal constructor( GestureState.ACTIVE -> { previousXTranslationOnActiveOffset = previousXTranslation updateRestingArrowDimens() - vibratorHelper.cancel() - mainHandler.postDelayed(10L) { - vibratorHelper.vibrate(VIBRATE_ACTIVATED_EFFECT) - } - val popVelocity = if (previousState == GestureState.INACTIVE) { - POP_ON_INACTIVE_TO_ACTIVE_VELOCITY + if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { + vibratorHelper.performHapticFeedback( + mView, + HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE + ) } else { - POP_ON_ENTRY_TO_ACTIVE_VELOCITY + vibratorHelper.cancel() + mainHandler.postDelayed(10L) { + vibratorHelper.vibrate(VIBRATE_ACTIVATED_EFFECT) + } } + val popVelocity = + if (previousState == GestureState.INACTIVE) { + POP_ON_INACTIVE_TO_ACTIVE_VELOCITY + } else { + POP_ON_ENTRY_TO_ACTIVE_VELOCITY + } mView.popOffEdge(popVelocity) } - GestureState.INACTIVE -> { gestureInactiveTime = SystemClock.uptimeMillis() @@ -937,7 +958,14 @@ class BackPanelController internal constructor( mView.popOffEdge(POP_ON_INACTIVE_VELOCITY) - vibratorHelper.vibrate(VIBRATE_DEACTIVATED_EFFECT) + if (featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { + vibratorHelper.performHapticFeedback( + mView, + HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE + ) + } else { + vibratorHelper.vibrate(VIBRATE_DEACTIVATED_EFFECT) + } updateRestingArrowDimens() } GestureState.FLUNG -> { @@ -945,8 +973,10 @@ class BackPanelController internal constructor( mView.popScale(POP_ON_FLING_VELOCITY) } updateRestingArrowDimens() - mainHandler.postDelayed(onEndSetCommittedStateListener.runnable, - MIN_DURATION_FLING_ANIMATION) + mainHandler.postDelayed( + onEndSetCommittedStateListener.runnable, + MIN_DURATION_FLING_ANIMATION + ) } GestureState.COMMITTED -> { // In most cases, animating between states is handled via `updateRestingArrowDimens` @@ -956,36 +986,43 @@ class BackPanelController internal constructor( // manually play these kinds of animations in parallel. if (previousState == GestureState.FLUNG) { updateRestingArrowDimens() - mainHandler.postDelayed(onEndSetGoneStateListener.runnable, - MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION) + mainHandler.postDelayed( + onEndSetGoneStateListener.runnable, + MIN_DURATION_COMMITTED_AFTER_FLING_ANIMATION + ) } else { mView.popScale(POP_ON_COMMITTED_VELOCITY) - mainHandler.postDelayed(onAlphaEndSetGoneStateListener.runnable, - MIN_DURATION_COMMITTED_ANIMATION) + mainHandler.postDelayed( + onAlphaEndSetGoneStateListener.runnable, + MIN_DURATION_COMMITTED_ANIMATION + ) } } GestureState.CANCELLED -> { val delay = max(0, MIN_DURATION_CANCELLED_ANIMATION - elapsedTimeSinceEntry) playWithBackgroundWidthAnimation(onEndSetGoneStateListener, delay) - val springForceOnCancelled = params.cancelledIndicator - .arrowDimens.alphaSpring?.get(0f)?.value + val springForceOnCancelled = + params.cancelledIndicator.arrowDimens.alphaSpring?.get(0f)?.value mView.popArrowAlpha(0f, springForceOnCancelled) - mainHandler.postDelayed(10L) { vibratorHelper.cancel() } + if (!featureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) + mainHandler.postDelayed(10L) { vibratorHelper.cancel() } } } } private fun convertVelocityToAnimationFactor( - valueOnFastVelocity: Float, - valueOnSlowVelocity: Float, - fastVelocityBound: Float = 1f, - slowVelocityBound: Float = 0.5f, + valueOnFastVelocity: Float, + valueOnSlowVelocity: Float, + fastVelocityBound: Float = 1f, + slowVelocityBound: Float = 0.5f, ): Float { - val factor = velocityTracker?.run { - computeCurrentVelocity(PX_PER_MS) - MathUtils.smoothStep(slowVelocityBound, fastVelocityBound, abs(xVelocity)) - } ?: valueOnFastVelocity + val factor = + velocityTracker?.run { + computeCurrentVelocity(PX_PER_MS) + MathUtils.smoothStep(slowVelocityBound, fastVelocityBound, abs(xVelocity)) + } + ?: valueOnFastVelocity return MathUtils.lerp(valueOnFastVelocity, valueOnSlowVelocity, 1 - factor) } @@ -1014,77 +1051,76 @@ class BackPanelController internal constructor( } init { - if (DEBUG) mView.drawDebugInfo = { canvas -> - val debugStrings = listOf( - "$currentState", - "startX=$startX", - "startY=$startY", - "xDelta=${"%.1f".format(totalTouchDeltaActive)}", - "xTranslation=${"%.1f".format(previousXTranslation)}", - "pre=${"%.0f".format(staticThresholdProgress(previousXTranslation) * 100)}%", - "post=${"%.0f".format(fullScreenProgress(previousXTranslation) * 100)}%" - ) - val debugPaint = Paint().apply { - color = Color.WHITE - } - val debugInfoBottom = debugStrings.size * 32f + 4f - canvas.drawRect( + if (DEBUG) + mView.drawDebugInfo = { canvas -> + val preProgress = staticThresholdProgress(previousXTranslation) * 100 + val postProgress = fullScreenProgress(previousXTranslation) * 100 + val debugStrings = + listOf( + "$currentState", + "startX=$startX", + "startY=$startY", + "xDelta=${"%.1f".format(totalTouchDeltaActive)}", + "xTranslation=${"%.1f".format(previousXTranslation)}", + "pre=${"%.0f".format(preProgress)}%", + "post=${"%.0f".format(postProgress)}%" + ) + val debugPaint = Paint().apply { color = Color.WHITE } + val debugInfoBottom = debugStrings.size * 32f + 4f + canvas.drawRect( 4f, 4f, canvas.width.toFloat(), debugStrings.size * 32f + 4f, debugPaint - ) - debugPaint.apply { - color = Color.BLACK - textSize = 32f - } - var offset = 32f - for (debugText in debugStrings) { - canvas.drawText(debugText, 10f, offset, debugPaint) - offset += 32f - } - debugPaint.apply { - color = Color.RED - style = Paint.Style.STROKE - strokeWidth = 4f - } - val canvasWidth = canvas.width.toFloat() - val canvasHeight = canvas.height.toFloat() - canvas.drawRect(0f, 0f, canvasWidth, canvasHeight, debugPaint) - - fun drawVerticalLine(x: Float, color: Int) { - debugPaint.color = color - val x = if (mView.isLeftPanel) x else canvasWidth - x - canvas.drawLine(x, debugInfoBottom, x, canvas.height.toFloat(), debugPaint) - } + ) + debugPaint.apply { + color = Color.BLACK + textSize = 32f + } + var offset = 32f + for (debugText in debugStrings) { + canvas.drawText(debugText, 10f, offset, debugPaint) + offset += 32f + } + debugPaint.apply { + color = Color.RED + style = Paint.Style.STROKE + strokeWidth = 4f + } + val canvasWidth = canvas.width.toFloat() + val canvasHeight = canvas.height.toFloat() + canvas.drawRect(0f, 0f, canvasWidth, canvasHeight, debugPaint) + + fun drawVerticalLine(x: Float, color: Int) { + debugPaint.color = color + val x = if (mView.isLeftPanel) x else canvasWidth - x + canvas.drawLine(x, debugInfoBottom, x, canvas.height.toFloat(), debugPaint) + } - drawVerticalLine(x = params.staticTriggerThreshold, color = Color.BLUE) - drawVerticalLine(x = params.deactivationTriggerThreshold, color = Color.BLUE) - drawVerticalLine(x = startX, color = Color.GREEN) - drawVerticalLine(x = previousXTranslation, color = Color.DKGRAY) - } + drawVerticalLine(x = params.staticTriggerThreshold, color = Color.BLUE) + drawVerticalLine(x = params.deactivationTriggerThreshold, color = Color.BLUE) + drawVerticalLine(x = startX, color = Color.GREEN) + drawVerticalLine(x = previousXTranslation, color = Color.DKGRAY) + } } } /** - * In addition to a typical step function which returns one or two - * values based on a threshold, `Step` also gracefully handles quick - * changes in input near the threshold value that would typically - * result in the output rapidly changing. + * In addition to a typical step function which returns one or two values based on a threshold, + * `Step` also gracefully handles quick changes in input near the threshold value that would + * typically result in the output rapidly changing. * - * In the context of Back arrow, the arrow's stroke opacity should - * always appear transparent or opaque. Using a typical Step function, - * this would resulting in a flickering appearance as the output would - * change rapidly. `Step` addresses this by moving the threshold after - * it is crossed so it cannot be easily crossed again with small changes - * in touch events. + * In the context of Back arrow, the arrow's stroke opacity should always appear transparent or + * opaque. Using a typical Step function, this would resulting in a flickering appearance as the + * output would change rapidly. `Step` addresses this by moving the threshold after it is crossed so + * it cannot be easily crossed again with small changes in touch events. */ class Step<T>( - private val threshold: Float, - private val factor: Float = 1.1f, - private val postThreshold: T, - private val preThreshold: T + private val threshold: Float, + private val factor: Float = 1.1f, + private val postThreshold: T, + private val preThreshold: T ) { data class Value<T>(val value: T, val isNewState: Boolean) diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index 48790c23e688..2adc211ef23f 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -41,6 +41,8 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.devicepolicy.areKeyguardShortcutsDisabled import com.android.systemui.log.DebugLogger.debugLog +import com.android.systemui.notetask.NoteTaskEntryPoint.QUICK_AFFORDANCE +import com.android.systemui.notetask.NoteTaskEntryPoint.TAIL_BUTTON import com.android.systemui.notetask.NoteTaskRoleManagerExt.createNoteShortcutInfoAsUser import com.android.systemui.notetask.NoteTaskRoleManagerExt.getDefaultRoleHolderAsUser import com.android.systemui.notetask.shortcut.CreateNoteTaskShortcutActivity @@ -121,23 +123,26 @@ constructor( /** * Returns the [UserHandle] of an android user that should handle the notes taking [entryPoint]. - * - * On company owned personally enabled (COPE) devices, if the given [entryPoint] is in the - * [FORCE_WORK_NOTE_APPS_ENTRY_POINTS_ON_COPE_DEVICES] list, the default notes app in the work - * profile user will always be launched. - * - * On non managed devices or devices with other management modes, the current [UserHandle] is - * returned. + * 1. tail button entry point: In COPE or work profile devices, the user can select whether the + * work or main profile notes app should be launched in the Settings app. In non-management + * or device owner devices, the user can only select main profile notes app. + * 2. lock screen quick affordance: since there is no user setting, the main profile notes app + * is used as default for work profile devices while the work profile notes app is used for + * COPE devices. + * 3. Other entry point: the current user from [UserTracker.userHandle]. */ fun getUserForHandlingNotesTaking(entryPoint: NoteTaskEntryPoint): UserHandle = - if ( - entryPoint in FORCE_WORK_NOTE_APPS_ENTRY_POINTS_ON_COPE_DEVICES && - devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile - ) { - userTracker.userProfiles.firstOrNull { userManager.isManagedProfile(it.id) }?.userHandle - ?: userTracker.userHandle - } else { - secureSettings.preferredUser + when { + entryPoint == TAIL_BUTTON -> secureSettings.preferredUser + devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile && + entryPoint == QUICK_AFFORDANCE -> { + userTracker.userProfiles + .firstOrNull { userManager.isManagedProfile(it.id) } + ?.userHandle + ?: userTracker.userHandle + } + // On work profile devices, SysUI always run in the main user. + else -> userTracker.userHandle } /** @@ -267,15 +272,7 @@ constructor( PackageManager.COMPONENT_ENABLED_STATE_DISABLED } - // If the required user matches the tracking user, the injected context is already a context - // of the required user. Avoid calling #createContextAsUser because creating a context for - // a user takes time. - val userContext = - if (user == userTracker.userHandle) { - context - } else { - context.createContextAsUser(user, /* flags= */ 0) - } + val userContext = context.createContextAsUser(user, /* flags= */ 0) userContext.packageManager.setComponentEnabledSetting( componentName, @@ -283,7 +280,7 @@ constructor( PackageManager.DONT_KILL_APP, ) - debugLog { "setNoteTaskShortcutEnabled - completed: $isEnabled" } + debugLog { "setNoteTaskShortcutEnabled for user $user- completed: $enabledState" } } /** @@ -359,10 +356,12 @@ constructor( private val SecureSettings.preferredUser: UserHandle get() { + val trackingUserId = userTracker.userHandle.identifier val userId = - secureSettings.getInt( - Settings.Secure.DEFAULT_NOTE_TASK_PROFILE, - userTracker.userHandle.identifier, + secureSettings.getIntForUser( + /* name= */ Settings.Secure.DEFAULT_NOTE_TASK_PROFILE, + /* def= */ trackingUserId, + /* userHandle= */ trackingUserId, ) return UserHandle.of(userId) } @@ -381,16 +380,6 @@ constructor( * @see com.android.launcher3.icons.IconCache.EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE */ const val EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE = "extra_shortcut_badge_override_package" - - /** - * A list of entry points which should be redirected to the work profile default notes app - * on company owned personally enabled (COPE) devices. - * - * Entry points in this list don't let users / admin to select the work or personal default - * notes app to be launched. - */ - val FORCE_WORK_NOTE_APPS_ENTRY_POINTS_ON_COPE_DEVICES = - listOf(NoteTaskEntryPoint.TAIL_BUTTON, NoteTaskEntryPoint.QUICK_AFFORDANCE) } } diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt new file mode 100644 index 000000000000..fdc70a83e8b1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt @@ -0,0 +1,367 @@ +/* + * 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.privacy + +import android.Manifest +import android.app.ActivityManager +import android.app.Dialog +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.UserHandle +import android.permission.PermissionGroupUsage +import android.permission.PermissionManager +import android.view.View +import androidx.annotation.MainThread +import androidx.annotation.WorkerThread +import com.android.internal.logging.UiEventLogger +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.appops.AppOpsController +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.privacy.logging.PrivacyLogger +import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.policy.KeyguardStateController +import java.util.concurrent.Executor +import javax.inject.Inject + +private val defaultDialogProvider = + object : PrivacyDialogControllerV2.DialogProvider { + override fun makeDialog( + context: Context, + list: List<PrivacyDialogV2.PrivacyElement>, + manageApp: (String, Int, Intent) -> Unit, + closeApp: (String, Int) -> Unit, + openPrivacyDashboard: () -> Unit + ): PrivacyDialogV2 { + return PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard) + } + } + +/** + * Controller for [PrivacyDialogV2]. + * + * This controller shows and dismissed the dialog, as well as determining the information to show in + * it. + */ +@SysUISingleton +class PrivacyDialogControllerV2( + private val permissionManager: PermissionManager, + private val packageManager: PackageManager, + private val privacyItemController: PrivacyItemController, + private val userTracker: UserTracker, + private val activityStarter: ActivityStarter, + private val backgroundExecutor: Executor, + private val uiExecutor: Executor, + private val privacyLogger: PrivacyLogger, + private val keyguardStateController: KeyguardStateController, + private val appOpsController: AppOpsController, + private val uiEventLogger: UiEventLogger, + private val dialogLaunchAnimator: DialogLaunchAnimator, + private val dialogProvider: DialogProvider +) { + + @Inject + constructor( + permissionManager: PermissionManager, + packageManager: PackageManager, + privacyItemController: PrivacyItemController, + userTracker: UserTracker, + activityStarter: ActivityStarter, + @Background backgroundExecutor: Executor, + @Main uiExecutor: Executor, + privacyLogger: PrivacyLogger, + keyguardStateController: KeyguardStateController, + appOpsController: AppOpsController, + uiEventLogger: UiEventLogger, + dialogLaunchAnimator: DialogLaunchAnimator + ) : this( + permissionManager, + packageManager, + privacyItemController, + userTracker, + activityStarter, + backgroundExecutor, + uiExecutor, + privacyLogger, + keyguardStateController, + appOpsController, + uiEventLogger, + dialogLaunchAnimator, + defaultDialogProvider + ) + + private var dialog: Dialog? = null + + private val onDialogDismissed = + object : PrivacyDialogV2.OnDialogDismissed { + override fun onDialogDismissed() { + privacyLogger.logPrivacyDialogDismissed() + uiEventLogger.log(PrivacyDialogEvent.PRIVACY_DIALOG_DISMISSED) + dialog = null + } + } + + @WorkerThread + private fun closeApp(packageName: String, userId: Int) { + uiEventLogger.log( + PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_CLOSE_APP, + userId, + packageName + ) + privacyLogger.logCloseAppFromDialog(packageName, userId) + ActivityManager.getService().stopAppForUser(packageName, userId) + } + + @MainThread + private fun manageApp(packageName: String, userId: Int, navigationIntent: Intent) { + uiEventLogger.log( + PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS, + userId, + packageName + ) + privacyLogger.logStartSettingsActivityFromDialog(packageName, userId) + startActivity(navigationIntent) + } + + @MainThread + private fun openPrivacyDashboard() { + uiEventLogger.log(PrivacyDialogEvent.PRIVACY_DIALOG_CLICK_TO_PRIVACY_DASHBOARD) + privacyLogger.logStartPrivacyDashboardFromDialog() + startActivity(Intent(Intent.ACTION_REVIEW_PERMISSION_USAGE)) + } + + @MainThread + private fun startActivity(navigationIntent: Intent) { + if (!keyguardStateController.isUnlocked) { + // If we are locked, hide the dialog so the user can unlock + dialog?.hide() + } + // startActivity calls internally startActivityDismissingKeyguard + activityStarter.startActivity(navigationIntent, true) { + if (ActivityManager.isStartResultSuccessful(it)) { + dismissDialog() + } else { + dialog?.show() + } + } + } + + @WorkerThread + private fun getStartViewPermissionUsageIntent( + packageName: String, + permGroupName: String, + attributionTag: CharSequence?, + isAttributionSupported: Boolean + ): Intent? { + if (attributionTag != null && isAttributionSupported) { + val intent = Intent(Intent.ACTION_MANAGE_PERMISSION_USAGE) + intent.setPackage(packageName) + intent.putExtra(Intent.EXTRA_PERMISSION_GROUP_NAME, permGroupName) + intent.putExtra(Intent.EXTRA_ATTRIBUTION_TAGS, arrayOf(attributionTag.toString())) + intent.putExtra(Intent.EXTRA_SHOWING_ATTRIBUTION, true) + val resolveInfo = + packageManager.resolveActivity(intent, PackageManager.ResolveInfoFlags.of(0)) + if ( + resolveInfo?.activityInfo?.permission == + Manifest.permission.START_VIEW_PERMISSION_USAGE + ) { + intent.component = ComponentName(packageName, resolveInfo.activityInfo.name) + return intent + } + } + return null + } + + fun getDefaultManageAppPermissionsIntent(packageName: String, userId: Int): Intent { + val intent = Intent(Intent.ACTION_MANAGE_APP_PERMISSIONS) + intent.putExtra(Intent.EXTRA_PACKAGE_NAME, packageName) + intent.putExtra(Intent.EXTRA_USER, UserHandle.of(userId)) + return intent + } + + @WorkerThread + private fun permGroupUsage(): List<PermissionGroupUsage> { + return permissionManager.getIndicatorAppOpUsageData(appOpsController.isMicMuted) + } + + /** + * Show the [PrivacyDialogV2] + * + * This retrieves the permission usage from [PermissionManager] and creates a new + * [PrivacyDialogV2] with a list of [PrivacyDialogV2.PrivacyElement] to show. + * + * This list will be filtered by [filterAndSelect]. Only types available by + * [PrivacyItemController] will be shown. + * + * @param context A context to use to create the dialog. + * @see filterAndSelect + */ + fun showDialog(context: Context, view: View? = null) { + dismissDialog() + backgroundExecutor.execute { + val usage = permGroupUsage() + val userInfos = userTracker.userProfiles + privacyLogger.logUnfilteredPermGroupUsage(usage) + val items = + usage.mapNotNull { + val userInfo = + userInfos.firstOrNull { ui -> ui.id == UserHandle.getUserId(it.uid) } + if ( + isAvailable(it.permissionGroupName) && (userInfo != null || it.isPhoneCall) + ) { + // Only try to get the app name if we actually need it + val appName = + if (it.isPhoneCall) { + "" + } else { + getLabelForPackage(it.packageName, it.uid) + } + val userId = UserHandle.getUserId(it.uid) + val viewUsageIntent = + getStartViewPermissionUsageIntent( + it.packageName, + it.permissionGroupName, + it.attributionTag, + // attributionLabel is set only when subattribution policies + // are supported and satisfied + it.attributionLabel != null + ) + PrivacyDialogV2.PrivacyElement( + permGroupToPrivacyType(it.permissionGroupName)!!, + it.packageName, + userId, + appName, + it.attributionTag, + it.attributionLabel, + it.proxyLabel, + it.lastAccessTimeMillis, + it.isActive, + it.isPhoneCall, + viewUsageIntent != null, + it.permissionGroupName, + viewUsageIntent + ?: getDefaultManageAppPermissionsIntent(it.packageName, userId) + ) + } else { + null + } + } + uiExecutor.execute { + val elements = filterAndSelect(items) + if (elements.isNotEmpty()) { + val d = + dialogProvider.makeDialog( + context, + elements, + this::manageApp, + this::closeApp, + this::openPrivacyDashboard + ) + d.setShowForAllUsers(true) + d.addOnDismissListener(onDialogDismissed) + if (view != null) { + dialogLaunchAnimator.showFromView(d, view) + } else { + d.show() + } + privacyLogger.logShowDialogV2Contents(elements) + dialog = d + } else { + privacyLogger.logEmptyDialog() + } + } + } + } + + /** Dismisses the dialog */ + fun dismissDialog() { + dialog?.dismiss() + } + + @WorkerThread + private fun getLabelForPackage(packageName: String, uid: Int): CharSequence { + return try { + packageManager + .getApplicationInfoAsUser(packageName, 0, UserHandle.getUserId(uid)) + .loadLabel(packageManager) + } catch (_: PackageManager.NameNotFoundException) { + privacyLogger.logLabelNotFound(packageName) + packageName + } + } + + private fun permGroupToPrivacyType(group: String): PrivacyType? { + return when (group) { + Manifest.permission_group.CAMERA -> PrivacyType.TYPE_CAMERA + Manifest.permission_group.MICROPHONE -> PrivacyType.TYPE_MICROPHONE + Manifest.permission_group.LOCATION -> PrivacyType.TYPE_LOCATION + else -> null + } + } + + private fun isAvailable(group: String): Boolean { + return when (group) { + Manifest.permission_group.CAMERA -> privacyItemController.micCameraAvailable + Manifest.permission_group.MICROPHONE -> privacyItemController.micCameraAvailable + Manifest.permission_group.LOCATION -> privacyItemController.locationAvailable + else -> false + } + } + + /** + * Filters the list of elements to show. + * + * For each privacy type, it'll return all active elements. If there are no active elements, + * it'll return the most recent access + */ + private fun filterAndSelect( + list: List<PrivacyDialogV2.PrivacyElement> + ): List<PrivacyDialogV2.PrivacyElement> { + return list + .groupBy { it.type } + .toSortedMap() + .flatMap { (_, elements) -> + val actives = elements.filter { it.isActive } + if (actives.isNotEmpty()) { + actives.sortedByDescending { it.lastActiveTimestamp } + } else { + elements.maxByOrNull { it.lastActiveTimestamp }?.let { listOf(it) } + ?: emptyList() + } + } + } + + /** + * Interface to create a [PrivacyDialogV2]. + * + * Can be used to inject a mock creator. + */ + interface DialogProvider { + /** Create a [PrivacyDialogV2]. */ + fun makeDialog( + context: Context, + list: List<PrivacyDialogV2.PrivacyElement>, + manageApp: (String, Int, Intent) -> Unit, + closeApp: (String, Int) -> Unit, + openPrivacyDashboard: () -> Unit + ): PrivacyDialogV2 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogEvent.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogEvent.kt index 3ecc5a5e5b00..250976cf47a4 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogEvent.kt @@ -18,13 +18,20 @@ package com.android.systemui.privacy import com.android.internal.logging.UiEvent import com.android.internal.logging.UiEventLogger +import com.android.internal.logging.UiEventLogger.UiEventEnum.RESERVE_NEW_UI_EVENT_ID enum class PrivacyDialogEvent(private val _id: Int) : UiEventLogger.UiEventEnum { @UiEvent(doc = "Privacy dialog is clicked by user to go to the app settings page.") PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS(904), @UiEvent(doc = "Privacy dialog is dismissed by user.") - PRIVACY_DIALOG_DISMISSED(905); + PRIVACY_DIALOG_DISMISSED(905), + + @UiEvent(doc = "Privacy dialog item is clicked by user to close the app using a sensor.") + PRIVACY_DIALOG_ITEM_CLICKED_TO_CLOSE_APP(1396), + + @UiEvent(doc = "Privacy dialog is clicked by user to see the privacy dashboard.") + PRIVACY_DIALOG_CLICK_TO_PRIVACY_DASHBOARD(1397); override fun getId() = _id -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt new file mode 100644 index 000000000000..f4aa27d5fcbb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogV2.kt @@ -0,0 +1,539 @@ +/* + * 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.privacy + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageItemInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.NameNotFoundException +import android.content.res.Resources.NotFoundException +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import android.os.Bundle +import android.text.TextUtils +import android.util.Log +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.annotation.WorkerThread +import androidx.core.view.ViewCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat +import com.android.settingslib.Utils +import com.android.systemui.R +import com.android.systemui.animation.ViewHierarchyAnimator +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.util.maybeForceFullscreen +import java.lang.ref.WeakReference +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Dialog to show ongoing and recent app ops element. + * + * @param context A context to create the dialog + * @param list list of elements to show in the dialog. The elements will show in the same order they + * appear in the list + * @param manageApp a callback to start an activity for a given package name, user id, and intent + * @param closeApp a callback to close an app for a given package name, user id + * @param openPrivacyDashboard a callback to open the privacy dashboard + * @see PrivacyDialogControllerV2 + */ +class PrivacyDialogV2( + context: Context, + private val list: List<PrivacyElement>, + private val manageApp: (String, Int, Intent) -> Unit, + private val closeApp: (String, Int) -> Unit, + private val openPrivacyDashboard: () -> Unit +) : SystemUIDialog(context, R.style.Theme_PrivacyDialog) { + + private val dismissListeners = mutableListOf<WeakReference<OnDialogDismissed>>() + private val dismissed = AtomicBoolean(false) + // Note: this will call the dialog create method during init + private val decorViewLayoutListener = maybeForceFullscreen()?.component2() + + /** + * Add a listener that will be called when the dialog is dismissed. + * + * If the dialog has already been dismissed, the listener will be called immediately, in the + * same thread. + */ + fun addOnDismissListener(listener: OnDialogDismissed) { + if (dismissed.get()) { + listener.onDialogDismissed() + } else { + dismissListeners.add(WeakReference(listener)) + } + } + + override fun stop() { + dismissed.set(true) + val iterator = dismissListeners.iterator() + while (iterator.hasNext()) { + val el = iterator.next() + iterator.remove() + el.get()?.onDialogDismissed() + } + // Remove the layout change listener we may have added to the DecorView. + if (decorViewLayoutListener != null) { + window!!.decorView.removeOnLayoutChangeListener(decorViewLayoutListener) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + window!!.setGravity(Gravity.CENTER) + setTitle(R.string.privacy_dialog_title) + setContentView(R.layout.privacy_dialog_v2) + + val closeButton = requireViewById<Button>(R.id.privacy_dialog_close_button) + closeButton.setOnClickListener { dismiss() } + + val moreButton = requireViewById<Button>(R.id.privacy_dialog_more_button) + moreButton.setOnClickListener { openPrivacyDashboard() } + + val itemsContainer = requireViewById<ViewGroup>(R.id.privacy_dialog_items_container) + list.forEach { itemsContainer.addView(createView(it, itemsContainer)) } + } + + private fun createView(element: PrivacyElement, itemsContainer: ViewGroup): View { + val itemCard = + LayoutInflater.from(context) + .inflate(R.layout.privacy_dialog_item_v2, itemsContainer, false) as ViewGroup + + updateItemHeader(element, itemCard) + + if (element.isPhoneCall) { + return itemCard + } + + setItemExpansionBehavior(itemCard) + + configureIndicatorActionButtons(element, itemCard) + + return itemCard + } + + private fun updateItemHeader(element: PrivacyElement, itemCard: View) { + val itemHeader = itemCard.findViewById<ViewGroup>(R.id.privacy_dialog_item_header)!! + val permGroupLabel = context.packageManager.getDefaultPermGroupLabel(element.permGroupName) + + val iconView = itemHeader.findViewById<ImageView>(R.id.privacy_dialog_item_header_icon)!! + val indicatorIcon = context.getPermGroupIcon(element.permGroupName) + updateIconView(iconView, indicatorIcon, element.isActive) + iconView.contentDescription = permGroupLabel + + val titleView = itemHeader.findViewById<TextView>(R.id.privacy_dialog_item_header_title)!! + titleView.text = permGroupLabel + titleView.contentDescription = permGroupLabel + + val usageText = getUsageText(element) + val summaryView = + itemHeader.findViewById<TextView>(R.id.privacy_dialog_item_header_summary)!! + summaryView.text = usageText + summaryView.contentDescription = usageText + } + + private fun configureIndicatorActionButtons(element: PrivacyElement, itemCard: View) { + val expandedLayout = + itemCard.findViewById<ViewGroup>(R.id.privacy_dialog_item_header_expanded_layout)!! + + val buttons: MutableList<View> = mutableListOf() + configureCloseAppButton(element, expandedLayout)?.also { buttons.add(it) } + buttons.add(configureManageButton(element, expandedLayout)) + + val backgroundColor = getBackgroundColor(element.isActive) + when (buttons.size) { + 0 -> return + 1 -> { + val background = + getMutableDrawable(R.drawable.privacy_dialog_background_large_top_large_bottom) + background.setTint(backgroundColor) + buttons[0].background = background + } + else -> { + val firstBackground = + getMutableDrawable(R.drawable.privacy_dialog_background_large_top_small_bottom) + val middleBackground = + getMutableDrawable(R.drawable.privacy_dialog_background_small_top_small_bottom) + val lastBackground = + getMutableDrawable(R.drawable.privacy_dialog_background_small_top_large_bottom) + firstBackground.setTint(backgroundColor) + middleBackground.setTint(backgroundColor) + lastBackground.setTint(backgroundColor) + buttons.forEach { it.background = middleBackground } + buttons.first().background = firstBackground + buttons.last().background = lastBackground + } + } + } + + private fun configureCloseAppButton(element: PrivacyElement, expandedLayout: ViewGroup): View? { + if (element.isService || !element.isActive) { + return null + } + val closeAppButton = + window.layoutInflater.inflate( + R.layout.privacy_dialog_card_button, + expandedLayout, + false + ) as Button + expandedLayout.addView(closeAppButton) + closeAppButton.id = R.id.privacy_dialog_close_app_button + closeAppButton.setText(R.string.privacy_dialog_close_app_button) + closeAppButton.setTextColor(getForegroundColor(true)) + closeAppButton.tag = element + closeAppButton.setOnClickListener { v -> + v.tag?.let { + val element = it as PrivacyElement + closeApp(element.packageName, element.userId) + closeAppTransition(element.packageName, element.userId) + } + } + return closeAppButton + } + + private fun closeAppTransition(packageName: String, userId: Int) { + val itemsContainer = requireViewById<ViewGroup>(R.id.privacy_dialog_items_container) + var shouldTransition = false + for (i in 0 until itemsContainer.getChildCount()) { + val itemCard = itemsContainer.getChildAt(i) + val button = itemCard.findViewById<Button>(R.id.privacy_dialog_close_app_button) + if (button == null || button.tag == null) { + continue + } + val element = button.tag as PrivacyElement + if (element.packageName != packageName || element.userId != userId) { + continue + } + + itemCard.setEnabled(false) + + val expandToggle = + itemCard.findViewById<ImageView>(R.id.privacy_dialog_item_header_expand_toggle)!! + expandToggle.visibility = View.GONE + + disableIndicatorCardUi(itemCard, element.applicationName) + + val expandedLayout = + itemCard.findViewById<View>(R.id.privacy_dialog_item_header_expanded_layout)!! + if (expandedLayout.visibility == View.VISIBLE) { + expandedLayout.visibility = View.GONE + shouldTransition = true + } + } + if (shouldTransition) { + ViewHierarchyAnimator.animateNextUpdate(window!!.decorView) + } + } + + private fun configureManageButton(element: PrivacyElement, expandedLayout: ViewGroup): View { + val manageButton = + window.layoutInflater.inflate( + R.layout.privacy_dialog_card_button, + expandedLayout, + false + ) as Button + expandedLayout.addView(manageButton) + manageButton.id = R.id.privacy_dialog_manage_app_button + manageButton.setText( + if (element.isService) R.string.privacy_dialog_manage_service + else R.string.privacy_dialog_manage_permissions + ) + manageButton.setTextColor(getForegroundColor(element.isActive)) + manageButton.tag = element + manageButton.setOnClickListener { v -> + v.tag?.let { + val element = it as PrivacyElement + manageApp(element.packageName, element.userId, element.navigationIntent) + } + } + return manageButton + } + + private fun disableIndicatorCardUi(itemCard: View, applicationName: CharSequence) { + val iconView = itemCard.findViewById<ImageView>(R.id.privacy_dialog_item_header_icon)!! + val indicatorIcon = getMutableDrawable(R.drawable.privacy_dialog_check_icon) + updateIconView(iconView, indicatorIcon, false) + + val closedAppText = + context.getString(R.string.privacy_dialog_close_app_message, applicationName) + val summaryView = itemCard.findViewById<TextView>(R.id.privacy_dialog_item_header_summary)!! + summaryView.text = closedAppText + summaryView.contentDescription = closedAppText + } + + private fun setItemExpansionBehavior(itemCard: ViewGroup) { + val itemHeader = itemCard.findViewById<ViewGroup>(R.id.privacy_dialog_item_header)!! + + val expandToggle = + itemHeader.findViewById<ImageView>(R.id.privacy_dialog_item_header_expand_toggle)!! + expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_down) + expandToggle.visibility = View.VISIBLE + + ViewCompat.replaceAccessibilityAction( + itemCard, + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, + context.getString(R.string.privacy_dialog_expand_action), + null + ) + + val expandedLayout = + itemCard.findViewById<View>(R.id.privacy_dialog_item_header_expanded_layout)!! + expandedLayout.setOnClickListener { + // Stop clicks from propagating + } + + itemCard.setOnClickListener { + if (expandedLayout.visibility == View.VISIBLE) { + expandedLayout.visibility = View.GONE + expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_down) + ViewCompat.replaceAccessibilityAction( + it!!, + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, + context.getString(R.string.privacy_dialog_expand_action), + null + ) + } else { + expandedLayout.visibility = View.VISIBLE + expandToggle.setImageResource(R.drawable.privacy_dialog_expand_toggle_up) + ViewCompat.replaceAccessibilityAction( + it!!, + AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK, + context.getString(R.string.privacy_dialog_collapse_action), + null + ) + } + ViewHierarchyAnimator.animateNextUpdate( + rootView = window!!.decorView, + excludedViews = setOf(expandedLayout) + ) + } + } + + private fun updateIconView(iconView: ImageView, indicatorIcon: Drawable, active: Boolean) { + indicatorIcon.setTint(getForegroundColor(active)) + val backgroundIcon = getMutableDrawable(R.drawable.privacy_dialog_background_circle) + backgroundIcon.setTint(getBackgroundColor(active)) + val backgroundSize = + context.resources.getDimension(R.dimen.ongoing_appops_dialog_circle_size).toInt() + val indicatorSize = + context.resources.getDimension(R.dimen.ongoing_appops_dialog_icon_size).toInt() + iconView.setImageDrawable( + constructLayeredIcon(indicatorIcon, indicatorSize, backgroundIcon, backgroundSize) + ) + } + + @ColorInt + private fun getForegroundColor(active: Boolean) = + Utils.getColorAttrDefaultColor( + context, + if (active) com.android.internal.R.attr.materialColorOnPrimaryFixed + else com.android.internal.R.attr.materialColorOnSurface + ) + + @ColorInt + private fun getBackgroundColor(active: Boolean) = + Utils.getColorAttrDefaultColor( + context, + if (active) com.android.internal.R.attr.materialColorPrimaryFixed + else com.android.internal.R.attr.materialColorSurfaceContainerHigh + ) + + private fun getMutableDrawable(@DrawableRes resId: Int) = context.getDrawable(resId)!!.mutate() + + private fun getUsageText(element: PrivacyElement) = + if (element.isPhoneCall) { + val phoneCallResId = + if (element.isActive) R.string.privacy_dialog_active_call_usage + else R.string.privacy_dialog_recent_call_usage + context.getString(phoneCallResId) + } else if (element.attributionLabel == null && element.proxyLabel == null) { + val usageResId: Int = + if (element.isActive) R.string.privacy_dialog_active_app_usage + else R.string.privacy_dialog_recent_app_usage + context.getString(usageResId, element.applicationName) + } else if (element.attributionLabel == null || element.proxyLabel == null) { + val singleUsageResId: Int = + if (element.isActive) R.string.privacy_dialog_active_app_usage_1 + else R.string.privacy_dialog_recent_app_usage_1 + context.getString( + singleUsageResId, + element.applicationName, + element.attributionLabel ?: element.proxyLabel + ) + } else { + val doubleUsageResId: Int = + if (element.isActive) R.string.privacy_dialog_active_app_usage_2 + else R.string.privacy_dialog_recent_app_usage_2 + context.getString( + doubleUsageResId, + element.applicationName, + element.attributionLabel, + element.proxyLabel + ) + } + + companion object { + private const val LOG_TAG = "PrivacyDialogV2" + private const val REVIEW_PERMISSION_USAGE = "android.intent.action.REVIEW_PERMISSION_USAGE" + + /** + * Gets a permission group's icon from the system. + * + * @param groupName The name of the permission group whose icon we want + * @return The permission group's icon, the privacy_dialog_default_permission_icon icon if + * the group has no icon, or the group does not exist + */ + @WorkerThread + private fun Context.getPermGroupIcon(groupName: String): Drawable { + val groupInfo = packageManager.getGroupInfo(groupName) + if (groupInfo != null && groupInfo.icon != 0) { + val icon = packageManager.loadDrawable(groupInfo.packageName, groupInfo.icon) + if (icon != null) { + return icon + } + } + + return getDrawable(R.drawable.privacy_dialog_default_permission_icon)!!.mutate() + } + + /** + * Gets a permission group's label from the system. + * + * @param groupName The name of the permission group whose label we want + * @return The permission group's label, or the group name, if the group is invalid + */ + @WorkerThread + private fun PackageManager.getDefaultPermGroupLabel(groupName: String): CharSequence { + val groupInfo = getGroupInfo(groupName) ?: return groupName + return groupInfo.loadSafeLabel( + this, + 0f, + TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM + ) + } + + /** + * Get the [infos][PackageItemInfo] for the given permission group. + * + * @param groupName the group + * @return The info of permission group or null if the group does not have runtime + * permissions. + */ + @WorkerThread + private fun PackageManager.getGroupInfo(groupName: String): PackageItemInfo? { + try { + return getPermissionGroupInfo(groupName, 0) + } catch (e: NameNotFoundException) { + /* ignore */ + } + try { + return getPermissionInfo(groupName, 0) + } catch (e: NameNotFoundException) { + /* ignore */ + } + return null + } + + @WorkerThread + private fun PackageManager.loadDrawable(pkg: String, @DrawableRes resId: Int): Drawable? { + return try { + getResourcesForApplication(pkg).getDrawable(resId, null)?.mutate() + } catch (e: NotFoundException) { + Log.w(LOG_TAG, "Couldn't get resource", e) + null + } catch (e: NameNotFoundException) { + Log.w(LOG_TAG, "Couldn't get resource", e) + null + } + } + + private fun constructLayeredIcon( + icon: Drawable, + iconSize: Int, + background: Drawable, + backgroundSize: Int + ): Drawable { + val layered = LayerDrawable(arrayOf(background, icon)) + layered.setLayerSize(0, backgroundSize, backgroundSize) + layered.setLayerGravity(0, Gravity.CENTER) + layered.setLayerSize(1, iconSize, iconSize) + layered.setLayerGravity(1, Gravity.CENTER) + return layered + } + } + + /** */ + data class PrivacyElement( + val type: PrivacyType, + val packageName: String, + val userId: Int, + val applicationName: CharSequence, + val attributionTag: CharSequence?, + val attributionLabel: CharSequence?, + val proxyLabel: CharSequence?, + val lastActiveTimestamp: Long, + val isActive: Boolean, + val isPhoneCall: Boolean, + val isService: Boolean, + val permGroupName: String, + val navigationIntent: Intent + ) { + private val builder = StringBuilder("PrivacyElement(") + + init { + builder.append("type=${type.logName}") + builder.append(", packageName=$packageName") + builder.append(", userId=$userId") + builder.append(", appName=$applicationName") + if (attributionTag != null) { + builder.append(", attributionTag=$attributionTag") + } + if (attributionLabel != null) { + builder.append(", attributionLabel=$attributionLabel") + } + if (proxyLabel != null) { + builder.append(", proxyLabel=$proxyLabel") + } + builder.append(", lastActive=$lastActiveTimestamp") + if (isActive) { + builder.append(", active") + } + if (isPhoneCall) { + builder.append(", phoneCall") + } + if (isService) { + builder.append(", service") + } + builder.append(", permGroupName=$permGroupName") + builder.append(", navigationIntent=$navigationIntent)") + } + + override fun toString(): String = builder.toString() + } + + /** */ + interface OnDialogDismissed { + fun onDialogDismissed() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt index f934346d9775..1a4642f4df74 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/logging/PrivacyLogger.kt @@ -18,11 +18,12 @@ package com.android.systemui.privacy.logging import android.icu.text.SimpleDateFormat import android.permission.PermissionGroupUsage -import com.android.systemui.log.dagger.PrivacyLog import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.LogLevel import com.android.systemui.log.core.LogMessage +import com.android.systemui.log.dagger.PrivacyLog import com.android.systemui.privacy.PrivacyDialog +import com.android.systemui.privacy.PrivacyDialogV2 import com.android.systemui.privacy.PrivacyItem import java.util.Locale import javax.inject.Inject @@ -126,6 +127,14 @@ class PrivacyLogger @Inject constructor( }) } + fun logShowDialogV2Contents(contents: List<PrivacyDialogV2.PrivacyElement>) { + log(LogLevel.INFO, { + str1 = contents.toString() + }, { + "Privacy dialog shown. Contents: $str1" + }) + } + fun logEmptyDialog() { log(LogLevel.WARNING, {}, { "Trying to show an empty dialog" @@ -147,6 +156,23 @@ class PrivacyLogger @Inject constructor( }) } + fun logCloseAppFromDialog(packageName: String, userId: Int) { + log(LogLevel.INFO, { + str1 = packageName + int1 = userId + }, { + "Close app from dialog for packageName=$str1, userId=$int1" + }) + } + + fun logStartPrivacyDashboardFromDialog() { + log(LogLevel.INFO, {}, { "Start privacy dashboard from dialog" }) + } + + fun logLabelNotFound(packageName: String) { + log(LogLevel.WARNING, { str1 = packageName }, { "Label not found for: $str1" }) + } + private fun listToString(list: List<PrivacyItem>): String { return list.joinToString(separator = ", ", transform = PrivacyItem::log) } @@ -158,4 +184,4 @@ class PrivacyLogger @Inject constructor( ) { buffer.log(TAG, logLevel, initializer, printer) } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt b/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt index 33c47cc082e1..0941a2082cfd 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/HeaderPrivacyIconsController.kt @@ -14,10 +14,13 @@ import com.android.internal.logging.UiEventLogger import com.android.systemui.animation.ActivityLaunchAnimator import com.android.systemui.appops.AppOpsController import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.plugins.ActivityStarter import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.privacy.PrivacyChipEvent import com.android.systemui.privacy.PrivacyDialogController +import com.android.systemui.privacy.PrivacyDialogControllerV2 import com.android.systemui.privacy.PrivacyItem import com.android.systemui.privacy.PrivacyItemController import com.android.systemui.privacy.logging.PrivacyLogger @@ -49,6 +52,7 @@ class HeaderPrivacyIconsController @Inject constructor( private val uiEventLogger: UiEventLogger, @Named(SHADE_HEADER) private val privacyChip: OngoingPrivacyChip, private val privacyDialogController: PrivacyDialogController, + private val privacyDialogControllerV2: PrivacyDialogControllerV2, private val privacyLogger: PrivacyLogger, @Named(SHADE_HEADER) private val iconContainer: StatusIconContainer, private val permissionManager: PermissionManager, @@ -58,7 +62,8 @@ class HeaderPrivacyIconsController @Inject constructor( private val appOpsController: AppOpsController, private val broadcastDispatcher: BroadcastDispatcher, private val safetyCenterManager: SafetyCenterManager, - private val deviceProvisionedController: DeviceProvisionedController + private val deviceProvisionedController: DeviceProvisionedController, + private val featureFlags: FeatureFlags ) { var chipVisibilityListener: ChipVisibilityListener? = null @@ -143,7 +148,11 @@ class HeaderPrivacyIconsController @Inject constructor( // If the privacy chip is visible, it means there were some indicators uiEventLogger.log(PrivacyChipEvent.ONGOING_INDICATORS_CHIP_CLICK) if (safetyCenterEnabled) { - showSafetyCenter() + if (featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)) { + privacyDialogControllerV2.showDialog(privacyChip.context, privacyChip) + } else { + showSafetyCenter() + } } else { privacyDialogController.showDialog(privacyChip.context) } diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 207cc1398279..bf40a2d0ad51 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -78,13 +78,14 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.AssistUtils; import com.android.internal.app.IVoiceInteractionSessionListener; import com.android.internal.logging.UiEventLogger; -import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.util.ScreenshotHelper; import com.android.internal.util.ScreenshotRequest; import com.android.systemui.Dumpable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; @@ -95,6 +96,8 @@ import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.navigationbar.buttons.KeyButtonView; import com.android.systemui.recents.OverviewProxyService.OverviewProxyListener; +import com.android.systemui.scene.domain.interactor.SceneInteractor; +import com.android.systemui.scene.shared.model.SceneContainerNames; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeViewController; @@ -121,6 +124,7 @@ import java.util.concurrent.Executor; import java.util.function.Supplier; import javax.inject.Inject; +import javax.inject.Provider; /** * Class to send information from overview to launcher with a binder. @@ -140,13 +144,17 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis private static final long MAX_BACKOFF_MILLIS = 10 * 60 * 1000; private final Context mContext; + private final FeatureFlags mFeatureFlags; private final Executor mMainExecutor; private final ShellInterface mShellInterface; private final Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy; + private final Lazy<ShadeViewController> mShadeViewControllerLazy; private SysUiState mSysUiState; private final Handler mHandler; private final Lazy<NavigationBarController> mNavBarControllerLazy; private final NotificationShadeWindowController mStatusBarWinController; + private final Provider<SceneInteractor> mSceneInteractor; + private final Runnable mConnectionRunnable = () -> internalConnectToCurrentUser("runnable: startConnectionToCurrentUser"); private final ComponentName mRecentsComponentName; @@ -201,11 +209,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis // TODO move this logic to message queue mCentralSurfacesOptionalLazy.get().ifPresent(centralSurfaces -> { if (event.getActionMasked() == ACTION_DOWN) { - ShadeViewController shadeViewController = - centralSurfaces.getShadeViewController(); - if (shadeViewController != null) { - shadeViewController.startExpandLatencyTracking(); - } + mShadeViewControllerLazy.get().startExpandLatencyTracking(); } mHandler.post(() -> { int action = event.getActionMasked(); @@ -213,17 +217,28 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis mInputFocusTransferStarted = true; mInputFocusTransferStartY = event.getY(); mInputFocusTransferStartMillis = event.getEventTime(); - centralSurfaces.onInputFocusTransfer( - mInputFocusTransferStarted, false /* cancel */, - 0 /* velocity */); + + // If scene framework is enabled, set the scene container window to + // visible and let the touch "slip" into that window. + if (mFeatureFlags.isEnabled(Flags.SCENE_CONTAINER)) { + mSceneInteractor.get().setVisible( + SceneContainerNames.SYSTEM_UI_DEFAULT, true); + } else { + centralSurfaces.onInputFocusTransfer( + mInputFocusTransferStarted, false /* cancel */, + 0 /* velocity */); + } } if (action == ACTION_UP || action == ACTION_CANCEL) { mInputFocusTransferStarted = false; - float velocity = (event.getY() - mInputFocusTransferStartY) - / (event.getEventTime() - mInputFocusTransferStartMillis); - centralSurfaces.onInputFocusTransfer(mInputFocusTransferStarted, - action == ACTION_CANCEL, - velocity); + + if (!mFeatureFlags.isEnabled(Flags.SCENE_CONTAINER)) { + float velocity = (event.getY() - mInputFocusTransferStartY) + / (event.getEventTime() - mInputFocusTransferStartMillis); + centralSurfaces.onInputFocusTransfer(mInputFocusTransferStarted, + action == ACTION_CANCEL, + velocity); + } } event.recycle(); }); @@ -552,8 +567,11 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis ShellInterface shellInterface, Lazy<NavigationBarController> navBarControllerLazy, Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy, + Lazy<ShadeViewController> shadeViewControllerLazy, NavigationModeController navModeController, - NotificationShadeWindowController statusBarWinController, SysUiState sysUiState, + NotificationShadeWindowController statusBarWinController, + SysUiState sysUiState, + Provider<SceneInteractor> sceneInteractor, UserTracker userTracker, ScreenLifecycle screenLifecycle, WakefulnessLifecycle wakefulnessLifecycle, @@ -561,6 +579,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis DisplayTracker displayTracker, KeyguardUnlockAnimationController sysuiUnlockAnimationController, AssistUtils assistUtils, + FeatureFlags featureFlags, DumpManager dumpManager, Optional<UnfoldTransitionProgressForwarder> unfoldTransitionProgressForwarder ) { @@ -570,12 +589,15 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } mContext = context; + mFeatureFlags = featureFlags; mMainExecutor = mainExecutor; mShellInterface = shellInterface; mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy; + mShadeViewControllerLazy = shadeViewControllerLazy; mHandler = new Handler(); mNavBarControllerLazy = navBarControllerLazy; mStatusBarWinController = statusBarWinController; + mSceneInteractor = sceneInteractor; mUserTracker = userTracker; mConnectionBackoffAttempts = 0; mRecentsComponentName = ComponentName.unflattenFromString(context.getString( @@ -677,13 +699,10 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis mNavBarControllerLazy.get().getDefaultNavigationBar(); final NavigationBarView navBarView = mNavBarControllerLazy.get().getNavigationBarView(mContext.getDisplayId()); - final ShadeViewController panelController = - mCentralSurfacesOptionalLazy.get() - .map(CentralSurfaces::getShadeViewController) - .orElse(null); if (SysUiState.DEBUG) { Log.d(TAG_OPS, "Updating sysui state flags: navBarFragment=" + navBarFragment - + " navBarView=" + navBarView + " panelController=" + panelController); + + " navBarView=" + navBarView + + " shadeViewController=" + mShadeViewControllerLazy.get()); } if (navBarFragment != null) { @@ -692,9 +711,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis if (navBarView != null) { navBarView.updateDisabledSystemUiStateFlags(mSysUiState); } - if (panelController != null) { - panelController.updateSystemUiStateFlags(); - } + mShadeViewControllerLazy.get().updateSystemUiStateFlags(); if (mStatusBarWinController != null) { mStatusBarWinController.notifyStateChangedCallbacks(); } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index 4582370679ab..f03f040c206d 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -18,11 +18,14 @@ package com.android.systemui.scene.domain.interactor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.scene.data.repository.SceneContainerRepository +import com.android.systemui.scene.shared.model.RemoteUserInput import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.scene.shared.model.SceneTransitionModel import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow /** * Generic business logic and app state accessors for the scene framework. @@ -92,4 +95,14 @@ constructor( fun sceneTransitions(containerName: String): StateFlow<SceneTransitionModel?> { return repository.sceneTransitions(containerName) } + + private val _remoteUserInput: MutableStateFlow<RemoteUserInput?> = MutableStateFlow(null) + + /** A flow of motion events originating from outside of the scene framework. */ + val remoteUserInput: StateFlow<RemoteUserInput?> = _remoteUserInput.asStateFlow() + + /** Handles a remote user input. */ + fun onRemoteUserInput(input: RemoteUserInput) { + _remoteUserInput.value = input + } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/RemoteUserInput.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/RemoteUserInput.kt new file mode 100644 index 000000000000..680de590a3fc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/RemoteUserInput.kt @@ -0,0 +1,35 @@ +package com.android.systemui.scene.shared.model + +import android.view.MotionEvent + +/** A representation of user input that is used by the scene framework. */ +data class RemoteUserInput( + val x: Float, + val y: Float, + val action: RemoteUserInputAction, +) { + companion object { + fun translateMotionEvent(event: MotionEvent): RemoteUserInput { + return RemoteUserInput( + x = event.x, + y = event.y, + action = + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> RemoteUserInputAction.DOWN + MotionEvent.ACTION_MOVE -> RemoteUserInputAction.MOVE + MotionEvent.ACTION_UP -> RemoteUserInputAction.UP + MotionEvent.ACTION_CANCEL -> RemoteUserInputAction.CANCEL + else -> RemoteUserInputAction.UNKNOWN + } + ) + } + } +} + +enum class RemoteUserInputAction { + DOWN, + MOVE, + UP, + CANCEL, + UNKNOWN, +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt index c456be6e5ab2..b89179289a3d 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt @@ -2,6 +2,7 @@ package com.android.systemui.scene.ui.view import android.content.Context import android.util.AttributeSet +import android.view.MotionEvent import android.view.View import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.shared.model.SceneContainerConfig @@ -16,11 +17,15 @@ class SceneWindowRootView( context, attrs, ) { + + private lateinit var viewModel: SceneContainerViewModel + fun init( viewModel: SceneContainerViewModel, containerConfig: SceneContainerConfig, scenes: Set<Scene>, ) { + this.viewModel = viewModel SceneWindowRootViewBinder.bind( view = this@SceneWindowRootView, viewModel = viewModel, @@ -32,6 +37,14 @@ class SceneWindowRootView( ) } + override fun onTouchEvent(event: MotionEvent?): Boolean { + return event?.let { + viewModel.onRemoteUserInput(event) + true + } + ?: false + } + override fun setVisibility(visibility: Int) { // Do nothing. We don't want external callers to invoke this. Instead, we drive our own // visibility from our view-binder. diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt index 8c1ad9b4571b..005f48d9f250 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt @@ -16,7 +16,9 @@ package com.android.systemui.scene.ui.viewmodel +import android.view.MotionEvent import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.RemoteUserInput import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import kotlinx.coroutines.flow.StateFlow @@ -26,6 +28,9 @@ class SceneContainerViewModel( private val interactor: SceneInteractor, val containerName: String, ) { + /** A flow of motion events originating from outside of the scene framework. */ + val remoteUserInput: StateFlow<RemoteUserInput?> = interactor.remoteUserInput + /** * Keys of all scenes in the container. * @@ -49,4 +54,9 @@ class SceneContainerViewModel( fun setSceneTransitionProgress(progress: Float) { interactor.setSceneTransitionProgress(containerName, progress) } + + /** Handles a [MotionEvent] representing remote user input. */ + fun onRemoteUserInput(event: MotionEvent) { + interactor.onRemoteUserInput(RemoteUserInput.translateMotionEvent(event)) + } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 202d6e66d800..e428976c5ce2 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -147,7 +147,6 @@ import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.media.controls.ui.KeyguardMediaController; import com.android.systemui.media.controls.ui.MediaHierarchyManager; import com.android.systemui.model.SysUiState; -import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.navigationbar.NavigationModeController; @@ -365,7 +364,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private KeyguardBottomAreaView mKeyguardBottomArea; private boolean mExpanding; private boolean mSplitShadeEnabled; - private boolean mDualShadeEnabled; /** The bottom padding reserved for elements of the keyguard measuring notifications. */ private float mKeyguardNotificationBottomPadding; /** @@ -599,7 +597,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private final KeyguardTransitionInteractor mKeyguardTransitionInteractor; private final KeyguardInteractor mKeyguardInteractor; private final KeyguardViewConfigurator mKeyguardViewConfigurator; - private final @Nullable MultiShadeInteractor mMultiShadeInteractor; private final CoroutineDispatcher mMainDispatcher; private boolean mIsAnyMultiShadeExpanded; private boolean mIsOcclusionTransitionRunning = false; @@ -735,7 +732,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump LockscreenToOccludedTransitionViewModel lockscreenToOccludedTransitionViewModel, @Main CoroutineDispatcher mainDispatcher, KeyguardTransitionInteractor keyguardTransitionInteractor, - Provider<MultiShadeInteractor> multiShadeInteractorProvider, DumpManager dumpManager, KeyguardLongPressViewModel keyguardLongPressViewModel, KeyguardInteractor keyguardInteractor, @@ -839,8 +835,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mFeatureFlags = featureFlags; mAnimateBack = mFeatureFlags.isEnabled(Flags.WM_SHADE_ANIMATE_BACK_GESTURE); mTrackpadGestureFeaturesEnabled = mFeatureFlags.isEnabled(Flags.TRACKPAD_GESTURE_FEATURES); - mDualShadeEnabled = mFeatureFlags.isEnabled(Flags.DUAL_SHADE); - mMultiShadeInteractor = mDualShadeEnabled ? multiShadeInteractorProvider.get() : null; mFalsingCollector = falsingCollector; mPowerManager = powerManager; mWakeUpCoordinator = coordinator; @@ -1079,11 +1073,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mNotificationPanelUnfoldAnimationController.ifPresent(controller -> controller.setup(mNotificationContainerParent)); - if (mDualShadeEnabled) { - collectFlow(mView, mMultiShadeInteractor.isAnyShadeExpanded(), - mMultiShadeExpansionConsumer, mMainDispatcher); - } - // Dreaming->Lockscreen collectFlow(mView, mKeyguardTransitionInteractor.getDreamingToLockscreenTransition(), mDreamingToLockscreenTransition, mMainDispatcher); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 108ea68ae8e0..6afed1d76918 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -31,9 +31,6 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.view.ViewStub; - -import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.keyguard.AuthKeyguardMessageArea; @@ -46,7 +43,6 @@ import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor; import com.android.systemui.bouncer.ui.binder.KeyguardBouncerViewBinder; import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel; import com.android.systemui.classifier.FalsingCollector; -import com.android.systemui.compose.ComposeFacade; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dock.DockManager; import com.android.systemui.flags.FeatureFlags; @@ -57,9 +53,6 @@ import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel; import com.android.systemui.log.BouncerLogger; -import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor; -import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor; -import com.android.systemui.multishade.ui.view.MultiShadeView; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.shared.animation.DisableSubpixelTextTransitionListener; import com.android.systemui.statusbar.DragDownHelper; @@ -83,7 +76,6 @@ import java.util.Optional; import java.util.function.Consumer; import javax.inject.Inject; -import javax.inject.Provider; /** * Controller for {@link NotificationShadeWindowView}. @@ -135,7 +127,6 @@ public class NotificationShadeWindowViewController { step.getTransitionState() == TransitionState.RUNNING; }; private final SystemClock mClock; - private final @Nullable MultiShadeMotionEventInteractor mMultiShadeMotionEventInteractor; @Inject public NotificationShadeWindowViewController( @@ -167,9 +158,7 @@ public class NotificationShadeWindowViewController { KeyguardTransitionInteractor keyguardTransitionInteractor, PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel, FeatureFlags featureFlags, - Provider<MultiShadeInteractor> multiShadeInteractorProvider, SystemClock clock, - Provider<MultiShadeMotionEventInteractor> multiShadeMotionEventInteractorProvider, BouncerMessageInteractor bouncerMessageInteractor, BouncerLogger bouncerLogger) { mLockscreenShadeTransitionController = transitionController; @@ -219,17 +208,6 @@ public class NotificationShadeWindowViewController { progressProvider -> progressProvider.addCallback( mDisableSubpixelTextTransitionListener)); } - if (ComposeFacade.INSTANCE.isComposeAvailable() - && featureFlags.isEnabled(Flags.DUAL_SHADE)) { - mMultiShadeMotionEventInteractor = multiShadeMotionEventInteractorProvider.get(); - final ViewStub multiShadeViewStub = mView.findViewById(R.id.multi_shade_stub); - if (multiShadeViewStub != null) { - final MultiShadeView multiShadeView = (MultiShadeView) multiShadeViewStub.inflate(); - multiShadeView.init(multiShadeInteractorProvider.get(), clock); - } - } else { - mMultiShadeMotionEventInteractor = null; - } } /** @@ -395,10 +373,7 @@ public class NotificationShadeWindowViewController { return true; } - if (mMultiShadeMotionEventInteractor != null) { - // This interactor is not null only if the dual shade feature is enabled. - return mMultiShadeMotionEventInteractor.shouldIntercept(ev); - } else if (mNotificationPanelViewController.isFullyExpanded() + if (mNotificationPanelViewController.isFullyExpanded() && mDragDownHelper.isDragDownEnabled() && !mService.isBouncerShowing() && !mStatusBarStateController.isDozing()) { @@ -428,10 +403,7 @@ public class NotificationShadeWindowViewController { return true; } - if (mMultiShadeMotionEventInteractor != null) { - // This interactor is not null only if the dual shade feature is enabled. - return mMultiShadeMotionEventInteractor.onTouchEvent(ev, mView.getWidth()); - } else if (mDragDownHelper.isDragDownEnabled() + if (mDragDownHelper.isDragDownEnabled() || mDragDownHelper.isDraggingDown()) { // we still want to finish our drag down gesture when locking the screen return mDragDownHelper.onTouchEvent(ev) || handled; diff --git a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt index ee4e98e094fc..fe4832f0895b 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt @@ -16,6 +16,7 @@ package com.android.systemui.shade +import android.graphics.Point import android.hardware.display.AmbientDisplayConfiguration import android.os.PowerManager import android.provider.Settings @@ -25,6 +26,7 @@ import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dock.DockManager import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.domain.interactor.DozeInteractor import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.FalsingManager.LOW_PENALTY import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -52,6 +54,7 @@ class PulsingGestureListener @Inject constructor( private val ambientDisplayConfiguration: AmbientDisplayConfiguration, private val statusBarStateController: StatusBarStateController, private val shadeLogger: ShadeLogger, + private val dozeInteractor: DozeInteractor, userTracker: UserTracker, tunerService: TunerService, dumpManager: DumpManager @@ -86,6 +89,7 @@ class PulsingGestureListener @Inject constructor( shadeLogger.logSingleTapUpFalsingState(proximityIsNotNear, isNotAFalseTap) if (proximityIsNotNear && isNotAFalseTap) { shadeLogger.d("Single tap handled, requesting centralSurfaces.wakeUpIfDozing") + dozeInteractor.setLastTapToWakePosition(Point(e.x.toInt(), e.y.toInt())) powerInteractor.wakeUpIfDozing("PULSING_SINGLE_TAP", PowerManager.WAKE_REASON_TAP) } return true diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index 92df78bac17f..6304c1ea2635 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -168,7 +168,10 @@ public class CommandQueue extends IStatusBar.Stub implements private static final int MSG_ENTER_STAGE_SPLIT_FROM_RUNNING_APP = 71 << MSG_SHIFT; private static final int MSG_SHOW_MEDIA_OUTPUT_SWITCHER = 72 << MSG_SHIFT; private static final int MSG_TOGGLE_TASKBAR = 73 << MSG_SHIFT; - + private static final int MSG_SETTING_CHANGED = 74 << MSG_SHIFT; + private static final int MSG_LOCK_TASK_MODE_CHANGED = 75 << MSG_SHIFT; + private static final int MSG_CONFIRM_IMMERSIVE_PROMPT = 77 << MSG_SHIFT; + private static final int MSG_IMMERSIVE_CHANGED = 78 << MSG_SHIFT; public static final int FLAG_EXCLUDE_NONE = 0; public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0; public static final int FLAG_EXCLUDE_RECENTS_PANEL = 1 << 1; @@ -498,6 +501,16 @@ public class CommandQueue extends IStatusBar.Stub implements * @see IStatusBar#showMediaOutputSwitcher */ default void showMediaOutputSwitcher(String packageName) {} + + /** + * @see IStatusBar#confirmImmersivePrompt + */ + default void confirmImmersivePrompt() {} + + /** + * @see IStatusBar#immersiveModeChanged + */ + default void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) {} } @VisibleForTesting @@ -783,6 +796,23 @@ public class CommandQueue extends IStatusBar.Stub implements } @Override + public void confirmImmersivePrompt() { + synchronized (mLock) { + mHandler.obtainMessage(MSG_CONFIRM_IMMERSIVE_PROMPT).sendToTarget(); + } + } + + @Override + public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) { + synchronized (mLock) { + final SomeArgs args = SomeArgs.obtain(); + args.argi1 = rootDisplayAreaId; + args.argi2 = isImmersiveMode ? 1 : 0; + mHandler.obtainMessage(MSG_IMMERSIVE_CHANGED, args).sendToTarget(); + } + } + + @Override public void appTransitionPending(int displayId) { appTransitionPending(displayId, false /* forced */); } @@ -1810,6 +1840,19 @@ public class CommandQueue extends IStatusBar.Stub implements mCallbacks.get(i).showMediaOutputSwitcher(clientPackageName); } break; + case MSG_CONFIRM_IMMERSIVE_PROMPT: + for (int i = 0; i < mCallbacks.size(); i++) { + mCallbacks.get(i).confirmImmersivePrompt(); + } + break; + case MSG_IMMERSIVE_CHANGED: + args = (SomeArgs) msg.obj; + int rootDisplayAreaId = args.argi1; + boolean isImmersiveMode = args.argi2 != 0; + for (int i = 0; i < mCallbacks.size(); i++) { + mCallbacks.get(i).immersiveModeChanged(rootDisplayAreaId, isImmersiveMode); + } + break; } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java new file mode 100644 index 000000000000..a7ec02ff43c3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/ImmersiveModeConfirmation.java @@ -0,0 +1,590 @@ +/* + * 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; + +import static android.app.ActivityManager.LOCK_TASK_MODE_LOCKED; +import static android.app.ActivityManager.LOCK_TASK_MODE_NONE; +import static android.app.StatusBarManager.DISABLE_BACK; +import static android.app.StatusBarManager.DISABLE_HOME; +import static android.app.StatusBarManager.DISABLE_RECENT; +import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.ViewRootImpl.CLIENT_IMMERSIVE_CONFIRMATION; +import static android.view.ViewRootImpl.CLIENT_TRANSIENT; +import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED; +import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID; + +import android.animation.ArgbEvaluator; +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.ColorStateList; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.database.ContentObserver; +import android.graphics.Insets; +import android.graphics.PixelFormat; +import android.graphics.drawable.ColorDrawable; +import android.os.Binder; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; +import android.service.vr.IVrManager; +import android.service.vr.IVrStateCallbacks; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.Display; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.WindowInsets; +import android.view.WindowInsets.Type; +import android.view.WindowManager; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.Button; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import com.android.systemui.CoreStartable; +import com.android.systemui.R; +import com.android.systemui.shared.system.TaskStackChangeListener; +import com.android.systemui.shared.system.TaskStackChangeListeners; +import com.android.systemui.util.settings.SecureSettings; + +import javax.inject.Inject; + +/** + * Helper to manage showing/hiding a confirmation prompt when the navigation bar is hidden + * entering immersive mode. + */ +public class ImmersiveModeConfirmation implements CoreStartable, CommandQueue.Callbacks, + TaskStackChangeListener { + private static final String TAG = "ImmersiveModeConfirm"; + private static final boolean DEBUG = false; + private static final boolean DEBUG_SHOW_EVERY_TIME = false; // super annoying, use with caution + private static final String CONFIRMED = "confirmed"; + private static final int IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE = + WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL; + + private static boolean sConfirmed; + private final SecureSettings mSecureSettings; + + private Context mDisplayContext; + private final Context mSysUiContext; + private final Handler mHandler = new H(Looper.getMainLooper()); + private long mShowDelayMs = 0L; + private final IBinder mWindowToken = new Binder(); + private final CommandQueue mCommandQueue; + + private ClingWindowView mClingWindow; + /** The last {@link WindowManager} that is used to add the confirmation window. */ + @Nullable + private WindowManager mWindowManager; + /** + * The WindowContext that is registered with {@link #mWindowManager} with options to specify the + * {@link RootDisplayArea} to attach the confirmation window. + */ + @Nullable + private Context mWindowContext; + /** + * The root display area feature id that the {@link #mWindowContext} is attaching to. + */ + private int mWindowContextRootDisplayAreaId = FEATURE_UNDEFINED; + // Local copy of vr mode enabled state, to avoid calling into VrManager with + // the lock held. + private boolean mVrModeEnabled = false; + private boolean mCanSystemBarsBeShownByUser = true; + private int mLockTaskState = LOCK_TASK_MODE_NONE; + private boolean mNavBarEmpty; + + private ContentObserver mContentObserver; + + @Inject + public ImmersiveModeConfirmation(Context context, CommandQueue commandQueue, + SecureSettings secureSettings) { + mSysUiContext = context; + final Display display = mSysUiContext.getDisplay(); + mDisplayContext = display.getDisplayId() == DEFAULT_DISPLAY + ? mSysUiContext : mSysUiContext.createDisplayContext(display); + mCommandQueue = commandQueue; + mSecureSettings = secureSettings; + } + + boolean loadSetting(int currentUserId) { + final boolean wasConfirmed = sConfirmed; + sConfirmed = false; + if (DEBUG) Log.d(TAG, String.format("loadSetting() currentUserId=%d", currentUserId)); + String value = null; + try { + value = mSecureSettings.getStringForUser(Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS, + UserHandle.USER_CURRENT); + sConfirmed = CONFIRMED.equals(value); + if (DEBUG) Log.d(TAG, "Loaded sConfirmed=" + sConfirmed); + } catch (Throwable t) { + Log.w(TAG, "Error loading confirmations, value=" + value, t); + } + return sConfirmed != wasConfirmed; + } + + private static void saveSetting(Context context) { + if (DEBUG) Log.d(TAG, "saveSetting()"); + try { + final String value = sConfirmed ? CONFIRMED : null; + Settings.Secure.putStringForUser(context.getContentResolver(), + Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS, + value, + UserHandle.USER_CURRENT); + if (DEBUG) Log.d(TAG, "Saved value=" + value); + } catch (Throwable t) { + Log.w(TAG, "Error saving confirmations, sConfirmed=" + sConfirmed, t); + } + } + + @Override + public void onDisplayRemoved(int displayId) { + if (displayId != mSysUiContext.getDisplayId()) { + return; + } + mHandler.removeMessages(H.SHOW); + mHandler.removeMessages(H.HIDE); + IVrManager vrManager = IVrManager.Stub.asInterface( + ServiceManager.getService(Context.VR_SERVICE)); + if (vrManager != null) { + try { + vrManager.unregisterListener(mVrStateCallbacks); + } catch (RemoteException ex) { + } + } + mCommandQueue.removeCallback(this); + } + + private void onSettingChanged(int currentUserId) { + final boolean changed = loadSetting(currentUserId); + // Remove the window if the setting changes to be confirmed. + if (changed && sConfirmed) { + mHandler.sendEmptyMessage(H.HIDE); + } + } + + @Override + public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) { + mHandler.removeMessages(H.SHOW); + if (isImmersiveMode) { + if (DEBUG) Log.d(TAG, "immersiveModeChanged() sConfirmed=" + sConfirmed); + boolean userSetupComplete = (mSecureSettings.getIntForUser( + Settings.Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) != 0); + + if ((DEBUG_SHOW_EVERY_TIME || !sConfirmed) + && userSetupComplete + && !mVrModeEnabled + && mCanSystemBarsBeShownByUser + && !mNavBarEmpty + && !UserManager.isDeviceInDemoMode(mDisplayContext) + && (mLockTaskState != LOCK_TASK_MODE_LOCKED)) { + final Message msg = mHandler.obtainMessage( + H.SHOW); + msg.arg1 = rootDisplayAreaId; + mHandler.sendMessageDelayed(msg, mShowDelayMs); + } + } else { + mHandler.sendEmptyMessage(H.HIDE); + } + } + + @Override + public void disable(int displayId, int disableFlag, int disableFlag2, boolean animate) { + if (mSysUiContext.getDisplayId() != displayId) { + return; + } + final int disableNavigationBar = (DISABLE_HOME | DISABLE_BACK | DISABLE_RECENT); + mNavBarEmpty = (disableFlag & disableNavigationBar) == disableNavigationBar; + } + + @Override + public void confirmImmersivePrompt() { + if (mClingWindow != null) { + if (DEBUG) Log.d(TAG, "confirmImmersivePrompt()"); + mHandler.post(mConfirm); + } + } + + private void handleHide() { + if (mClingWindow != null) { + if (DEBUG) Log.d(TAG, "Hiding immersive mode confirmation"); + if (mWindowManager != null) { + try { + mWindowManager.removeView(mClingWindow); + } catch (WindowManager.InvalidDisplayException e) { + Log.w(TAG, "Fail to hide the immersive confirmation window because of " + + e); + } + mWindowManager = null; + mWindowContext = null; + } + mClingWindow = null; + } + } + + private WindowManager.LayoutParams getClingWindowLayoutParams() { + final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE, + WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, + PixelFormat.TRANSLUCENT); + lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~Type.statusBars()); + // Trusted overlay so touches outside the touchable area are allowed to pass through + lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS + | WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY + | WindowManager.LayoutParams.PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW; + lp.setTitle("ImmersiveModeConfirmation"); + lp.windowAnimations = com.android.internal.R.style.Animation_ImmersiveModeConfirmation; + lp.token = getWindowToken(); + return lp; + } + + private FrameLayout.LayoutParams getBubbleLayoutParams() { + return new FrameLayout.LayoutParams( + mSysUiContext.getResources().getDimensionPixelSize( + R.dimen.immersive_mode_cling_width), + ViewGroup.LayoutParams.WRAP_CONTENT, + Gravity.CENTER_HORIZONTAL | Gravity.TOP); + } + + /** + * @return the window token that's used by all ImmersiveModeConfirmation windows. + */ + IBinder getWindowToken() { + return mWindowToken; + } + + @Override + public void start() { + if (CLIENT_TRANSIENT || CLIENT_IMMERSIVE_CONFIRMATION) { + mCommandQueue.addCallback(this); + + final Resources r = mSysUiContext.getResources(); + mShowDelayMs = r.getInteger(R.integer.dock_enter_exit_duration) * 3L; + mCanSystemBarsBeShownByUser = !r.getBoolean( + R.bool.config_remoteInsetsControllerControlsSystemBars) || r.getBoolean( + R.bool.config_remoteInsetsControllerSystemBarsCanBeShownByUserAction); + IVrManager vrManager = IVrManager.Stub.asInterface( + ServiceManager.getService(Context.VR_SERVICE)); + if (vrManager != null) { + try { + mVrModeEnabled = vrManager.getVrModeState(); + vrManager.registerListener(mVrStateCallbacks); + mVrStateCallbacks.onVrStateChanged(mVrModeEnabled); + } catch (RemoteException e) { + // Ignore, we cannot do anything if we failed to access vr manager. + } + } + TaskStackChangeListeners.getInstance().registerTaskStackListener(this); + mContentObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange) { + onSettingChanged(mSysUiContext.getUserId()); + } + }; + + // Register to listen for changes in Settings.Secure settings. + mSecureSettings.registerContentObserverForUser( + Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS, mContentObserver, + UserHandle.USER_CURRENT); + mSecureSettings.registerContentObserverForUser( + Settings.Secure.USER_SETUP_COMPLETE, mContentObserver, + UserHandle.USER_CURRENT); + } + } + + private final IVrStateCallbacks mVrStateCallbacks = new IVrStateCallbacks.Stub() { + @Override + public void onVrStateChanged(boolean enabled) { + mVrModeEnabled = enabled; + if (mVrModeEnabled) { + mHandler.removeMessages(H.SHOW); + mHandler.sendEmptyMessage(H.HIDE); + } + } + }; + + private class ClingWindowView extends FrameLayout { + private static final int BGCOLOR = 0x80000000; + private static final int OFFSET_DP = 96; + private static final int ANIMATION_DURATION = 250; + + private final Runnable mConfirm; + private final ColorDrawable mColor = new ColorDrawable(0); + private final Interpolator mInterpolator; + private ValueAnimator mColorAnim; + private ViewGroup mClingLayout; + + private Runnable mUpdateLayoutRunnable = new Runnable() { + @Override + public void run() { + if (mClingLayout != null && mClingLayout.getParent() != null) { + mClingLayout.setLayoutParams(getBubbleLayoutParams()); + } + } + }; + + private ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener = + new ViewTreeObserver.OnComputeInternalInsetsListener() { + private final int[] mTmpInt2 = new int[2]; + + @Override + public void onComputeInternalInsets( + ViewTreeObserver.InternalInsetsInfo inoutInfo) { + // Set touchable region to cover the cling layout. + mClingLayout.getLocationInWindow(mTmpInt2); + inoutInfo.setTouchableInsets( + ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); + inoutInfo.touchableRegion.set( + mTmpInt2[0], + mTmpInt2[1], + mTmpInt2[0] + mClingLayout.getWidth(), + mTmpInt2[1] + mClingLayout.getHeight()); + } + }; + + private BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_CONFIGURATION_CHANGED)) { + post(mUpdateLayoutRunnable); + } + } + }; + + ClingWindowView(Context context, Runnable confirm) { + super(context); + mConfirm = confirm; + setBackground(mColor); + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); + mInterpolator = AnimationUtils + .loadInterpolator(mContext, android.R.interpolator.linear_out_slow_in); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + DisplayMetrics metrics = new DisplayMetrics(); + mContext.getDisplay().getMetrics(metrics); + float density = metrics.density; + + getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsListener); + + // create the confirmation cling + mClingLayout = (ViewGroup) + View.inflate(mSysUiContext, R.layout.immersive_mode_cling, null); + + TypedArray ta = mDisplayContext.obtainStyledAttributes( + new int[]{android.R.attr.colorAccent}); + int colorAccent = ta.getColor(0, 0); + ta.recycle(); + mClingLayout.setBackgroundColor(colorAccent); + ImageView expandMore = mClingLayout.findViewById(R.id.immersive_cling_ic_expand_more); + if (expandMore != null) { + expandMore.setImageTintList(ColorStateList.valueOf(colorAccent)); + } + ImageView lightBgCirc = mClingLayout.findViewById(R.id.immersive_cling_back_bg_light); + if (lightBgCirc != null) { + // Set transparency to 50% + lightBgCirc.setImageAlpha(128); + } + + final Button ok = mClingLayout.findViewById(R.id.ok); + ok.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mConfirm.run(); + } + }); + addView(mClingLayout, getBubbleLayoutParams()); + + if (ActivityManager.isHighEndGfx()) { + final View cling = mClingLayout; + cling.setAlpha(0f); + cling.setTranslationY(-OFFSET_DP * density); + + postOnAnimation(new Runnable() { + @Override + public void run() { + cling.animate() + .alpha(1f) + .translationY(0) + .setDuration(ANIMATION_DURATION) + .setInterpolator(mInterpolator) + .withLayer() + .start(); + + mColorAnim = ValueAnimator.ofObject(new ArgbEvaluator(), 0, BGCOLOR); + mColorAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + final int c = (Integer) animation.getAnimatedValue(); + mColor.setColor(c); + } + }); + mColorAnim.setDuration(ANIMATION_DURATION); + mColorAnim.setInterpolator(mInterpolator); + mColorAnim.start(); + } + }); + } else { + mColor.setColor(BGCOLOR); + } + + mContext.registerReceiver(mReceiver, + new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED)); + } + + @Override + public void onDetachedFromWindow() { + mContext.unregisterReceiver(mReceiver); + } + + @Override + public boolean onTouchEvent(MotionEvent motion) { + return true; + } + + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + // we will be hiding the nav bar, so layout as if it's already hidden + return new WindowInsets.Builder(insets).setInsets( + Type.systemBars(), Insets.NONE).build(); + } + } + + /** + * To get window manager for the display. + * + * @return the WindowManager specifying with the {@code rootDisplayAreaId} to attach the + * confirmation window. + */ + @NonNull + private WindowManager createWindowManager(int rootDisplayAreaId) { + if (mWindowManager != null) { + throw new IllegalStateException( + "Must not create a new WindowManager while there is an existing one"); + } + // Create window context to specify the RootDisplayArea + final Bundle options = getOptionsForWindowContext(rootDisplayAreaId); + mWindowContextRootDisplayAreaId = rootDisplayAreaId; + mWindowContext = mDisplayContext.createWindowContext( + IMMERSIVE_MODE_CONFIRMATION_WINDOW_TYPE, options); + mWindowManager = mWindowContext.getSystemService(WindowManager.class); + return mWindowManager; + } + + /** + * Returns options that specify the {@link RootDisplayArea} to attach the confirmation window. + * {@code null} if the {@code rootDisplayAreaId} is {@link FEATURE_UNDEFINED}. + */ + @Nullable + private Bundle getOptionsForWindowContext(int rootDisplayAreaId) { + // In case we don't care which root display area the window manager is specifying. + if (rootDisplayAreaId == FEATURE_UNDEFINED) { + return null; + } + + final Bundle options = new Bundle(); + options.putInt(KEY_ROOT_DISPLAY_AREA_ID, rootDisplayAreaId); + return options; + } + + private void handleShow(int rootDisplayAreaId) { + if (mClingWindow != null) { + if (rootDisplayAreaId == mWindowContextRootDisplayAreaId) { + if (DEBUG) Log.d(TAG, "Immersive mode confirmation has already been shown"); + return; + } else { + // Hide the existing confirmation before show a new one in the new root. + if (DEBUG) Log.d(TAG, "Immersive mode confirmation was shown in a different root"); + handleHide(); + } + } + if (DEBUG) Log.d(TAG, "Showing immersive mode confirmation"); + mClingWindow = new ClingWindowView(mDisplayContext, mConfirm); + // show the confirmation + final WindowManager.LayoutParams lp = getClingWindowLayoutParams(); + try { + createWindowManager(rootDisplayAreaId).addView(mClingWindow, lp); + } catch (WindowManager.InvalidDisplayException e) { + Log.w(TAG, "Fail to show the immersive confirmation window because of " + e); + } + } + + private final Runnable mConfirm = new Runnable() { + @Override + public void run() { + if (DEBUG) Log.d(TAG, "mConfirm.run()"); + if (!sConfirmed) { + sConfirmed = true; + saveSetting(mDisplayContext); + } + handleHide(); + } + }; + + private final class H extends Handler { + private static final int SHOW = 1; + private static final int HIDE = 2; + + H(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) { + return; + } + switch(msg.what) { + case SHOW: + handleShow(msg.arg1); + break; + case HIDE: + handleHide(); + break; + } + } + } + + @Override + public void onLockTaskModeChanged(int lockTaskState) { + mLockTaskState = lockTaskState; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java index 324e97294f4e..645595c1f7bf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java @@ -23,6 +23,7 @@ import android.os.Process; import android.os.VibrationAttributes; import android.os.VibrationEffect; import android.os.Vibrator; +import android.view.View; import androidx.annotation.VisibleForTesting; @@ -151,4 +152,20 @@ public class VibratorHelper { BIOMETRIC_ERROR_VIBRATION_EFFECT, reason, HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES); } + + /** + * Perform a vibration using a view and the one-way API with flags + * @see View#performHapticFeedback(int feedbackConstant, int flags) + */ + public void performHapticFeedback(@NonNull View view, int feedbackConstant, int flags) { + view.performHapticFeedback(feedbackConstant, flags); + } + + /** + * Perform a vibration using a view and the one-way API + * @see View#performHapticFeedback(int feedbackConstant) + */ + public void performHapticFeedback(@NonNull View view, int feedbackConstant) { + view.performHapticFeedback(feedbackConstant); + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java index 93b9ac61ebec..f381b3792f68 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java @@ -83,8 +83,6 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController.DeviceP import com.android.systemui.telephony.TelephonyListenerManager; import com.android.systemui.util.CarrierConfigTracker; -import kotlin.Unit; - import java.io.PrintWriter; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -99,6 +97,8 @@ import java.util.stream.Collectors; import javax.inject.Inject; +import kotlin.Unit; + /** Platform implementation of the network controller. **/ @SysUISingleton public class NetworkControllerImpl extends BroadcastReceiver @@ -465,7 +465,7 @@ public class NetworkControllerImpl extends BroadcastReceiver mDemoModeController.addCallback(this); - mDumpManager.registerDumpable(TAG, this); + mDumpManager.registerNormalDumpable(TAG, this); } private final Runnable mClearForceValidated = () -> { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt index 56390002490c..6e8b8bdebbe3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt @@ -265,6 +265,11 @@ class SystemEventChipAnimationController @Inject constructor( // not animating then [prepareChipAnimation] will take care of it for us currentAnimatedView?.let { updateChipBounds(it, newContentArea) + // Since updateCurrentAnimatedView can only be called during an animation, we + // have to create a dummy animator here to apply the new chip bounds + val animator = ValueAnimator.ofInt(0, 1).setDuration(0) + animator.addUpdateListener { updateCurrentAnimatedView() } + animator.start() } } }) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt index 62a0d138fd05..5c2f9a8d28ec 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt @@ -39,7 +39,10 @@ class StackCoordinator @Inject internal constructor( override fun attach(pipeline: NotifPipeline) { pipeline.addOnAfterRenderListListener(::onAfterRenderList) - groupExpansionManagerImpl.attach(pipeline) + // TODO(b/282865576): This has an issue where it makes changes to some groups without + // notifying listeners. To be fixed in QPR, but for now let's comment it out to avoid the + // group expansion bug. + // groupExpansionManagerImpl.attach(pipeline) } fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) = 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 42b99a1dc68c..ed489a6c5343 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 @@ -3678,6 +3678,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView pw.print(", mShowingPublicInitialized: " + mShowingPublicInitialized); NotificationContentView showingLayout = getShowingLayout(); pw.print(", privateShowing: " + (showingLayout == mPrivateLayout)); + pw.print(", mShowNoBackground: " + mShowNoBackground); pw.println(); showingLayout.dump(pw, args); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java index a4e8c2ece894..80f5d1939ac0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java @@ -21,12 +21,16 @@ import static com.android.systemui.statusbar.NotificationRemoteInputManager.ENAB import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.statusbar.notification.NotificationUtils.logKey; +import android.net.Uri; +import android.os.UserHandle; +import android.provider.Settings; import android.util.Log; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.statusbar.IStatusBarService; @@ -71,6 +75,10 @@ import javax.inject.Named; @NotificationRowScope public class ExpandableNotificationRowController implements NotifViewController { private static final String TAG = "NotifRowController"; + + static final Uri BUBBLES_SETTING_URI = + Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_BUBBLES); + private static final String BUBBLES_SETTING_ENABLED_VALUE = "1"; private final ExpandableNotificationRow mView; private final NotificationListContainer mListContainer; private final RemoteInputViewSubcomponent.Factory mRemoteInputViewSubcomponentFactory; @@ -104,6 +112,23 @@ public class ExpandableNotificationRowController implements NotifViewController private final ExpandableNotificationRowDragController mDragController; private final NotificationDismissibilityProvider mDismissibilityProvider; private final IStatusBarService mStatusBarService; + + private final NotificationSettingsController mSettingsController; + + @VisibleForTesting + final NotificationSettingsController.Listener mSettingsListener = + new NotificationSettingsController.Listener() { + @Override + public void onSettingChanged(Uri setting, int userId, String value) { + if (BUBBLES_SETTING_URI.equals(setting)) { + final int viewUserId = mView.getEntry().getSbn().getUserId(); + if (viewUserId == UserHandle.USER_ALL || viewUserId == userId) { + mView.getPrivateLayout().setBubblesEnabledForUser( + BUBBLES_SETTING_ENABLED_VALUE.equals(value)); + } + } + } + }; private final ExpandableNotificationRow.ExpandableNotificationRowLogger mLoggerCallback = new ExpandableNotificationRow.ExpandableNotificationRowLogger() { @Override @@ -201,6 +226,7 @@ public class ExpandableNotificationRowController implements NotifViewController FeatureFlags featureFlags, PeopleNotificationIdentifier peopleNotificationIdentifier, Optional<BubblesManager> bubblesManagerOptional, + NotificationSettingsController settingsController, ExpandableNotificationRowDragController dragController, NotificationDismissibilityProvider dismissibilityProvider, IStatusBarService statusBarService) { @@ -229,6 +255,7 @@ public class ExpandableNotificationRowController implements NotifViewController mFeatureFlags = featureFlags; mPeopleNotificationIdentifier = peopleNotificationIdentifier; mBubblesManagerOptional = bubblesManagerOptional; + mSettingsController = settingsController; mDragController = dragController; mMetricsLogger = metricsLogger; mChildrenContainerLogger = childrenContainerLogger; @@ -298,12 +325,14 @@ public class ExpandableNotificationRowController implements NotifViewController NotificationMenuRowPlugin.class, false /* Allow multiple */); mView.setOnKeyguard(mStatusBarStateController.getState() == KEYGUARD); mStatusBarStateController.addCallback(mStatusBarStateListener); + mSettingsController.addCallback(BUBBLES_SETTING_URI, mSettingsListener); } @Override public void onViewDetachedFromWindow(View v) { mPluginManager.removePluginListener(mView); mStatusBarStateController.removeCallback(mStatusBarStateListener); + mSettingsController.removeCallback(BUBBLES_SETTING_URI, mSettingsListener); } }); } 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 20f4429f294b..7b6802f95cda 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 @@ -44,6 +44,7 @@ import android.widget.LinearLayout; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.R; +import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.SmartReplyController; @@ -65,7 +66,6 @@ import com.android.systemui.statusbar.policy.SmartReplyStateInflaterKt; import com.android.systemui.statusbar.policy.SmartReplyView; import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent; import com.android.systemui.util.Compile; -import com.android.systemui.wmshell.BubblesManager; import java.io.PrintWriter; import java.util.ArrayList; @@ -134,6 +134,7 @@ public class NotificationContentView extends FrameLayout implements Notification private PeopleNotificationIdentifier mPeopleIdentifier; private RemoteInputViewSubcomponent.Factory mRemoteInputSubcomponentFactory; private IStatusBarService mStatusBarService; + private boolean mBubblesEnabledForUser; /** * List of listeners for when content views become inactive (i.e. not the showing view). @@ -1440,12 +1441,17 @@ public class NotificationContentView extends FrameLayout implements Notification } } + @Background + public void setBubblesEnabledForUser(boolean enabled) { + mBubblesEnabledForUser = enabled; + } + @VisibleForTesting boolean shouldShowBubbleButton(NotificationEntry entry) { boolean isPersonWithShortcut = mPeopleIdentifier.getPeopleNotificationType(entry) >= PeopleNotificationIdentifier.TYPE_FULL_PERSON; - return BubblesManager.areBubblesEnabled(mContext, entry.getSbn().getUser()) + return mBubblesEnabledForUser && isPersonWithShortcut && entry.getBubbleMetadata() != null; } @@ -2079,6 +2085,7 @@ public class NotificationContentView extends FrameLayout implements Notification pw.print("null"); } pw.println(); + pw.println("mBubblesEnabledForUser: " + mBubblesEnabledForUser); pw.print("RemoteInputViews { "); pw.print(" visibleType: " + mVisibleType); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSettingsController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSettingsController.java new file mode 100644 index 000000000000..585ff523b9a0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationSettingsController.java @@ -0,0 +1,167 @@ +/* + * 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.row; + +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerExecutor; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.systemui.Dumpable; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dump.DumpManager; +import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.HashMap; + +import javax.inject.Inject; + +/** + * Centralized controller for listening to Secure Settings changes and informing in-process + * listeners, on a background thread. + */ +@SysUISingleton +public class NotificationSettingsController implements Dumpable { + + private final static String TAG = "NotificationSettingsController"; + private final UserTracker mUserTracker; + private final UserTracker.Callback mCurrentUserTrackerCallback; + private final Handler mHandler; + private final ContentObserver mContentObserver; + private final SecureSettings mSecureSettings; + private final HashMap<Uri, ArrayList<Listener>> mListeners = new HashMap<>(); + + @Inject + public NotificationSettingsController(UserTracker userTracker, + @Background Handler handler, + SecureSettings secureSettings, + DumpManager dumpManager) { + mUserTracker = userTracker; + mHandler = handler; + mSecureSettings = secureSettings; + mContentObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + synchronized (mListeners) { + if (mListeners.containsKey(uri)) { + for (Listener listener : mListeners.get(uri)) { + notifyListener(listener, uri); + } + } + } + } + }; + + mCurrentUserTrackerCallback = new UserTracker.Callback() { + @Override + public void onUserChanged(int newUser, Context userContext) { + synchronized (mListeners) { + if (mListeners.size() > 0) { + mSecureSettings.unregisterContentObserver(mContentObserver); + for (Uri uri : mListeners.keySet()) { + mSecureSettings.registerContentObserverForUser( + uri, false, mContentObserver, newUser); + } + } + } + } + }; + mUserTracker.addCallback(mCurrentUserTrackerCallback, new HandlerExecutor(handler)); + + dumpManager.registerNormalDumpable(TAG, this); + } + + /** + * Register callback whenever the given secure settings changes. + * + * On registration, will call back on the provided handler with the current value of + * the setting. + */ + public void addCallback(@NonNull Uri uri, @NonNull Listener listener) { + if (uri == null || listener == null) { + return; + } + synchronized (mListeners) { + ArrayList<Listener> currentListeners = mListeners.get(uri); + if (currentListeners == null) { + currentListeners = new ArrayList<>(); + } + if (!currentListeners.contains(listener)) { + currentListeners.add(listener); + } + mListeners.put(uri, currentListeners); + if (currentListeners.size() == 1) { + mSecureSettings.registerContentObserverForUser( + uri, false, mContentObserver, mUserTracker.getUserId()); + } + } + mHandler.post(() -> notifyListener(listener, uri)); + + } + + public void removeCallback(Uri uri, Listener listener) { + synchronized (mListeners) { + ArrayList<Listener> currentListeners = mListeners.get(uri); + + if (currentListeners != null) { + currentListeners.remove(listener); + } + if (currentListeners == null || currentListeners.size() == 0) { + mListeners.remove(uri); + } + + if (mListeners.size() == 0) { + mSecureSettings.unregisterContentObserver(mContentObserver); + } + } + } + + @Override + public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { + synchronized (mListeners) { + pw.println("Settings Uri Listener List:"); + for (Uri uri : mListeners.keySet()) { + pw.println(" Uri=" + uri); + for (Listener listener : mListeners.get(uri)) { + pw.println(" Listener=" + listener.getClass().getName()); + } + } + } + } + + private void notifyListener(Listener listener, Uri uri) { + final String setting = uri == null ? null : uri.getLastPathSegment(); + int userId = mUserTracker.getUserId(); + listener.onSettingChanged(uri, userId, mSecureSettings.getStringForUser(setting, userId)); + } + + /** + * Listener invoked whenever settings are changed. + */ + public interface Listener { + void onSettingChanged(@NonNull Uri setting, int userId, @Nullable String value); + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt index 26b51a95acad..dcd18dd7d1bf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt @@ -45,6 +45,7 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.plugins.ActivityStarter.OnDismissAction import com.android.systemui.settings.UserTracker import com.android.systemui.shade.ShadeController +import com.android.systemui.shade.ShadeViewController import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.SysuiStatusBarStateController @@ -69,6 +70,7 @@ constructor( private val biometricUnlockControllerLazy: Lazy<BiometricUnlockController>, private val keyguardViewMediatorLazy: Lazy<KeyguardViewMediator>, private val shadeControllerLazy: Lazy<ShadeController>, + private val shadeViewControllerLazy: Lazy<ShadeViewController>, private val statusBarKeyguardViewManagerLazy: Lazy<StatusBarKeyguardViewManager>, private val notifShadeWindowControllerLazy: Lazy<NotificationShadeWindowController>, private val activityLaunchAnimator: ActivityLaunchAnimator, @@ -896,7 +898,7 @@ constructor( if (dismissShade) { return StatusBarLaunchAnimatorController( animationController, - it.shadeViewController, + shadeViewControllerLazy.get(), shadeControllerLazy.get(), notifShadeWindowControllerLazy.get(), isLaunchForActivity diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index dc021feffd63..fcfb7ff8525d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.phone; import static android.app.StatusBarManager.SESSION_KEYGUARD; +import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION; import static com.android.systemui.keyguard.WakefulnessLifecycle.UNKNOWN_LAST_WAKE_TIME; import android.annotation.IntDef; @@ -30,6 +31,7 @@ import android.metrics.LogMaker; import android.os.Handler; import android.os.PowerManager; import android.os.Trace; +import android.view.HapticFeedbackConstants; import androidx.annotation.Nullable; @@ -51,6 +53,7 @@ import com.android.systemui.biometrics.AuthController; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; @@ -160,7 +163,6 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp private KeyguardViewController mKeyguardViewController; private DozeScrimController mDozeScrimController; private KeyguardViewMediator mKeyguardViewMediator; - private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private PendingAuthenticated mPendingAuthenticated = null; private boolean mHasScreenTurnedOnSinceAuthenticating; private boolean mFadedAwayAfterWakeAndUnlock; @@ -178,6 +180,8 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp private long mLastFpFailureUptimeMillis; private int mNumConsecutiveFpFailures; + private final FeatureFlags mFeatureFlags; + private static final class PendingAuthenticated { public final int userId; public final BiometricSourceType biometricSourceType; @@ -282,7 +286,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp ScreenOffAnimationController screenOffAnimationController, VibratorHelper vibrator, SystemClock systemClock, - StatusBarKeyguardViewManager statusBarKeyguardViewManager + FeatureFlags featureFlags ) { mPowerManager = powerManager; mUpdateMonitor = keyguardUpdateMonitor; @@ -310,7 +314,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp mVibratorHelper = vibrator; mLogger = biometricUnlockLogger; mSystemClock = systemClock; - mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; + mFeatureFlags = featureFlags; dumpManager.registerDumpable(this); } @@ -452,19 +456,8 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp // During wake and unlock, we need to draw black before waking up to avoid abrupt // brightness changes due to display state transitions. Runnable wakeUp = ()-> { - // Check to see if we are still locked when we are waking and unlocking from dream. - // This runnable should be executed after unlock. If that's true, we could be not - // dreaming, but still locked. In this case, we should attempt to authenticate instead - // of waking up. - if (mode == MODE_WAKE_AND_UNLOCK_FROM_DREAM - && !mKeyguardStateController.isUnlocked() - && !mUpdateMonitor.isDreaming()) { - // Post wakeUp runnable is called from a callback in keyguard. - mHandler.post(() -> mKeyguardViewController.notifyKeyguardAuthenticated( - false /* primaryAuth */)); - } else if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) { + if (!wasDeviceInteractive || mUpdateMonitor.isDreaming()) { mLogger.i("bio wakelock: Authenticated, waking up..."); - mPowerManager.wakeUp( mSystemClock.uptimeMillis(), PowerManager.WAKE_REASON_BIOMETRIC, @@ -476,7 +469,10 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp Trace.endSection(); }; - if (mMode != MODE_NONE && mMode != MODE_WAKE_AND_UNLOCK_FROM_DREAM) { + final boolean wakingFromDream = mMode == MODE_WAKE_AND_UNLOCK_FROM_DREAM + && !mStatusBarStateController.isDozing(); + + if (mMode != MODE_NONE && !wakingFromDream) { wakeUp.run(); } switch (mMode) { @@ -498,10 +494,6 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp Trace.endSection(); break; case MODE_WAKE_AND_UNLOCK_FROM_DREAM: - // In the case of waking and unlocking from dream, waking up is delayed until after - // unlock is complete to avoid conflicts during each sequence's transitions. - mStatusBarKeyguardViewManager.addAfterKeyguardGoneRunnable(wakeUp); - // Execution falls through here to proceed unlocking. case MODE_WAKE_AND_UNLOCK_PULSING: case MODE_WAKE_AND_UNLOCK: if (mMode == MODE_WAKE_AND_UNLOCK_PULSING) { @@ -765,8 +757,15 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp mLogger.d("Skip auth success haptic. Power button was recently pressed."); return; } - mVibratorHelper.vibrateAuthSuccess( - getClass().getSimpleName() + ", type =" + type + "device-entry::success"); + if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { + mVibratorHelper.performHapticFeedback( + mKeyguardViewController.getViewRootImpl().getView(), + HapticFeedbackConstants.CONFIRM + ); + } else { + mVibratorHelper.vibrateAuthSuccess( + getClass().getSimpleName() + ", type =" + type + "device-entry::success"); + } } private boolean lastWakeupFromPowerButtonWithinHapticThreshold() { @@ -779,8 +778,15 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp } private void vibrateError(BiometricSourceType type) { - mVibratorHelper.vibrateAuthError( - getClass().getSimpleName() + ", type =" + type + "device-entry::error"); + if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { + mVibratorHelper.performHapticFeedback( + mKeyguardViewController.getViewRootImpl().getView(), + HapticFeedbackConstants.REJECT + ); + } else { + mVibratorHelper.vibrateAuthError( + getClass().getSimpleName() + ", type =" + type + "device-entry::error"); + } } private void cleanup() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index acd6e49fe8c1..5c28be3bc678 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -44,7 +44,6 @@ import com.android.systemui.display.data.repository.DisplayMetricsRepository; import com.android.systemui.navigationbar.NavigationBarView; import com.android.systemui.plugins.ActivityStarter.OnDismissAction; import com.android.systemui.qs.QSPanelController; -import com.android.systemui.shade.ShadeViewController; import com.android.systemui.shared.system.RemoteAnimationRunnerCompat; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; @@ -195,9 +194,6 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner { @Override Lifecycle getLifecycle(); - /** */ - ShadeViewController getShadeViewController(); - /** Get the Keyguard Message Area that displays auth messages. */ AuthKeyguardMessageArea getKeyguardMessageArea(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index c3c9a61df2ea..6eeb25fdeb9e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -1326,7 +1326,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { } }); - mScreenOffAnimationController.initialize(this, mLightRevealScrim); + mScreenOffAnimationController.initialize(this, mShadeSurface, mLightRevealScrim); updateLightRevealScrimVisibility(); mShadeSurface.initDependencies( @@ -1677,8 +1677,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { Trace.endSection(); } - @Override - public ShadeViewController getShadeViewController() { + protected ShadeViewController getShadeViewController() { return mShadeSurface; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt index 862f169b2176..4e136deab5e3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt @@ -28,6 +28,9 @@ import com.android.systemui.Gefingerpoken import com.android.systemui.R import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.RemoteUserInput +import com.android.systemui.scene.shared.model.SceneContainerNames import com.android.systemui.shade.ShadeController import com.android.systemui.shade.ShadeLogger import com.android.systemui.shade.ShadeViewController @@ -43,6 +46,7 @@ import com.android.systemui.util.view.ViewUtil import java.util.Optional import javax.inject.Inject import javax.inject.Named +import javax.inject.Provider private const val TAG = "PhoneStatusBarViewController" @@ -53,10 +57,12 @@ class PhoneStatusBarViewController private constructor( private val centralSurfaces: CentralSurfaces, private val shadeController: ShadeController, private val shadeViewController: ShadeViewController, + private val sceneInteractor: Provider<SceneInteractor>, private val shadeLogger: ShadeLogger, private val moveFromCenterAnimationController: StatusBarMoveFromCenterAnimationController?, private val userChipViewModel: StatusBarUserChipViewModel, private val viewUtil: ViewUtil, + private val featureFlags: FeatureFlags, private val configurationController: ConfigurationController ) : ViewController<PhoneStatusBarView>(view) { @@ -164,6 +170,16 @@ class PhoneStatusBarViewController private constructor( return false } + // If scene framework is enabled, route the touch to it and + // ignore the rest of the gesture. + if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) { + sceneInteractor.get() + .onRemoteUserInput(RemoteUserInput.translateMotionEvent(event)) + // TODO(b/291965119): remove once view is expanded to cover the status bar + sceneInteractor.get().setVisible(SceneContainerNames.SYSTEM_UI_DEFAULT, true) + return false + } + if (event.action == MotionEvent.ACTION_DOWN) { // If the view that would receive the touch is disabled, just have status // bar eat the gesture. @@ -225,6 +241,7 @@ class PhoneStatusBarViewController private constructor( private val centralSurfaces: CentralSurfaces, private val shadeController: ShadeController, private val shadeViewController: ShadeViewController, + private val sceneInteractor: Provider<SceneInteractor>, private val shadeLogger: ShadeLogger, private val viewUtil: ViewUtil, private val configurationController: ConfigurationController, @@ -245,10 +262,12 @@ class PhoneStatusBarViewController private constructor( centralSurfaces, shadeController, shadeViewController, + sceneInteractor, shadeLogger, statusBarMoveFromCenterAnimationController, userChipViewModel, viewUtil, + featureFlags, configurationController ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt index c8174669cc65..89c3a02f9401 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScreenOffAnimationController.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.phone import android.view.View import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.shade.ShadeViewController import com.android.systemui.statusbar.LightRevealScrim import com.android.systemui.unfold.FoldAodAnimationController import com.android.systemui.unfold.SysUIUnfoldComponent @@ -37,8 +38,12 @@ class ScreenOffAnimationController @Inject constructor( private val animations: List<ScreenOffAnimation> = listOfNotNull(foldToAodAnimation, unlockedScreenOffAnimation) - fun initialize(centralSurfaces: CentralSurfaces, lightRevealScrim: LightRevealScrim) { - animations.forEach { it.initialize(centralSurfaces, lightRevealScrim) } + fun initialize( + centralSurfaces: CentralSurfaces, + shadeViewController: ShadeViewController, + lightRevealScrim: LightRevealScrim, + ) { + animations.forEach { it.initialize(centralSurfaces, shadeViewController, lightRevealScrim) } wakefulnessLifecycle.addObserver(this) } @@ -197,7 +202,11 @@ class ScreenOffAnimationController @Inject constructor( } interface ScreenOffAnimation { - fun initialize(centralSurfaces: CentralSurfaces, lightRevealScrim: LightRevealScrim) {} + fun initialize( + centralSurfaces: CentralSurfaces, + shadeViewController: ShadeViewController, + lightRevealScrim: LightRevealScrim, + ) {} /** * Called when started going to sleep, should return true if the animation will be played diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java index 2a039dade059..68a6b3d62bae 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java @@ -34,15 +34,21 @@ import com.android.systemui.Dumpable; import com.android.systemui.R; import com.android.systemui.ScreenDecorations; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.flags.FeatureFlags; +import com.android.systemui.flags.Flags; +import com.android.systemui.scene.domain.interactor.SceneInteractor; +import com.android.systemui.scene.shared.model.SceneContainerNames; import com.android.systemui.shade.ShadeExpansionStateManager; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener; +import com.android.systemui.util.kotlin.JavaAdapter; import java.io.PrintWriter; import javax.inject.Inject; +import javax.inject.Provider; /** * Manages what parts of the status bar are touchable. Clients are primarily UI that display in the @@ -78,6 +84,9 @@ public final class StatusBarTouchableRegionManager implements Dumpable { ConfigurationController configurationController, HeadsUpManagerPhone headsUpManager, ShadeExpansionStateManager shadeExpansionStateManager, + Provider<SceneInteractor> sceneInteractor, + Provider<JavaAdapter> javaAdapter, + FeatureFlags featureFlags, UnlockedScreenOffAnimationController unlockedScreenOffAnimationController ) { mContext = context; @@ -115,6 +124,12 @@ public final class StatusBarTouchableRegionManager implements Dumpable { mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController; shadeExpansionStateManager.addFullExpansionListener(this::onShadeExpansionFullyChanged); + if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) { + javaAdapter.get().alwaysCollectFlow( + sceneInteractor.get().isVisible(SceneContainerNames.SYSTEM_UI_DEFAULT), + this::onShadeExpansionFullyChanged); + } + mOnComputeInternalInsetsListener = this::onComputeInternalInsets; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt index 96a4d900c160..e8da9519c7ef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt @@ -20,6 +20,7 @@ import com.android.app.animation.Interpolators import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.shade.ShadeViewController import com.android.systemui.statusbar.CircleReveal import com.android.systemui.statusbar.LightRevealScrim import com.android.systemui.statusbar.NotificationShadeWindowController @@ -66,7 +67,8 @@ class UnlockedScreenOffAnimationController @Inject constructor( private val powerManager: PowerManager, private val handler: Handler = Handler() ) : WakefulnessLifecycle.Observer, ScreenOffAnimation { - private lateinit var mCentralSurfaces: CentralSurfaces + private lateinit var centralSurfaces: CentralSurfaces + private lateinit var shadeViewController: ShadeViewController /** * Whether or not [initialize] has been called to provide us with the StatusBar, * NotificationPanelViewController, and LightRevealSrim so that we can run the unlocked screen @@ -126,7 +128,7 @@ class UnlockedScreenOffAnimationController @Inject constructor( lightRevealAnimator.start() } - val animatorDurationScaleObserver = object : ContentObserver(null) { + private val animatorDurationScaleObserver = object : ContentObserver(null) { override fun onChange(selfChange: Boolean) { updateAnimatorDurationScale() } @@ -134,11 +136,13 @@ class UnlockedScreenOffAnimationController @Inject constructor( override fun initialize( centralSurfaces: CentralSurfaces, + shadeViewController: ShadeViewController, lightRevealScrim: LightRevealScrim ) { this.initialized = true this.lightRevealScrim = lightRevealScrim - this.mCentralSurfaces = centralSurfaces + this.centralSurfaces = centralSurfaces + this.shadeViewController = shadeViewController updateAnimatorDurationScale() globalSettings.registerContentObserver( @@ -184,7 +188,7 @@ class UnlockedScreenOffAnimationController @Inject constructor( // Cancel any existing CUJs before starting the animation interactionJankMonitor.cancel(CUJ_SCREEN_OFF_SHOW_AOD) - + PropertyAnimator.cancelAnimation(keyguardView, AnimatableProperty.ALPHA) PropertyAnimator.setProperty( keyguardView, AnimatableProperty.ALPHA, 1f, AnimationProperties() @@ -198,7 +202,7 @@ class UnlockedScreenOffAnimationController @Inject constructor( // Tell the CentralSurfaces to become keyguard for real - we waited on that // since it is slow and would have caused the animation to jank. - mCentralSurfaces.updateIsKeyguard() + centralSurfaces.updateIsKeyguard() // Run the callback given to us by the KeyguardVisibilityHelper. after.run() @@ -251,7 +255,7 @@ class UnlockedScreenOffAnimationController @Inject constructor( // even if we're going from SHADE to SHADE or KEYGUARD to KEYGUARD, since we might have // changed parts of the UI (such as showing AOD in the shade) without actually changing // the StatusBarState. This ensures that the UI definitely reflects the desired state. - mCentralSurfaces.updateIsKeyguard(true /* forceStateChange */) + centralSurfaces.updateIsKeyguard(true /* forceStateChange */) } } @@ -280,7 +284,7 @@ class UnlockedScreenOffAnimationController @Inject constructor( // Show AOD. That'll cause the KeyguardVisibilityHelper to call // #animateInKeyguard. - mCentralSurfaces.shadeViewController.showAodUi() + shadeViewController.showAodUi() } }, (ANIMATE_IN_KEYGUARD_DELAY * animatorDurationScale).toLong()) @@ -328,8 +332,8 @@ class UnlockedScreenOffAnimationController @Inject constructor( // We currently draw both the light reveal scrim, and the AOD UI, in the shade. If it's // already expanded and showing notifications/QS, the animation looks really messy. For now, // disable it if the notification panel is expanded. - if ((!this::mCentralSurfaces.isInitialized || - mCentralSurfaces.shadeViewController.isPanelExpanded) && + if ((!this::centralSurfaces.isInitialized || + shadeViewController.isPanelExpanded) && // Status bar might be expanded because we have started // playing the animation already !isAnimationPlaying() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index 02b7e9176cf2..e00365d8fbcb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -30,6 +30,7 @@ import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; +import android.os.Trace; import android.os.UserHandle; import android.text.Editable; import android.text.SpannedString; @@ -1032,10 +1033,12 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene } private void hideIme() { + Trace.beginSection("RemoteEditText#hideIme"); final WindowInsetsController insetsController = getWindowInsetsController(); if (insetsController != null) { insetsController.hide(WindowInsets.Type.ime()); } + Trace.endSection(); } private void defocusIfNeeded(boolean animate) { diff --git a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt index cbe402017c41..098d51e94fc7 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt @@ -31,6 +31,7 @@ import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.shade.ShadeFoldAnimator +import com.android.systemui.shade.ShadeViewController import com.android.systemui.statusbar.LightRevealScrim import com.android.systemui.statusbar.phone.CentralSurfaces import com.android.systemui.statusbar.phone.ScreenOffAnimation @@ -62,7 +63,7 @@ constructor( private val keyguardInteractor: Lazy<KeyguardInteractor>, ) : CallbackController<FoldAodAnimationStatus>, ScreenOffAnimation, WakefulnessLifecycle.Observer { - private lateinit var centralSurfaces: CentralSurfaces + private lateinit var shadeViewController: ShadeViewController private var isFolded = false private var isFoldHandled = true @@ -87,8 +88,12 @@ constructor( ) } - override fun initialize(centralSurfaces: CentralSurfaces, lightRevealScrim: LightRevealScrim) { - this.centralSurfaces = centralSurfaces + override fun initialize( + centralSurfaces: CentralSurfaces, + shadeViewController: ShadeViewController, + lightRevealScrim: LightRevealScrim, + ) { + this.shadeViewController = shadeViewController deviceStateManager.registerCallback(mainExecutor, FoldListener()) wakefulnessLifecycle.addObserver(this) @@ -128,7 +133,7 @@ constructor( } private fun getShadeFoldAnimator(): ShadeFoldAnimator = - centralSurfaces.shadeViewController.shadeFoldAnimator + shadeViewController.shadeFoldAnimator private fun setAnimationState(playing: Boolean) { shouldPlayAnimation = playing diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index 943e906a1ebc..6fafcd51bd50 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -352,6 +352,13 @@ public final class WMShell implements } @Override + public boolean isDumpCritical() { + // Dump can't be critical because the shell has to dump on the main thread for + // synchronization reasons, which isn't reliably fast. + return false; + } + + @Override public void dump(PrintWriter pw, String[] args) { // Handle commands if provided if (mShell.handleCommand(args, pw)) { diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index d44717420bdf..3abae6bcd197 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -37,6 +37,7 @@ import com.android.keyguard.KeyguardSecurityContainer.UserSwitcherViewMode.UserS import com.android.keyguard.KeyguardSecurityModel.SecurityMode import com.android.systemui.R import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.FaceAuthAccessibilityDelegate import com.android.systemui.biometrics.SideFpsController import com.android.systemui.biometrics.SideFpsUiRequestSource import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants @@ -123,6 +124,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { @Mock private lateinit var viewMediatorCallback: ViewMediatorCallback @Mock private lateinit var audioManager: AudioManager @Mock private lateinit var userInteractor: UserInteractor + @Mock private lateinit var faceAuthAccessibilityDelegate: FaceAuthAccessibilityDelegate @Captor private lateinit var swipeListenerArgumentCaptor: @@ -224,6 +226,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { mock(), { JavaAdapter(sceneTestUtils.testScope.backgroundScope) }, userInteractor, + faceAuthAccessibilityDelegate, ) { sceneInteractor } @@ -244,6 +247,11 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { } @Test + fun setAccessibilityDelegate() { + verify(view).accessibilityDelegate = eq(faceAuthAccessibilityDelegate) + } + + @Test fun showSecurityScreen_canInflateAllModes() { val modes = SecurityMode.values() for (mode in modes) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/SystemActionsTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/SystemActionsTest.java index 025c88c36203..576f689a16d0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/SystemActionsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/SystemActionsTest.java @@ -39,6 +39,7 @@ import com.android.systemui.recents.Recents; import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.ShadeController; +import com.android.systemui.shade.ShadeViewController; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.CentralSurfaces; @@ -65,6 +66,8 @@ public class SystemActionsTest extends SysuiTestCase { @Mock private ShadeController mShadeController; @Mock + private ShadeViewController mShadeViewController; + @Mock private Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy; @Mock private Optional<Recents> mRecentsOptional; @@ -82,7 +85,8 @@ public class SystemActionsTest extends SysuiTestCase { mContext.addMockSystemService(TelecomManager.class, mTelecomManager); mContext.addMockSystemService(InputManager.class, mInputManager); mSystemActions = new SystemActions(mContext, mUserTracker, mNotificationShadeController, - mShadeController, mCentralSurfacesOptionalLazy, mRecentsOptional, mDisplayTracker); + mShadeController, () -> mShadeViewController, mCentralSurfacesOptionalLazy, + mRecentsOptional, mDisplayTracker); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java index 56f81606a282..36cdfe642874 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java @@ -66,6 +66,7 @@ import android.graphics.RegionIterator; import android.os.Handler; import android.os.RemoteException; import android.os.SystemClock; +import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableResources; @@ -224,6 +225,14 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { } @Test + public void initWindowMagnificationController_checkAllowDiagonalScrollingWithSecureSettings() { + verify(mSecureSettings).getIntForUser( + eq(Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING), + /* def */ eq(1), /* userHandle= */ anyInt()); + assertTrue(mWindowMagnificationController.isDiagonalScrollingEnabled()); + } + + @Test public void enableWindowMagnification_showControlAndNotifyBoundsChanged() { mInstrumentation.runOnMainSync(() -> { mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java index eddb8d186d73..91c47480ae45 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationSettingsTest.java @@ -26,10 +26,12 @@ import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; +import static org.mockito.AdditionalAnswers.returnsSecondArg; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -109,6 +111,11 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager); mContext.addMockSystemService(Context.ACCESSIBILITY_SERVICE, mAccessibilityManager); + when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).then( + returnsSecondArg()); + when(mSecureSettings.getFloatForUser(anyString(), anyFloat(), anyInt())).then( + returnsSecondArg()); + mWindowMagnificationSettings = new WindowMagnificationSettings(mContext, mWindowMagnificationSettingsCallback, mSfVsyncFrameProvider, mSecureSettings); @@ -128,6 +135,14 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { } @Test + public void initSettingPanel_checkAllowDiagonalScrollingWithSecureSettings() { + verify(mSecureSettings).getIntForUser( + eq(Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING), + /* def */ eq(1), /* userHandle= */ anyInt()); + assertThat(mWindowMagnificationSettings.isDiagonalScrollingEnabled()).isTrue(); + } + + @Test public void showSettingPanel_hasAccessibilityWindowTitle() { setupMagnificationCapabilityAndMode( /* capability= */ ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW, @@ -273,7 +288,12 @@ public class WindowMagnificationSettingsTest extends SysuiTestCase { // Perform click diagonalScrollingSwitch.performClick(); - verify(mWindowMagnificationSettingsCallback).onSetDiagonalScrolling(!currentCheckedState); + final boolean isAllowed = !currentCheckedState; + verify(mSecureSettings).putIntForUser( + eq(Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING), + /* value= */ eq(isAllowed ? 1 : 0), + /* userHandle= */ anyInt()); + verify(mWindowMagnificationSettingsCallback).onSetDiagonalScrolling(isAllowed); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt index d5e6881500bc..7b99314692b4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt @@ -15,11 +15,13 @@ */ package com.android.systemui.accessibility.fontscaling +import android.content.res.Configuration import android.os.Handler import android.provider.Settings import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.ViewGroup +import android.widget.Button import android.widget.SeekBar import androidx.test.filters.SmallTest import com.android.systemui.R @@ -61,6 +63,7 @@ class FontScalingDialogTest : SysuiTestCase() { private lateinit var secureSettings: SecureSettings private lateinit var systemClock: FakeSystemClock private lateinit var backgroundDelayableExecutor: FakeExecutor + private lateinit var testableLooper: TestableLooper private val fontSizeValueArray: Array<String> = mContext .getResources() @@ -73,7 +76,8 @@ class FontScalingDialogTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - val mainHandler = Handler(TestableLooper.get(this).getLooper()) + testableLooper = TestableLooper.get(this) + val mainHandler = Handler(testableLooper.looper) systemSettings = FakeSettings() // Guarantee that the systemSettings always starts with the default font scale. systemSettings.putFloatForUser(Settings.System.FONT_SCALE, 1.0f, userTracker.userId) @@ -286,4 +290,26 @@ class FontScalingDialogTest : SysuiTestCase() { verify(fontScalingDialog).createTextPreview(/* index= */ 0) fontScalingDialog.dismiss() } + + @Test + fun changeFontSize_buttonIsDisabledBeforeFontSizeChangeFinishes() { + fontScalingDialog.show() + + val iconEndFrame: ViewGroup = fontScalingDialog.findViewById(R.id.icon_end_frame)!! + val doneButton: Button = fontScalingDialog.findViewById(com.android.internal.R.id.button1)!! + + iconEndFrame.performClick() + backgroundDelayableExecutor.runAllReady() + backgroundDelayableExecutor.advanceClockToNext() + backgroundDelayableExecutor.runAllReady() + + // Verify that the button is disabled before receiving onConfigurationChanged + assertThat(doneButton.isEnabled).isFalse() + + val config = Configuration() + config.fontScale = 1.15f + fontScalingDialog.onConfigurationChanged(config) + testableLooper.processAllMessages() + assertThat(doneButton.isEnabled).isTrue() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt new file mode 100644 index 000000000000..005697044c0f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt @@ -0,0 +1,113 @@ +/* + * Copyright 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.authentication.data.repository + +import android.content.pm.UserInfo +import androidx.test.filters.SmallTest +import com.android.internal.widget.LockPatternUtils +import com.android.keyguard.KeyguardSecurityModel +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectValues +import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mock +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(JUnit4::class) +class AuthenticationRepositoryTest : SysuiTestCase() { + + @Mock private lateinit var lockPatternUtils: LockPatternUtils + + private val testUtils = SceneTestUtils(this) + private val testScope = testUtils.testScope + private val userRepository = FakeUserRepository() + + private lateinit var underTest: AuthenticationRepository + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + userRepository.setUserInfos(USER_INFOS) + runBlocking { userRepository.setSelectedUserInfo(USER_INFOS[0]) } + + underTest = + AuthenticationRepositoryImpl( + applicationScope = testScope.backgroundScope, + getSecurityMode = { KeyguardSecurityModel.SecurityMode.PIN }, + backgroundDispatcher = testUtils.testDispatcher, + userRepository = userRepository, + keyguardRepository = testUtils.keyguardRepository, + lockPatternUtils = lockPatternUtils, + ) + } + + @Test + fun isAutoConfirmEnabled() = + testScope.runTest { + whenever(lockPatternUtils.isAutoPinConfirmEnabled(USER_INFOS[0].id)).thenReturn(true) + whenever(lockPatternUtils.isAutoPinConfirmEnabled(USER_INFOS[1].id)).thenReturn(false) + + val values by collectValues(underTest.isAutoConfirmEnabled) + assertThat(values.first()).isFalse() + assertThat(values.last()).isTrue() + + userRepository.setSelectedUserInfo(USER_INFOS[1]) + assertThat(values.last()).isFalse() + } + + @Test + fun isPatternVisible() = + testScope.runTest { + whenever(lockPatternUtils.isVisiblePatternEnabled(USER_INFOS[0].id)).thenReturn(false) + whenever(lockPatternUtils.isVisiblePatternEnabled(USER_INFOS[1].id)).thenReturn(true) + + val values by collectValues(underTest.isPatternVisible) + assertThat(values.first()).isTrue() + assertThat(values.last()).isFalse() + + userRepository.setSelectedUserInfo(USER_INFOS[1]) + assertThat(values.last()).isTrue() + } + + companion object { + private val USER_INFOS = + listOf( + UserInfo( + /* id= */ 100, + /* name= */ "First user", + /* flags= */ 0, + ), + UserInfo( + /* id= */ 101, + /* name= */ "Second user", + /* flags= */ 0, + ), + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt new file mode 100644 index 000000000000..ec17794d4ee2 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/FaceAuthAccessibilityDelegateTest.kt @@ -0,0 +1,111 @@ +/* + * 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.biometrics + +import android.testing.TestableLooper +import android.view.View +import android.view.accessibility.AccessibilityNodeInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.keyguard.FaceAuthApiRequestReason +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.domain.interactor.KeyguardFaceAuthInteractor +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.argumentCaptor +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@RunWith(AndroidJUnit4::class) +@SmallTest +@TestableLooper.RunWithLooper +class FaceAuthAccessibilityDelegateTest : SysuiTestCase() { + + @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock private lateinit var hostView: View + @Mock private lateinit var faceAuthInteractor: KeyguardFaceAuthInteractor + private lateinit var underTest: FaceAuthAccessibilityDelegate + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + underTest = + FaceAuthAccessibilityDelegate( + context.resources, + keyguardUpdateMonitor, + faceAuthInteractor, + ) + } + + @Test + fun shouldListenForFaceTrue_onInitializeAccessibilityNodeInfo_clickActionAdded() { + whenever(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(true) + + // WHEN node is initialized + val mockedNodeInfo = mock(AccessibilityNodeInfo::class.java) + underTest.onInitializeAccessibilityNodeInfo(hostView, mockedNodeInfo) + + // THEN a11y action is added + val argumentCaptor = argumentCaptor<AccessibilityNodeInfo.AccessibilityAction>() + verify(mockedNodeInfo).addAction(argumentCaptor.capture()) + + // AND the a11y action is a click action + assertEquals( + AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id, + argumentCaptor.value.id + ) + } + + @Test + fun shouldListenForFaceFalse_onInitializeAccessibilityNodeInfo_clickActionNotAdded() { + whenever(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(false) + + // WHEN node is initialized + val mockedNodeInfo = mock(AccessibilityNodeInfo::class.java) + underTest.onInitializeAccessibilityNodeInfo(hostView, mockedNodeInfo) + + // THEN a11y action is NOT added + verify(mockedNodeInfo, never()) + .addAction(any(AccessibilityNodeInfo.AccessibilityAction::class.java)) + } + + @Test + fun performAccessibilityAction_actionClick_retriesFaceAuth() { + whenever(keyguardUpdateMonitor.shouldListenForFace()).thenReturn(true) + + // WHEN click action is performed + underTest.performAccessibilityAction( + hostView, + AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK.id, + null + ) + + // THEN retry face auth + verify(keyguardUpdateMonitor) + .requestFaceAuth(eq(FaceAuthApiRequestReason.ACCESSIBILITY_ACTION)) + verify(faceAuthInteractor).onAccessibilityAction() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt index ecc776b98c6c..3169b091217b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SideFpsControllerTest.kt @@ -44,6 +44,9 @@ import android.view.View import android.view.ViewPropertyAnimator import android.view.WindowInsets import android.view.WindowManager +import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION +import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY +import android.view.WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG import android.view.WindowMetrics import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -53,14 +56,9 @@ import com.android.systemui.R import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase import com.android.systemui.SysuiTestableContext -import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository import com.android.systemui.biometrics.data.repository.FakeRearDisplayStateRepository import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractorImpl -import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractorImpl -import com.android.systemui.biometrics.shared.model.FingerprintSensorType -import com.android.systemui.biometrics.shared.model.SensorStrength -import com.android.systemui.biometrics.ui.viewmodel.SideFpsOverlayViewModel import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.dump.DumpManager @@ -101,7 +99,7 @@ private const val REAR_DISPLAY_MODE_DEVICE_STATE = 3 @SmallTest @RoboPilotTest @RunWith(AndroidJUnit4::class) -@TestableLooper.RunWithLooper(setAsMainLooper = true) +@TestableLooper.RunWithLooper class SideFpsControllerTest : SysuiTestCase() { @JvmField @Rule var rule = MockitoJUnit.rule() @@ -120,8 +118,6 @@ class SideFpsControllerTest : SysuiTestCase() { private lateinit var keyguardBouncerRepository: FakeKeyguardBouncerRepository private lateinit var alternateBouncerInteractor: AlternateBouncerInteractor private lateinit var displayStateInteractor: DisplayStateInteractor - private lateinit var sideFpsOverlayViewModel: SideFpsOverlayViewModel - private val fingerprintRepository = FakeFingerprintPropertyRepository() private val executor = FakeExecutor(FakeSystemClock()) private val rearDisplayStateRepository = FakeRearDisplayStateRepository() @@ -163,15 +159,6 @@ class SideFpsControllerTest : SysuiTestCase() { executor, rearDisplayStateRepository ) - sideFpsOverlayViewModel = - SideFpsOverlayViewModel(context, SideFpsOverlayInteractorImpl(fingerprintRepository)) - - fingerprintRepository.setProperties( - sensorId = 1, - strength = SensorStrength.STRONG, - sensorType = FingerprintSensorType.REAR, - sensorLocations = mapOf("" to SensorLocationInternal("", 2500, 0, 0)) - ) context.addMockSystemService(DisplayManager::class.java, displayManager) context.addMockSystemService(WindowManager::class.java, windowManager) @@ -278,7 +265,6 @@ class SideFpsControllerTest : SysuiTestCase() { executor, handler, alternateBouncerInteractor, - { sideFpsOverlayViewModel }, TestCoroutineScope(), dumpManager ) @@ -697,6 +683,106 @@ class SideFpsControllerTest : SysuiTestCase() { verify(windowManager).removeView(any()) } + /** + * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0, + * and uses RotateUtils.rotateBounds to map to the correct indicator location given the device + * rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement + * in other rotations have been omitted. + */ + @Test + fun verifiesIndicatorPlacementForXAlignedSensor_0() = + testWithDisplay( + deviceConfig = DeviceConfig.X_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_0 } + ) { + sideFpsController.overlayOffsets = sensorLocation + + sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) + + overlayController.show(SENSOR_ID, REASON_UNKNOWN) + executor.runAllReady() + + verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture()) + assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX) + assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0) + } + + /** + * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270 + * in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct + * indicator location given the device rotation. Assuming RotationUtils.rotateBounds works + * correctly, tests for indicator placement in other rotations have been omitted. + */ + @Test + fun verifiesIndicatorPlacementForXAlignedSensor_InReverseDefaultRotation_270() = + testWithDisplay( + deviceConfig = DeviceConfig.X_ALIGNED, + isReverseDefaultRotation = true, + { rotation = Surface.ROTATION_270 } + ) { + sideFpsController.overlayOffsets = sensorLocation + + sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) + + overlayController.show(SENSOR_ID, REASON_UNKNOWN) + executor.runAllReady() + + verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture()) + assertThat(overlayViewParamsCaptor.value.x).isEqualTo(sensorLocation.sensorLocationX) + assertThat(overlayViewParamsCaptor.value.y).isEqualTo(0) + } + + /** + * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_0, + * and uses RotateUtils.rotateBounds to map to the correct indicator location given the device + * rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator placement + * in other rotations have been omitted. + */ + @Test + fun verifiesIndicatorPlacementForYAlignedSensor_0() = + testWithDisplay( + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = false, + { rotation = Surface.ROTATION_0 } + ) { + sideFpsController.overlayOffsets = sensorLocation + + sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) + + overlayController.show(SENSOR_ID, REASON_UNKNOWN) + executor.runAllReady() + + verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture()) + assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth) + assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY) + } + + /** + * {@link SideFpsController#updateOverlayParams} calculates indicator placement for ROTATION_270 + * in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the correct + * indicator location given the device rotation. Assuming RotationUtils.rotateBounds works + * correctly, tests for indicator placement in other rotations have been omitted. + */ + @Test + fun verifiesIndicatorPlacementForYAlignedSensor_InReverseDefaultRotation_270() = + testWithDisplay( + deviceConfig = DeviceConfig.Y_ALIGNED, + isReverseDefaultRotation = true, + { rotation = Surface.ROTATION_270 } + ) { + sideFpsController.overlayOffsets = sensorLocation + + sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) + + overlayController.show(SENSOR_ID, REASON_UNKNOWN) + executor.runAllReady() + + verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture()) + assertThat(overlayViewParamsCaptor.value.x).isEqualTo(displayWidth - boundsWidth) + assertThat(overlayViewParamsCaptor.value.y).isEqualTo(sensorLocation.sensorLocationY) + } + @Test fun hasSideFpsSensor_withSensorProps_returnsTrue() = testWithDisplay { // By default all those tests assume the side fps sensor is available. @@ -709,6 +795,51 @@ class SideFpsControllerTest : SysuiTestCase() { assertThat(fingerprintManager.hasSideFpsSensor()).isFalse() } + + @Test + fun testLayoutParams_isKeyguardDialogType() = + testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) { + sideFpsController.overlayOffsets = sensorLocation + sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) + overlayController.show(SENSOR_ID, REASON_UNKNOWN) + executor.runAllReady() + + verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture()) + + val lpType = overlayViewParamsCaptor.value.type + + assertThat((lpType and TYPE_KEYGUARD_DIALOG) != 0).isTrue() + } + + @Test + fun testLayoutParams_hasNoMoveAnimationWindowFlag() = + testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) { + sideFpsController.overlayOffsets = sensorLocation + sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) + overlayController.show(SENSOR_ID, REASON_UNKNOWN) + executor.runAllReady() + + verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture()) + + val lpFlags = overlayViewParamsCaptor.value.privateFlags + + assertThat((lpFlags and PRIVATE_FLAG_NO_MOVE_ANIMATION) != 0).isTrue() + } + + @Test + fun testLayoutParams_hasTrustedOverlayWindowFlag() = + testWithDisplay(deviceConfig = DeviceConfig.Y_ALIGNED) { + sideFpsController.overlayOffsets = sensorLocation + sideFpsController.updateOverlayParams(windowManager.defaultDisplay, indicatorBounds) + overlayController.show(SENSOR_ID, REASON_UNKNOWN) + executor.runAllReady() + + verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture()) + + val lpFlags = overlayViewParamsCaptor.value.privateFlags + + assertThat((lpFlags and PRIVATE_FLAG_TRUSTED_OVERLAY) != 0).isTrue() + } } private fun insetsForSmallNavbar() = insetsWithBottom(60) diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt index ea2561594793..239e317b92f5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FingerprintRepositoryImplTest.kt @@ -73,15 +73,10 @@ class FingerprintRepositoryImplTest : SysuiTestCase() { @Test fun initializeProperties() = testScope.runTest { - val sensorId by collectLastValue(repository.sensorId) - val strength by collectLastValue(repository.strength) - val sensorType by collectLastValue(repository.sensorType) - val sensorLocations by collectLastValue(repository.sensorLocations) + val isInitialized = collectLastValue(repository.isInitialized) - // Assert default properties. - assertThat(sensorId).isEqualTo(-1) - assertThat(strength).isEqualTo(SensorStrength.CONVENIENCE) - assertThat(sensorType).isEqualTo(FingerprintSensorType.UNKNOWN) + assertDefaultProperties() + assertThat(isInitialized()).isFalse() val fingerprintProps = listOf( @@ -120,24 +115,31 @@ class FingerprintRepositoryImplTest : SysuiTestCase() { fingerprintAuthenticatorsCaptor.value.onAllAuthenticatorsRegistered(fingerprintProps) - assertThat(sensorId).isEqualTo(1) - assertThat(strength).isEqualTo(SensorStrength.STRONG) - assertThat(sensorType).isEqualTo(FingerprintSensorType.REAR) + assertThat(repository.sensorId.value).isEqualTo(1) + assertThat(repository.strength.value).isEqualTo(SensorStrength.STRONG) + assertThat(repository.sensorType.value).isEqualTo(FingerprintSensorType.REAR) - assertThat(sensorLocations?.size).isEqualTo(2) - assertThat(sensorLocations).containsKey("display_id_1") - with(sensorLocations?.get("display_id_1")!!) { + assertThat(repository.sensorLocations.value.size).isEqualTo(2) + assertThat(repository.sensorLocations.value).containsKey("display_id_1") + with(repository.sensorLocations.value["display_id_1"]!!) { assertThat(displayId).isEqualTo("display_id_1") assertThat(sensorLocationX).isEqualTo(100) assertThat(sensorLocationY).isEqualTo(300) assertThat(sensorRadius).isEqualTo(20) } - assertThat(sensorLocations).containsKey("") - with(sensorLocations?.get("")!!) { + assertThat(repository.sensorLocations.value).containsKey("") + with(repository.sensorLocations.value[""]!!) { assertThat(displayId).isEqualTo("") assertThat(sensorLocationX).isEqualTo(540) assertThat(sensorLocationY).isEqualTo(1636) assertThat(sensorRadius).isEqualTo(130) } + assertThat(isInitialized()).isTrue() } + + private fun assertDefaultProperties() { + assertThat(repository.sensorId.value).isEqualTo(-1) + assertThat(repository.strength.value).isEqualTo(SensorStrength.CONVENIENCE) + assertThat(repository.sensorType.value).isEqualTo(FingerprintSensorType.UNKNOWN) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt index 896f9b114679..fd96cf45504b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/SideFpsOverlayInteractorTest.kt @@ -22,7 +22,6 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.SensorStrength -import com.android.systemui.coroutines.collectLastValue import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope @@ -52,9 +51,8 @@ class SideFpsOverlayInteractorTest : SysuiTestCase() { } @Test - fun testGetOverlayoffsets() = + fun testGetOverlayOffsets() = testScope.runTest { - // Arrange. fingerprintRepository.setProperties( sensorId = 1, strength = SensorStrength.STRONG, @@ -78,33 +76,16 @@ class SideFpsOverlayInteractorTest : SysuiTestCase() { ) ) - // Act. - val offsets by collectLastValue(interactor.overlayOffsets) - val displayId by collectLastValue(interactor.displayId) + var offsets = interactor.getOverlayOffsets("display_id_1") + assertThat(offsets.displayId).isEqualTo("display_id_1") + assertThat(offsets.sensorLocationX).isEqualTo(100) + assertThat(offsets.sensorLocationY).isEqualTo(300) + assertThat(offsets.sensorRadius).isEqualTo(20) - // Assert offsets of empty displayId. - assertThat(displayId).isEqualTo("") - assertThat(offsets?.displayId).isEqualTo("") - assertThat(offsets?.sensorLocationX).isEqualTo(540) - assertThat(offsets?.sensorLocationY).isEqualTo(1636) - assertThat(offsets?.sensorRadius).isEqualTo(130) - - // Offsets should be updated correctly. - interactor.changeDisplay("display_id_1") - assertThat(displayId).isEqualTo("display_id_1") - assertThat(offsets?.displayId).isEqualTo("display_id_1") - assertThat(offsets?.sensorLocationX).isEqualTo(100) - assertThat(offsets?.sensorLocationY).isEqualTo(300) - assertThat(offsets?.sensorRadius).isEqualTo(20) - - // Should return default offset when the displayId is invalid. - interactor.changeDisplay("invalid_display_id") - assertThat(displayId).isEqualTo("invalid_display_id") - assertThat(offsets?.displayId).isEqualTo(SensorLocationInternal.DEFAULT.displayId) - assertThat(offsets?.sensorLocationX) - .isEqualTo(SensorLocationInternal.DEFAULT.sensorLocationX) - assertThat(offsets?.sensorLocationY) - .isEqualTo(SensorLocationInternal.DEFAULT.sensorLocationY) - assertThat(offsets?.sensorRadius).isEqualTo(SensorLocationInternal.DEFAULT.sensorRadius) + offsets = interactor.getOverlayOffsets("invalid_display_id") + assertThat(offsets.displayId).isEqualTo("") + assertThat(offsets.sensorLocationX).isEqualTo(540) + assertThat(offsets.sensorLocationY).isEqualTo(1636) + assertThat(offsets.sensorRadius).isEqualTo(130) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt deleted file mode 100644 index a8593216e22a..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt +++ /dev/null @@ -1,263 +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.biometrics.ui.viewmodel - -import android.graphics.Rect -import android.hardware.biometrics.SensorLocationInternal -import android.hardware.display.DisplayManagerGlobal -import android.view.Display -import android.view.DisplayAdjustments -import android.view.DisplayInfo -import android.view.Surface -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.SysuiTestableContext -import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository -import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractor -import com.android.systemui.biometrics.domain.interactor.SideFpsOverlayInteractorImpl -import com.android.systemui.biometrics.shared.model.FingerprintSensorType -import com.android.systemui.biometrics.shared.model.SensorStrength -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.util.mockito.whenever -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.mockito.ArgumentMatchers -import org.mockito.Mockito -import org.mockito.junit.MockitoJUnit - -private const val DISPLAY_ID = 2 - -@SmallTest -@RunWith(JUnit4::class) -class SideFpsOverlayViewModelTest : SysuiTestCase() { - - @JvmField @Rule var mockitoRule = MockitoJUnit.rule() - private var testScope: TestScope = TestScope(StandardTestDispatcher()) - - private val fingerprintRepository = FakeFingerprintPropertyRepository() - private lateinit var interactor: SideFpsOverlayInteractor - private lateinit var viewModel: SideFpsOverlayViewModel - - enum class DeviceConfig { - X_ALIGNED, - Y_ALIGNED, - } - - private lateinit var deviceConfig: DeviceConfig - private lateinit var indicatorBounds: Rect - private lateinit var displayBounds: Rect - private lateinit var sensorLocation: SensorLocationInternal - private var displayWidth: Int = 0 - private var displayHeight: Int = 0 - private var boundsWidth: Int = 0 - private var boundsHeight: Int = 0 - - @Before - fun setup() { - interactor = SideFpsOverlayInteractorImpl(fingerprintRepository) - - fingerprintRepository.setProperties( - sensorId = 1, - strength = SensorStrength.STRONG, - sensorType = FingerprintSensorType.REAR, - sensorLocations = - mapOf( - "" to - SensorLocationInternal( - "" /* displayId */, - 540 /* sensorLocationX */, - 1636 /* sensorLocationY */, - 130 /* sensorRadius */ - ), - "display_id_1" to - SensorLocationInternal( - "display_id_1" /* displayId */, - 100 /* sensorLocationX */, - 300 /* sensorLocationY */, - 20 /* sensorRadius */ - ) - ) - ) - } - - @Test - fun testOverlayOffsets() = - testScope.runTest { - viewModel = SideFpsOverlayViewModel(mContext, interactor) - - val interactorOffsets by collectLastValue(interactor.overlayOffsets) - val viewModelOffsets by collectLastValue(viewModel.overlayOffsets) - - assertThat(viewModelOffsets).isEqualTo(interactorOffsets) - } - - private fun testWithDisplay( - deviceConfig: DeviceConfig = DeviceConfig.X_ALIGNED, - isReverseDefaultRotation: Boolean = false, - initInfo: DisplayInfo.() -> Unit = {}, - block: () -> Unit - ) { - this.deviceConfig = deviceConfig - - when (deviceConfig) { - DeviceConfig.X_ALIGNED -> { - displayWidth = 3000 - displayHeight = 1500 - sensorLocation = SensorLocationInternal("", 2500, 0, 0) - boundsWidth = 200 - boundsHeight = 100 - } - DeviceConfig.Y_ALIGNED -> { - displayWidth = 2500 - displayHeight = 2000 - sensorLocation = SensorLocationInternal("", 0, 300, 0) - boundsWidth = 100 - boundsHeight = 200 - } - } - - indicatorBounds = Rect(0, 0, boundsWidth, boundsHeight) - displayBounds = Rect(0, 0, displayWidth, displayHeight) - - val displayInfo = DisplayInfo() - displayInfo.initInfo() - - val dmGlobal = Mockito.mock(DisplayManagerGlobal::class.java) - val display = - Display( - dmGlobal, - DISPLAY_ID, - displayInfo, - DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS - ) - - whenever(dmGlobal.getDisplayInfo(ArgumentMatchers.eq(DISPLAY_ID))).thenReturn(displayInfo) - - val sideFpsOverlayViewModelContext = - context.createDisplayContext(display) as SysuiTestableContext - sideFpsOverlayViewModelContext.orCreateTestableResources.addOverride( - com.android.internal.R.bool.config_reverseDefaultRotation, - isReverseDefaultRotation - ) - viewModel = SideFpsOverlayViewModel(sideFpsOverlayViewModelContext, interactor) - - block() - } - - /** - * {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for - * ROTATION_0, and uses RotateUtils.rotateBounds to map to the correct indicator location given - * the device rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator - * placement in other rotations have been omitted. - */ - @Test - fun verifiesIndicatorPlacementForXAlignedSensor_0() = - testScope.runTest { - testWithDisplay( - deviceConfig = DeviceConfig.X_ALIGNED, - isReverseDefaultRotation = false, - { rotation = Surface.ROTATION_0 } - ) { - viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation) - - val displayInfo: DisplayInfo = DisplayInfo() - context.display!!.getDisplayInfo(displayInfo) - assertThat(displayInfo.rotation).isEqualTo(Surface.ROTATION_0) - - assertThat(viewModel.sensorBounds.value).isNotNull() - assertThat(viewModel.sensorBounds.value.left) - .isEqualTo(sensorLocation.sensorLocationX) - assertThat(viewModel.sensorBounds.value.top).isEqualTo(0) - } - } - - /** - * {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for - * ROTATION_270 in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the - * correct indicator location given the device rotation. Assuming RotationUtils.rotateBounds - * works correctly, tests for indicator placement in other rotations have been omitted. - */ - @Test - fun verifiesIndicatorPlacementForXAlignedSensor_InReverseDefaultRotation_270() = - testScope.runTest { - testWithDisplay( - deviceConfig = DeviceConfig.X_ALIGNED, - isReverseDefaultRotation = true, - { rotation = Surface.ROTATION_270 } - ) { - viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation) - - assertThat(viewModel.sensorBounds.value).isNotNull() - assertThat(viewModel.sensorBounds.value.left) - .isEqualTo(sensorLocation.sensorLocationX) - assertThat(viewModel.sensorBounds.value.top).isEqualTo(0) - } - } - - /** - * {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for - * ROTATION_0, and uses RotateUtils.rotateBounds to map to the correct indicator location given - * the device rotation. Assuming RotationUtils.rotateBounds works correctly, tests for indicator - * placement in other rotations have been omitted. - */ - @Test - fun verifiesIndicatorPlacementForYAlignedSensor_0() = - testScope.runTest { - testWithDisplay( - deviceConfig = DeviceConfig.Y_ALIGNED, - isReverseDefaultRotation = false, - { rotation = Surface.ROTATION_0 } - ) { - viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation) - - assertThat(viewModel.sensorBounds.value).isNotNull() - assertThat(viewModel.sensorBounds.value.left).isEqualTo(displayWidth - boundsWidth) - assertThat(viewModel.sensorBounds.value.top) - .isEqualTo(sensorLocation.sensorLocationY) - } - } - - /** - * {@link SideFpsOverlayViewModel#updateSensorBounds} calculates indicator placement for - * ROTATION_270 in reverse default rotation. It then uses RotateUtils.rotateBounds to map to the - * correct indicator location given the device rotation. Assuming RotationUtils.rotateBounds - * works correctly, tests for indicator placement in other rotations have been omitted. - */ - @Test - fun verifiesIndicatorPlacementForYAlignedSensor_InReverseDefaultRotation_270() = - testScope.runTest { - testWithDisplay( - deviceConfig = DeviceConfig.Y_ALIGNED, - isReverseDefaultRotation = true, - { rotation = Surface.ROTATION_270 } - ) { - viewModel.updateSensorBounds(indicatorBounds, displayBounds, sensorLocation) - - assertThat(viewModel.sensorBounds.value).isNotNull() - assertThat(viewModel.sensorBounds.value.left).isEqualTo(displayWidth - boundsWidth) - assertThat(viewModel.sensorBounds.value.top) - .isEqualTo(sensorLocation.sensorLocationY) - } - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt index 45d1af722369..8edc6cf8dd54 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt @@ -29,6 +29,7 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -77,7 +78,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { val currentScene by collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) - val entries by collectLastValue(underTest.pinEntries) + val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) utils.authenticationRepository.setUnlocked(false) sceneInteractor.setCurrentScene( SceneTestUtils.CONTAINER_1, @@ -88,7 +89,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onShown() assertThat(message?.text).isEqualTo(ENTER_YOUR_PIN) - assertThat(entries).hasSize(0) + assertThat(pin).isEmpty() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) } @@ -98,7 +99,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { val currentScene by collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) - val entries by collectLastValue(underTest.pinEntries) + val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) sceneInteractor.setCurrentScene( @@ -112,8 +113,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onPinButtonClicked(1) assertThat(message?.text).isEmpty() - assertThat(entries).hasSize(1) - assertThat(entries?.map { it.input }).containsExactly(1) + assertThat(pin).containsExactly(1) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) } @@ -123,7 +123,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { val currentScene by collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) - val entries by collectLastValue(underTest.pinEntries) + val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) sceneInteractor.setCurrentScene( @@ -134,12 +134,12 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onShown() runCurrent() underTest.onPinButtonClicked(1) - assertThat(entries).hasSize(1) + assertThat(pin).hasSize(1) underTest.onBackspaceButtonClicked() assertThat(message?.text).isEmpty() - assertThat(entries).hasSize(0) + assertThat(pin).isEmpty() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) } @@ -148,7 +148,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { testScope.runTest { val currentScene by collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) - val entries by collectLastValue(underTest.pinEntries) + val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) sceneInteractor.setCurrentScene( @@ -166,9 +166,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onPinButtonClicked(4) underTest.onPinButtonClicked(5) - assertThat(entries).hasSize(3) - assertThat(entries?.map { it.input }).containsExactly(1, 4, 5).inOrder() - assertThat(entries?.map { it.sequenceNumber }).isInStrictOrder() + assertThat(pin).containsExactly(1, 4, 5).inOrder() } @Test @@ -177,7 +175,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { val currentScene by collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) - val entries by collectLastValue(underTest.pinEntries) + val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) sceneInteractor.setCurrentScene( @@ -195,7 +193,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onBackspaceButtonLongPressed() assertThat(message?.text).isEmpty() - assertThat(entries).hasSize(0) + assertThat(pin).isEmpty() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) } @@ -227,7 +225,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { val currentScene by collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) - val entries by collectLastValue(underTest.pinEntries) + val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) sceneInteractor.setCurrentScene( @@ -244,7 +242,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onAuthenticateButtonClicked() - assertThat(entries).hasSize(0) + assertThat(pin).isEmpty() assertThat(message?.text).isEqualTo(WRONG_PIN) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) } @@ -255,7 +253,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { val currentScene by collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) - val entries by collectLastValue(underTest.pinEntries) + val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) sceneInteractor.setCurrentScene( @@ -271,7 +269,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { underTest.onPinButtonClicked(5) // PIN is now wrong! underTest.onAuthenticateButtonClicked() assertThat(message?.text).isEqualTo(WRONG_PIN) - assertThat(entries).hasSize(0) + assertThat(pin).isEmpty() assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) // Enter the correct PIN: @@ -312,7 +310,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { val currentScene by collectLastValue(sceneInteractor.currentScene(SceneTestUtils.CONTAINER_1)) val message by collectLastValue(bouncerViewModel.message) - val entries by collectLastValue(underTest.pinEntries) + val pin by collectLastValue(underTest.pinInput.map { it.getPin() }) utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) utils.authenticationRepository.setUnlocked(false) utils.authenticationRepository.setAutoConfirmEnabled(true) @@ -329,7 +327,7 @@ class PinBouncerViewModelTest : SysuiTestCase() { FakeAuthenticationRepository.DEFAULT_PIN.last() + 1 ) // PIN is now wrong! - assertThat(entries).hasSize(0) + assertThat(pin).isEmpty() assertThat(message?.text).isEqualTo(WRONG_PIN) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer)) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt new file mode 100644 index 000000000000..4c279ea08fd7 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt @@ -0,0 +1,277 @@ +package com.android.systemui.bouncer.ui.viewmodel + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.bouncer.ui.viewmodel.EntryToken.ClearAll +import com.android.systemui.bouncer.ui.viewmodel.EntryToken.Digit +import com.android.systemui.bouncer.ui.viewmodel.PinInputSubject.Companion.assertThat +import com.android.systemui.bouncer.ui.viewmodel.PinInputViewModel.Companion.empty +import com.google.common.truth.Fact +import com.google.common.truth.FailureMetadata +import com.google.common.truth.Subject +import com.google.common.truth.Subject.Factory +import com.google.common.truth.Truth.assertAbout +import com.google.common.truth.Truth.assertThat +import java.lang.Character.isDigit +import org.junit.Assert.assertThrows +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +/** + * This test uses a mnemonic code to create and verify PinInput instances: strings of digits [0-9] + * for [Digit] tokens, as well as a `C` for the [ClearAll] token. + */ +@SmallTest +@RunWith(JUnit4::class) +class PinInputViewModelTest : SysuiTestCase() { + + @Test + fun create_emptyList_throws() { + assertThrows(IllegalArgumentException::class.java) { PinInputViewModel(emptyList()) } + } + + @Test + fun create_inputWithoutLeadingClearAll_throws() { + val exception = + assertThrows(IllegalArgumentException::class.java) { + PinInputViewModel(listOf(Digit(0))) + } + assertThat(exception).hasMessageThat().contains("does not begin with a ClearAll token") + } + + @Test + fun create_inputNotInAscendingOrder_throws() { + val sentinel = ClearAll() + val first = Digit(0) + val second = Digit(1) + // [first] is created before [second] is created, thus their sequence numbers are ordered. + check(first.sequenceNumber < second.sequenceNumber) + + val exception = + assertThrows(IllegalArgumentException::class.java) { + // Passing the [Digit] tokens in reverse order throws. + PinInputViewModel(listOf(sentinel, second, first)) + } + assertThat(exception).hasMessageThat().contains("EntryTokens are not sorted") + } + + @Test + fun append_digitToEmptyInput() { + val result = empty().append(0) + assertThat(result).matches("C0") + } + + @Test + fun append_digitToExistingPin() { + val subject = pinInput("C1") + assertThat(subject.append(2)).matches("C12") + } + + @Test + fun append_withTwoCompletePinsEntered_dropsFirst() { + val subject = pinInput("C12C34C") + assertThat(subject.append(5)).matches("C34C5") + } + + @Test + fun deleteLast_removesLastDigit() { + val subject = pinInput("C12") + assertThat(subject.deleteLast()).matches("C1") + } + + @Test + fun deleteLast_onEmptyInput_returnsSameInstance() { + val subject = empty() + assertThat(subject.deleteLast()).isSameInstanceAs(subject) + } + + @Test + fun deleteLast_onInputEndingInClearAll_returnsSameInstance() { + val subject = pinInput("C12C") + assertThat(subject.deleteLast()).isSameInstanceAs(subject) + } + + @Test + fun clearAll_appendsClearAllEntryToExistingInput() { + val subject = pinInput("C12") + assertThat(subject.clearAll()).matches("C12C") + } + + @Test + fun clearAll_onInputEndingInClearAll_returnsSameInstance() { + val subject = pinInput("C12C") + assertThat(subject.clearAll()).isSameInstanceAs(subject) + } + + @Test + fun clearAll_retainsUpToTwoPinEntries() { + val subject = pinInput("C12C34") + assertThat(subject.clearAll()).matches("C12C34C") + } + + @Test + fun isEmpty_onEmptyInput_returnsTrue() { + val subject = empty() + assertThat(subject.isEmpty()).isTrue() + } + + @Test + fun isEmpty_whenLastEntryIsDigit_returnsFalse() { + val subject = pinInput("C1234") + assertThat(subject.isEmpty()).isFalse() + } + + @Test + fun isEmpty_whenLastEntryIsClearAll_returnsTrue() { + val subject = pinInput("C1234C") + assertThat(subject.isEmpty()).isTrue() + } + + @Test + fun getPin_onEmptyInput_returnsEmptyList() { + val subject = empty() + assertThat(subject.getPin()).isEmpty() + } + + @Test + fun getPin_whenLastEntryIsDigit_returnsPin() { + val subject = pinInput("C1234") + assertThat(subject.getPin()).containsExactly(1, 2, 3, 4) + } + + @Test + fun getPin_withMultiplePins_returnsLastPin() { + val subject = pinInput("C1234C5678") + assertThat(subject.getPin()).containsExactly(5, 6, 7, 8) + } + + @Test + fun getPin_whenLastEntryIsClearAll_returnsEmptyList() { + val subject = pinInput("C1234C") + assertThat(subject.getPin()).isEmpty() + } + + @Test + fun mostRecentClearAllMarker_onEmptyInput_returnsSentinel() { + val subject = empty() + val sentinel = subject.input[0] as ClearAll + + assertThat(subject.mostRecentClearAll()).isSameInstanceAs(sentinel) + } + + @Test + fun mostRecentClearAllMarker_whenLastEntryIsDigit_returnsSentinel() { + val subject = pinInput("C1234") + val sentinel = subject.input[0] as ClearAll + + assertThat(subject.mostRecentClearAll()).isSameInstanceAs(sentinel) + } + + @Test + fun mostRecentClearAllMarker_withMultiplePins_returnsLastMarker() { + val subject = pinInput("C1234C5678") + val lastMarker = subject.input[5] as ClearAll + + assertThat(subject.mostRecentClearAll()).isSameInstanceAs(lastMarker) + } + + @Test + fun mostRecentClearAllMarker_whenLastEntryIsClearAll_returnsLastEntry() { + val subject = pinInput("C1234C") + val lastEntry = subject.input[5] as ClearAll + + assertThat(subject.mostRecentClearAll()).isSameInstanceAs(lastEntry) + } + + @Test + fun getDigits_invalidClearAllMarker_onEmptyInput_returnsEmptyList() { + val subject = empty() + assertThat(subject.getDigits(ClearAll())).isEmpty() + } + + @Test + fun getDigits_invalidClearAllMarker_whenLastEntryIsDigit_returnsEmptyList() { + val subject = pinInput("C1234") + assertThat(subject.getDigits(ClearAll())).isEmpty() + } + + @Test + fun getDigits_clearAllMarkerPointsToFirstPin_returnsFirstPinDigits() { + val subject = pinInput("C1234C5678") + val marker = subject.input[0] as ClearAll + + assertThat(subject.getDigits(marker).map { it.input }).containsExactly(1, 2, 3, 4) + } + + @Test + fun getDigits_clearAllMarkerPointsToLastPin_returnsLastPinDigits() { + val subject = pinInput("C1234C5678") + val marker = subject.input[5] as ClearAll + + assertThat(subject.getDigits(marker).map { it.input }).containsExactly(5, 6, 7, 8) + } + + @Test + fun entryToken_equality() { + val clearAll = ClearAll() + val zero = Digit(0) + val one = Digit(1) + + // Guava's EqualsTester is not available in this codebase. + assertThat(zero.equals(zero.copy())).isTrue() + + assertThat(zero.equals(one)).isFalse() + assertThat(zero.equals(clearAll)).isFalse() + + assertThat(clearAll.equals(clearAll.copy())).isTrue() + assertThat(clearAll.equals(zero)).isFalse() + + // Not equal when the sequence number does not match + assertThat(zero.equals(Digit(0))).isFalse() + assertThat(clearAll.equals(ClearAll())).isFalse() + } + + private fun pinInput(mnemonics: String): PinInputViewModel { + return PinInputViewModel( + mnemonics.map { + when { + it == 'C' -> ClearAll() + isDigit(it) -> Digit(it.digitToInt()) + else -> throw AssertionError() + } + } + ) + } +} + +private class PinInputSubject +private constructor(metadata: FailureMetadata, private val actual: PinInputViewModel) : + Subject(metadata, actual) { + + fun matches(mnemonics: String) { + val actualMnemonics = + actual.input + .map { entry -> + when (entry) { + is Digit -> entry.input.digitToChar() + is ClearAll -> 'C' + else -> throw IllegalArgumentException() + } + } + .joinToString(separator = "") + + if (mnemonics != actualMnemonics) { + failWithActual( + Fact.simpleFact( + "expected pin input to be '$mnemonics' but is '$actualMnemonics' instead" + ) + ) + } + } + + companion object { + fun assertThat(input: PinInputViewModel): PinInputSubject = + assertAbout(Factory(::PinInputSubject)).that(input) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java index 2a4c0eb18d02..7628be44755d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/IntentCreatorTest.java @@ -126,7 +126,8 @@ public class IntentCreatorTest extends SysuiTestCase { assertEquals(Intent.ACTION_CHOOSER, intent.getAction()); assertFlags(intent, EXTERNAL_INTENT_FLAGS); Intent target = intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent.class); - assertEquals(uri, target.getData()); + assertEquals(uri, target.getParcelableExtra(Intent.EXTRA_STREAM, Uri.class)); + assertEquals(uri, target.getClipData().getItemAt(0).getUri()); assertEquals("image/png", target.getType()); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java index 872c0794ce64..2b9821406fea 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java @@ -61,10 +61,8 @@ public class ShadeTouchHandlerTest extends SysuiTestCase { @Before public void setup() { MockitoAnnotations.initMocks(this); - mTouchHandler = new ShadeTouchHandler(Optional.of(mCentralSurfaces), + mTouchHandler = new ShadeTouchHandler(Optional.of(mCentralSurfaces), mShadeViewController, TOUCH_HEIGHT); - when(mCentralSurfaces.getShadeViewController()) - .thenReturn(mShadeViewController); } /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index 00f67a32ff8e..1f66e5ba703b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -38,8 +38,10 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.inOrder; @@ -386,6 +388,21 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mViewMediator.setKeyguardEnabled(false); TestableLooper.get(this).processAllMessages(); + mViewMediator.mViewMediatorCallback.keyguardDonePending(true, + mUpdateMonitor.getCurrentUser()); + mViewMediator.mViewMediatorCallback.readyForKeyguardDone(); + final ArgumentCaptor<Runnable> animationRunnableCaptor = + ArgumentCaptor.forClass(Runnable.class); + verify(mStatusBarKeyguardViewManager).startPreHideAnimation( + animationRunnableCaptor.capture()); + + when(mStatusBarStateController.isDreaming()).thenReturn(true); + when(mStatusBarStateController.isDozing()).thenReturn(false); + animationRunnableCaptor.getValue().run(); + + when(mKeyguardStateController.isShowing()).thenReturn(false); + mViewMediator.mViewMediatorCallback.keyguardGone(); + // Then dream should wake up verify(mPowerManager).wakeUp(anyLong(), anyInt(), eq("com.android.systemui:UNLOCK_DREAMING")); @@ -705,6 +722,67 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { } @Test + public void testWakeAndUnlockingOverDream() { + // Send signal to wake + mViewMediator.onWakeAndUnlocking(); + + // Ensure not woken up yet + verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString()); + + // Verify keyguard told of authentication + verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(anyBoolean()); + mViewMediator.mViewMediatorCallback.keyguardDonePending(true, + mUpdateMonitor.getCurrentUser()); + mViewMediator.mViewMediatorCallback.readyForKeyguardDone(); + final ArgumentCaptor<Runnable> animationRunnableCaptor = + ArgumentCaptor.forClass(Runnable.class); + verify(mStatusBarKeyguardViewManager).startPreHideAnimation( + animationRunnableCaptor.capture()); + + when(mStatusBarStateController.isDreaming()).thenReturn(true); + when(mStatusBarStateController.isDozing()).thenReturn(false); + animationRunnableCaptor.getValue().run(); + + when(mKeyguardStateController.isShowing()).thenReturn(false); + mViewMediator.mViewMediatorCallback.keyguardGone(); + + // Verify woken up now. + verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString()); + } + + @Test + public void testWakeAndUnlockingOverDream_signalAuthenticateIfStillShowing() { + // Send signal to wake + mViewMediator.onWakeAndUnlocking(); + + // Ensure not woken up yet + verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString()); + + // Verify keyguard told of authentication + verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(anyBoolean()); + clearInvocations(mStatusBarKeyguardViewManager); + mViewMediator.mViewMediatorCallback.keyguardDonePending(true, + mUpdateMonitor.getCurrentUser()); + mViewMediator.mViewMediatorCallback.readyForKeyguardDone(); + final ArgumentCaptor<Runnable> animationRunnableCaptor = + ArgumentCaptor.forClass(Runnable.class); + verify(mStatusBarKeyguardViewManager).startPreHideAnimation( + animationRunnableCaptor.capture()); + + when(mStatusBarStateController.isDreaming()).thenReturn(true); + when(mStatusBarStateController.isDozing()).thenReturn(false); + animationRunnableCaptor.getValue().run(); + + when(mKeyguardStateController.isShowing()).thenReturn(true); + + mViewMediator.mViewMediatorCallback.keyguardGone(); + + + // Verify keyguard view controller informed of authentication again + verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(anyBoolean()); + } + + @Test @TestableLooper.RunWithLooper(setAsMainLooper = true) public void testDoKeyguardWhileInteractive_resets() { mViewMediator.setShowingLocked(true); diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt index 925ac30b99fd..05d6b99fe227 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ResourceTrimmerTest.kt @@ -16,6 +16,7 @@ import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.shared.model.WakeSleepReason import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.keyguard.shared.model.WakefulnessState +import com.android.systemui.util.mockito.any import com.android.systemui.utils.GlobalWindowManager import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope @@ -225,6 +226,9 @@ class ResourceTrimmerTest : SysuiTestCase() { keyguardTransitionRepository.sendTransitionStep( TransitionStep(KeyguardState.LOCKSCREEN, KeyguardState.GONE) ) - verifyNoMoreInteractions(globalWindowManager) + // Memory hidden should still be called. + verify(globalWindowManager, times(1)) + .trimMemory(ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) + verify(globalWindowManager, times(0)).trimCaches(any()) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt index 8127ac625748..b3f800087bdf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt @@ -265,7 +265,8 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { val successResult = successResult() authenticationCallback.value.onAuthenticationSucceeded(successResult) - assertThat(authStatus()).isEqualTo(SuccessFaceAuthenticationStatus(successResult)) + val response = authStatus() as SuccessFaceAuthenticationStatus + assertThat(response.successResult).isEqualTo(successResult) assertThat(authenticated()).isTrue() assertThat(authRunning()).isFalse() assertThat(canFaceAuthRun()).isFalse() @@ -494,7 +495,9 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { authenticationCallback.value.onAuthenticationHelp(10, "Ignored help msg") authenticationCallback.value.onAuthenticationHelp(11, "Ignored help msg") - assertThat(authStatus()).isEqualTo(HelpFaceAuthenticationStatus(9, "help msg")) + val response = authStatus() as HelpFaceAuthenticationStatus + assertThat(response.msg).isEqualTo("help msg") + assertThat(response.msgId).isEqualTo(response.msgId) } @Test @@ -550,10 +553,8 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { } @Test - fun authenticateDoesNotRunWhenFpIsLockedOut() = - testScope.runTest { - testGatingCheckForFaceAuth { deviceEntryFingerprintAuthRepository.setLockedOut(true) } - } + fun authenticateDoesNotRunWhenFaceIsDisabled() = + testScope.runTest { testGatingCheckForFaceAuth { underTest.lockoutFaceAuth() } } @Test fun authenticateDoesNotRunWhenUserIsCurrentlyTrusted() = @@ -858,6 +859,19 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { } @Test + fun disableFaceUnlockLocksOutFaceUnlock() = + testScope.runTest { + runCurrent() + initCollectors() + assertThat(underTest.isLockedOut.value).isFalse() + + underTest.lockoutFaceAuth() + runCurrent() + + assertThat(underTest.isLockedOut.value).isTrue() + } + + @Test fun detectDoesNotRunWhenFaceAuthNotSupportedInCurrentPosture() = testScope.runTest { testGatingCheckForDetect { @@ -1070,10 +1084,11 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { } private suspend fun TestScope.allPreconditionsToRunFaceAuthAreTrue() { + verify(faceManager, atLeastOnce()) + .addLockoutResetCallback(faceLockoutResetCallback.capture()) biometricSettingsRepository.setFaceEnrolled(true) biometricSettingsRepository.setIsFaceAuthEnabled(true) fakeUserRepository.setUserSwitching(false) - deviceEntryFingerprintAuthRepository.setLockedOut(false) trustRepository.setCurrentUserTrusted(false) keyguardRepository.setKeyguardGoingAway(false) keyguardRepository.setWakefulnessModel( @@ -1087,6 +1102,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { biometricSettingsRepository.setIsUserInLockdown(false) fakeUserRepository.setSelectedUserInfo(primaryUser) biometricSettingsRepository.setIsFaceAuthSupportedInCurrentPosture(true) + faceLockoutResetCallback.value.onLockoutReset(0) bouncerRepository.setAlternateVisible(true) keyguardRepository.setKeyguardShowing(true) runCurrent() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt index f9745779ecfb..ec30732dda23 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepositoryTest.kt @@ -17,31 +17,43 @@ package com.android.systemui.keyguard.data.repository import android.graphics.Point -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.core.animation.AnimatorTestRule import androidx.test.filters.SmallTest import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.BiometricUnlockSource +import com.android.systemui.keyguard.shared.model.WakeSleepReason +import com.android.systemui.keyguard.shared.model.WakefulnessModel +import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.statusbar.CircleReveal import com.android.systemui.statusbar.LightRevealEffect import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.MockitoAnnotations @SmallTest @RoboPilotTest -@RunWith(AndroidJUnit4::class) +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidTestingRunner::class) class LightRevealScrimRepositoryTest : SysuiTestCase() { private lateinit var fakeKeyguardRepository: FakeKeyguardRepository private lateinit var underTest: LightRevealScrimRepositoryImpl + @get:Rule val animatorTestRule = AnimatorTestRule() + @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -50,112 +62,127 @@ class LightRevealScrimRepositoryTest : SysuiTestCase() { } @Test - fun nextRevealEffect_effectSwitchesBetweenDefaultAndBiometricWithNoDupes() = - runTest { - val values = mutableListOf<LightRevealEffect>() - val job = launch { underTest.revealEffect.collect { values.add(it) } } - - // We should initially emit the default reveal effect. - runCurrent() - values.assertEffectsMatchPredicates({ it == DEFAULT_REVEAL_EFFECT }) - - // The source and sensor locations are still null, so we should still be using the - // default reveal despite a biometric unlock. - fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK) - - runCurrent() - values.assertEffectsMatchPredicates( - { it == DEFAULT_REVEAL_EFFECT }, + fun nextRevealEffect_effectSwitchesBetweenDefaultAndBiometricWithNoDupes() = runTest { + val values = mutableListOf<LightRevealEffect>() + val job = launch { underTest.revealEffect.collect { values.add(it) } } + + fakeKeyguardRepository.setWakefulnessModel( + WakefulnessModel( + WakefulnessState.STARTING_TO_WAKE, + WakeSleepReason.OTHER, + WakeSleepReason.OTHER ) + ) + // We should initially emit the default reveal effect. + runCurrent() + values.assertEffectsMatchPredicates({ it == DEFAULT_REVEAL_EFFECT }) - // We got a source but still have no sensor locations, so should be sticking with - // the default effect. - fakeKeyguardRepository.setBiometricUnlockSource( - BiometricUnlockSource.FINGERPRINT_SENSOR - ) + // The source and sensor locations are still null, so we should still be using the + // default reveal despite a biometric unlock. + fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK) - runCurrent() - values.assertEffectsMatchPredicates( - { it == DEFAULT_REVEAL_EFFECT }, - ) + runCurrent() + values.assertEffectsMatchPredicates( + { it == DEFAULT_REVEAL_EFFECT }, + ) - // We got a location for the face sensor, but we unlocked with fingerprint. - val faceLocation = Point(250, 0) - fakeKeyguardRepository.setFaceSensorLocation(faceLocation) + // We got a source but still have no sensor locations, so should be sticking with + // the default effect. + fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FINGERPRINT_SENSOR) - runCurrent() - values.assertEffectsMatchPredicates( - { it == DEFAULT_REVEAL_EFFECT }, - ) + runCurrent() + values.assertEffectsMatchPredicates( + { it == DEFAULT_REVEAL_EFFECT }, + ) - // Now we have fingerprint sensor locations, and wake and unlock via fingerprint. - val fingerprintLocation = Point(500, 500) - fakeKeyguardRepository.setFingerprintSensorLocation(fingerprintLocation) - fakeKeyguardRepository.setBiometricUnlockSource( - BiometricUnlockSource.FINGERPRINT_SENSOR - ) - fakeKeyguardRepository.setBiometricUnlockState( - BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING - ) + // We got a location for the face sensor, but we unlocked with fingerprint. + val faceLocation = Point(250, 0) + fakeKeyguardRepository.setFaceSensorLocation(faceLocation) - // We should now have switched to the circle reveal, at the fingerprint location. - runCurrent() - values.assertEffectsMatchPredicates( - { it == DEFAULT_REVEAL_EFFECT }, - { - it is CircleReveal && - it.centerX == fingerprintLocation.x && - it.centerY == fingerprintLocation.y - }, - ) + runCurrent() + values.assertEffectsMatchPredicates( + { it == DEFAULT_REVEAL_EFFECT }, + ) - // Subsequent wake and unlocks should not emit duplicate, identical CircleReveals. - val valuesPrevSize = values.size - fakeKeyguardRepository.setBiometricUnlockState( - BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING - ) - fakeKeyguardRepository.setBiometricUnlockState( - BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM - ) - assertEquals(valuesPrevSize, values.size) - - // Non-biometric unlock, we should return to the default reveal. - fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE) - - runCurrent() - values.assertEffectsMatchPredicates( - { it == DEFAULT_REVEAL_EFFECT }, - { - it is CircleReveal && - it.centerX == fingerprintLocation.x && - it.centerY == fingerprintLocation.y - }, - { it == DEFAULT_REVEAL_EFFECT }, - ) + // Now we have fingerprint sensor locations, and wake and unlock via fingerprint. + val fingerprintLocation = Point(500, 500) + fakeKeyguardRepository.setFingerprintSensorLocation(fingerprintLocation) + fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FINGERPRINT_SENSOR) + fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING) + + // We should now have switched to the circle reveal, at the fingerprint location. + runCurrent() + values.assertEffectsMatchPredicates( + { it == DEFAULT_REVEAL_EFFECT }, + { + it is CircleReveal && + it.centerX == fingerprintLocation.x && + it.centerY == fingerprintLocation.y + }, + ) - // We already have a face location, so switching to face source should update the - // CircleReveal. - fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FACE_SENSOR) - runCurrent() - fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK) - runCurrent() - - values.assertEffectsMatchPredicates( - { it == DEFAULT_REVEAL_EFFECT }, - { - it is CircleReveal && - it.centerX == fingerprintLocation.x && - it.centerY == fingerprintLocation.y - }, - { it == DEFAULT_REVEAL_EFFECT }, - { - it is CircleReveal && - it.centerX == faceLocation.x && - it.centerY == faceLocation.y - }, - ) + // Subsequent wake and unlocks should not emit duplicate, identical CircleReveals. + val valuesPrevSize = values.size + fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING) + fakeKeyguardRepository.setBiometricUnlockState( + BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM + ) + assertEquals(valuesPrevSize, values.size) + + // Non-biometric unlock, we should return to the default reveal. + fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE) + + runCurrent() + values.assertEffectsMatchPredicates( + { it == DEFAULT_REVEAL_EFFECT }, + { + it is CircleReveal && + it.centerX == fingerprintLocation.x && + it.centerY == fingerprintLocation.y + }, + { it == DEFAULT_REVEAL_EFFECT }, + ) + + // We already have a face location, so switching to face source should update the + // CircleReveal. + fakeKeyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FACE_SENSOR) + runCurrent() + fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK) + runCurrent() + + values.assertEffectsMatchPredicates( + { it == DEFAULT_REVEAL_EFFECT }, + { + it is CircleReveal && + it.centerX == fingerprintLocation.x && + it.centerY == fingerprintLocation.y + }, + { it == DEFAULT_REVEAL_EFFECT }, + { it is CircleReveal && it.centerX == faceLocation.x && it.centerY == faceLocation.y }, + ) + + job.cancel() + } - job.cancel() + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + fun revealAmount_emitsTo1AfterAnimationStarted() = + runTest(UnconfinedTestDispatcher()) { + val value by collectLastValue(underTest.revealAmount) + underTest.startRevealAmountAnimator(true) + assertEquals(0.0f, value) + animatorTestRule.advanceTimeBy(500L) + assertEquals(1.0f, value) + } + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + fun revealAmount_emitsTo0AfterAnimationStartedReversed() = + runTest(UnconfinedTestDispatcher()) { + val value by collectLastValue(underTest.revealAmount) + underTest.startRevealAmountAnimator(false) + assertEquals(1.0f, value) + animatorTestRule.advanceTimeBy(500L) + assertEquals(0.0f, value) } /** diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt index ced0a213ca97..8c9ed5b2ef4e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardFaceAuthInteractorTest.kt @@ -38,6 +38,7 @@ import com.android.systemui.flags.Flags import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.FakeTrustRepository import com.android.systemui.keyguard.shared.model.ErrorFaceAuthenticationStatus @@ -73,6 +74,8 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() { private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor private lateinit var faceAuthRepository: FakeDeviceEntryFaceAuthRepository + private lateinit var fakeDeviceEntryFingerprintAuthRepository: + FakeDeviceEntryFingerprintAuthRepository @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @@ -94,6 +97,7 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() { ) .keyguardTransitionInteractor + fakeDeviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository() underTest = SystemUIKeyguardFaceAuthInteractor( mContext, @@ -127,6 +131,7 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() { featureFlags, FaceAuthenticationLogger(logcatLogBuffer("faceAuthBuffer")), keyguardUpdateMonitor, + fakeDeviceEntryFingerprintAuthRepository ) } @@ -335,4 +340,14 @@ class KeyguardFaceAuthInteractorTest : SysuiTestCase() { assertThat(faceAuthRepository.runningAuthRequest.value) .isEqualTo(Pair(FaceAuthUiEvent.FACE_AUTH_TRIGGERED_SWIPE_UP_ON_BOUNCER, false)) } + + @Test + fun faceUnlockIsDisabledWhenFpIsLockedOut() = testScope.runTest { + underTest.start() + + fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true) + runCurrent() + + assertThat(faceAuthRepository.wasDisabled).isTrue() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt index 6e7ba6dd1ecb..906d94859140 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt @@ -27,27 +27,37 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.statusbar.LightRevealEffect import com.android.systemui.statusbar.LightRevealScrim +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.never +import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.mockito.Spy @SmallTest @RoboPilotTest +@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class LightRevealScrimInteractorTest : SysuiTestCase() { private val fakeKeyguardTransitionRepository = FakeKeyguardTransitionRepository() - private val fakeLightRevealScrimRepository = FakeLightRevealScrimRepository() + + @Spy private val fakeLightRevealScrimRepository = FakeLightRevealScrimRepository() + + private val testScope = TestScope() private val keyguardTransitionInteractor = KeyguardTransitionInteractorFactory.create( - scope = TestScope().backgroundScope, + scope = testScope.backgroundScope, repository = fakeKeyguardTransitionRepository, ) .keyguardTransitionInteractor @@ -69,9 +79,9 @@ class LightRevealScrimInteractorTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) underTest = LightRevealScrimInteractor( - fakeKeyguardTransitionRepository, keyguardTransitionInteractor, - fakeLightRevealScrimRepository + fakeLightRevealScrimRepository, + testScope.backgroundScope ) } @@ -110,52 +120,36 @@ class LightRevealScrimInteractorTest : SysuiTestCase() { } @Test - fun revealAmount_invertedWhenAppropriate() = - runTest(UnconfinedTestDispatcher()) { - val values = mutableListOf<Float>() - val job = underTest.revealAmount.onEach(values::add).launchIn(this) - + fun lightRevealEffect_startsAnimationOnlyForDifferentStateTargets() = + testScope.runTest { fakeKeyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.AOD, - to = KeyguardState.LOCKSCREEN, - value = 0.3f + transitionState = TransitionState.STARTED, + from = KeyguardState.OFF, + to = KeyguardState.OFF ) ) - - assertEquals(values, listOf(0.3f)) + runCurrent() + verify(fakeLightRevealScrimRepository, never()).startRevealAmountAnimator(anyBoolean()) fakeKeyguardTransitionRepository.sendTransitionStep( TransitionStep( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.AOD, - value = 0.3f + transitionState = TransitionState.STARTED, + from = KeyguardState.DOZING, + to = KeyguardState.LOCKSCREEN ) ) - - assertEquals(values, listOf(0.3f, 0.7f)) - - job.cancel() - } - - @Test - fun revealAmount_ignoresTransitionsThatDoNotAffectRevealAmount() = - runTest(UnconfinedTestDispatcher()) { - val values = mutableListOf<Float>() - val job = underTest.revealAmount.onEach(values::add).launchIn(this) - - fakeKeyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.DOZING, to = KeyguardState.AOD, value = 0.3f) - ) - - assertEquals(values, emptyList<Float>()) + runCurrent() + verify(fakeLightRevealScrimRepository).startRevealAmountAnimator(true) fakeKeyguardTransitionRepository.sendTransitionStep( - TransitionStep(from = KeyguardState.AOD, to = KeyguardState.DOZING, value = 0.3f) + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DOZING + ) ) - - assertEquals(values, emptyList<Float>()) - - job.cancel() + runCurrent() + verify(fakeLightRevealScrimRepository).startRevealAmountAnimator(false) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/data/repository/MultiShadeRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/data/repository/MultiShadeRepositoryTest.kt deleted file mode 100644 index ceacaf9557ca..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/multishade/data/repository/MultiShadeRepositoryTest.kt +++ /dev/null @@ -1,191 +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.multishade.data.repository - -import android.content.Context -import androidx.test.filters.SmallTest -import com.android.systemui.R -import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.multishade.data.model.MultiShadeInteractionModel -import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy -import com.android.systemui.multishade.shared.model.ProxiedInputModel -import com.android.systemui.multishade.shared.model.ShadeConfig -import com.android.systemui.multishade.shared.model.ShadeId -import com.google.common.truth.Truth.assertThat -import com.google.common.truth.Truth.assertWithMessage -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 - -@OptIn(ExperimentalCoroutinesApi::class) -@SmallTest -@RunWith(JUnit4::class) -class MultiShadeRepositoryTest : SysuiTestCase() { - - private lateinit var inputProxy: MultiShadeInputProxy - - @Before - fun setUp() { - inputProxy = MultiShadeInputProxy() - } - - @Test - fun proxiedInput() = runTest { - val underTest = create() - val latest: ProxiedInputModel? by collectLastValue(underTest.proxiedInput) - - assertWithMessage("proxiedInput should start with null").that(latest).isNull() - - inputProxy.onProxiedInput(ProxiedInputModel.OnTap) - assertThat(latest).isEqualTo(ProxiedInputModel.OnTap) - - inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 100f)) - assertThat(latest).isEqualTo(ProxiedInputModel.OnDrag(0f, 100f)) - - inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 120f)) - assertThat(latest).isEqualTo(ProxiedInputModel.OnDrag(0f, 120f)) - - inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd) - assertThat(latest).isEqualTo(ProxiedInputModel.OnDragEnd) - } - - @Test - fun shadeConfig_dualShadeEnabled() = runTest { - overrideResource(R.bool.dual_shade_enabled, true) - val underTest = create() - val shadeConfig: ShadeConfig? by collectLastValue(underTest.shadeConfig) - - assertThat(shadeConfig).isInstanceOf(ShadeConfig.DualShadeConfig::class.java) - } - - @Test - fun shadeConfig_dualShadeNotEnabled() = runTest { - overrideResource(R.bool.dual_shade_enabled, false) - val underTest = create() - val shadeConfig: ShadeConfig? by collectLastValue(underTest.shadeConfig) - - assertThat(shadeConfig).isInstanceOf(ShadeConfig.SingleShadeConfig::class.java) - } - - @Test - fun forceCollapseAll() = runTest { - val underTest = create() - val forceCollapseAll: Boolean? by collectLastValue(underTest.forceCollapseAll) - - assertWithMessage("forceCollapseAll should start as false!") - .that(forceCollapseAll) - .isFalse() - - underTest.setForceCollapseAll(true) - assertThat(forceCollapseAll).isTrue() - - underTest.setForceCollapseAll(false) - assertThat(forceCollapseAll).isFalse() - } - - @Test - fun shadeInteraction() = runTest { - val underTest = create() - val shadeInteraction: MultiShadeInteractionModel? by - collectLastValue(underTest.shadeInteraction) - - assertWithMessage("shadeInteraction should start as null!").that(shadeInteraction).isNull() - - underTest.setShadeInteraction( - MultiShadeInteractionModel(shadeId = ShadeId.LEFT, isProxied = false) - ) - assertThat(shadeInteraction) - .isEqualTo(MultiShadeInteractionModel(shadeId = ShadeId.LEFT, isProxied = false)) - - underTest.setShadeInteraction( - MultiShadeInteractionModel(shadeId = ShadeId.RIGHT, isProxied = true) - ) - assertThat(shadeInteraction) - .isEqualTo(MultiShadeInteractionModel(shadeId = ShadeId.RIGHT, isProxied = true)) - - underTest.setShadeInteraction(null) - assertThat(shadeInteraction).isNull() - } - - @Test - fun expansion() = runTest { - val underTest = create() - val leftExpansion: Float? by - collectLastValue(underTest.getShade(ShadeId.LEFT).map { it.expansion }) - val rightExpansion: Float? by - collectLastValue(underTest.getShade(ShadeId.RIGHT).map { it.expansion }) - val singleExpansion: Float? by - collectLastValue(underTest.getShade(ShadeId.SINGLE).map { it.expansion }) - - assertWithMessage("expansion should start as 0!").that(leftExpansion).isZero() - assertWithMessage("expansion should start as 0!").that(rightExpansion).isZero() - assertWithMessage("expansion should start as 0!").that(singleExpansion).isZero() - - underTest.setExpansion( - shadeId = ShadeId.LEFT, - 0.4f, - ) - assertThat(leftExpansion).isEqualTo(0.4f) - assertThat(rightExpansion).isEqualTo(0f) - assertThat(singleExpansion).isEqualTo(0f) - - underTest.setExpansion( - shadeId = ShadeId.RIGHT, - 0.73f, - ) - assertThat(leftExpansion).isEqualTo(0.4f) - assertThat(rightExpansion).isEqualTo(0.73f) - assertThat(singleExpansion).isEqualTo(0f) - - underTest.setExpansion( - shadeId = ShadeId.LEFT, - 0.1f, - ) - underTest.setExpansion( - shadeId = ShadeId.SINGLE, - 0.88f, - ) - assertThat(leftExpansion).isEqualTo(0.1f) - assertThat(rightExpansion).isEqualTo(0.73f) - assertThat(singleExpansion).isEqualTo(0.88f) - } - - private fun create(): MultiShadeRepository { - return create( - context = context, - inputProxy = inputProxy, - ) - } - - companion object { - fun create( - context: Context, - inputProxy: MultiShadeInputProxy, - ): MultiShadeRepository { - return MultiShadeRepository( - applicationContext = context, - inputProxy = inputProxy, - ) - } - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt deleted file mode 100644 index bcc99bc8dd0c..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeInteractorTest.kt +++ /dev/null @@ -1,323 +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.multishade.domain.interactor - -import android.content.Context -import androidx.test.filters.SmallTest -import com.android.systemui.R -import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy -import com.android.systemui.multishade.data.repository.MultiShadeRepositoryTest -import com.android.systemui.multishade.shared.model.ProxiedInputModel -import com.android.systemui.multishade.shared.model.ShadeId -import com.google.common.truth.Truth.assertThat -import com.google.common.truth.Truth.assertWithMessage -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 - -@OptIn(ExperimentalCoroutinesApi::class) -@SmallTest -@RunWith(JUnit4::class) -class MultiShadeInteractorTest : SysuiTestCase() { - - private lateinit var testScope: TestScope - private lateinit var inputProxy: MultiShadeInputProxy - - @Before - fun setUp() { - testScope = TestScope() - inputProxy = MultiShadeInputProxy() - } - - @Test - fun maxShadeExpansion() = - testScope.runTest { - val underTest = create() - val maxShadeExpansion: Float? by collectLastValue(underTest.maxShadeExpansion) - assertWithMessage("maxShadeExpansion must start with 0.0!") - .that(maxShadeExpansion) - .isEqualTo(0f) - - underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0.441f) - assertThat(maxShadeExpansion).isEqualTo(0.441f) - - underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0.442f) - assertThat(maxShadeExpansion).isEqualTo(0.442f) - - underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0f) - assertThat(maxShadeExpansion).isEqualTo(0.441f) - - underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0f) - assertThat(maxShadeExpansion).isEqualTo(0f) - } - - @Test - fun isAnyShadeExpanded() = - testScope.runTest { - val underTest = create() - val isAnyShadeExpanded: Boolean? by collectLastValue(underTest.isAnyShadeExpanded) - assertWithMessage("isAnyShadeExpanded must start with false!") - .that(isAnyShadeExpanded) - .isFalse() - - underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0.441f) - assertThat(isAnyShadeExpanded).isTrue() - - underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0.442f) - assertThat(isAnyShadeExpanded).isTrue() - - underTest.setExpansion(shadeId = ShadeId.RIGHT, expansion = 0f) - assertThat(isAnyShadeExpanded).isTrue() - - underTest.setExpansion(shadeId = ShadeId.LEFT, expansion = 0f) - assertThat(isAnyShadeExpanded).isFalse() - } - - @Test - fun isVisible_dualShadeConfig() = - testScope.runTest { - overrideResource(R.bool.dual_shade_enabled, true) - val underTest = create() - val isLeftShadeVisible: Boolean? by collectLastValue(underTest.isVisible(ShadeId.LEFT)) - val isRightShadeVisible: Boolean? by - collectLastValue(underTest.isVisible(ShadeId.RIGHT)) - val isSingleShadeVisible: Boolean? by - collectLastValue(underTest.isVisible(ShadeId.SINGLE)) - - assertThat(isLeftShadeVisible).isTrue() - assertThat(isRightShadeVisible).isTrue() - assertThat(isSingleShadeVisible).isFalse() - } - - @Test - fun isVisible_singleShadeConfig() = - testScope.runTest { - overrideResource(R.bool.dual_shade_enabled, false) - val underTest = create() - val isLeftShadeVisible: Boolean? by collectLastValue(underTest.isVisible(ShadeId.LEFT)) - val isRightShadeVisible: Boolean? by - collectLastValue(underTest.isVisible(ShadeId.RIGHT)) - val isSingleShadeVisible: Boolean? by - collectLastValue(underTest.isVisible(ShadeId.SINGLE)) - - assertThat(isLeftShadeVisible).isFalse() - assertThat(isRightShadeVisible).isFalse() - assertThat(isSingleShadeVisible).isTrue() - } - - @Test - fun isNonProxiedInputAllowed() = - testScope.runTest { - val underTest = create() - val isLeftShadeNonProxiedInputAllowed: Boolean? by - collectLastValue(underTest.isNonProxiedInputAllowed(ShadeId.LEFT)) - assertWithMessage("isNonProxiedInputAllowed should start as true!") - .that(isLeftShadeNonProxiedInputAllowed) - .isTrue() - - // Need to collect proxied input so the flows become hot as the gesture cancelation code - // logic sits in side the proxiedInput flow for each shade. - collectLastValue(underTest.proxiedInput(ShadeId.LEFT)) - collectLastValue(underTest.proxiedInput(ShadeId.RIGHT)) - - // Starting a proxied interaction on the LEFT shade disallows non-proxied interaction on - // the - // same shade. - inputProxy.onProxiedInput( - ProxiedInputModel.OnDrag(xFraction = 0f, yDragAmountPx = 123f) - ) - assertThat(isLeftShadeNonProxiedInputAllowed).isFalse() - - // Registering the end of the proxied interaction re-allows it. - inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd) - assertThat(isLeftShadeNonProxiedInputAllowed).isTrue() - - // Starting a proxied interaction on the RIGHT shade force-collapses the LEFT shade, - // disallowing non-proxied input on the LEFT shade. - inputProxy.onProxiedInput( - ProxiedInputModel.OnDrag(xFraction = 1f, yDragAmountPx = 123f) - ) - assertThat(isLeftShadeNonProxiedInputAllowed).isFalse() - - // Registering the end of the interaction on the RIGHT shade re-allows it. - inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd) - assertThat(isLeftShadeNonProxiedInputAllowed).isTrue() - } - - @Test - fun isForceCollapsed_whenOtherShadeInteractionUnderway() = - testScope.runTest { - val underTest = create() - val isLeftShadeForceCollapsed: Boolean? by - collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT)) - val isRightShadeForceCollapsed: Boolean? by - collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT)) - val isSingleShadeForceCollapsed: Boolean? by - collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE)) - - assertWithMessage("isForceCollapsed should start as false!") - .that(isLeftShadeForceCollapsed) - .isFalse() - assertWithMessage("isForceCollapsed should start as false!") - .that(isRightShadeForceCollapsed) - .isFalse() - assertWithMessage("isForceCollapsed should start as false!") - .that(isSingleShadeForceCollapsed) - .isFalse() - - // Registering the start of an interaction on the RIGHT shade force-collapses the LEFT - // shade. - underTest.onUserInteractionStarted(ShadeId.RIGHT) - assertThat(isLeftShadeForceCollapsed).isTrue() - assertThat(isRightShadeForceCollapsed).isFalse() - assertThat(isSingleShadeForceCollapsed).isFalse() - - // Registering the end of the interaction on the RIGHT shade re-allows it. - underTest.onUserInteractionEnded(ShadeId.RIGHT) - assertThat(isLeftShadeForceCollapsed).isFalse() - assertThat(isRightShadeForceCollapsed).isFalse() - assertThat(isSingleShadeForceCollapsed).isFalse() - - // Registering the start of an interaction on the LEFT shade force-collapses the RIGHT - // shade. - underTest.onUserInteractionStarted(ShadeId.LEFT) - assertThat(isLeftShadeForceCollapsed).isFalse() - assertThat(isRightShadeForceCollapsed).isTrue() - assertThat(isSingleShadeForceCollapsed).isFalse() - - // Registering the end of the interaction on the LEFT shade re-allows it. - underTest.onUserInteractionEnded(ShadeId.LEFT) - assertThat(isLeftShadeForceCollapsed).isFalse() - assertThat(isRightShadeForceCollapsed).isFalse() - assertThat(isSingleShadeForceCollapsed).isFalse() - } - - @Test - fun collapseAll() = - testScope.runTest { - val underTest = create() - val isLeftShadeForceCollapsed: Boolean? by - collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT)) - val isRightShadeForceCollapsed: Boolean? by - collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT)) - val isSingleShadeForceCollapsed: Boolean? by - collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE)) - - assertWithMessage("isForceCollapsed should start as false!") - .that(isLeftShadeForceCollapsed) - .isFalse() - assertWithMessage("isForceCollapsed should start as false!") - .that(isRightShadeForceCollapsed) - .isFalse() - assertWithMessage("isForceCollapsed should start as false!") - .that(isSingleShadeForceCollapsed) - .isFalse() - - underTest.collapseAll() - assertThat(isLeftShadeForceCollapsed).isTrue() - assertThat(isRightShadeForceCollapsed).isTrue() - assertThat(isSingleShadeForceCollapsed).isTrue() - - // Receiving proxied input on that's not a tap gesture, on the left-hand side resets the - // "collapse all". Note that now the RIGHT shade is force-collapsed because we're - // interacting with the LEFT shade. - inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0f, 0f)) - assertThat(isLeftShadeForceCollapsed).isFalse() - assertThat(isRightShadeForceCollapsed).isTrue() - assertThat(isSingleShadeForceCollapsed).isFalse() - } - - @Test - fun onTapOutside_collapsesAll() = - testScope.runTest { - val underTest = create() - val isLeftShadeForceCollapsed: Boolean? by - collectLastValue(underTest.isForceCollapsed(ShadeId.LEFT)) - val isRightShadeForceCollapsed: Boolean? by - collectLastValue(underTest.isForceCollapsed(ShadeId.RIGHT)) - val isSingleShadeForceCollapsed: Boolean? by - collectLastValue(underTest.isForceCollapsed(ShadeId.SINGLE)) - - assertWithMessage("isForceCollapsed should start as false!") - .that(isLeftShadeForceCollapsed) - .isFalse() - assertWithMessage("isForceCollapsed should start as false!") - .that(isRightShadeForceCollapsed) - .isFalse() - assertWithMessage("isForceCollapsed should start as false!") - .that(isSingleShadeForceCollapsed) - .isFalse() - - inputProxy.onProxiedInput(ProxiedInputModel.OnTap) - assertThat(isLeftShadeForceCollapsed).isTrue() - assertThat(isRightShadeForceCollapsed).isTrue() - assertThat(isSingleShadeForceCollapsed).isTrue() - } - - @Test - fun proxiedInput_ignoredWhileNonProxiedGestureUnderway() = - testScope.runTest { - val underTest = create() - val proxiedInput: ProxiedInputModel? by - collectLastValue(underTest.proxiedInput(ShadeId.RIGHT)) - underTest.onUserInteractionStarted(shadeId = ShadeId.RIGHT) - - inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f)) - assertThat(proxiedInput).isNull() - - inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.8f, 110f)) - assertThat(proxiedInput).isNull() - - underTest.onUserInteractionEnded(shadeId = ShadeId.RIGHT) - - inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f)) - assertThat(proxiedInput).isNotNull() - } - - private fun create(): MultiShadeInteractor { - return create( - testScope = testScope, - context = context, - inputProxy = inputProxy, - ) - } - - companion object { - fun create( - testScope: TestScope, - context: Context, - inputProxy: MultiShadeInputProxy, - ): MultiShadeInteractor { - return MultiShadeInteractor( - applicationScope = testScope.backgroundScope, - repository = - MultiShadeRepositoryTest.create( - context = context, - inputProxy = inputProxy, - ), - inputProxy = inputProxy, - ) - } - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt deleted file mode 100644 index 5890cbd06476..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/multishade/domain/interactor/MultiShadeMotionEventInteractorTest.kt +++ /dev/null @@ -1,530 +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.multishade.domain.interactor - -import android.view.MotionEvent -import android.view.ViewConfiguration -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.classifier.FalsingManagerFake -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags -import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory -import com.android.systemui.keyguard.shared.model.KeyguardState -import com.android.systemui.keyguard.shared.model.TransitionState -import com.android.systemui.keyguard.shared.model.TransitionStep -import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy -import com.android.systemui.multishade.data.repository.MultiShadeRepository -import com.android.systemui.multishade.shared.model.ProxiedInputModel -import com.android.systemui.multishade.shared.model.ShadeId -import com.android.systemui.shade.ShadeController -import com.android.systemui.util.mockito.whenever -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.currentTime -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import org.junit.After -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.mockito.Mock -import org.mockito.Mockito.anyBoolean -import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations - -@OptIn(ExperimentalCoroutinesApi::class) -@SmallTest -@RunWith(JUnit4::class) -class MultiShadeMotionEventInteractorTest : SysuiTestCase() { - - private lateinit var underTest: MultiShadeMotionEventInteractor - - private lateinit var testScope: TestScope - private lateinit var motionEvents: MutableSet<MotionEvent> - private lateinit var repository: MultiShadeRepository - private lateinit var interactor: MultiShadeInteractor - private val touchSlop: Int = ViewConfiguration.get(context).scaledTouchSlop - private lateinit var keyguardTransitionRepository: FakeKeyguardTransitionRepository - private lateinit var falsingManager: FalsingManagerFake - @Mock private lateinit var shadeController: ShadeController - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - testScope = TestScope() - motionEvents = mutableSetOf() - - val inputProxy = MultiShadeInputProxy() - repository = - MultiShadeRepository( - applicationContext = context, - inputProxy = inputProxy, - ) - interactor = - MultiShadeInteractor( - applicationScope = testScope.backgroundScope, - repository = repository, - inputProxy = inputProxy, - ) - val featureFlags = FakeFeatureFlags() - featureFlags.set(Flags.DUAL_SHADE, true) - keyguardTransitionRepository = FakeKeyguardTransitionRepository() - falsingManager = FalsingManagerFake() - - underTest = - MultiShadeMotionEventInteractor( - applicationContext = context, - applicationScope = testScope.backgroundScope, - multiShadeInteractor = interactor, - featureFlags = featureFlags, - keyguardTransitionInteractor = - KeyguardTransitionInteractorFactory.create( - scope = TestScope().backgroundScope, - repository = keyguardTransitionRepository, - ) - .keyguardTransitionInteractor, - falsingManager = falsingManager, - shadeController = shadeController, - ) - } - - @After - fun tearDown() { - motionEvents.forEach { motionEvent -> motionEvent.recycle() } - } - - @Test - fun listenForIsAnyShadeExpanded_expanded_makesWindowViewVisible() = - testScope.runTest { - whenever(shadeController.isKeyguard).thenReturn(false) - repository.setExpansion(ShadeId.LEFT, 0.1f) - val expanded by collectLastValue(interactor.isAnyShadeExpanded) - assertThat(expanded).isTrue() - - verify(shadeController).makeExpandedVisible(anyBoolean()) - } - - @Test - fun listenForIsAnyShadeExpanded_collapsed_makesWindowViewInvisible() = - testScope.runTest { - whenever(shadeController.isKeyguard).thenReturn(false) - repository.setForceCollapseAll(true) - val expanded by collectLastValue(interactor.isAnyShadeExpanded) - assertThat(expanded).isFalse() - - verify(shadeController).makeExpandedInvisible() - } - - @Test - fun listenForIsAnyShadeExpanded_collapsedOnKeyguard_makesWindowViewVisible() = - testScope.runTest { - whenever(shadeController.isKeyguard).thenReturn(true) - repository.setForceCollapseAll(true) - val expanded by collectLastValue(interactor.isAnyShadeExpanded) - assertThat(expanded).isFalse() - - verify(shadeController).makeExpandedVisible(anyBoolean()) - } - - @Test - fun shouldIntercept_initialDown_returnsFalse() = - testScope.runTest { - assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN))).isFalse() - } - - @Test - fun shouldIntercept_moveBelowTouchSlop_returnsFalse() = - testScope.runTest { - underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN)) - - assertThat( - underTest.shouldIntercept( - motionEvent( - MotionEvent.ACTION_MOVE, - y = touchSlop - 1f, - ) - ) - ) - .isFalse() - } - - @Test - fun shouldIntercept_moveAboveTouchSlop_returnsTrue() = - testScope.runTest { - underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN)) - - assertThat( - underTest.shouldIntercept( - motionEvent( - MotionEvent.ACTION_MOVE, - y = touchSlop + 1f, - ) - ) - ) - .isTrue() - } - - @Test - fun shouldIntercept_moveAboveTouchSlop_butHorizontalFirst_returnsFalse() = - testScope.runTest { - underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN)) - - assertThat( - underTest.shouldIntercept( - motionEvent( - MotionEvent.ACTION_MOVE, - x = touchSlop + 1f, - ) - ) - ) - .isFalse() - assertThat( - underTest.shouldIntercept( - motionEvent( - MotionEvent.ACTION_MOVE, - y = touchSlop + 1f, - ) - ) - ) - .isFalse() - } - - @Test - fun shouldIntercept_up_afterMovedAboveTouchSlop_returnsTrue() = - testScope.runTest { - underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN)) - underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_MOVE, y = touchSlop + 1f)) - - assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))).isTrue() - } - - @Test - fun shouldIntercept_cancel_afterMovedAboveTouchSlop_returnsTrue() = - testScope.runTest { - underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN)) - underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_MOVE, y = touchSlop + 1f)) - - assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_CANCEL))).isTrue() - } - - @Test - fun shouldIntercept_moveAboveTouchSlopAndUp_butShadeExpanded_returnsFalse() = - testScope.runTest { - repository.setExpansion(ShadeId.LEFT, 0.1f) - runCurrent() - - underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN)) - - assertThat( - underTest.shouldIntercept( - motionEvent( - MotionEvent.ACTION_MOVE, - y = touchSlop + 1f, - ) - ) - ) - .isFalse() - assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))).isFalse() - } - - @Test - fun shouldIntercept_moveAboveTouchSlopAndCancel_butShadeExpanded_returnsFalse() = - testScope.runTest { - repository.setExpansion(ShadeId.LEFT, 0.1f) - runCurrent() - - underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN)) - - assertThat( - underTest.shouldIntercept( - motionEvent( - MotionEvent.ACTION_MOVE, - y = touchSlop + 1f, - ) - ) - ) - .isFalse() - assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_CANCEL))).isFalse() - } - - @Test - fun shouldIntercept_moveAboveTouchSlopAndUp_butBouncerShowing_returnsFalse() = - testScope.runTest { - keyguardTransitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.PRIMARY_BOUNCER, - value = 0.1f, - transitionState = TransitionState.STARTED, - ) - ) - runCurrent() - - underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN)) - - assertThat( - underTest.shouldIntercept( - motionEvent( - MotionEvent.ACTION_MOVE, - y = touchSlop + 1f, - ) - ) - ) - .isFalse() - assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP))).isFalse() - } - - @Test - fun shouldIntercept_moveAboveTouchSlopAndCancel_butBouncerShowing_returnsFalse() = - testScope.runTest { - keyguardTransitionRepository.sendTransitionStep( - TransitionStep( - from = KeyguardState.LOCKSCREEN, - to = KeyguardState.PRIMARY_BOUNCER, - value = 0.1f, - transitionState = TransitionState.STARTED, - ) - ) - runCurrent() - - underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN)) - - assertThat( - underTest.shouldIntercept( - motionEvent( - MotionEvent.ACTION_MOVE, - y = touchSlop + 1f, - ) - ) - ) - .isFalse() - assertThat(underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_CANCEL))).isFalse() - } - - @Test - fun tap_doesNotSendProxiedInput() = - testScope.runTest { - val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT)) - val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT)) - val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE)) - - underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN)) - underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP)) - - assertThat(leftShadeProxiedInput).isNull() - assertThat(rightShadeProxiedInput).isNull() - assertThat(singleShadeProxiedInput).isNull() - } - - @Test - fun dragBelowTouchSlop_doesNotSendProxiedInput() = - testScope.runTest { - val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT)) - val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT)) - val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE)) - - underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_DOWN)) - underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_MOVE, y = touchSlop - 1f)) - underTest.shouldIntercept(motionEvent(MotionEvent.ACTION_UP)) - - assertThat(leftShadeProxiedInput).isNull() - assertThat(rightShadeProxiedInput).isNull() - assertThat(singleShadeProxiedInput).isNull() - } - - @Test - fun dragShadeAboveTouchSlopAndUp() = - testScope.runTest { - val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT)) - val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT)) - val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE)) - - underTest.shouldIntercept( - motionEvent( - MotionEvent.ACTION_DOWN, - x = 100f, // left shade - ) - ) - assertThat(leftShadeProxiedInput).isNull() - assertThat(rightShadeProxiedInput).isNull() - assertThat(singleShadeProxiedInput).isNull() - - val yDragAmountPx = touchSlop + 1f - val moveEvent = - motionEvent( - MotionEvent.ACTION_MOVE, - x = 100f, // left shade - y = yDragAmountPx, - ) - assertThat(underTest.shouldIntercept(moveEvent)).isTrue() - underTest.onTouchEvent(moveEvent, viewWidthPx = 1000) - assertThat(leftShadeProxiedInput) - .isEqualTo( - ProxiedInputModel.OnDrag( - xFraction = 0.1f, - yDragAmountPx = yDragAmountPx, - ) - ) - assertThat(rightShadeProxiedInput).isNull() - assertThat(singleShadeProxiedInput).isNull() - - val upEvent = motionEvent(MotionEvent.ACTION_UP) - assertThat(underTest.shouldIntercept(upEvent)).isTrue() - underTest.onTouchEvent(upEvent, viewWidthPx = 1000) - assertThat(leftShadeProxiedInput).isNull() - assertThat(rightShadeProxiedInput).isNull() - assertThat(singleShadeProxiedInput).isNull() - } - - @Test - fun dragShadeAboveTouchSlopAndCancel() = - testScope.runTest { - val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT)) - val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT)) - val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE)) - - underTest.shouldIntercept( - motionEvent( - MotionEvent.ACTION_DOWN, - x = 900f, // right shade - ) - ) - assertThat(leftShadeProxiedInput).isNull() - assertThat(rightShadeProxiedInput).isNull() - assertThat(singleShadeProxiedInput).isNull() - - val yDragAmountPx = touchSlop + 1f - val moveEvent = - motionEvent( - MotionEvent.ACTION_MOVE, - x = 900f, // right shade - y = yDragAmountPx, - ) - assertThat(underTest.shouldIntercept(moveEvent)).isTrue() - underTest.onTouchEvent(moveEvent, viewWidthPx = 1000) - assertThat(leftShadeProxiedInput).isNull() - assertThat(rightShadeProxiedInput) - .isEqualTo( - ProxiedInputModel.OnDrag( - xFraction = 0.9f, - yDragAmountPx = yDragAmountPx, - ) - ) - assertThat(singleShadeProxiedInput).isNull() - - val cancelEvent = motionEvent(MotionEvent.ACTION_CANCEL) - assertThat(underTest.shouldIntercept(cancelEvent)).isTrue() - underTest.onTouchEvent(cancelEvent, viewWidthPx = 1000) - assertThat(leftShadeProxiedInput).isNull() - assertThat(rightShadeProxiedInput).isNull() - assertThat(singleShadeProxiedInput).isNull() - } - - @Test - fun dragUp_withUp_doesNotShowShade() = - testScope.runTest { - val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT)) - val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT)) - val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE)) - - underTest.shouldIntercept( - motionEvent( - MotionEvent.ACTION_DOWN, - x = 100f, // left shade - ) - ) - assertThat(leftShadeProxiedInput).isNull() - assertThat(rightShadeProxiedInput).isNull() - assertThat(singleShadeProxiedInput).isNull() - - val yDragAmountPx = -(touchSlop + 1f) // dragging up - val moveEvent = - motionEvent( - MotionEvent.ACTION_MOVE, - x = 100f, // left shade - y = yDragAmountPx, - ) - assertThat(underTest.shouldIntercept(moveEvent)).isFalse() - underTest.onTouchEvent(moveEvent, viewWidthPx = 1000) - assertThat(leftShadeProxiedInput).isNull() - assertThat(rightShadeProxiedInput).isNull() - assertThat(singleShadeProxiedInput).isNull() - - val upEvent = motionEvent(MotionEvent.ACTION_UP) - assertThat(underTest.shouldIntercept(upEvent)).isFalse() - underTest.onTouchEvent(upEvent, viewWidthPx = 1000) - assertThat(leftShadeProxiedInput).isNull() - assertThat(rightShadeProxiedInput).isNull() - assertThat(singleShadeProxiedInput).isNull() - } - - @Test - fun dragUp_withCancel_falseTouch_showsThenHidesBouncer() = - testScope.runTest { - val leftShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.LEFT)) - val rightShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.RIGHT)) - val singleShadeProxiedInput by collectLastValue(interactor.proxiedInput(ShadeId.SINGLE)) - - underTest.shouldIntercept( - motionEvent( - MotionEvent.ACTION_DOWN, - x = 900f, // right shade - ) - ) - assertThat(leftShadeProxiedInput).isNull() - assertThat(rightShadeProxiedInput).isNull() - assertThat(singleShadeProxiedInput).isNull() - - val yDragAmountPx = -(touchSlop + 1f) // drag up - val moveEvent = - motionEvent( - MotionEvent.ACTION_MOVE, - x = 900f, // right shade - y = yDragAmountPx, - ) - assertThat(underTest.shouldIntercept(moveEvent)).isFalse() - underTest.onTouchEvent(moveEvent, viewWidthPx = 1000) - assertThat(leftShadeProxiedInput).isNull() - assertThat(rightShadeProxiedInput).isNull() - assertThat(singleShadeProxiedInput).isNull() - - falsingManager.setIsFalseTouch(true) - val cancelEvent = motionEvent(MotionEvent.ACTION_CANCEL) - assertThat(underTest.shouldIntercept(cancelEvent)).isFalse() - underTest.onTouchEvent(cancelEvent, viewWidthPx = 1000) - assertThat(leftShadeProxiedInput).isNull() - assertThat(rightShadeProxiedInput).isNull() - assertThat(singleShadeProxiedInput).isNull() - } - - private fun TestScope.motionEvent( - action: Int, - downTime: Long = currentTime, - eventTime: Long = currentTime, - x: Float = 0f, - y: Float = 0f, - ): MotionEvent { - val motionEvent = MotionEvent.obtain(downTime, eventTime, action, x, y, 0) - motionEvents.add(motionEvent) - return motionEvent - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/shared/math/MathTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/shared/math/MathTest.kt deleted file mode 100644 index 893530982926..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/multishade/shared/math/MathTest.kt +++ /dev/null @@ -1,68 +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.multishade.shared.math - -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.google.common.truth.Truth.assertThat -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 - -@SmallTest -@RunWith(JUnit4::class) -class MathTest : SysuiTestCase() { - - @Test - fun isZero_zero_true() { - assertThat(0f.isZero(epsilon = EPSILON)).isTrue() - } - - @Test - fun isZero_belowPositiveEpsilon_true() { - assertThat((EPSILON * 0.999999f).isZero(epsilon = EPSILON)).isTrue() - } - - @Test - fun isZero_aboveNegativeEpsilon_true() { - assertThat((EPSILON * -0.999999f).isZero(epsilon = EPSILON)).isTrue() - } - - @Test - fun isZero_positiveEpsilon_false() { - assertThat(EPSILON.isZero(epsilon = EPSILON)).isFalse() - } - - @Test - fun isZero_negativeEpsilon_false() { - assertThat((-EPSILON).isZero(epsilon = EPSILON)).isFalse() - } - - @Test - fun isZero_positive_false() { - assertThat(1f.isZero(epsilon = EPSILON)).isFalse() - } - - @Test - fun isZero_negative_false() { - assertThat((-1f).isZero(epsilon = EPSILON)).isFalse() - } - - companion object { - private const val EPSILON = 0.0001f - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModelTest.kt deleted file mode 100644 index 0484515e38bd..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/MultiShadeViewModelTest.kt +++ /dev/null @@ -1,127 +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.multishade.ui.viewmodel - -import androidx.test.filters.SmallTest -import com.android.systemui.R -import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy -import com.android.systemui.multishade.domain.interactor.MultiShadeInteractorTest -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 - -@OptIn(ExperimentalCoroutinesApi::class) -@SmallTest -@RunWith(JUnit4::class) -class MultiShadeViewModelTest : SysuiTestCase() { - - private lateinit var testScope: TestScope - private lateinit var inputProxy: MultiShadeInputProxy - - @Before - fun setUp() { - testScope = TestScope() - inputProxy = MultiShadeInputProxy() - } - - @Test - fun scrim_whenDualShadeCollapsed() = - testScope.runTest { - val alpha = 0.5f - overrideResource(R.dimen.dual_shade_scrim_alpha, alpha) - overrideResource(R.bool.dual_shade_enabled, true) - - val underTest = create() - val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha) - val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled) - - assertThat(scrimAlpha).isZero() - assertThat(isScrimEnabled).isFalse() - } - - @Test - fun scrim_whenDualShadeExpanded() = - testScope.runTest { - val alpha = 0.5f - overrideResource(R.dimen.dual_shade_scrim_alpha, alpha) - overrideResource(R.bool.dual_shade_enabled, true) - val underTest = create() - val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha) - val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled) - assertThat(scrimAlpha).isZero() - assertThat(isScrimEnabled).isFalse() - - underTest.leftShade.onExpansionChanged(0.5f) - assertThat(scrimAlpha).isEqualTo(alpha * 0.5f) - assertThat(isScrimEnabled).isTrue() - - underTest.rightShade.onExpansionChanged(1f) - assertThat(scrimAlpha).isEqualTo(alpha * 1f) - assertThat(isScrimEnabled).isTrue() - } - - @Test - fun scrim_whenSingleShadeCollapsed() = - testScope.runTest { - val alpha = 0.5f - overrideResource(R.dimen.dual_shade_scrim_alpha, alpha) - overrideResource(R.bool.dual_shade_enabled, false) - - val underTest = create() - val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha) - val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled) - - assertThat(scrimAlpha).isZero() - assertThat(isScrimEnabled).isFalse() - } - - @Test - fun scrim_whenSingleShadeExpanded() = - testScope.runTest { - val alpha = 0.5f - overrideResource(R.dimen.dual_shade_scrim_alpha, alpha) - overrideResource(R.bool.dual_shade_enabled, false) - val underTest = create() - val scrimAlpha: Float? by collectLastValue(underTest.scrimAlpha) - val isScrimEnabled: Boolean? by collectLastValue(underTest.isScrimEnabled) - - underTest.singleShade.onExpansionChanged(0.95f) - - assertThat(scrimAlpha).isZero() - assertThat(isScrimEnabled).isFalse() - } - - private fun create(): MultiShadeViewModel { - return MultiShadeViewModel( - viewModelScope = testScope.backgroundScope, - interactor = - MultiShadeInteractorTest.create( - testScope = testScope, - context = context, - inputProxy = inputProxy, - ), - ) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModelTest.kt deleted file mode 100644 index e32aac596e5b..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/multishade/ui/viewmodel/ShadeViewModelTest.kt +++ /dev/null @@ -1,226 +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.multishade.ui.viewmodel - -import androidx.test.filters.SmallTest -import com.android.systemui.R -import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy -import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor -import com.android.systemui.multishade.domain.interactor.MultiShadeInteractorTest -import com.android.systemui.multishade.shared.model.ProxiedInputModel -import com.android.systemui.multishade.shared.model.ShadeId -import com.google.common.truth.Truth.assertThat -import com.google.common.truth.Truth.assertWithMessage -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 - -@OptIn(ExperimentalCoroutinesApi::class) -@SmallTest -@RunWith(JUnit4::class) -class ShadeViewModelTest : SysuiTestCase() { - - private lateinit var testScope: TestScope - private lateinit var inputProxy: MultiShadeInputProxy - private var interactor: MultiShadeInteractor? = null - - @Before - fun setUp() { - testScope = TestScope() - inputProxy = MultiShadeInputProxy() - } - - @Test - fun isVisible_dualShadeConfig() = - testScope.runTest { - overrideResource(R.bool.dual_shade_enabled, true) - val isLeftShadeVisible: Boolean? by collectLastValue(create(ShadeId.LEFT).isVisible) - val isRightShadeVisible: Boolean? by collectLastValue(create(ShadeId.RIGHT).isVisible) - val isSingleShadeVisible: Boolean? by collectLastValue(create(ShadeId.SINGLE).isVisible) - - assertThat(isLeftShadeVisible).isTrue() - assertThat(isRightShadeVisible).isTrue() - assertThat(isSingleShadeVisible).isFalse() - } - - @Test - fun isVisible_singleShadeConfig() = - testScope.runTest { - overrideResource(R.bool.dual_shade_enabled, false) - val isLeftShadeVisible: Boolean? by collectLastValue(create(ShadeId.LEFT).isVisible) - val isRightShadeVisible: Boolean? by collectLastValue(create(ShadeId.RIGHT).isVisible) - val isSingleShadeVisible: Boolean? by collectLastValue(create(ShadeId.SINGLE).isVisible) - - assertThat(isLeftShadeVisible).isFalse() - assertThat(isRightShadeVisible).isFalse() - assertThat(isSingleShadeVisible).isTrue() - } - - @Test - fun isSwipingEnabled() = - testScope.runTest { - val underTest = create(ShadeId.LEFT) - val isSwipingEnabled: Boolean? by collectLastValue(underTest.isSwipingEnabled) - assertWithMessage("isSwipingEnabled should start as true!") - .that(isSwipingEnabled) - .isTrue() - - // Need to collect proxied input so the flows become hot as the gesture cancelation code - // logic sits in side the proxiedInput flow for each shade. - collectLastValue(underTest.proxiedInput) - collectLastValue(create(ShadeId.RIGHT).proxiedInput) - - // Starting a proxied interaction on the LEFT shade disallows non-proxied interaction on - // the - // same shade. - inputProxy.onProxiedInput( - ProxiedInputModel.OnDrag(xFraction = 0f, yDragAmountPx = 123f) - ) - assertThat(isSwipingEnabled).isFalse() - - // Registering the end of the proxied interaction re-allows it. - inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd) - assertThat(isSwipingEnabled).isTrue() - - // Starting a proxied interaction on the RIGHT shade force-collapses the LEFT shade, - // disallowing non-proxied input on the LEFT shade. - inputProxy.onProxiedInput( - ProxiedInputModel.OnDrag(xFraction = 1f, yDragAmountPx = 123f) - ) - assertThat(isSwipingEnabled).isFalse() - - // Registering the end of the interaction on the RIGHT shade re-allows it. - inputProxy.onProxiedInput(ProxiedInputModel.OnDragEnd) - assertThat(isSwipingEnabled).isTrue() - } - - @Test - fun isForceCollapsed_whenOtherShadeInteractionUnderway() = - testScope.runTest { - val leftShade = create(ShadeId.LEFT) - val rightShade = create(ShadeId.RIGHT) - val isLeftShadeForceCollapsed: Boolean? by collectLastValue(leftShade.isForceCollapsed) - val isRightShadeForceCollapsed: Boolean? by - collectLastValue(rightShade.isForceCollapsed) - val isSingleShadeForceCollapsed: Boolean? by - collectLastValue(create(ShadeId.SINGLE).isForceCollapsed) - - assertWithMessage("isForceCollapsed should start as false!") - .that(isLeftShadeForceCollapsed) - .isFalse() - assertWithMessage("isForceCollapsed should start as false!") - .that(isRightShadeForceCollapsed) - .isFalse() - assertWithMessage("isForceCollapsed should start as false!") - .that(isSingleShadeForceCollapsed) - .isFalse() - - // Registering the start of an interaction on the RIGHT shade force-collapses the LEFT - // shade. - rightShade.onDragStarted() - assertThat(isLeftShadeForceCollapsed).isTrue() - assertThat(isRightShadeForceCollapsed).isFalse() - assertThat(isSingleShadeForceCollapsed).isFalse() - - // Registering the end of the interaction on the RIGHT shade re-allows it. - rightShade.onDragEnded() - assertThat(isLeftShadeForceCollapsed).isFalse() - assertThat(isRightShadeForceCollapsed).isFalse() - assertThat(isSingleShadeForceCollapsed).isFalse() - - // Registering the start of an interaction on the LEFT shade force-collapses the RIGHT - // shade. - leftShade.onDragStarted() - assertThat(isLeftShadeForceCollapsed).isFalse() - assertThat(isRightShadeForceCollapsed).isTrue() - assertThat(isSingleShadeForceCollapsed).isFalse() - - // Registering the end of the interaction on the LEFT shade re-allows it. - leftShade.onDragEnded() - assertThat(isLeftShadeForceCollapsed).isFalse() - assertThat(isRightShadeForceCollapsed).isFalse() - assertThat(isSingleShadeForceCollapsed).isFalse() - } - - @Test - fun onTapOutside_collapsesAll() = - testScope.runTest { - val isLeftShadeForceCollapsed: Boolean? by - collectLastValue(create(ShadeId.LEFT).isForceCollapsed) - val isRightShadeForceCollapsed: Boolean? by - collectLastValue(create(ShadeId.RIGHT).isForceCollapsed) - val isSingleShadeForceCollapsed: Boolean? by - collectLastValue(create(ShadeId.SINGLE).isForceCollapsed) - - assertWithMessage("isForceCollapsed should start as false!") - .that(isLeftShadeForceCollapsed) - .isFalse() - assertWithMessage("isForceCollapsed should start as false!") - .that(isRightShadeForceCollapsed) - .isFalse() - assertWithMessage("isForceCollapsed should start as false!") - .that(isSingleShadeForceCollapsed) - .isFalse() - - inputProxy.onProxiedInput(ProxiedInputModel.OnTap) - assertThat(isLeftShadeForceCollapsed).isTrue() - assertThat(isRightShadeForceCollapsed).isTrue() - assertThat(isSingleShadeForceCollapsed).isTrue() - } - - @Test - fun proxiedInput_ignoredWhileNonProxiedGestureUnderway() = - testScope.runTest { - val underTest = create(ShadeId.RIGHT) - val proxiedInput: ProxiedInputModel? by collectLastValue(underTest.proxiedInput) - underTest.onDragStarted() - - inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f)) - assertThat(proxiedInput).isNull() - - inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.8f, 110f)) - assertThat(proxiedInput).isNull() - - underTest.onDragEnded() - - inputProxy.onProxiedInput(ProxiedInputModel.OnDrag(0.9f, 100f)) - assertThat(proxiedInput).isNotNull() - } - - private fun create( - shadeId: ShadeId, - ): ShadeViewModel { - return ShadeViewModel( - viewModelScope = testScope.backgroundScope, - shadeId = shadeId, - interactor = interactor - ?: MultiShadeInteractorTest.create( - testScope = testScope, - context = context, - inputProxy = inputProxy, - ) - .also { interactor = it }, - ) - } -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java index 25d494cee5e8..cbfad56ed617 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java @@ -94,6 +94,7 @@ import com.android.systemui.settings.UserContextProvider; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.NotificationShadeWindowView; import com.android.systemui.shade.ShadeController; +import com.android.systemui.shade.ShadeViewController; import com.android.systemui.shared.rotation.RotationButtonController; import com.android.systemui.shared.system.TaskStackChangeListeners; import com.android.systemui.statusbar.CommandQueue; @@ -467,6 +468,7 @@ public class NavigationBarTest extends SysuiTestCase { when(deviceProvisionedController.isDeviceProvisioned()).thenReturn(true); return spy(new NavigationBar( mNavigationBarView, + mock(ShadeController.class), mNavigationBarFrame, null, context, @@ -485,7 +487,7 @@ public class NavigationBarTest extends SysuiTestCase { Optional.of(mock(Pip.class)), Optional.of(mock(Recents.class)), () -> Optional.of(mCentralSurfaces), - mock(ShadeController.class), + mock(ShadeViewController.class), mock(NotificationRemoteInputManager.class), mock(NotificationShadeDepthController.class), mHandler, 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 611c5b987d84..fab1de00dcbc 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 @@ -20,6 +20,7 @@ import android.os.Handler import android.os.Looper import android.testing.AndroidTestingRunner import android.testing.TestableLooper +import android.view.HapticFeedbackConstants import android.view.MotionEvent import android.view.MotionEvent.ACTION_DOWN import android.view.MotionEvent.ACTION_MOVE @@ -29,6 +30,8 @@ import android.view.WindowManager import androidx.test.filters.SmallTest import com.android.internal.util.LatencyTracker import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION import com.android.systemui.plugins.NavigationEdgeBackPlugin import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.ConfigurationController @@ -36,6 +39,8 @@ import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.verify @@ -58,6 +63,7 @@ class BackPanelControllerTest : SysuiTestCase() { @Mock private lateinit var latencyTracker: LatencyTracker @Mock private lateinit var layoutParams: WindowManager.LayoutParams @Mock private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback + private val featureFlags = FakeFeatureFlags() @Before fun setup() { @@ -70,7 +76,8 @@ class BackPanelControllerTest : SysuiTestCase() { Handler.createAsync(Looper.myLooper()), vibratorHelper, configurationController, - latencyTracker + latencyTracker, + featureFlags ) mBackPanelController.setLayoutParams(layoutParams) mBackPanelController.setBackCallback(backCallback) @@ -99,6 +106,7 @@ class BackPanelControllerTest : SysuiTestCase() { @Test fun handlesBackCommitted() { + featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false) startTouch() // Move once to cross the touch slop continueTouch(START_X + touchSlop.toFloat() + 1) @@ -122,7 +130,34 @@ class BackPanelControllerTest : SysuiTestCase() { } @Test + fun handlesBackCommitted_withOneWayHapticsAPI() { + featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true) + startTouch() + // Move once to cross the touch slop + continueTouch(START_X + touchSlop.toFloat() + 1) + // Move again to cross the back trigger threshold + continueTouch(START_X + touchSlop + triggerThreshold + 1) + // Wait threshold duration and hold touch past trigger threshold + Thread.sleep((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong()) + continueTouch(START_X + touchSlop + triggerThreshold + 1) + + assertThat(mBackPanelController.currentState) + .isEqualTo(BackPanelController.GestureState.ACTIVE) + verify(backCallback).setTriggerBack(true) + testableLooper.moveTimeForward(100) + testableLooper.processAllMessages() + verify(vibratorHelper) + .performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE)) + + finishTouchActionUp(START_X + touchSlop + triggerThreshold + 1) + assertThat(mBackPanelController.currentState) + .isEqualTo(BackPanelController.GestureState.COMMITTED) + verify(backCallback).triggerBack() + } + + @Test fun handlesBackCancelled() { + featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false) startTouch() // Move once to cross the touch slop continueTouch(START_X + touchSlop.toFloat() + 1) @@ -151,6 +186,38 @@ class BackPanelControllerTest : SysuiTestCase() { verify(backCallback).cancelBack() } + @Test + fun handlesBackCancelled_withOneWayHapticsAPI() { + featureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true) + startTouch() + // Move once to cross the touch slop + continueTouch(START_X + touchSlop.toFloat() + 1) + // Move again to cross the back trigger threshold + continueTouch( + START_X + touchSlop + triggerThreshold - + mBackPanelController.params.deactivationTriggerThreshold + ) + // Wait threshold duration and hold touch before trigger threshold + Thread.sleep((MAX_DURATION_ENTRY_BEFORE_ACTIVE_ANIMATION + 1).toLong()) + continueTouch( + START_X + touchSlop + triggerThreshold - + mBackPanelController.params.deactivationTriggerThreshold + ) + clearInvocations(backCallback) + Thread.sleep(MIN_DURATION_ACTIVE_BEFORE_INACTIVE_ANIMATION) + // Move in the opposite direction to cross the deactivation threshold and cancel back + continueTouch(START_X) + + assertThat(mBackPanelController.currentState) + .isEqualTo(BackPanelController.GestureState.INACTIVE) + verify(backCallback).setTriggerBack(false) + verify(vibratorHelper) + .performHapticFeedback(any(), eq(HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE)) + + finishTouchActionUp(START_X) + verify(backCallback).cancelBack() + } + private fun startTouch() { mBackPanelController.onMotionEvent(createMotionEvent(ACTION_DOWN, START_X, 0f)) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt index a76af8e83248..c65a2d36e223 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskControllerTest.kt @@ -118,6 +118,7 @@ internal class NoteTaskControllerTest : SysuiTestCase() { whenever(context.getString(eq(R.string.note_task_shortcut_long_label), any())) .thenReturn(NOTE_TASK_LONG_LABEL) whenever(context.packageManager).thenReturn(packageManager) + whenever(context.createContextAsUser(any(), any())).thenReturn(context) whenever(packageManager.getApplicationInfo(any(), any<Int>())).thenReturn(mock()) whenever(packageManager.getApplicationLabel(any())).thenReturn(NOTE_TASK_LONG_LABEL) whenever(resolver.resolveInfo(any(), any(), any())).thenReturn(NOTE_TASK_INFO) @@ -353,7 +354,13 @@ internal class NoteTaskControllerTest : SysuiTestCase() { @Test fun showNoteTask_defaultUserSet_shouldStartActivityWithExpectedUserAndLogUiEvent() { - whenever(secureSettings.getInt(eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE), any())) + whenever( + secureSettings.getIntForUser( + /* name= */ eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE), + /* def= */ any(), + /* userHandle= */ any() + ) + ) .thenReturn(10) val user10 = UserHandle.of(/* userId= */ 10) @@ -615,13 +622,21 @@ internal class NoteTaskControllerTest : SysuiTestCase() { } @Test - fun showNoteTask_copeDevices_tailButtonEntryPoint_shouldStartBubbleInWorkProfile() { + fun showNoteTask_copeDevices_tailButtonEntryPoint_shouldStartBubbleInTheUserSelectedUser() { + whenever( + secureSettings.getIntForUser( + /* name= */ eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE), + /* def= */ any(), + /* userHandle= */ any() + ) + ) + .thenReturn(mainUserInfo.id) whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) createNoteTaskController().showNoteTask(entryPoint = TAIL_BUTTON) - verifyNoteTaskOpenInBubbleInUser(workUserInfo.userHandle) + verifyNoteTaskOpenInBubbleInUser(mainUserInfo.userHandle) } @Test @@ -813,7 +828,15 @@ internal class NoteTaskControllerTest : SysuiTestCase() { } @Test - fun getUserForHandlingNotesTaking_cope_tailButton_shouldReturnWorkProfileUser() { + fun getUserForHandlingNotesTaking_cope_userSelectedWorkProfile_tailButton_shouldReturnWorkProfileUser() { // ktlint-disable max-line-length + whenever( + secureSettings.getIntForUser( + /* name= */ eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE), + /* def= */ any(), + /* userHandle= */ any() + ) + ) + .thenReturn(workUserInfo.id) whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) @@ -823,6 +846,24 @@ internal class NoteTaskControllerTest : SysuiTestCase() { } @Test + fun getUserForHandlingNotesTaking_cope_userSelectedMainProfile_tailButton_shouldReturnMainProfileUser() { // ktlint-disable max-line-length + whenever( + secureSettings.getIntForUser( + /* name= */ eq(Settings.Secure.DEFAULT_NOTE_TASK_PROFILE), + /* def= */ any(), + /* userHandle= */ any() + ) + ) + .thenReturn(mainUserInfo.id) + whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) + userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) + + val user = createNoteTaskController().getUserForHandlingNotesTaking(TAIL_BUTTON) + + assertThat(user).isEqualTo(UserHandle.of(mainUserInfo.id)) + } + + @Test fun getUserForHandlingNotesTaking_cope_appClip_shouldReturnCurrentUser() { whenever(devicePolicyManager.isOrganizationOwnedDeviceWithManagedProfile).thenReturn(true) userTracker.set(mainAndWorkProfileUsers, mainAndWorkProfileUsers.indexOf(mainUserInfo)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt new file mode 100644 index 000000000000..0a8c0ab9817d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt @@ -0,0 +1,825 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.privacy + +import android.app.ActivityManager +import android.content.Context +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.PackageManager.ResolveInfoFlags +import android.content.pm.ResolveInfo +import android.content.pm.UserInfo +import android.os.Process.SYSTEM_UID +import android.os.UserHandle +import android.permission.PermissionGroupUsage +import android.permission.PermissionManager +import android.testing.AndroidTestingRunner +import android.view.View +import androidx.test.filters.SmallTest +import com.android.internal.logging.UiEventLogger +import com.android.systemui.SysuiTestCase +import com.android.systemui.animation.DialogLaunchAnimator +import com.android.systemui.appops.AppOpsController +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.privacy.logging.PrivacyLogger +import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.Mockito.atLeastOnce +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class PrivacyDialogControllerV2Test : SysuiTestCase() { + + companion object { + private const val USER_ID = 0 + private const val ENT_USER_ID = 10 + + private const val TEST_PACKAGE_NAME = "test package name" + private const val TEST_ATTRIBUTION_TAG = "test attribution tag" + private const val TEST_PROXY_LABEL = "test proxy label" + + private const val PERM_CAMERA = android.Manifest.permission_group.CAMERA + private const val PERM_MICROPHONE = android.Manifest.permission_group.MICROPHONE + private const val PERM_LOCATION = android.Manifest.permission_group.LOCATION + + private val TEST_INTENT = Intent("test_intent_action") + } + + @Mock + private lateinit var dialog: PrivacyDialogV2 + @Mock + private lateinit var permissionManager: PermissionManager + @Mock + private lateinit var packageManager: PackageManager + @Mock + private lateinit var privacyItemController: PrivacyItemController + @Mock + private lateinit var userTracker: UserTracker + @Mock + private lateinit var activityStarter: ActivityStarter + @Mock + private lateinit var privacyLogger: PrivacyLogger + @Mock + private lateinit var keyguardStateController: KeyguardStateController + @Mock + private lateinit var appOpsController: AppOpsController + @Captor + private lateinit var dialogDismissedCaptor: ArgumentCaptor<PrivacyDialogV2.OnDialogDismissed> + @Captor + private lateinit var activityStartedCaptor: ArgumentCaptor<ActivityStarter.Callback> + @Captor + private lateinit var intentCaptor: ArgumentCaptor<Intent> + @Mock + private lateinit var uiEventLogger: UiEventLogger + @Mock + private lateinit var dialogLaunchAnimator: DialogLaunchAnimator + + private val backgroundExecutor = FakeExecutor(FakeSystemClock()) + private val uiExecutor = FakeExecutor(FakeSystemClock()) + private lateinit var controller: PrivacyDialogControllerV2 + private var nextUid: Int = 0 + + private val dialogProvider = object : PrivacyDialogControllerV2.DialogProvider { + var list: List<PrivacyDialogV2.PrivacyElement>? = null + var manageApp: ((String, Int, Intent) -> Unit)? = null + var closeApp: ((String, Int) -> Unit)? = null + var openPrivacyDashboard: (() -> Unit)? = null + + override fun makeDialog( + context: Context, + list: List<PrivacyDialogV2.PrivacyElement>, + manageApp: (String, Int, Intent) -> Unit, + closeApp: (String, Int) -> Unit, + openPrivacyDashboard: () -> Unit + ): PrivacyDialogV2 { + this.list = list + this.manageApp = manageApp + this.closeApp = closeApp + this.openPrivacyDashboard = openPrivacyDashboard + return dialog + } + } + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + nextUid = 0 + setUpDefaultMockResponses() + + controller = PrivacyDialogControllerV2( + permissionManager, + packageManager, + privacyItemController, + userTracker, + activityStarter, + backgroundExecutor, + uiExecutor, + privacyLogger, + keyguardStateController, + appOpsController, + uiEventLogger, + dialogLaunchAnimator, + dialogProvider + ) + } + + @After + fun tearDown() { + FakeExecutor.exhaustExecutors(uiExecutor, backgroundExecutor) + dialogProvider.list = null + dialogProvider.manageApp = null + dialogProvider.closeApp = null + dialogProvider.openPrivacyDashboard = null + } + + @Test + fun testMicMutedParameter() { + `when`(appOpsController.isMicMuted).thenReturn(true) + controller.showDialog(context) + backgroundExecutor.runAllReady() + + verify(permissionManager).getIndicatorAppOpUsageData(true) + } + + @Test + fun testPermissionManagerOnlyCalledInBackgroundThread() { + controller.showDialog(context) + verify(permissionManager, never()).getIndicatorAppOpUsageData(anyBoolean()) + backgroundExecutor.runAllReady() + verify(permissionManager).getIndicatorAppOpUsageData(anyBoolean()) + } + + @Test + fun testPackageManagerOnlyCalledInBackgroundThread() { + val usage = createMockPermGroupUsage() + `when`(usage.isPhoneCall).thenReturn(false) + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) + + controller.showDialog(context) + verify(packageManager, never()).getApplicationInfoAsUser(anyString(), anyInt(), anyInt()) + backgroundExecutor.runAllReady() + verify(packageManager, atLeastOnce()) + .getApplicationInfoAsUser(anyString(), anyInt(), anyInt()) + } + + @Test + fun testShowDialogShowsDialogWithoutView() { + val usage = createMockPermGroupUsage() + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) + + controller.showDialog(context) + exhaustExecutors() + + verify(dialogLaunchAnimator, never()).showFromView(any(), any(), any(), anyBoolean()) + verify(dialog).show() + } + + @Test + fun testShowDialogShowsDialogWithView() { + val view = View(context) + val usage = createMockPermGroupUsage() + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) + + controller.showDialog(context, view) + exhaustExecutors() + + verify(dialogLaunchAnimator).showFromView(dialog, view) + verify(dialog, never()).show() + } + + @Test + fun testDontShowEmptyDialog() { + controller.showDialog(context) + exhaustExecutors() + + verify(dialog, never()).show() + } + + @Test + fun testHideDialogDismissesDialogIfShown() { + val usage = createMockPermGroupUsage() + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) + controller.showDialog(context) + exhaustExecutors() + + controller.dismissDialog() + verify(dialog).dismiss() + } + + @Test + fun testHideDialogNoopIfNotShown() { + controller.dismissDialog() + verify(dialog, never()).dismiss() + } + + @Test + fun testHideDialogNoopAfterDismissed() { + val usage = createMockPermGroupUsage() + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) + controller.showDialog(context) + exhaustExecutors() + + verify(dialog).addOnDismissListener(capture(dialogDismissedCaptor)) + + dialogDismissedCaptor.value.onDialogDismissed() + controller.dismissDialog() + verify(dialog, never()).dismiss() + } + + @Test + fun testShowForAllUsers() { + val usage = createMockPermGroupUsage() + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) + controller.showDialog(context) + + exhaustExecutors() + verify(dialog).setShowForAllUsers(true) + } + + @Test + fun testSingleElementInList() { + val usage = createMockPermGroupUsage( + packageName = TEST_PACKAGE_NAME, + uid = generateUidForUser(USER_ID), + permissionGroupName = PERM_CAMERA, + lastAccessTimeMillis = 5L, + isActive = true, + isPhoneCall = false, + attributionTag = null, + proxyLabel = TEST_PROXY_LABEL + ) + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) + + controller.showDialog(context) + exhaustExecutors() + + dialogProvider.list?.let { list -> + assertThat(list.get(0).type).isEqualTo(PrivacyType.TYPE_CAMERA) + assertThat(list.get(0).packageName).isEqualTo(TEST_PACKAGE_NAME) + assertThat(list.get(0).userId).isEqualTo(USER_ID) + assertThat(list.get(0).applicationName).isEqualTo(TEST_PACKAGE_NAME) + assertThat(list.get(0).attributionTag).isNull() + assertThat(list.get(0).attributionLabel).isNull() + assertThat(list.get(0).proxyLabel).isEqualTo(TEST_PROXY_LABEL) + assertThat(list.get(0).lastActiveTimestamp).isEqualTo(5L) + assertThat(list.get(0).isActive).isTrue() + assertThat(list.get(0).isPhoneCall).isFalse() + assertThat(list.get(0).isService).isFalse() + assertThat(list.get(0).permGroupName).isEqualTo(PERM_CAMERA) + assertThat(isIntentEqual(list.get(0).navigationIntent!!, + controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID))) + .isTrue() + } + } + + private fun isIntentEqual(actual: Intent, expected: Intent): Boolean { + return actual.action == expected.action && + actual.getStringExtra(Intent.EXTRA_PACKAGE_NAME) == + expected.getStringExtra(Intent.EXTRA_PACKAGE_NAME) && + actual.getParcelableExtra(Intent.EXTRA_USER) as? UserHandle == + expected.getParcelableExtra(Intent.EXTRA_USER) as? UserHandle + } + + @Test + fun testTwoElementsDifferentType_sorted() { + val usage_camera = createMockPermGroupUsage( + packageName = "${TEST_PACKAGE_NAME}_camera", + permissionGroupName = PERM_CAMERA + ) + val usage_microphone = createMockPermGroupUsage( + packageName = "${TEST_PACKAGE_NAME}_microphone", + permissionGroupName = PERM_MICROPHONE + ) + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn( + listOf(usage_microphone, usage_camera) + ) + + controller.showDialog(context) + exhaustExecutors() + + dialogProvider.list?.let { list -> + assertThat(list).hasSize(2) + assertThat(list.get(0).type.compareTo(list.get(1).type)).isLessThan(0) + } + } + + @Test + fun testTwoElementsSameType_oneActive() { + val usage_active = createMockPermGroupUsage( + packageName = "${TEST_PACKAGE_NAME}_active", + isActive = true + ) + val usage_recent = createMockPermGroupUsage( + packageName = "${TEST_PACKAGE_NAME}_recent", + isActive = false + ) + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn( + listOf(usage_recent, usage_active) + ) + + controller.showDialog(context) + exhaustExecutors() + + assertThat(dialogProvider.list).hasSize(1) + assertThat(dialogProvider.list?.get(0)?.isActive).isTrue() + } + + @Test + fun testTwoElementsSameType_twoActive() { + val usage_active = createMockPermGroupUsage( + packageName = "${TEST_PACKAGE_NAME}_active", + isActive = true, + lastAccessTimeMillis = 0L + ) + val usage_active_moreRecent = createMockPermGroupUsage( + packageName = "${TEST_PACKAGE_NAME}_active_recent", + isActive = true, + lastAccessTimeMillis = 1L + ) + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn( + listOf(usage_active, usage_active_moreRecent) + ) + controller.showDialog(context) + exhaustExecutors() + assertThat(dialogProvider.list).hasSize(2) + assertThat(dialogProvider.list?.get(0)?.lastActiveTimestamp).isEqualTo(1L) + assertThat(dialogProvider.list?.get(1)?.lastActiveTimestamp).isEqualTo(0L) + } + + @Test + fun testManyElementsSameType_bothRecent() { + val usage_recent = createMockPermGroupUsage( + packageName = "${TEST_PACKAGE_NAME}_recent", + isActive = false, + lastAccessTimeMillis = 0L + ) + val usage_moreRecent = createMockPermGroupUsage( + packageName = "${TEST_PACKAGE_NAME}_moreRecent", + isActive = false, + lastAccessTimeMillis = 1L + ) + val usage_mostRecent = createMockPermGroupUsage( + packageName = "${TEST_PACKAGE_NAME}_mostRecent", + isActive = false, + lastAccessTimeMillis = 2L + ) + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn( + listOf(usage_recent, usage_mostRecent, usage_moreRecent) + ) + + controller.showDialog(context) + exhaustExecutors() + + assertThat(dialogProvider.list).hasSize(1) + assertThat(dialogProvider.list?.get(0)?.lastActiveTimestamp).isEqualTo(2L) + } + + @Test + fun testMicAndCameraDisabled() { + val usage_camera = createMockPermGroupUsage( + permissionGroupName = PERM_CAMERA + ) + val usage_microphone = createMockPermGroupUsage( + permissionGroupName = PERM_MICROPHONE + ) + val usage_location = createMockPermGroupUsage( + permissionGroupName = PERM_LOCATION + ) + + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn( + listOf(usage_camera, usage_location, usage_microphone) + ) + `when`(privacyItemController.micCameraAvailable).thenReturn(false) + + controller.showDialog(context) + exhaustExecutors() + + assertThat(dialogProvider.list).hasSize(1) + assertThat(dialogProvider.list?.get(0)?.type).isEqualTo(PrivacyType.TYPE_LOCATION) + } + + @Test + fun testLocationDisabled() { + val usage_camera = createMockPermGroupUsage( + permissionGroupName = PERM_CAMERA + ) + val usage_microphone = createMockPermGroupUsage( + permissionGroupName = PERM_MICROPHONE + ) + val usage_location = createMockPermGroupUsage( + permissionGroupName = PERM_LOCATION + ) + + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn( + listOf(usage_camera, usage_location, usage_microphone) + ) + `when`(privacyItemController.locationAvailable).thenReturn(false) + + controller.showDialog(context) + exhaustExecutors() + + assertThat(dialogProvider.list).hasSize(2) + dialogProvider.list?.forEach { + assertThat(it.type).isNotEqualTo(PrivacyType.TYPE_LOCATION) + } + } + + @Test + fun testAllIndicatorsAvailable() { + val usage_camera = createMockPermGroupUsage( + permissionGroupName = PERM_CAMERA + ) + val usage_microphone = createMockPermGroupUsage( + permissionGroupName = PERM_MICROPHONE + ) + val usage_location = createMockPermGroupUsage( + permissionGroupName = PERM_LOCATION + ) + + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn( + listOf(usage_camera, usage_location, usage_microphone) + ) + `when`(privacyItemController.micCameraAvailable).thenReturn(true) + `when`(privacyItemController.locationAvailable).thenReturn(true) + + controller.showDialog(context) + exhaustExecutors() + + assertThat(dialogProvider.list).hasSize(3) + } + + @Test + fun testNoIndicatorsAvailable() { + val usage_camera = createMockPermGroupUsage( + permissionGroupName = PERM_CAMERA + ) + val usage_microphone = createMockPermGroupUsage( + permissionGroupName = PERM_MICROPHONE + ) + val usage_location = createMockPermGroupUsage( + permissionGroupName = PERM_LOCATION + ) + + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn( + listOf(usage_camera, usage_location, usage_microphone) + ) + `when`(privacyItemController.micCameraAvailable).thenReturn(false) + `when`(privacyItemController.locationAvailable).thenReturn(false) + + controller.showDialog(context) + exhaustExecutors() + + verify(dialog, never()).show() + } + + @Test + fun testNotCurrentUser() { + val usage_other = createMockPermGroupUsage( + uid = generateUidForUser(ENT_USER_ID + 1) + ) + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())) + .thenReturn(listOf(usage_other)) + + controller.showDialog(context) + exhaustExecutors() + + verify(dialog, never()).show() + } + + @Test + fun testStartActivitySuccess() { + val usage = createMockPermGroupUsage() + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) + controller.showDialog(context) + exhaustExecutors() + + dialogProvider.manageApp?.invoke(TEST_PACKAGE_NAME, USER_ID, TEST_INTENT) + verify(activityStarter).startActivity(any(), eq(true), capture(activityStartedCaptor)) + + activityStartedCaptor.value.onActivityStarted(ActivityManager.START_DELIVERED_TO_TOP) + + verify(dialog).dismiss() + } + + @Test + fun testStartActivityFailure() { + val usage = createMockPermGroupUsage() + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) + controller.showDialog(context) + exhaustExecutors() + + dialogProvider.manageApp?.invoke(TEST_PACKAGE_NAME, USER_ID, TEST_INTENT) + verify(activityStarter).startActivity(any(), eq(true), capture(activityStartedCaptor)) + + activityStartedCaptor.value.onActivityStarted(ActivityManager.START_ABORTED) + + verify(dialog, never()).dismiss() + } + + @Test + fun testCallOnSecondaryUser() { + // Calls happen in + val usage = createMockPermGroupUsage(uid = SYSTEM_UID, isPhoneCall = true) + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) + `when`(userTracker.userProfiles).thenReturn(listOf( + UserInfo(ENT_USER_ID, "", 0) + )) + + controller.showDialog(context) + exhaustExecutors() + + verify(dialog).show() + } + + @Test + fun testManageAppLogs() { + val usage = createMockPermGroupUsage() + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) + controller.showDialog(context) + exhaustExecutors() + + dialogProvider.manageApp?.invoke(TEST_PACKAGE_NAME, USER_ID, TEST_INTENT) + verify(uiEventLogger).log(PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_APP_SETTINGS, + USER_ID, TEST_PACKAGE_NAME) + } + + @Test + fun testCloseAppLogs() { + val usage = createMockPermGroupUsage() + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) + controller.showDialog(context) + exhaustExecutors() + + dialogProvider.closeApp?.invoke(TEST_PACKAGE_NAME, USER_ID) + verify(uiEventLogger).log(PrivacyDialogEvent.PRIVACY_DIALOG_ITEM_CLICKED_TO_CLOSE_APP, + USER_ID, TEST_PACKAGE_NAME) + } + + @Test + fun testOpenPrivacyDashboardLogs() { + val usage = createMockPermGroupUsage() + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) + controller.showDialog(context) + exhaustExecutors() + + dialogProvider.openPrivacyDashboard?.invoke() + verify(uiEventLogger).log(PrivacyDialogEvent.PRIVACY_DIALOG_CLICK_TO_PRIVACY_DASHBOARD) + } + + @Test + fun testDismissedDialogLogs() { + val usage = createMockPermGroupUsage() + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) + controller.showDialog(context) + exhaustExecutors() + + verify(dialog).addOnDismissListener(capture(dialogDismissedCaptor)) + + dialogDismissedCaptor.value.onDialogDismissed() + + controller.dismissDialog() + + verify(uiEventLogger, times(1)).log(PrivacyDialogEvent.PRIVACY_DIALOG_DISMISSED) + } + + @Test + fun testDefaultIntent() { + val usage = createMockPermGroupUsage() + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) + + controller.showDialog(context) + exhaustExecutors() + + dialogProvider.list?.let { list -> + assertThat(isIntentEqual(list.get(0).navigationIntent!!, + controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID))) + .isTrue() + assertThat(list.get(0).isService).isFalse() + } + } + + @Test + fun testDefaultIntentOnEnterpriseUser() { + val usage = + createMockPermGroupUsage( + uid = generateUidForUser(ENT_USER_ID), + ) + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) + + controller.showDialog(context) + exhaustExecutors() + + dialogProvider.list?.let { list -> + assertThat(isIntentEqual(list.get(0).navigationIntent!!, + controller.getDefaultManageAppPermissionsIntent( + TEST_PACKAGE_NAME, ENT_USER_ID))) + .isTrue() + assertThat(list.get(0).isService).isFalse() + } + } + + @Test + fun testDefaultIntentOnInvalidAttributionTag() { + val usage = createMockPermGroupUsage( + attributionTag = "INVALID_ATTRIBUTION_TAG", + proxyLabel = TEST_PROXY_LABEL + ) + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) + + controller.showDialog(context) + exhaustExecutors() + + dialogProvider.list?.let { list -> + assertThat(isIntentEqual(list.get(0).navigationIntent!!, + controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID))) + .isTrue() + assertThat(list.get(0).isService).isFalse() + } + } + + @Test + fun testServiceIntentOnCorrectSubAttribution() { + val usage = createMockPermGroupUsage( + attributionTag = TEST_ATTRIBUTION_TAG, + attributionLabel = "TEST_LABEL" + ) + + val activityInfo = createMockActivityInfo() + val resolveInfo = createMockResolveInfo(activityInfo) + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) + `when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>())) + .thenAnswer { resolveInfo } + controller.showDialog(context) + exhaustExecutors() + + dialogProvider.list?.let { list -> + val navigationIntent = list.get(0).navigationIntent!! + assertThat(navigationIntent.action).isEqualTo(Intent.ACTION_MANAGE_PERMISSION_USAGE) + assertThat(navigationIntent.getStringExtra(Intent.EXTRA_PERMISSION_GROUP_NAME)) + .isEqualTo(PERM_CAMERA) + assertThat(navigationIntent.getStringArrayExtra(Intent.EXTRA_ATTRIBUTION_TAGS)) + .isEqualTo(arrayOf(TEST_ATTRIBUTION_TAG.toString())) + assertThat(navigationIntent.getBooleanExtra(Intent.EXTRA_SHOWING_ATTRIBUTION, false)) + .isTrue() + assertThat(list.get(0).isService).isTrue() + } + } + + @Test + fun testDefaultIntentOnMissingAttributionLabel() { + val usage = createMockPermGroupUsage( + attributionTag = TEST_ATTRIBUTION_TAG + ) + + val activityInfo = createMockActivityInfo() + val resolveInfo = createMockResolveInfo(activityInfo) + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) + `when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>())) + .thenAnswer { resolveInfo } + controller.showDialog(context) + exhaustExecutors() + + dialogProvider.list?.let { list -> + assertThat(isIntentEqual(list.get(0).navigationIntent!!, + controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID))) + .isTrue() + assertThat(list.get(0).isService).isFalse() + } + } + + @Test + fun testDefaultIntentOnIncorrectPermission() { + val usage = createMockPermGroupUsage( + attributionTag = TEST_ATTRIBUTION_TAG + ) + + val activityInfo = createMockActivityInfo( + permission = "INCORRECT_PERMISSION" + ) + val resolveInfo = createMockResolveInfo(activityInfo) + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) + `when`(packageManager.resolveActivity(any(), any<ResolveInfoFlags>())) + .thenAnswer { resolveInfo } + controller.showDialog(context) + exhaustExecutors() + + dialogProvider.list?.let { list -> + assertThat(isIntentEqual(list.get(0).navigationIntent!!, + controller.getDefaultManageAppPermissionsIntent(TEST_PACKAGE_NAME, USER_ID))) + .isTrue() + assertThat(list.get(0).isService).isFalse() + } + } + + private fun exhaustExecutors() { + FakeExecutor.exhaustExecutors(backgroundExecutor, uiExecutor) + } + + private fun setUpDefaultMockResponses() { + `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(emptyList()) + `when`(appOpsController.isMicMuted).thenReturn(false) + + `when`(packageManager.getApplicationInfoAsUser(anyString(), anyInt(), anyInt())) + .thenAnswer { FakeApplicationInfo(it.getArgument(0)) } + + `when`(privacyItemController.locationAvailable).thenReturn(true) + `when`(privacyItemController.micCameraAvailable).thenReturn(true) + + `when`(userTracker.userProfiles).thenReturn(listOf( + UserInfo(USER_ID, "", 0), + UserInfo(ENT_USER_ID, "", UserInfo.FLAG_MANAGED_PROFILE) + )) + + `when`(keyguardStateController.isUnlocked).thenReturn(true) + } + + private class FakeApplicationInfo(val label: CharSequence) : ApplicationInfo() { + override fun loadLabel(pm: PackageManager): CharSequence { + return label + } + } + + private fun generateUidForUser(user: Int): Int { + return user * UserHandle.PER_USER_RANGE + nextUid++ + } + + private fun createMockResolveInfo( + activityInfo: ActivityInfo? = null + ): ResolveInfo { + val resolveInfo = mock(ResolveInfo::class.java) + resolveInfo.activityInfo = activityInfo + return resolveInfo + } + + private fun createMockActivityInfo( + permission: String = android.Manifest.permission.START_VIEW_PERMISSION_USAGE, + className: String = "TEST_CLASS_NAME" + ): ActivityInfo { + val activityInfo = mock(ActivityInfo::class.java) + activityInfo.permission = permission + activityInfo.name = className + return activityInfo + } + + private fun createMockPermGroupUsage( + packageName: String = TEST_PACKAGE_NAME, + uid: Int = generateUidForUser(USER_ID), + permissionGroupName: String = PERM_CAMERA, + lastAccessTimeMillis: Long = 0L, + isActive: Boolean = false, + isPhoneCall: Boolean = false, + attributionTag: CharSequence? = null, + attributionLabel: CharSequence? = null, + proxyLabel: CharSequence? = null + ): PermissionGroupUsage { + val usage = mock(PermissionGroupUsage::class.java) + `when`(usage.packageName).thenReturn(packageName) + `when`(usage.uid).thenReturn(uid) + `when`(usage.permissionGroupName).thenReturn(permissionGroupName) + `when`(usage.lastAccessTimeMillis).thenReturn(lastAccessTimeMillis) + `when`(usage.isActive).thenReturn(isActive) + `when`(usage.isPhoneCall).thenReturn(isPhoneCall) + `when`(usage.attributionTag).thenReturn(attributionTag) + `when`(usage.attributionLabel).thenReturn(attributionLabel) + `when`(usage.proxyLabel).thenReturn(proxyLabel) + return usage + } +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogV2Test.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogV2Test.kt new file mode 100644 index 000000000000..f4644a578d24 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogV2Test.kt @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.privacy + +import android.content.Intent +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.widget.TextView +import androidx.test.filters.SmallTest +import com.android.systemui.R +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class PrivacyDialogV2Test : SysuiTestCase() { + + companion object { + private const val TEST_PACKAGE_NAME = "test_pkg" + private const val TEST_USER_ID = 0 + private const val TEST_PERM_GROUP = "test_perm_group" + + private val TEST_INTENT = Intent("test_intent_action") + + private fun createPrivacyElement( + type: PrivacyType = PrivacyType.TYPE_MICROPHONE, + packageName: String = TEST_PACKAGE_NAME, + userId: Int = TEST_USER_ID, + applicationName: CharSequence = "App", + attributionTag: CharSequence? = null, + attributionLabel: CharSequence? = null, + proxyLabel: CharSequence? = null, + lastActiveTimestamp: Long = 0L, + isActive: Boolean = false, + isPhoneCall: Boolean = false, + isService: Boolean = false, + permGroupName: String = TEST_PERM_GROUP, + navigationIntent: Intent = TEST_INTENT + ) = + PrivacyDialogV2.PrivacyElement( + type, + packageName, + userId, + applicationName, + attributionTag, + attributionLabel, + proxyLabel, + lastActiveTimestamp, + isActive, + isPhoneCall, + isService, + permGroupName, + navigationIntent + ) + } + + @Mock private lateinit var manageApp: (String, Int, Intent) -> Unit + @Mock private lateinit var closeApp: (String, Int) -> Unit + @Mock private lateinit var openPrivacyDashboard: () -> Unit + private lateinit var dialog: PrivacyDialogV2 + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @After + fun teardown() { + if (this::dialog.isInitialized) { + dialog.dismiss() + } + } + + @Test + fun testManageAppCalledWithCorrectParams() { + val list = listOf(createPrivacyElement()) + dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard) + dialog.show() + + dialog.requireViewById<View>(R.id.privacy_dialog_manage_app_button).callOnClick() + + verify(manageApp).invoke(TEST_PACKAGE_NAME, TEST_USER_ID, TEST_INTENT) + } + + @Test + fun testCloseAppCalledWithCorrectParams() { + val list = listOf(createPrivacyElement(isActive = true)) + dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard) + dialog.show() + + dialog.requireViewById<View>(R.id.privacy_dialog_close_app_button).callOnClick() + + verify(closeApp).invoke(TEST_PACKAGE_NAME, TEST_USER_ID) + } + + @Test + fun testCloseAppMissingForService() { + val list = listOf(createPrivacyElement(isActive = true, isService = true)) + dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard) + + dialog.show() + + assertThat(dialog.findViewById<View>(R.id.privacy_dialog_manage_app_button)).isNotNull() + assertThat(dialog.findViewById<View>(R.id.privacy_dialog_close_app_button)).isNull() + } + + @Test + fun testMoreButton() { + dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard) + dialog.show() + + dialog.requireViewById<View>(R.id.privacy_dialog_more_button).callOnClick() + + verify(openPrivacyDashboard).invoke() + } + + @Test + fun testCloseButton() { + dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard) + val dismissListener = mock(PrivacyDialogV2.OnDialogDismissed::class.java) + dialog.addOnDismissListener(dismissListener) + dialog.show() + verify(dismissListener, never()).onDialogDismissed() + + dialog.requireViewById<View>(R.id.privacy_dialog_close_button).callOnClick() + + verify(dismissListener).onDialogDismissed() + } + + @Test + fun testDismissListenerCalledOnDismiss() { + dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard) + val dismissListener = mock(PrivacyDialogV2.OnDialogDismissed::class.java) + dialog.addOnDismissListener(dismissListener) + dialog.show() + verify(dismissListener, never()).onDialogDismissed() + + dialog.dismiss() + + verify(dismissListener).onDialogDismissed() + } + + @Test + fun testDismissListenerCalledImmediatelyIfDialogAlreadyDismissed() { + dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard) + val dismissListener = mock(PrivacyDialogV2.OnDialogDismissed::class.java) + dialog.show() + dialog.dismiss() + + dialog.addOnDismissListener(dismissListener) + + verify(dismissListener).onDialogDismissed() + } + + @Test + fun testCorrectNumElements() { + val list = + listOf( + createPrivacyElement(type = PrivacyType.TYPE_CAMERA, isActive = true), + createPrivacyElement() + ) + dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard) + + dialog.show() + + assertThat( + dialog.requireViewById<ViewGroup>(R.id.privacy_dialog_items_container).childCount + ) + .isEqualTo(2) + } + + @Test + fun testHeaderText() { + val list = listOf(createPrivacyElement()) + dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard) + + dialog.show() + + assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_title).text) + .isEqualTo(TEST_PERM_GROUP) + } + + @Test + fun testUsingText() { + val list = listOf(createPrivacyElement(type = PrivacyType.TYPE_CAMERA, isActive = true)) + dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard) + + dialog.show() + + assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text) + .isEqualTo("In use by App") + } + + @Test + fun testRecentText() { + val list = listOf(createPrivacyElement()) + dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard) + + dialog.show() + + assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text) + .isEqualTo("Recently used by App") + } + + @Test + fun testPhoneCall() { + val list = listOf(createPrivacyElement(isPhoneCall = true)) + dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard) + + dialog.show() + + assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text) + .isEqualTo("Recently used in phone call") + } + + @Test + fun testPhoneCallNotClickable() { + val list = listOf(createPrivacyElement(isPhoneCall = true)) + dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard) + + dialog.show() + + assertThat(dialog.requireViewById<View>(R.id.privacy_dialog_item_card).isClickable) + .isFalse() + assertThat( + dialog + .requireViewById<View>(R.id.privacy_dialog_item_header_expand_toggle) + .visibility + ) + .isEqualTo(View.GONE) + } + + @Test + fun testProxyLabel() { + val list = listOf(createPrivacyElement(proxyLabel = "proxy label")) + dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard) + + dialog.show() + + assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text) + .isEqualTo("Recently used by App (proxy label)") + } + + @Test + fun testSubattribution() { + val list = + listOf( + createPrivacyElement( + attributionLabel = "For subattribution", + isActive = true, + isService = true + ) + ) + dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard) + + dialog.show() + + assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text) + .isEqualTo("In use by App (For subattribution)") + } + + @Test + fun testSubattributionAndProxyLabel() { + val list = + listOf( + createPrivacyElement( + attributionLabel = "For subattribution", + proxyLabel = "proxy label", + isActive = true + ) + ) + dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard) + dialog.show() + assertThat(dialog.requireViewById<TextView>(R.id.privacy_dialog_item_header_summary).text) + .isEqualTo("In use by App (For subattribution \u2022 proxy label)") + } + + @Test + fun testDialogHasTitle() { + val list = listOf(createPrivacyElement()) + dialog = PrivacyDialogV2(context, list, manageApp, closeApp, openPrivacyDashboard) + dialog.show() + + assertThat(dialog.window?.attributes?.title).isEqualTo("Microphone & Camera") + } + + @Test + fun testDialogIsFullscreen() { + dialog = PrivacyDialogV2(context, emptyList(), manageApp, closeApp, openPrivacyDashboard) + dialog.show() + + assertThat(dialog.window?.attributes?.width).isEqualTo(MATCH_PARENT) + assertThat(dialog.window?.attributes?.height).isEqualTo(MATCH_PARENT) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt index 3620233fc9df..fa02e8cb3e54 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt @@ -13,9 +13,12 @@ import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.appops.AppOpsController import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags import com.android.systemui.plugins.ActivityStarter import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.privacy.PrivacyDialogController +import com.android.systemui.privacy.PrivacyDialogControllerV2 import com.android.systemui.privacy.PrivacyItemController import com.android.systemui.privacy.logging.PrivacyLogger import com.android.systemui.statusbar.phone.StatusIconContainer @@ -24,6 +27,7 @@ import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.nullable import com.android.systemui.util.time.FakeSystemClock import org.junit.Before @@ -54,6 +58,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() { @Mock private lateinit var privacyDialogController: PrivacyDialogController @Mock + private lateinit var privacyDialogControllerV2: PrivacyDialogControllerV2 + @Mock private lateinit var privacyLogger: PrivacyLogger @Mock private lateinit var iconContainer: StatusIconContainer @@ -69,6 +75,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() { private lateinit var safetyCenterManager: SafetyCenterManager @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController + @Mock + private lateinit var featureFlags: FeatureFlags private val uiExecutor = FakeExecutor(FakeSystemClock()) private val backgroundExecutor = FakeExecutor(FakeSystemClock()) @@ -94,6 +102,7 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() { uiEventLogger, privacyChip, privacyDialogController, + privacyDialogControllerV2, privacyLogger, iconContainer, permissionManager, @@ -103,7 +112,8 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() { appOpsController, broadcastDispatcher, safetyCenterManager, - deviceProvisionedController + deviceProvisionedController, + featureFlags ) backgroundExecutor.runAllReady() @@ -154,17 +164,55 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() { } @Test - fun testPrivacyChipClicked() { + fun testPrivacyChipClickedWhenNewDialogDisabledAndSafetyCenterDisabled() { + whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(false) + whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(false) + controller.onParentVisible() + val captor = argumentCaptor<View.OnClickListener>() + verify(privacyChip).setOnClickListener(capture(captor)) + captor.value.onClick(privacyChip) + verify(privacyDialogController).showDialog(any(Context::class.java)) + verify(privacyDialogControllerV2, never()) + .showDialog(any(Context::class.java), any(View::class.java)) + } + + @Test + fun testPrivacyChipClickedWhenNewDialogEnabledAndSafetyCenterDisabled() { + whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(true) whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(false) controller.onParentVisible() val captor = argumentCaptor<View.OnClickListener>() verify(privacyChip).setOnClickListener(capture(captor)) captor.value.onClick(privacyChip) verify(privacyDialogController).showDialog(any(Context::class.java)) + verify(privacyDialogControllerV2, never()) + .showDialog(any(Context::class.java), any(View::class.java)) + } + + @Test + fun testPrivacyChipClickedWhenNewDialogDisabledAndSafetyCenterEnabled() { + whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(false) + val receiverCaptor = argumentCaptor<BroadcastReceiver>() + whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(true) + verify(broadcastDispatcher).registerReceiver(capture(receiverCaptor), + any(), any(), nullable(), anyInt(), nullable()) + receiverCaptor.value.onReceive( + context, + Intent(SafetyCenterManager.ACTION_SAFETY_CENTER_ENABLED_CHANGED) + ) + backgroundExecutor.runAllReady() + controller.onParentVisible() + val captor = argumentCaptor<View.OnClickListener>() + verify(privacyChip).setOnClickListener(capture(captor)) + captor.value.onClick(privacyChip) + verify(privacyDialogController, never()).showDialog(any(Context::class.java)) + verify(privacyDialogControllerV2, never()) + .showDialog(any(Context::class.java), any(View::class.java)) } @Test - fun testSafetyCenterFlag() { + fun testPrivacyChipClickedWhenNewDialogEnabledAndSafetyCenterEnabled() { + whenever(featureFlags.isEnabled(Flags.ENABLE_NEW_PRIVACY_DIALOG)).thenReturn(true) val receiverCaptor = argumentCaptor<BroadcastReceiver>() whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(true) verify(broadcastDispatcher).registerReceiver(capture(receiverCaptor), @@ -178,6 +226,7 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() { val captor = argumentCaptor<View.OnClickListener>() verify(privacyChip).setOnClickListener(capture(captor)) captor.value.onClick(privacyChip) + verify(privacyDialogControllerV2).showDialog(any(Context::class.java), eq(privacyChip)) verify(privacyDialogController, never()).showDialog(any(Context::class.java)) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java index 5b3068744df0..3e20511d6278 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java @@ -31,6 +31,7 @@ import static org.mockito.Mockito.when; import android.animation.Animator; import android.content.Intent; +import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.PixelFormat; import android.graphics.drawable.Drawable; @@ -90,6 +91,7 @@ import org.mockito.quality.Strictness; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; @SmallTest @@ -182,6 +184,8 @@ public class InternetDialogControllerTest extends SysuiTestCase { private List<WifiEntry> mAccessPoints = new ArrayList<>(); private List<WifiEntry> mWifiEntries = new ArrayList<>(); + private Configuration mConfig; + @Before public void setUp() { mStaticMockSession = mockitoSession() @@ -226,11 +230,17 @@ public class InternetDialogControllerTest extends SysuiTestCase { mInternetDialogController.mActivityStarter = mActivityStarter; mInternetDialogController.mWifiIconInjector = mWifiIconInjector; mFlags.set(Flags.QS_SECONDARY_DATA_SUB_INFO, false); + + mConfig = new Configuration(mContext.getResources().getConfiguration()); + Configuration c2 = new Configuration(mConfig); + c2.setLocale(Locale.US); + mContext.getResources().updateConfiguration(c2, null); } @After public void tearDown() { mStaticMockSession.finishMocking(); + mContext.getResources().updateConfiguration(mConfig, null); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt index 355c4b667333..49ece66e0cfd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt @@ -29,6 +29,7 @@ import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.keyguard.KeyguardUnlockAnimationController import com.android.systemui.keyguard.ScreenLifecycle import com.android.systemui.keyguard.WakefulnessLifecycle @@ -38,6 +39,7 @@ import com.android.systemui.navigationbar.NavigationModeController import com.android.systemui.recents.OverviewProxyService.ACTION_QUICKSTEP import com.android.systemui.settings.FakeDisplayTracker import com.android.systemui.settings.UserTracker +import com.android.systemui.shade.ShadeViewController import com.android.systemui.shared.recents.IOverviewProxy import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_WAKEFULNESS_MASK import com.android.systemui.shared.system.QuickStepContract.WAKEFULNESS_ASLEEP @@ -48,11 +50,11 @@ import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.phone.CentralSurfaces import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder +import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.android.wm.shell.sysui.ShellInterface import com.google.common.util.concurrent.MoreExecutors -import dagger.Lazy import java.util.Optional import java.util.concurrent.Executor import org.junit.After @@ -81,6 +83,7 @@ class OverviewProxyServiceTest : SysuiTestCase() { private val displayTracker = FakeDisplayTracker(mContext) private val fakeSystemClock = FakeSystemClock() private val sysUiState = SysUiState(displayTracker) + private val featureFlags = FakeFeatureFlags() private val screenLifecycle = ScreenLifecycle(dumpManager) private val wakefulnessLifecycle = WakefulnessLifecycle(mContext, null, fakeSystemClock, dumpManager) @@ -93,6 +96,7 @@ class OverviewProxyServiceTest : SysuiTestCase() { @Mock private lateinit var shellInterface: ShellInterface @Mock private lateinit var navBarController: NavigationBarController @Mock private lateinit var centralSurfaces: CentralSurfaces + @Mock private lateinit var shadeViewController: ShadeViewController @Mock private lateinit var navModeController: NavigationModeController @Mock private lateinit var statusBarWinController: NotificationShadeWindowController @Mock private lateinit var userTracker: UserTracker @@ -130,11 +134,13 @@ class OverviewProxyServiceTest : SysuiTestCase() { executor, commandQueue, shellInterface, - Lazy { navBarController }, - Lazy { Optional.of(centralSurfaces) }, + { navBarController }, + { Optional.of(centralSurfaces) }, + { shadeViewController }, navModeController, statusBarWinController, sysUiState, + mock(), userTracker, screenLifecycle, wakefulnessLifecycle, @@ -142,6 +148,7 @@ class OverviewProxyServiceTest : SysuiTestCase() { displayTracker, sysuiUnlockAnimationController, assistUtils, + featureFlags, dumpManager, unfoldTransitionProgressForwarder ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index 3050c4edd24f..d2bbfa85604b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -100,4 +100,15 @@ class SceneInteractorTest : SysuiTestCase() { ) ) } + + @Test + fun remoteUserInput() = runTest { + val remoteUserInput by collectLastValue(underTest.remoteUserInput) + assertThat(remoteUserInput).isNull() + + for (input in SceneTestUtils.REMOTE_INPUT_DOWN_GESTURE) { + underTest.onRemoteUserInput(input) + assertThat(remoteUserInput).isEqualTo(input) + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt index 6882be7fe184..63ea918c904a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt @@ -18,14 +18,19 @@ package com.android.systemui.scene.ui.viewmodel +import android.view.MotionEvent import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.scene.shared.model.RemoteUserInput +import com.android.systemui.scene.shared.model.RemoteUserInputAction import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.currentTime import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -68,4 +73,35 @@ class SceneContainerViewModelTest : SysuiTestCase() { underTest.setCurrentScene(SceneModel(SceneKey.Shade)) assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Shade)) } + + @Test + fun onRemoteUserInput() = runTest { + val remoteUserInput by collectLastValue(underTest.remoteUserInput) + assertThat(remoteUserInput).isNull() + + val inputs = + SceneTestUtils.REMOTE_INPUT_DOWN_GESTURE.map { remoteUserInputToMotionEvent(it) } + + inputs.forEachIndexed { index, input -> + underTest.onRemoteUserInput(input) + assertThat(remoteUserInput).isEqualTo(SceneTestUtils.REMOTE_INPUT_DOWN_GESTURE[index]) + } + } + + private fun TestScope.remoteUserInputToMotionEvent(input: RemoteUserInput): MotionEvent { + return MotionEvent.obtain( + currentTime, + currentTime, + when (input.action) { + RemoteUserInputAction.DOWN -> MotionEvent.ACTION_DOWN + RemoteUserInputAction.MOVE -> MotionEvent.ACTION_MOVE + RemoteUserInputAction.UP -> MotionEvent.ACTION_UP + RemoteUserInputAction.CANCEL -> MotionEvent.ACTION_CANCEL + RemoteUserInputAction.UNKNOWN -> MotionEvent.ACTION_OUTSIDE + }, + input.x, + input.y, + 0 + ) + } } 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 ecd3308d48d9..202c7bf0d5fd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -108,7 +108,6 @@ import com.android.systemui.media.controls.pipeline.MediaDataManager; import com.android.systemui.media.controls.ui.KeyguardMediaController; import com.android.systemui.media.controls.ui.MediaHierarchyManager; import com.android.systemui.model.SysUiState; -import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.plugins.ActivityStarter; @@ -299,7 +298,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { @Mock protected GoneToDreamingTransitionViewModel mGoneToDreamingTransitionViewModel; @Mock protected KeyguardTransitionInteractor mKeyguardTransitionInteractor; - @Mock protected MultiShadeInteractor mMultiShadeInteractor; @Mock protected KeyguardLongPressViewModel mKeyuardLongPressViewModel; @Mock protected AlternateBouncerInteractor mAlternateBouncerInteractor; @Mock protected MotionEvent mDownMotionEvent; @@ -615,7 +613,6 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mLockscreenToOccludedTransitionViewModel, mMainDispatcher, mKeyguardTransitionInteractor, - () -> mMultiShadeInteractor, mDumpManager, mKeyuardLongPressViewModel, mKeyguardInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index 2a398c55560c..893123d57c99 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -34,21 +34,15 @@ import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor import com.android.systemui.bouncer.domain.interactor.CountDownTimerUtil import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel import com.android.systemui.classifier.FalsingCollectorFake -import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.dock.DockManager import com.android.systemui.dump.logcatLogBuffer import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.KeyguardUnlockAnimationController import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel import com.android.systemui.log.BouncerLogger -import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy -import com.android.systemui.multishade.data.repository.MultiShadeRepository -import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor -import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler import com.android.systemui.statusbar.LockscreenShadeTransitionController @@ -67,6 +61,7 @@ import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.mockito.any import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat +import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.TestScope @@ -80,9 +75,8 @@ import org.mockito.Mockito.anyFloat import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations -import java.util.Optional import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -117,8 +111,9 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { @Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory @Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent @Mock lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController - @Mock private lateinit var unfoldTransitionProgressProvider: - Optional<UnfoldTransitionProgressProvider> + @Mock + private lateinit var unfoldTransitionProgressProvider: + Optional<UnfoldTransitionProgressProvider> @Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor @Mock lateinit var primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel @@ -146,23 +141,11 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { val featureFlags = FakeFeatureFlags() featureFlags.set(Flags.TRACKPAD_GESTURE_COMMON, true) featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false) - featureFlags.set(Flags.DUAL_SHADE, false) featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true) featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false) - val inputProxy = MultiShadeInputProxy() testScope = TestScope() - val multiShadeInteractor = - MultiShadeInteractor( - applicationScope = testScope.backgroundScope, - repository = - MultiShadeRepository( - applicationContext = context, - inputProxy = inputProxy, - ), - inputProxy = inputProxy, - ) underTest = NotificationShadeWindowViewController( lockscreenShadeTransitionController, @@ -193,25 +176,14 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { keyguardTransitionInteractor, primaryBouncerToGoneTransitionViewModel, featureFlags, - { multiShadeInteractor }, FakeSystemClock(), - { - MultiShadeMotionEventInteractor( - applicationContext = context, - applicationScope = testScope.backgroundScope, - multiShadeInteractor = multiShadeInteractor, - featureFlags = featureFlags, - keyguardTransitionInteractor = - KeyguardTransitionInteractorFactory.create( - scope = TestScope().backgroundScope, - ).keyguardTransitionInteractor, - falsingManager = FalsingManagerFake(), - shadeController = shadeController, - ) - }, - BouncerMessageInteractor(FakeBouncerMessageRepository(), - mock(BouncerMessageFactory::class.java), - FakeUserRepository(), CountDownTimerUtil(), featureFlags), + BouncerMessageInteractor( + FakeBouncerMessageRepository(), + mock(BouncerMessageFactory::class.java), + FakeUserRepository(), + CountDownTimerUtil(), + featureFlags + ), BouncerLogger(logcatLogBuffer("BouncerLog")) ) underTest.setupExpandedStatusBar() diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt index d9eb9b9166b3..ed4ac35c7272 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt @@ -34,20 +34,14 @@ import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor import com.android.systemui.bouncer.domain.interactor.CountDownTimerUtil import com.android.systemui.bouncer.ui.viewmodel.KeyguardBouncerViewModel import com.android.systemui.classifier.FalsingCollectorFake -import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.dock.DockManager import com.android.systemui.dump.logcatLogBuffer import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.KeyguardUnlockAnimationController import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor -import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractorFactory import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel import com.android.systemui.log.BouncerLogger -import com.android.systemui.multishade.data.remoteproxy.MultiShadeInputProxy -import com.android.systemui.multishade.data.repository.MultiShadeRepository -import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor -import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler import com.android.systemui.statusbar.DragDownHelper @@ -70,7 +64,6 @@ import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.Optional -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest @@ -85,7 +78,6 @@ import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidTestingRunner::class) @RunWithLooper(setAsMainLooper = true) @SmallTest @@ -160,22 +152,10 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { val featureFlags = FakeFeatureFlags() featureFlags.set(Flags.TRACKPAD_GESTURE_COMMON, true) featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false) - featureFlags.set(Flags.DUAL_SHADE, false) featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true) featureFlags.set(Flags.REVAMPED_BOUNCER_MESSAGES, true) featureFlags.set(Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED, false) - val inputProxy = MultiShadeInputProxy() testScope = TestScope() - val multiShadeInteractor = - MultiShadeInteractor( - applicationScope = testScope.backgroundScope, - repository = - MultiShadeRepository( - applicationContext = context, - inputProxy = inputProxy, - ), - inputProxy = inputProxy, - ) controller = NotificationShadeWindowViewController( lockscreenShadeTransitionController, @@ -206,23 +186,7 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { keyguardTransitionInteractor, primaryBouncerToGoneTransitionViewModel, featureFlags, - { multiShadeInteractor }, FakeSystemClock(), - { - MultiShadeMotionEventInteractor( - applicationContext = context, - applicationScope = testScope.backgroundScope, - multiShadeInteractor = multiShadeInteractor, - featureFlags = featureFlags, - keyguardTransitionInteractor = - KeyguardTransitionInteractorFactory.create( - scope = TestScope().backgroundScope, - ) - .keyguardTransitionInteractor, - falsingManager = FalsingManagerFake(), - shadeController = shadeController, - ) - }, BouncerMessageInteractor( FakeBouncerMessageRepository(), Mockito.mock(BouncerMessageFactory::class.java), diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt index 77a22ac9b092..29bc64e6249d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt @@ -29,6 +29,7 @@ import com.android.systemui.classifier.FalsingCollector import com.android.systemui.dock.DockManager import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.domain.interactor.DozeInteractor import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.power.data.repository.FakePowerRepository @@ -73,6 +74,8 @@ class PulsingGestureListenerTest : SysuiTestCase() { @Mock private lateinit var userTracker: UserTracker @Mock + private lateinit var dozeInteractor: DozeInteractor + @Mock private lateinit var screenOffAnimationController: ScreenOffAnimationController private lateinit var powerRepository: FakePowerRepository @@ -98,6 +101,7 @@ class PulsingGestureListenerTest : SysuiTestCase() { ambientDisplayConfiguration, statusBarStateController, shadeLogger, + dozeInteractor, userTracker, tunerService, dumpManager diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java index 1643e174ee13..b04d5d3d44e4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java @@ -517,6 +517,21 @@ public class CommandQueueTest extends SysuiTestCase { } @Test + public void testConfirmImmersivePrompt() { + mCommandQueue.confirmImmersivePrompt(); + waitForIdleSync(); + verify(mCallbacks).confirmImmersivePrompt(); + } + + @Test + public void testImmersiveModeChanged() { + final int displayAreaId = 10; + mCommandQueue.immersiveModeChanged(displayAreaId, true); + waitForIdleSync(); + verify(mCallbacks).immersiveModeChanged(displayAreaId, true); + } + + @Test public void testShowRearDisplayDialog() { final int currentBaseState = 1; mCommandQueue.showRearDisplayDialog(currentBaseState); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt index ad908e7f8000..aab4bc361d7b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/VibratorHelperTest.kt @@ -6,6 +6,8 @@ import android.os.VibrationAttributes import android.os.VibrationEffect import android.os.Vibrator import android.testing.AndroidTestingRunner +import android.view.HapticFeedbackConstants +import android.view.View import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.util.mockito.eq @@ -33,6 +35,7 @@ class VibratorHelperTest : SysuiTestCase() { @Mock lateinit var vibrator: Vibrator @Mock lateinit var executor: Executor + @Mock lateinit var view: View @Captor lateinit var backgroundTaskCaptor: ArgumentCaptor<Runnable> lateinit var vibratorHelper: VibratorHelper @@ -72,6 +75,21 @@ class VibratorHelperTest : SysuiTestCase() { } @Test + fun testPerformHapticFeedback() { + val constant = HapticFeedbackConstants.CONFIRM + vibratorHelper.performHapticFeedback(view, constant) + verify(view).performHapticFeedback(eq(constant)) + } + + @Test + fun testPerformHapticFeedback_withFlags() { + val constant = HapticFeedbackConstants.CONFIRM + val flag = HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING + vibratorHelper.performHapticFeedback(view, constant, flag) + verify(view).performHapticFeedback(eq(constant), eq(flag)) + } + + @Test fun testHasVibrator() { assertThat(vibratorHelper.hasVibrator()).isTrue() verify(vibrator).hasVibrator() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt index 55b6be9679f2..0b2da8bfa649 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt @@ -18,6 +18,8 @@ package com.android.systemui.statusbar.events import android.content.Context import android.graphics.Rect +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper import android.util.Pair import android.view.Gravity import android.view.View @@ -37,11 +39,14 @@ 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.Mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper class SystemEventChipAnimationControllerTest : SysuiTestCase() { private lateinit var controller: SystemEventChipAnimationController @@ -159,7 +164,7 @@ class SystemEventChipAnimationControllerTest : SysuiTestCase() { assertThat(chipRect).isEqualTo(Rect(890, 25, 990, 75)) } - class TestView(context: Context) : View(context), BackgroundAnimatableView { + private class TestView(context: Context) : View(context), BackgroundAnimatableView { override val view: View get() = this diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt index 2e68cec1fe63..4d4d319a3540 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt @@ -17,6 +17,10 @@ package com.android.systemui.statusbar.notification.row +import android.app.Notification +import android.net.Uri +import android.os.UserHandle +import android.os.UserHandle.USER_ALL import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest @@ -28,13 +32,17 @@ import com.android.systemui.flags.FeatureFlags import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.PluginManager import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.statusbar.SbnBuilder import com.android.systemui.statusbar.SmartReplyController +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.provider.NotificationDismissibilityProvider import com.android.systemui.statusbar.notification.collection.render.FakeNodeController import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager import com.android.systemui.statusbar.notification.logging.NotificationLogger import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier +import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController.BUBBLES_SETTING_URI import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainerLogger import com.android.systemui.statusbar.notification.stack.NotificationListContainer @@ -45,9 +53,9 @@ import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent 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.withArgCaptor import com.android.systemui.util.time.SystemClock import com.android.systemui.wmshell.BubblesManager -import java.util.Optional import junit.framework.Assert import org.junit.After import org.junit.Before @@ -55,7 +63,10 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.mock import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import java.util.* import org.mockito.Mockito.`when` as whenever @SmallTest @@ -92,10 +103,10 @@ class ExpandableNotificationRowControllerTest : SysuiTestCase() { private val featureFlags: FeatureFlags = mock() private val peopleNotificationIdentifier: PeopleNotificationIdentifier = mock() private val bubblesManager: BubblesManager = mock() + private val settingsController: NotificationSettingsController = mock() private val dragController: ExpandableNotificationRowDragController = mock() private val dismissibilityProvider: NotificationDismissibilityProvider = mock() private val statusBarService: IStatusBarService = mock() - private lateinit var controller: ExpandableNotificationRowController @Before @@ -132,11 +143,16 @@ class ExpandableNotificationRowControllerTest : SysuiTestCase() { featureFlags, peopleNotificationIdentifier, Optional.of(bubblesManager), + settingsController, dragController, dismissibilityProvider, statusBarService ) whenever(view.childrenContainer).thenReturn(childrenContainer) + + val notification = Notification.Builder(mContext).build() + val sbn = SbnBuilder().setNotification(notification).build() + whenever(view.entry).thenReturn(NotificationEntryBuilder().setSbn(sbn).build()) } @After @@ -204,4 +220,74 @@ class ExpandableNotificationRowControllerTest : SysuiTestCase() { Mockito.verify(view).removeChildNotification(eq(childView)) Mockito.verify(listContainer).notifyGroupChildRemoved(eq(childView), eq(childrenContainer)) } + + @Test + fun registerSettingsListener_forBubbles() { + controller.init(mock(NotificationEntry::class.java)) + val viewStateObserver = withArgCaptor { + verify(view).addOnAttachStateChangeListener(capture()); + } + viewStateObserver.onViewAttachedToWindow(view); + verify(settingsController).addCallback(any(), any()); + } + + @Test + fun unregisterSettingsListener_forBubbles() { + controller.init(mock(NotificationEntry::class.java)) + val viewStateObserver = withArgCaptor { + verify(view).addOnAttachStateChangeListener(capture()); + } + viewStateObserver.onViewDetachedFromWindow(view); + verify(settingsController).removeCallback(any(), any()); + } + + @Test + fun settingsListener_invalidUri() { + controller.mSettingsListener.onSettingChanged(Uri.EMPTY, view.entry.sbn.userId, "1") + + verify(view, never()).getPrivateLayout() + } + + @Test + fun settingsListener_invalidUserId() { + controller.mSettingsListener.onSettingChanged(BUBBLES_SETTING_URI, -1000, "1") + controller.mSettingsListener.onSettingChanged(BUBBLES_SETTING_URI, -1000, null) + + verify(view, never()).getPrivateLayout() + } + + @Test + fun settingsListener_validUserId() { + val childView: NotificationContentView = mock() + whenever(view.privateLayout).thenReturn(childView) + + controller.mSettingsListener.onSettingChanged( + BUBBLES_SETTING_URI, view.entry.sbn.userId, "1") + verify(childView).setBubblesEnabledForUser(true) + + controller.mSettingsListener.onSettingChanged( + BUBBLES_SETTING_URI, view.entry.sbn.userId, "9") + verify(childView).setBubblesEnabledForUser(false) + } + + @Test + fun settingsListener_userAll() { + val childView: NotificationContentView = mock() + whenever(view.privateLayout).thenReturn(childView) + + val notification = Notification.Builder(mContext).build() + val sbn = SbnBuilder().setNotification(notification) + .setUser(UserHandle.of(USER_ALL)) + .build() + whenever(view.entry).thenReturn(NotificationEntryBuilder() + .setSbn(sbn) + .setUser(UserHandle.of(USER_ALL)) + .build()) + + controller.mSettingsListener.onSettingChanged(BUBBLES_SETTING_URI, 9, "1") + verify(childView).setBubblesEnabledForUser(true) + + controller.mSettingsListener.onSettingChanged(BUBBLES_SETTING_URI, 1, "0") + verify(childView).setBubblesEnabledForUser(false) + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FeedbackInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FeedbackInfoTest.java index 6d687a62b077..cf1d2ca2859d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FeedbackInfoTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FeedbackInfoTest.java @@ -41,6 +41,7 @@ import android.app.NotificationChannel; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.content.res.Configuration; import android.graphics.drawable.Drawable; import android.os.UserHandle; import android.service.notification.StatusBarNotification; @@ -60,12 +61,15 @@ import com.android.systemui.statusbar.notification.AssistantFeedbackController; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Locale; + @SmallTest @RunWith(AndroidTestingRunner.class) @UiThreadTest @@ -87,6 +91,8 @@ public class FeedbackInfoTest extends SysuiTestCase { @Mock private NotificationGutsManager mNotificationGutsManager; + private Configuration mConfig; + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -111,7 +117,15 @@ public class FeedbackInfoTest extends SysuiTestCase { mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0, new Notification(), UserHandle.CURRENT, null, 0); + mConfig = new Configuration(mContext.getResources().getConfiguration()); + Configuration c2 = new Configuration(mConfig); + c2.setLocale(Locale.US); + mContext.getResources().updateConfiguration(c2, null); + } + @After + public void tearDown() { + mContext.getResources().updateConfiguration(mConfig, null); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt index 0b90ebec3ec6..ba6c7fd50bc5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.kt @@ -250,6 +250,9 @@ class NotificationContentViewTest : SysuiTestCase() { .thenReturn(actionListMarginTarget) view.setContainingNotification(mockContainingNotification) + // Given: controller says bubbles are enabled for the user + view.setBubblesEnabledForUser(true); + // When: call NotificationContentView.setExpandedChild() to set the expandedChild view.expandedChild = mockExpandedChild @@ -301,6 +304,9 @@ class NotificationContentViewTest : SysuiTestCase() { view.expandedChild = mockExpandedChild assertEquals(notificationContentMargin, getMarginBottom(actionListMarginTarget)) + // Given: controller says bubbles are enabled for the user + view.setBubblesEnabledForUser(true); + // When: call NotificationContentView.onNotificationUpdated() to update the // NotificationEntry, which should show bubble button view.onNotificationUpdated(createMockNotificationEntry(true)) @@ -405,7 +411,6 @@ class NotificationContentViewTest : SysuiTestCase() { val userMock: UserHandle = mock() whenever(this.sbn).thenReturn(sbnMock) whenever(sbnMock.user).thenReturn(userMock) - doReturn(showButton).whenever(view).shouldShowBubbleButton(this) } private fun createLinearLayoutWithBottomMargin(bottomMargin: Int): LinearLayout { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSettingsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSettingsControllerTest.kt new file mode 100644 index 000000000000..2bccdcafbb6e --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSettingsControllerTest.kt @@ -0,0 +1,245 @@ +/* + * 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.row + +import android.app.ActivityManager +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.provider.Settings.Secure +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.dump.DumpManager +import com.android.systemui.settings.UserTracker +import com.android.systemui.statusbar.notification.row.NotificationSettingsController.Listener +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.settings.SecureSettings +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class NotificationSettingsControllerTest : SysuiTestCase() { + + val setting1: String = Secure.NOTIFICATION_BUBBLES + val setting2: String = Secure.ACCESSIBILITY_ENABLED + val settingUri1: Uri = Secure.getUriFor(setting1) + val settingUri2: Uri = Secure.getUriFor(setting2) + + @Mock + private lateinit var userTracker: UserTracker + private lateinit var handler: Handler + private lateinit var testableLooper: TestableLooper + @Mock + private lateinit var secureSettings: SecureSettings + @Mock + private lateinit var dumpManager: DumpManager + + @Captor + private lateinit var userTrackerCallbackCaptor: ArgumentCaptor<UserTracker.Callback> + @Captor + private lateinit var settingsObserverCaptor: ArgumentCaptor<ContentObserver> + + private lateinit var controller: NotificationSettingsController + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + testableLooper = TestableLooper.get(this) + handler = Handler(testableLooper.looper) + allowTestableLooperAsMainThread() + controller = + NotificationSettingsController( + userTracker, + handler, + secureSettings, + dumpManager + ) + } + + @After + fun tearDown() { + disallowTestableLooperAsMainThread() + } + + @Test + fun creationRegistersCallbacks() { + verify(userTracker).addCallback(any(), any()) + verify(dumpManager).registerNormalDumpable(anyString(), eq(controller)) + } + @Test + fun updateContentObserverRegistration_onUserChange_noSettingsListeners() { + verify(userTracker).addCallback(capture(userTrackerCallbackCaptor), any()) + val userCallback = userTrackerCallbackCaptor.value + val userId = 9 + + // When: User is changed + userCallback.onUserChanged(userId, context) + + // Validate: Nothing to do, since we aren't monitoring settings + verify(secureSettings, never()).unregisterContentObserver(any()) + verify(secureSettings, never()).registerContentObserverForUser( + any(Uri::class.java), anyBoolean(), any(), anyInt()) + } + @Test + fun updateContentObserverRegistration_onUserChange_withSettingsListeners() { + // When: someone is listening to a setting + controller.addCallback(settingUri1, + Mockito.mock(Listener::class.java)) + + verify(userTracker).addCallback(capture(userTrackerCallbackCaptor), any()) + val userCallback = userTrackerCallbackCaptor.value + val userId = 9 + + // Then: User is changed + userCallback.onUserChanged(userId, context) + + // Validate: The tracker is unregistered and re-registered with the new user + verify(secureSettings).unregisterContentObserver(any()) + verify(secureSettings).registerContentObserverForUser( + eq(settingUri1), eq(false), any(), eq(userId)) + } + + @Test + fun addCallback_onlyFirstForUriRegistersObserver() { + controller.addCallback(settingUri1, + Mockito.mock(Listener::class.java)) + verify(secureSettings).registerContentObserverForUser( + eq(settingUri1), eq(false), any(), eq(ActivityManager.getCurrentUser())) + + controller.addCallback(settingUri1, + Mockito.mock(Listener::class.java)) + verify(secureSettings).registerContentObserverForUser( + any(Uri::class.java), anyBoolean(), any(), anyInt()) + } + + @Test + fun addCallback_secondUriRegistersObserver() { + controller.addCallback(settingUri1, + Mockito.mock(Listener::class.java)) + verify(secureSettings).registerContentObserverForUser( + eq(settingUri1), eq(false), any(), eq(ActivityManager.getCurrentUser())) + + controller.addCallback(settingUri2, + Mockito.mock(Listener::class.java)) + verify(secureSettings).registerContentObserverForUser( + eq(settingUri2), eq(false), any(), eq(ActivityManager.getCurrentUser())) + verify(secureSettings).registerContentObserverForUser( + eq(settingUri1), anyBoolean(), any(), anyInt()) + } + + @Test + fun removeCallback_lastUnregistersObserver() { + val listenerSetting1 : Listener = mock() + val listenerSetting2 : Listener = mock() + controller.addCallback(settingUri1, listenerSetting1) + verify(secureSettings).registerContentObserverForUser( + eq(settingUri1), eq(false), any(), eq(ActivityManager.getCurrentUser())) + + controller.addCallback(settingUri2, listenerSetting2) + verify(secureSettings).registerContentObserverForUser( + eq(settingUri2), anyBoolean(), any(), anyInt()) + + controller.removeCallback(settingUri2, listenerSetting2) + verify(secureSettings, never()).unregisterContentObserver(any()) + + controller.removeCallback(settingUri1, listenerSetting1) + verify(secureSettings).unregisterContentObserver(any()) + } + + @Test + fun addCallback_updatesCurrentValue() { + whenever(secureSettings.getStringForUser( + setting1, ActivityManager.getCurrentUser())).thenReturn("9") + whenever(secureSettings.getStringForUser( + setting2, ActivityManager.getCurrentUser())).thenReturn("5") + + val listenerSetting1a : Listener = mock() + val listenerSetting1b : Listener = mock() + val listenerSetting2 : Listener = mock() + + controller.addCallback(settingUri1, listenerSetting1a) + controller.addCallback(settingUri1, listenerSetting1b) + controller.addCallback(settingUri2, listenerSetting2) + + testableLooper.processAllMessages() + + verify(listenerSetting1a).onSettingChanged( + settingUri1, ActivityManager.getCurrentUser(), "9") + verify(listenerSetting1b).onSettingChanged( + settingUri1, ActivityManager.getCurrentUser(), "9") + verify(listenerSetting2).onSettingChanged( + settingUri2, ActivityManager.getCurrentUser(), "5") + } + + @Test + fun removeCallback_noMoreUpdates() { + whenever(secureSettings.getStringForUser( + setting1, ActivityManager.getCurrentUser())).thenReturn("9") + + val listenerSetting1a : Listener = mock() + val listenerSetting1b : Listener = mock() + + // First, register + controller.addCallback(settingUri1, listenerSetting1a) + controller.addCallback(settingUri1, listenerSetting1b) + testableLooper.processAllMessages() + + verify(secureSettings).registerContentObserverForUser( + any(Uri::class.java), anyBoolean(), capture(settingsObserverCaptor), anyInt()) + verify(listenerSetting1a).onSettingChanged( + settingUri1, ActivityManager.getCurrentUser(), "9") + verify(listenerSetting1b).onSettingChanged( + settingUri1, ActivityManager.getCurrentUser(), "9") + Mockito.clearInvocations(listenerSetting1b) + Mockito.clearInvocations(listenerSetting1a) + + // Remove one of them + controller.removeCallback(settingUri1, listenerSetting1a) + + // On update, only remaining listener should get the callback + settingsObserverCaptor.value.onChange(false, settingUri1) + testableLooper.processAllMessages() + + verify(listenerSetting1a, never()).onSettingChanged( + settingUri1, ActivityManager.getCurrentUser(), "9") + verify(listenerSetting1b).onSettingChanged( + settingUri1, ActivityManager.getCurrentUser(), "9") + } + +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt index 5e0e140563cd..68f2728c9ace 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt @@ -30,6 +30,7 @@ import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.plugins.ActivityStarter.OnDismissAction import com.android.systemui.settings.UserTracker import com.android.systemui.shade.ShadeController +import com.android.systemui.shade.ShadeViewController import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.SysuiStatusBarStateController @@ -65,6 +66,7 @@ class ActivityStarterImplTest : SysuiTestCase() { @Mock private lateinit var biometricUnlockController: BiometricUnlockController @Mock private lateinit var keyguardViewMediator: KeyguardViewMediator @Mock private lateinit var shadeController: ShadeController + @Mock private lateinit var shadeViewController: ShadeViewController @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager @Mock private lateinit var activityLaunchAnimator: ActivityLaunchAnimator @Mock private lateinit var lockScreenUserManager: NotificationLockscreenUserManager @@ -91,6 +93,7 @@ class ActivityStarterImplTest : SysuiTestCase() { Lazy { biometricUnlockController }, Lazy { keyguardViewMediator }, Lazy { shadeController }, + Lazy { shadeViewController }, Lazy { statusBarKeyguardViewManager }, Lazy { notifShadeWindowController }, activityLaunchAnimator, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java index 479803e1dfac..045a63cd44a0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.phone; +import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION; import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK; import static com.google.common.truth.Truth.assertThat; @@ -39,6 +40,8 @@ import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper.RunWithLooper; import android.testing.TestableResources; +import android.view.HapticFeedbackConstants; +import android.view.ViewRootImpl; import com.android.internal.logging.MetricsLogger; import com.android.internal.util.LatencyTracker; @@ -47,6 +50,7 @@ import com.android.keyguard.logging.BiometricUnlockLogger; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; @@ -118,8 +122,11 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { private VibratorHelper mVibratorHelper; @Mock private BiometricUnlockLogger mLogger; + @Mock + private ViewRootImpl mViewRootImpl; private final FakeSystemClock mSystemClock = new FakeSystemClock(); private BiometricUnlockController mBiometricUnlockController; + private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); @Before public void setUp() { @@ -143,11 +150,13 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { mAuthController, mStatusBarStateController, mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper, mSystemClock, - mStatusBarKeyguardViewManager + mFeatureFlags ); mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager); mBiometricUnlockController.addListener(mBiometricUnlockEventsListener); when(mUpdateMonitor.getStrongAuthTracker()).thenReturn(mStrongAuthTracker); + when(mStatusBarKeyguardViewManager.getViewRootImpl()).thenReturn(mViewRootImpl); + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false); } @Test @@ -465,78 +474,59 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { } @Test - public void onSideFingerprintSuccess_dreaming_unlockThenWake() { + public void onSideFingerprintSuccess_oldPowerButtonPress_playHaptic() { + // GIVEN side fingerprint enrolled, last wake reason was power button when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); when(mWakefulnessLifecycle.getLastWakeReason()) .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); - final ArgumentCaptor<Runnable> afterKeyguardGoneRunnableCaptor = - ArgumentCaptor.forClass(Runnable.class); - givenDreamingLocked(); - mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, true); - // Make sure the BiometricUnlockController has registered a callback for when the keyguard - // is gone - verify(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable( - afterKeyguardGoneRunnableCaptor.capture()); - // Ensure that the power hasn't been told to wake up yet. - verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString()); - // Check that the keyguard has been told to unlock. - verify(mKeyguardViewMediator).onWakeAndUnlocking(); + // GIVEN last wake time was 500ms ago + when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); + mSystemClock.advanceTime(500); - // Simulate the keyguard disappearing. - afterKeyguardGoneRunnableCaptor.getValue().run(); - // Verify that the power manager has been told to wake up now. - verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString()); + // WHEN biometric fingerprint succeeds + givenFingerprintModeUnlockCollapsing(); + mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, + true); + + // THEN vibrate the device + verify(mVibratorHelper).vibrateAuthSuccess(anyString()); } @Test - public void onSideFingerprintSuccess_dreaming_unlockIfStillLockedNotDreaming() { + public void onSideFingerprintSuccess_oldPowerButtonPress_playOneWayHaptic() { + // GIVEN oneway haptics is enabled + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true); + // GIVEN side fingerprint enrolled, last wake reason was power button when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); when(mWakefulnessLifecycle.getLastWakeReason()) .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); - final ArgumentCaptor<Runnable> afterKeyguardGoneRunnableCaptor = - ArgumentCaptor.forClass(Runnable.class); - givenDreamingLocked(); - mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, true); - - // Make sure the BiometricUnlockController has registered a callback for when the keyguard - // is gone - verify(mStatusBarKeyguardViewManager).addAfterKeyguardGoneRunnable( - afterKeyguardGoneRunnableCaptor.capture()); - // Ensure that the power hasn't been told to wake up yet. - verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString()); - // Check that the keyguard has been told to unlock. - verify(mKeyguardViewMediator).onWakeAndUnlocking(); - - when(mUpdateMonitor.isDreaming()).thenReturn(false); - when(mKeyguardStateController.isUnlocked()).thenReturn(false); - // Simulate the keyguard disappearing. - afterKeyguardGoneRunnableCaptor.getValue().run(); - - final ArgumentCaptor<Runnable> dismissKeyguardRunnableCaptor = - ArgumentCaptor.forClass(Runnable.class); - verify(mHandler).post(dismissKeyguardRunnableCaptor.capture()); + // GIVEN last wake time was 500ms ago + when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); + mSystemClock.advanceTime(500); - // Verify that the power manager was not told to wake up. - verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString()); + // WHEN biometric fingerprint succeeds + givenFingerprintModeUnlockCollapsing(); + mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, + true); - dismissKeyguardRunnableCaptor.getValue().run(); - // Verify that the keyguard controller is told to unlock. - verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(eq(false)); + // THEN vibrate the device + verify(mVibratorHelper).performHapticFeedback( + any(), + eq(HapticFeedbackConstants.CONFIRM) + ); } - @Test - public void onSideFingerprintSuccess_oldPowerButtonPress_playHaptic() { - // GIVEN side fingerprint enrolled, last wake reason was power button + public void onSideFingerprintSuccess_recentGestureWakeUp_playHaptic() { + // GIVEN side fingerprint enrolled, wakeup just happened when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); - when(mWakefulnessLifecycle.getLastWakeReason()) - .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); - - // GIVEN last wake time was 500ms ago when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); - mSystemClock.advanceTime(500); + + // GIVEN last wake reason was from a gesture + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_GESTURE); // WHEN biometric fingerprint succeeds givenFingerprintModeUnlockCollapsing(); @@ -548,7 +538,9 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { } @Test - public void onSideFingerprintSuccess_recentGestureWakeUp_playHaptic() { + public void onSideFingerprintSuccess_recentGestureWakeUp_playOnewayHaptic() { + //GIVEN oneway haptics is enabled + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true); // GIVEN side fingerprint enrolled, wakeup just happened when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); @@ -563,7 +555,10 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { true); // THEN vibrate the device - verify(mVibratorHelper).vibrateAuthSuccess(anyString()); + verify(mVibratorHelper).performHapticFeedback( + any(), + eq(HapticFeedbackConstants.CONFIRM) + ); } @Test @@ -582,6 +577,26 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { } @Test + public void onSideFingerprintFail_alwaysPlaysOneWayHaptic() { + // GIVEN oneway haptics is enabled + mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true); + // GIVEN side fingerprint enrolled, last wake reason was recent power button + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); + when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); + + // WHEN biometric fingerprint fails + mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); + + // THEN always vibrate the device + verify(mVibratorHelper).performHapticFeedback( + any(), + eq(HapticFeedbackConstants.REJECT) + ); + } + + @Test public void onFingerprintDetect_showBouncer() { // WHEN fingerprint detect occurs mBiometricUnlockController.onBiometricDetected(UserHandle.USER_CURRENT, @@ -601,14 +616,25 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { verify(mStatusBarKeyguardViewManager).showPrimaryBouncer(anyBoolean()); } - private void givenDreamingLocked() { - when(mUpdateMonitor.isDreaming()).thenReturn(true); - when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); - } - private void givenFingerprintModeUnlockCollapsing() { when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); when(mUpdateMonitor.isDeviceInteractive()).thenReturn(true); when(mKeyguardStateController.isShowing()).thenReturn(true); } + + private void givenDreamingLocked() { + when(mUpdateMonitor.isDreaming()).thenReturn(true); + when(mUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); + } + @Test + public void onSideFingerprintSuccess_dreaming_unlockNoWake() { + when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); + when(mWakefulnessLifecycle.getLastWakeReason()) + .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); + givenDreamingLocked(); + mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, true); + verify(mKeyguardViewMediator).onWakeAndUnlocking(); + // Ensure that the power hasn't been told to wake up yet. + verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString()); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt index c8ec1bf4af9f..7de0075c45ff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt @@ -27,6 +27,7 @@ import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags +import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.shade.ShadeControllerImpl import com.android.systemui.shade.ShadeLogger import com.android.systemui.shade.ShadeViewController @@ -50,6 +51,7 @@ import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import java.util.Optional +import javax.inject.Provider @SmallTest class PhoneStatusBarViewControllerTest : SysuiTestCase() { @@ -73,6 +75,8 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { @Mock private lateinit var shadeControllerImpl: ShadeControllerImpl @Mock + private lateinit var sceneInteractor: Provider<SceneInteractor> + @Mock private lateinit var shadeLogger: ShadeLogger @Mock private lateinit var viewUtil: ViewUtil @@ -140,8 +144,6 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { @Test fun handleTouchEventFromStatusBar_viewNotEnabled_returnsTrueAndNoViewEvent() { `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true) - `when`(centralSurfacesImpl.shadeViewController) - .thenReturn(shadeViewController) `when`(shadeViewController.isViewEnabled).thenReturn(false) val returnVal = view.onTouchEvent( MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0)) @@ -152,8 +154,6 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { @Test fun handleTouchEventFromStatusBar_viewNotEnabledButIsMoveEvent_viewReceivesEvent() { `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true) - `when`(centralSurfacesImpl.shadeViewController) - .thenReturn(shadeViewController) `when`(shadeViewController.isViewEnabled).thenReturn(false) val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0) @@ -165,8 +165,6 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { @Test fun handleTouchEventFromStatusBar_panelAndViewEnabled_viewReceivesEvent() { `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true) - `when`(centralSurfacesImpl.shadeViewController) - .thenReturn(shadeViewController) `when`(shadeViewController.isViewEnabled).thenReturn(true) val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 2f, 0) @@ -178,8 +176,6 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { @Test fun handleTouchEventFromStatusBar_topEdgeTouch_viewNeverReceivesEvent() { `when`(centralSurfacesImpl.commandQueuePanelsEnabled).thenReturn(true) - `when`(centralSurfacesImpl.shadeViewController) - .thenReturn(shadeViewController) `when`(shadeViewController.isFullyCollapsed).thenReturn(true) val event = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0) @@ -205,6 +201,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { centralSurfacesImpl, shadeControllerImpl, shadeViewController, + sceneInteractor, shadeLogger, viewUtil, configurationController diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt index 2e9a6909e402..e76f26d8128e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt @@ -97,9 +97,7 @@ class UnlockedScreenOffAnimationControllerTest : SysuiTestCase() { powerManager, handler = handler ) - controller.initialize(centralSurfaces, lightRevealScrim) - `when`(centralSurfaces.shadeViewController).thenReturn( - shadeViewController) + controller.initialize(centralSurfaces, shadeViewController, lightRevealScrim) // Screen off does not run if the panel is expanded, so we should say it's collapsed to test // screen off. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt index 5bc98e0d19af..dbaa29bb3688 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositorySwitcherTest.kt @@ -18,7 +18,9 @@ package com.android.systemui.statusbar.pipeline.wifi.data.repository import android.net.ConnectivityManager import android.net.wifi.WifiManager +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase import com.android.systemui.demomode.DemoMode import com.android.systemui.demomode.DemoModeController @@ -43,6 +45,7 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito import org.mockito.MockitoAnnotations @@ -50,6 +53,8 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @SmallTest +@RoboPilotTest +@RunWith(AndroidJUnit4::class) class WifiRepositorySwitcherTest : SysuiTestCase() { private lateinit var underTest: WifiRepositorySwitcher private lateinit var realImpl: WifiRepositoryImpl diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt index 9cf08c03b5d1..206ac1d37074 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt @@ -16,15 +16,20 @@ package com.android.systemui.statusbar.pipeline.wifi.data.repository.prod +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import com.android.systemui.statusbar.pipeline.wifi.shared.model.WifiNetworkModel import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith @SmallTest +@RoboPilotTest +@RunWith(AndroidJUnit4::class) class DisabledWifiRepositoryTest : SysuiTestCase() { private lateinit var underTest: DisabledWifiRepository diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt index 7007345c175c..3cf5f5249f1a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/WifiRepositoryImplTest.kt @@ -32,7 +32,9 @@ import android.net.wifi.WifiManager import android.net.wifi.WifiManager.TrafficStateCallback import android.net.wifi.WifiManager.UNKNOWN_SSID import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.RoboPilotTest import com.android.systemui.SysuiTestCase import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlots @@ -57,6 +59,7 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever @@ -65,6 +68,8 @@ import org.mockito.MockitoAnnotations @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) @SmallTest +@RoboPilotTest +@RunWith(AndroidJUnit4::class) class WifiRepositoryImplTest : SysuiTestCase() { private lateinit var underTest: WifiRepositoryImpl diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt index 813597a8b576..7f990a446aaf 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt @@ -101,7 +101,6 @@ class FoldAodAnimationControllerTest : SysuiTestCase() { whenever(viewGroup.viewTreeObserver).thenReturn(viewTreeObserver) whenever(wakefulnessLifecycle.lastSleepReason) .thenReturn(PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD) - whenever(centralSurfaces.shadeViewController).thenReturn(shadeViewController) whenever(shadeFoldAnimator.startFoldToAodAnimation(any(), any(), any())).then { val onActionStarted = it.arguments[0] as Runnable onActionStarted.run() @@ -124,7 +123,7 @@ class FoldAodAnimationControllerTest : SysuiTestCase() { latencyTracker, { keyguardInteractor }, ) - .apply { initialize(centralSurfaces, lightRevealScrim) } + .apply { initialize(centralSurfaces, shadeViewController, lightRevealScrim) } verify(deviceStateManager).registerCallback(any(), foldStateListenerCaptor.capture()) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt index 0c5e43809fab..2362a5241244 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FakeFingerprintPropertyRepository.kt @@ -20,12 +20,16 @@ import android.hardware.biometrics.SensorLocationInternal import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.SensorStrength import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow class FakeFingerprintPropertyRepository : FingerprintPropertyRepository { + private val _isInitialized: MutableStateFlow<Boolean> = MutableStateFlow(false) + override val isInitialized = _isInitialized.asStateFlow() + private val _sensorId: MutableStateFlow<Int> = MutableStateFlow(-1) - override val sensorId = _sensorId.asStateFlow() + override val sensorId: StateFlow<Int> = _sensorId.asStateFlow() private val _strength: MutableStateFlow<SensorStrength> = MutableStateFlow(SensorStrength.CONVENIENCE) @@ -33,11 +37,12 @@ class FakeFingerprintPropertyRepository : FingerprintPropertyRepository { private val _sensorType: MutableStateFlow<FingerprintSensorType> = MutableStateFlow(FingerprintSensorType.UNKNOWN) - override val sensorType = _sensorType.asStateFlow() + override val sensorType: StateFlow<FingerprintSensorType> = _sensorType.asStateFlow() private val _sensorLocations: MutableStateFlow<Map<String, SensorLocationInternal>> = MutableStateFlow(mapOf("" to SensorLocationInternal.DEFAULT)) - override val sensorLocations = _sensorLocations.asStateFlow() + override val sensorLocations: StateFlow<Map<String, SensorLocationInternal>> = + _sensorLocations.asStateFlow() fun setProperties( sensorId: Int, @@ -49,5 +54,6 @@ class FakeFingerprintPropertyRepository : FingerprintPropertyRepository { _strength.value = strength _sensorType.value = sensorType _sensorLocations.value = sensorLocations + _isInitialized.value = true } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt index 548169e6cccd..f4c2db1b944e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeDeviceEntryFaceAuthRepository.kt @@ -27,6 +27,11 @@ import kotlinx.coroutines.flow.filterNotNull class FakeDeviceEntryFaceAuthRepository : DeviceEntryFaceAuthRepository { + private var _wasDisabled: Boolean = false + + val wasDisabled: Boolean + get() = _wasDisabled + override val isAuthenticated = MutableStateFlow(false) override val canRunFaceAuth = MutableStateFlow(false) private val _authenticationStatus = MutableStateFlow<FaceAuthenticationStatus?>(null) @@ -52,6 +57,9 @@ class FakeDeviceEntryFaceAuthRepository : DeviceEntryFaceAuthRepository { override val isAuthRunning: StateFlow<Boolean> = _isAuthRunning override val isBypassEnabled = MutableStateFlow(false) + override fun lockoutFaceAuth() { + _wasDisabled = true + } override suspend fun authenticate(uiEvent: FaceAuthUiEvent, fallbackToDetection: Boolean) { _runningAuthRequest.value = uiEvent to fallbackToDetection diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt index 7c22604dc546..b24b95e0d3d7 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeLightRevealScrimRepository.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.data.repository import com.android.systemui.statusbar.LightRevealEffect +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow /** Fake implementation of [LightRevealScrimRepository] */ @@ -30,4 +31,15 @@ class FakeLightRevealScrimRepository : LightRevealScrimRepository { fun setRevealEffect(effect: LightRevealEffect) { _revealEffect.tryEmit(effect) } + + private val _revealAmount: MutableStateFlow<Float> = MutableStateFlow(0.0f) + override val revealAmount: Flow<Float> = _revealAmount + + override fun startRevealAmountAnimator(reveal: Boolean) { + if (reveal) { + _revealAmount.value = 1.0f + } else { + _revealAmount.value = 0.0f + } + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt index 931798130499..f39982f54441 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt @@ -38,6 +38,8 @@ import com.android.systemui.keyguard.shared.model.WakefulnessModel import com.android.systemui.keyguard.shared.model.WakefulnessState import com.android.systemui.scene.data.repository.SceneContainerRepository import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.RemoteUserInput +import com.android.systemui.scene.shared.model.RemoteUserInputAction import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.SceneContainerNames import com.android.systemui.scene.shared.model.SceneKey @@ -217,5 +219,14 @@ class SceneTestUtils( companion object { const val CONTAINER_1 = SceneContainerNames.SYSTEM_UI_DEFAULT const val CONTAINER_2 = "container2" + + val REMOTE_INPUT_DOWN_GESTURE = + listOf( + RemoteUserInput(10f, 10f, RemoteUserInputAction.DOWN), + RemoteUserInput(10f, 20f, RemoteUserInputAction.MOVE), + RemoteUserInput(10f, 30f, RemoteUserInputAction.MOVE), + RemoteUserInput(10f, 40f, RemoteUserInputAction.MOVE), + RemoteUserInput(10f, 40f, RemoteUserInputAction.UP), + ) } } diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java index 3f3fa3419117..d33d224cb2d2 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationController.java @@ -142,6 +142,8 @@ public class FullScreenMagnificationController implements private int mIdOfLastServiceToMagnify = INVALID_SERVICE_ID; private boolean mMagnificationActivated = false; + private boolean mZoomedOutFromService = false; + @GuardedBy("mLock") @Nullable private MagnificationThumbnail mMagnificationThumbnail; DisplayMagnification(int displayId) { @@ -545,6 +547,24 @@ public class FullScreenMagnificationController implements return changed; } + /** + * Directly Zooms out the scale to 1f with animating the transition. This method is + * triggered only by service automatically, such as when user context changed. + */ + void zoomOutFromService() { + setScaleAndCenter(1.0f, Float.NaN, Float.NaN, + transformToStubCallback(true), + AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); + mZoomedOutFromService = true; + } + + /** + * Whether the zooming out is triggered by {@link #zoomOutFromService}. + */ + boolean isZoomedOutFromService() { + return mZoomedOutFromService; + } + @GuardedBy("mLock") boolean reset(boolean animate) { return reset(transformToStubCallback(animate)); @@ -619,6 +639,8 @@ public class FullScreenMagnificationController implements mIdOfLastServiceToMagnify); }); } + // the zoom scale would be changed so we reset the flag + mZoomedOutFromService = false; return changed; } @@ -944,9 +966,7 @@ public class FullScreenMagnificationController implements } if (isAlwaysOnMagnificationEnabled()) { - setScaleAndCenter(displayId, 1.0f, Float.NaN, Float.NaN, - true, - AccessibilityManagerService.MAGNIFICATION_GESTURE_HANDLER_ID); + zoomOutFromService(displayId); } else { reset(displayId, true); } @@ -1428,6 +1448,37 @@ public class FullScreenMagnificationController implements } /** + * Directly Zooms out the scale to 1f with animating the transition. This method is + * triggered only by service automatically, such as when user context changed. + * + * @param displayId The logical display id. + */ + private void zoomOutFromService(int displayId) { + synchronized (mLock) { + final DisplayMagnification display = mDisplays.get(displayId); + if (display == null || !display.isActivated()) { + return; + } + display.zoomOutFromService(); + } + } + + /** + * Whether the magnification is zoomed out by {@link #zoomOutFromService(int)}. + * + * @param displayId The logical display id. + */ + public boolean isZoomedOutFromService(int displayId) { + synchronized (mLock) { + final DisplayMagnification display = mDisplays.get(displayId); + if (display == null || !display.isActivated()) { + return false; + } + return display.isZoomedOutFromService(); + } + } + + /** * Resets all displays' magnification if last magnifying service is disabled. * * @param connectionId diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java index 4aebbf11c7af..fd8babbbd5b1 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java @@ -204,7 +204,9 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH mViewportDraggingState = new ViewportDraggingState(); mPanningScalingState = new PanningScalingState(context); mSinglePanningState = new SinglePanningState(context); - setSinglePanningEnabled(false); + setSinglePanningEnabled( + context.getResources() + .getBoolean(R.bool.config_enable_a11y_magnification_single_panning)); if (mDetectShortcutTrigger) { mScreenStateReceiver = new ScreenStateReceiver(context, this); @@ -1093,7 +1095,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH mViewportDraggingState.prepareForZoomInTemporary(shortcutTriggered); - zoomInTemporary(down.getX(), down.getY()); + zoomInTemporary(down.getX(), down.getY(), shortcutTriggered); transitionTo(mViewportDraggingState); } @@ -1150,14 +1152,20 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH } } - private void zoomInTemporary(float centerX, float centerY) { + private void zoomInTemporary(float centerX, float centerY, boolean shortcutTriggered) { final float currentScale = mFullScreenMagnificationController.getScale(mDisplayId); final float persistedScale = MathUtils.constrain( mFullScreenMagnificationController.getPersistedScale(mDisplayId), MIN_SCALE, MAX_SCALE); final boolean isActivated = mFullScreenMagnificationController.isActivated(mDisplayId); - final float scale = isActivated ? (currentScale + 1.0f) : persistedScale; + final boolean isShortcutTriggered = shortcutTriggered; + final boolean isZoomedOutFromService = + mFullScreenMagnificationController.isZoomedOutFromService(mDisplayId); + + boolean zoomInWithPersistedScale = + !isActivated || isShortcutTriggered || isZoomedOutFromService; + final float scale = zoomInWithPersistedScale ? persistedScale : (currentScale + 1.0f); zoomToScale(scale, centerX, centerY); } diff --git a/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING index 279981bb719a..d862a5410079 100644 --- a/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING +++ b/services/companion/java/com/android/server/companion/virtual/TEST_MAPPING @@ -9,6 +9,30 @@ ] }, { + "name": "CtsVirtualDevicesAudioTestCases", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + }, + { + "name": "CtsVirtualDevicesSensorTestCases", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + }, + { + "name": "CtsVirtualDevicesAppLaunchTestCases", + "options": [ + { + "exclude-annotation": "androidx.test.filters.FlakyTest" + } + ] + }, + { "name": "CtsVirtualDevicesTestCases", "options": [ { diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index bbe72896ab3d..77508a8fcc0d 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -687,14 +687,12 @@ public class VirtualDeviceManagerService extends SystemService { } @Override - public int getAssociationIdForDevice(int deviceId) { + public @Nullable String getPersistentIdForDevice(int deviceId) { VirtualDeviceImpl virtualDevice; synchronized (mVirtualDeviceManagerLock) { virtualDevice = mVirtualDevices.get(deviceId); } - return virtualDevice == null - ? VirtualDeviceManager.ASSOCIATION_ID_INVALID - : virtualDevice.getAssociationId(); + return virtualDevice == null ? null : virtualDevice.getPersistentDeviceId(); } @Override diff --git a/services/core/java/com/android/server/PersistentDataBlockService.java b/services/core/java/com/android/server/PersistentDataBlockService.java index 6fd6afed49b9..754a7ede8006 100644 --- a/services/core/java/com/android/server/PersistentDataBlockService.java +++ b/services/core/java/com/android/server/PersistentDataBlockService.java @@ -159,9 +159,10 @@ public class PersistentDataBlockService extends SystemService { private int getAllowedUid() { final UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class); - final int mainUserId = umInternal.getMainUserId(); + int mainUserId = umInternal.getMainUserId(); if (mainUserId < 0) { - return -1; + // If main user is not defined. Use the SYSTEM user instead. + mainUserId = UserHandle.USER_SYSTEM; } String allowedPackage = mContext.getResources() .getString(R.string.config_persistentDataPackageName); diff --git a/services/core/java/com/android/server/TEST_MAPPING b/services/core/java/com/android/server/TEST_MAPPING index 1e3a5f4f7500..41cca4968f2c 100644 --- a/services/core/java/com/android/server/TEST_MAPPING +++ b/services/core/java/com/android/server/TEST_MAPPING @@ -14,10 +14,10 @@ "file_patterns": ["NotificationManagerService\\.java"] }, { - "name": "CtsWindowManagerDeviceTestCases", + "name": "CtsWindowManagerDeviceWindow", "options": [ { - "include-filter": "android.server.wm.ToastWindowTest" + "include-filter": "android.server.wm.window.ToastWindowTest" } ], "file_patterns": ["NotificationManagerService\\.java"] diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index 146215b211d3..d39826077395 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -4932,7 +4932,6 @@ public class AccountManagerService if (accountType == null) throw new IllegalArgumentException("accountType is null"); mAccounts = accounts; mStripAuthTokenFromResult = stripAuthTokenFromResult; - mResponse = response; mAccountType = accountType; mExpectActivityLaunch = expectActivityLaunch; mCreationTime = SystemClock.elapsedRealtime(); @@ -4946,8 +4945,8 @@ public class AccountManagerService if (response != null) { try { response.asBinder().linkToDeath(this, 0 /* flags */); + mResponse = response; } catch (RemoteException e) { - mResponse = null; binderDied(); } } diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index d199006bd4e8..dc83125503a9 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -8169,7 +8169,13 @@ public final class ActiveServices { mAm.getUidStateLocked(r.mRecentCallingUid), mAm.getUidProcessCapabilityLocked(r.mRecentCallingUid), 0, - 0); + 0, + r.mAllowWhileInUsePermissionInFgsReasonNoBinding, + r.mAllowWIUInBindService, + r.mAllowWIUByBindings, + r.mAllowStartForegroundNoBinding, + r.mAllowStartInBindService, + r.mAllowStartByBindings); int event = 0; if (state == FOREGROUND_SERVICE_STATE_CHANGED__STATE__ENTER) { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 6c434e158069..a0fae26fcac1 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -9013,7 +9013,9 @@ public class ActivityManagerService extends IActivityManager.Stub final boolean isSystemApp = process == null || (process.info.flags & (ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) != 0; - final String processName = process == null ? "unknown" : process.processName; + final String processName = process != null && process.getPid() == MY_PID + ? "system_server" + : (process == null ? "unknown" : process.processName); final DropBoxManager dbox = (DropBoxManager) mContext.getSystemService(Context.DROPBOX_SERVICE); diff --git a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java index 4f5b5e1fbd68..786e1cc7075f 100644 --- a/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java +++ b/services/core/java/com/android/server/am/ForegroundServiceTypeLoggerModule.java @@ -506,7 +506,13 @@ public class ForegroundServiceTypeLoggerModule { ActivityManager.PROCESS_STATE_UNKNOWN, ActivityManager.PROCESS_CAPABILITY_NONE, apiDurationBeforeFgsStart, - apiDurationAfterFgsEnd); + apiDurationAfterFgsEnd, + r.mAllowWhileInUsePermissionInFgsReasonNoBinding, + r.mAllowWIUInBindService, + r.mAllowWIUByBindings, + r.mAllowStartForegroundNoBinding, + r.mAllowStartInBindService, + r.mAllowStartByBindings); } /** @@ -557,7 +563,13 @@ public class ForegroundServiceTypeLoggerModule { ActivityManager.PROCESS_STATE_UNKNOWN, ActivityManager.PROCESS_CAPABILITY_NONE, 0, - apiDurationAfterFgsEnd); + apiDurationAfterFgsEnd, + 0, + 0, + 0, + 0, + 0, + 0); } /** diff --git a/services/core/java/com/android/server/appop/TEST_MAPPING b/services/core/java/com/android/server/appop/TEST_MAPPING index 72d3835efc26..68062b566906 100644 --- a/services/core/java/com/android/server/appop/TEST_MAPPING +++ b/services/core/java/com/android/server/appop/TEST_MAPPING @@ -1,7 +1,12 @@ { "presubmit": [ { - "name": "CtsAppOpsTestCases" + "name": "CtsAppOpsTestCases", + "options": [ + { + "exclude-annotation": "android.platform.test.annotations.FlakyTest" + } + ] }, { "name": "CtsAppOps2TestCases" @@ -26,6 +31,9 @@ "name": "CtsPermissionTestCases", "options": [ { + "exclude-annotation": "android.platform.test.annotations.FlakyTest" + }, + { "include-filter": "android.permission.cts.BackgroundPermissionsTest" }, { @@ -55,5 +63,27 @@ } ] } + ], + "postsubmit": [ + { + "name": "CtsAppOpsTestCases" + }, + { + "name": "CtsPermissionTestCases", + "options": [ + { + "include-filter": "android.permission.cts.BackgroundPermissionsTest" + }, + { + "include-filter": "android.permission.cts.SplitPermissionTest" + }, + { + "include-filter": "android.permission.cts.PermissionFlagsTest" + }, + { + "include-filter": "android.permission.cts.SharedUidPermissionsTest" + } + ] + } ] } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java index 54d1faa39be0..3d0ea9d8bef6 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java @@ -260,14 +260,6 @@ class FingerprintAuthenticationClient final AidlSession session = getFreshDaemon(); final OperationContextExt opContext = getOperationContext(); - final ICancellationSignal cancel; - if (session.hasContextMethods()) { - cancel = session.getSession().authenticateWithContext( - mOperationId, opContext.toAidlContext(getOptions())); - } else { - cancel = session.getSession().authenticate(mOperationId); - } - getBiometricContext().subscribe(opContext, ctx -> { if (session.hasContextMethods()) { try { @@ -289,7 +281,12 @@ class FingerprintAuthenticationClient mALSProbeCallback.getProbe().enable(); } - return cancel; + if (session.hasContextMethods()) { + return session.getSession().authenticateWithContext( + mOperationId, opContext.toAidlContext(getOptions())); + } else { + return session.getSession().authenticate(mOperationId); + } } @Override diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java index ee323f9dfd6a..283353ddc25d 100644 --- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java +++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java @@ -19,6 +19,7 @@ package com.android.server.companion.virtual; import android.annotation.NonNull; import android.annotation.Nullable; import android.companion.virtual.IVirtualDevice; +import android.companion.virtual.VirtualDevice; import android.companion.virtual.sensor.VirtualSensor; import android.os.LocaleList; import android.util.ArraySet; @@ -158,12 +159,12 @@ public abstract class VirtualDeviceManagerInternal { public abstract @NonNull ArraySet<Integer> getDisplayIdsForDevice(int deviceId); /** - * Gets the CDM association ID for the VirtualDevice with the given device ID. + * Gets the persistent ID for the VirtualDevice with the given device ID. * * @param deviceId which device we're asking about - * @return the CDM association ID for this device, or - * {@link android.companion.virtual.VirtualDeviceManager#ASSOCIATION_ID_INVALID} if no such - * association exists. + * @return the persistent ID for this device, or {@code null} if no such ID exists. + * + * @see VirtualDevice#getPersistentDeviceId() */ - public abstract int getAssociationIdForDevice(int deviceId); + public abstract @Nullable String getPersistentIdForDevice(int deviceId); } diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 3a0b8b5f12b1..19dffeba868e 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -3138,11 +3138,6 @@ public final class DisplayManagerService extends SystemService { // with the corresponding displaydevice. HighBrightnessModeMetadata hbmMetadata = mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display); - if (hbmMetadata == null) { - Slog.wtf(TAG, "High Brightness Mode Metadata is null in DisplayManagerService for " - + "display: " + display.getDisplayIdLocked()); - return null; - } if (mConfigParameterProvider.isNewPowerControllerFeatureEnabled()) { displayPowerController = new DisplayPowerController2( mContext, /* injector= */ null, mDisplayPowerCallbacks, mPowerHandler, diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 75c15ebaade9..a717808ef1ed 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -450,6 +450,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call private float[] mNitsRange; private final BrightnessRangeController mBrightnessRangeController; + + @Nullable private final HighBrightnessModeMetadata mHighBrightnessModeMetadata; private final BrightnessThrottler mBrightnessThrottler; diff --git a/services/core/java/com/android/server/display/HighBrightnessModeController.java b/services/core/java/com/android/server/display/HighBrightnessModeController.java index 11160a532609..c04c2793b3c5 100644 --- a/services/core/java/com/android/server/display/HighBrightnessModeController.java +++ b/services/core/java/com/android/server/display/HighBrightnessModeController.java @@ -16,6 +16,7 @@ package com.android.server.display; +import android.annotation.Nullable; import android.content.Context; import android.database.ContentObserver; import android.hardware.display.BrightnessInfo; @@ -75,6 +76,8 @@ class HighBrightnessModeController { private final Injector mInjector; private HdrListener mHdrListener; + + @Nullable private HighBrightnessModeData mHbmData; private HdrBrightnessDeviceConfig mHdrBrightnessCfg; private IBinder mRegisteredDisplayToken; @@ -107,7 +110,9 @@ class HighBrightnessModeController { * If HBM is currently running, this is the start time and set of all events, * for the current HBM session. */ - private HighBrightnessModeMetadata mHighBrightnessModeMetadata = null; + @Nullable + private HighBrightnessModeMetadata mHighBrightnessModeMetadata; + HighBrightnessModeController(Handler handler, int width, int height, IBinder displayToken, String displayUniqueId, float brightnessMin, float brightnessMax, HighBrightnessModeData hbmData, HdrBrightnessDeviceConfig hdrBrightnessCfg, @@ -310,23 +315,29 @@ class HighBrightnessModeController { pw.println(" mBrightnessMax=" + mBrightnessMax); pw.println(" remainingTime=" + calculateRemainingTime(mClock.uptimeMillis())); pw.println(" mIsTimeAvailable= " + mIsTimeAvailable); - pw.println(" mRunningStartTimeMillis=" - + TimeUtils.formatUptime(mHighBrightnessModeMetadata.getRunningStartTimeMillis())); pw.println(" mIsBlockedByLowPowerMode=" + mIsBlockedByLowPowerMode); pw.println(" width*height=" + mWidth + "*" + mHeight); - pw.println(" mEvents="); - final long currentTime = mClock.uptimeMillis(); - long lastStartTime = currentTime; - long runningStartTimeMillis = mHighBrightnessModeMetadata.getRunningStartTimeMillis(); - if (runningStartTimeMillis != -1) { - lastStartTime = dumpHbmEvent(pw, new HbmEvent(runningStartTimeMillis, currentTime)); - } - for (HbmEvent event : mHighBrightnessModeMetadata.getHbmEventQueue()) { - if (lastStartTime > event.getEndTimeMillis()) { - pw.println(" event: [normal brightness]: " - + TimeUtils.formatDuration(lastStartTime - event.getEndTimeMillis())); + + if (mHighBrightnessModeMetadata != null) { + pw.println(" mRunningStartTimeMillis=" + + TimeUtils.formatUptime( + mHighBrightnessModeMetadata.getRunningStartTimeMillis())); + pw.println(" mEvents="); + final long currentTime = mClock.uptimeMillis(); + long lastStartTime = currentTime; + long runningStartTimeMillis = mHighBrightnessModeMetadata.getRunningStartTimeMillis(); + if (runningStartTimeMillis != -1) { + lastStartTime = dumpHbmEvent(pw, new HbmEvent(runningStartTimeMillis, currentTime)); } - lastStartTime = dumpHbmEvent(pw, event); + for (HbmEvent event : mHighBrightnessModeMetadata.getHbmEventQueue()) { + if (lastStartTime > event.getEndTimeMillis()) { + pw.println(" event: [normal brightness]: " + + TimeUtils.formatDuration(lastStartTime - event.getEndTimeMillis())); + } + lastStartTime = dumpHbmEvent(pw, event); + } + } else { + pw.println(" mHighBrightnessModeMetadata=null"); } } @@ -353,7 +364,7 @@ class HighBrightnessModeController { } private boolean deviceSupportsHbm() { - return mHbmData != null; + return mHbmData != null && mHighBrightnessModeMetadata != null; } private long calculateRemainingTime(long currentTime) { diff --git a/services/core/java/com/android/server/display/HighBrightnessModeMetadataMapper.java b/services/core/java/com/android/server/display/HighBrightnessModeMetadataMapper.java index 76702d3f6f8c..9e6f0eb93831 100644 --- a/services/core/java/com/android/server/display/HighBrightnessModeMetadataMapper.java +++ b/services/core/java/com/android/server/display/HighBrightnessModeMetadataMapper.java @@ -41,6 +41,9 @@ class HighBrightnessModeMetadataMapper { + display.getDisplayIdLocked()); return null; } + if (device.getDisplayDeviceConfig().getHighBrightnessModeData() == null) { + return null; + } final String uniqueId = device.getUniqueId(); diff --git a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java index 45f1be076508..f141c20158cd 100644 --- a/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java +++ b/services/core/java/com/android/server/display/brightness/DisplayBrightnessStrategySelector.java @@ -119,7 +119,7 @@ public class DisplayBrightnessStrategySelector { if (!mOldBrightnessStrategyName.equals(displayBrightnessStrategy.getName())) { Slog.i(TAG, "Changing the DisplayBrightnessStrategy from " + mOldBrightnessStrategyName - + " to" + displayBrightnessStrategy.getName() + " for display " + + " to " + displayBrightnessStrategy.getName() + " for display " + mDisplayId); mOldBrightnessStrategyName = displayBrightnessStrategy.getName(); } diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java index 11e35ce09c28..44524e210a77 100644 --- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java +++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java @@ -2322,8 +2322,7 @@ public class DisplayModeDirector { private final SparseBooleanArray mAuthenticationPossible = new SparseBooleanArray(); public void observe() { - StatusBarManagerInternal statusBar = - LocalServices.getService(StatusBarManagerInternal.class); + StatusBarManagerInternal statusBar = mInjector.getStatusBarManagerInternal(); if (statusBar == null) { return; } @@ -2427,10 +2426,9 @@ public class DisplayModeDirector { public void observe() { mDisplayManager = mContext.getSystemService(DisplayManager.class); - mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); + mDisplayManagerInternal = mInjector.getDisplayManagerInternal(); - final SensorManagerInternal sensorManager = - LocalServices.getService(SensorManagerInternal.class); + final SensorManagerInternal sensorManager = mInjector.getSensorManagerInternal(); sensorManager.addProximityActiveListener(BackgroundThread.getExecutor(), this); synchronized (mSensorObserverLock) { @@ -2547,7 +2545,7 @@ public class DisplayModeDirector { synchronized (mLock) { setupHdrRefreshRates(mDefaultDisplayDeviceConfig); } - mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); + mDisplayManagerInternal = mInjector.getDisplayManagerInternal(); mInjector.registerDisplayListener(this, mHandler, DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS | DisplayManager.EVENT_FLAG_DISPLAY_REMOVED); @@ -2788,6 +2786,12 @@ public class DisplayModeDirector { boolean registerThermalServiceListener(IThermalEventListener listener); boolean supportsFrameRateOverride(); + + DisplayManagerInternal getDisplayManagerInternal(); + + StatusBarManagerInternal getStatusBarManagerInternal(); + + SensorManagerInternal getSensorManagerInternal(); } @VisibleForTesting @@ -2885,6 +2889,21 @@ public class DisplayModeDirector { return SurfaceFlingerProperties.enable_frame_rate_override().orElse(true); } + @Override + public DisplayManagerInternal getDisplayManagerInternal() { + return LocalServices.getService(DisplayManagerInternal.class); + } + + @Override + public StatusBarManagerInternal getStatusBarManagerInternal() { + return LocalServices.getService(StatusBarManagerInternal.class); + } + + @Override + public SensorManagerInternal getSensorManagerInternal() { + return LocalServices.getService(SensorManagerInternal.class); + } + private DisplayManager getDisplayManager() { if (mDisplayManager == null) { mDisplayManager = mContext.getSystemService(DisplayManager.class); diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index e78addaf36c4..6241ebbc60cc 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -116,6 +116,7 @@ import com.android.server.DisplayThread; import com.android.server.LocalServices; import com.android.server.Watchdog; import com.android.server.input.InputManagerInternal.LidSwitchCallback; +import com.android.server.inputmethod.InputMethodManagerInternal; import com.android.server.policy.WindowManagerPolicy; import libcore.io.IoUtils; @@ -165,6 +166,8 @@ public class InputManagerService extends IInputManager.Stub private final InputManagerHandler mHandler; private DisplayManagerInternal mDisplayManagerInternal; + private InputMethodManagerInternal mInputMethodManagerInternal; + // Context cache used for loading pointer resources. private Context mPointerIconDisplayContext; @@ -510,6 +513,8 @@ public class InputManagerService extends IInputManager.Stub } mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class); + mInputMethodManagerInternal = + LocalServices.getService(InputMethodManagerInternal.class); mSettingsObserver.registerAndUpdate(); @@ -2725,11 +2730,12 @@ public class InputManagerService extends IInputManager.Stub // Native callback. @SuppressWarnings("unused") - private String[] getKeyboardLayoutOverlay(InputDeviceIdentifier identifier) { + private String[] getKeyboardLayoutOverlay(InputDeviceIdentifier identifier, String languageTag, + String layoutType) { if (!mSystemReady) { return null; } - return mKeyboardLayoutManager.getKeyboardLayoutOverlay(identifier); + return mKeyboardLayoutManager.getKeyboardLayoutOverlay(identifier, languageTag, layoutType); } @EnforcePermission(Manifest.permission.REMAP_MODIFIER_KEYS) @@ -2783,6 +2789,13 @@ public class InputManagerService extends IInputManager.Stub yPosition)).sendToTarget(); } + // Native callback. + @SuppressWarnings("unused") + boolean isInputMethodConnectionActive() { + return mInputMethodManagerInternal != null + && mInputMethodManagerInternal.isAnyInputConnectionActive(); + } + /** * Callback interface implemented by the Window Manager. */ diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java index 7a8de341854d..5bdf26307d11 100644 --- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java +++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java @@ -25,6 +25,7 @@ import android.annotation.AnyThread; import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.app.Notification; import android.app.NotificationManager; @@ -109,7 +110,6 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { private static final int MSG_SWITCH_KEYBOARD_LAYOUT = 2; private static final int MSG_RELOAD_KEYBOARD_LAYOUTS = 3; private static final int MSG_UPDATE_KEYBOARD_LAYOUTS = 4; - private static final int MSG_CURRENT_IME_INFO_CHANGED = 5; private final Context mContext; private final NativeInputManagerService mNative; @@ -186,6 +186,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) { return; } + final KeyboardIdentifier keyboardIdentifier = new KeyboardIdentifier(inputDevice); KeyboardConfiguration config = mConfiguredKeyboards.get(deviceId); if (config == null) { config = new KeyboardConfiguration(); @@ -202,8 +203,6 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { setCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier(), layout); } } - config.setCurrentLayout( - new KeyboardLayoutInfo(layout, LAYOUT_SELECTION_CRITERIA_USER)); if (layout == null) { // In old settings show notification always until user manually selects a // layout in the settings. @@ -211,16 +210,14 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { } } } else { - final InputDeviceIdentifier identifier = inputDevice.getIdentifier(); - final String key = getLayoutDescriptor(identifier); Set<String> selectedLayouts = new HashSet<>(); List<ImeInfo> imeInfoList = getImeInfoListForLayoutMapping(); List<KeyboardLayoutInfo> layoutInfoList = new ArrayList<>(); boolean hasMissingLayout = false; for (ImeInfo imeInfo : imeInfoList) { // Check if the layout has been previously configured - KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal(identifier, - imeInfo); + KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal( + keyboardIdentifier, imeInfo); boolean noLayoutFound = layoutInfo == null || layoutInfo.mDescriptor == null; if (!noLayoutFound) { selectedLayouts.add(layoutInfo.mDescriptor); @@ -231,8 +228,8 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { if (DEBUG) { Slog.d(TAG, - "Layouts selected for input device: " + identifier + " -> selectedLayouts: " - + selectedLayouts); + "Layouts selected for input device: " + keyboardIdentifier + + " -> selectedLayouts: " + selectedLayouts); } // If even one layout not configured properly, we need to ask user to configure @@ -243,18 +240,9 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { config.setConfiguredLayouts(selectedLayouts); - // Update current layout: If there is a change then need to reload. - synchronized (mImeInfoLock) { - KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal( - inputDevice.getIdentifier(), mCurrentImeInfo); - if (!Objects.equals(layoutInfo, config.getCurrentLayout())) { - config.setCurrentLayout(layoutInfo); - mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS); - } - } - synchronized (mDataStore) { try { + final String key = keyboardIdentifier.toString(); boolean isFirstConfiguration = !mDataStore.hasInputDeviceEntry(key); if (mDataStore.setSelectedKeyboardLayouts(key, selectedLayouts)) { // Need to show the notification only if layout selection changed @@ -563,35 +551,6 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { return LocaleList.forLanguageTags(languageTags.replace('|', ',')); } - private String getLayoutDescriptor(@NonNull InputDeviceIdentifier identifier) { - Objects.requireNonNull(identifier, "identifier must not be null"); - Objects.requireNonNull(identifier.getDescriptor(), "descriptor must not be null"); - - if (identifier.getVendorId() == 0 && identifier.getProductId() == 0) { - return identifier.getDescriptor(); - } - // If vendor id and product id is available, use it as keys. This allows us to have the - // same setup for all keyboards with same product and vendor id. i.e. User can swap 2 - // identical keyboards and still get the same setup. - StringBuilder key = new StringBuilder(); - key.append("vendor:").append(identifier.getVendorId()).append(",product:").append( - identifier.getProductId()); - - if (useNewSettingsUi()) { - InputDevice inputDevice = getInputDevice(identifier); - Objects.requireNonNull(inputDevice, "Input device must not be null"); - // Some keyboards can have same product ID and vendor ID but different Keyboard info - // like language tag and layout type. - if (!TextUtils.isEmpty(inputDevice.getKeyboardLanguageTag())) { - key.append(",languageTag:").append(inputDevice.getKeyboardLanguageTag()); - } - if (!TextUtils.isEmpty(inputDevice.getKeyboardLayoutType())) { - key.append(",layoutType:").append(inputDevice.getKeyboardLayoutType()); - } - } - return key.toString(); - } - @AnyThread @Nullable public String getCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier) { @@ -599,7 +558,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { Slog.e(TAG, "getCurrentKeyboardLayoutForInputDevice API not supported"); return null; } - String key = getLayoutDescriptor(identifier); + String key = new KeyboardIdentifier(identifier).toString(); synchronized (mDataStore) { String layout; // try loading it using the layout descriptor if we have it @@ -626,7 +585,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { Objects.requireNonNull(keyboardLayoutDescriptor, "keyboardLayoutDescriptor must not be null"); - String key = getLayoutDescriptor(identifier); + String key = new KeyboardIdentifier(identifier).toString(); synchronized (mDataStore) { try { if (mDataStore.setCurrentKeyboardLayout(key, keyboardLayoutDescriptor)) { @@ -649,7 +608,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { Slog.e(TAG, "getEnabledKeyboardLayoutsForInputDevice API not supported"); return new String[0]; } - String key = getLayoutDescriptor(identifier); + String key = new KeyboardIdentifier(identifier).toString(); synchronized (mDataStore) { String[] layouts = mDataStore.getKeyboardLayouts(key); if ((layouts == null || layouts.length == 0) @@ -670,7 +629,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { Objects.requireNonNull(keyboardLayoutDescriptor, "keyboardLayoutDescriptor must not be null"); - String key = getLayoutDescriptor(identifier); + String key = new KeyboardIdentifier(identifier).toString(); synchronized (mDataStore) { try { String oldLayout = mDataStore.getCurrentKeyboardLayout(key); @@ -698,7 +657,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { Objects.requireNonNull(keyboardLayoutDescriptor, "keyboardLayoutDescriptor must not be null"); - String key = getLayoutDescriptor(identifier); + String key = new KeyboardIdentifier(identifier).toString(); synchronized (mDataStore) { try { String oldLayout = mDataStore.getCurrentKeyboardLayout(key); @@ -737,7 +696,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { final boolean changed; final String keyboardLayoutDescriptor; - String key = getLayoutDescriptor(device.getIdentifier()); + String key = new KeyboardIdentifier(device.getIdentifier()).toString(); synchronized (mDataStore) { try { changed = mDataStore.switchKeyboardLayout(key, direction); @@ -769,11 +728,13 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { @Nullable @AnyThread - public String[] getKeyboardLayoutOverlay(InputDeviceIdentifier identifier) { + public String[] getKeyboardLayoutOverlay(InputDeviceIdentifier identifier, String languageTag, + String layoutType) { String keyboardLayoutDescriptor; if (useNewSettingsUi()) { synchronized (mImeInfoLock) { - KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal(identifier, + KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal( + new KeyboardIdentifier(identifier, languageTag, layoutType), mCurrentImeInfo); keyboardLayoutDescriptor = layoutInfo == null ? null : layoutInfo.mDescriptor; } @@ -811,12 +772,16 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { Slog.e(TAG, "getKeyboardLayoutForInputDevice() API not supported"); return null; } - InputMethodSubtypeHandle subtypeHandle = InputMethodSubtypeHandle.of(imeInfo, imeSubtype); - KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal(identifier, - new ImeInfo(userId, subtypeHandle, imeSubtype)); + InputDevice inputDevice = getInputDevice(identifier); + if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) { + return null; + } + KeyboardIdentifier keyboardIdentifier = new KeyboardIdentifier(inputDevice); + KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal( + keyboardIdentifier, new ImeInfo(userId, imeInfo, imeSubtype)); if (DEBUG) { Slog.d(TAG, "getKeyboardLayoutForInputDevice() " + identifier.toString() + ", userId : " - + userId + ", subtypeHandle = " + subtypeHandle + " -> " + layoutInfo); + + userId + ", subtype = " + imeSubtype + " -> " + layoutInfo); } return layoutInfo != null ? layoutInfo.mDescriptor : null; } @@ -832,16 +797,20 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { } Objects.requireNonNull(keyboardLayoutDescriptor, "keyboardLayoutDescriptor must not be null"); - String key = createLayoutKey(identifier, - new ImeInfo(userId, InputMethodSubtypeHandle.of(imeInfo, imeSubtype), imeSubtype)); + InputDevice inputDevice = getInputDevice(identifier); + if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) { + return; + } + KeyboardIdentifier keyboardIdentifier = new KeyboardIdentifier(inputDevice); + String layoutKey = new LayoutKey(keyboardIdentifier, + new ImeInfo(userId, imeInfo, imeSubtype)).toString(); synchronized (mDataStore) { try { - // Key for storing into data store = <device descriptor>,<userId>,<subtypeHandle> - if (mDataStore.setKeyboardLayout(getLayoutDescriptor(identifier), key, + if (mDataStore.setKeyboardLayout(keyboardIdentifier.toString(), layoutKey, keyboardLayoutDescriptor)) { if (DEBUG) { Slog.d(TAG, "setKeyboardLayoutForInputDevice() " + identifier - + " key: " + key + + " key: " + layoutKey + " keyboardLayoutDescriptor: " + keyboardLayoutDescriptor); } mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS); @@ -860,18 +829,23 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { Slog.e(TAG, "getKeyboardLayoutListForInputDevice() API not supported"); return new KeyboardLayout[0]; } - return getKeyboardLayoutListForInputDeviceInternal(identifier, new ImeInfo(userId, - InputMethodSubtypeHandle.of(imeInfo, imeSubtype), imeSubtype)); + InputDevice inputDevice = getInputDevice(identifier); + if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) { + return new KeyboardLayout[0]; + } + return getKeyboardLayoutListForInputDeviceInternal(new KeyboardIdentifier(inputDevice), + new ImeInfo(userId, imeInfo, imeSubtype)); } private KeyboardLayout[] getKeyboardLayoutListForInputDeviceInternal( - InputDeviceIdentifier identifier, @Nullable ImeInfo imeInfo) { - String key = createLayoutKey(identifier, imeInfo); + KeyboardIdentifier keyboardIdentifier, @Nullable ImeInfo imeInfo) { + String layoutKey = new LayoutKey(keyboardIdentifier, imeInfo).toString(); // Fetch user selected layout and always include it in layout list. String userSelectedLayout; synchronized (mDataStore) { - userSelectedLayout = mDataStore.getKeyboardLayout(getLayoutDescriptor(identifier), key); + userSelectedLayout = mDataStore.getKeyboardLayout(keyboardIdentifier.toString(), + layoutKey); } final ArrayList<KeyboardLayout> potentialLayouts = new ArrayList<>(); @@ -894,8 +868,8 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { // devices that have special layouts we assume there's a reason that the generic // layouts don't work for them, so we don't want to return them since it's likely // to result in a poor user experience. - if (layout.getVendorId() == identifier.getVendorId() - && layout.getProductId() == identifier.getProductId()) { + if (layout.getVendorId() == keyboardIdentifier.mIdentifier.getVendorId() + && layout.getProductId() == keyboardIdentifier.mIdentifier.getProductId()) { if (!mDeviceSpecificLayoutAvailable) { mDeviceSpecificLayoutAvailable = true; potentialLayouts.clear(); @@ -934,7 +908,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { if (mCurrentImeInfo == null || !subtypeHandle.equals(mCurrentImeInfo.mImeSubtypeHandle) || mCurrentImeInfo.mUserId != userId) { mCurrentImeInfo = new ImeInfo(userId, subtypeHandle, subtype); - mHandler.sendEmptyMessage(MSG_CURRENT_IME_INFO_CHANGED); + mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS); if (DEBUG) { Slog.d(TAG, "InputMethodSubtype changed: userId=" + userId + " subtypeHandle=" + subtypeHandle); @@ -943,34 +917,12 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { } } - @MainThread - private void onCurrentImeInfoChanged() { - synchronized (mImeInfoLock) { - for (int i = 0; i < mConfiguredKeyboards.size(); i++) { - InputDevice inputDevice = Objects.requireNonNull( - getInputDevice(mConfiguredKeyboards.keyAt(i))); - KeyboardLayoutInfo layoutInfo = getKeyboardLayoutForInputDeviceInternal( - inputDevice.getIdentifier(), mCurrentImeInfo); - KeyboardConfiguration config = mConfiguredKeyboards.valueAt(i); - if (!Objects.equals(layoutInfo, config.getCurrentLayout())) { - config.setCurrentLayout(layoutInfo); - mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS); - return; - } - } - } - } - @Nullable private KeyboardLayoutInfo getKeyboardLayoutForInputDeviceInternal( - InputDeviceIdentifier identifier, @Nullable ImeInfo imeInfo) { - InputDevice inputDevice = getInputDevice(identifier); - if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) { - return null; - } - String key = createLayoutKey(identifier, imeInfo); + KeyboardIdentifier keyboardIdentifier, @Nullable ImeInfo imeInfo) { + String layoutKey = new LayoutKey(keyboardIdentifier, imeInfo).toString(); synchronized (mDataStore) { - String layout = mDataStore.getKeyboardLayout(getLayoutDescriptor(identifier), key); + String layout = mDataStore.getKeyboardLayout(keyboardIdentifier.toString(), layoutKey); if (layout != null) { return new KeyboardLayoutInfo(layout, LAYOUT_SELECTION_CRITERIA_USER); } @@ -978,16 +930,17 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { synchronized (mKeyboardLayoutCache) { // Check Auto-selected layout cache to see if layout had been previously selected - if (mKeyboardLayoutCache.containsKey(key)) { - return mKeyboardLayoutCache.get(key); + if (mKeyboardLayoutCache.containsKey(layoutKey)) { + return mKeyboardLayoutCache.get(layoutKey); } else { // NOTE: This list is already filtered based on IME Script code KeyboardLayout[] layoutList = getKeyboardLayoutListForInputDeviceInternal( - identifier, imeInfo); + keyboardIdentifier, imeInfo); // Call auto-matching algorithm to find the best matching layout KeyboardLayoutInfo layoutInfo = - getDefaultKeyboardLayoutBasedOnImeInfo(inputDevice, imeInfo, layoutList); - mKeyboardLayoutCache.put(key, layoutInfo); + getDefaultKeyboardLayoutBasedOnImeInfo(keyboardIdentifier, imeInfo, + layoutList); + mKeyboardLayoutCache.put(layoutKey, layoutInfo); return layoutInfo; } } @@ -995,17 +948,18 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { @Nullable private static KeyboardLayoutInfo getDefaultKeyboardLayoutBasedOnImeInfo( - InputDevice inputDevice, @Nullable ImeInfo imeInfo, KeyboardLayout[] layoutList) { + KeyboardIdentifier keyboardIdentifier, @Nullable ImeInfo imeInfo, + KeyboardLayout[] layoutList) { Arrays.sort(layoutList); // Check <VendorID, ProductID> matching for explicitly declared custom KCM files. for (KeyboardLayout layout : layoutList) { - if (layout.getVendorId() == inputDevice.getVendorId() - && layout.getProductId() == inputDevice.getProductId()) { + if (layout.getVendorId() == keyboardIdentifier.mIdentifier.getVendorId() + && layout.getProductId() == keyboardIdentifier.mIdentifier.getProductId()) { if (DEBUG) { Slog.d(TAG, "getDefaultKeyboardLayoutBasedOnImeInfo() : Layout found based on " - + "vendor and product Ids. " + inputDevice.getIdentifier() + + "vendor and product Ids. " + keyboardIdentifier + " : " + layout.getDescriptor()); } return new KeyboardLayoutInfo(layout.getDescriptor(), @@ -1014,17 +968,17 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { } // Check layout type, language tag information from InputDevice for matching - String inputLanguageTag = inputDevice.getKeyboardLanguageTag(); + String inputLanguageTag = keyboardIdentifier.mLanguageTag; if (inputLanguageTag != null) { String layoutDesc = getMatchingLayoutForProvidedLanguageTagAndLayoutType(layoutList, - inputLanguageTag, inputDevice.getKeyboardLayoutType()); + inputLanguageTag, keyboardIdentifier.mLayoutType); if (layoutDesc != null) { if (DEBUG) { Slog.d(TAG, "getDefaultKeyboardLayoutBasedOnImeInfo() : Layout found based on " + "HW information (Language tag and Layout type). " - + inputDevice.getIdentifier() + " : " + layoutDesc); + + keyboardIdentifier + " : " + layoutDesc); } return new KeyboardLayoutInfo(layoutDesc, LAYOUT_SELECTION_CRITERIA_DEVICE); } @@ -1045,7 +999,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { if (DEBUG) { Slog.d(TAG, "getDefaultKeyboardLayoutBasedOnImeInfo() : Layout found based on " - + "IME locale matching. " + inputDevice.getIdentifier() + " : " + + "IME locale matching. " + keyboardIdentifier + " : " + layoutDesc); } if (layoutDesc != null) { @@ -1105,7 +1059,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { } } return layoutMatchingLanguageAndCountry != null - ? layoutMatchingLanguageAndCountry : layoutMatchingLanguage; + ? layoutMatchingLanguageAndCountry : layoutMatchingLanguage; } private void reloadKeyboardLayouts() { @@ -1297,9 +1251,6 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { case MSG_UPDATE_KEYBOARD_LAYOUTS: updateKeyboardLayouts(); return true; - case MSG_CURRENT_IME_INFO_CHANGED: - onCurrentImeInfoChanged(); - return true; default: return false; } @@ -1322,6 +1273,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { identifier.getDescriptor()) : null; } + @SuppressLint("MissingPermission") private List<ImeInfo> getImeInfoListForLayoutMapping() { List<ImeInfo> imeInfoList = new ArrayList<>(); UserManager userManager = Objects.requireNonNull( @@ -1337,31 +1289,20 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { int userId = userHandle.getIdentifier(); for (InputMethodInfo imeInfo : inputMethodManagerInternal.getEnabledInputMethodListAsUser( - userId)) { + userId)) { for (InputMethodSubtype imeSubtype : inputMethodManager.getEnabledInputMethodSubtypeList( imeInfo, true /* allowsImplicitlyEnabledSubtypes */)) { if (!imeSubtype.isSuitableForPhysicalKeyboardLayoutMapping()) { continue; } - imeInfoList.add( - new ImeInfo(userId, InputMethodSubtypeHandle.of(imeInfo, imeSubtype), - imeSubtype)); + imeInfoList.add(new ImeInfo(userId, imeInfo, imeSubtype)); } } } return imeInfoList; } - private String createLayoutKey(InputDeviceIdentifier identifier, @Nullable ImeInfo imeInfo) { - if (imeInfo == null) { - return getLayoutDescriptor(identifier); - } - Objects.requireNonNull(imeInfo.mImeSubtypeHandle, "subtypeHandle must not be null"); - return "layoutDescriptor:" + getLayoutDescriptor(identifier) + ",userId:" + imeInfo.mUserId - + ",subtypeHandle:" + imeInfo.mImeSubtypeHandle.toStringHandle(); - } - private static boolean isLayoutCompatibleWithLanguageTag(KeyboardLayout layout, @NonNull String languageTag) { LocaleList layoutLocales = layout.getLocales(); @@ -1451,6 +1392,11 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { mImeSubtypeHandle = imeSubtypeHandle; mImeSubtype = imeSubtype; } + + ImeInfo(@UserIdInt int userId, @NonNull InputMethodInfo imeInfo, + @Nullable InputMethodSubtype imeSubtype) { + this(userId, InputMethodSubtypeHandle.of(imeInfo, imeSubtype), imeSubtype); + } } private static class KeyboardConfiguration { @@ -1459,10 +1405,6 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { @Nullable private Set<String> mConfiguredLayouts; - // If null, it means no layout is selected for the device. - @Nullable - private KeyboardLayoutInfo mCurrentLayout; - private boolean hasConfiguredLayouts() { return mConfiguredLayouts != null && !mConfiguredLayouts.isEmpty(); } @@ -1475,15 +1417,6 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { private void setConfiguredLayouts(Set<String> configuredLayouts) { mConfiguredLayouts = configuredLayouts; } - - @Nullable - private KeyboardLayoutInfo getCurrentLayout() { - return mCurrentLayout; - } - - private void setCurrentLayout(KeyboardLayoutInfo currentLayout) { - mCurrentLayout = currentLayout; - } } private static class KeyboardLayoutInfo { @@ -1517,4 +1450,88 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener { void visitKeyboardLayout(Resources resources, int keyboardLayoutResId, KeyboardLayout layout); } + + private static class KeyboardIdentifier { + @NonNull + private final InputDeviceIdentifier mIdentifier; + @Nullable + private final String mLanguageTag; + @Nullable + private final String mLayoutType; + + // NOTE: Use this only for old settings UI where we don't use language tag and layout + // type to determine the KCM file. + private KeyboardIdentifier(@NonNull InputDeviceIdentifier inputDeviceIdentifier) { + this(inputDeviceIdentifier, null, null); + } + + private KeyboardIdentifier(@NonNull InputDevice inputDevice) { + this(inputDevice.getIdentifier(), inputDevice.getKeyboardLanguageTag(), + inputDevice.getKeyboardLayoutType()); + } + + private KeyboardIdentifier(@NonNull InputDeviceIdentifier identifier, + @Nullable String languageTag, @Nullable String layoutType) { + Objects.requireNonNull(identifier, "identifier must not be null"); + Objects.requireNonNull(identifier.getDescriptor(), "descriptor must not be null"); + mIdentifier = identifier; + mLanguageTag = languageTag; + mLayoutType = layoutType; + } + + @Override + public int hashCode() { + return Objects.hashCode(toString()); + } + + @Override + public String toString() { + if (mIdentifier.getVendorId() == 0 && mIdentifier.getProductId() == 0) { + return mIdentifier.getDescriptor(); + } + // If vendor id and product id is available, use it as keys. This allows us to have the + // same setup for all keyboards with same product and vendor id. i.e. User can swap 2 + // identical keyboards and still get the same setup. + StringBuilder key = new StringBuilder(); + key.append("vendor:").append(mIdentifier.getVendorId()).append(",product:").append( + mIdentifier.getProductId()); + + // Some keyboards can have same product ID and vendor ID but different Keyboard info + // like language tag and layout type. + if (!TextUtils.isEmpty(mLanguageTag)) { + key.append(",languageTag:").append(mLanguageTag); + } + if (!TextUtils.isEmpty(mLayoutType)) { + key.append(",layoutType:").append(mLayoutType); + } + return key.toString(); + } + } + + private static class LayoutKey { + + private final KeyboardIdentifier mKeyboardIdentifier; + @Nullable + private final ImeInfo mImeInfo; + + private LayoutKey(KeyboardIdentifier keyboardIdentifier, @Nullable ImeInfo imeInfo) { + mKeyboardIdentifier = keyboardIdentifier; + mImeInfo = imeInfo; + } + + @Override + public int hashCode() { + return Objects.hashCode(toString()); + } + + @Override + public String toString() { + if (mImeInfo == null) { + return mKeyboardIdentifier.toString(); + } + Objects.requireNonNull(mImeInfo.mImeSubtypeHandle, "subtypeHandle must not be null"); + return "layoutDescriptor:" + mKeyboardIdentifier + ",userId:" + mImeInfo.mUserId + + ",subtypeHandle:" + mImeInfo.mImeSubtypeHandle.toStringHandle(); + } + } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java index 8c7658e53dcd..08503cb2e9f8 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerInternal.java @@ -186,6 +186,12 @@ public abstract class InputMethodManagerInternal { public abstract void switchKeyboardLayout(int direction); /** + * Returns true if any InputConnection is currently active. + * {@hide} + */ + public abstract boolean isAnyInputConnectionActive(); + + /** * Fake implementation of {@link InputMethodManagerInternal}. All the methods do nothing. */ private static final InputMethodManagerInternal NOP = @@ -268,6 +274,11 @@ public abstract class InputMethodManagerInternal { @Override public void switchKeyboardLayout(int direction) { } + + @Override + public boolean isAnyInputConnectionActive() { + return false; + } }; /** diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index c5fbcb968ab6..cfcb4620bf25 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -5937,6 +5937,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } } } + + /** + * Returns true if any InputConnection is currently active. + */ + @Override + public boolean isAnyInputConnectionActive() { + return mCurInputConnection != null; + } } @BinderThread diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index c649164562d3..9ce6f8fec4b2 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -117,6 +117,10 @@ public final class MediaProjectionManagerService extends SystemService // WindowManagerService -> MediaProjectionManagerService -> DisplayManagerService // See mediaprojection.md private final Object mLock = new Object(); + // A handler for posting tasks that must interact with a service holding another lock, + // especially for services that will eventually acquire the WindowManager lock. + @NonNull private final Handler mHandler; + private final Map<IBinder, IBinder.DeathRecipient> mDeathEaters; private final CallbackDelegate mCallbackDelegate; @@ -145,6 +149,8 @@ public final class MediaProjectionManagerService extends SystemService super(context); mContext = context; mInjector = injector; + // Post messages on the main thread; no need for a separate thread. + mHandler = new Handler(Looper.getMainLooper()); mClock = injector.createClock(); mDeathEaters = new ArrayMap<IBinder, IBinder.DeathRecipient>(); mCallbackDelegate = new CallbackDelegate(injector.createCallbackLooper()); @@ -243,14 +249,17 @@ public final class MediaProjectionManagerService extends SystemService if (!mProjectionGrant.requiresForegroundService()) { return; } + } - if (mActivityManagerInternal.hasRunningForegroundService( - uid, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)) { - // If there is any process within this UID running a FGS - // with the mediaProjection type, that's Okay. - return; - } + // Run outside the lock when calling into ActivityManagerService. + if (mActivityManagerInternal.hasRunningForegroundService( + uid, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)) { + // If there is any process within this UID running a FGS + // with the mediaProjection type, that's Okay. + return; + } + synchronized (mLock) { mProjectionGrant.stop(); } } @@ -849,7 +858,6 @@ public final class MediaProjectionManagerService extends SystemService mTargetSdkVersion = targetSdkVersion; mIsPrivileged = isPrivileged; mCreateTimeMs = mClock.uptimeMillis(); - // TODO(b/267740338): Add unit test. mActivityManagerInternal.notifyMediaProjectionEvent(uid, asBinder(), MEDIA_PROJECTION_TOKEN_EVENT_CREATED); } @@ -903,6 +911,10 @@ public final class MediaProjectionManagerService extends SystemService if (callback == null) { throw new IllegalArgumentException("callback must not be null"); } + // Cache result of calling into ActivityManagerService outside of the lock, to prevent + // deadlock with WindowManagerService. + final boolean hasFGS = mActivityManagerInternal.hasRunningForegroundService( + uid, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION); synchronized (mLock) { if (isCurrentProjection(asBinder())) { Slog.w(TAG, "UID " + Binder.getCallingUid() @@ -914,9 +926,7 @@ public final class MediaProjectionManagerService extends SystemService } if (REQUIRE_FG_SERVICE_FOR_PROJECTION - && requiresForegroundService() - && !mActivityManagerInternal.hasRunningForegroundService( - uid, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)) { + && requiresForegroundService() && !hasFGS) { throw new SecurityException("Media projections require a foreground service" + " of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION"); } @@ -1005,10 +1015,11 @@ public final class MediaProjectionManagerService extends SystemService mToken = null; unregisterCallback(mCallback); mCallback = null; - // TODO(b/267740338): Add unit test. - mActivityManagerInternal.notifyMediaProjectionEvent(uid, asBinder(), - MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED); } + // Run on a separate thread, to ensure no lock is held when calling into + // ActivityManagerService. + mHandler.post(() -> mActivityManagerInternal.notifyMediaProjectionEvent(uid, asBinder(), + MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED)); } @Override // Binder call diff --git a/services/core/java/com/android/server/media/projection/mediaprojection.md b/services/core/java/com/android/server/media/projection/mediaprojection.md index bccdf3411903..34e7ecc6c6c5 100644 --- a/services/core/java/com/android/server/media/projection/mediaprojection.md +++ b/services/core/java/com/android/server/media/projection/mediaprojection.md @@ -11,6 +11,11 @@ Calls must follow the below invocation order while holding locks: `WindowManagerService -> MediaProjectionManagerService -> DisplayManagerService` +`MediaProjectionManagerService` should never lock when calling into a service that may acquire +the `WindowManagerService` global lock (for example, +`MediaProjectionManagerService -> ActivityManagerService` may result in deadlock, since +`ActivityManagerService -> WindowManagerService`). + ### Justification `MediaProjectionManagerService` calls into `WindowManagerService` in the below cases. While handling diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index e84be035be41..7452aabaa91a 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -2491,6 +2491,16 @@ public class NotificationManagerService extends SystemService { getContext().registerReceiver(mReviewNotificationPermissionsReceiver, ReviewNotificationPermissionsReceiver.getFilter(), Context.RECEIVER_NOT_EXPORTED); + + mAppOps.startWatchingMode(AppOpsManager.OP_POST_NOTIFICATION, null, + new AppOpsManager.OnOpChangedInternalListener() { + @Override + public void onOpChanged(@NonNull String op, @NonNull String packageName, + int userId) { + mHandler.post( + () -> handleNotificationPermissionChange(packageName, userId)); + } + }); } /** @@ -3281,6 +3291,11 @@ public class NotificationManagerService extends SystemService { return new MultiRateLimiter.Builder(getContext()).addRateLimits(TOAST_RATE_LIMITS).build(); } + protected int checkComponentPermission(String permission, int uid, int owningUid, + boolean exported) { + return ActivityManager.checkComponentPermission(permission, uid, owningUid, exported); + } + @VisibleForTesting final IBinder mService = new INotificationManager.Stub() { // Toasts @@ -3557,13 +3572,9 @@ public class NotificationManagerService extends SystemService { .setPackageName(pkg) .setSubtype(enabled ? 1 : 0)); mNotificationChannelLogger.logAppNotificationsAllowed(uid, pkg, enabled); - // Now, cancel any outstanding notifications that are part of a just-disabled app - if (!enabled) { - cancelAllNotificationsInt(MY_UID, MY_PID, pkg, null, 0, 0, - UserHandle.getUserId(uid), REASON_PACKAGE_BANNED); - } - handleSavePolicyFile(); + // Outstanding notifications from this package will be cancelled as soon as we get the + // callback from AppOpsManager. } /** @@ -5235,10 +5246,11 @@ public class NotificationManagerService extends SystemService { } private boolean checkPolicyAccess(String pkg) { + final int uid; try { - int uid = getContext().getPackageManager().getPackageUidAsUser(pkg, + uid = getContext().getPackageManager().getPackageUidAsUser(pkg, UserHandle.getCallingUserId()); - if (PackageManager.PERMISSION_GRANTED == ActivityManager.checkComponentPermission( + if (PackageManager.PERMISSION_GRANTED == checkComponentPermission( android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true)) { return true; @@ -5249,8 +5261,8 @@ public class NotificationManagerService extends SystemService { //TODO(b/169395065) Figure out if this flow makes sense in Device Owner mode. return checkPackagePolicyAccess(pkg) || mListeners.isComponentEnabledForPackage(pkg) - || (mDpm != null && (mDpm.isActiveProfileOwner(Binder.getCallingUid()) - || mDpm.isActiveDeviceOwner(Binder.getCallingUid()))); + || (mDpm != null && (mDpm.isActiveProfileOwner(uid) + || mDpm.isActiveDeviceOwner(uid))); } @Override @@ -5883,6 +5895,23 @@ public class NotificationManagerService extends SystemService { } }; + private void handleNotificationPermissionChange(String pkg, @UserIdInt int userId) { + if (!mUmInternal.isUserInitialized(userId)) { + return; // App-op "updates" are sent when starting a new user the first time. + } + int uid = mPackageManagerInternal.getPackageUid(pkg, 0, userId); + if (uid == INVALID_UID) { + Log.e(TAG, String.format("No uid found for %s, %s!", pkg, userId)); + return; + } + boolean hasPermission = mPermissionHelper.hasPermission(uid); + if (!hasPermission) { + cancelAllNotificationsInt(MY_UID, MY_PID, pkg, /* channelId= */ null, + /* mustHaveFlags= */ 0, /* mustNotHaveFlags= */ 0, userId, + REASON_PACKAGE_BANNED); + } + } + protected void checkNotificationListenerAccess() { if (!isCallerSystemOrPhone()) { getContext().enforceCallingPermission( diff --git a/services/core/java/com/android/server/pm/permission/TEST_MAPPING b/services/core/java/com/android/server/pm/permission/TEST_MAPPING index 579d4e3562b4..b2dcf379fe7d 100644 --- a/services/core/java/com/android/server/pm/permission/TEST_MAPPING +++ b/services/core/java/com/android/server/pm/permission/TEST_MAPPING @@ -4,6 +4,9 @@ "name": "CtsPermissionTestCases", "options": [ { + "exclude-annotation": "android.platform.test.annotations.FlakyTest" + }, + { "include-filter": "android.permission.cts.BackgroundPermissionsTest" }, { @@ -29,6 +32,9 @@ "name": "CtsPermissionPolicyTestCases", "options": [ { + "exclude-annotation": "android.platform.test.annotations.FlakyTest" + }, + { "include-filter": "android.permissionpolicy.cts.RestrictedPermissionsTest" }, { @@ -59,6 +65,29 @@ "options": [ { "include-filter": "android.permission.cts.PermissionUpdateListenerTest" + }, + { + "include-filter": "android.permission.cts.BackgroundPermissionsTest" + }, + { + "include-filter": "android.permission.cts.SplitPermissionTest" + }, + { + "include-filter": "android.permission.cts.PermissionFlagsTest" + }, + { + "include-filter": "android.permission.cts.SharedUidPermissionsTest" + } + ] + }, + { + "name": "CtsPermissionPolicyTestCases", + "options": [ + { + "include-filter": "android.permissionpolicy.cts.RestrictedPermissionsTest" + }, + { + "include-filter": "android.permission.cts.PermissionMaxSdkVersionTest" } ] } diff --git a/services/core/java/com/android/server/pm/split/DefaultSplitAssetLoader.java b/services/core/java/com/android/server/pm/split/DefaultSplitAssetLoader.java index a2177e87bcdb..0bb969f488fe 100644 --- a/services/core/java/com/android/server/pm/split/DefaultSplitAssetLoader.java +++ b/services/core/java/com/android/server/pm/split/DefaultSplitAssetLoader.java @@ -17,13 +17,13 @@ package com.android.server.pm.split; import android.content.pm.parsing.ApkLiteParseUtils; import android.content.pm.parsing.PackageLite; -import com.android.server.pm.pkg.parsing.ParsingPackageUtils; -import com.android.server.pm.pkg.parsing.ParsingPackageUtils.ParseFlags; import android.content.res.ApkAssets; import android.content.res.AssetManager; import android.os.Build; import com.android.internal.util.ArrayUtils; +import com.android.server.pm.pkg.parsing.ParsingPackageUtils; +import com.android.server.pm.pkg.parsing.ParsingPackageUtils.ParseFlags; import libcore.io.IoUtils; @@ -82,8 +82,8 @@ public class DefaultSplitAssetLoader implements SplitAssetLoader { } AssetManager assets = new AssetManager(); - assets.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - Build.VERSION.RESOURCES_SDK_INT); + assets.setConfiguration(0, 0, null, new String[0], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, Build.VERSION.RESOURCES_SDK_INT); assets.setApkAssets(apkAssets, false /*invalidateCaches*/); mCachedAssetManager = assets; diff --git a/services/core/java/com/android/server/pm/split/SplitAssetDependencyLoader.java b/services/core/java/com/android/server/pm/split/SplitAssetDependencyLoader.java index 1a8c199608df..56d92fbc95a2 100644 --- a/services/core/java/com/android/server/pm/split/SplitAssetDependencyLoader.java +++ b/services/core/java/com/android/server/pm/split/SplitAssetDependencyLoader.java @@ -80,8 +80,8 @@ public class SplitAssetDependencyLoader extends SplitDependencyLoader<IllegalArg private static AssetManager createAssetManagerWithAssets(ApkAssets[] apkAssets) { final AssetManager assets = new AssetManager(); - assets.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - Build.VERSION.RESOURCES_SDK_INT); + assets.setConfiguration(0, 0, null, new String[0], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, Build.VERSION.RESOURCES_SDK_INT); assets.setApkAssets(apkAssets, false /*invalidateCaches*/); return assets; } diff --git a/services/core/java/com/android/server/policy/TEST_MAPPING b/services/core/java/com/android/server/policy/TEST_MAPPING index 9f1cb1a451c6..819a82c24bfe 100644 --- a/services/core/java/com/android/server/policy/TEST_MAPPING +++ b/services/core/java/com/android/server/policy/TEST_MAPPING @@ -32,6 +32,9 @@ "name": "CtsPermissionPolicyTestCases", "options": [ { + "exclude-annotation": "android.platform.test.annotations.FlakyTest" + }, + { "include-filter": "android.permissionpolicy.cts.RestrictedPermissionsTest" }, { @@ -46,6 +49,9 @@ "name": "CtsPermissionTestCases", "options": [ { + "exclude-annotation": "android.platform.test.annotations.FlakyTest" + }, + { "include-filter": "android.permission.cts.SplitPermissionTest" }, { @@ -78,6 +84,31 @@ "include-filter": "com.android.server.policy." } ] + }, + { + "name": "CtsPermissionPolicyTestCases", + "options": [ + { + "include-filter": "android.permissionpolicy.cts.RestrictedPermissionsTest" + }, + { + "include-filter": "android.permissionpolicy.cts.RestrictedStoragePermissionSharedUidTest" + }, + { + "include-filter": "android.permissionpolicy.cts.RestrictedStoragePermissionTest" + } + ] + }, + { + "name": "CtsPermissionTestCases", + "options": [ + { + "include-filter": "android.permission.cts.SplitPermissionTest" + }, + { + "include-filter": "android.permission.cts.BackgroundPermissionsTest" + } + ] } ] } diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 5a050ac33639..75fcca5eec9a 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -5583,7 +5583,7 @@ public final class PowerManagerService extends SystemService private void recordReferenceLocked(String id) { LongArray times = mOpenReferenceTimes.get(id); if (times == null) { - times = new LongArray(); + times = new LongArray(2); mOpenReferenceTimes.put(id, times); } times.add(System.currentTimeMillis()); @@ -5593,6 +5593,9 @@ public final class PowerManagerService extends SystemService LongArray times = mOpenReferenceTimes.get(id); if (times != null && times.size() > 0) { times.remove(times.size() - 1); + if (times.size() == 0) { + mOpenReferenceTimes.remove(id); + } } } } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java index efd8b6d9a943..a5123311d499 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java @@ -140,6 +140,20 @@ public interface StatusBarManagerInternal { boolean showShutdownUi(boolean isReboot, String requestString); /** + * Notify system UI the immersive prompt should be dismissed as confirmed, and the confirmed + * status should be saved without user clicking on the button. This could happen when a user + * swipe on the edge with the confirmation prompt showing. + */ + void confirmImmersivePrompt(); + + /** + * Notify System UI that the system get into or exit immersive mode. + * @param rootDisplayAreaId The changed display area Id. + * @param isImmersiveMode {@code true} if the display area get into immersive mode. + */ + void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode); + + /** * Show a rotation suggestion that a user may approve to rotate the screen. * * @param rotation rotation suggestion diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index cc849b6fbf91..40e9c1305f01 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -27,6 +27,7 @@ import static android.app.StatusBarManager.NavBarMode; import static android.app.StatusBarManager.SessionFlags; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.ViewRootImpl.CLIENT_TRANSIENT; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY; import android.Manifest; @@ -638,6 +639,31 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D return false; } + @Override + public void confirmImmersivePrompt() { + if (mBar == null) { + return; + } + try { + mBar.confirmImmersivePrompt(); + } catch (RemoteException ex) { + } + } + + @Override + public void immersiveModeChanged(int rootDisplayAreaId, boolean isImmersiveMode) { + if (mBar == null) { + return; + } + if (!CLIENT_TRANSIENT) { + // Only call from here when the client transient is not enabled. + try { + mBar.immersiveModeChanged(rootDisplayAreaId, isImmersiveMode); + } catch (RemoteException ex) { + } + } + } + // TODO(b/118592525): support it per display if necessary. @Override public void onProposedRotationChanged(int rotation, boolean isValid) { diff --git a/services/core/java/com/android/server/uri/TEST_MAPPING b/services/core/java/com/android/server/uri/TEST_MAPPING index 6c36af5d3e01..b42d154e04a7 100644 --- a/services/core/java/com/android/server/uri/TEST_MAPPING +++ b/services/core/java/com/android/server/uri/TEST_MAPPING @@ -39,10 +39,10 @@ ] }, { - "name": "CtsWindowManagerDeviceTestCases", + "name": "CtsWindowManagerDeviceWindow", "options": [ { - "include-filter": "android.server.wm.CrossAppDragAndDropTests" + "include-filter": "android.server.wm.window.CrossAppDragAndDropTests" } ] } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index b55af76e3799..ddc05194c300 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -2935,6 +2935,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub checkPermission(android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT); final long ident = Binder.clearCallingIdentity(); try { + List<WallpaperData> pendingColorExtraction = new ArrayList<>(); synchronized (mLock) { WallpaperData wallpaper = mWallpaperMap.get(mCurrentUserId); WallpaperData lockWallpaper = mLockWallpaperMap.get(mCurrentUserId); @@ -2970,7 +2971,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub // Need to extract colors again to re-calculate dark hints after // applying dimming. wp.mIsColorExtractedFromDim = true; - notifyWallpaperColorsChanged(wp, wp.mWhich); + pendingColorExtraction.add(wp); changed = true; } } @@ -3002,6 +3003,9 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } } } + for (WallpaperData wp: pendingColorExtraction) { + notifyWallpaperColorsChanged(wp, wp.mWhich); + } } finally { Binder.restoreCallingIdentity(ident); } diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 0a62c881e251..4113094f0cc3 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -2042,6 +2042,13 @@ class ActivityStarter { } // ASM rules have failed. Log why + return logAsmFailureAndCheckFeatureEnabled(r, newTask, targetTask, shouldBlockActivityStart, + taskToFront); + } + + private boolean logAsmFailureAndCheckFeatureEnabled(ActivityRecord r, boolean newTask, + Task targetTask, boolean shouldBlockActivityStart, boolean taskToFront) { + // ASM rules have failed. Log why ActivityRecord targetTopActivity = targetTask == null ? null : targetTask.getActivity(ar -> !ar.finishing && !ar.isAlwaysOnTop()); @@ -2051,6 +2058,13 @@ class ActivityStarter { ? FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__ACTIVITY_START_SAME_TASK : FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED__ACTION__ACTIVITY_START_DIFFERENT_TASK); + boolean blockActivityStartAndFeatureEnabled = ActivitySecurityModelFeatureFlags + .shouldRestrictActivitySwitch(mCallingUid) + && shouldBlockActivityStart; + + String asmDebugInfo = getDebugInfoForActivitySecurity("Launch", r, targetTask, + targetTopActivity, blockActivityStartAndFeatureEnabled, /*taskToFront*/taskToFront); + FrameworkStatsLog.write(FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED, /* caller_uid */ mSourceRecord != null ? mSourceRecord.getUid() : mCallingUid, @@ -2079,24 +2093,21 @@ class ActivityStarter { targetTask != null && mSourceRecord != null && !targetTask.equals(mSourceRecord.getTask()) && targetTask.isVisible(), /* bal_code */ - mBalCode + mBalCode, + /* task_stack */ + asmDebugInfo ); - boolean blockActivityStartAndFeatureEnabled = ActivitySecurityModelFeatureFlags - .shouldRestrictActivitySwitch(mCallingUid) - && shouldBlockActivityStart; - String launchedFromPackageName = r.launchedFromPackage; if (ActivitySecurityModelFeatureFlags.shouldShowToast(mCallingUid)) { String toastText = ActivitySecurityModelFeatureFlags.DOC_LINK + (blockActivityStartAndFeatureEnabled ? " blocked " : " would block ") + getApplicationLabel(mService.mContext.getPackageManager(), - launchedFromPackageName); + launchedFromPackageName); UiThread.getHandler().post(() -> Toast.makeText(mService.mContext, toastText, Toast.LENGTH_LONG).show()); - logDebugInfoForActivitySecurity("Launch", r, targetTask, targetTopActivity, - blockActivityStartAndFeatureEnabled, /* taskToFront */ taskToFront); + Slog.i(TAG, asmDebugInfo); } if (blockActivityStartAndFeatureEnabled) { @@ -2114,7 +2125,7 @@ class ActivityStarter { } /** Only called when an activity launch may be blocked, which should happen very rarely */ - private void logDebugInfoForActivitySecurity(String action, ActivityRecord r, Task targetTask, + private String getDebugInfoForActivitySecurity(String action, ActivityRecord r, Task targetTask, ActivityRecord targetTopActivity, boolean blockActivityStartAndFeatureEnabled, boolean taskToFront) { final String prefix = "[ASM] "; @@ -2175,7 +2186,7 @@ class ActivityStarter { joiner.add(prefix + "BalCode: " + balCodeToString(mBalCode)); joiner.add(prefix + "------ Activity Security " + action + " Debug Logging End ------"); - Slog.i(TAG, joiner.toString()); + return joiner.toString(); } /** @@ -2349,7 +2360,7 @@ class ActivityStarter { + ActivitySecurityModelFeatureFlags.DOC_LINK, Toast.LENGTH_LONG).show()); - logDebugInfoForActivitySecurity("Clear Top", mStartActivity, targetTask, targetTaskTop, + getDebugInfoForActivitySecurity("Clear Top", mStartActivity, targetTask, targetTaskTop, shouldBlockActivityStart, /* taskToFront */ true); } } @@ -3110,7 +3121,18 @@ class ActivityStarter { } else { TaskFragment candidateTf = mAddingToTaskFragment != null ? mAddingToTaskFragment : null; if (candidateTf == null) { - final ActivityRecord top = task.topRunningActivity(false /* focusableOnly */); + // Puts the activity on the top-most non-isolated navigation TF, unless the + // activity is launched from the same TF. + final TaskFragment sourceTaskFragment = + mSourceRecord != null ? mSourceRecord.getTaskFragment() : null; + final ActivityRecord top = task.getActivity(r -> { + if (!r.canBeTopRunning()) { + return false; + } + final TaskFragment taskFragment = r.getTaskFragment(); + return !taskFragment.isIsolatedNav() || (sourceTaskFragment != null + && sourceTaskFragment == taskFragment); + }); if (top != null) { candidateTf = top.getTaskFragment(); } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index aeaf78327d57..2c866abe614a 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -311,6 +311,7 @@ import java.util.Set; * {@hide} */ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { + private static final String GRAMMATICAL_GENDER_PROPERTY = "persist.sys.grammatical_gender"; private static final String TAG = TAG_WITH_CLASS_NAME ? "ActivityTaskManagerService" : TAG_ATM; static final String TAG_ROOT_TASK = TAG + POSTFIX_ROOT_TASK; static final String TAG_SWITCH = TAG + POSTFIX_SWITCH; @@ -928,6 +929,14 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { configuration.setLayoutDirection(configuration.locale); } + // Retrieve the grammatical gender from system property, set it into configuration which + // will get updated later if the grammatical gender raw value of current configuration is + // {@link Configuration#GRAMMATICAL_GENDER_UNDEFINED}. + if (configuration.getGrammaticalGenderRaw() == Configuration.GRAMMATICAL_GENDER_UNDEFINED) { + configuration.setGrammaticalGender(SystemProperties.getInt(GRAMMATICAL_GENDER_PROPERTY, + Configuration.GRAMMATICAL_GENDER_UNDEFINED)); + } + synchronized (mGlobalLock) { mForceResizableActivities = forceResizable; mDevEnableNonResizableMultiWindow = devEnableNonResizableMultiWindow; diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 5553600b403f..cc130c407690 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -1752,7 +1752,9 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { /* multi_window */ false, /* bal_code */ - -1 + -1, + /* task_stack */ + null ); boolean restrictActivitySwitch = ActivitySecurityModelFeatureFlags diff --git a/services/core/java/com/android/server/wm/Dimmer.java b/services/core/java/com/android/server/wm/Dimmer.java index 89f044bdd163..d7667d8ce7a8 100644 --- a/services/core/java/com/android/server/wm/Dimmer.java +++ b/services/core/java/com/android/server/wm/Dimmer.java @@ -215,8 +215,7 @@ class Dimmer { return mDimState; } - private void dim(SurfaceControl.Transaction t, WindowContainer container, int relativeLayer, - float alpha, int blurRadius) { + private void dim(WindowContainer container, int relativeLayer, float alpha, int blurRadius) { final DimState d = getDimState(container); if (d == null) { @@ -226,6 +225,7 @@ class Dimmer { // The dim method is called from WindowState.prepareSurfaces(), which is always called // in the correct Z from lowest Z to highest. This ensures that the dim layer is always // relative to the highest Z layer with a dim. + SurfaceControl.Transaction t = mHost.getPendingTransaction(); t.setRelativeLayer(d.mDimLayer, container.getSurfaceControl(), relativeLayer); t.setAlpha(d.mDimLayer, alpha); t.setBackgroundBlurRadius(d.mDimLayer, blurRadius); @@ -238,26 +238,23 @@ class Dimmer { * for each call to {@link WindowContainer#prepareSurfaces} the Dim state will be reset * and the child should call dimAbove again to request the Dim to continue. * - * @param t A transaction in which to apply the Dim. * @param container The container which to dim above. Should be a child of our host. * @param alpha The alpha at which to Dim. */ - void dimAbove(SurfaceControl.Transaction t, WindowContainer container, float alpha) { - dim(t, container, 1, alpha, 0); + void dimAbove(WindowContainer container, float alpha) { + dim(container, 1, alpha, 0); } /** * Like {@link #dimAbove} but places the dim below the given container. * - * @param t A transaction in which to apply the Dim. * @param container The container which to dim below. Should be a child of our host. * @param alpha The alpha at which to Dim. * @param blurRadius The amount of blur added to the Dim. */ - void dimBelow(SurfaceControl.Transaction t, WindowContainer container, float alpha, - int blurRadius) { - dim(t, container, -1, alpha, blurRadius); + void dimBelow(WindowContainer container, float alpha, int blurRadius) { + dim(container, -1, alpha, blurRadius); } /** diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index d675753dbbdf..a29aeff935d6 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -1934,7 +1934,7 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } else if (mFixedRotationLaunchingApp != null && r == null) { mWmService.mDisplayNotificationController.dispatchFixedRotationFinished(this); // Keep async rotation controller if the next transition of display is requested. - if (!mTransitionController.isCollecting(this)) { + if (!mTransitionController.hasCollectingRotationChange(this, getRotation())) { finishAsyncRotationIfPossible(); } } @@ -5422,8 +5422,10 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp // Attach the SystemUiContext to this DisplayContent the get latest configuration. // Note that the SystemUiContext will be removed automatically if this DisplayContent // is detached. + final WindowProcessController wpc = mAtmService.getProcessController( + getDisplayUiContext().getIApplicationThread()); mWmService.mWindowContextListenerController.registerWindowContainerListener( - getDisplayUiContext().getWindowContextToken(), this, SYSTEM_UID, + wpc, getDisplayUiContext().getWindowContextToken(), this, SYSTEM_UID, INVALID_WINDOW_TYPE, null /* options */); } } diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index ab89b8748e2f..1871cf6175c5 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -22,6 +22,7 @@ import static android.view.InsetsFrameProvider.SOURCE_ARBITRARY_RECTANGLE; import static android.view.InsetsFrameProvider.SOURCE_CONTAINER_BOUNDS; import static android.view.InsetsFrameProvider.SOURCE_DISPLAY; import static android.view.InsetsFrameProvider.SOURCE_FRAME; +import static android.view.ViewRootImpl.CLIENT_IMMERSIVE_CONFIRMATION; import static android.view.ViewRootImpl.CLIENT_TRANSIENT; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; @@ -38,6 +39,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACK import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DRAW_BAR_BACKGROUNDS; +import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INTERCEPT_GLOBAL_DRAG_AND_DROP; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; @@ -194,6 +196,8 @@ public class DisplayPolicy { private final ScreenshotHelper mScreenshotHelper; private final Object mServiceAcquireLock = new Object(); + private long mPanicTime; + private final long mPanicThresholdMs; private StatusBarManagerInternal mStatusBarManagerInternal; @Px @@ -246,6 +250,8 @@ public class DisplayPolicy { private volatile boolean mKeyguardDrawComplete; private volatile boolean mWindowManagerDrawComplete; + private boolean mImmersiveConfirmationWindowExists; + private WindowState mStatusBar = null; private volatile WindowState mNotificationShade; private WindowState mNavigationBar = null; @@ -402,6 +408,7 @@ public class DisplayPolicy { mCanSystemBarsBeShownByUser = !r.getBoolean( R.bool.config_remoteInsetsControllerControlsSystemBars) || r.getBoolean( R.bool.config_remoteInsetsControllerSystemBarsCanBeShownByUserAction); + mPanicThresholdMs = r.getInteger(R.integer.config_immersive_mode_confirmation_panic); mAccessibilityManager = (AccessibilityManager) mContext.getSystemService( Context.ACCESSIBILITY_SERVICE); @@ -623,8 +630,12 @@ public class DisplayPolicy { }; displayContent.mAppTransition.registerListenerLocked(mAppTransitionListener); displayContent.mTransitionController.registerLegacyListener(mAppTransitionListener); - mImmersiveModeConfirmation = new ImmersiveModeConfirmation(mContext, looper, - mService.mVrModeEnabled, mCanSystemBarsBeShownByUser); + if (CLIENT_TRANSIENT || CLIENT_IMMERSIVE_CONFIRMATION) { + mImmersiveModeConfirmation = null; + } else { + mImmersiveModeConfirmation = new ImmersiveModeConfirmation(mContext, looper, + mService.mVrModeEnabled, mCanSystemBarsBeShownByUser); + } // TODO: Make it can take screenshot on external display mScreenshotHelper = displayContent.isDefaultDisplay @@ -1075,6 +1086,9 @@ public class DisplayPolicy { mNavigationBar = win; break; } + if ((attrs.privateFlags & PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW) != 0) { + mImmersiveConfirmationWindowExists = true; + } if (attrs.providedInsets != null) { for (int i = attrs.providedInsets.length - 1; i >= 0; i--) { final InsetsFrameProvider provider = attrs.providedInsets[i]; @@ -1234,6 +1248,9 @@ public class DisplayPolicy { } } mInsetsSourceWindowsExceptIme.remove(win); + if ((win.mAttrs.privateFlags & PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW) != 0) { + mImmersiveConfirmationWindowExists = false; + } } WindowState getStatusBar() { @@ -2171,7 +2188,11 @@ public class DisplayPolicy { } } } - mImmersiveModeConfirmation.confirmCurrentPrompt(); + if (CLIENT_IMMERSIVE_CONFIRMATION || CLIENT_TRANSIENT) { + mStatusBarManagerInternal.confirmImmersivePrompt(); + } else { + mImmersiveModeConfirmation.confirmCurrentPrompt(); + } } boolean isKeyguardShowing() { @@ -2221,7 +2242,8 @@ public class DisplayPolicy { // Immersive mode confirmation should never affect the system bar visibility, otherwise // it will unhide the navigation bar and hide itself. - if (winCandidate.getAttrs().token == mImmersiveModeConfirmation.getWindowToken()) { + if ((winCandidate.getAttrs().privateFlags + & PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW) != 0) { if (mNotificationShade != null && mNotificationShade.canReceiveKeys()) { // Let notification shade control the system bar visibility. winCandidate = mNotificationShade; @@ -2389,9 +2411,16 @@ public class DisplayPolicy { // The immersive confirmation window should be attached to the immersive window root. final RootDisplayArea root = win.getRootDisplayArea(); final int rootDisplayAreaId = root == null ? FEATURE_UNDEFINED : root.mFeatureId; - mImmersiveModeConfirmation.immersiveModeChangedLw(rootDisplayAreaId, isImmersiveMode, - mService.mPolicy.isUserSetupComplete(), - isNavBarEmpty(disableFlags)); + if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) { + mImmersiveModeConfirmation.immersiveModeChangedLw(rootDisplayAreaId, + isImmersiveMode, + mService.mPolicy.isUserSetupComplete(), + isNavBarEmpty(disableFlags)); + } else { + // TODO (b/277290737): Move this to the client side, instead of using a proxy. + callStatusBarSafely(statusBar -> statusBar.immersiveModeChanged(rootDisplayAreaId, + isImmersiveMode)); + } } // Show transient bars for panic if needed. @@ -2604,16 +2633,39 @@ public class DisplayPolicy { void onPowerKeyDown(boolean isScreenOn) { // Detect user pressing the power button in panic when an application has // taken over the whole screen. - boolean panic = mImmersiveModeConfirmation.onPowerKeyDown(isScreenOn, - SystemClock.elapsedRealtime(), isImmersiveMode(mSystemUiControllingWindow), - isNavBarEmpty(mLastDisableFlags)); + boolean panic = false; + if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) { + panic = mImmersiveModeConfirmation.onPowerKeyDown(isScreenOn, + SystemClock.elapsedRealtime(), isImmersiveMode(mSystemUiControllingWindow), + isNavBarEmpty(mLastDisableFlags)); + } else { + panic = isPowerKeyDownPanic(isScreenOn, SystemClock.elapsedRealtime(), + isImmersiveMode(mSystemUiControllingWindow), isNavBarEmpty(mLastDisableFlags)); + } if (panic) { mHandler.post(mHiddenNavPanic); } } + private boolean isPowerKeyDownPanic(boolean isScreenOn, long time, boolean inImmersiveMode, + boolean navBarEmpty) { + if (!isScreenOn && (time - mPanicTime < mPanicThresholdMs)) { + // turning the screen back on within the panic threshold + return !mImmersiveConfirmationWindowExists; + } + if (isScreenOn && inImmersiveMode && !navBarEmpty) { + // turning the screen off, remember if we were in immersive mode + mPanicTime = time; + } else { + mPanicTime = 0; + } + return false; + } + void onVrStateChangedLw(boolean enabled) { - mImmersiveModeConfirmation.onVrStateChangedLw(enabled); + if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) { + mImmersiveModeConfirmation.onVrStateChangedLw(enabled); + } } /** @@ -2626,7 +2678,9 @@ public class DisplayPolicy { * {@link ActivityManager#LOCK_TASK_MODE_PINNED}. */ public void onLockTaskStateChangedLw(int lockTaskState) { - mImmersiveModeConfirmation.onLockTaskModeChangedLw(lockTaskState); + if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) { + mImmersiveModeConfirmation.onLockTaskModeChangedLw(lockTaskState); + } } /** Called when a {@link android.os.PowerManager#USER_ACTIVITY_EVENT_TOUCH} is sent. */ @@ -2643,7 +2697,11 @@ public class DisplayPolicy { } boolean onSystemUiSettingsChanged() { - return mImmersiveModeConfirmation.onSettingChanged(mService.mCurrentUserId); + if (CLIENT_TRANSIENT || CLIENT_IMMERSIVE_CONFIRMATION) { + return false; + } else { + return mImmersiveModeConfirmation.onSettingChanged(mService.mCurrentUserId); + } } /** @@ -2857,7 +2915,9 @@ public class DisplayPolicy { mDisplayContent.mTransitionController.unregisterLegacyListener(mAppTransitionListener); mHandler.post(mGestureNavigationSettingsObserver::unregister); mHandler.post(mForceShowNavBarSettingsObserver::unregister); - mImmersiveModeConfirmation.release(); + if (!CLIENT_TRANSIENT && !CLIENT_IMMERSIVE_CONFIRMATION) { + mImmersiveModeConfirmation.release(); + } if (mService.mPointerLocationEnabled) { setPointerLocationEnabled(false); } diff --git a/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java b/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java index 271d71ed0ec0..b2ba9d1cc8fe 100644 --- a/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java +++ b/services/core/java/com/android/server/wm/ImmersiveModeConfirmation.java @@ -19,6 +19,7 @@ package com.android.server.wm; import static android.app.ActivityManager.LOCK_TASK_MODE_LOCKED; import static android.app.ActivityManager.LOCK_TASK_MODE_NONE; import static android.view.Display.DEFAULT_DISPLAY; +import static android.view.ViewRootImpl.CLIENT_TRANSIENT; import static android.window.DisplayAreaOrganizer.FEATURE_UNDEFINED; import static android.window.DisplayAreaOrganizer.KEY_ROOT_DISPLAY_AREA_ID; @@ -234,7 +235,8 @@ public class ImmersiveModeConfirmation { lp.setFitInsetsTypes(lp.getFitInsetsTypes() & ~Type.statusBars()); // Trusted overlay so touches outside the touchable area are allowed to pass through lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS - | WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; + | WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY + | WindowManager.LayoutParams.PRIVATE_FLAG_IMMERSIVE_CONFIRMATION_WINDOW; lp.setTitle("ImmersiveModeConfirmation"); lp.windowAnimations = com.android.internal.R.style.Animation_ImmersiveModeConfirmation; lp.token = getWindowToken(); @@ -475,6 +477,9 @@ public class ImmersiveModeConfirmation { @Override public void handleMessage(Message msg) { + if (CLIENT_TRANSIENT) { + return; + } switch(msg.what) { case SHOW: handleShow(msg.arg1); diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 92c0987d5636..57ce368aae87 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -329,6 +329,15 @@ class TaskFragment extends WindowContainer<WindowContainer> { */ private boolean mDelayLastActivityRemoval; + /** + * Whether the activity navigation should be isolated. That is, Activities cannot be launched + * on an isolated TaskFragment, unless the activity is launched from an Activity in the same + * isolated TaskFragment, or explicitly requested to be launched to. + * <p> + * Note that only an embedded TaskFragment can be isolated. + */ + private boolean mIsolatedNav; + final Point mLastSurfaceSize = new Point(); private final Rect mTmpBounds = new Rect(); @@ -481,6 +490,19 @@ class TaskFragment extends WindowContainer<WindowContainer> { return mAnimationParams; } + /** @see #mIsolatedNav */ + void setIsolatedNav(boolean isolatedNav) { + if (!isEmbedded()) { + return; + } + mIsolatedNav = isolatedNav; + } + + /** @see #mIsolatedNav */ + boolean isIsolatedNav() { + return isEmbedded() && mIsolatedNav; + } + TaskFragment getAdjacentTaskFragment() { return mAdjacentTaskFragment; } @@ -3034,7 +3056,8 @@ class TaskFragment extends WindowContainer<WindowContainer> { @Override void dump(PrintWriter pw, String prefix, boolean dumpAll) { super.dump(pw, prefix, dumpAll); - pw.println(prefix + "bounds=" + getBounds().toShortString()); + pw.println(prefix + "bounds=" + getBounds().toShortString() + + (mIsolatedNav ? ", isolatedNav" : "")); final String doublePrefix = prefix + " "; for (int i = mChildren.size() - 1; i >= 0; i--) { final WindowContainer<?> child = mChildren.get(i); diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index dfaa17494855..ae05725f81ff 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -578,6 +578,17 @@ class TransitionController { } /** + * Returns {@code true} if the window container is in the collecting transition, and its + * collected rotation is different from the target rotation. + */ + boolean hasCollectingRotationChange(@NonNull WindowContainer<?> wc, int targetRotation) { + final Transition transition = mCollectingTransition; + if (transition == null || !transition.mParticipants.contains(wc)) return false; + final Transition.ChangeInfo changeInfo = transition.mChanges.get(wc); + return changeInfo != null && changeInfo.mRotation != targetRotation; + } + + /** * @see #requestTransitionIfNeeded(int, int, WindowContainer, WindowContainer, RemoteTransition) */ @Nullable diff --git a/services/core/java/com/android/server/wm/WindowContextListenerController.java b/services/core/java/com/android/server/wm/WindowContextListenerController.java index 4025cbf77756..a59c23007b93 100644 --- a/services/core/java/com/android/server/wm/WindowContextListenerController.java +++ b/services/core/java/com/android/server/wm/WindowContextListenerController.java @@ -69,22 +69,24 @@ class WindowContextListenerController { final ArrayMap<IBinder, WindowContextListenerImpl> mListeners = new ArrayMap<>(); /** - * @see #registerWindowContainerListener(IBinder, WindowContainer, int, int, Bundle, boolean) + * @see #registerWindowContainerListener(WindowProcessController, IBinder, WindowContainer, int, + * int, Bundle, boolean) */ - void registerWindowContainerListener(@NonNull IBinder clientToken, - @NonNull WindowContainer<?> container, int ownerUid, @WindowType int type, - @Nullable Bundle options) { - registerWindowContainerListener(clientToken, container, ownerUid, type, options, + void registerWindowContainerListener(@NonNull WindowProcessController wpc, + @NonNull IBinder clientToken, @NonNull WindowContainer<?> container, int ownerUid, + @WindowType int type, @Nullable Bundle options) { + registerWindowContainerListener(wpc, clientToken, container, ownerUid, type, options, true /* shouDispatchConfigWhenRegistering */); } /** * Registers the listener to a {@code container} which is associated with - * a {@code clientToken}, which is a {@link android.window.WindowContext} representation. If the + * a {@code clientToken}, which is a {@link WindowContext} representation. If the * listener associated with {@code clientToken} hasn't been initialized yet, create one * {@link WindowContextListenerImpl}. Otherwise, the listener associated with * {@code clientToken} switches to listen to the {@code container}. * + * @param wpc the process that we should send the window configuration change to * @param clientToken the token to associate with the listener * @param container the {@link WindowContainer} which the listener is going to listen to. * @param ownerUid the caller UID @@ -94,19 +96,32 @@ class WindowContextListenerController { * {@code container}'s config will dispatch to the client side when * registering the {@link WindowContextListenerImpl} */ - void registerWindowContainerListener(@NonNull IBinder clientToken, - @NonNull WindowContainer<?> container, int ownerUid, @WindowType int type, - @Nullable Bundle options, boolean shouDispatchConfigWhenRegistering) { + void registerWindowContainerListener(@NonNull WindowProcessController wpc, + @NonNull IBinder clientToken, @NonNull WindowContainer<?> container, int ownerUid, + @WindowType int type, @Nullable Bundle options, + boolean shouDispatchConfigWhenRegistering) { WindowContextListenerImpl listener = mListeners.get(clientToken); if (listener == null) { - listener = new WindowContextListenerImpl(clientToken, container, ownerUid, type, + listener = new WindowContextListenerImpl(wpc, clientToken, container, ownerUid, type, options); listener.register(shouDispatchConfigWhenRegistering); } else { - listener.updateContainer(container); + updateContainerForWindowContextListener(clientToken, container); } } + /** + * Updates the {@link WindowContainer} that an existing {@link WindowContext} is listening to. + */ + void updateContainerForWindowContextListener(@NonNull IBinder clientToken, + @NonNull WindowContainer<?> container) { + final WindowContextListenerImpl listener = mListeners.get(clientToken); + if (listener == null) { + throw new IllegalArgumentException("Can't find listener for " + clientToken); + } + listener.updateContainer(container); + } + void unregisterWindowContainerListener(IBinder clientToken) { final WindowContextListenerImpl listener = mListeners.get(clientToken); // Listeners may be removed earlier. An example is the display where the listener is @@ -189,9 +204,13 @@ class WindowContextListenerController { @VisibleForTesting class WindowContextListenerImpl implements WindowContainerListener { - @NonNull private final IWindowToken mClientToken; + @NonNull + private final WindowProcessController mWpc; + @NonNull + private final IWindowToken mClientToken; private final int mOwnerUid; - @NonNull private WindowContainer<?> mContainer; + @NonNull + private WindowContainer<?> mContainer; /** * The options from {@link Context#createWindowContext(int, Bundle)}. * <p>It can be used for choosing the {@link DisplayArea} where the window context @@ -207,8 +226,10 @@ class WindowContextListenerController { private boolean mHasPendingConfiguration; - private WindowContextListenerImpl(IBinder clientToken, WindowContainer<?> container, + private WindowContextListenerImpl(@NonNull WindowProcessController wpc, + @NonNull IBinder clientToken, @NonNull WindowContainer<?> container, int ownerUid, @WindowType int type, @Nullable Bundle options) { + mWpc = Objects.requireNonNull(wpc); mClientToken = IWindowToken.Stub.asInterface(clientToken); mContainer = Objects.requireNonNull(container); mOwnerUid = ownerUid; @@ -308,6 +329,7 @@ class WindowContextListenerController { mLastReportedDisplay = displayId; try { + // TODO(b/290876897): migrate to dispatch through wpc mClientToken.onConfigurationChanged(config, displayId); } catch (RemoteException e) { ProtoLog.w(WM_ERROR, "Could not report config changes to the window token client."); @@ -337,6 +359,7 @@ class WindowContextListenerController { } mDeathRecipient.unlinkToDeath(); try { + // TODO(b/290876897): migrate to dispatch through wpc mClientToken.onWindowTokenRemoved(); } catch (RemoteException e) { ProtoLog.w(WM_ERROR, "Could not report token removal to the window token client."); diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 15b15a85a689..2a28010c81a3 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -1698,8 +1698,8 @@ public class WindowManagerService extends IWindowManager.Stub return WindowManagerGlobal.ADD_INVALID_TYPE; } } else { - mWindowContextListenerController.registerWindowContainerListener( - windowContextToken, token, callingUid, type, options); + mWindowContextListenerController.updateContainerForWindowContextListener( + windowContextToken, token); } } @@ -2744,10 +2744,17 @@ public class WindowManagerService extends IWindowManager.Stub Objects.requireNonNull(clientToken); final boolean callerCanManageAppTokens = checkCallingPermission(MANAGE_APP_TOKENS, "attachWindowContextToDisplayArea", false /* printLog */); + final int callingPid = Binder.getCallingPid(); final int callingUid = Binder.getCallingUid(); final long origId = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { + final WindowProcessController wpc = mAtmService.getProcessController(appThread); + if (wpc == null) { + ProtoLog.w(WM_ERROR, "attachWindowContextToDisplayArea: calling from" + + " non-existing process pid=%d uid=%d", callingPid, callingUid); + return null; + } final DisplayContent dc = mRoot.getDisplayContentOrCreate(displayId); if (dc == null) { ProtoLog.w(WM_ERROR, "attachWindowContextToDisplayArea: trying to attach" @@ -2758,8 +2765,9 @@ public class WindowManagerService extends IWindowManager.Stub // the feature b/155340867 is completed. final DisplayArea<?> da = dc.findAreaForWindowType(type, options, callerCanManageAppTokens, false /* roundedCornerOverlay */); - mWindowContextListenerController.registerWindowContainerListener(clientToken, da, - callingUid, type, options, false /* shouDispatchConfigWhenRegistering */); + mWindowContextListenerController.registerWindowContainerListener(wpc, clientToken, + da, callingUid, type, options, + false /* shouDispatchConfigWhenRegistering */); return da.getConfiguration(); } } finally { @@ -2773,10 +2781,17 @@ public class WindowManagerService extends IWindowManager.Stub @NonNull IBinder clientToken, int displayId) { Objects.requireNonNull(appThread); Objects.requireNonNull(clientToken); + final int callingPid = Binder.getCallingPid(); final int callingUid = Binder.getCallingUid(); final long origId = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { + final WindowProcessController wpc = mAtmService.getProcessController(appThread); + if (wpc == null) { + ProtoLog.w(WM_ERROR, "attachWindowContextToDisplayContent: calling from" + + " non-existing process pid=%d uid=%d", callingPid, callingUid); + return null; + } // We use "getDisplayContent" instead of "getDisplayContentOrCreate" because // this method may be called in DisplayPolicy's constructor and may cause // infinite loop. In this scenario, we early return here and switch to do the @@ -2793,8 +2808,8 @@ public class WindowManagerService extends IWindowManager.Stub return null; } - mWindowContextListenerController.registerWindowContainerListener(clientToken, dc, - callingUid, INVALID_WINDOW_TYPE, null /* options */, + mWindowContextListenerController.registerWindowContainerListener(wpc, clientToken, + dc, callingUid, INVALID_WINDOW_TYPE, null /* options */, false /* shouDispatchConfigWhenRegistering */); return dc.getConfiguration(); } @@ -2811,10 +2826,17 @@ public class WindowManagerService extends IWindowManager.Stub Objects.requireNonNull(token); final boolean callerCanManageAppTokens = checkCallingPermission(MANAGE_APP_TOKENS, "attachWindowContextToWindowToken", false /* printLog */); + final int callingPid = Binder.getCallingPid(); final int callingUid = Binder.getCallingUid(); final long origId = Binder.clearCallingIdentity(); try { synchronized (mGlobalLock) { + final WindowProcessController wpc = mAtmService.getProcessController(appThread); + if (wpc == null) { + ProtoLog.w(WM_ERROR, "attachWindowContextToWindowToken: calling from" + + " non-existing process pid=%d uid=%d", callingPid, callingUid); + return; + } final WindowToken windowToken = mRoot.getWindowToken(token); if (windowToken == null) { ProtoLog.w(WM_ERROR, "Then token:%s is invalid. It might be " @@ -2835,7 +2857,7 @@ public class WindowManagerService extends IWindowManager.Stub callerCanManageAppTokens, callingUid)) { return; } - mWindowContextListenerController.registerWindowContainerListener(clientToken, + mWindowContextListenerController.registerWindowContainerListener(wpc, clientToken, windowToken, callingUid, windowToken.windowType, windowToken.mOptions); } } finally { diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 164c8b013c84..a84749aa6643 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -30,6 +30,7 @@ import static android.window.TaskFragmentOperation.OP_TYPE_REQUEST_FOCUS_ON_TASK import static android.window.TaskFragmentOperation.OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; import static android.window.TaskFragmentOperation.OP_TYPE_SET_COMPANION_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION; import static android.window.TaskFragmentOperation.OP_TYPE_SET_RELATIVE_BOUNDS; import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_UNKNOWN; @@ -1356,6 +1357,11 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } break; } + case OP_TYPE_SET_ISOLATED_NAVIGATION: { + final boolean isolatedNav = operation.isIsolatedNav(); + taskFragment.setIsolatedNav(isolatedNav); + break; + } } return effects; } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 9f0c78f4b40d..cad8f5190f72 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -5137,7 +5137,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP private void applyDims() { if (((mAttrs.flags & FLAG_DIM_BEHIND) != 0 || shouldDrawBlurBehind()) - && isVisibleNow() && !mHidden && mTransitionController.canApplyDim(getTask())) { + && mToken.isVisibleRequested() && isVisibleNow() && !mHidden + && mTransitionController.canApplyDim(getTask())) { // Only show the Dimmer when the following is satisfied: // 1. The window has the flag FLAG_DIM_BEHIND or blur behind is requested // 2. The WindowToken is not hidden so dims aren't shown when the window is exiting. @@ -5147,7 +5148,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP mIsDimming = true; final float dimAmount = (mAttrs.flags & FLAG_DIM_BEHIND) != 0 ? mAttrs.dimAmount : 0; final int blurRadius = shouldDrawBlurBehind() ? mAttrs.getBlurBehindRadius() : 0; - getDimmer().dimBelow(getSyncTransaction(), this, dimAmount, blurRadius); + getDimmer().dimBelow(this, dimAmount, blurRadius); } } diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index cb0b9c9ace4f..7c8c558474bb 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -108,6 +108,7 @@ static struct { jmethodID notifySensorEvent; jmethodID notifySensorAccuracy; jmethodID notifyStylusGestureStarted; + jmethodID isInputMethodConnectionActive; jmethodID notifyVibratorState; jmethodID filterInputEvent; jmethodID interceptKeyBeforeQueueing; @@ -313,13 +314,15 @@ public: std::shared_ptr<PointerControllerInterface> obtainPointerController(int32_t deviceId) override; void notifyInputDevicesChanged(const std::vector<InputDeviceInfo>& inputDevices) override; std::shared_ptr<KeyCharacterMap> getKeyboardLayoutOverlay( - const InputDeviceIdentifier& identifier) override; + const InputDeviceIdentifier& identifier, + const std::optional<KeyboardLayoutInfo> keyboardLayoutInfo) override; std::string getDeviceAlias(const InputDeviceIdentifier& identifier) override; TouchAffineTransformation getTouchAffineTransformation(const std::string& inputDeviceDescriptor, ui::Rotation surfaceRotation) override; TouchAffineTransformation getTouchAffineTransformation(JNIEnv* env, jfloatArray matrixArr); void notifyStylusGestureStarted(int32_t deviceId, nsecs_t eventTime) override; + bool isInputMethodConnectionActive() override; /* --- InputDispatcherPolicyInterface implementation --- */ @@ -779,17 +782,32 @@ void NativeInputManager::notifyInputDevicesChanged(const std::vector<InputDevice } std::shared_ptr<KeyCharacterMap> NativeInputManager::getKeyboardLayoutOverlay( - const InputDeviceIdentifier& identifier) { + const InputDeviceIdentifier& identifier, + const std::optional<KeyboardLayoutInfo> keyboardLayoutInfo) { ATRACE_CALL(); JNIEnv* env = jniEnv(); std::shared_ptr<KeyCharacterMap> result; ScopedLocalRef<jstring> descriptor(env, env->NewStringUTF(identifier.descriptor.c_str())); + ScopedLocalRef<jstring> languageTag(env, + keyboardLayoutInfo + ? env->NewStringUTF( + keyboardLayoutInfo->languageTag.c_str()) + : nullptr); + ScopedLocalRef<jstring> layoutType(env, + keyboardLayoutInfo + ? env->NewStringUTF( + keyboardLayoutInfo->layoutType.c_str()) + : nullptr); ScopedLocalRef<jobject> identifierObj(env, env->NewObject(gInputDeviceIdentifierInfo.clazz, gInputDeviceIdentifierInfo.constructor, descriptor.get(), identifier.vendor, identifier.product)); - ScopedLocalRef<jobjectArray> arrayObj(env, jobjectArray(env->CallObjectMethod(mServiceObj, - gServiceClassInfo.getKeyboardLayoutOverlay, identifierObj.get()))); + ScopedLocalRef<jobjectArray> + arrayObj(env, + jobjectArray(env->CallObjectMethod(mServiceObj, + gServiceClassInfo.getKeyboardLayoutOverlay, + identifierObj.get(), languageTag.get(), + layoutType.get()))); if (arrayObj.get()) { ScopedLocalRef<jstring> filenameObj(env, jstring(env->GetObjectArrayElement(arrayObj.get(), 0))); @@ -1291,6 +1309,14 @@ void NativeInputManager::notifyStylusGestureStarted(int32_t deviceId, nsecs_t ev checkAndClearExceptionFromCallback(env, "notifyStylusGestureStarted"); } +bool NativeInputManager::isInputMethodConnectionActive() { + JNIEnv* env = jniEnv(); + const jboolean result = + env->CallBooleanMethod(mServiceObj, gServiceClassInfo.isInputMethodConnectionActive); + checkAndClearExceptionFromCallback(env, "isInputMethodConnectionActive"); + return result; +} + bool NativeInputManager::filterInputEvent(const InputEvent& inputEvent, uint32_t policyFlags) { ATRACE_CALL(); JNIEnv* env = jniEnv(); @@ -2733,6 +2759,9 @@ int register_android_server_InputManager(JNIEnv* env) { GET_METHOD_ID(gServiceClassInfo.notifyStylusGestureStarted, clazz, "notifyStylusGestureStarted", "(IJ)V"); + GET_METHOD_ID(gServiceClassInfo.isInputMethodConnectionActive, clazz, + "isInputMethodConnectionActive", "()Z"); + GET_METHOD_ID(gServiceClassInfo.notifyVibratorState, clazz, "notifyVibratorState", "(IZ)V"); GET_METHOD_ID(gServiceClassInfo.notifyNoFocusedWindowAnr, clazz, "notifyNoFocusedWindowAnr", @@ -2803,9 +2832,9 @@ int register_android_server_InputManager(JNIEnv* env) { GET_METHOD_ID(gServiceClassInfo.getPointerIcon, clazz, "getPointerIcon", "(I)Landroid/view/PointerIcon;"); - GET_METHOD_ID(gServiceClassInfo.getKeyboardLayoutOverlay, clazz, - "getKeyboardLayoutOverlay", - "(Landroid/hardware/input/InputDeviceIdentifier;)[Ljava/lang/String;"); + GET_METHOD_ID(gServiceClassInfo.getKeyboardLayoutOverlay, clazz, "getKeyboardLayoutOverlay", + "(Landroid/hardware/input/InputDeviceIdentifier;Ljava/lang/String;Ljava/lang/" + "String;)[Ljava/lang/String;"); GET_METHOD_ID(gServiceClassInfo.getDeviceAlias, clazz, "getDeviceAlias", "(Ljava/lang/String;)Ljava/lang/String;"); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 77f8de57f3ae..c323a7f4430d 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -241,7 +241,6 @@ import static android.provider.Telephony.Carriers.ENFORCE_KEY; import static android.provider.Telephony.Carriers.ENFORCE_MANAGED_URI; import static android.provider.Telephony.Carriers.INVALID_APN_ID; import static android.security.keystore.AttestationUtils.USE_INDIVIDUAL_ATTESTATION; - import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.PROVISIONING_ENTRY_POINT_ADB; import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW; @@ -540,6 +539,8 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; @@ -18825,10 +18826,16 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { }); } + ThreadPoolExecutor calculateHasIncompatibleAccountsExecutor = new ThreadPoolExecutor( + 1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>()); + @Override public void calculateHasIncompatibleAccounts() { + if (calculateHasIncompatibleAccountsExecutor.getQueue().size() > 1) { + return; + } new CalculateHasIncompatibleAccountsTask().executeOnExecutor( - AsyncTask.THREAD_POOL_EXECUTOR, null); + calculateHasIncompatibleAccountsExecutor, null); } @Nullable diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index ee4bc1237bcb..6a2d4dc333f6 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -2447,8 +2447,9 @@ public final class SystemServer implements Dumpable { t.traceBegin("StartCompanionDeviceManager"); mSystemServiceManager.startService(COMPANION_DEVICE_MANAGER_SERVICE_CLASS); t.traceEnd(); + } - // VirtualDeviceManager depends on CDM to control the associations. + if (context.getResources().getBoolean(R.bool.config_enableVirtualDeviceManager)) { t.traceBegin("StartVirtualDeviceManager"); mSystemServiceManager.startService(VIRTUAL_DEVICE_MANAGER_SERVICE_CLASS); t.traceEnd(); diff --git a/services/midi/java/com/android/server/midi/MidiService.java b/services/midi/java/com/android/server/midi/MidiService.java index dd78f21a54e8..c0cfa53a0a98 100644 --- a/services/midi/java/com/android/server/midi/MidiService.java +++ b/services/midi/java/com/android/server/midi/MidiService.java @@ -1006,7 +1006,7 @@ public class MidiService extends IMidiManager.Stub { for (int i = 0; i < count; i++) { ServiceInfo serviceInfo = resolveInfos.get(i).serviceInfo; if (serviceInfo != null) { - addLegacyPackageDeviceServer(serviceInfo, user.getUserIdentifier()); + addUmpPackageDeviceServer(serviceInfo, user.getUserIdentifier()); } } } diff --git a/services/permission/TEST_MAPPING b/services/permission/TEST_MAPPING index 579d4e3562b4..b2dcf379fe7d 100644 --- a/services/permission/TEST_MAPPING +++ b/services/permission/TEST_MAPPING @@ -4,6 +4,9 @@ "name": "CtsPermissionTestCases", "options": [ { + "exclude-annotation": "android.platform.test.annotations.FlakyTest" + }, + { "include-filter": "android.permission.cts.BackgroundPermissionsTest" }, { @@ -29,6 +32,9 @@ "name": "CtsPermissionPolicyTestCases", "options": [ { + "exclude-annotation": "android.platform.test.annotations.FlakyTest" + }, + { "include-filter": "android.permissionpolicy.cts.RestrictedPermissionsTest" }, { @@ -59,6 +65,29 @@ "options": [ { "include-filter": "android.permission.cts.PermissionUpdateListenerTest" + }, + { + "include-filter": "android.permission.cts.BackgroundPermissionsTest" + }, + { + "include-filter": "android.permission.cts.SplitPermissionTest" + }, + { + "include-filter": "android.permission.cts.PermissionFlagsTest" + }, + { + "include-filter": "android.permission.cts.SharedUidPermissionsTest" + } + ] + }, + { + "name": "CtsPermissionPolicyTestCases", + "options": [ + { + "include-filter": "android.permissionpolicy.cts.RestrictedPermissionsTest" + }, + { + "include-filter": "android.permission.cts.PermissionMaxSdkVersionTest" } ] } diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java index 8a9a851c16b7..4c3d80a6422b 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerController2Test.java @@ -50,6 +50,7 @@ import android.os.IBinder; import android.os.Looper; import android.os.PowerManager; import android.os.SystemProperties; +import android.os.UserHandle; import android.os.test.TestLooper; import android.provider.Settings; import android.testing.TestableContext; @@ -144,11 +145,12 @@ public final class DisplayPowerController2Test { mTestLooper = new TestLooper(mClock::now); mHandler = new Handler(mTestLooper.getLooper()); - // Put the system into manual brightness by default, just to minimize unexpected events and - // have a consistent starting state + // Set some settings to minimize unexpected events and have a consistent starting state Settings.System.putInt(mContext.getContentResolver(), Settings.System.SCREEN_BRIGHTNESS_MODE, Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL); + Settings.System.putFloatForUser(mContext.getContentResolver(), + Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0, UserHandle.USER_CURRENT); addLocalServiceMock(WindowManagerPolicy.class, mWindowManagerPolicyMock); addLocalServiceMock(ColorDisplayService.ColorDisplayServiceInternal.class, diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java index 113c46b86e34..e58ec450dd61 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java @@ -50,6 +50,7 @@ import android.os.IBinder; import android.os.Looper; import android.os.PowerManager; import android.os.SystemProperties; +import android.os.UserHandle; import android.os.test.TestLooper; import android.provider.Settings; import android.testing.TestableContext; @@ -144,12 +145,12 @@ public final class DisplayPowerControllerTest { mTestLooper = new TestLooper(mClock::now); mHandler = new Handler(mTestLooper.getLooper()); - // Put the system into manual brightness by default, just to minimize unexpected events and - // have a consistent starting state + // Set some settings to minimize unexpected events and have a consistent starting state Settings.System.putInt(mContext.getContentResolver(), Settings.System.SCREEN_BRIGHTNESS_MODE, Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL); - + Settings.System.putFloatForUser(mContext.getContentResolver(), + Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0, UserHandle.USER_CURRENT); addLocalServiceMock(WindowManagerPolicy.class, mWindowManagerPolicyMock); addLocalServiceMock(ColorDisplayService.ColorDisplayServiceInternal.class, diff --git a/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java index d9fbba5b4274..7e7ccf733876 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/HighBrightnessModeMetadataMapperTest.java @@ -17,35 +17,69 @@ package com.android.server.display; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import org.junit.Before; import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; public class HighBrightnessModeMetadataMapperTest { + @Mock + private LogicalDisplay mDisplayMock; + + @Mock + private DisplayDevice mDeviceMock; + + @Mock + private DisplayDeviceConfig mDdcMock; + + @Mock + private DisplayDeviceConfig.HighBrightnessModeData mHbmDataMock; + private HighBrightnessModeMetadataMapper mHighBrightnessModeMetadataMapper; @Before public void setUp() { + MockitoAnnotations.initMocks(this); + when(mDisplayMock.getPrimaryDisplayDeviceLocked()).thenReturn(mDeviceMock); + when(mDeviceMock.getDisplayDeviceConfig()).thenReturn(mDdcMock); + when(mDdcMock.getHighBrightnessModeData()).thenReturn(mHbmDataMock); mHighBrightnessModeMetadataMapper = new HighBrightnessModeMetadataMapper(); } @Test - public void testGetHighBrightnessModeMetadata() { - // Display device is null - final LogicalDisplay display = mock(LogicalDisplay.class); - when(display.getPrimaryDisplayDeviceLocked()).thenReturn(null); - assertNull(mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display)); - - // No HBM metadata stored for this display yet - final DisplayDevice device = mock(DisplayDevice.class); - when(display.getPrimaryDisplayDeviceLocked()).thenReturn(device); + public void testGetHighBrightnessModeMetadata_NoDisplayDevice() { + when(mDisplayMock.getPrimaryDisplayDeviceLocked()).thenReturn(null); + assertNull(mHighBrightnessModeMetadataMapper + .getHighBrightnessModeMetadataLocked(mDisplayMock)); + } + + @Test + public void testGetHighBrightnessModeMetadata_NoHBMData() { + when(mDdcMock.getHighBrightnessModeData()).thenReturn(null); + assertNull(mHighBrightnessModeMetadataMapper + .getHighBrightnessModeMetadataLocked(mDisplayMock)); + } + + @Test + public void testGetHighBrightnessModeMetadata_NewDisplay() { HighBrightnessModeMetadata hbmMetadata = - mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display); + mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(mDisplayMock); + assertNotNull(hbmMetadata); + assertTrue(hbmMetadata.getHbmEventQueue().isEmpty()); + assertTrue(hbmMetadata.getRunningStartTimeMillis() < 0); + } + + @Test + public void testGetHighBrightnessModeMetadata_Modify() { + HighBrightnessModeMetadata hbmMetadata = + mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(mDisplayMock); + assertNotNull(hbmMetadata); assertTrue(hbmMetadata.getHbmEventQueue().isEmpty()); assertTrue(hbmMetadata.getRunningStartTimeMillis() < 0); @@ -55,8 +89,10 @@ public class HighBrightnessModeMetadataMapperTest { long setTime = 300; hbmMetadata.addHbmEvent(new HbmEvent(startTimeMillis, endTimeMillis)); hbmMetadata.setRunningStartTimeMillis(setTime); + hbmMetadata = - mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(display); + mHighBrightnessModeMetadataMapper.getHighBrightnessModeMetadataLocked(mDisplayMock); + assertEquals(1, hbmMetadata.getHbmEventQueue().size()); assertEquals(startTimeMillis, hbmMetadata.getHbmEventQueue().getFirst().getStartTimeMillis()); diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java index 04273d6f4ed6..a820d1b9cfdb 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/mode/DisplayModeDirectorTest.java @@ -88,7 +88,6 @@ import com.android.internal.os.BackgroundThread; import com.android.internal.util.Preconditions; import com.android.internal.util.test.FakeSettingsProvider; import com.android.internal.util.test.FakeSettingsProviderRule; -import com.android.server.LocalServices; import com.android.server.display.DisplayDeviceConfig; import com.android.server.display.TestUtils; import com.android.server.display.mode.DisplayModeDirector.BrightnessObserver; @@ -149,15 +148,9 @@ public class DisplayModeDirectorTest { mContext = spy(new ContextWrapper(ApplicationProvider.getApplicationContext())); final MockContentResolver resolver = mSettingsProviderRule.mockContentResolver(mContext); when(mContext.getContentResolver()).thenReturn(resolver); - mInjector = spy(new FakesInjector()); + mInjector = spy(new FakesInjector(mDisplayManagerInternalMock, mStatusBarMock, + mSensorManagerInternalMock)); mHandler = new Handler(Looper.getMainLooper()); - - LocalServices.removeServiceForTest(StatusBarManagerInternal.class); - LocalServices.addService(StatusBarManagerInternal.class, mStatusBarMock); - LocalServices.removeServiceForTest(SensorManagerInternal.class); - LocalServices.addService(SensorManagerInternal.class, mSensorManagerInternalMock); - LocalServices.removeServiceForTest(DisplayManagerInternal.class); - LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock); } private DisplayModeDirector createDirectorFromRefreshRateArray( @@ -2831,16 +2824,28 @@ public class DisplayModeDirectorTest { private final DisplayInfo mDisplayInfo; private final Display mDisplay; private boolean mDisplayInfoValid = true; - private ContentObserver mBrightnessObserver; + private final DisplayManagerInternal mDisplayManagerInternal; + private final StatusBarManagerInternal mStatusBarManagerInternal; + private final SensorManagerInternal mSensorManagerInternal; + private ContentObserver mPeakRefreshRateObserver; FakesInjector() { + this(null, null, null); + } + + FakesInjector(DisplayManagerInternal displayManagerInternal, + StatusBarManagerInternal statusBarManagerInternal, + SensorManagerInternal sensorManagerInternal) { mDeviceConfig = new FakeDeviceConfig(); mDisplayInfo = new DisplayInfo(); mDisplayInfo.defaultModeId = MODE_ID; mDisplayInfo.supportedModes = new Display.Mode[] {new Display.Mode(MODE_ID, 800, 600, /* refreshRate= */ 60)}; mDisplay = createDisplay(DISPLAY_ID); + mDisplayManagerInternal = displayManagerInternal; + mStatusBarManagerInternal = statusBarManagerInternal; + mSensorManagerInternal = sensorManagerInternal; } @NonNull @@ -2896,6 +2901,21 @@ public class DisplayModeDirectorTest { return true; } + @Override + public DisplayManagerInternal getDisplayManagerInternal() { + return mDisplayManagerInternal; + } + + @Override + public StatusBarManagerInternal getStatusBarManagerInternal() { + return mStatusBarManagerInternal; + } + + @Override + public SensorManagerInternal getSensorManagerInternal() { + return mSensorManagerInternal; + } + protected Display createDisplay(int id) { return new Display(DisplayManagerGlobal.getInstance(), id, mDisplayInfo, ApplicationProvider.getApplicationContext().getResources()); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java index 5b5c8d415f84..1b024982c41f 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationControllerTest.java @@ -130,6 +130,8 @@ public class FullScreenMagnificationControllerTest { public DisplayManagerInternal mDisplayManagerInternalMock = mock(DisplayManagerInternal.class); + private float mOriginalMagnificationPersistedScale; + @Before public void setUp() { Looper looper = InstrumentationRegistry.getContext().getMainLooper(); @@ -143,6 +145,9 @@ public class FullScreenMagnificationControllerTest { mResolver = new MockContentResolver(); mResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); when(mMockContext.getContentResolver()).thenReturn(mResolver); + mOriginalMagnificationPersistedScale = Settings.Secure.getFloatForUser(mResolver, + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, 2.0f, + CURRENT_USER_ID); Settings.Secure.putFloatForUser(mResolver, Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, 2.0f, CURRENT_USER_ID); @@ -169,6 +174,10 @@ public class FullScreenMagnificationControllerTest { @After public void tearDown() { mMessageCapturingHandler.removeAllMessages(); + Settings.Secure.putFloatForUser(mResolver, + Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE, + mOriginalMagnificationPersistedScale, + CURRENT_USER_ID); } @@ -1311,7 +1320,10 @@ public class FullScreenMagnificationControllerTest { mFullScreenMagnificationController.setAlwaysOnMagnificationEnabled(false); mFullScreenMagnificationController.onUserContextChanged(DISPLAY_0); + // the magnifier should be deactivated. verify(mRequestObserver).onFullScreenMagnificationActivationState(eq(DISPLAY_0), eq(false)); + assertFalse(mFullScreenMagnificationController.isZoomedOutFromService(DISPLAY_0)); + verify(mMockThumbnail).setThumbnailBounds( /* currentBounds= */ any(), /* scale= */ anyFloat(), @@ -1330,8 +1342,11 @@ public class FullScreenMagnificationControllerTest { mFullScreenMagnificationController.setAlwaysOnMagnificationEnabled(true); mFullScreenMagnificationController.onUserContextChanged(DISPLAY_0); + // the magnifier should be zoomed out and keep activated by service action. assertEquals(1.0f, mFullScreenMagnificationController.getScale(DISPLAY_0), 0); assertTrue(mFullScreenMagnificationController.isActivated(DISPLAY_0)); + assertTrue(mFullScreenMagnificationController.isZoomedOutFromService(DISPLAY_0)); + verify(mMockThumbnail).setThumbnailBounds( /* currentBounds= */ any(), /* scale= */ anyFloat(), diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java index 989aee06a1df..9304b32ec2c0 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java @@ -96,19 +96,29 @@ import java.util.function.IntConsumer; * NON_ACTIVATED_ZOOMED_TMP -> IDLE [label="release"] * SHORTCUT_TRIGGERED -> IDLE [label="a11y\nbtn"] * SHORTCUT_TRIGGERED -> ACTIVATED [label="tap"] - * SHORTCUT_TRIGGERED -> SHORTCUT_TRIGGERED_ZOOMED_TMP [label="hold"] - * SHORTCUT_TRIGGERED -> PANNING [label="2hold] + * SHORTCUT_TRIGGERED -> ZOOMED_WITH_PERSISTED_SCALE_TMP [label="hold"] + * SHORTCUT_TRIGGERED -> PANNING [label="2hold"] + * ZOOMED_OUT_FROM_SERVICE -> IDLE [label="a11y\nbtn"] + * ZOOMED_OUT_FROM_SERVICE -> ZOOMED_OUT_FROM_SERVICE_DOUBLE_TAP [label="2tap"] + * ZOOMED_OUT_FROM_SERVICE -> PANNING [label="2hold"] + * ZOOMED_OUT_FROM_SERVICE_DOUBLE_TAP -> IDLE [label="tap"] + * ZOOMED_OUT_FROM_SERVICE_DOUBLE_TAP -> ZOOMED_OUT_FROM_SERVICE [label="timeout"] + * ZOOMED_OUT_FROM_SERVICE_DOUBLE_TAP -> ZOOMED_WITH_PERSISTED_SCALE_TMP [label="hold"] * if always-on enabled: - * SHORTCUT_TRIGGERED_ZOOMED_TMP -> ACTIVATED [label="release"] + * ZOOMED_WITH_PERSISTED_SCALE_TMP -> ACTIVATED [label="release"] * else: - * SHORTCUT_TRIGGERED_ZOOMED_TMP -> IDLE [label="release"] + * ZOOMED_WITH_PERSISTED_SCALE_TMP -> IDLE [label="release"] * ACTIVATED -> ACTIVATED_DOUBLE_TAP [label="2tap"] * ACTIVATED -> IDLE [label="a11y\nbtn"] * ACTIVATED -> PANNING [label="2hold"] + * if always-on enabled: + * ACTIVATED -> ZOOMED_OUT_FROM_SERVICE [label="contextChanged"] + * else: + * ACTIVATED -> IDLE [label="contextChanged"] * ACTIVATED_DOUBLE_TAP -> ACTIVATED [label="timeout"] - * ACTIVATED_DOUBLE_TAP -> ACTIVATED_ZOOMED_TMP [label="hold"] + * ACTIVATED_DOUBLE_TAP -> ZOOMED_FURTHER_TMP [label="hold"] * ACTIVATED_DOUBLE_TAP -> IDLE [label="tap"] - * ACTIVATED_ZOOMED_TMP -> ACTIVATED [label="release"] + * ZOOMED_FURTHER_TMP -> ACTIVATED [label="release"] * PANNING -> ACTIVATED [label="release"] * PANNING -> PANNING_SCALING [label="pinch"] * PANNING_SCALING -> ACTIVATED [label="release"] @@ -120,15 +130,20 @@ public class FullScreenMagnificationGestureHandlerTest { public static final int STATE_IDLE = 1; public static final int STATE_ACTIVATED = 2; - public static final int STATE_2TAPS = 3; - public static final int STATE_ACTIVATED_2TAPS = 4; - public static final int STATE_SHORTCUT_TRIGGERED = 5; - public static final int STATE_NON_ACTIVATED_ZOOMED_TMP = 6; - public static final int STATE_ACTIVATED_ZOOMED_TMP = 7; - public static final int STATE_SHORTCUT_TRIGGERED_ZOOMED_TMP = 8; - public static final int STATE_PANNING = 9; - public static final int STATE_SCALING_AND_PANNING = 10; - public static final int STATE_SINGLE_PANNING = 11; + public static final int STATE_SHORTCUT_TRIGGERED = 3; + public static final int STATE_ZOOMED_OUT_FROM_SERVICE = 4; + + public static final int STATE_2TAPS = 5; + public static final int STATE_ACTIVATED_2TAPS = 6; + public static final int STATE_ZOOMED_OUT_FROM_SERVICE_2TAPS = 7; + + public static final int STATE_NON_ACTIVATED_ZOOMED_TMP = 8; + public static final int STATE_ZOOMED_FURTHER_TMP = 9; + public static final int STATE_ZOOMED_WITH_PERSISTED_SCALE_TMP = 10; + + public static final int STATE_PANNING = 11; + public static final int STATE_SCALING_AND_PANNING = 12; + public static final int STATE_SINGLE_PANNING = 13; public static final int FIRST_STATE = STATE_IDLE; public static final int LAST_STATE = STATE_SINGLE_PANNING; @@ -313,7 +328,7 @@ public class FullScreenMagnificationGestureHandlerTest { assertTransition(STATE_SHORTCUT_TRIGGERED, () -> { send(downEvent()); fastForward1sec(); - }, STATE_SHORTCUT_TRIGGERED_ZOOMED_TMP); + }, STATE_ZOOMED_WITH_PERSISTED_SCALE_TMP); // A11y button followed by a tap turns magnifier on assertTransition(STATE_SHORTCUT_TRIGGERED, () -> tap(), STATE_ACTIVATED); @@ -337,22 +352,22 @@ public class FullScreenMagnificationGestureHandlerTest { assertTransition(STATE_2TAPS, () -> swipeAndHold(), STATE_NON_ACTIVATED_ZOOMED_TMP); // release when activated temporary zoom in back to activated - assertTransition(STATE_ACTIVATED_ZOOMED_TMP, () -> send(upEvent()), STATE_ACTIVATED); + assertTransition(STATE_ZOOMED_FURTHER_TMP, () -> send(upEvent()), STATE_ACTIVATED); } @Test - public void testRelease_shortcutTriggeredZoomedTmp_alwaysOnNotEnabled_shouldInIdle() { + public void testRelease_zoomedWithPersistedScaleTmpAndAlwaysOnNotEnabled_shouldInIdle() { mFullScreenMagnificationController.setAlwaysOnMagnificationEnabled(false); - goFromStateIdleTo(STATE_SHORTCUT_TRIGGERED_ZOOMED_TMP); + goFromStateIdleTo(STATE_ZOOMED_WITH_PERSISTED_SCALE_TMP); send(upEvent()); assertIn(STATE_IDLE); } @Test - public void testRelease_shortcutTriggeredZoomedTmp_alwaysOnEnabled_shouldInActivated() { + public void testRelease_zoomedWithPersistedScaleTmpAndAlwaysOnEnabled_shouldInActivated() { mFullScreenMagnificationController.setAlwaysOnMagnificationEnabled(true); - goFromStateIdleTo(STATE_SHORTCUT_TRIGGERED_ZOOMED_TMP); + goFromStateIdleTo(STATE_ZOOMED_WITH_PERSISTED_SCALE_TMP); send(upEvent()); assertIn(STATE_ACTIVATED); @@ -404,9 +419,11 @@ public class FullScreenMagnificationGestureHandlerTest { @Test public void testTripleTapAndHold_zoomsImmediately() { assertZoomsImmediatelyOnSwipeFrom(STATE_2TAPS, STATE_NON_ACTIVATED_ZOOMED_TMP); + assertZoomsImmediatelyOnSwipeFrom(STATE_ACTIVATED_2TAPS, STATE_ZOOMED_FURTHER_TMP); assertZoomsImmediatelyOnSwipeFrom(STATE_SHORTCUT_TRIGGERED, - STATE_SHORTCUT_TRIGGERED_ZOOMED_TMP); - assertZoomsImmediatelyOnSwipeFrom(STATE_ACTIVATED_2TAPS, STATE_ACTIVATED_ZOOMED_TMP); + STATE_ZOOMED_WITH_PERSISTED_SCALE_TMP); + assertZoomsImmediatelyOnSwipeFrom(STATE_ZOOMED_OUT_FROM_SERVICE_2TAPS, + STATE_ZOOMED_WITH_PERSISTED_SCALE_TMP); } @Test @@ -755,6 +772,10 @@ public class FullScreenMagnificationGestureHandlerTest { mHandler.timeAdvance(); } + private void triggerContextChanged() { + mFullScreenMagnificationController.onUserContextChanged(DISPLAY_0); + } + /** * Asserts that {@link #mMgh the handler} is in the given {@code state} */ @@ -763,43 +784,63 @@ public class FullScreenMagnificationGestureHandlerTest { // Asserts on separate lines for accurate stack traces - case STATE_IDLE: { + case STATE_IDLE: check(tapCount() < 2, state); check(!mMgh.mDetectingState.mShortcutTriggered, state); check(!isActivated(), state); check(!isZoomed(), state); - } break; - case STATE_ACTIVATED: { + break; + case STATE_ACTIVATED: check(isActivated(), state); check(tapCount() < 2, state); - } break; - case STATE_2TAPS: { + break; + case STATE_SHORTCUT_TRIGGERED: + check(mMgh.mDetectingState.mShortcutTriggered, state); + check(isActivated(), state); + check(!isZoomed(), state); + break; + case STATE_ZOOMED_OUT_FROM_SERVICE: + // the always-on feature must be enabled then this state is reachable. + assertTrue(mFullScreenMagnificationController.isAlwaysOnMagnificationEnabled()); + check(isActivated(), state); + check(!isZoomed(), state); + check(mMgh.mFullScreenMagnificationController.isZoomedOutFromService(DISPLAY_0), + state); + break; + case STATE_2TAPS: check(!isActivated(), state); check(!isZoomed(), state); check(tapCount() == 2, state); - } break; - case STATE_ACTIVATED_2TAPS: { + break; + case STATE_ACTIVATED_2TAPS: check(isActivated(), state); check(isZoomed(), state); check(tapCount() == 2, state); - } break; - case STATE_NON_ACTIVATED_ZOOMED_TMP: { + break; + case STATE_ZOOMED_OUT_FROM_SERVICE_2TAPS: + check(isActivated(), state); + check(!isZoomed(), state); + check(mMgh.mFullScreenMagnificationController.isZoomedOutFromService(DISPLAY_0), + state); + check(tapCount() == 2, state); + break; + case STATE_NON_ACTIVATED_ZOOMED_TMP: check(isActivated(), state); check(isZoomed(), state); check(mMgh.mCurrentState == mMgh.mViewportDraggingState, state); check(Float.isNaN(mMgh.mViewportDraggingState.mScaleToRecoverAfterDraggingEnd), state); - } break; - case STATE_ACTIVATED_ZOOMED_TMP: { + break; + case STATE_ZOOMED_FURTHER_TMP: check(isActivated(), state); check(isZoomed(), state); check(mMgh.mCurrentState == mMgh.mViewportDraggingState, state); check(mMgh.mViewportDraggingState.mScaleToRecoverAfterDraggingEnd >= 1.0f, state); - } break; - case STATE_SHORTCUT_TRIGGERED_ZOOMED_TMP: { + break; + case STATE_ZOOMED_WITH_PERSISTED_SCALE_TMP: check(isActivated(), state); check(isZoomed(), state); check(mMgh.mCurrentState == mMgh.mViewportDraggingState, @@ -811,29 +852,25 @@ public class FullScreenMagnificationGestureHandlerTest { check(Float.isNaN(mMgh.mViewportDraggingState.mScaleToRecoverAfterDraggingEnd), state); } - } break; - case STATE_SHORTCUT_TRIGGERED: { - check(mMgh.mDetectingState.mShortcutTriggered, state); - check(isActivated(), state); - check(!isZoomed(), state); - } break; - case STATE_PANNING: { + break; + case STATE_PANNING: check(isActivated(), state); check(mMgh.mCurrentState == mMgh.mPanningScalingState, state); check(!mMgh.mPanningScalingState.mScaling, state); - } break; - case STATE_SCALING_AND_PANNING: { + break; + case STATE_SCALING_AND_PANNING: check(isActivated(), state); check(mMgh.mCurrentState == mMgh.mPanningScalingState, state); check(mMgh.mPanningScalingState.mScaling, state); - } break; - case STATE_SINGLE_PANNING: { + break; + case STATE_SINGLE_PANNING: check(isZoomed(), state); check(mMgh.mCurrentState == mMgh.mSinglePanningState, state); - } break; - default: throw new IllegalArgumentException("Illegal state: " + state); + break; + default: + throw new IllegalArgumentException("Illegal state: " + state); } } @@ -843,15 +880,10 @@ public class FullScreenMagnificationGestureHandlerTest { private void goFromStateIdleTo(int state) { try { switch (state) { - case STATE_IDLE: { + case STATE_IDLE: mMgh.clearAndTransitionToStateDetecting(); - } break; - case STATE_2TAPS: { - goFromStateIdleTo(STATE_IDLE); - tap(); - tap(); - } break; - case STATE_ACTIVATED: { + break; + case STATE_ACTIVATED: if (mMgh.mDetectTripleTap) { goFromStateIdleTo(STATE_2TAPS); tap(); @@ -859,47 +891,63 @@ public class FullScreenMagnificationGestureHandlerTest { goFromStateIdleTo(STATE_SHORTCUT_TRIGGERED); tap(); } - } break; - case STATE_ACTIVATED_2TAPS: { + break; + case STATE_SHORTCUT_TRIGGERED: + goFromStateIdleTo(STATE_IDLE); + triggerShortcut(); + break; + case STATE_ZOOMED_OUT_FROM_SERVICE: + // the always-on feature must be enabled then this state is reachable. + assertTrue(mFullScreenMagnificationController.isAlwaysOnMagnificationEnabled()); goFromStateIdleTo(STATE_ACTIVATED); + triggerContextChanged(); + break; + case STATE_2TAPS: + goFromStateIdleTo(STATE_IDLE); + tap(); + tap(); + break; + case STATE_ACTIVATED_2TAPS: + goFromStateIdleTo(STATE_ACTIVATED); + tap(); tap(); + break; + case STATE_ZOOMED_OUT_FROM_SERVICE_2TAPS: + goFromStateIdleTo(STATE_ZOOMED_OUT_FROM_SERVICE); tap(); - } break; - case STATE_NON_ACTIVATED_ZOOMED_TMP: { + tap(); + break; + case STATE_NON_ACTIVATED_ZOOMED_TMP: goFromStateIdleTo(STATE_2TAPS); send(downEvent()); fastForward1sec(); - } break; - case STATE_ACTIVATED_ZOOMED_TMP: { + break; + case STATE_ZOOMED_FURTHER_TMP: goFromStateIdleTo(STATE_ACTIVATED_2TAPS); send(downEvent()); fastForward1sec(); - } break; - case STATE_SHORTCUT_TRIGGERED_ZOOMED_TMP: { + break; + case STATE_ZOOMED_WITH_PERSISTED_SCALE_TMP: goFromStateIdleTo(STATE_SHORTCUT_TRIGGERED); send(downEvent()); fastForward1sec(); - } break; - case STATE_SHORTCUT_TRIGGERED: { - goFromStateIdleTo(STATE_IDLE); - triggerShortcut(); - } break; - case STATE_PANNING: { + break; + case STATE_PANNING: goFromStateIdleTo(STATE_ACTIVATED); send(downEvent()); send(pointerEvent(ACTION_POINTER_DOWN, DEFAULT_X * 2, DEFAULT_Y)); fastForward(ViewConfiguration.getTapTimeout()); - } break; - case STATE_SCALING_AND_PANNING: { + break; + case STATE_SCALING_AND_PANNING: goFromStateIdleTo(STATE_PANNING); send(pointerEvent(ACTION_MOVE, DEFAULT_X * 2, DEFAULT_Y * 3)); send(pointerEvent(ACTION_MOVE, DEFAULT_X * 2, DEFAULT_Y * 4)); send(pointerEvent(ACTION_MOVE, DEFAULT_X * 2, DEFAULT_Y * 5)); - } break; - case STATE_SINGLE_PANNING: { + break; + case STATE_SINGLE_PANNING: goFromStateIdleTo(STATE_ACTIVATED); swipeAndHold(); - } break; + break; default: throw new IllegalArgumentException("Illegal state: " + state); } @@ -913,14 +961,11 @@ public class FullScreenMagnificationGestureHandlerTest { */ private void returnToNormalFrom(int state) { switch (state) { - case STATE_IDLE: { + case STATE_IDLE: // no op - } break; - case STATE_2TAPS: { - allowEventDelegation(); - fastForward1sec(); - } break; - case STATE_ACTIVATED: { + break; + case STATE_ACTIVATED: + case STATE_ZOOMED_OUT_FROM_SERVICE: if (mMgh.mDetectTripleTap) { tap(); tap(); @@ -928,39 +973,45 @@ public class FullScreenMagnificationGestureHandlerTest { } else { triggerShortcut(); } - } break; - case STATE_ACTIVATED_2TAPS: { + break; + case STATE_SHORTCUT_TRIGGERED: + triggerShortcut(); + break; + case STATE_2TAPS: + allowEventDelegation(); + fastForward1sec(); + break; + case STATE_ACTIVATED_2TAPS: + case STATE_ZOOMED_OUT_FROM_SERVICE_2TAPS: tap(); - } break; - case STATE_NON_ACTIVATED_ZOOMED_TMP: { + break; + case STATE_NON_ACTIVATED_ZOOMED_TMP: send(upEvent()); - } break; - case STATE_ACTIVATED_ZOOMED_TMP: { + break; + case STATE_ZOOMED_FURTHER_TMP: send(upEvent()); returnToNormalFrom(STATE_ACTIVATED); - } break; - case STATE_SHORTCUT_TRIGGERED_ZOOMED_TMP: { + break; + case STATE_ZOOMED_WITH_PERSISTED_SCALE_TMP: send(upEvent()); if (mFullScreenMagnificationController.isAlwaysOnMagnificationEnabled()) { returnToNormalFrom(STATE_ACTIVATED); } - } break; - case STATE_SHORTCUT_TRIGGERED: { - triggerShortcut(); - } break; - case STATE_PANNING: { + break; + case STATE_PANNING: send(pointerEvent(ACTION_POINTER_UP, DEFAULT_X * 2, DEFAULT_Y)); send(upEvent()); returnToNormalFrom(STATE_ACTIVATED); - } break; - case STATE_SCALING_AND_PANNING: { + break; + case STATE_SCALING_AND_PANNING: returnToNormalFrom(STATE_PANNING); - } break; - case STATE_SINGLE_PANNING: { + break; + case STATE_SINGLE_PANNING: send(upEvent()); returnToNormalFrom(STATE_ACTIVATED); - } break; - default: throw new IllegalArgumentException("Illegal state: " + state); + break; + default: + throw new IllegalArgumentException("Illegal state: " + state); } } diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index d76d615cc36a..e6b12693c5e4 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -16,7 +16,6 @@ package com.android.server.companion.virtual; -import static android.companion.virtual.VirtualDeviceManager.ASSOCIATION_ID_INVALID; import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM; import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT; import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS; @@ -1714,19 +1713,16 @@ public class VirtualDeviceManagerServiceTest { } @Test - public void getAssociationIdForDevice_invalidDeviceId_returnsInvalidAssociationId() { - assertThat(mLocalService.getAssociationIdForDevice(DEVICE_ID_INVALID)) - .isEqualTo(ASSOCIATION_ID_INVALID); - assertThat(mLocalService.getAssociationIdForDevice(DEVICE_ID_DEFAULT)) - .isEqualTo(ASSOCIATION_ID_INVALID); - assertThat(mLocalService.getAssociationIdForDevice(VIRTUAL_DEVICE_ID_2)) - .isEqualTo(ASSOCIATION_ID_INVALID); + public void getPersistentIdForDevice_invalidDeviceId_returnsNull() { + assertThat(mLocalService.getPersistentIdForDevice(DEVICE_ID_INVALID)).isNull(); + assertThat(mLocalService.getPersistentIdForDevice(DEVICE_ID_DEFAULT)).isNull(); + assertThat(mLocalService.getPersistentIdForDevice(VIRTUAL_DEVICE_ID_2)).isNull(); } @Test - public void getAssociationIdForDevice_returnsCorrectAssociationId() { - assertThat(mLocalService.getAssociationIdForDevice(VIRTUAL_DEVICE_ID_1)) - .isEqualTo(mAssociationInfo.getId()); + public void getPersistentIdForDevice_returnsCorrectId() { + assertThat(mLocalService.getPersistentIdForDevice(VIRTUAL_DEVICE_ID_1)) + .isEqualTo(mDeviceImpl.getPersistentDeviceId()); } private VirtualDeviceImpl createVirtualDevice(int virtualDeviceId, int ownerUid) { @@ -1740,12 +1736,13 @@ public class VirtualDeviceManagerServiceTest { VirtualDeviceParams params) { VirtualDeviceImpl virtualDeviceImpl = new VirtualDeviceImpl(mContext, mAssociationInfo, mVdms, new Binder(), ownerUid, virtualDeviceId, - mInputController, mCameraAccessController - /* onDeviceCloseListener= */ /*deviceId -> mVdms.removeVirtualDevice(deviceId)*/, + mInputController, mCameraAccessController, mPendingTrampolineCallback, mActivityListener, mSoundEffectListener, mRunningAppsChangedCallback, params, new DisplayManagerGlobal(mIDisplayManager)); mVdms.addVirtualDevice(virtualDeviceImpl); assertThat(virtualDeviceImpl.getAssociationId()).isEqualTo(mAssociationInfo.getId()); + assertThat(virtualDeviceImpl.getPersistentDeviceId()) + .isEqualTo("companion:" + mAssociationInfo.getId()); return virtualDeviceImpl; } diff --git a/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java index b5bf1ea34a46..a1d42ffc1b74 100644 --- a/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/dreams/DreamControllerTest.java @@ -33,6 +33,7 @@ import android.app.ActivityTaskManager; import android.content.ComponentName; import android.content.Context; import android.content.ServiceConnection; +import android.content.res.Resources; import android.os.Binder; import android.os.Handler; import android.os.IBinder; @@ -72,6 +73,9 @@ public class DreamControllerTest { @Mock private IDreamService mIDreamService; + @Mock + private Resources mResources; + @Captor private ArgumentCaptor<ServiceConnection> mServiceConnectionACaptor; @Captor @@ -105,6 +109,7 @@ public class DreamControllerTest { .thenReturn(powerManager); when(mContext.getSystemServiceName(PowerManager.class)) .thenReturn(Context.POWER_SERVICE); + when(mContext.getResources()).thenReturn(mResources); mToken = new Binder(); mDreamName = ComponentName.unflattenFromString("dream"); diff --git a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt index 003797066b39..8c07b6cbc894 100644 --- a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt +++ b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt @@ -18,6 +18,7 @@ package com.android.server.pm.parsing import android.content.res.Validator import android.os.Environment +import android.os.SystemProperties.PROP_VALUE_MAX import android.platform.test.annotations.Postsubmit import com.android.internal.R import com.android.server.pm.PackageManagerService @@ -28,7 +29,6 @@ import org.junit.Assert.assertThrows import org.junit.Assert.fail import org.junit.Test import org.xmlpull.v1.XmlPullParser -import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserFactory import java.io.ByteArrayInputStream import java.io.File @@ -75,548 +75,370 @@ class AndroidPackageParsingValidationTest { } @Test - fun parseBadManifests() { + fun parseManifestTag() { val tag = "manifest" - val prefix = "<manifest $ns>" - val suffix = "</manifest>" - parseTagBadAttr(tag, "package", 256, ) - parseTagBadAttr(tag, "android:sharedUserId", 256) - parseTagBadAttr(tag, "android:versionName", 4000) - parseBadApplicationTags(100, prefix, suffix, tag) - parseBadOverlayTags(100, prefix, suffix, tag) - parseBadInstrumentationTags(100, prefix, suffix, tag) - parseBadPermissionGroupTags(100, prefix, suffix, tag) - parseBadPermissionTreeTags(100, prefix, suffix, tag) - parseBadSupportsGlTextureTags(100, prefix, suffix, tag) - parseBadSupportsScreensTags(100, prefix, suffix, tag) - parseBadUsesConfigurationTags(100, prefix, suffix, tag) - parseBadUsesPermissionSdk23Tags(100, prefix, suffix, tag) - parseBadUsesSdkTags(100, prefix, suffix, tag) - parseBadCompatibleScreensTags(200, prefix, suffix, tag) - parseBadQueriesTags(200, prefix, suffix, tag) - parseBadAttributionTags(400, prefix, suffix, tag) - parseBadUsesFeatureTags(400, prefix, suffix, tag) - parseBadPermissionTags(2000, prefix, suffix, tag) - parseBadUsesPermissionTags(20000, prefix, suffix, tag) - } - - private fun parseBadApplicationTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + validateTagAttr(tag, "package", null, 256) + validateTagAttr(tag, "sharedUserId", null, 256) + validateTagAttr(tag, "versionName", null, 1024) + validateTagCount("application", 100, tag) + validateTagCount("overlay", 100, tag) + validateTagCount("instrumentation", 100, tag) + validateTagCount("permission-group", 100, tag) + validateTagCount("permission-tree", 100, tag) + validateTagCount("supports-gl-texture", 100, tag) + validateTagCount("supports-screens", 100, tag) + validateTagCount("uses-configuration", 100, tag) + validateTagCount("uses-sdk", 100, tag) + validateTagCount("compatible-screens", 200, tag) + validateTagCount("queries", 200, tag) + validateTagCount("attribution", 400, tag) + validateTagCount("uses-feature", 400, tag) + validateTagCount("permission", 2000, tag) + validateTagCount("uses-permission", 20000, tag) + } + + @Test + fun parseApplicationTag() { val tag = "application" - val newPrefix = "$prefix<$tag>" - val newSuffix = "</$tag>$suffix" - - parseTagBadAttr(tag, "android:backupAgent", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:manageSpaceActivity", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:permission", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:process", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:requiredAccountType", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:restrictedAccountType", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:taskAffinity", 1024, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) - - parseBadProfileableTags(100, newPrefix, newSuffix, tag) - parseBadUsesNativeLibraryTags(100, newPrefix, newSuffix, tag) - parseBadReceiverTags(1000, newPrefix, newSuffix, tag) - parseBadServiceTags(1000, newPrefix, newSuffix, tag) - parseBadActivityAliasTags(4000, newPrefix, newSuffix, tag) - parseBadUsesLibraryTags(4000, newPrefix, newSuffix, tag) - parseBadProviderTags(8000, newPrefix, newSuffix, tag) - parseBadMetaDataTags(8000, newPrefix, newSuffix, tag) - parseBadActivityTags(40000, newPrefix, newSuffix, tag) - } - - private fun parseBadProfileableTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { - val tag = "profileable" - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) + validateTagAttr(tag, "backupAgent", + R.styleable.AndroidManifestApplication_backupAgent, 1024) + validateTagAttr(tag, "manageSpaceActivity", + R.styleable.AndroidManifestApplication_manageSpaceActivity, 1024) + validateTagAttr(tag, "name", R.styleable.AndroidManifestApplication_name, 1024) + validateTagAttr(tag, "permission", R.styleable.AndroidManifestApplication_permission, 1024) + validateTagAttr(tag, "process", R.styleable.AndroidManifestApplication_process, 1024) + validateTagAttr(tag, "requiredAccountType", + R.styleable.AndroidManifestApplication_requiredAccountType, 1024) + validateTagAttr(tag, "restrictedAccountType", + R.styleable.AndroidManifestApplication_restrictedAccountType, 1024) + validateTagAttr(tag, "taskAffinity", + R.styleable.AndroidManifestApplication_taskAffinity, 1024) + validateTagCount("profileable", 100, tag) + validateTagCount("uses-native-library", 100, tag) + validateTagCount("receiver", 1000, tag) + validateTagCount("service", 1000, tag) + validateTagCount("meta-data", 1000, tag) + validateTagCount("uses-library", 1000, tag) + validateTagCount("activity-alias", 4000, tag) + validateTagCount("provider", 8000, tag) + validateTagCount("activity", 40000, tag) } - private fun parseBadUsesNativeLibraryTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + @Test + fun parseUsesNativeLibraryTag() { val tag = "uses-native-library" - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) + validateTagAttr(tag, "name", R.styleable.AndroidManifestUsesNativeLibrary_name, 1024) } - private fun parseBadReceiverTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + @Test + fun parseReceiverTag() { val tag = "receiver" - val newPrefix = "$prefix<$tag>" - val newSuffix = "</$tag>$suffix" - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:permission", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:process", 1024, prefix, suffix) - - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) - parseBadMetaDataTags(8000, newPrefix, newSuffix, tag) - parseBadIntentFilterTags(20000, newPrefix, newSuffix, tag) - } - - private fun parseBadServiceTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + validateTagAttr(tag, "name", R.styleable.AndroidManifestReceiver_name, 1024) + validateTagAttr(tag, "permission", R.styleable.AndroidManifestReceiver_permission, 1024) + validateTagAttr(tag, "process", R.styleable.AndroidManifestReceiver_process, 1024) + validateTagCount("meta-data", 1000, tag) + validateTagCount("intent-filter", 20000, tag) + } + + @Test + fun parseServiceTag() { val tag = "service" - val newPrefix = "$prefix<$tag>" - val newSuffix = "</$tag>$suffix" - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:permission", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:process", 1024, prefix, suffix) - - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) - parseBadMetaDataTags(8000, newPrefix, newSuffix, tag) - parseBadIntentFilterTags(20000, newPrefix, newSuffix, tag) - } - - private fun parseBadActivityAliasTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + validateTagAttr(tag, "name", R.styleable.AndroidManifestService_name, 1024) + validateTagAttr(tag, "permission", R.styleable.AndroidManifestService_permission, 1024) + validateTagAttr(tag, "process", R.styleable.AndroidManifestService_process, 1024) + validateTagCount("meta-data", 1000, tag) + validateTagCount("intent-filter", 20000, tag) + } + + @Test + fun parseActivityAliasTag() { val tag = "activity-alias" - val newPrefix = "$prefix<$tag>" - val newSuffix = "</$tag>$suffix" - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:permission", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:targetActivity", 1024, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) - parseBadMetaDataTags(8000, newPrefix, newSuffix, tag) - parseBadIntentFilterTags(20000, newPrefix, newSuffix, tag) - } - - private fun parseBadUsesLibraryTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + validateTagAttr(tag, "name", R.styleable.AndroidManifestActivityAlias_name, 1024) + validateTagAttr(tag, "permission", + R.styleable.AndroidManifestActivityAlias_permission, 1024) + validateTagAttr(tag, "targetActivity", + R.styleable.AndroidManifestActivityAlias_targetActivity, 1024) + validateTagCount("meta-data", 1000, tag) + validateTagCount("intent-filter", 20000, tag) + } + + @Test + fun parseUsesLibraryTag() { val tag = "uses-library" - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) + validateTagAttr(tag, "name", R.styleable.AndroidManifestUsesLibrary_name, 1024) } - private fun parseBadActivityTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + @Test + fun parseActivityTag() { val tag = "activity" - val newPrefix = "$prefix<$tag>" - val newSuffix = "</$tag>$suffix" - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:parentActivityName", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:permission", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:process", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:taskAffinity", 1024, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) - parseBadLayoutTags(1000, newPrefix, newSuffix, tag) - parseBadMetaDataTags(8000, newPrefix, newSuffix, tag) - parseBadIntentFilterTags(20000, newPrefix, newSuffix, tag) - } - - private fun parseBadLayoutTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { - val tag = "layout" - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) + validateTagAttr(tag, "name", R.styleable.AndroidManifestActivity_name, 1024) + validateTagAttr(tag, "parentActivityName", + R.styleable.AndroidManifestActivity_parentActivityName, 1024) + validateTagAttr(tag, "permission", R.styleable.AndroidManifestActivity_permission, 1024) + validateTagAttr(tag, "process", R.styleable.AndroidManifestActivity_process, 1024) + validateTagAttr(tag, "taskAffinity", R.styleable.AndroidManifestActivity_taskAffinity, 1024) + validateTagCount("layout", 1000, tag) + validateTagCount("meta-data", 1000, tag) + validateTagCount("intent-filter", 20000, tag) } - private fun parseBadOverlayTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + @Test + fun parseOverlayTag() { val tag = "overlay" - parseTagBadAttr(tag, "android:category", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:requiredSystemPropertyName", 32768, prefix, suffix) - parseTagBadAttr(tag, "android:requiredSystemPropertyValue", 256, prefix, suffix) - parseTagBadAttr(tag, "android:targetPackage", 256, prefix, suffix) - parseTagBadAttr(tag, "android:targetName", 1024, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) - } - - private fun parseBadInstrumentationTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + validateTagAttr(tag, "category", R.styleable.AndroidManifestResourceOverlay_category, 1024) + validateTagAttr(tag, "requiredSystemPropertyName", + R.styleable.AndroidManifestResourceOverlay_requiredSystemPropertyName, 1024) + validateTagAttr(tag, "requiredSystemPropertyValue", + R.styleable.AndroidManifestResourceOverlay_requiredSystemPropertyValue, PROP_VALUE_MAX) + validateTagAttr(tag, "targetPackage", + R.styleable.AndroidManifestResourceOverlay_targetPackage, 256) + validateTagAttr(tag, "targetName", + R.styleable.AndroidManifestResourceOverlay_targetName, 1024) + } + + @Test + fun parseInstrumentationTag() { val tag = "instrumentation" - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:targetPackage", 256, prefix, suffix) - parseTagBadAttr(tag, "android:targetProcesses", 1024, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) + validateTagAttr(tag, "name", R.styleable.AndroidManifestInstrumentation_name, 1024) + validateTagAttr(tag, "targetPackage", + R.styleable.AndroidManifestInstrumentation_targetPackage, 256) + validateTagAttr(tag, "targetProcesses", + R.styleable.AndroidManifestInstrumentation_targetProcesses, 1024) } - private fun parseBadPermissionGroupTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + @Test + fun parsePermissionGroupTag() { val tag = "permission-group" - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) + validateTagAttr(tag, "name", R.styleable.AndroidManifestPermissionGroup_name, 1024) } - private fun parseBadPermissionTreeTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + @Test + fun parsePermissionTreeTag() { val tag = "permission-tree" - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) + validateTagAttr(tag, "name", R.styleable.AndroidManifestPermissionTree_name, 1024) } - private fun parseBadSupportsGlTextureTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + @Test + fun parseSupportsGlTextureTag() { val tag = "supports-gl-texture" - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) + validateTagAttr(tag, "name", null, 1024) } - private fun parseBadSupportsScreensTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { - val tag = "supports-screens" - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) - } - - private fun parseBadUsesConfigurationTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { - val tag = "uses-configuration" - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) - } - - private fun parseBadUsesPermissionSdk23Tags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + @Test + fun parseUsesPermissionSdk23Tag() { val tag = "uses-permission-sdk-23" - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) + validateTagAttr(tag, "name", R.styleable.AndroidManifestUsesPermission_name, 1024) } - private fun parseBadUsesSdkTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { - val tag = "uses-sdk" - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) - } - - private fun parseBadCompatibleScreensTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + @Test + fun parseCompatibleScreensTag() { val tag = "compatible-screens" - val newPrefix = "$prefix<$tag>" - val newSuffix = "</$tag>$suffix" - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) - parseBadScreenTags(4000, newPrefix, newSuffix, tag) + validateTagCount("screen", 4000, tag) } - private fun parseBadScreenTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { - val tag = "screen" - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) + @Test + fun parseQueriesTag() { + val tag = "queries" + validateTagCount("package", 1000, tag) + validateTagCount("intent", 2000, tag) + validateTagCount("provider", 8000, tag) } - private fun parseBadQueriesTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { - val tag = "queries" - val newPrefix = "$prefix<$tag>" - val newSuffix = "</$tag>$suffix" - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) - parseBadPackageTags(1000, newPrefix, newSuffix, tag) - parseBadIntentTags(2000, newPrefix, newSuffix, tag) - parseBadProviderTags(8000, newPrefix, newSuffix, tag) - } - - private fun parseBadPackageTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + @Test + fun parsePackageTag() { val tag = "package" - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) + validateTagAttr(tag, "name", null, 1024) } - private fun parseBadIntentTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + @Test + fun parseIntentTag() { val tag = "intent" - val newPrefix = "$prefix<$tag>" - val newSuffix = "</$tag>$suffix" - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) - parseBadActionTags(20000, newPrefix, newSuffix, tag) - parseBadCategoryTags(40000, newPrefix, newSuffix, tag) - parseBadDataTags(40000, newPrefix, newSuffix, tag) - } - - private fun parseBadProviderTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + validateTagCount("action", 20000, tag) + validateTagCount("category", 40000, tag) + validateTagCount("data", 40000, tag) + } + + @Test + fun parseProviderTag() { val tag = "provider" - val newPrefix = "$prefix<$tag>" - val newSuffix = "</$tag>$suffix" - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:permission", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:process", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:readPermission", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:writePermission", 1024, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) - parseBadGrantUriPermissionTags(100, newPrefix, newSuffix, tag) - parseBadPathPermissionTags(100, newPrefix, newSuffix, tag) - parseBadMetaDataTags(8000, newPrefix, newSuffix, tag) - parseBadIntentFilterTags(20000, newPrefix, newSuffix, tag) - } - - private fun parseBadGrantUriPermissionTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + validateTagAttr(tag, "name", R.styleable.AndroidManifestProvider_name, 1024) + validateTagAttr(tag, "permission", R.styleable.AndroidManifestProvider_permission, 1024) + validateTagAttr(tag, "process", R.styleable.AndroidManifestProvider_process, 1024) + validateTagAttr(tag, "readPermission", + R.styleable.AndroidManifestProvider_readPermission, 1024) + validateTagAttr(tag, "writePermission", + R.styleable.AndroidManifestProvider_writePermission, 1024) + validateTagCount("grant-uri-permission", 100, tag) + validateTagCount("path-permission", 100, tag) + validateTagCount("meta-data", 1000, tag) + validateTagCount("intent-filter", 20000, tag) + } + + @Test + fun parseGrantUriPermissionTag() { val tag = "grant-uri-permission" - parseTagBadAttr(tag, "android:path", 4000, prefix, suffix) - parseTagBadAttr(tag, "android:pathPrefix", 4000, prefix, suffix) - parseTagBadAttr(tag, "android:pathPattern", 4000, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) + validateTagAttr(tag, "path", R.styleable.AndroidManifestGrantUriPermission_path, 4000) + validateTagAttr(tag, "pathPrefix", + R.styleable.AndroidManifestGrantUriPermission_pathPrefix, 4000) + validateTagAttr(tag, "pathPattern", + R.styleable.AndroidManifestGrantUriPermission_pathPattern, 4000) } - private fun parseBadPathPermissionTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + @Test + fun parsePathPermissionTag() { val tag = "path-permission" - parseTagBadAttr(tag, "android:path", 4000, prefix, suffix) - parseTagBadAttr(tag, "android:pathPrefix", 4000, prefix, suffix) - parseTagBadAttr(tag, "android:pathPattern", 4000, prefix, suffix) - parseTagBadAttr(tag, "android:permission", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:readPermission", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:writePermission", 1024, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) - } - - private fun parseBadMetaDataTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + validateTagAttr(tag, "path", R.styleable.AndroidManifestPathPermission_path, 4000) + validateTagAttr(tag, "pathPrefix", + R.styleable.AndroidManifestPathPermission_pathPrefix, 4000) + validateTagAttr(tag, "pathPattern", + R.styleable.AndroidManifestPathPermission_pathPattern, 4000) + validateTagAttr(tag, "permission", + R.styleable.AndroidManifestPathPermission_permission, 1024) + validateTagAttr(tag, "readPermission", + R.styleable.AndroidManifestPathPermission_readPermission, 1024) + validateTagAttr(tag, "writePermission", + R.styleable.AndroidManifestPathPermission_writePermission, 1024) + } + + @Test + fun parseMetaDataTag() { val tag = "meta-data" - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:value", 32768, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) + validateTagAttr(tag, "name", R.styleable.AndroidManifestMetaData_name, 1024) + validateTagAttr(tag, "value", R.styleable.AndroidManifestMetaData_value, 4000) } - private fun parseBadIntentFilterTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + @Test + fun parseIntentFilterTag() { val tag = "intent-filter" - val newPrefix = "$prefix<$tag>" - val newSuffix = "</$tag>$suffix" - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) - parseBadActionTags(20000, newPrefix, newSuffix, tag) - parseBadCategoryTags(40000, newPrefix, newSuffix, tag) - parseBadDataTags(40000, newPrefix, newSuffix, tag) - } - - private fun parseBadActionTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + validateTagCount("action", 20000, tag) + validateTagCount("category", 40000, tag) + validateTagCount("data", 40000, tag) + } + + @Test + fun parseActionTag() { val tag = "action" - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) + validateTagAttr(tag, "name", R.styleable.AndroidManifestAction_name, 1024) } - private fun parseBadCategoryTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + @Test + fun parseCategoryTag() { val tag = "category" - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) + validateTagAttr(tag, "name", R.styleable.AndroidManifestCategory_name, 1024) } - private fun parseBadDataTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + @Test + fun parseDataTag() { val tag = "data" - parseTagBadAttr(tag, "android:scheme", 256, prefix, suffix) - parseTagBadAttr(tag, "android:host", 256, prefix, suffix) - parseTagBadAttr(tag, "android:path", 4000, prefix, suffix) - parseTagBadAttr(tag, "android:pathPattern", 4000, prefix, suffix) - parseTagBadAttr(tag, "android:pathPrefix", 4000, prefix, suffix) - parseTagBadAttr(tag, "android:pathSuffix", 4000, prefix, suffix) - parseTagBadAttr(tag, "android:pathAdvancedPattern", 4000, prefix, suffix) - parseTagBadAttr(tag, "android:mimeType", 512, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) - } - - private fun parseBadAttributionTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { - val tag = "attribution" - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) + validateTagAttr(tag, "scheme", R.styleable.AndroidManifestData_scheme, 256) + validateTagAttr(tag, "host", R.styleable.AndroidManifestData_host, 256) + validateTagAttr(tag, "path", R.styleable.AndroidManifestData_path, 4000) + validateTagAttr(tag, "pathPattern", R.styleable.AndroidManifestData_pathPattern, 4000) + validateTagAttr(tag, "pathPrefix", R.styleable.AndroidManifestData_pathPrefix, 4000) + validateTagAttr(tag, "pathSuffix", R.styleable.AndroidManifestData_pathSuffix, 4000) + validateTagAttr(tag, "pathAdvancedPattern", + R.styleable.AndroidManifestData_pathAdvancedPattern, 4000) + validateTagAttr(tag, "mimeType", R.styleable.AndroidManifestData_mimeType, 512) } - private fun parseBadUsesFeatureTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + @Test + fun parseUsesFeatureTag() { val tag = "uses-feature" - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) + validateTagAttr(tag, "name", R.styleable.AndroidManifestUsesFeature_name, 1024) } - private fun parseBadPermissionTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + @Test + fun parsePermissionTag() { val tag = "permission" - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseTagBadAttr(tag, "android:permissionGroup", 256, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) + validateTagAttr(tag, "name", R.styleable.AndroidManifestPermission_name, 1024) + validateTagAttr(tag, "permissionGroup", + R.styleable.AndroidManifestPermission_permissionGroup, 256) } - private fun parseBadUsesPermissionTags( - maxNum: Int, - prefix: String, - suffix: String, - parentTag: String - ) { + @Test + fun parseUsesPermissionTag() { val tag = "uses-permission" - parseTagBadAttr(tag, "android:name", 1024, prefix, suffix) - parseBadTagCount(tag, maxNum, parentTag, prefix, suffix) + validateTagAttr(tag, "name", R.styleable.AndroidManifestUsesPermission_name, 1024) + } + + private fun validateTagAttr(tag: String, name: String, index: Int?, maxLen: Int) { + validateTagAttr_shouldPass(tag, name, index, maxLen) + validateTagAttr_shouldFail(tag, name, index, maxLen) } - private fun parseTagBadAttr( - tag: String, - attrName: String, - maxLength: Int, - prefix: String = "", - suffix: String = "" + private fun validateTagAttr_shouldPass( + tag: String, + name: String, + index: Int?, + maxLen: Int ) { - var attrValue = "x".repeat(maxLength) - var tagValue = if (tag.equals("manifest")) "$tag $ns" else tag - var manifestStr = "$prefix<$tagValue $attrName=\"$attrValue\" />$suffix" + val value = "x".repeat(maxLen) + val xml = "<$tag $name=\"$value\" />" + pullParser.setInput(ByteArrayInputStream(xml.toByteArray()), null) + val validator = Validator() + pullParser.nextTag() + validator.validate(pullParser) try { - parseManifestStr(manifestStr) - } catch (e: XmlPullParserException) { - fail("Failed to parse valid <$tag> attribute $attrName with max length of $maxLength:" + + validator.validateStrAttr(pullParser, name, value) + } catch (e: SecurityException) { + fail("Failed to parse valid <$tag> attribute $name with max length of $maxLen:" + " ${e.message}") } - attrValue = "x".repeat(maxLength + 1) - manifestStr = "$prefix<$tagValue $attrName=\"$attrValue\" />$suffix" - val e = assertThrows(XmlPullParserException::class.java) { - parseManifestStr(manifestStr) + if (index != null) { + try { + validator.validateResStrAttr(pullParser, index, value) + } catch (e: SecurityException) { + fail("Failed to parse valid <$tag> resource string attribute $name with max" + + " length of $maxLen: ${e.message}") + } } - assertEquals(expectedAttrLengthErrorMsg(attrName.split(":").last(), tag), e.message) } - private fun parseBadTagCount( - tag: String, - maxNum: Int, - parentTag: String, - prefix: String, - suffix: String + private fun validateTagAttr_shouldFail( + tag: String, + name: String, + index: Int?, + maxLen: Int ) { - var tags = "<$tag />".repeat(maxNum) - var manifestStr = "$prefix$tags$suffix" + val value = "x".repeat(maxLen + 1) + val xml = "<$tag $name=\"$value\" />" + pullParser.setInput(ByteArrayInputStream(xml.toByteArray()), null) + val validator = Validator() + pullParser.nextTag() + validator.validate(pullParser) + val e1 = assertThrows(SecurityException::class.java) { + validator.validateStrAttr(pullParser, name, value) + } + assertEquals(expectedAttrLengthErrorMsg(name, tag), e1.message) + if (index != null) { + val e2 = assertThrows(SecurityException::class.java) { + validator.validateResStrAttr(pullParser, index, value) + } + assertEquals(expectedResAttrLengthErrorMsg(tag), e2.message) + } + } + + private fun validateTagCount(tag: String, maxNum: Int, parentTag: String) { + validateTagCount_shouldPass(tag, maxNum, parentTag) + validateTagCount_shouldFail(tag, maxNum, parentTag) + } + + private fun validateTagCount_shouldPass(tag: String, maxNum: Int, parentTag: String) { + val tags = "<$tag />".repeat(maxNum) + val xml = "<$parentTag>$tags</$parentTag>" try { - parseManifestStr(manifestStr) - } catch (e: XmlPullParserException) { + parseXmlStr(xml) + } catch (e: SecurityException) { fail("Failed to parse <$tag> with max count limit of $maxNum under" + " <$parentTag>: ${e.message}") } - tags = "<$tag />".repeat(maxNum + 1) - manifestStr = "$prefix$tags$suffix" - val e = assertThrows(XmlPullParserException::class.java) { - parseManifestStr(manifestStr) + } + + private fun validateTagCount_shouldFail(tag: String, maxNum: Int, parentTag: String) { + val tags = "<$tag />".repeat(maxNum + 1) + val xml = "<$parentTag>$tags</$parentTag>" + val e = assertThrows(SecurityException::class.java) { + parseXmlStr(xml) } assertEquals(expectedCountErrorMsg(tag, parentTag), e.message) } @@ -624,13 +446,12 @@ class AndroidPackageParsingValidationTest { @Test fun parseUnexpectedTag_shouldSkip() { val host = "x".repeat(256) - val dataTags = "<data android:host=\"$host\" />".repeat(2049) - val ns = "http://schemas.android.com/apk/res/android" - val manifestStr = "<manifest xmlns:android=\"$ns\" package=\"test\">$dataTags</manifest>" - parseManifestStr(manifestStr) + val dataTags = "<data host=\"$host\" />".repeat(2049) + val xml = "<manifest package=\"test\">$dataTags</manifest>" + parseXmlStr(xml) } - fun parseManifestStr(manifestStr: String) { + fun parseXmlStr(manifestStr: String) { pullParser.setInput(ByteArrayInputStream(manifestStr.toByteArray()), null) val validator = Validator() do { @@ -647,39 +468,4 @@ class AndroidPackageParsingValidationTest { fun expectedResAttrLengthErrorMsg(tag: String) = "String length limit exceeded for attribute in $tag" - - @Test - fun validateResAttrs() { - pullParser.setInput(ByteArrayInputStream("<manifest />".toByteArray()), null) - pullParser.next() - val validator = Validator() - validator.validate(pullParser) - validateResAttr(pullParser, validator, R.styleable.AndroidManifestData_host, - "R.styleable.AndroidManifestData_host", 255) - validateResAttr(pullParser, validator, R.styleable.AndroidManifestData_port, - "R.styleable.AndroidManifestData_port", 255) - validateResAttr(pullParser, validator, R.styleable.AndroidManifestData_scheme, - "R.styleable.AndroidManifestData_scheme", 255) - validateResAttr(pullParser, validator, R.styleable.AndroidManifestData_mimeType, - "R.styleable.AndroidManifestData_mimeType", 512) - } - - fun validateResAttr( - parser: XmlPullParser, - validator: Validator, - resId: Int, - resIdStr: String, - maxLength: Int - ) { - try { - validator.validateAttr(parser, resId, "x".repeat(maxLength)) - } catch (e: XmlPullParserException) { - fail("Failed to parse valid string resource attribute $resIdStr with max length of" + - " $maxLength: ${e.message}") - } - val e = assertThrows(XmlPullParserException::class.java) { - validator.validateAttr(parser, resId, "x".repeat(maxLength + 1)) - } - assertEquals(expectedResAttrLengthErrorMsg("manifest"), e.message) - } -}
\ No newline at end of file +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index f1e26d2c7083..6b225fc945d5 100755 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -405,6 +405,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { UriGrantsManagerInternal mUgmInternal; @Mock AppOpsManager mAppOpsManager; + private AppOpsManager.OnOpChangedListener mOnPermissionChangeListener; @Mock private TestableNotificationManagerService.NotificationAssistantAccessGrantedCallback mNotificationAssistantAccessGrantedCallback; @@ -605,6 +606,13 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { tr.addOverride(com.android.internal.R.string.config_defaultSearchSelectorPackageName, SEARCH_SELECTOR_PKG); + doAnswer(invocation -> { + mOnPermissionChangeListener = invocation.getArgument(2); + return null; + }).when(mAppOpsManager).startWatchingMode(eq(AppOpsManager.OP_POST_NOTIFICATION), any(), + any()); + when(mUmInternal.isUserInitialized(anyInt())).thenReturn(true); + mWorkerHandler = spy(mService.new WorkerHandler(mTestableLooper.getLooper())); mService.init(mWorkerHandler, mRankingHandler, mPackageManager, mPackageManagerClient, mockLightsManager, mListeners, mAssistants, mConditionProviders, mCompanionMgr, @@ -697,6 +705,11 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mTestFlagResolver.setFlagOverride(FSI_FORCE_DEMOTE, false); mTestFlagResolver.setFlagOverride(SHOW_STICKY_HUN_FOR_DENIED_FSI, false); + + var checker = mock(TestableNotificationManagerService.ComponentPermissionChecker.class); + mService.permissionChecker = checker; + when(checker.check(anyString(), anyInt(), anyInt(), anyBoolean())) + .thenReturn(PackageManager.PERMISSION_DENIED); } @After @@ -3221,6 +3234,108 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void onOpChanged_permissionRevoked_cancelsAllNotificationsFromPackage() + throws RemoteException { + // Have preexisting posted notifications from revoked package and other packages. + mService.addNotification(new NotificationRecord(mContext, + generateSbn("revoked", 1001, 1, 0), mTestNotificationChannel)); + mService.addNotification(new NotificationRecord(mContext, + generateSbn("other", 1002, 2, 0), mTestNotificationChannel)); + // Have preexisting enqueued notifications from revoked package and other packages. + mService.addEnqueuedNotification(new NotificationRecord(mContext, + generateSbn("revoked", 1001, 3, 0), mTestNotificationChannel)); + mService.addEnqueuedNotification(new NotificationRecord(mContext, + generateSbn("other", 1002, 4, 0), mTestNotificationChannel)); + assertThat(mService.mNotificationList).hasSize(2); + assertThat(mService.mEnqueuedNotifications).hasSize(2); + + when(mPackageManagerInternal.getPackageUid("revoked", 0, 0)).thenReturn(1001); + when(mPermissionHelper.hasPermission(eq(1001))).thenReturn(false); + + mOnPermissionChangeListener.onOpChanged( + AppOpsManager.OPSTR_POST_NOTIFICATION, "revoked", 0); + waitForIdle(); + + assertThat(mService.mNotificationList).hasSize(1); + assertThat(mService.mNotificationList.get(0).getSbn().getPackageName()).isEqualTo("other"); + assertThat(mService.mEnqueuedNotifications).hasSize(1); + assertThat(mService.mEnqueuedNotifications.get(0).getSbn().getPackageName()).isEqualTo( + "other"); + } + + @Test + public void onOpChanged_permissionStillGranted_notificationsAreNotAffected() + throws RemoteException { + // NOTE: This combination (receiving the onOpChanged broadcast for a package, the permission + // being now granted, AND having previously posted notifications from said package) should + // never happen (if we trust the broadcasts are correct). So this test is for a what-if + // scenario, to verify we still handle it reasonably. + + // Have preexisting posted notifications from specific package and other packages. + mService.addNotification(new NotificationRecord(mContext, + generateSbn("granted", 1001, 1, 0), mTestNotificationChannel)); + mService.addNotification(new NotificationRecord(mContext, + generateSbn("other", 1002, 2, 0), mTestNotificationChannel)); + // Have preexisting enqueued notifications from specific package and other packages. + mService.addEnqueuedNotification(new NotificationRecord(mContext, + generateSbn("granted", 1001, 3, 0), mTestNotificationChannel)); + mService.addEnqueuedNotification(new NotificationRecord(mContext, + generateSbn("other", 1002, 4, 0), mTestNotificationChannel)); + assertThat(mService.mNotificationList).hasSize(2); + assertThat(mService.mEnqueuedNotifications).hasSize(2); + + when(mPackageManagerInternal.getPackageUid("granted", 0, 0)).thenReturn(1001); + when(mPermissionHelper.hasPermission(eq(1001))).thenReturn(true); + + mOnPermissionChangeListener.onOpChanged( + AppOpsManager.OPSTR_POST_NOTIFICATION, "granted", 0); + waitForIdle(); + + assertThat(mService.mNotificationList).hasSize(2); + assertThat(mService.mEnqueuedNotifications).hasSize(2); + } + + @Test + public void onOpChanged_notInitializedUser_ignored() throws RemoteException { + when(mUmInternal.isUserInitialized(eq(0))).thenReturn(false); + + mOnPermissionChangeListener.onOpChanged( + AppOpsManager.OPSTR_POST_NOTIFICATION, "package", 0); + waitForIdle(); + + // We early-exited and didn't even query PM for package details. + verify(mPackageManagerInternal, never()).getPackageUid(any(), anyLong(), anyInt()); + } + + @Test + public void setNotificationsEnabledForPackage_disabling_clearsNotifications() throws Exception { + mService.addNotification(new NotificationRecord(mContext, + generateSbn("package", 1001, 1, 0), mTestNotificationChannel)); + assertThat(mService.mNotificationList).hasSize(1); + when(mPackageManagerInternal.getPackageUid("package", 0, 0)).thenReturn(1001); + when(mPermissionHelper.hasRequestedPermission(any(), eq("package"), anyInt())).thenReturn( + true); + + // Start with granted permission and simulate effect of revoking it. + when(mPermissionHelper.hasPermission(1001)).thenReturn(true); + doAnswer(invocation -> { + when(mPermissionHelper.hasPermission(1001)).thenReturn(false); + mOnPermissionChangeListener.onOpChanged( + AppOpsManager.OPSTR_POST_NOTIFICATION, "package", 0); + return null; + }).when(mPermissionHelper).setNotificationPermission("package", 0, false, true); + + mBinderService.setNotificationsEnabledForPackage("package", 1001, false); + waitForIdle(); + + assertThat(mService.mNotificationList).hasSize(0); + + mTestableLooper.moveTimeForward(500); + waitForIdle(); + verify(mContext).sendBroadcastAsUser(any(), eq(UserHandle.of(0)), eq(null)); + } + + @Test public void testUpdateAppNotifyCreatorBlock() throws Exception { when(mPermissionHelper.hasPermission(mUid)).thenReturn(true); @@ -4240,6 +4355,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testSetNASMigrationDoneAndResetDefault_enableNAS() throws Exception { int userId = 10; + setNASMigrationDone(false, userId); when(mUm.getProfileIds(userId, false)).thenReturn(new int[]{userId}); mBinderService.setNASMigrationDoneAndResetDefault(userId, true); @@ -4251,6 +4367,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void testSetNASMigrationDoneAndResetDefault_disableNAS() throws Exception { int userId = 10; + setNASMigrationDone(false, userId); when(mUm.getProfileIds(userId, false)).thenReturn(new int[]{userId}); mBinderService.setNASMigrationDoneAndResetDefault(userId, false); @@ -4263,6 +4380,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testSetNASMigrationDoneAndResetDefault_multiProfile() throws Exception { int userId1 = 11; int userId2 = 12; //work profile + setNASMigrationDone(false, userId1); + setNASMigrationDone(false, userId2); setUsers(new int[]{userId1, userId2}); when(mUm.isManagedProfile(userId2)).thenReturn(true); when(mUm.getProfileIds(userId1, false)).thenReturn(new int[]{userId1, userId2}); @@ -4276,6 +4395,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testSetNASMigrationDoneAndResetDefault_multiUser() throws Exception { int userId1 = 11; int userId2 = 12; + setNASMigrationDone(false, userId1); + setNASMigrationDone(false, userId2); setUsers(new int[]{userId1, userId2}); when(mUm.getProfileIds(userId1, false)).thenReturn(new int[]{userId1}); when(mUm.getProfileIds(userId2, false)).thenReturn(new int[]{userId2}); @@ -12168,6 +12289,130 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { inOrder.verifyNoMoreInteractions(); } + @Test + public void isNotificationPolicyAccessGranted_invalidPackage() throws Exception { + final String notReal = "NOT REAL"; + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(notReal), anyInt())).thenThrow( + PackageManager.NameNotFoundException.class); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(notReal)).isFalse(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(notReal), anyInt()); + verify(checker, never()).check(any(), anyInt(), anyInt(), anyBoolean()); + verify(mConditionProviders, never()).isPackageOrComponentAllowed(eq(notReal), anyInt()); + verify(mListeners, never()).isComponentEnabledForPackage(any()); + verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt()); + } + + @Test + public void isNotificationPolicyAccessGranted_hasPermission() throws Exception { + final String packageName = "target"; + final int uid = 123; + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid); + when(checker.check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true)) + .thenReturn(PackageManager.PERMISSION_GRANTED); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt()); + verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true); + verify(mConditionProviders, never()).isPackageOrComponentAllowed(eq(packageName), anyInt()); + verify(mListeners, never()).isComponentEnabledForPackage(any()); + verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt()); + } + + @Test + public void isNotificationPolicyAccessGranted_isPackageAllowed() throws Exception { + final String packageName = "target"; + final int uid = 123; + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid); + when(mConditionProviders.isPackageOrComponentAllowed(eq(packageName), anyInt())) + .thenReturn(true); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt()); + verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true); + verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt()); + verify(mListeners, never()).isComponentEnabledForPackage(any()); + verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt()); + } + + @Test + public void isNotificationPolicyAccessGranted_isComponentEnabled() throws Exception { + final String packageName = "target"; + final int uid = 123; + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid); + when(mListeners.isComponentEnabledForPackage(packageName)).thenReturn(true); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt()); + verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true); + verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt()); + verify(mListeners).isComponentEnabledForPackage(packageName); + verify(mDevicePolicyManager, never()).isActiveDeviceOwner(anyInt()); + } + + @Test + public void isNotificationPolicyAccessGranted_isDeviceOwner() throws Exception { + final String packageName = "target"; + final int uid = 123; + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid); + when(mDevicePolicyManager.isActiveDeviceOwner(uid)).thenReturn(true); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isTrue(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt()); + verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true); + verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt()); + verify(mListeners).isComponentEnabledForPackage(packageName); + verify(mDevicePolicyManager).isActiveDeviceOwner(uid); + } + + /** + * b/292163859 + */ + @Test + public void isNotificationPolicyAccessGranted_callerIsDeviceOwner() throws Exception { + final String packageName = "target"; + final int uid = 123; + final int callingUid = Binder.getCallingUid(); + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid); + when(mDevicePolicyManager.isActiveDeviceOwner(callingUid)).thenReturn(true); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isFalse(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt()); + verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true); + verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt()); + verify(mListeners).isComponentEnabledForPackage(packageName); + verify(mDevicePolicyManager).isActiveDeviceOwner(uid); + verify(mDevicePolicyManager, never()).isActiveDeviceOwner(callingUid); + } + + @Test + public void isNotificationPolicyAccessGranted_notGranted() throws Exception { + final String packageName = "target"; + final int uid = 123; + final var checker = mService.permissionChecker; + + when(mPackageManagerClient.getPackageUidAsUser(eq(packageName), anyInt())).thenReturn(uid); + + assertThat(mBinderService.isNotificationPolicyAccessGranted(packageName)).isFalse(); + verify(mPackageManagerClient).getPackageUidAsUser(eq(packageName), anyInt()); + verify(checker).check(android.Manifest.permission.MANAGE_NOTIFICATIONS, uid, -1, true); + verify(mConditionProviders).isPackageOrComponentAllowed(eq(packageName), anyInt()); + verify(mListeners).isComponentEnabledForPackage(packageName); + verify(mDevicePolicyManager).isActiveDeviceOwner(uid); + } + private static <T extends Parcelable> T parcelAndUnparcel(T source, Parcelable.Creator<T> creator) { Parcel parcel = Parcel.obtain(); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java index 9f4eee7e332f..27e8f3664a65 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/TestableNotificationManagerService.java @@ -43,6 +43,8 @@ public class TestableNotificationManagerService extends NotificationManagerServi @Nullable Boolean mIsVisibleToListenerReturnValue = null; + ComponentPermissionChecker permissionChecker; + TestableNotificationManagerService(Context context, NotificationRecordLogger logger, InstanceIdSequence notificationInstanceIdSequence) { super(context, logger, notificationInstanceIdSequence); @@ -150,6 +152,12 @@ public class TestableNotificationManagerService extends NotificationManagerServi return super.isVisibleToListener(sbn, notificationType, listener); } + @Override + protected int checkComponentPermission(String permission, int uid, int owningUid, + boolean exported) { + return permissionChecker.check(permission, uid, owningUid, exported); + } + public class StrongAuthTrackerFake extends NotificationManagerService.StrongAuthTracker { private int mGetStrongAuthForUserReturnValue = 0; StrongAuthTrackerFake(Context context) { @@ -165,4 +173,8 @@ public class TestableNotificationManagerService extends NotificationManagerServi return mGetStrongAuthForUserReturnValue; } } + + public interface ComponentPermissionChecker { + int check(String permission, int uid, int owningUid, boolean exported); + } } diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java index c6cd07844ffe..4e3a893954a2 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java @@ -509,7 +509,7 @@ public class VibratorManagerServiceTest { verify(listeners[0]).onVibrating(eq(true)); verify(listeners[1]).onVibrating(eq(true)); verify(listeners[2], never()).onVibrating(eq(true)); - cancelVibrate(service); + cancelVibrate(service); // Clean up long-ish effect. } @Test @@ -1333,6 +1333,8 @@ public class VibratorManagerServiceTest { // Alarm vibration will be scaled with SCALE_NONE. assertEquals(1f, ((PrimitiveSegment) fakeVibrator.getAllEffectSegments().get(2)).getScale(), 1e-5); + + cancelVibrate(service); // Clean up long-ish effect. } @Test @@ -1368,7 +1370,7 @@ public class VibratorManagerServiceTest { // Vibration is not stopped nearly after updating service. assertFalse(waitUntil(s -> !s.isVibrating(1), service, 50)); - cancelVibrate(service); + cancelVibrate(service); // Clean up long effect. } @Test diff --git a/services/tests/wmtests/AndroidManifest.xml b/services/tests/wmtests/AndroidManifest.xml index 554b0f408ef9..85d0473ca5ca 100644 --- a/services/tests/wmtests/AndroidManifest.xml +++ b/services/tests/wmtests/AndroidManifest.xml @@ -90,7 +90,7 @@ <activity android:name="com.android.server.wm.SurfaceControlViewHostTests$TestActivity" /> - <activity android:name="android.server.wm.scvh.SurfaceSyncGroupActivity" + <activity android:name="com.android.server.wm.SurfaceSyncGroupTests$TestActivity" android:screenOrientation="locked" android:turnScreenOn="true" android:theme="@style/WhiteBackgroundTheme" diff --git a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java index f235d153c658..233a2076a867 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java @@ -52,7 +52,8 @@ public class DimmerTests extends WindowTestsBase { private static class TestWindowContainer extends WindowContainer<TestWindowContainer> { final SurfaceControl mControl = mock(SurfaceControl.class); - final SurfaceControl.Transaction mTransaction = spy(StubTransaction.class); + final SurfaceControl.Transaction mPendingTransaction = spy(StubTransaction.class); + final SurfaceControl.Transaction mSyncTransaction = spy(StubTransaction.class); TestWindowContainer(WindowManagerService wm) { super(wm); @@ -65,12 +66,12 @@ public class DimmerTests extends WindowTestsBase { @Override public SurfaceControl.Transaction getSyncTransaction() { - return mTransaction; + return mSyncTransaction; } @Override public SurfaceControl.Transaction getPendingTransaction() { - return mTransaction; + return mPendingTransaction; } } @@ -144,7 +145,7 @@ public class DimmerTests extends WindowTestsBase { mHost.addChild(child, 0); final float alpha = 0.8f; - mDimmer.dimAbove(mTransaction, child, alpha); + mDimmer.dimAbove(child, alpha); int width = 100; int height = 300; @@ -161,13 +162,13 @@ public class DimmerTests extends WindowTestsBase { mHost.addChild(child, 0); final float alpha = 0.8f; - mDimmer.dimAbove(mTransaction, child, alpha); + mDimmer.dimAbove(child, alpha); SurfaceControl dimLayer = getDimLayer(); assertNotNull("Dimmer should have created a surface", dimLayer); - verify(mTransaction).setAlpha(dimLayer, alpha); - verify(mTransaction).setRelativeLayer(dimLayer, child.mControl, 1); + verify(mHost.getPendingTransaction()).setAlpha(dimLayer, alpha); + verify(mHost.getPendingTransaction()).setRelativeLayer(dimLayer, child.mControl, 1); } @Test @@ -176,13 +177,13 @@ public class DimmerTests extends WindowTestsBase { mHost.addChild(child, 0); final float alpha = 0.8f; - mDimmer.dimBelow(mTransaction, child, alpha, 0); + mDimmer.dimBelow(child, alpha, 0); SurfaceControl dimLayer = getDimLayer(); assertNotNull("Dimmer should have created a surface", dimLayer); - verify(mTransaction).setAlpha(dimLayer, alpha); - verify(mTransaction).setRelativeLayer(dimLayer, child.mControl, -1); + verify(mHost.getPendingTransaction()).setAlpha(dimLayer, alpha); + verify(mHost.getPendingTransaction()).setRelativeLayer(dimLayer, child.mControl, -1); } @Test @@ -191,7 +192,7 @@ public class DimmerTests extends WindowTestsBase { mHost.addChild(child, 0); final float alpha = 0.8f; - mDimmer.dimAbove(mTransaction, child, alpha); + mDimmer.dimAbove(child, alpha); SurfaceControl dimLayer = getDimLayer(); mDimmer.resetDimStates(); @@ -208,10 +209,10 @@ public class DimmerTests extends WindowTestsBase { mHost.addChild(child, 0); final float alpha = 0.8f; - mDimmer.dimAbove(mTransaction, child, alpha); + mDimmer.dimAbove(child, alpha); SurfaceControl dimLayer = getDimLayer(); mDimmer.resetDimStates(); - mDimmer.dimAbove(mTransaction, child, alpha); + mDimmer.dimAbove(child, alpha); mDimmer.updateDims(mTransaction); verify(mTransaction).show(dimLayer); @@ -224,7 +225,7 @@ public class DimmerTests extends WindowTestsBase { mHost.addChild(child, 0); final float alpha = 0.8f; - mDimmer.dimAbove(mTransaction, child, alpha); + mDimmer.dimAbove(child, alpha); final Rect bounds = mDimmer.mDimState.mDimBounds; SurfaceControl dimLayer = getDimLayer(); @@ -245,7 +246,7 @@ public class DimmerTests extends WindowTestsBase { TestWindowContainer child = new TestWindowContainer(mWm); mHost.addChild(child, 0); - mDimmer.dimAbove(mTransaction, child, 1); + mDimmer.dimAbove(child, 1); SurfaceControl dimLayer = getDimLayer(); mDimmer.updateDims(mTransaction); verify(mTransaction, times(1)).show(dimLayer); @@ -266,13 +267,13 @@ public class DimmerTests extends WindowTestsBase { mHost.addChild(child, 0); final int blurRadius = 50; - mDimmer.dimBelow(mTransaction, child, 0, blurRadius); + mDimmer.dimBelow(child, 0, blurRadius); SurfaceControl dimLayer = getDimLayer(); assertNotNull("Dimmer should have created a surface", dimLayer); - verify(mTransaction).setBackgroundBlurRadius(dimLayer, blurRadius); - verify(mTransaction).setRelativeLayer(dimLayer, child.mControl, -1); + verify(mHost.getPendingTransaction()).setBackgroundBlurRadius(dimLayer, blurRadius); + verify(mHost.getPendingTransaction()).setRelativeLayer(dimLayer, child.mControl, -1); } private SurfaceControl getDimLayer() { diff --git a/services/tests/wmtests/src/com/android/server/wm/InputMethodDialogWindowContextTest.java b/services/tests/wmtests/src/com/android/server/wm/InputMethodDialogWindowContextTest.java index 586bb0f08c1f..cac32622cb5f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InputMethodDialogWindowContextTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InputMethodDialogWindowContextTest.java @@ -36,6 +36,7 @@ import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import android.app.ActivityThread; +import android.app.IApplicationThread; import android.content.Context; import android.content.res.Resources; import android.graphics.Rect; @@ -91,10 +92,12 @@ public class InputMethodDialogWindowContextTest extends WindowTestsBase { spyOn(mIWindowManager); doAnswer(invocation -> { Object[] args = invocation.getArguments(); + IApplicationThread appThread = (IApplicationThread) args[0]; IBinder clientToken = (IBinder) args[1]; int displayId = (int) args[3]; DisplayContent dc = mWm.mRoot.getDisplayContent(displayId); - mWm.mWindowContextListenerController.registerWindowContainerListener(clientToken, + final WindowProcessController wpc = mAtm.getProcessController(appThread); + mWm.mWindowContextListenerController.registerWindowContainerListener(wpc, clientToken, dc.getImeContainer(), 1000 /* ownerUid */, TYPE_INPUT_METHOD_DIALOG, null /* options */); return dc.getImeContainer().getConfiguration(); diff --git a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTests.java b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTests.java index 89d7252b72a0..abaa77632c23 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SurfaceSyncGroupTests.java @@ -23,22 +23,27 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import android.app.Activity; import android.app.Instrumentation; +import android.app.KeyguardManager; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.PixelFormat; import android.graphics.Rect; +import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.platform.test.annotations.Presubmit; -import android.server.wm.scvh.SurfaceSyncGroupActivity; import android.view.SurfaceControl; import android.view.View; +import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.WindowManager; import android.view.cts.surfacevalidator.BitmapPixelChecker; +import android.widget.FrameLayout; import android.window.SurfaceSyncGroup; +import androidx.annotation.Nullable; import androidx.test.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; @@ -56,10 +61,10 @@ public class SurfaceSyncGroupTests { private static final long TIMEOUT_S = HW_TIMEOUT_MULTIPLIER * 5L; @Rule - public ActivityTestRule<SurfaceSyncGroupActivity> mActivityRule = new ActivityTestRule<>( - SurfaceSyncGroupActivity.class); + public ActivityTestRule<TestActivity> mActivityRule = new ActivityTestRule<>( + TestActivity.class); - private SurfaceSyncGroupActivity mActivity; + private TestActivity mActivity; Instrumentation mInstrumentation; @@ -187,4 +192,24 @@ public class SurfaceSyncGroupTests { swBitmap.recycle(); } + + public static class TestActivity extends Activity { + private ViewGroup mParentView; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + + mParentView = new FrameLayout(this); + setContentView(mParentView); + + KeyguardManager km = getSystemService(KeyguardManager.class); + km.requestDismissKeyguard(this, null); + } + + public View getBackgroundView() { + return mParentView; + } + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java index e252b9e1c8be..be436bfc8bd9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java +++ b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java @@ -43,6 +43,7 @@ import static org.mockito.Mockito.when; import static org.mockito.Mockito.withSettings; import android.app.ActivityManagerInternal; +import android.app.ActivityThread; import android.app.AppOpsManager; import android.app.IApplicationThread; import android.app.usage.UsageStatsManagerInternal; @@ -321,6 +322,10 @@ public class SystemServicesTestRule implements TestRule { mock(ActivityManagerService.class, withSettings().stubOnly()); mAtmService = new TestActivityTaskManagerService(mContext, amService); LocalServices.addService(ActivityTaskManagerInternal.class, mAtmService.getAtmInternal()); + // Create a fake WindowProcessController for the system process. + final WindowProcessController wpc = + addProcess("android", "system", 1485 /* pid */, 1000 /* uid */); + wpc.setThread(ActivityThread.currentActivityThread().getApplicationThread()); } private void setUpWindowManagerService() { diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index 9d597b11120d..6a9bb6c85c70 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -653,4 +653,23 @@ public class TaskFragmentTest extends WindowTestsBase { assertEquals(mDisplayContent.getImeContainer().getParent().getSurfaceControl(), mDisplayContent.computeImeParent()); } + + @Test + public void testIsolatedNavigation() { + final Task task = createTask(mDisplayContent); + final TaskFragment tf0 = new TaskFragmentBuilder(mAtm) + .setParentTask(task) + .createActivityCount(1) + .setOrganizer(mOrganizer) + .setFragmentToken(new Binder()) + .build(); + + // Cannot be isolated if not embedded. + task.setIsolatedNav(true); + assertFalse(task.isIsolatedNav()); + + // Ensure the TaskFragment is isolated once set. + tf0.setIsolatedNav(true); + assertTrue(tf0.isIsolatedNav()); + } } diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 5154d17f2e6b..3ce10cf5656b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -1244,6 +1244,27 @@ public class TransitionTests extends WindowTestsBase { } @Test + public void testFinishRotationControllerWithFixedRotation() { + final ActivityRecord app = new ActivityBuilder(mAtm).setCreateTask(true).build(); + mDisplayContent.setFixedRotationLaunchingAppUnchecked(app); + registerTestTransitionPlayer(); + mDisplayContent.setLastHasContent(); + mDisplayContent.requestChangeTransitionIfNeeded(1 /* changes */, null /* displayChange */); + assertNotNull(mDisplayContent.getAsyncRotationController()); + mDisplayContent.setFixedRotationLaunchingAppUnchecked(null); + assertNull("Clear rotation controller if rotation is not changed", + mDisplayContent.getAsyncRotationController()); + + mDisplayContent.setFixedRotationLaunchingAppUnchecked(app); + assertNotNull(mDisplayContent.getAsyncRotationController()); + mDisplayContent.getDisplayRotation().setRotation( + mDisplayContent.getWindowConfiguration().getRotation() + 1); + mDisplayContent.setFixedRotationLaunchingAppUnchecked(null); + assertNotNull("Keep rotation controller if rotation will be changed", + mDisplayContent.getAsyncRotationController()); + } + + @Test public void testDeferRotationForTransientLaunch() { final TestTransitionPlayer player = registerTestTransitionPlayer(); assumeFalse(mDisplayContent.mTransitionController.useShellTransitionsRotation()); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContextListenerControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContextListenerControllerTests.java index f6d0bf110047..fa42e26a883e 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContextListenerControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowContextListenerControllerTests.java @@ -37,6 +37,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.MockitoAnnotations.initMocks; import android.app.IWindowToken; import android.content.res.Configuration; @@ -53,6 +54,7 @@ import androidx.test.filters.SmallTest; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; import org.mockito.Mockito; /** @@ -68,11 +70,14 @@ public class WindowContextListenerControllerTests extends WindowTestsBase { private static final int TEST_UID = 12345; private static final int ANOTHER_UID = 1000; + @Mock + private WindowProcessController mWpc; private final IBinder mClientToken = new Binder(); private WindowContainer<?> mContainer; @Before public void setUp() { + initMocks(this); mController = new WindowContextListenerController(); mContainer = createTestWindowToken(TYPE_APPLICATION_OVERLAY, mDisplayContent); // Make display on to verify configuration propagation. @@ -82,20 +87,20 @@ public class WindowContextListenerControllerTests extends WindowTestsBase { @Test public void testRegisterWindowContextListener() { - mController.registerWindowContainerListener(mClientToken, mContainer, -1, + mController.registerWindowContainerListener(mWpc, mClientToken, mContainer, -1, TYPE_APPLICATION_OVERLAY, null /* options */); assertEquals(1, mController.mListeners.size()); final IBinder clientToken = mock(IBinder.class); - mController.registerWindowContainerListener(clientToken, mContainer, -1, + mController.registerWindowContainerListener(mWpc, clientToken, mContainer, -1, TYPE_APPLICATION_OVERLAY, null /* options */); assertEquals(2, mController.mListeners.size()); final WindowContainer<?> container = createTestWindowToken(TYPE_APPLICATION_OVERLAY, mDefaultDisplay); - mController.registerWindowContainerListener(mClientToken, container, -1, + mController.registerWindowContainerListener(mWpc, mClientToken, container, -1, TYPE_APPLICATION_OVERLAY, null /* options */); // The number of listeners doesn't increase since the listener just gets updated. @@ -121,7 +126,7 @@ public class WindowContextListenerControllerTests extends WindowTestsBase { config1.densityDpi = 100; mContainer.onRequestedOverrideConfigurationChanged(config1); - mController.registerWindowContainerListener(clientToken, mContainer, -1, + mController.registerWindowContainerListener(mWpc, clientToken, mContainer, -1, TYPE_APPLICATION_OVERLAY, null /* options */); assertEquals(bounds1, clientToken.mConfiguration.windowConfiguration.getBounds()); @@ -137,7 +142,7 @@ public class WindowContextListenerControllerTests extends WindowTestsBase { config2.densityDpi = 200; container.onRequestedOverrideConfigurationChanged(config2); - mController.registerWindowContainerListener(clientToken, container, -1, + mController.registerWindowContainerListener(mWpc, clientToken, container, -1, TYPE_APPLICATION_OVERLAY, null /* options */); assertEquals(bounds2, clientToken.mConfiguration.windowConfiguration.getBounds()); @@ -164,7 +169,7 @@ public class WindowContextListenerControllerTests extends WindowTestsBase { @Test public void testAssertCallerCanModifyListener_CanManageAppTokens_ReturnTrue() { - mController.registerWindowContainerListener(mClientToken, mContainer, TEST_UID, + mController.registerWindowContainerListener(mWpc, mClientToken, mContainer, TEST_UID, TYPE_APPLICATION_OVERLAY, null /* options */); assertTrue(mController.assertCallerCanModifyListener(mClientToken, @@ -173,7 +178,7 @@ public class WindowContextListenerControllerTests extends WindowTestsBase { @Test public void testAssertCallerCanModifyListener_SameUid_ReturnTrue() { - mController.registerWindowContainerListener(mClientToken, mContainer, TEST_UID, + mController.registerWindowContainerListener(mWpc, mClientToken, mContainer, TEST_UID, TYPE_APPLICATION_OVERLAY, null /* options */); assertTrue(mController.assertCallerCanModifyListener(mClientToken, @@ -182,7 +187,7 @@ public class WindowContextListenerControllerTests extends WindowTestsBase { @Test(expected = UnsupportedOperationException.class) public void testAssertCallerCanModifyListener_DifferentUid_ThrowException() { - mController.registerWindowContainerListener(mClientToken, mContainer, TEST_UID, + mController.registerWindowContainerListener(mWpc, mClientToken, mContainer, TEST_UID, TYPE_APPLICATION_OVERLAY, null /* options */); mController.assertCallerCanModifyListener(mClientToken, @@ -198,7 +203,7 @@ public class WindowContextListenerControllerTests extends WindowTestsBase { .build(); final DisplayArea<?> da = windowContextCreatedToken.getDisplayArea(); - mController.registerWindowContainerListener(mClientToken, windowContextCreatedToken, + mController.registerWindowContainerListener(mWpc, mClientToken, windowContextCreatedToken, TEST_UID, TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, null /* options */); assertThat(mController.getContainer(mClientToken)).isEqualTo(windowContextCreatedToken); @@ -233,7 +238,7 @@ public class WindowContextListenerControllerTests extends WindowTestsBase { .setDisplayContent(dualDisplayContent) .setFromClientToken(true) .build(); - mController.registerWindowContainerListener(mClientToken, windowContextCreatedToken, + mController.registerWindowContainerListener(mWpc, mClientToken, windowContextCreatedToken, TEST_UID, TYPE_INPUT_METHOD_DIALOG, null /* options */); assertThat(mController.getContainer(mClientToken)).isEqualTo(windowContextCreatedToken); @@ -258,7 +263,7 @@ public class WindowContextListenerControllerTests extends WindowTestsBase { config1.densityDpi = 100; mContainer.onRequestedOverrideConfigurationChanged(config1); - mController.registerWindowContainerListener(mockToken, mContainer, -1, + mController.registerWindowContainerListener(mWpc, mockToken, mContainer, -1, TYPE_APPLICATION_OVERLAY, null /* options */); verify(mockToken, never()).onConfigurationChanged(any(), anyInt()); @@ -293,7 +298,7 @@ public class WindowContextListenerControllerTests extends WindowTestsBase { config1.densityDpi = 100; mContainer.onRequestedOverrideConfigurationChanged(config1); - mController.registerWindowContainerListener(clientToken, mContainer, -1, + mController.registerWindowContainerListener(mWpc, clientToken, mContainer, -1, TYPE_APPLICATION_OVERLAY, options); assertThat(clientToken.mConfiguration).isEqualTo(config1); 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 1fa4134eacfe..495a80ba8ca5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -473,7 +473,7 @@ public class WindowManagerServiceTests extends WindowTestsBase { mWm.attachWindowContextToWindowToken(mAppThread, new Binder(), windowToken.token); verify(mWm.mWindowContextListenerController, never()).registerWindowContainerListener( - any(), any(), anyInt(), anyInt(), any()); + any(), any(), any(), anyInt(), anyInt(), any()); } @Test @@ -488,8 +488,8 @@ public class WindowManagerServiceTests extends WindowTestsBase { final IBinder clientToken = new Binder(); mWm.attachWindowContextToWindowToken(mAppThread, clientToken, windowToken.token); - - verify(mWm.mWindowContextListenerController).registerWindowContainerListener( + final WindowProcessController wpc = mAtm.getProcessController(mAppThread); + verify(mWm.mWindowContextListenerController).registerWindowContainerListener(eq(wpc), eq(clientToken), eq(windowToken), anyInt(), eq(TYPE_INPUT_METHOD), eq(windowToken.mOptions)); } @@ -519,7 +519,7 @@ public class WindowManagerServiceTests extends WindowTestsBase { new InsetsSourceControl.Array(), new Rect(), new float[1]); verify(mWm.mWindowContextListenerController, never()).registerWindowContainerListener(any(), - any(), anyInt(), anyInt(), any()); + any(), any(), anyInt(), anyInt(), any()); } @Test diff --git a/services/usage/java/com/android/server/usage/BroadcastResponseStatsTracker.java b/services/usage/java/com/android/server/usage/BroadcastResponseStatsTracker.java index c47d459f20c4..d9cbea95e563 100644 --- a/services/usage/java/com/android/server/usage/BroadcastResponseStatsTracker.java +++ b/services/usage/java/com/android/server/usage/BroadcastResponseStatsTracker.java @@ -292,6 +292,19 @@ class BroadcastResponseStatsTracker { } } + boolean isPackageExemptedFromBroadcastResponseStats(@NonNull String packageName, + @NonNull UserHandle user) { + synchronized (mLock) { + if (doesPackageHoldExemptedPermission(packageName, user)) { + return true; + } + if (doesPackageHoldExemptedRole(packageName, user)) { + return true; + } + return false; + } + } + boolean doesPackageHoldExemptedRole(@NonNull String packageName, @NonNull UserHandle user) { final List<String> exemptedRoles = mAppStandby.getBroadcastResponseExemptedRoles(); synchronized (mLock) { diff --git a/services/usage/java/com/android/server/usage/UsageStatsService.java b/services/usage/java/com/android/server/usage/UsageStatsService.java index 43cebe83c538..e738d292a0f9 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UsageStatsService.java @@ -2867,6 +2867,18 @@ public class UsageStatsService extends SystemService implements } @Override + public boolean isPackageExemptedFromBroadcastResponseStats(@NonNull String callingPackage, + @UserIdInt int userId) { + Objects.requireNonNull(callingPackage); + + getContext().enforceCallingOrSelfPermission( + android.Manifest.permission.DUMP, + "isPackageExemptedFromBroadcastResponseStats"); + return mResponseStatsTracker.isPackageExemptedFromBroadcastResponseStats( + callingPackage, UserHandle.of(userId)); + } + + @Override @Nullable public String getAppStandbyConstant(@NonNull String key) { Objects.requireNonNull(key); diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index c0f4008c653d..040c5b013c6a 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -155,6 +155,15 @@ public class SubscriptionManager { "restoreSimSpecificSettings"; /** + * The key of the boolean flag indicating whether restoring subscriptions actually changes + * the subscription database or not. + * + * @hide + */ + public static final String RESTORE_SIM_SPECIFIC_SETTINGS_DATABASE_UPDATED = + "restoreSimSpecificSettingsDatabaseUpdated"; + + /** * Key to the backup & restore data byte array in the Bundle that is returned by {@link * #getAllSimSpecificSettingsForBackup()} or to be pass in to {@link * #restoreAllSimSpecificSettingsFromBackup(byte[])}. diff --git a/telephony/java/android/telephony/satellite/AntennaDirection.java b/telephony/java/android/telephony/satellite/AntennaDirection.java index 02b0bc7364a2..22412e6efadf 100644 --- a/telephony/java/android/telephony/satellite/AntennaDirection.java +++ b/telephony/java/android/telephony/satellite/AntennaDirection.java @@ -17,7 +17,7 @@ package android.telephony.satellite; import android.annotation.NonNull; -import android.compat.annotation.UnsupportedAppUsage; +import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; @@ -37,6 +37,7 @@ import java.util.Objects; * positive X and pointing away from back of the phone for negative X. * @hide */ +@SystemApi public final class AntennaDirection implements Parcelable { /** Antenna x axis direction. */ private float mX; @@ -50,7 +51,6 @@ public final class AntennaDirection implements Parcelable { /** * @hide */ - @UnsupportedAppUsage public AntennaDirection(float x, float y, float z) { mX = x; mY = y; diff --git a/telephony/java/android/telephony/satellite/AntennaPosition.java b/telephony/java/android/telephony/satellite/AntennaPosition.java index eefc8b00f8e8..588be6cae773 100644 --- a/telephony/java/android/telephony/satellite/AntennaPosition.java +++ b/telephony/java/android/telephony/satellite/AntennaPosition.java @@ -17,7 +17,7 @@ package android.telephony.satellite; import android.annotation.NonNull; -import android.compat.annotation.UnsupportedAppUsage; +import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; @@ -28,6 +28,7 @@ import java.util.Objects; * direction to be used with satellite communication and suggested device hold positions. * @hide */ +@SystemApi public final class AntennaPosition implements Parcelable { /** Antenna direction used for satellite communication. */ @NonNull AntennaDirection mAntennaDirection; @@ -38,7 +39,6 @@ public final class AntennaPosition implements Parcelable { /** * @hide */ - @UnsupportedAppUsage public AntennaPosition(@NonNull AntennaDirection antennaDirection, int suggestedHoldPosition) { mAntennaDirection = antennaDirection; mSuggestedHoldPosition = suggestedHoldPosition; diff --git a/telephony/java/android/telephony/satellite/PointingInfo.java b/telephony/java/android/telephony/satellite/PointingInfo.java index a559b32f0021..47dbdaff23bc 100644 --- a/telephony/java/android/telephony/satellite/PointingInfo.java +++ b/telephony/java/android/telephony/satellite/PointingInfo.java @@ -17,12 +17,19 @@ package android.telephony.satellite; import android.annotation.NonNull; +import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; +import java.util.Objects; + /** + * PointingInfo is used to store the position of satellite received from satellite modem. + * The position of satellite is represented by azimuth and elevation angles + * with degrees as unit of measurement. * @hide */ +@SystemApi public final class PointingInfo implements Parcelable { /** Satellite azimuth in degrees */ private float mSatelliteAzimuthDegrees; @@ -33,7 +40,6 @@ public final class PointingInfo implements Parcelable { /** * @hide */ - public PointingInfo(float satelliteAzimuthDegrees, float satelliteElevationDegrees) { mSatelliteAzimuthDegrees = satelliteAzimuthDegrees; mSatelliteElevationDegrees = satelliteElevationDegrees; @@ -67,6 +73,20 @@ public final class PointingInfo implements Parcelable { } }; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PointingInfo that = (PointingInfo) o; + return mSatelliteAzimuthDegrees == that.mSatelliteAzimuthDegrees + && mSatelliteElevationDegrees == that.mSatelliteElevationDegrees; + } + + @Override + public int hashCode() { + return Objects.hash(mSatelliteAzimuthDegrees, mSatelliteElevationDegrees); + } + @NonNull @Override public String toString() { diff --git a/telephony/java/android/telephony/satellite/SatelliteCapabilities.java b/telephony/java/android/telephony/satellite/SatelliteCapabilities.java index 6856cc0c5df2..bc45be110ace 100644 --- a/telephony/java/android/telephony/satellite/SatelliteCapabilities.java +++ b/telephony/java/android/telephony/satellite/SatelliteCapabilities.java @@ -17,6 +17,8 @@ package android.telephony.satellite; import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; @@ -27,8 +29,11 @@ import java.util.Objects; import java.util.Set; /** + * SatelliteCapabilities is used to represent the capabilities of the satellite service + * received from satellite modem. * @hide */ +@SystemApi public final class SatelliteCapabilities implements Parcelable { /** * List of technologies supported by the satellite modem. @@ -56,7 +61,7 @@ public final class SatelliteCapabilities implements Parcelable { /** * @hide */ - public SatelliteCapabilities(Set<Integer> supportedRadioTechnologies, + public SatelliteCapabilities(@Nullable Set<Integer> supportedRadioTechnologies, boolean isPointingRequired, int maxBytesPerOutgoingDatagram, @NonNull Map<Integer, AntennaPosition> antennaPositionMap) { mSupportedRadioTechnologies = supportedRadioTechnologies == null diff --git a/telephony/java/android/telephony/satellite/SatelliteDatagram.java b/telephony/java/android/telephony/satellite/SatelliteDatagram.java index d3cb8a07e4ba..9037f0c4078d 100644 --- a/telephony/java/android/telephony/satellite/SatelliteDatagram.java +++ b/telephony/java/android/telephony/satellite/SatelliteDatagram.java @@ -17,12 +17,16 @@ package android.telephony.satellite; import android.annotation.NonNull; +import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; /** + * SatelliteDatagram is used to store data that is to be sent or received over satellite. + * Data is stored in byte array format. * @hide */ +@SystemApi public final class SatelliteDatagram implements Parcelable { /** * Datagram to be sent or received over satellite. @@ -63,6 +67,12 @@ public final class SatelliteDatagram implements Parcelable { } }; + /** + * Get satellite datagram. + * @return byte array. The format of the byte array is determined by the corresponding + * satellite provider. Client application should be aware of how to encode the datagram based + * upon the satellite provider. + */ @NonNull public byte[] getSatelliteDatagram() { return mData; } diff --git a/telephony/java/android/telephony/satellite/SatelliteDatagramCallback.java b/telephony/java/android/telephony/satellite/SatelliteDatagramCallback.java index b2dec71ecb32..cb2920f9e6ee 100644 --- a/telephony/java/android/telephony/satellite/SatelliteDatagramCallback.java +++ b/telephony/java/android/telephony/satellite/SatelliteDatagramCallback.java @@ -17,6 +17,7 @@ package android.telephony.satellite; import android.annotation.NonNull; +import android.annotation.SystemApi; import java.util.function.Consumer; @@ -25,6 +26,7 @@ import java.util.function.Consumer; * * @hide */ +@SystemApi public interface SatelliteDatagramCallback { /** * Called when there is an incoming datagram to be received. diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 2021ac7c4cd5..c498216cf75b 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -23,6 +23,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresFeature; import android.annotation.RequiresPermission; +import android.annotation.SystemApi; import android.content.Context; import android.content.pm.PackageManager; import android.os.Binder; @@ -55,6 +56,7 @@ import java.util.function.Consumer; * @hide */ @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SATELLITE) +@SystemApi public class SatelliteManager { private static final String TAG = "SatelliteManager"; @@ -386,9 +388,16 @@ public class SatelliteManager { public @interface DisplayMode {} /** - * Request to enable or disable the satellite modem and demo mode. If the satellite modem is - * enabled, this may also disable the cellular modem, and if the satellite modem is disabled, - * this may also re-enable the cellular modem. + * Request to enable or disable the satellite modem and demo mode. + * If satellite modem and cellular modem cannot work concurrently, + * then this will disable the cellular modem if satellite modem is enabled, + * and will re-enable the cellular modem if satellite modem is disabled. + * + * Demo mode is created to simulate the experience of sending and receiving messages over + * satellite. If user enters demo mode, a request should be sent to framework to enable + * satellite with enableDemoMode set to {code true}. Once satellite is enabled and device is + * aligned with the satellite, user can send a message and also receive a reply in demo mode. + * If enableSatellite is {@code false}, enableDemoMode has no impact on the behavior. * * @param enableSatellite {@code true} to enable the satellite modem and * {@code false} to disable. @@ -911,8 +920,8 @@ public class SatelliteManager { * Provision the device with a satellite provider. * This is needed if the provider allows dynamic registration. * - * @param token The token to be used as a unique identifier for provisioning with satellite - * gateway. + * @param token The token is generated by the user which is used as a unique identifier for + * provisioning with satellite gateway. * @param provisionData Data from the provisioning app that can be used by provisioning server * @param cancellationSignal The optional signal used by the caller to cancel the provision * request. Even when the cancellation is signaled, Telephony will @@ -965,9 +974,12 @@ public class SatelliteManager { * {@link SatelliteProvisionStateCallback#onSatelliteProvisionStateChanged(boolean)} * should report as deprovisioned. * For provisioning satellite service, refer to - * {@link #provisionSatelliteService(String, String, CancellationSignal, Executor, Consumer)} + * {@link #provisionSatelliteService(String, byte[], CancellationSignal, Executor, Consumer)} * * @param token The token of the device/subscription to be deprovisioned. + * This should match with the token passed as input in + * {@link #provisionSatelliteService(String, byte[], CancellationSignal, Executor, + * Consumer)} * @param resultListener Listener for the {@link SatelliteError} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. @@ -1213,6 +1225,9 @@ public class SatelliteManager { /** * Register to receive incoming datagrams over satellite. * + * To poll for pending satellite datagrams, refer to + * {@link #pollPendingSatelliteDatagrams(Executor, Consumer)} + * * @param executor The executor on which the callback will be called. * @param callback The callback to handle incoming datagrams over satellite. * This callback with be invoked when a new datagram is received from satellite. @@ -1305,6 +1320,7 @@ public class SatelliteManager { /** * Poll pending satellite datagrams over satellite. * + * This method should be called when user specifies to check incoming messages over satellite. * This method requests modem to check if there are any pending datagrams to be received over * satellite. If there are any incoming datagrams, they will be received via * {@link SatelliteDatagramCallback#onSatelliteDatagramReceived(long, SatelliteDatagram, int, @@ -1518,6 +1534,9 @@ public class SatelliteManager { /** * Inform whether the device is aligned with the satellite for demo mode. * + * Framework can send datagram to modem only when device is aligned with the satellite. + * This method helps framework to simulate the experience of sending datagram over satellite. + * * @param isAligned {@true} Device is aligned with the satellite for demo mode * {@false} Device is not aligned with the satellite for demo mode * diff --git a/telephony/java/android/telephony/satellite/SatelliteProvisionStateCallback.java b/telephony/java/android/telephony/satellite/SatelliteProvisionStateCallback.java index a62eb8b8a5fb..71168762cca1 100644 --- a/telephony/java/android/telephony/satellite/SatelliteProvisionStateCallback.java +++ b/telephony/java/android/telephony/satellite/SatelliteProvisionStateCallback.java @@ -16,17 +16,22 @@ package android.telephony.satellite; +import android.annotation.SystemApi; + /** * A callback class for monitoring satellite provision state change events. * * @hide */ +@SystemApi public interface SatelliteProvisionStateCallback { /** * Called when satellite provision state changes. * * @param provisioned The new provision state. {@code true} means satellite is provisioned * {@code false} means satellite is not provisioned. + * It is generally expected that the provisioning app retries if + * provisioning fails. */ void onSatelliteProvisionStateChanged(boolean provisioned); } diff --git a/telephony/java/android/telephony/satellite/SatelliteStateCallback.java b/telephony/java/android/telephony/satellite/SatelliteStateCallback.java index d9ecaa3467e3..812ff2decf5d 100644 --- a/telephony/java/android/telephony/satellite/SatelliteStateCallback.java +++ b/telephony/java/android/telephony/satellite/SatelliteStateCallback.java @@ -16,11 +16,14 @@ package android.telephony.satellite; +import android.annotation.SystemApi; + /** * A callback class for monitoring satellite modem state change events. * * @hide */ +@SystemApi public interface SatelliteStateCallback { /** * Called when satellite modem state changes. diff --git a/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java b/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java index d4fe57a0be2e..d7d892a7c30a 100644 --- a/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java +++ b/telephony/java/android/telephony/satellite/SatelliteTransmissionUpdateCallback.java @@ -17,6 +17,7 @@ package android.telephony.satellite; import android.annotation.NonNull; +import android.annotation.SystemApi; /** * A callback class for monitoring satellite position update and datagram transfer state change @@ -24,6 +25,7 @@ import android.annotation.NonNull; * * @hide */ +@SystemApi public interface SatelliteTransmissionUpdateCallback { /** * Called when the satellite position changed. diff --git a/tests/FlickerTests/Android.bp b/tests/FlickerTests/Android.bp index 72b515947ca6..3e67286998bc 100644 --- a/tests/FlickerTests/Android.bp +++ b/tests/FlickerTests/Android.bp @@ -93,6 +93,7 @@ java_defaults { "flickerlib-helpers", "platform-test-annotations", "wm-flicker-common-app-helpers", + "wm-shell-flicker-utils", ], data: [ ":FlickerTestApp", diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt index 45cd65d9776c..45176448a9f4 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/ActivityEmbeddingTestBase.kt @@ -17,9 +17,12 @@ package com.android.server.wm.flicker.activityembedding import android.tools.device.flicker.legacy.LegacyFlickerTest +import android.platform.test.annotations.Presubmit +import android.tools.common.traces.component.ComponentNameMatcher import com.android.server.wm.flicker.BaseTest import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper import org.junit.Before +import org.junit.Test abstract class ActivityEmbeddingTestBase(flicker: LegacyFlickerTest) : BaseTest(flicker) { val testApp = ActivityEmbeddingAppHelper(instrumentation) @@ -29,4 +32,14 @@ abstract class ActivityEmbeddingTestBase(flicker: LegacyFlickerTest) : BaseTest( // The test should only be run on devices that support ActivityEmbedding. ActivityEmbeddingAppHelper.assumeActivityEmbeddingSupportedDevice() } + + /** Asserts the background animation layer is never visible during bounds change transition. */ + @Presubmit + @Test + fun backgroundLayerNeverVisible() { + val backgroundColorLayer = ComponentNameMatcher("", "Animation Background") + flicker.assertLayers { + isInvisible(backgroundColorLayer) + } + } } diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt new file mode 100644 index 000000000000..badd876ae321 --- /dev/null +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/layoutchange/HorizontalSplitChangeRatioTest.kt @@ -0,0 +1,152 @@ +/* + * 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.server.wm.flicker.activityembedding + +import android.platform.test.annotations.Presubmit +import android.tools.common.datatypes.Rect +import android.tools.device.flicker.junit.FlickerParametersRunnerFactory +import android.tools.device.flicker.legacy.FlickerBuilder +import android.tools.device.flicker.legacy.LegacyFlickerTest +import android.tools.device.flicker.legacy.LegacyFlickerTestFactory +import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper +import androidx.test.filters.RequiresDevice +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import org.junit.runners.Parameterized + +/** + * Test changing split ratio at runtime on a horizona split. + * + * Setup: Launch A|B in horizontal split with B being the secondary activity, by default A and B + * windows are equal in size. B is on the top and A is on the bottom. + * Transitions: + * Change the split ratio to A:B=0.7:0.3, expect bounds change for both A and B. + * + * To run this test: `atest FlickerTests:HorizontalSplitChangeRatioTest` + */ +@RequiresDevice +@RunWith(Parameterized::class) +@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +class HorizontalSplitChangeRatioTest(flicker: LegacyFlickerTest) : + ActivityEmbeddingTestBase(flicker) { + /** {@inheritDoc} */ + override val transition: FlickerBuilder.() -> Unit = { + setup { + tapl.setExpectedRotationCheckEnabled(false) + testApp.launchViaIntent(wmHelper) + testApp.launchSecondaryActivityHorizontally(wmHelper) + startDisplayBounds = + wmHelper.currentState.layerState.physicalDisplayBounds + ?: error("Display not found") + } + transitions { + testApp.changeSecondaryActivityRatio(wmHelper) + } + teardown { + tapl.goHome() + testApp.exit(wmHelper) + } + } + + /** Assert the Main activity window is always visible. */ + @Presubmit + @Test + fun mainActivityWindowIsAlwaysVisible() { + flicker.assertWm { isAppWindowVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) } + } + + /** Assert the Main activity window is always visible. */ + @Presubmit + @Test + fun mainActivityLayerIsAlwaysVisible() { + flicker.assertLayers { isVisible(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) } + } + + /** Assert the Secondary activity window is always visible. */ + @Presubmit + @Test + fun secondaryActivityWindowIsAlwaysVisible() { + flicker.assertWm { + isAppWindowVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) } + } + + /** Assert the Secondary activity window is always visible. */ + @Presubmit + @Test + fun secondaryActivityLayerIsAlwaysVisible() { + flicker.assertLayers { isVisible(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) } + } + + /** Assert the Main and Secondary activity change height during the transition. */ + @Presubmit + @Test + fun secondaryActivityAdjustsHeightRuntime() { + flicker.assertLayersStart { + val topLayerRegion = + this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + val bottomLayerRegion = + this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + // Compare dimensions of two splits, given we're using default split attributes, + // both activities take up the same visible size on the display. + check { "height" } + .that(topLayerRegion.region.height).isEqual(bottomLayerRegion.region.height) + check { "width" } + .that(topLayerRegion.region.width).isEqual(bottomLayerRegion.region.width) + topLayerRegion.notOverlaps(bottomLayerRegion.region) + // Layers of two activities sum to be fullscreen size on display. + topLayerRegion.plus(bottomLayerRegion.region).coversExactly(startDisplayBounds) + } + + flicker.assertLayersEnd { + val topLayerRegion = + this.visibleRegion(ActivityEmbeddingAppHelper.SECONDARY_ACTIVITY_COMPONENT) + val bottomLayerRegion = + this.visibleRegion(ActivityEmbeddingAppHelper.MAIN_ACTIVITY_COMPONENT) + // Compare dimensions of two splits, given we're using default split attributes, + // both activities take up the same visible size on the display. + check { "height" } + .that(topLayerRegion.region.height).isLower(bottomLayerRegion.region.height) + check { "height" } + .that( + topLayerRegion.region.height / 0.3f - + bottomLayerRegion.region.height / 0.7f) + .isLower(0.1f) + check { "width" } + .that(topLayerRegion.region.width).isEqual(bottomLayerRegion.region.width) + topLayerRegion.notOverlaps(bottomLayerRegion.region) + // Layers of two activities sum to be fullscreen size on display. + topLayerRegion.plus(bottomLayerRegion.region).coversExactly(startDisplayBounds) + } + } + + companion object { + /** {@inheritDoc} */ + private var startDisplayBounds = Rect.EMPTY + /** + * Creates the test configurations. + * + * See [LegacyFlickerTestFactory.nonRotationTests] for configuring screen orientation and + * navigation modes. + */ + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun getParams() = LegacyFlickerTestFactory.nonRotationTests() + } +}
\ No newline at end of file diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt index 27de12e7dfdb..404f3290f04a 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenThirdActivityOverSplitTest.kt @@ -142,14 +142,6 @@ class OpenThirdActivityOverSplitTest(flicker: LegacyFlickerTest) : } } - /** Assert the background animation layer is never visible during transition. */ - @Presubmit - @Test - fun backgroundLayerNeverVisible() { - val backgroundColorLayer = ComponentNameMatcher("", "Animation Background") - flicker.assertLayers { isInvisible(backgroundColorLayer) } - } - companion object { /** {@inheritDoc} */ private var startDisplayBounds = Rect.EMPTY diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt index 6722cc8fd021..aaf0158a9d47 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt @@ -16,6 +16,7 @@ package com.android.server.wm.flicker.activityembedding +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.Presubmit import android.tools.common.datatypes.Rect import android.tools.common.flicker.subject.region.RegionSubject @@ -161,6 +162,7 @@ class OpenTrampolineActivityTest(flicker: LegacyFlickerTest) : ActivityEmbedding } } + @FlakyTest(bugId = 290736037) /** Main activity should go from fullscreen to being a split with secondary activity. */ @Presubmit @Test diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt index 2d3bc2d5eb15..35353c62d7e6 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/helpers/ActivityEmbeddingAppHelper.kt @@ -45,18 +45,22 @@ constructor( * based on the split pair rule. */ fun launchSecondaryActivity(wmHelper: WindowManagerStateHelper) { - val launchButton = - uiDevice.wait( - Until.findObject(By.res(getPackage(), "launch_secondary_activity_button")), - FIND_TIMEOUT - ) - require(launchButton != null) { "Can't find launch secondary activity button on screen." } - launchButton.click() - wmHelper - .StateSyncBuilder() - .withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED) - .withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED) - .waitForAndVerify() + launchSecondaryActivityFromButton(wmHelper, "launch_secondary_activity_button") + } + + /** + * Clicks the button to launch the secondary activity in RTL, which should split with the main + * activity based on the split pair rule. + */ + fun launchSecondaryActivityRTL(wmHelper: WindowManagerStateHelper) { + launchSecondaryActivityFromButton(wmHelper, "launch_secondary_activity_rtl_button") + } + + /** + * Clicks the button to launch the secondary activity in a horizontal split. + */ + fun launchSecondaryActivityHorizontally(wmHelper: WindowManagerStateHelper) { + launchSecondaryActivityFromButton(wmHelper, "launch_secondary_activity_horizontally_button") } /** Clicks the button to launch a third activity over a secondary activity. */ @@ -101,16 +105,38 @@ constructor( */ fun finishSecondaryActivity(wmHelper: WindowManagerStateHelper) { val finishButton = - uiDevice.wait( - Until.findObject(By.res(getPackage(), "finish_secondary_activity_button")), - FIND_TIMEOUT - ) + uiDevice.wait( + Until.findObject(By.res(getPackage(), "finish_secondary_activity_button")), + FIND_TIMEOUT + ) require(finishButton != null) { "Can't find finish secondary activity button on screen." } finishButton.click() wmHelper - .StateSyncBuilder() - .withActivityRemoved(SECONDARY_ACTIVITY_COMPONENT) - .waitForAndVerify() + .StateSyncBuilder() + .withActivityRemoved(SECONDARY_ACTIVITY_COMPONENT) + .waitForAndVerify() + } + + /** + * Clicks the button to toggle the split ratio of secondary activity. + */ + fun changeSecondaryActivityRatio(wmHelper: WindowManagerStateHelper) { + val launchButton = + uiDevice.wait( + Until.findObject( + By.res(getPackage(), + "toggle_split_ratio_button")), + FIND_TIMEOUT + ) + require(launchButton != null) { + "Can't find toggle ratio for secondary activity button on screen." + } + launchButton.click() + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .withTransitionSnapshotGone() + .waitForAndVerify() } fun secondaryActivityEnterPip(wmHelper: WindowManagerStateHelper) { @@ -149,25 +175,19 @@ constructor( .waitForAndVerify() } - /** - * Clicks the button to launch the secondary activity in RTL, which should split with the main - * activity based on the split pair rule. - */ - fun launchSecondaryActivityRTL(wmHelper: WindowManagerStateHelper) { + private fun launchSecondaryActivityFromButton( + wmHelper: WindowManagerStateHelper, buttonName: String) { val launchButton = - uiDevice.wait( - Until.findObject(By.res(getPackage(), "launch_secondary_activity_rtl_button")), - FIND_TIMEOUT - ) + uiDevice.wait(Until.findObject(By.res(getPackage(), buttonName)), FIND_TIMEOUT) require(launchButton != null) { - "Can't find launch secondary activity rtl button on screen." + "Can't find launch secondary activity button : " + buttonName + "on screen." } launchButton.click() wmHelper - .StateSyncBuilder() - .withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED) - .withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED) - .waitForAndVerify() + .StateSyncBuilder() + .withActivityState(SECONDARY_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED) + .withActivityState(MAIN_ACTIVITY_COMPONENT, PlatformConsts.STATE_RESUMED) + .waitForAndVerify() } /** diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt index 94b090f42c9b..063e2c3091ca 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/launch/OpenAppFromLockscreenViaIntentTest.kt @@ -71,7 +71,7 @@ open class OpenAppFromLockscreenViaIntentTest(flicker: LegacyFlickerTest) : * Checks that the [ComponentNameMatcher.NAV_BAR] layer starts invisible, becomes visible during * unlocking animation and remains visible at the end */ - @Presubmit + @FlakyTest(bugId = 288341660) @Test fun navBarLayerVisibilityChanges() { Assume.assumeFalse(flicker.scenario.isTablet) diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt index a6cf8eebf063..0e33390353e9 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt @@ -67,7 +67,10 @@ open class OpenAppFromNotificationWarmTest(flicker: LegacyFlickerTest) : wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify() testApp.postNotification(wmHelper) device.pressHome() - wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify() + wmHelper.StateSyncBuilder() + .withHomeActivityVisible() + .withWindowSurfaceDisappeared(ComponentNameMatcher.NOTIFICATION_SHADE) + .waitForAndVerify() } transitions { diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/service/Utils.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/service/Utils.kt index 8242e9a31992..276f97962d7f 100644 --- a/tests/FlickerTests/src/com/android/server/wm/flicker/service/Utils.kt +++ b/tests/FlickerTests/src/com/android/server/wm/flicker/service/Utils.kt @@ -22,9 +22,7 @@ import android.platform.test.rule.PressHomeRule import android.platform.test.rule.UnlockScreenRule import android.tools.common.NavBar import android.tools.common.Rotation -import android.tools.device.apphelpers.MessagingAppHelper import android.tools.device.flicker.rules.ChangeDisplayOrientationRule -import android.tools.device.flicker.rules.LaunchAppRule import android.tools.device.flicker.rules.RemoveAllTasksButHomeRule import androidx.test.platform.app.InstrumentationRegistry import org.junit.rules.RuleChain @@ -37,9 +35,6 @@ object Utils { .around( NavigationModeRule(navigationMode.value, /* changeNavigationModeAfterTest */ false) ) - .around( - LaunchAppRule(MessagingAppHelper(instrumentation), clearCacheAfterParsing = false) - ) .around(RemoveAllTasksButHomeRule()) .around( ChangeDisplayOrientationRule( diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml index e32a7092bf5d..86c21906163f 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_main_layout.xml @@ -38,6 +38,14 @@ android:text="Launch Secondary Activity in RTL" /> <Button + android:id="@+id/launch_secondary_activity_horizontally_button" + android:layout_width="wrap_content" + android:layout_height="48dp" + android:onClick="launchSecondaryActivity" + android:tag="BOTTOM_TO_TOP" + android:text="Launch Secondary Activity Horizontally" /> + + <Button android:id="@+id/launch_placeholder_split_button" android:layout_width="wrap_content" android:layout_height="48dp" diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml index 135140aa2377..6d4de995bd73 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_embedding_secondary_activity_layout.xml @@ -35,6 +35,14 @@ android:onClick="launchThirdActivity" android:text="Launch a third activity" /> + <ToggleButton + android:id="@+id/toggle_split_ratio_button" + android:layout_width="wrap_content" + android:layout_height="48dp" + android:textOn="Ratio 0.5" + android:textOff="Ratio 0.3" + android:checked="false" /> + <Button android:id="@+id/secondary_enter_pip_button" android:layout_width="wrap_content" diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java index 3b1a8599f3e1..23fa91c37728 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingMainActivity.java @@ -22,6 +22,7 @@ import android.content.Intent; import android.os.Bundle; import android.view.View; +import androidx.annotation.NonNull; import androidx.window.embedding.ActivityFilter; import androidx.window.embedding.ActivityRule; import androidx.window.embedding.EmbeddingAspectRatio; @@ -152,6 +153,9 @@ public class ActivityEmbeddingMainActivity extends Activity { if (layoutDirectionStr.equals(LayoutDirection.LEFT_TO_RIGHT.toString())) { return LayoutDirection.LEFT_TO_RIGHT; } + if (layoutDirectionStr.equals(LayoutDirection.BOTTOM_TO_TOP.toString())) { + return LayoutDirection.BOTTOM_TO_TOP; + } if (layoutDirectionStr.equals(LayoutDirection.RIGHT_TO_LEFT.toString())) { return LayoutDirection.RIGHT_TO_LEFT; } diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java index ee087ef9be2c..29cbf01dc6da 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/ActivityEmbeddingSecondaryActivity.java @@ -22,6 +22,11 @@ import android.app.PictureInPictureParams; import android.graphics.Color; import android.os.Bundle; import android.view.View; +import android.widget.ToggleButton; + +import androidx.window.embedding.SplitAttributes; +import androidx.window.embedding.SplitAttributesCalculatorParams; +import androidx.window.embedding.SplitController; /** * Activity to be used as the secondary activity to split with @@ -29,18 +34,41 @@ import android.view.View; */ public class ActivityEmbeddingSecondaryActivity extends Activity { + private SplitController mSplitController; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_embedding_secondary_activity_layout); findViewById(R.id.secondary_activity_layout).setBackgroundColor(Color.YELLOW); findViewById(R.id.finish_secondary_activity_button).setOnClickListener( - new View.OnClickListener() { + new View.OnClickListener() { @Override public void onClick(View v) { finish(); } - }); + }); + mSplitController = SplitController.getInstance(this); + final ToggleButton splitRatio = findViewById(R.id.toggle_split_ratio_button); + mSplitController.setSplitAttributesCalculator(params -> { + return new SplitAttributes.Builder() + .setSplitType( + SplitAttributes.SplitType.ratio( + splitRatio.isChecked() ? 0.7f : 0.5f) + ) + .setLayoutDirection( + params.getDefaultSplitAttributes() + .getLayoutDirection()) + .build(); + }); + splitRatio.setOnClickListener( + new View.OnClickListener() { + @Override + public void onClick(View v) { + // This triggers a recalcuation of splitatributes. + mSplitController.invalidateTopVisibleSplitAttributes(); + } + }); findViewById(R.id.secondary_enter_pip_button).setOnClickListener( new View.OnClickListener() { @Override diff --git a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt index 7cf69b7780d9..31ea8327c2b3 100644 --- a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt +++ b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt @@ -16,6 +16,10 @@ package com.android.test.silkfx.hdr +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.animation.ValueAnimator +import android.animation.ValueAnimator.AnimatorUpdateListener import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas @@ -65,15 +69,9 @@ class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context findViewById<RadioGroup>(R.id.output_mode)!!.also { it.check(outputMode) it.setOnCheckedChangeListener { _, checkedId -> - val previousMode = outputMode outputMode = checkedId - if (previousMode == R.id.output_sdr && checkedId == R.id.output_hdr) { - animateToHdr() - } else if (previousMode == R.id.output_hdr && checkedId == R.id.output_sdr) { - animateToSdr() - } else { - updateDisplay() - } + // Intentionally don't do anything fancy so that mode A/B comparisons are easy + updateDisplay() } } @@ -103,10 +101,41 @@ class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context imageView.apply { isClickable = true + // Example of animating between SDR and HDR using gainmap params; animates HDR->SDR->HDR + // with a brief pause on SDR. The key thing here is that the gainmap's + // minDisplayRatioForHdrTransition is animated between its original value (for full HDR) + // and displayRatioForFullHdr (for full SDR). The view must also be invalidated during + // the animation for the updates to take effect. setOnClickListener { - animate().alpha(.5f).withEndAction { - animate().alpha(1f).start() - }.start() + if (gainmap != null && (outputMode == R.id.output_hdr || + outputMode == R.id.output_hdr_test)) { + val animationLengthMs: Long = 500 + val updateListener = object : AnimatorUpdateListener { + override fun onAnimationUpdate(animation: ValueAnimator) { + imageView.invalidate() + } + } + val hdrToSdr = ObjectAnimator.ofFloat( + gainmap, "minDisplayRatioForHdrTransition", + gainmap!!.minDisplayRatioForHdrTransition, + gainmap!!.displayRatioForFullHdr).apply { + duration = animationLengthMs + addUpdateListener(updateListener) + } + val sdrToHdr = ObjectAnimator.ofFloat( + gainmap, "minDisplayRatioForHdrTransition", + gainmap!!.displayRatioForFullHdr, + gainmap!!.minDisplayRatioForHdrTransition).apply { + duration = animationLengthMs + addUpdateListener(updateListener) + } + + AnimatorSet().apply { + play(hdrToSdr) + play(sdrToHdr).after(animationLengthMs) + start() + } + } } } } @@ -164,20 +193,6 @@ class GainmapImage(context: Context, attrs: AttributeSet?) : FrameLayout(context updateDisplay() } - private fun animateToHdr() { - if (bitmap == null || gainmap == null) return - - // TODO: Trigger an animation - updateDisplay() - } - - private fun animateToSdr() { - if (bitmap == null) return - - // TODO: Trigger an animation - updateDisplay() - } - private fun updateDisplay() { if (bitmap == null) return diff --git a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java index fa5b7c15a6fe..ba9e4a831789 100644 --- a/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java +++ b/tests/UpdatableSystemFontTest/src/com/android/updatablesystemfont/UpdatableSystemFontTest.java @@ -24,7 +24,6 @@ import static android.os.ParcelFileDescriptor.MODE_READ_ONLY; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; -import static org.junit.Assume.assumeTrue; import static java.util.concurrent.TimeUnit.SECONDS; @@ -36,7 +35,6 @@ import android.graphics.fonts.FontManager; import android.graphics.fonts.FontStyle; import android.os.ParcelFileDescriptor; import android.platform.test.annotations.RootPermissionTest; -import android.security.FileIntegrityManager; import android.text.FontConfig; import android.util.Log; import android.util.Pair; @@ -139,10 +137,6 @@ public class UpdatableSystemFontTest { @Before public void setUp() throws Exception { Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); - // Run tests only if updatable system font is enabled. - FileIntegrityManager fim = context.getSystemService(FileIntegrityManager.class); - assumeTrue(fim != null); - assumeTrue(fim.isApkVeritySupported()); mKeyId = insertCert(CERT_PATH); mFontManager = context.getSystemService(FontManager.class); expectCommandToSucceed("cmd font clear"); diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp index d2ea59958f69..b5c290ec8dad 100644 --- a/tools/aapt2/cmd/Compile.cpp +++ b/tools/aapt2/cmd/Compile.cpp @@ -186,7 +186,20 @@ static bool CompileTable(IAaptContext* context, const CompileOptions& options, // These are created as weak symbols, and are only generated from default // configuration // strings and plurals. - PseudolocaleGenerator pseudolocale_generator; + std::string grammatical_gender_values; + std::string grammatical_gender_ratio; + if (options.pseudo_localize_gender_values) { + grammatical_gender_values = options.pseudo_localize_gender_values.value(); + } else { + grammatical_gender_values = "f,m,n"; + } + if (options.pseudo_localize_gender_ratio) { + grammatical_gender_ratio = options.pseudo_localize_gender_ratio.value(); + } else { + grammatical_gender_ratio = "1.0"; + } + PseudolocaleGenerator pseudolocale_generator(grammatical_gender_values, + grammatical_gender_ratio); if (!pseudolocale_generator.Consume(context, &table)) { return false; } diff --git a/tools/aapt2/cmd/Compile.h b/tools/aapt2/cmd/Compile.h index 14a730a1b1a0..22890fc53d5c 100644 --- a/tools/aapt2/cmd/Compile.h +++ b/tools/aapt2/cmd/Compile.h @@ -35,6 +35,8 @@ struct CompileOptions { std::optional<std::string> res_dir; std::optional<std::string> res_zip; std::optional<std::string> generate_text_symbols_path; + std::optional<std::string> pseudo_localize_gender_values; + std::optional<std::string> pseudo_localize_gender_ratio; std::optional<Visibility::Level> visibility; bool pseudolocalize = false; bool no_png_crunch = false; @@ -76,6 +78,15 @@ class CompileCommand : public Command { AddOptionalFlag("--source-path", "Sets the compiled resource file source file path to the given string.", &options_.source_path); + AddOptionalFlag("--pseudo-localize-gender-values", + "Sets the gender values to pick up for generating grammatical gender strings, " + "gender values should be f, m, or n, which are shortcuts for feminine, " + "masculine and neuter, and split with comma.", + &options_.pseudo_localize_gender_values); + AddOptionalFlag("--pseudo-localize-gender-ratio", + "Sets the ratio of resources to generate grammatical gender strings for. The " + "ratio has to be a float number between 0 and 1.", + &options_.pseudo_localize_gender_ratio); } int Action(const std::vector<std::string>& args) override; diff --git a/tools/aapt2/cmd/Compile_test.cpp b/tools/aapt2/cmd/Compile_test.cpp index 3464a7662c60..8880089d0e20 100644 --- a/tools/aapt2/cmd/Compile_test.cpp +++ b/tools/aapt2/cmd/Compile_test.cpp @@ -236,9 +236,24 @@ TEST_F(CompilerTest, DoNotTranslateTest) { // The first string (000) is translatable, the second is not // ar-XB uses "\u200F\u202E...\u202C\u200F" std::vector<std::string> expected_translatable = { - "000", "111", // default locale - "[000 one]", // en-XA - "\xE2\x80\x8F\xE2\x80\xAE" "000" "\xE2\x80\xAC\xE2\x80\x8F", // ar-XB + "(F)[000 one]", // en-XA-feminine + "(F)\xE2\x80\x8F\xE2\x80\xAE" + "000" + "\xE2\x80\xAC\xE2\x80\x8F", // ar-XB-feminine + "(M)[000 one]", // en-XA-masculine + "(M)\xE2\x80\x8F\xE2\x80\xAE" + "000" + "\xE2\x80\xAC\xE2\x80\x8F", // ar-XB-masculine + "(N)[000 one]", // en-XA-neuter + "(N)\xE2\x80\x8F\xE2\x80\xAE" + "000" + "\xE2\x80\xAC\xE2\x80\x8F", // ar-XB-neuter + "000", // default locale + "111", // default locale + "[000 one]", // en-XA + "\xE2\x80\x8F\xE2\x80\xAE" + "000" + "\xE2\x80\xAC\xE2\x80\x8F", // ar-XB }; AssertTranslations(this, "foo", expected_translatable); AssertTranslations(this, "foo_donottranslate", expected_translatable); diff --git a/tools/aapt2/compile/PseudolocaleGenerator.cpp b/tools/aapt2/compile/PseudolocaleGenerator.cpp index 09a8560f984a..8143052f4376 100644 --- a/tools/aapt2/compile/PseudolocaleGenerator.cpp +++ b/tools/aapt2/compile/PseudolocaleGenerator.cpp @@ -16,11 +16,15 @@ #include "compile/PseudolocaleGenerator.h" +#include <stdint.h> + #include <algorithm> +#include <random> #include "ResourceTable.h" #include "ResourceValues.h" #include "ValueVisitor.h" +#include "androidfw/ResourceTypes.h" #include "androidfw/Util.h" #include "compile/Pseudolocalizer.h" #include "util/Util.h" @@ -293,8 +297,85 @@ class Visitor : public ValueVisitor { Pseudolocalizer localizer_; }; +class GrammaticalGenderVisitor : public ValueVisitor { + public: + std::unique_ptr<Value> value; + std::unique_ptr<Item> item; + + GrammaticalGenderVisitor(android::StringPool* pool, uint8_t grammaticalInflection) + : pool_(pool), grammaticalInflection_(grammaticalInflection) { + } + + void Visit(Plural* plural) override { + CloningValueTransformer cloner(pool_); + std::unique_ptr<Plural> grammatical_gendered = util::make_unique<Plural>(); + for (size_t i = 0; i < plural->values.size(); i++) { + if (plural->values[i]) { + GrammaticalGenderVisitor sub_visitor(pool_, grammaticalInflection_); + plural->values[i]->Accept(&sub_visitor); + if (sub_visitor.item) { + grammatical_gendered->values[i] = std::move(sub_visitor.item); + } else { + grammatical_gendered->values[i] = plural->values[i]->Transform(cloner); + } + } + } + grammatical_gendered->SetSource(plural->GetSource()); + grammatical_gendered->SetWeak(true); + value = std::move(grammatical_gendered); + } + + std::string AddGrammaticalGenderPrefix(const std::string_view& original_string) { + std::string result; + switch (grammaticalInflection_) { + case android::ResTable_config::GRAMMATICAL_GENDER_MASCULINE: + result = std::string("(M)") + std::string(original_string); + break; + case android::ResTable_config::GRAMMATICAL_GENDER_FEMININE: + result = std::string("(F)") + std::string(original_string); + break; + case android::ResTable_config::GRAMMATICAL_GENDER_NEUTER: + result = std::string("(N)") + std::string(original_string); + break; + default: + result = std::string(original_string); + break; + } + return result; + } + + void Visit(String* string) override { + std::string prefixed_string = AddGrammaticalGenderPrefix(std::string(*string->value)); + std::unique_ptr<String> grammatical_gendered = + util::make_unique<String>(pool_->MakeRef(prefixed_string)); + grammatical_gendered->SetSource(string->GetSource()); + grammatical_gendered->SetWeak(true); + item = std::move(grammatical_gendered); + } + + void Visit(StyledString* string) override { + std::string prefixed_string = AddGrammaticalGenderPrefix(std::string(string->value->value)); + android::StyleString new_string; + new_string.str = std::move(prefixed_string); + for (const android::StringPool::Span& span : string->value->spans) { + new_string.spans.emplace_back(android::Span{*span.name, span.first_char, span.last_char}); + } + std::unique_ptr<StyledString> grammatical_gendered = + util::make_unique<StyledString>(pool_->MakeRef(new_string)); + grammatical_gendered->SetSource(string->GetSource()); + grammatical_gendered->SetWeak(true); + item = std::move(grammatical_gendered); + } + + private: + DISALLOW_COPY_AND_ASSIGN(GrammaticalGenderVisitor); + android::StringPool* pool_; + uint8_t grammaticalInflection_; +}; + ConfigDescription ModifyConfigForPseudoLocale(const ConfigDescription& base, - Pseudolocalizer::Method m) { + Pseudolocalizer::Method m, + uint8_t grammaticalInflection) { ConfigDescription modified = base; switch (m) { case Pseudolocalizer::Method::kAccent: @@ -313,12 +394,64 @@ ConfigDescription ModifyConfigForPseudoLocale(const ConfigDescription& base, default: break; } + modified.grammaticalInflection = grammaticalInflection; return modified; } +void GrammaticalGender(ResourceConfigValue* original_value, + ResourceConfigValue* localized_config_value, android::StringPool* pool, + ResourceEntry* entry, const Pseudolocalizer::Method method, + uint8_t grammaticalInflection) { + GrammaticalGenderVisitor visitor(pool, grammaticalInflection); + localized_config_value->value->Accept(&visitor); + + std::unique_ptr<Value> grammatical_gendered_value; + if (visitor.value) { + grammatical_gendered_value = std::move(visitor.value); + } else if (visitor.item) { + grammatical_gendered_value = std::move(visitor.item); + } + if (!grammatical_gendered_value) { + return; + } + + ConfigDescription config = + ModifyConfigForPseudoLocale(original_value->config, method, grammaticalInflection); + + ResourceConfigValue* grammatical_gendered_config_value = + entry->FindOrCreateValue(config, original_value->product); + if (!grammatical_gendered_config_value->value) { + // Only use auto-generated pseudo-localization if none is defined. + grammatical_gendered_config_value->value = std::move(grammatical_gendered_value); + } +} + +const uint32_t MASK_MASCULINE = 1; // Bit mask for masculine +const uint32_t MASK_FEMININE = 2; // Bit mask for feminine +const uint32_t MASK_NEUTER = 4; // Bit mask for neuter + +void GrammaticalGenderIfNeeded(ResourceConfigValue* original_value, ResourceConfigValue* new_value, + android::StringPool* pool, ResourceEntry* entry, + const Pseudolocalizer::Method method, uint32_t gender_state) { + if (gender_state & MASK_FEMININE) { + GrammaticalGender(original_value, new_value, pool, entry, method, + android::ResTable_config::GRAMMATICAL_GENDER_FEMININE); + } + + if (gender_state & MASK_MASCULINE) { + GrammaticalGender(original_value, new_value, pool, entry, method, + android::ResTable_config::GRAMMATICAL_GENDER_MASCULINE); + } + + if (gender_state & MASK_NEUTER) { + GrammaticalGender(original_value, new_value, pool, entry, method, + android::ResTable_config::GRAMMATICAL_GENDER_NEUTER); + } +} + void PseudolocalizeIfNeeded(const Pseudolocalizer::Method method, ResourceConfigValue* original_value, android::StringPool* pool, - ResourceEntry* entry) { + ResourceEntry* entry, uint32_t gender_state, bool gender_flag) { Visitor visitor(pool, method); original_value->value->Accept(&visitor); @@ -333,8 +466,8 @@ void PseudolocalizeIfNeeded(const Pseudolocalizer::Method method, return; } - ConfigDescription config_with_accent = - ModifyConfigForPseudoLocale(original_value->config, method); + ConfigDescription config_with_accent = ModifyConfigForPseudoLocale( + original_value->config, method, android::ResTable_config::GRAMMATICAL_GENDER_ANY); ResourceConfigValue* new_config_value = entry->FindOrCreateValue(config_with_accent, original_value->product); @@ -342,6 +475,9 @@ void PseudolocalizeIfNeeded(const Pseudolocalizer::Method method, // Only use auto-generated pseudo-localization if none is defined. new_config_value->value = std::move(localized_value); } + if (gender_flag) { + GrammaticalGenderIfNeeded(original_value, new_config_value, pool, entry, method, gender_state); + } } // A value is pseudolocalizable if it does not define a locale (or is the default locale) and is @@ -356,16 +492,71 @@ static bool IsPseudolocalizable(ResourceConfigValue* config_value) { } // namespace +bool ParseGenderValuesAndSaveState(const std::string& grammatical_gender_values, + uint32_t* gender_state, android::IDiagnostics* diag) { + std::vector<std::string> values = util::SplitAndLowercase(grammatical_gender_values, ','); + for (size_t i = 0; i < values.size(); i++) { + if (values[i].length() != 0) { + if (values[i] == "f") { + *gender_state |= MASK_FEMININE; + } else if (values[i] == "m") { + *gender_state |= MASK_MASCULINE; + } else if (values[i] == "n") { + *gender_state |= MASK_NEUTER; + } else { + diag->Error(android::DiagMessage() << "Invalid grammatical gender value: " << values[i]); + return false; + } + } + } + return true; +} + +bool ParseGenderRatio(const std::string& grammatical_gender_ratio, float* gender_ratio, + android::IDiagnostics* diag) { + const char* input = grammatical_gender_ratio.c_str(); + char* endPtr; + errno = 0; + *gender_ratio = strtof(input, &endPtr); + if (endPtr == input || *endPtr != '\0' || errno == ERANGE || *gender_ratio < 0 || + *gender_ratio > 1) { + diag->Error(android::DiagMessage() + << "Invalid grammatical gender ratio: " << grammatical_gender_ratio + << ", must be a real number between 0 and 1"); + return false; + } + return true; +} + bool PseudolocaleGenerator::Consume(IAaptContext* context, ResourceTable* table) { + uint32_t gender_state = 0; + if (!ParseGenderValuesAndSaveState(grammatical_gender_values_, &gender_state, + context->GetDiagnostics())) { + return false; + } + + float gender_ratio = 0; + if (!ParseGenderRatio(grammatical_gender_ratio_, &gender_ratio, context->GetDiagnostics())) { + return false; + } + + std::random_device rd; + std::mt19937 gen(rd()); + std::uniform_real_distribution<> distrib(0.0, 1.0); + for (auto& package : table->packages) { for (auto& type : package->types) { for (auto& entry : type->entries) { + bool gender_flag = false; + if (distrib(gen) < gender_ratio) { + gender_flag = true; + } std::vector<ResourceConfigValue*> values = entry->FindValuesIf(IsPseudolocalizable); for (ResourceConfigValue* value : values) { PseudolocalizeIfNeeded(Pseudolocalizer::Method::kAccent, value, &table->string_pool, - entry.get()); + entry.get(), gender_state, gender_flag); PseudolocalizeIfNeeded(Pseudolocalizer::Method::kBidi, value, &table->string_pool, - entry.get()); + entry.get(), gender_state, gender_flag); } } } diff --git a/tools/aapt2/compile/PseudolocaleGenerator.h b/tools/aapt2/compile/PseudolocaleGenerator.h index 44e6e3e86f92..ce92008cdba1 100644 --- a/tools/aapt2/compile/PseudolocaleGenerator.h +++ b/tools/aapt2/compile/PseudolocaleGenerator.h @@ -27,8 +27,19 @@ std::unique_ptr<StyledString> PseudolocalizeStyledString(StyledString* string, Pseudolocalizer::Method method, android::StringPool* pool); -struct PseudolocaleGenerator : public IResourceTableConsumer { - bool Consume(IAaptContext* context, ResourceTable* table) override; +class PseudolocaleGenerator : public IResourceTableConsumer { + public: + explicit PseudolocaleGenerator(std::string grammatical_gender_values, + std::string grammatical_gender_ratio) + : grammatical_gender_values_(std::move(grammatical_gender_values)), + grammatical_gender_ratio_(std::move(grammatical_gender_ratio)) { + } + + bool Consume(IAaptContext* context, ResourceTable* table); + + private: + std::string grammatical_gender_values_; + std::string grammatical_gender_ratio_; }; } // namespace aapt diff --git a/tools/aapt2/compile/PseudolocaleGenerator_test.cpp b/tools/aapt2/compile/PseudolocaleGenerator_test.cpp index 2f90cbf722c2..1477ebf8473d 100644 --- a/tools/aapt2/compile/PseudolocaleGenerator_test.cpp +++ b/tools/aapt2/compile/PseudolocaleGenerator_test.cpp @@ -197,7 +197,7 @@ TEST(PseudolocaleGeneratorTest, PseudolocalizeOnlyDefaultConfigs) { val->SetTranslatable(false); std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); - PseudolocaleGenerator generator; + PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0")); ASSERT_TRUE(generator.Consume(context.get(), table.get())); // Normal pseudolocalization should take place. @@ -249,7 +249,7 @@ TEST(PseudolocaleGeneratorTest, PluralsArePseudolocalized) { expected->values = {util::make_unique<String>(table->string_pool.MakeRef("[žéŕö one]")), util::make_unique<String>(table->string_pool.MakeRef("[öñé one]"))}; - PseudolocaleGenerator generator; + PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0")); ASSERT_TRUE(generator.Consume(context.get(), table.get())); const auto* actual = test::GetValueForConfig<Plural>(table.get(), "com.pkg:plurals/foo", @@ -287,7 +287,7 @@ TEST(PseudolocaleGeneratorTest, RespectUntranslateableSections) { context->GetDiagnostics())); } - PseudolocaleGenerator generator; + PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0")); ASSERT_TRUE(generator.Consume(context.get(), table.get())); StyledString* new_styled_string = test::GetValueForConfig<StyledString>( @@ -305,4 +305,213 @@ TEST(PseudolocaleGeneratorTest, RespectUntranslateableSections) { EXPECT_NE(std::string::npos, new_string->value->find("world")); } +TEST(PseudolocaleGeneratorTest, PseudolocalizeGrammaticalGenderForString) { + std::unique_ptr<ResourceTable> table = + test::ResourceTableBuilder().AddString("android:string/foo", "foo").Build(); + + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0")); + ASSERT_TRUE(generator.Consume(context.get(), table.get())); + + String* locale = test::GetValueForConfig<String>(table.get(), "android:string/foo", + test::ParseConfigOrDie("en-rXA")); + ASSERT_NE(nullptr, locale); + + // Grammatical gendered string + auto config_feminine = test::ParseConfigOrDie("en-rXA-feminine"); + config_feminine.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + String* feminine = + test::GetValueForConfig<String>(table.get(), "android:string/foo", config_feminine); + ASSERT_NE(nullptr, feminine); + EXPECT_EQ(std::string("(F)") + *locale->value, *feminine->value); + + auto config_masculine = test::ParseConfigOrDie("en-rXA-masculine"); + config_masculine.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + String* masculine = + test::GetValueForConfig<String>(table.get(), "android:string/foo", config_masculine); + ASSERT_NE(nullptr, masculine); + EXPECT_EQ(std::string("(M)") + *locale->value, *masculine->value); + + auto config_neuter = test::ParseConfigOrDie("en-rXA-neuter"); + config_neuter.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + String* neuter = + test::GetValueForConfig<String>(table.get(), "android:string/foo", config_neuter); + ASSERT_NE(nullptr, neuter); + EXPECT_EQ(std::string("(N)") + *locale->value, *neuter->value); +} + +TEST(PseudolocaleGeneratorTest, PseudolocalizeGrammaticalGenderForPlural) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder().Build(); + std::unique_ptr<Plural> plural = util::make_unique<Plural>(); + plural->values = {util::make_unique<String>(table->string_pool.MakeRef("zero")), + util::make_unique<String>(table->string_pool.MakeRef("one"))}; + ASSERT_TRUE(table->AddResource(NewResourceBuilder(test::ParseNameOrDie("com.pkg:plurals/foo")) + .SetValue(std::move(plural)) + .Build(), + context->GetDiagnostics())); + PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0")); + ASSERT_TRUE(generator.Consume(context.get(), table.get())); + + Plural* actual = test::GetValueForConfig<Plural>(table.get(), "com.pkg:plurals/foo", + test::ParseConfigOrDie("en-rXA")); + ASSERT_NE(nullptr, actual); + + // Grammatical gendered Plural + auto config_feminine = test::ParseConfigOrDie("en-rXA-feminine"); + config_feminine.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + Plural* actual_feminine = + test::GetValueForConfig<Plural>(table.get(), "com.pkg:plurals/foo", config_feminine); + for (size_t i = 0; i < actual->values.size(); i++) { + if (actual->values[i]) { + String* locale = ValueCast<String>(actual->values[i].get()); + String* feminine = ValueCast<String>(actual_feminine->values[i].get()); + EXPECT_EQ(std::string("(F)") + *locale->value, *feminine->value); + } + } + + auto config_masculine = test::ParseConfigOrDie("en-rXA-masculine"); + config_masculine.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + Plural* actual_masculine = + test::GetValueForConfig<Plural>(table.get(), "com.pkg:plurals/foo", config_masculine); + ASSERT_NE(nullptr, actual_masculine); + for (size_t i = 0; i < actual->values.size(); i++) { + if (actual->values[i]) { + String* locale = ValueCast<String>(actual->values[i].get()); + String* masculine = ValueCast<String>(actual_masculine->values[i].get()); + EXPECT_EQ(std::string("(M)") + *locale->value, *masculine->value); + } + } + + auto config_neuter = test::ParseConfigOrDie("en-rXA-neuter"); + config_neuter.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + Plural* actual_neuter = + test::GetValueForConfig<Plural>(table.get(), "com.pkg:plurals/foo", config_neuter); + for (size_t i = 0; i < actual->values.size(); i++) { + if (actual->values[i]) { + String* locale = ValueCast<String>(actual->values[i].get()); + String* neuter = ValueCast<String>(actual_neuter->values[i].get()); + EXPECT_EQ(std::string("(N)") + *locale->value, *neuter->value); + } + } +} + +TEST(PseudolocaleGeneratorTest, PseudolocalizeGrammaticalGenderForStyledString) { + std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build(); + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder().Build(); + android::StyleString original_style; + original_style.str = "Hello world!"; + original_style.spans = {android::Span{"i", 1, 10}}; + + std::unique_ptr<StyledString> original = + util::make_unique<StyledString>(table->string_pool.MakeRef(original_style)); + ASSERT_TRUE(table->AddResource(NewResourceBuilder(test::ParseNameOrDie("android:string/foo")) + .SetValue(std::move(original)) + .Build(), + context->GetDiagnostics())); + PseudolocaleGenerator generator(std::string("f,m,n"), std::string("1.0")); + ASSERT_TRUE(generator.Consume(context.get(), table.get())); + + StyledString* locale = test::GetValueForConfig<StyledString>(table.get(), "android:string/foo", + test::ParseConfigOrDie("en-rXA")); + ASSERT_NE(nullptr, locale); + EXPECT_EQ(1, locale->value->spans.size()); + EXPECT_EQ(std::string("i"), *locale->value->spans[0].name); + + // Grammatical gendered StyledString + auto config_feminine = test::ParseConfigOrDie("en-rXA-feminine"); + config_feminine.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + StyledString* feminine = + test::GetValueForConfig<StyledString>(table.get(), "android:string/foo", config_feminine); + ASSERT_NE(nullptr, feminine); + EXPECT_EQ(1, feminine->value->spans.size()); + EXPECT_EQ(std::string("i"), *feminine->value->spans[0].name); + EXPECT_EQ(std::string("(F)") + locale->value->value, feminine->value->value); + + auto config_masculine = test::ParseConfigOrDie("en-rXA-masculine"); + config_masculine.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + StyledString* masculine = + test::GetValueForConfig<StyledString>(table.get(), "android:string/foo", config_masculine); + ASSERT_NE(nullptr, masculine); + EXPECT_EQ(1, masculine->value->spans.size()); + EXPECT_EQ(std::string("i"), *masculine->value->spans[0].name); + EXPECT_EQ(std::string("(M)") + locale->value->value, masculine->value->value); + + auto config_neuter = test::ParseConfigOrDie("en-rXA-neuter"); + config_neuter.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + StyledString* neuter = + test::GetValueForConfig<StyledString>(table.get(), "android:string/foo", config_neuter); + ASSERT_NE(nullptr, neuter); + EXPECT_EQ(1, neuter->value->spans.size()); + EXPECT_EQ(std::string("i"), *neuter->value->spans[0].name); + EXPECT_EQ(std::string("(N)") + locale->value->value, neuter->value->value); +} + +TEST(PseudolocaleGeneratorTest, GrammaticalGenderForCertainValues) { + // single gender value + std::unique_ptr<ResourceTable> table_0 = + test::ResourceTableBuilder().AddString("android:string/foo", "foo").Build(); + + std::unique_ptr<IAaptContext> context_0 = test::ContextBuilder().Build(); + PseudolocaleGenerator generator_0(std::string("f"), std::string("1.0")); + ASSERT_TRUE(generator_0.Consume(context_0.get(), table_0.get())); + + String* locale_0 = test::GetValueForConfig<String>(table_0.get(), "android:string/foo", + test::ParseConfigOrDie("en-rXA")); + ASSERT_NE(nullptr, locale_0); + + auto config_feminine = test::ParseConfigOrDie("en-rXA-feminine"); + config_feminine.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + String* feminine_0 = + test::GetValueForConfig<String>(table_0.get(), "android:string/foo", config_feminine); + ASSERT_NE(nullptr, feminine_0); + EXPECT_EQ(std::string("(F)") + *locale_0->value, *feminine_0->value); + + auto config_masculine = test::ParseConfigOrDie("en-rXA-masculine"); + config_masculine.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + String* masculine_0 = + test::GetValueForConfig<String>(table_0.get(), "android:string/foo", config_masculine); + EXPECT_EQ(nullptr, masculine_0); + + auto config_neuter = test::ParseConfigOrDie("en-rXA-neuter"); + config_neuter.sdkVersion = android::ResTable_config::SDKVERSION_ANY; + String* neuter_0 = + test::GetValueForConfig<String>(table_0.get(), "android:string/foo", config_neuter); + EXPECT_EQ(nullptr, neuter_0); + + // multiple gender values + std::unique_ptr<ResourceTable> table_1 = + test::ResourceTableBuilder().AddString("android:string/foo", "foo").Build(); + + std::unique_ptr<IAaptContext> context_1 = test::ContextBuilder().Build(); + PseudolocaleGenerator generator_1(std::string("f,n"), std::string("1.0")); + ASSERT_TRUE(generator_1.Consume(context_1.get(), table_1.get())); + + String* locale_1 = test::GetValueForConfig<String>(table_1.get(), "android:string/foo", + test::ParseConfigOrDie("en-rXA")); + ASSERT_NE(nullptr, locale_1); + + String* feminine_1 = + test::GetValueForConfig<String>(table_1.get(), "android:string/foo", config_feminine); + ASSERT_NE(nullptr, feminine_1); + EXPECT_EQ(std::string("(F)") + *locale_1->value, *feminine_1->value); + + String* masculine_1 = + test::GetValueForConfig<String>(table_1.get(), "android:string/foo", config_masculine); + EXPECT_EQ(nullptr, masculine_1); + + String* neuter_1 = + test::GetValueForConfig<String>(table_1.get(), "android:string/foo", config_neuter); + ASSERT_NE(nullptr, neuter_1); + EXPECT_EQ(std::string("(N)") + *locale_1->value, *neuter_1->value); + + // invalid gender value + std::unique_ptr<ResourceTable> table_2 = + test::ResourceTableBuilder().AddString("android:string/foo", "foo").Build(); + + std::unique_ptr<IAaptContext> context_2 = test::ContextBuilder().Build(); + PseudolocaleGenerator generator_2(std::string("invald,"), std::string("1.0")); + ASSERT_FALSE(generator_2.Consume(context_2.get(), table_2.get())); +} + } // namespace aapt |