Merge "Disable flaky app launch from lockscree assertion" into main
diff --git a/cmds/uinput/README.md b/cmds/uinput/README.md
index 47e1dad..1ce8f9f 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",
+ // ...
+}
+```
-1. `register`
+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`
+
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 |
+| `type` | integer | `UI_SET_` control type |
+| `data` | integer array | control values |
-Device ID is used for matching the subsequent commands to a specific device
-to avoid ambiguity when multiple devices are registered.
+`ff_effects_max` must be provided if `UI_SET_FFBIT` is used in `configuration`.
-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.
+`abs_info` fields are provided to set the device axes information. It is an array of below objects:
| Field | Type | Description |
|:-------------:|:-------------:|:-------------------------- |
-| type | integer | UI_SET_ control type |
-| data | int array | control values |
+| `code` | integer | Axis code |
+| `info` | object | Axis information object |
-Device ff_effects_max must be provided if FFBIT is set.
+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]:
-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 |
-
-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 @@
"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 @@
}
]
}
-
```
-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 @@
}
```
-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 @@
}
```
-### 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.
+## Notes
-[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
+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 eaefe84..0d73695 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -26391,6 +26391,17 @@
method public abstract void onDisconnect(android.media.midi.MidiReceiver);
}
+ public abstract class MidiUmpDeviceService extends android.app.Service {
+ ctor public MidiUmpDeviceService();
+ method @Nullable public final android.media.midi.MidiDeviceInfo getDeviceInfo();
+ method @NonNull public final java.util.List<android.media.midi.MidiReceiver> getOutputPortReceivers();
+ method @Nullable public android.os.IBinder onBind(@NonNull android.content.Intent);
+ method public void onClose();
+ method public void onDeviceStatusChanged(@Nullable android.media.midi.MidiDeviceStatus);
+ method @NonNull public abstract java.util.List<android.media.midi.MidiReceiver> onGetInputPortReceivers();
+ field public static final String SERVICE_INTERFACE = "android.media.midi.MidiUmpDeviceService";
+ }
+
}
package android.media.projection {
diff --git a/core/java/android/app/HomeVisibilityListener.java b/core/java/android/app/HomeVisibilityListener.java
index ca20648..0b5a5ed1 100644
--- a/core/java/android/app/HomeVisibilityListener.java
+++ b/core/java/android/app/HomeVisibilityListener.java
@@ -25,6 +25,7 @@
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 @@
@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 @@
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 729e555..0857c96 100644
--- a/core/java/android/app/LocaleConfig.java
+++ b/core/java/android/app/LocaleConfig.java
@@ -40,7 +40,7 @@
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 @@
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 0f66e93..8da8442 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 4295517..49543a1 100644
--- a/core/java/android/app/usage/IUsageStatsManager.aidl
+++ b/core/java/android/app/usage/IUsageStatsManager.aidl
@@ -87,6 +87,8 @@
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 4b0ca28..ecf16439 100644
--- a/core/java/android/app/usage/UsageStatsManager.java
+++ b/core/java/android/app/usage/UsageStatsManager.java
@@ -1528,6 +1528,22 @@
}
}
+ /**
+ * 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/content/pm/IShortcutService.aidl b/core/java/android/content/pm/IShortcutService.aidl
index c9735b0..86087cb 100644
--- a/core/java/android/content/pm/IShortcutService.aidl
+++ b/core/java/android/content/pm/IShortcutService.aidl
@@ -61,7 +61,7 @@
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 3791777..289519d 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -2373,6 +2373,7 @@
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 @@
/**
* 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 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,
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
index 048289f..508eeed 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -1456,8 +1456,8 @@
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 @@
}
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 @@
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 b225de4..23b9d0b 100644
--- a/core/java/android/content/res/AssetManager.java
+++ b/core/java/android/content/res/AssetManager.java
@@ -1480,9 +1480,13 @@
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 @@
}
/**
+ * 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 @@
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 1c8276c..62630c8 100644
--- a/core/java/android/content/res/Configuration.java
+++ b/core/java/android/content/res/Configuration.java
@@ -154,7 +154,6 @@
/**
* Current user preference for the grammatical gender.
*/
- @GrammaticalGender
private int mGrammaticalGender;
/** @hide */
@@ -167,6 +166,13 @@
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 @@
} 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 @@
seq = 0;
windowConfiguration.setToDefaults();
fontWeightAdjustment = FONT_WEIGHT_ADJUSTMENT_UNDEFINED;
- mGrammaticalGender = GRAMMATICAL_GENDER_NOT_SPECIFIED;
+ mGrammaticalGender = GRAMMATICAL_GENDER_UNDEFINED;
}
/**
@@ -1773,7 +1779,8 @@
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 @@
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 @@
*/
@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 @@
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 62a46b6..3e0ab90 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 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 @@
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 @@
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 @@
}
void recycle() {
- mStringAttrNames = new String[0];
mTag = null;
sPool.get().release(this);
}
@@ -230,33 +199,79 @@
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;
+ }
+ }
+
+ 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:
+ return true;
+ default:
+ return false;
}
}
@@ -264,55 +279,31 @@
this.mTag = tag;
mChildTagMask = 0;
switch (tag) {
- case TAG_ACTION:
- case TAG_CATEGORY:
- case TAG_PACKAGE:
- case TAG_PERMISSION_GROUP:
- case TAG_PERMISSION_TREE:
- case TAG_SUPPORTS_GL_TEXTURE:
- 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_SDK:
- setStringAttrNames(NAME_ATTR);
- break;
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 61d5aa6..f2468b5 100644
--- a/core/java/android/content/res/ResourcesImpl.java
+++ b/core/java/android/content/res/ResourcesImpl.java
@@ -407,14 +407,12 @@
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 @@
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 @@
}
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 2e84636..48adfb9 100644
--- a/core/java/android/content/res/TypedArray.java
+++ b/core/java/android/content/res/TypedArray.java
@@ -1393,16 +1393,17 @@
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 8b5e6c6..cae353b 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 @@
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 @@
/**
* 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().validateResStringAttr(index, stringValue);
+ 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().validateStrAttr(attrName, attrValue);
}
}
diff --git a/core/java/android/content/res/XmlBlock.java b/core/java/android/content/res/XmlBlock.java
index 3afc830..7649b32 100644
--- a/core/java/android/content/res/XmlBlock.java
+++ b/core/java/android/content/res/XmlBlock.java
@@ -319,7 +319,11 @@
"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 b7489229..706e75e 100644
--- a/core/java/android/database/sqlite/SQLiteConnection.java
+++ b/core/java/android/database/sqlite/SQLiteConnection.java
@@ -113,9 +113,6 @@
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 @@
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 @@
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 @@
/**
* 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 @@
* 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 @@
* 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 @@
mRecentOperations.dump(printer);
if (verbose) {
- synchronized (mCacheLock) {
- mPreparedStatementCache.dump(printer);
- }
+ mPreparedStatementCache.dump(printer);
}
}
@@ -1430,7 +1431,7 @@
}
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 @@
statement.mNumParameters = numParameters;
statement.mType = type;
statement.mReadOnly = readOnly;
+ statement.mSeqNum = seqNum;
return statement;
}
@@ -1461,10 +1463,10 @@
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 @@
// 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 @@
}
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 b35a2e4..ad335b6 100644
--- a/core/java/android/database/sqlite/SQLiteConnectionPool.java
+++ b/core/java/android/database/sqlite/SQLiteConnectionPool.java
@@ -111,6 +111,13 @@
@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 @@
}
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/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 44fed67..60b11b4 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -2986,8 +2986,6 @@
ImeTracing.getInstance().triggerServiceDump(
"InputMethodService#applyVisibilityInInsetsConsumerIfNecessary", mDumper,
null /* icProto */);
- ImeTracker.forLogging().onProgress(mCurStatsToken,
- ImeTracker.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER);
mPrivOps.applyImeVisibilityAsync(setVisible
? mCurShowInputToken : mCurHideInputToken, setVisible, mCurStatsToken);
}
diff --git a/core/java/android/os/LocaleList.java b/core/java/android/os/LocaleList.java
index b74bb33..82cdd28 100644
--- a/core/java/android/os/LocaleList.java
+++ b/core/java/android/os/LocaleList.java
@@ -30,6 +30,7 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
+import java.util.List;
import java.util.Locale;
/**
@@ -151,6 +152,25 @@
}
/**
+ * 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 ea5499f..954ee3c 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/InputEventSender.java b/core/java/android/view/InputEventSender.java
index 64f62c7..c843bbe 100644
--- a/core/java/android/view/InputEventSender.java
+++ b/core/java/android/view/InputEventSender.java
@@ -18,6 +18,7 @@
import android.compat.annotation.UnsupportedAppUsage;
import android.os.Build;
+import android.os.Handler;
import android.os.Looper;
import android.os.MessageQueue;
import android.util.Log;
@@ -25,6 +26,10 @@
import dalvik.system.CloseGuard;
import java.lang.ref.WeakReference;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.RunnableFuture;
/**
* Provides a low-level mechanism for an application to send input events.
@@ -37,10 +42,10 @@
private long mSenderPtr;
- // We keep references to the input channel and message queue objects here so that
- // they are not GC'd while the native peer of the receiver is using them.
+ // We keep references to the input channel and message queue objects (indirectly through
+ // Handler) here so that they are not GC'd while the native peer of the receiver is using them.
private InputChannel mInputChannel;
- private MessageQueue mMessageQueue;
+ private Handler mHandler;
private static native long nativeInit(WeakReference<InputEventSender> sender,
InputChannel inputChannel, MessageQueue messageQueue);
@@ -63,9 +68,9 @@
}
mInputChannel = inputChannel;
- mMessageQueue = looper.getQueue();
+ mHandler = new Handler(looper);
mSenderPtr = nativeInit(new WeakReference<InputEventSender>(this),
- mInputChannel, mMessageQueue);
+ mInputChannel, looper.getQueue());
mCloseGuard.open("InputEventSender.dispose");
}
@@ -98,8 +103,8 @@
nativeDispose(mSenderPtr);
mSenderPtr = 0;
}
+ mHandler = null;
mInputChannel = null;
- mMessageQueue = null;
}
/**
@@ -122,8 +127,8 @@
}
/**
- * Sends an input event.
- * Must be called on the same Looper thread to which the sender is attached.
+ * Sends an input event. Can be called from any thread. Do not call this if the looper thread
+ * is blocked! It would cause a deadlock.
*
* @param seq The input event sequence number.
* @param event The input event to send.
@@ -140,6 +145,28 @@
return false;
}
+ if (mHandler.getLooper().isCurrentThread()) {
+ return sendInputEventInternal(seq, event);
+ }
+ // This is being called on another thread. Post a runnable to the looper thread
+ // with the event injection, and wait until it's processed.
+ final RunnableFuture<Boolean> task = new FutureTask<>(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ return sendInputEventInternal(seq, event);
+ }
+ });
+ mHandler.post(task);
+ try {
+ return task.get();
+ } catch (InterruptedException exc) {
+ throw new IllegalStateException("Interrupted while sending " + event + ": " + exc);
+ } catch (ExecutionException exc) {
+ throw new IllegalStateException("Couldn't send " + event + ": " + exc);
+ }
+ }
+
+ private boolean sendInputEventInternal(int seq, InputEvent event) {
if (event instanceof KeyEvent) {
return nativeSendKeyEvent(mSenderPtr, seq, (KeyEvent)event);
} else {
diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java
index e153bb7..43fa0be 100644
--- a/core/java/android/window/TaskFragmentOperation.java
+++ b/core/java/android/window/TaskFragmentOperation.java
@@ -80,6 +80,14 @@
*/
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 @@
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 @@
@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 @@
mBundle = bundle;
mSecondaryFragmentToken = secondaryFragmentToken;
mAnimationParams = animationParams;
+ mIsolatedNav = isolatedNav;
}
private TaskFragmentOperation(Parcel in) {
@@ -140,6 +153,7 @@
mBundle = in.readBundle(getClass().getClassLoader());
mSecondaryFragmentToken = in.readStrongBinder();
mAnimationParams = in.readTypedObject(TaskFragmentAnimationParams.CREATOR);
+ mIsolatedNav = in.readBoolean();
}
@Override
@@ -151,6 +165,7 @@
dest.writeBundle(mBundle);
dest.writeStrongBinder(mSecondaryFragmentToken);
dest.writeTypedObject(mAnimationParams, flags);
+ dest.writeBoolean(mIsolatedNav);
}
@NonNull
@@ -223,6 +238,14 @@
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 @@
if (mAnimationParams != null) {
sb.append(", animationParams=").append(mAnimationParams);
}
+ sb.append(", isolatedNav=").append(mIsolatedNav);
sb.append('}');
return sb.toString();
@@ -253,7 +277,7 @@
@Override
public int hashCode() {
return Objects.hash(mOpType, mTaskFragmentCreationParams, mActivityToken, mActivityIntent,
- mBundle, mSecondaryFragmentToken, mAnimationParams);
+ mBundle, mSecondaryFragmentToken, mAnimationParams, mIsolatedNav);
}
@Override
@@ -268,7 +292,8 @@
&& 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 @@
@Nullable
private TaskFragmentAnimationParams mAnimationParams;
+ private boolean mIsolatedNav;
+
/**
* @param opType the {@link OperationType} of this {@link TaskFragmentOperation}.
*/
@@ -363,12 +390,22 @@
}
/**
+ * 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/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
index 8a5c7ef..30ebbe2 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
@@ -386,8 +386,12 @@
@Nullable ImeTracker.Token statsToken) {
final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
if (ops == null) {
+ ImeTracker.forLogging().onFailed(statsToken,
+ ImeTracker.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER);
return;
}
+ ImeTracker.forLogging().onProgress(statsToken,
+ ImeTracker.PHASE_IME_APPLY_VISIBILITY_INSETS_CONSUMER);
try {
ops.applyImeVisibilityAsync(showOrHideInputToken, setVisible, statsToken);
} catch (RemoteException e) {
diff --git a/core/jni/android_util_AssetManager.cpp b/core/jni/android_util_AssetManager.cpp
index 979c9e3..1afae29 100644
--- a/core/jni/android_util_AssetManager.cpp
+++ b/core/jni/android_util_AssetManager.cpp
@@ -347,14 +347,23 @@
}
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 @@
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 @@
// 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 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 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 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},
+ // 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},
+ // 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},
+ // 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},
+ {"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},
+ // 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},
+ // 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},
+ // 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 0000000..bd77e59
--- /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 0000000..8c52a41
--- /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 51d4018..40673c1 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 6d6cfc7..9fa4bef 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 12bc406..8698e86 100644
--- a/core/res/res/values-watch/styles_material.xml
+++ b/core/res/res/values-watch/styles_material.xml
@@ -107,4 +107,14 @@
<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 7602f69..f63f8b3 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>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 978849d..8e52633 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" />
diff --git a/core/tests/coretests/src/android/database/DatabaseErrorHandlerTest.java b/core/tests/coretests/src/android/database/DatabaseErrorHandlerTest.java
index 91c7687..9677584 100644
--- a/core/tests/coretests/src/android/database/DatabaseErrorHandlerTest.java
+++ b/core/tests/coretests/src/android/database/DatabaseErrorHandlerTest.java
@@ -19,10 +19,13 @@
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 @@
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 @@
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 @@
* 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 @@
}
}
}
-}
\ 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 95b0e32..e8d90f5 100644
--- a/core/tests/coretests/src/android/database/DatabaseGeneralTest.java
+++ b/core/tests/coretests/src/android/database/DatabaseGeneralTest.java
@@ -914,6 +914,24 @@
verifyLookasideStats(true);
}
+ void verifyLookasideStats(boolean expectDisabled) {
+ boolean dbStatFound = false;
+ SQLiteDebug.PagerStats info = SQLiteDebug.getDatabaseInfo();
+ for (SQLiteDebug.DbStats dbStat : info.dbStats) {
+ if (dbStat.dbName.endsWith(mDatabaseFile.getName()) && !dbStat.arePoolStats) {
+ dbStatFound = true;
+ Log.i(TAG, "Lookaside for " + dbStat.dbName + " " + dbStat.lookaside);
+ if (expectDisabled) {
+ assertTrue("lookaside slots count should be zero", dbStat.lookaside == 0);
+ } else {
+ assertTrue("lookaside slots count should be greater than zero",
+ dbStat.lookaside > 0);
+ }
+ }
+ }
+ assertTrue("No dbstat found for " + mDatabaseFile.getName(), dbStatFound);
+ }
+
@SmallTest
public void testOpenParamsSetLookasideConfigValidation() {
try {
@@ -930,24 +948,6 @@
}
}
- void verifyLookasideStats(boolean expectDisabled) {
- boolean dbStatFound = false;
- SQLiteDebug.PagerStats info = SQLiteDebug.getDatabaseInfo();
- for (SQLiteDebug.DbStats dbStat : info.dbStats) {
- if (dbStat.dbName.endsWith(mDatabaseFile.getName())) {
- dbStatFound = true;
- Log.i(TAG, "Lookaside for " + dbStat.dbName + " " + dbStat.lookaside);
- if (expectDisabled) {
- assertTrue("lookaside slots count should be zero", dbStat.lookaside == 0);
- } else {
- assertTrue("lookaside slots count should be greater than zero",
- dbStat.lookaside > 0);
- }
- }
- }
- assertTrue("No dbstat found for " + mDatabaseFile.getName(), dbStatFound);
- }
-
@LargeTest
public void testDefaultDatabaseErrorHandler() {
DefaultDatabaseErrorHandler errorHandler = new DefaultDatabaseErrorHandler();
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index 94e23e7..87e6b18 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/graphics/java/android/graphics/RecordingCanvas.java b/graphics/java/android/graphics/RecordingCanvas.java
index a5184f2..635e78e 100644
--- a/graphics/java/android/graphics/RecordingCanvas.java
+++ b/graphics/java/android/graphics/RecordingCanvas.java
@@ -276,9 +276,7 @@
@CriticalNative
private static native void nResetDisplayListCanvas(long canvas, long node,
int width, int height);
- @CriticalNative
private static native int nGetMaximumTextureWidth();
- @CriticalNative
private static native int nGetMaximumTextureHeight();
@CriticalNative
private static native void nEnableZ(long renderer, boolean enableZ);
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 d94e8e4..4d73c20 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 @@
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 a2f75e0..f95f3ff 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 @@
// 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 4dafbd1..5de6acf 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.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 @@
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/tests/flicker/Android.bp b/libs/WindowManager/Shell/tests/flicker/Android.bp
index 208ae84..b062fbd 100644
--- a/libs/WindowManager/Shell/tests/flicker/Android.bp
+++ b/libs/WindowManager/Shell/tests/flicker/Android.bp
@@ -24,6 +24,11 @@
}
filegroup {
+ name: "WMShellFlickerTestsUtils-src",
+ srcs: ["src/com/android/wm/shell/flicker/utils/*.kt"],
+}
+
+filegroup {
name: "WMShellFlickerTestsBase-src",
srcs: ["src/com/android/wm/shell/flicker/*.kt"],
}
@@ -53,6 +58,28 @@
],
}
+java_library {
+ name: "wm-shell-flicker-utils",
+ platform_apis: true,
+ optimize: {
+ enabled: false,
+ },
+ srcs: [
+ ":WMShellFlickerTestsUtils-src",
+ ],
+ static_libs: [
+ "androidx.test.ext.junit",
+ "flickertestapplib",
+ "flickerlib",
+ "flickerlib-helpers",
+ "platform-test-annotations",
+ "wm-flicker-common-app-helpers",
+ "wm-flicker-common-assertions",
+ "launcher-helper-lib",
+ "launcher-aosp-tapl",
+ ],
+}
+
java_defaults {
name: "WMShellFlickerTestsDefault",
manifest: "manifests/AndroidManifest.xml",
@@ -65,6 +92,7 @@
test_suites: ["device-tests"],
libs: ["android.test.runner"],
static_libs: [
+ "wm-shell-flicker-utils",
"androidx.test.ext.junit",
"flickertestapplib",
"flickerlib",
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
index d2fe9fe..735fbfb 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/BaseTest.kt
@@ -21,6 +21,7 @@
import android.tools.device.flicker.legacy.LegacyFlickerTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.wm.shell.flicker.utils.ICommonAssertions
/**
* Base test class containing common assertions for [ComponentNameMatcher.NAV_BAR],
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
index 69c8ecd..77f14f1 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/BaseAppCompat.kt
@@ -24,11 +24,10 @@
import com.android.server.wm.flicker.helpers.LetterboxAppHelper
import com.android.server.wm.flicker.helpers.setRotation
import com.android.wm.shell.flicker.BaseTest
-import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.appWindowIsVisibleAtStart
-import com.android.wm.shell.flicker.appWindowKeepVisible
-import com.android.wm.shell.flicker.layerKeepVisible
-
+import com.android.wm.shell.flicker.utils.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.appWindowIsVisibleAtStart
+import com.android.wm.shell.flicker.utils.appWindowKeepVisible
+import com.android.wm.shell.flicker.utils.layerKeepVisible
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
@@ -37,9 +36,7 @@
protected val context: Context = instrumentation.context
protected val letterboxApp = LetterboxAppHelper(instrumentation)
- @JvmField
- @Rule
- val letterboxRule: LetterboxRule = LetterboxRule()
+ @JvmField @Rule val letterboxRule: LetterboxRule = LetterboxRule()
/** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt
index 5a1136f..744e8c2 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/LetterboxRule.kt
@@ -23,16 +23,14 @@
import org.junit.runner.Description
import org.junit.runners.model.Statement
-/**
- * JUnit Rule to handle letterboxStyles and states
- */
+/** JUnit Rule to handle letterboxStyles and states */
class LetterboxRule(
- private val withLetterboxEducationEnabled: Boolean = false,
- private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(),
- private val cmdHelper: CommandsHelper = CommandsHelper.getInstance(instrumentation)
+ private val withLetterboxEducationEnabled: Boolean = false,
+ private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation(),
+ private val cmdHelper: CommandsHelper = CommandsHelper.getInstance(instrumentation)
) : TestRule {
- private val execAdb: (String) -> String = {cmd -> cmdHelper.executeShellCommand(cmd)}
+ private val execAdb: (String) -> String = { cmd -> cmdHelper.executeShellCommand(cmd) }
private lateinit var _letterboxStyle: MutableMap<String, String>
val letterboxStyle: Map<String, String>
@@ -62,8 +60,7 @@
var hasLetterboxEducationStateChanged = false
if ("$withLetterboxEducationEnabled" != isLetterboxEducationEnabled) {
hasLetterboxEducationStateChanged = true
- execAdb("wm set-letterbox-style --isEducationEnabled " +
- withLetterboxEducationEnabled)
+ execAdb("wm set-letterbox-style --isEducationEnabled " + withLetterboxEducationEnabled)
}
return try {
object : Statement() {
@@ -74,8 +71,8 @@
}
} finally {
if (hasLetterboxEducationStateChanged) {
- execAdb("wm set-letterbox-style --isEducationEnabled " +
- isLetterboxEducationEnabled
+ execAdb(
+ "wm set-letterbox-style --isEducationEnabled " + isLetterboxEducationEnabled
)
}
resetLetterboxStyle()
@@ -100,9 +97,10 @@
execAdb("wm reset-letterbox-style")
}
- private fun asInt(str: String?): Int? = try {
- str?.toInt()
- } catch (e: NumberFormatException) {
- null
- }
-}
\ No newline at end of file
+ private fun asInt(str: String?): Int? =
+ try {
+ str?.toInt()
+ } catch (e: NumberFormatException) {
+ null
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt
index e6ca261..2fa1ec3 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/OpenTransparentActivityTest.kt
@@ -54,9 +54,7 @@
/** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit
get() = {
- setup {
- letterboxTranslucentLauncherApp.launchViaIntent(wmHelper)
- }
+ setup { letterboxTranslucentLauncherApp.launchViaIntent(wmHelper) }
transitions {
waitAndGetLaunchTransparent()?.click() ?: error("Launch Transparent not found")
}
@@ -66,9 +64,7 @@
}
}
- /**
- * Checks the transparent activity is launched on top of the opaque one
- */
+ /** Checks the transparent activity is launched on top of the opaque one */
@Postsubmit
@Test
fun translucentActivityIsLaunchedOnTopOfOpaqueActivity() {
@@ -79,18 +75,14 @@
}
}
- /**
- * Checks that the activity is letterboxed
- */
+ /** Checks that the activity is letterboxed */
@Postsubmit
@Test
fun translucentActivityIsLetterboxed() {
flicker.assertLayers { isVisible(ComponentNameMatcher.LETTERBOX) }
}
- /**
- * Checks that the translucent activity inherits bounds from the opaque one.
- */
+ /** Checks that the translucent activity inherits bounds from the opaque one. */
@Postsubmit
@Test
fun translucentActivityInheritsBoundsFromOpaqueActivity() {
@@ -100,29 +92,26 @@
}
}
- /**
- * Checks that the translucent activity has rounded corners
- */
+ /** Checks that the translucent activity has rounded corners */
@Postsubmit
@Test
fun translucentActivityHasRoundedCorners() {
- flicker.assertLayersEnd {
- this.hasRoundedCorners(letterboxTranslucentApp)
- }
+ flicker.assertLayersEnd { this.hasRoundedCorners(letterboxTranslucentApp) }
}
companion object {
/**
* Creates the test configurations.
*
- * See [FlickerTestFactory.rotationTests] for configuring screen orientation and
- * navigation modes.
+ * See [FlickerTestFactory.rotationTests] for configuring screen orientation and navigation
+ * modes.
*/
@Parameterized.Parameters(name = "{0}")
@JvmStatic
fun getParams(): Collection<FlickerTest> {
- return LegacyFlickerTestFactory
- .nonRotationTests(supportedRotations = listOf(Rotation.ROTATION_90))
+ return LegacyFlickerTestFactory.nonRotationTests(
+ supportedRotations = listOf(Rotation.ROTATION_90)
+ )
}
}
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt
index 47a7e65..b74aa1d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/QuickSwitchLauncherToLetterboxAppTest.kt
@@ -49,8 +49,7 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
-class QuickSwitchLauncherToLetterboxAppTest(flicker: LegacyFlickerTest) :
- BaseAppCompat(flicker) {
+class QuickSwitchLauncherToLetterboxAppTest(flicker: LegacyFlickerTest) : BaseAppCompat(flicker) {
/** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit = {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/TransparentBaseAppCompat.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/TransparentBaseAppCompat.kt
index ea0392c..9792c85 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/TransparentBaseAppCompat.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/appcompat/TransparentBaseAppCompat.kt
@@ -33,20 +33,20 @@
abstract class TransparentBaseAppCompat(flicker: LegacyFlickerTest) : BaseTest(flicker) {
protected val context: Context = instrumentation.context
- protected val letterboxTranslucentLauncherApp = LetterboxAppHelper(
- instrumentation,
- launcherName = ActivityOptions.LaunchTransparentActivity.LABEL,
- component = ActivityOptions.LaunchTransparentActivity.COMPONENT.toFlickerComponent()
- )
- protected val letterboxTranslucentApp = LetterboxAppHelper(
- instrumentation,
- launcherName = ActivityOptions.TransparentActivity.LABEL,
- component = ActivityOptions.TransparentActivity.COMPONENT.toFlickerComponent()
- )
+ protected val letterboxTranslucentLauncherApp =
+ LetterboxAppHelper(
+ instrumentation,
+ launcherName = ActivityOptions.LaunchTransparentActivity.LABEL,
+ component = ActivityOptions.LaunchTransparentActivity.COMPONENT.toFlickerComponent()
+ )
+ protected val letterboxTranslucentApp =
+ LetterboxAppHelper(
+ instrumentation,
+ launcherName = ActivityOptions.TransparentActivity.LABEL,
+ component = ActivityOptions.TransparentActivity.COMPONENT.toFlickerComponent()
+ )
- @JvmField
- @Rule
- val letterboxRule: LetterboxRule = LetterboxRule()
+ @JvmField @Rule val letterboxRule: LetterboxRule = LetterboxRule()
@Before
fun before() {
@@ -54,10 +54,7 @@
}
protected fun FlickerTestData.waitAndGetLaunchTransparent(): UiObject2? =
- device.wait(
- Until.findObject(By.text("Launch Transparent")),
- FIND_TIMEOUT
- )
+ device.wait(Until.findObject(By.text("Launch Transparent")), FIND_TIMEOUT)
protected fun FlickerTestData.goBack() = device.pressBack()
}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt
index f165cb1..9cc9fb9 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/bubble/ChangeActiveActivityFromBubbleTest.kt
@@ -42,8 +42,7 @@
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FlakyTest(bugId = 217777115)
-class ChangeActiveActivityFromBubbleTest(flicker: LegacyFlickerTest) :
- BaseBubbleScreen(flicker) {
+class ChangeActiveActivityFromBubbleTest(flicker: LegacyFlickerTest) : BaseBubbleScreen(flicker) {
/** {@inheritDoc} */
override val transition: FlickerBuilder.() -> Unit
get() = buildTransition {
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipFromSplitScreenOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipFromSplitScreenOnGoToHomeTest.kt
index 8bd44c3..c335d3d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipFromSplitScreenOnGoToHomeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/AutoEnterPipFromSplitScreenOnGoToHomeTest.kt
@@ -28,7 +28,7 @@
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.helpers.SimpleAppHelper
import com.android.server.wm.flicker.testapp.ActivityOptions
-import com.android.wm.shell.flicker.SplitScreenUtils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.Assume
import org.junit.FixMethodOrder
import org.junit.Test
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt
index 4f88184..421ad75 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipDownOnShelfHeightChange.kt
@@ -21,7 +21,7 @@
import android.tools.device.flicker.legacy.FlickerBuilder
import android.tools.device.flicker.legacy.LegacyFlickerTest
import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.Direction
+import com.android.wm.shell.flicker.utils.Direction
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt
index 9a2fa09..a8fb63d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipShelfHeightTransition.kt
@@ -22,7 +22,7 @@
import android.tools.device.flicker.legacy.LegacyFlickerTest
import android.tools.device.flicker.legacy.LegacyFlickerTestFactory
import com.android.server.wm.flicker.helpers.FixedOrientationAppHelper
-import com.android.wm.shell.flicker.Direction
+import com.android.wm.shell.flicker.utils.Direction
import org.junit.Test
import org.junit.runners.Parameterized
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt
index afb4af6..992f1bc 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/MovePipUpOnShelfHeightChangeTest.kt
@@ -21,7 +21,7 @@
import android.tools.device.flicker.legacy.FlickerBuilder
import android.tools.device.flicker.legacy.LegacyFlickerTest
import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.Direction
+import com.android.wm.shell.flicker.utils.Direction
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt
index 0432a84..d4cd6da 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipMenuTests.kt
@@ -20,8 +20,8 @@
import androidx.test.filters.RequiresDevice
import androidx.test.uiautomator.UiObject2
import com.android.server.wm.flicker.testapp.ActivityOptions
-import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
-import com.android.wm.shell.flicker.wait
+import com.android.wm.shell.flicker.utils.SYSTEM_UI_PACKAGE_NAME
+import com.android.wm.shell.flicker.utils.wait
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt
index 90406c5..4402e21 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipNotificationTests.kt
@@ -21,11 +21,11 @@
import android.os.Bundle
import android.service.notification.StatusBarNotification
import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.NotificationListener.Companion.findNotification
-import com.android.wm.shell.flicker.NotificationListener.Companion.startNotificationListener
-import com.android.wm.shell.flicker.NotificationListener.Companion.stopNotificationListener
-import com.android.wm.shell.flicker.NotificationListener.Companion.waitForNotificationToAppear
-import com.android.wm.shell.flicker.NotificationListener.Companion.waitForNotificationToDisappear
+import com.android.wm.shell.flicker.utils.NotificationListener.Companion.findNotification
+import com.android.wm.shell.flicker.utils.NotificationListener.Companion.startNotificationListener
+import com.android.wm.shell.flicker.utils.NotificationListener.Companion.stopNotificationListener
+import com.android.wm.shell.flicker.utils.NotificationListener.Companion.waitForNotificationToAppear
+import com.android.wm.shell.flicker.utils.NotificationListener.Companion.waitForNotificationToDisappear
import org.junit.After
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt
index 6104b7b..47bff8d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt
@@ -24,7 +24,7 @@
import android.tools.device.traces.parsers.WindowManagerStateHelper
import android.view.Surface.ROTATION_0
import android.view.Surface.rotationToString
-import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
+import com.android.wm.shell.flicker.utils.SYSTEM_UI_PACKAGE_NAME
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assume.assumeTrue
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt
index b0adbe1..4aee61a 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/pip/tv/TvUtils.kt
@@ -22,7 +22,7 @@
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject2
import androidx.test.uiautomator.Until
-import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
+import com.android.wm.shell.flicker.utils.SYSTEM_UI_PACKAGE_NAME
/** Id of the root view in the com.android.wm.shell.pip.tv.PipMenuActivity */
private const val TV_PIP_MENU_ROOT_ID = "tv_pip_menu"
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/SplitScreenUtils.kt
deleted file mode 100644
index e640dc4..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/SplitScreenUtils.kt
+++ /dev/null
@@ -1,387 +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.wm.shell.flicker.service.splitscreen
-
-import android.app.Instrumentation
-import android.graphics.Point
-import android.os.SystemClock
-import android.tools.common.traces.component.ComponentNameMatcher
-import android.tools.common.traces.component.IComponentMatcher
-import android.tools.common.traces.component.IComponentNameMatcher
-import android.tools.device.apphelpers.StandardAppHelper
-import android.tools.device.traces.parsers.WindowManagerStateHelper
-import android.tools.device.traces.parsers.toFlickerComponent
-import android.view.InputDevice
-import android.view.MotionEvent
-import android.view.ViewConfiguration
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.BySelector
-import androidx.test.uiautomator.UiDevice
-import androidx.test.uiautomator.UiObject2
-import androidx.test.uiautomator.Until
-import com.android.launcher3.tapl.LauncherInstrumentation
-import com.android.server.wm.flicker.helpers.ImeAppHelper
-import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
-import com.android.server.wm.flicker.helpers.NotificationAppHelper
-import com.android.server.wm.flicker.helpers.SimpleAppHelper
-import com.android.server.wm.flicker.testapp.ActivityOptions
-import com.android.wm.shell.flicker.LAUNCHER_UI_PACKAGE_NAME
-import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
-import org.junit.Assert.assertNotNull
-
-object SplitScreenUtils {
- private const val TIMEOUT_MS = 3_000L
- private const val DRAG_DURATION_MS = 1_000L
- private const val NOTIFICATION_SCROLLER = "notification_stack_scroller"
- private const val DIVIDER_BAR = "docked_divider_handle"
- private const val OVERVIEW_SNAPSHOT = "snapshot"
- private const val GESTURE_STEP_MS = 16L
- private val LONG_PRESS_TIME_MS = ViewConfiguration.getLongPressTimeout() * 2L
- private val SPLIT_DECOR_MANAGER = ComponentNameMatcher("", "SplitDecorManager#")
-
- private val notificationScrollerSelector: BySelector
- get() = By.res(SYSTEM_UI_PACKAGE_NAME, NOTIFICATION_SCROLLER)
- private val notificationContentSelector: BySelector
- get() = By.text("Flicker Test Notification")
- private val dividerBarSelector: BySelector
- get() = By.res(SYSTEM_UI_PACKAGE_NAME, DIVIDER_BAR)
- private val overviewSnapshotSelector: BySelector
- get() = By.res(LAUNCHER_UI_PACKAGE_NAME, OVERVIEW_SNAPSHOT)
-
- fun getPrimary(instrumentation: Instrumentation): StandardAppHelper =
- SimpleAppHelper(
- instrumentation,
- ActivityOptions.SplitScreen.Primary.LABEL,
- ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent()
- )
-
- fun getSecondary(instrumentation: Instrumentation): StandardAppHelper =
- SimpleAppHelper(
- instrumentation,
- ActivityOptions.SplitScreen.Secondary.LABEL,
- ActivityOptions.SplitScreen.Secondary.COMPONENT.toFlickerComponent()
- )
-
- fun getNonResizeable(instrumentation: Instrumentation): NonResizeableAppHelper =
- NonResizeableAppHelper(instrumentation)
-
- fun getSendNotification(instrumentation: Instrumentation): NotificationAppHelper =
- NotificationAppHelper(instrumentation)
-
- fun getIme(instrumentation: Instrumentation): ImeAppHelper = ImeAppHelper(instrumentation)
-
- fun waitForSplitComplete(
- wmHelper: WindowManagerStateHelper,
- primaryApp: IComponentMatcher,
- secondaryApp: IComponentMatcher,
- ) {
- wmHelper
- .StateSyncBuilder()
- .withWindowSurfaceAppeared(primaryApp)
- .withWindowSurfaceAppeared(secondaryApp)
- .withSplitDividerVisible()
- .waitForAndVerify()
- }
-
- fun enterSplit(
- wmHelper: WindowManagerStateHelper,
- tapl: LauncherInstrumentation,
- device: UiDevice,
- primaryApp: StandardAppHelper,
- secondaryApp: StandardAppHelper
- ) {
- primaryApp.launchViaIntent(wmHelper)
- secondaryApp.launchViaIntent(wmHelper)
- tapl.goHome()
- wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
- splitFromOverview(tapl, device)
- waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
- }
-
- fun splitFromOverview(tapl: LauncherInstrumentation, device: UiDevice) {
- // Note: The initial split position in landscape is different between tablet and phone.
- // In landscape, tablet will let the first app split to right side, and phone will
- // split to left side.
- if (tapl.isTablet) {
- // TAPL's currentTask on tablet is sometimes not what we expected if the overview
- // contains more than 3 task views. We need to use uiautomator directly to find the
- // second task to split.
- tapl.workspace.switchToOverview().overviewActions.clickSplit()
- val snapshots = device.wait(Until.findObjects(overviewSnapshotSelector), TIMEOUT_MS)
- if (snapshots == null || snapshots.size < 1) {
- error("Fail to find a overview snapshot to split.")
- }
-
- // Find the second task in the upper right corner in split select mode by sorting
- // 'left' in descending order and 'top' in ascending order.
- snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
- t2.getVisibleBounds().left - t1.getVisibleBounds().left
- }
- snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
- t1.getVisibleBounds().top - t2.getVisibleBounds().top
- }
- snapshots[0].click()
- } else {
- tapl.workspace
- .switchToOverview()
- .currentTask
- .tapMenu()
- .tapSplitMenuItem()
- .currentTask
- .open()
- }
- SystemClock.sleep(TIMEOUT_MS)
- }
-
- fun enterSplitViaIntent(
- wmHelper: WindowManagerStateHelper,
- primaryApp: StandardAppHelper,
- secondaryApp: StandardAppHelper
- ) {
- val stringExtras =
- mapOf(ActivityOptions.SplitScreen.Primary.EXTRA_LAUNCH_ADJACENT to "true")
- primaryApp.launchViaIntent(wmHelper, null, null, stringExtras)
- SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
- }
-
- fun dragFromNotificationToSplit(
- instrumentation: Instrumentation,
- device: UiDevice,
- wmHelper: WindowManagerStateHelper
- ) {
- val displayBounds =
- wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
- ?: error("Display not found")
-
- // Pull down the notifications
- device.swipe(
- displayBounds.centerX(),
- 5,
- displayBounds.centerX(),
- displayBounds.bottom,
- 50 /* steps */
- )
- SystemClock.sleep(TIMEOUT_MS)
-
- // Find the target notification
- val notificationScroller =
- device.wait(Until.findObject(notificationScrollerSelector), TIMEOUT_MS)
- ?: error("Unable to find view $notificationScrollerSelector")
- var notificationContent = notificationScroller.findObject(notificationContentSelector)
-
- while (notificationContent == null) {
- device.swipe(
- displayBounds.centerX(),
- displayBounds.centerY(),
- displayBounds.centerX(),
- displayBounds.centerY() - 150,
- 20 /* steps */
- )
- notificationContent = notificationScroller.findObject(notificationContentSelector)
- }
-
- // Drag to split
- val dragStart = notificationContent.visibleCenter
- val dragMiddle = Point(dragStart.x + 50, dragStart.y)
- val dragEnd = Point(displayBounds.width / 4, displayBounds.width / 4)
- val downTime = SystemClock.uptimeMillis()
-
- touch(instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, TIMEOUT_MS, dragStart)
- // It needs a horizontal movement to trigger the drag
- touchMove(
- instrumentation,
- downTime,
- SystemClock.uptimeMillis(),
- DRAG_DURATION_MS,
- dragStart,
- dragMiddle
- )
- touchMove(
- instrumentation,
- downTime,
- SystemClock.uptimeMillis(),
- DRAG_DURATION_MS,
- dragMiddle,
- dragEnd
- )
- // Wait for a while to start splitting
- SystemClock.sleep(TIMEOUT_MS)
- touch(
- instrumentation,
- MotionEvent.ACTION_UP,
- downTime,
- SystemClock.uptimeMillis(),
- GESTURE_STEP_MS,
- dragEnd
- )
- SystemClock.sleep(TIMEOUT_MS)
- }
-
- fun touch(
- instrumentation: Instrumentation,
- action: Int,
- downTime: Long,
- eventTime: Long,
- duration: Long,
- point: Point
- ) {
- val motionEvent =
- MotionEvent.obtain(downTime, eventTime, action, point.x.toFloat(), point.y.toFloat(), 0)
- motionEvent.source = InputDevice.SOURCE_TOUCHSCREEN
- instrumentation.uiAutomation.injectInputEvent(motionEvent, true)
- motionEvent.recycle()
- SystemClock.sleep(duration)
- }
-
- fun touchMove(
- instrumentation: Instrumentation,
- downTime: Long,
- eventTime: Long,
- duration: Long,
- from: Point,
- to: Point
- ) {
- val steps: Long = duration / GESTURE_STEP_MS
- var currentTime = eventTime
- var currentX = from.x.toFloat()
- var currentY = from.y.toFloat()
- val stepX = (to.x.toFloat() - from.x.toFloat()) / steps.toFloat()
- val stepY = (to.y.toFloat() - from.y.toFloat()) / steps.toFloat()
-
- for (i in 1..steps) {
- val motionMove =
- MotionEvent.obtain(
- downTime,
- currentTime,
- MotionEvent.ACTION_MOVE,
- currentX,
- currentY,
- 0
- )
- motionMove.source = InputDevice.SOURCE_TOUCHSCREEN
- instrumentation.uiAutomation.injectInputEvent(motionMove, true)
- motionMove.recycle()
-
- currentTime += GESTURE_STEP_MS
- if (i == steps - 1) {
- currentX = to.x.toFloat()
- currentY = to.y.toFloat()
- } else {
- currentX += stepX
- currentY += stepY
- }
- SystemClock.sleep(GESTURE_STEP_MS)
- }
- }
-
- fun createShortcutOnHotseatIfNotExist(tapl: LauncherInstrumentation, appName: String) {
- tapl.workspace.deleteAppIcon(tapl.workspace.getHotseatAppIcon(0))
- val allApps = tapl.workspace.switchToAllApps()
- allApps.freeze()
- try {
- allApps.getAppIcon(appName).dragToHotseat(0)
- } finally {
- allApps.unfreeze()
- }
- }
-
- fun dragDividerToResizeAndWait(device: UiDevice, wmHelper: WindowManagerStateHelper) {
- val displayBounds =
- wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
- ?: error("Display not found")
- val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
- dividerBar.drag(Point(displayBounds.width * 1 / 3, displayBounds.height * 2 / 3), 200)
-
- wmHelper
- .StateSyncBuilder()
- .withWindowSurfaceDisappeared(SPLIT_DECOR_MANAGER)
- .waitForAndVerify()
- }
-
- fun dragDividerToDismissSplit(
- device: UiDevice,
- wmHelper: WindowManagerStateHelper,
- dragToRight: Boolean,
- dragToBottom: Boolean
- ) {
- val displayBounds =
- wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
- ?: error("Display not found")
- val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
- dividerBar.drag(
- Point(
- if (dragToRight) {
- displayBounds.width * 4 / 5
- } else {
- displayBounds.width * 1 / 5
- },
- if (dragToBottom) {
- displayBounds.height * 4 / 5
- } else {
- displayBounds.height * 1 / 5
- }
- )
- )
- }
-
- fun doubleTapDividerToSwitch(device: UiDevice) {
- val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
- val interval =
- (ViewConfiguration.getDoubleTapTimeout() + ViewConfiguration.getDoubleTapMinTime()) / 2
- dividerBar.click()
- SystemClock.sleep(interval.toLong())
- dividerBar.click()
- }
-
- fun copyContentInSplit(
- instrumentation: Instrumentation,
- device: UiDevice,
- sourceApp: IComponentNameMatcher,
- destinationApp: IComponentNameMatcher,
- ) {
- // Copy text from sourceApp
- val textView =
- device.wait(
- Until.findObject(By.res(sourceApp.packageName, "SplitScreenTest")),
- TIMEOUT_MS
- )
- assertNotNull("Unable to find the TextView", textView)
- textView.click(LONG_PRESS_TIME_MS)
-
- val copyBtn = device.wait(Until.findObject(By.text("Copy")), TIMEOUT_MS)
- assertNotNull("Unable to find the copy button", copyBtn)
- copyBtn.click()
-
- // Paste text to destinationApp
- val editText =
- device.wait(
- Until.findObject(By.res(destinationApp.packageName, "plain_text_input")),
- TIMEOUT_MS
- )
- assertNotNull("Unable to find the EditText", editText)
- editText.click(LONG_PRESS_TIME_MS)
-
- val pasteBtn = device.wait(Until.findObject(By.text("Paste")), TIMEOUT_MS)
- assertNotNull("Unable to find the paste button", pasteBtn)
- pasteBtn.click()
-
- // Verify text
- if (!textView.text.contentEquals(editText.text)) {
- error("Fail to copy content in split")
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt
index 5bfc889..e530f63 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/CopyContentInSplit.kt
@@ -24,6 +24,7 @@
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByDivider.kt
index d07daff..e9fc437 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByDivider.kt
@@ -24,6 +24,7 @@
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByGoHome.kt
index d428dea..416692c 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByGoHome.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DismissSplitScreenByGoHome.kt
@@ -24,6 +24,7 @@
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DragDividerToResize.kt
index dc2a6ac..494a246 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DragDividerToResize.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/DragDividerToResize.kt
@@ -24,6 +24,7 @@
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt
index 74f441d..a3aae85 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromAllApps.kt
@@ -24,6 +24,7 @@
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Assume
import org.junit.Before
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt
index 8d93c0d..50151f1 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromNotification.kt
@@ -24,6 +24,7 @@
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Assume
import org.junit.Before
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt
index 36a458f..5d67dc7 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromShortcut.kt
@@ -24,6 +24,7 @@
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Assume
import org.junit.Before
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt
index ed08041..ae5bb68 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenByDragFromTaskbar.kt
@@ -24,6 +24,7 @@
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Assume
import org.junit.Before
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt
index f164451..c2100f6 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/EnterSplitScreenFromOverview.kt
@@ -24,6 +24,7 @@
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SplitScreenUtils.kt
deleted file mode 100644
index 3831c65..0000000
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SplitScreenUtils.kt
+++ /dev/null
@@ -1,387 +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.wm.shell.flicker.service.splitscreen.scenarios
-
-import android.app.Instrumentation
-import android.graphics.Point
-import android.os.SystemClock
-import android.tools.common.traces.component.ComponentNameMatcher
-import android.tools.common.traces.component.IComponentMatcher
-import android.tools.common.traces.component.IComponentNameMatcher
-import android.tools.device.apphelpers.StandardAppHelper
-import android.tools.device.traces.parsers.WindowManagerStateHelper
-import android.tools.device.traces.parsers.toFlickerComponent
-import android.view.InputDevice
-import android.view.MotionEvent
-import android.view.ViewConfiguration
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.BySelector
-import androidx.test.uiautomator.UiDevice
-import androidx.test.uiautomator.UiObject2
-import androidx.test.uiautomator.Until
-import com.android.launcher3.tapl.LauncherInstrumentation
-import com.android.server.wm.flicker.helpers.ImeAppHelper
-import com.android.server.wm.flicker.helpers.NonResizeableAppHelper
-import com.android.server.wm.flicker.helpers.NotificationAppHelper
-import com.android.server.wm.flicker.helpers.SimpleAppHelper
-import com.android.server.wm.flicker.testapp.ActivityOptions
-import com.android.wm.shell.flicker.LAUNCHER_UI_PACKAGE_NAME
-import com.android.wm.shell.flicker.SYSTEM_UI_PACKAGE_NAME
-import org.junit.Assert.assertNotNull
-
-object SplitScreenUtils {
- private const val TIMEOUT_MS = 3_000L
- private const val DRAG_DURATION_MS = 1_000L
- private const val NOTIFICATION_SCROLLER = "notification_stack_scroller"
- private const val DIVIDER_BAR = "docked_divider_handle"
- private const val OVERVIEW_SNAPSHOT = "snapshot"
- private const val GESTURE_STEP_MS = 16L
- private val LONG_PRESS_TIME_MS = ViewConfiguration.getLongPressTimeout() * 2L
- private val SPLIT_DECOR_MANAGER = ComponentNameMatcher("", "SplitDecorManager#")
-
- private val notificationScrollerSelector: BySelector
- get() = By.res(SYSTEM_UI_PACKAGE_NAME, NOTIFICATION_SCROLLER)
- private val notificationContentSelector: BySelector
- get() = By.text("Flicker Test Notification")
- private val dividerBarSelector: BySelector
- get() = By.res(SYSTEM_UI_PACKAGE_NAME, DIVIDER_BAR)
- private val overviewSnapshotSelector: BySelector
- get() = By.res(LAUNCHER_UI_PACKAGE_NAME, OVERVIEW_SNAPSHOT)
-
- fun getPrimary(instrumentation: Instrumentation): StandardAppHelper =
- SimpleAppHelper(
- instrumentation,
- ActivityOptions.SplitScreen.Primary.LABEL,
- ActivityOptions.SplitScreen.Primary.COMPONENT.toFlickerComponent()
- )
-
- fun getSecondary(instrumentation: Instrumentation): StandardAppHelper =
- SimpleAppHelper(
- instrumentation,
- ActivityOptions.SplitScreen.Secondary.LABEL,
- ActivityOptions.SplitScreen.Secondary.COMPONENT.toFlickerComponent()
- )
-
- fun getNonResizeable(instrumentation: Instrumentation): NonResizeableAppHelper =
- NonResizeableAppHelper(instrumentation)
-
- fun getSendNotification(instrumentation: Instrumentation): NotificationAppHelper =
- NotificationAppHelper(instrumentation)
-
- fun getIme(instrumentation: Instrumentation): ImeAppHelper = ImeAppHelper(instrumentation)
-
- fun waitForSplitComplete(
- wmHelper: WindowManagerStateHelper,
- primaryApp: IComponentMatcher,
- secondaryApp: IComponentMatcher,
- ) {
- wmHelper
- .StateSyncBuilder()
- .withWindowSurfaceAppeared(primaryApp)
- .withWindowSurfaceAppeared(secondaryApp)
- .withSplitDividerVisible()
- .waitForAndVerify()
- }
-
- fun enterSplit(
- wmHelper: WindowManagerStateHelper,
- tapl: LauncherInstrumentation,
- device: UiDevice,
- primaryApp: StandardAppHelper,
- secondaryApp: StandardAppHelper
- ) {
- primaryApp.launchViaIntent(wmHelper)
- secondaryApp.launchViaIntent(wmHelper)
- tapl.goHome()
- wmHelper.StateSyncBuilder().withHomeActivityVisible().waitForAndVerify()
- splitFromOverview(tapl, device)
- waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
- }
-
- fun splitFromOverview(tapl: LauncherInstrumentation, device: UiDevice) {
- // Note: The initial split position in landscape is different between tablet and phone.
- // In landscape, tablet will let the first app split to right side, and phone will
- // split to left side.
- if (tapl.isTablet) {
- // TAPL's currentTask on tablet is sometimes not what we expected if the overview
- // contains more than 3 task views. We need to use uiautomator directly to find the
- // second task to split.
- tapl.workspace.switchToOverview().overviewActions.clickSplit()
- val snapshots = device.wait(Until.findObjects(overviewSnapshotSelector), TIMEOUT_MS)
- if (snapshots == null || snapshots.size < 1) {
- error("Fail to find a overview snapshot to split.")
- }
-
- // Find the second task in the upper right corner in split select mode by sorting
- // 'left' in descending order and 'top' in ascending order.
- snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
- t2.getVisibleBounds().left - t1.getVisibleBounds().left
- }
- snapshots.sortWith { t1: UiObject2, t2: UiObject2 ->
- t1.getVisibleBounds().top - t2.getVisibleBounds().top
- }
- snapshots[0].click()
- } else {
- tapl.workspace
- .switchToOverview()
- .currentTask
- .tapMenu()
- .tapSplitMenuItem()
- .currentTask
- .open()
- }
- SystemClock.sleep(TIMEOUT_MS)
- }
-
- fun enterSplitViaIntent(
- wmHelper: WindowManagerStateHelper,
- primaryApp: StandardAppHelper,
- secondaryApp: StandardAppHelper
- ) {
- val stringExtras =
- mapOf(ActivityOptions.SplitScreen.Primary.EXTRA_LAUNCH_ADJACENT to "true")
- primaryApp.launchViaIntent(wmHelper, null, null, stringExtras)
- SplitScreenUtils.waitForSplitComplete(wmHelper, primaryApp, secondaryApp)
- }
-
- fun dragFromNotificationToSplit(
- instrumentation: Instrumentation,
- device: UiDevice,
- wmHelper: WindowManagerStateHelper
- ) {
- val displayBounds =
- wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
- ?: error("Display not found")
-
- // Pull down the notifications
- device.swipe(
- displayBounds.centerX(),
- 5,
- displayBounds.centerX(),
- displayBounds.bottom,
- 50 /* steps */
- )
- SystemClock.sleep(TIMEOUT_MS)
-
- // Find the target notification
- val notificationScroller =
- device.wait(Until.findObject(notificationScrollerSelector), TIMEOUT_MS)
- ?: error("Unable to find view $notificationScrollerSelector")
- var notificationContent = notificationScroller.findObject(notificationContentSelector)
-
- while (notificationContent == null) {
- device.swipe(
- displayBounds.centerX(),
- displayBounds.centerY(),
- displayBounds.centerX(),
- displayBounds.centerY() - 150,
- 20 /* steps */
- )
- notificationContent = notificationScroller.findObject(notificationContentSelector)
- }
-
- // Drag to split
- val dragStart = notificationContent.visibleCenter
- val dragMiddle = Point(dragStart.x + 50, dragStart.y)
- val dragEnd = Point(displayBounds.width / 4, displayBounds.width / 4)
- val downTime = SystemClock.uptimeMillis()
-
- touch(instrumentation, MotionEvent.ACTION_DOWN, downTime, downTime, TIMEOUT_MS, dragStart)
- // It needs a horizontal movement to trigger the drag
- touchMove(
- instrumentation,
- downTime,
- SystemClock.uptimeMillis(),
- DRAG_DURATION_MS,
- dragStart,
- dragMiddle
- )
- touchMove(
- instrumentation,
- downTime,
- SystemClock.uptimeMillis(),
- DRAG_DURATION_MS,
- dragMiddle,
- dragEnd
- )
- // Wait for a while to start splitting
- SystemClock.sleep(TIMEOUT_MS)
- touch(
- instrumentation,
- MotionEvent.ACTION_UP,
- downTime,
- SystemClock.uptimeMillis(),
- GESTURE_STEP_MS,
- dragEnd
- )
- SystemClock.sleep(TIMEOUT_MS)
- }
-
- fun touch(
- instrumentation: Instrumentation,
- action: Int,
- downTime: Long,
- eventTime: Long,
- duration: Long,
- point: Point
- ) {
- val motionEvent =
- MotionEvent.obtain(downTime, eventTime, action, point.x.toFloat(), point.y.toFloat(), 0)
- motionEvent.source = InputDevice.SOURCE_TOUCHSCREEN
- instrumentation.uiAutomation.injectInputEvent(motionEvent, true)
- motionEvent.recycle()
- SystemClock.sleep(duration)
- }
-
- fun touchMove(
- instrumentation: Instrumentation,
- downTime: Long,
- eventTime: Long,
- duration: Long,
- from: Point,
- to: Point
- ) {
- val steps: Long = duration / GESTURE_STEP_MS
- var currentTime = eventTime
- var currentX = from.x.toFloat()
- var currentY = from.y.toFloat()
- val stepX = (to.x.toFloat() - from.x.toFloat()) / steps.toFloat()
- val stepY = (to.y.toFloat() - from.y.toFloat()) / steps.toFloat()
-
- for (i in 1..steps) {
- val motionMove =
- MotionEvent.obtain(
- downTime,
- currentTime,
- MotionEvent.ACTION_MOVE,
- currentX,
- currentY,
- 0
- )
- motionMove.source = InputDevice.SOURCE_TOUCHSCREEN
- instrumentation.uiAutomation.injectInputEvent(motionMove, true)
- motionMove.recycle()
-
- currentTime += GESTURE_STEP_MS
- if (i == steps - 1) {
- currentX = to.x.toFloat()
- currentY = to.y.toFloat()
- } else {
- currentX += stepX
- currentY += stepY
- }
- SystemClock.sleep(GESTURE_STEP_MS)
- }
- }
-
- fun createShortcutOnHotseatIfNotExist(tapl: LauncherInstrumentation, appName: String) {
- tapl.workspace.deleteAppIcon(tapl.workspace.getHotseatAppIcon(0))
- val allApps = tapl.workspace.switchToAllApps()
- allApps.freeze()
- try {
- allApps.getAppIcon(appName).dragToHotseat(0)
- } finally {
- allApps.unfreeze()
- }
- }
-
- fun dragDividerToResizeAndWait(device: UiDevice, wmHelper: WindowManagerStateHelper) {
- val displayBounds =
- wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
- ?: error("Display not found")
- val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
- dividerBar.drag(Point(displayBounds.width * 1 / 3, displayBounds.height * 2 / 3), 200)
-
- wmHelper
- .StateSyncBuilder()
- .withWindowSurfaceDisappeared(SPLIT_DECOR_MANAGER)
- .waitForAndVerify()
- }
-
- fun dragDividerToDismissSplit(
- device: UiDevice,
- wmHelper: WindowManagerStateHelper,
- dragToRight: Boolean,
- dragToBottom: Boolean
- ) {
- val displayBounds =
- wmHelper.currentState.layerState.displays.firstOrNull { !it.isVirtual }?.layerStackSpace
- ?: error("Display not found")
- val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
- dividerBar.drag(
- Point(
- if (dragToRight) {
- displayBounds.width * 4 / 5
- } else {
- displayBounds.width * 1 / 5
- },
- if (dragToBottom) {
- displayBounds.height * 4 / 5
- } else {
- displayBounds.height * 1 / 5
- }
- )
- )
- }
-
- fun doubleTapDividerToSwitch(device: UiDevice) {
- val dividerBar = device.wait(Until.findObject(dividerBarSelector), TIMEOUT_MS)
- val interval =
- (ViewConfiguration.getDoubleTapTimeout() + ViewConfiguration.getDoubleTapMinTime()) / 2
- dividerBar.click()
- SystemClock.sleep(interval.toLong())
- dividerBar.click()
- }
-
- fun copyContentInSplit(
- instrumentation: Instrumentation,
- device: UiDevice,
- sourceApp: IComponentNameMatcher,
- destinationApp: IComponentNameMatcher,
- ) {
- // Copy text from sourceApp
- val textView =
- device.wait(
- Until.findObject(By.res(sourceApp.packageName, "SplitScreenTest")),
- TIMEOUT_MS
- )
- assertNotNull("Unable to find the TextView", textView)
- textView.click(LONG_PRESS_TIME_MS)
-
- val copyBtn = device.wait(Until.findObject(By.text("Copy")), TIMEOUT_MS)
- assertNotNull("Unable to find the copy button", copyBtn)
- copyBtn.click()
-
- // Paste text to destinationApp
- val editText =
- device.wait(
- Until.findObject(By.res(destinationApp.packageName, "plain_text_input")),
- TIMEOUT_MS
- )
- assertNotNull("Unable to find the EditText", editText)
- editText.click(LONG_PRESS_TIME_MS)
-
- val pasteBtn = device.wait(Until.findObject(By.text("Paste")), TIMEOUT_MS)
- assertNotNull("Unable to find the paste button", pasteBtn)
- pasteBtn.click()
-
- // Verify text
- if (!textView.text.contentEquals(editText.text)) {
- error("Fail to copy content in split")
- }
- }
-}
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt
index 805d987..70f3bed 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchAppByDoubleTapDivider.kt
@@ -26,6 +26,7 @@
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt
index 4229ebb..86f394d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromAnotherApp.kt
@@ -24,6 +24,7 @@
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt
index f2d56b9..d7b611e 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromHome.kt
@@ -24,6 +24,7 @@
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt
index 9f9d4bb..3cc5df0 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBackToSplitFromRecent.kt
@@ -24,6 +24,7 @@
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt
index e2c6ca6..4a9c32f 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/SwitchBetweenSplitPairs.kt
@@ -24,6 +24,7 @@
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt
index df98d8f..383a6b3 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/service/splitscreen/scenarios/UnlockKeyguardToSplitScreen.kt
@@ -24,6 +24,7 @@
import androidx.test.uiautomator.UiDevice
import com.android.launcher3.tapl.LauncherInstrumentation
import com.android.wm.shell.flicker.service.Utils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.After
import org.junit.Before
import org.junit.Ignore
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
index 1d4c4d2..3702be9 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/CopyContentInSplit.kt
@@ -25,12 +25,12 @@
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.ICommonAssertions
-import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.appWindowKeepVisible
-import com.android.wm.shell.flicker.layerKeepVisible
-import com.android.wm.shell.flicker.splitAppLayerBoundsKeepVisible
import com.android.wm.shell.flicker.splitscreen.benchmark.CopyContentInSplitBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.utils.appWindowKeepVisible
+import com.android.wm.shell.flicker.utils.layerKeepVisible
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsKeepVisible
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
index 0b8f109..8b90630 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByDivider.kt
@@ -24,14 +24,14 @@
import android.tools.device.flicker.legacy.LegacyFlickerTest
import android.tools.device.helpers.WindowUtils
import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.ICommonAssertions
-import com.android.wm.shell.flicker.appWindowBecomesInvisible
-import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.layerBecomesInvisible
-import com.android.wm.shell.flicker.layerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesInvisible
-import com.android.wm.shell.flicker.splitScreenDividerBecomesInvisible
import com.android.wm.shell.flicker.splitscreen.benchmark.DismissSplitScreenByDividerBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.appWindowBecomesInvisible
+import com.android.wm.shell.flicker.utils.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.layerBecomesInvisible
+import com.android.wm.shell.flicker.utils.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsBecomesInvisible
+import com.android.wm.shell.flicker.utils.splitScreenDividerBecomesInvisible
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
index 38d4b40..50f6a38 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DismissSplitScreenByGoHome.kt
@@ -23,12 +23,12 @@
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.ICommonAssertions
-import com.android.wm.shell.flicker.appWindowBecomesInvisible
-import com.android.wm.shell.flicker.layerBecomesInvisible
-import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesInvisible
-import com.android.wm.shell.flicker.splitScreenDividerBecomesInvisible
import com.android.wm.shell.flicker.splitscreen.benchmark.DismissSplitScreenByGoHomeBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.appWindowBecomesInvisible
+import com.android.wm.shell.flicker.utils.layerBecomesInvisible
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsBecomesInvisible
+import com.android.wm.shell.flicker.utils.splitScreenDividerBecomesInvisible
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
index 0d967eb..cc3b783 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/DragDividerToResize.kt
@@ -23,12 +23,12 @@
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.ICommonAssertions
-import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.appWindowKeepVisible
-import com.android.wm.shell.flicker.layerKeepVisible
-import com.android.wm.shell.flicker.splitAppLayerBoundsChanges
import com.android.wm.shell.flicker.splitscreen.benchmark.DragDividerToResizeBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.utils.appWindowKeepVisible
+import com.android.wm.shell.flicker.utils.layerKeepVisible
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsChanges
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
@@ -57,7 +57,7 @@
@Test
fun splitScreenDividerKeepVisible() = flicker.layerKeepVisible(SPLIT_SCREEN_DIVIDER_COMPONENT)
- @Presubmit
+ @FlakyTest(bugId = 291678271)
@Test
fun primaryAppLayerVisibilityChanges() {
flicker.assertLayers {
@@ -69,7 +69,7 @@
}
}
- @Presubmit
+ @FlakyTest(bugId = 291678271)
@Test
fun secondaryAppLayerVisibilityChanges() {
flicker.assertLayers {
@@ -87,7 +87,7 @@
@Test
fun secondaryAppWindowKeepVisible() = flicker.appWindowKeepVisible(secondaryApp)
- @FlakyTest(bugId = 245472831)
+ @FlakyTest(bugId = 291678271)
@Test
fun primaryAppBoundsChanges() {
flicker.splitAppLayerBoundsChanges(
@@ -97,7 +97,7 @@
)
}
- @Presubmit
+ @FlakyTest(bugId = 291678271)
@Test
fun secondaryAppBoundsChanges() =
flicker.splitAppLayerBoundsChanges(
@@ -106,6 +106,12 @@
portraitPosTop = true
)
+ @FlakyTest(bugId = 291678271)
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+ }
+
companion object {
@Parameterized.Parameters(name = "{0}")
@JvmStatic
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt
index 05c0480..f8d1e1f 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromAllApps.kt
@@ -25,16 +25,16 @@
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.ICommonAssertions
-import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.appWindowBecomesVisible
-import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.layerBecomesVisible
-import com.android.wm.shell.flicker.layerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisibleByDrag
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
import com.android.wm.shell.flicker.splitscreen.benchmark.EnterSplitScreenByDragFromAllAppsBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.utils.appWindowBecomesVisible
+import com.android.wm.shell.flicker.utils.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.layerBecomesVisible
+import com.android.wm.shell.flicker.utils.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsBecomesVisibleByDrag
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitScreenDividerBecomesVisible
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
index 3a75fa6..ff5d935 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromNotification.kt
@@ -25,15 +25,15 @@
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.ICommonAssertions
-import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.layerBecomesVisible
-import com.android.wm.shell.flicker.layerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisibleByDrag
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
import com.android.wm.shell.flicker.splitscreen.benchmark.EnterSplitScreenByDragFromNotificationBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.utils.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.layerBecomesVisible
+import com.android.wm.shell.flicker.utils.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsBecomesVisibleByDrag
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitScreenDividerBecomesVisible
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
index 6d73f92..7c71077 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromShortcut.kt
@@ -24,14 +24,14 @@
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.ICommonAssertions
-import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.layerBecomesVisible
-import com.android.wm.shell.flicker.layerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisibleByDrag
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
import com.android.wm.shell.flicker.splitscreen.benchmark.EnterSplitScreenByDragFromShortcutBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.layerBecomesVisible
+import com.android.wm.shell.flicker.utils.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsBecomesVisibleByDrag
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitScreenDividerBecomesVisible
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
index 15cae69..8371706 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenByDragFromTaskbar.kt
@@ -25,16 +25,16 @@
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.ICommonAssertions
-import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.appWindowBecomesVisible
-import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.layerBecomesVisible
-import com.android.wm.shell.flicker.layerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisibleByDrag
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
import com.android.wm.shell.flicker.splitscreen.benchmark.EnterSplitScreenByDragFromTaskbarBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.utils.appWindowBecomesVisible
+import com.android.wm.shell.flicker.utils.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.layerBecomesVisible
+import com.android.wm.shell.flicker.utils.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsBecomesVisibleByDrag
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitScreenDividerBecomesVisible
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
index 90399fc..0bfdbb4 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/EnterSplitScreenFromOverview.kt
@@ -23,14 +23,14 @@
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.ICommonAssertions
-import com.android.wm.shell.flicker.appWindowBecomesVisible
-import com.android.wm.shell.flicker.layerBecomesVisible
-import com.android.wm.shell.flicker.layerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitAppLayerBoundsBecomesVisible
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
import com.android.wm.shell.flicker.splitscreen.benchmark.EnterSplitScreenFromOverviewBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.appWindowBecomesVisible
+import com.android.wm.shell.flicker.utils.layerBecomesVisible
+import com.android.wm.shell.flicker.utils.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsBecomesVisible
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitScreenDividerBecomesVisible
import org.junit.FixMethodOrder
import org.junit.Test
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/SplitScreenBase.kt
index 3064bd7..540c11f 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/SplitScreenBase.kt
@@ -21,7 +21,7 @@
import android.tools.device.flicker.legacy.LegacyFlickerTest
import com.android.server.wm.flicker.helpers.setRotation
import com.android.wm.shell.flicker.BaseBenchmarkTest
-import com.android.wm.shell.flicker.SplitScreenUtils
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
abstract class SplitScreenBase(flicker: LegacyFlickerTest) : BaseBenchmarkTest(flicker) {
protected val context: Context = instrumentation.context
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
index f236c2d..fac97c8 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchAppByDoubleTapDivider.kt
@@ -24,13 +24,13 @@
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.ICommonAssertions
-import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.layerIsVisibleAtEnd
-import com.android.wm.shell.flicker.layerKeepVisible
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchAppByDoubleTapDividerBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.utils.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.layerKeepVisible
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
index a406009..88bbc0e 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromAnotherApp.kt
@@ -24,12 +24,12 @@
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.ICommonAssertions
-import com.android.wm.shell.flicker.appWindowBecomesVisible
-import com.android.wm.shell.flicker.layerBecomesVisible
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchBackToSplitFromAnotherAppBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.appWindowBecomesVisible
+import com.android.wm.shell.flicker.utils.layerBecomesVisible
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitScreenDividerBecomesVisible
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
index 251bd10..e85dc24 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromHome.kt
@@ -24,12 +24,12 @@
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.ICommonAssertions
-import com.android.wm.shell.flicker.appWindowBecomesVisible
-import com.android.wm.shell.flicker.layerBecomesVisible
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchBackToSplitFromHomeBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.appWindowBecomesVisible
+import com.android.wm.shell.flicker.utils.layerBecomesVisible
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitScreenDividerBecomesVisible
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
index 1dd45fe..f7a9ed0 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBackToSplitFromRecent.kt
@@ -24,12 +24,12 @@
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.ICommonAssertions
-import com.android.wm.shell.flicker.appWindowBecomesVisible
-import com.android.wm.shell.flicker.layerBecomesVisible
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitScreenDividerBecomesVisible
import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchBackToSplitFromRecentBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.appWindowBecomesVisible
+import com.android.wm.shell.flicker.utils.layerBecomesVisible
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitScreenDividerBecomesVisible
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt
index 8aaa98a..66f9b85 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/SwitchBetweenSplitPairs.kt
@@ -23,15 +23,15 @@
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.ICommonAssertions
-import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.appWindowBecomesInvisible
-import com.android.wm.shell.flicker.appWindowBecomesVisible
-import com.android.wm.shell.flicker.layerBecomesInvisible
-import com.android.wm.shell.flicker.layerBecomesVisible
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitAppLayerBoundsSnapToDivider
import com.android.wm.shell.flicker.splitscreen.benchmark.SwitchBetweenSplitPairsBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.utils.appWindowBecomesInvisible
+import com.android.wm.shell.flicker.utils.appWindowBecomesVisible
+import com.android.wm.shell.flicker.utils.layerBecomesInvisible
+import com.android.wm.shell.flicker.utils.layerBecomesVisible
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsSnapToDivider
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt
index 994d6cb..851391d 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/splitscreen/UnlockKeyguardToSplitScreen.kt
@@ -24,12 +24,12 @@
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.ICommonAssertions
-import com.android.wm.shell.flicker.SPLIT_SCREEN_DIVIDER_COMPONENT
-import com.android.wm.shell.flicker.appWindowIsVisibleAtEnd
-import com.android.wm.shell.flicker.layerIsVisibleAtEnd
-import com.android.wm.shell.flicker.splitAppLayerBoundsIsVisibleAtEnd
import com.android.wm.shell.flicker.splitscreen.benchmark.UnlockKeyguardToSplitScreenBenchmark
+import com.android.wm.shell.flicker.utils.ICommonAssertions
+import com.android.wm.shell.flicker.utils.SPLIT_SCREEN_DIVIDER_COMPONENT
+import com.android.wm.shell.flicker.utils.appWindowIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.layerIsVisibleAtEnd
+import com.android.wm.shell.flicker.utils.splitAppLayerBoundsIsVisibleAtEnd
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
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 d9d22de..e5c1e75 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,8 +22,8 @@
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.SplitScreenUtils
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
import org.junit.runners.MethodSorters
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 7e8d60b4..e4e1af9 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,8 +21,8 @@
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.SplitScreenUtils
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
import org.junit.runners.MethodSorters
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 770e032..b2dd02b 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,8 +21,8 @@
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.SplitScreenUtils
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
import org.junit.runners.MethodSorters
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 46570fd..0788591 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,8 +21,8 @@
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.SplitScreenUtils
import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.Assume
import org.junit.Before
import org.junit.FixMethodOrder
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 5c3d4ff..884e451 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,8 +22,8 @@
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.SplitScreenUtils
import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.Assume
import org.junit.Before
import org.junit.FixMethodOrder
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 6b122c6..e5c40b6 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,8 +22,8 @@
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.SplitScreenUtils
import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.Assume
import org.junit.Before
import org.junit.FixMethodOrder
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 78f9bab..0451001 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,8 +22,8 @@
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.SplitScreenUtils
import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.Assume
import org.junit.Before
import org.junit.FixMethodOrder
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 78907f0..9e0ca1b 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,8 +22,8 @@
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.SplitScreenUtils
import com.android.wm.shell.flicker.splitscreen.SplitScreenBase
+import com.android.wm.shell.flicker.utils.SplitScreenUtils
import org.junit.Assume
import org.junit.Before
import org.junit.FixMethodOrder
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 2c91e84..06b4fe7 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,8 +21,8 @@
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.SplitScreenUtils
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
import org.junit.runners.MethodSorters
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 fa09c2e..007b751 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,8 +25,8 @@
import android.tools.device.helpers.WindowUtils
import android.tools.device.traces.parsers.WindowManagerStateHelper
import androidx.test.filters.RequiresDevice
-import com.android.wm.shell.flicker.SplitScreenUtils
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
import org.junit.runners.MethodSorters
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 ff22006..10c8eeb 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,8 +22,8 @@
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.SplitScreenUtils
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
import org.junit.runners.MethodSorters
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 5787b02..a6e750f 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,8 +22,8 @@
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.SplitScreenUtils
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
import org.junit.runners.MethodSorters
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 b2d5091..7e8d5fb 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,8 +22,8 @@
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.SplitScreenUtils
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
import org.junit.runners.MethodSorters
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 f234e46..56edad1 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,8 +21,8 @@
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.SplitScreenUtils
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
import org.junit.runners.MethodSorters
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 61c3679..065d4d6 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,8 +22,8 @@
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.SplitScreenUtils
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
import org.junit.runners.MethodSorters
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonAssertions.kt
similarity index 99%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonAssertions.kt
index 9cc03a5..e5c124c 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonAssertions.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonAssertions.kt
@@ -16,7 +16,7 @@
@file:JvmName("CommonAssertions")
-package com.android.wm.shell.flicker
+package com.android.wm.shell.flicker.utils
import android.tools.common.Rotation
import android.tools.common.datatypes.Region
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonConstants.kt
similarity index 96%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonConstants.kt
index 3bc1e2a..3b66d6a 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/CommonConstants.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/CommonConstants.kt
@@ -16,7 +16,7 @@
@file:JvmName("CommonConstants")
-package com.android.wm.shell.flicker
+package com.android.wm.shell.flicker.utils
import android.tools.common.traces.component.ComponentNameMatcher
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/ICommonAssertions.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt
similarity index 98%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/ICommonAssertions.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt
index 7b32901..7f58ced 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/ICommonAssertions.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/ICommonAssertions.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell.flicker
+package com.android.wm.shell.flicker.utils
import android.platform.test.annotations.Presubmit
import android.tools.common.traces.component.ComponentNameMatcher
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/MultiWindowUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MultiWindowUtils.kt
similarity index 97%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/MultiWindowUtils.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MultiWindowUtils.kt
index 87b94ff..9b3a480 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/MultiWindowUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MultiWindowUtils.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell.flicker
+package com.android.wm.shell.flicker.utils
import android.app.Instrumentation
import android.content.Context
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NotificationListener.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/NotificationListener.kt
similarity index 98%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NotificationListener.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/NotificationListener.kt
index e0ef924..529c125 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/NotificationListener.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/NotificationListener.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell.flicker
+package com.android.wm.shell.flicker.utils
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/SplitScreenUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt
similarity index 99%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/SplitScreenUtils.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt
index 8a3c2c9..3f8a1ae 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/SplitScreenUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/SplitScreenUtils.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.wm.shell.flicker
+package com.android.wm.shell.flicker.utils
import android.app.Instrumentation
import android.graphics.Point
diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/WaitUtils.kt
similarity index 96%
rename from libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt
rename to libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/WaitUtils.kt
index 556cb06..cf2df4e 100644
--- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/WaitUtils.kt
+++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/WaitUtils.kt
@@ -16,7 +16,7 @@
@file:JvmName("WaitUtils")
-package com.android.wm.shell.flicker
+package com.android.wm.shell.flicker.utils
import android.os.SystemClock
diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp
index dcbaaec..ad09067 100644
--- a/libs/androidfw/AssetManager2.cpp
+++ b/libs/androidfw/AssetManager2.cpp
@@ -91,12 +91,17 @@
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 @@
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 @@
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 @@
}
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;
- 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);
+ // 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;
}
- 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) {
+
+ 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());
+ }
+ 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;
}
- result->entry = best_frro_value;
+ 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 (!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();
- 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 @@
// 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::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 @@
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 5a63612..06d19e0 100644
--- a/libs/androidfw/ResourceTypes.cpp
+++ b/libs/androidfw/ResourceTypes.cpp
@@ -2568,6 +2568,22 @@
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 f611d0d..d9ff35b 100644
--- a/libs/androidfw/include/androidfw/AssetManager2.h
+++ b/libs/androidfw/include/androidfw/AssetManager2.h
@@ -100,7 +100,7 @@
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 @@
// 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 @@
// 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 4eb1d7a..52666ab 100644
--- a/libs/androidfw/include/androidfw/ResourceTypes.h
+++ b/libs/androidfw/include/androidfw/ResourceTypes.h
@@ -1375,6 +1375,8 @@
// 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 6fae72a..2caa98c 100644
--- a/libs/androidfw/tests/AssetManager2_bench.cpp
+++ b/libs/androidfw/tests/AssetManager2_bench.cpp
@@ -228,10 +228,12 @@
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 df3fa02..c62f095 100644
--- a/libs/androidfw/tests/AssetManager2_test.cpp
+++ b/libs/androidfw/tests/AssetManager2_test.cpp
@@ -113,7 +113,7 @@
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 @@
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, 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 b97dd96..8b883f4 100644
--- a/libs/androidfw/tests/BenchmarkHelpers.cpp
+++ b/libs/androidfw/tests/BenchmarkHelpers.cpp
@@ -66,7 +66,7 @@
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 e08a6a7..181d141 100644
--- a/libs/androidfw/tests/Theme_test.cpp
+++ b/libs/androidfw/tests/Theme_test.cpp
@@ -260,7 +260,7 @@
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/SkiaCanvas.h b/libs/hwui/SkiaCanvas.h
index b785989..ced0224 100644
--- a/libs/hwui/SkiaCanvas.h
+++ b/libs/hwui/SkiaCanvas.h
@@ -175,7 +175,7 @@
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/jni/android_graphics_DisplayListCanvas.cpp b/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp
index f060bb3..426644e 100644
--- a/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp
+++ b/libs/hwui/jni/android_graphics_DisplayListCanvas.cpp
@@ -84,7 +84,7 @@
canvas->resetRecording(width, height, renderNode);
}
-static jint android_view_DisplayListCanvas_getMaxTextureSize(CRITICAL_JNI_PARAMS) {
+static jint android_view_DisplayListCanvas_getMaxTextureSize(JNIEnv*, jobject) {
#ifdef __ANDROID__ // Layoutlib does not support RenderProxy (RenderThread)
return android::uirenderer::renderthread::RenderProxy::maxTextureSize();
#else
@@ -175,14 +175,14 @@
const char* const kClassPathName = "android/graphics/RecordingCanvas";
static JNINativeMethod gMethods[] = {
+ {"nGetMaximumTextureWidth", "()I", (void*)android_view_DisplayListCanvas_getMaxTextureSize},
+ {"nGetMaximumTextureHeight", "()I",
+ (void*)android_view_DisplayListCanvas_getMaxTextureSize},
// ------------ @CriticalNative --------------
{"nCreateDisplayListCanvas", "(JII)J",
(void*)android_view_DisplayListCanvas_createDisplayListCanvas},
{"nResetDisplayListCanvas", "(JJII)V",
(void*)android_view_DisplayListCanvas_resetDisplayListCanvas},
- {"nGetMaximumTextureWidth", "()I", (void*)android_view_DisplayListCanvas_getMaxTextureSize},
- {"nGetMaximumTextureHeight", "()I",
- (void*)android_view_DisplayListCanvas_getMaxTextureSize},
{"nEnableZ", "(JZ)V", (void*)android_view_DisplayListCanvas_enableZ},
{"nFinishRecording", "(JJ)V", (void*)android_view_DisplayListCanvas_finishRecording},
{"nDrawRenderNode", "(JJ)V", (void*)android_view_DisplayListCanvas_drawRenderNode},
diff --git a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
index 58c14c1..e917f9a 100644
--- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
+++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
@@ -236,6 +236,17 @@
}
}
+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 a8e4580..3bd091d 100644
--- a/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
+++ b/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
@@ -105,6 +105,8 @@
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 224c878..f949ddd 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 @@
}
});
}
+ 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 f6be7b2..064d42e 100644
--- a/libs/hwui/tests/unit/SkiaDisplayListTests.cpp
+++ b/libs/hwui/tests/unit/SkiaDisplayListTests.cpp
@@ -129,6 +129,33 @@
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/MidiDeviceServer.java b/media/java/android/media/midi/MidiDeviceServer.java
index fc33cef..8f1b9fe 100644
--- a/media/java/android/media/midi/MidiDeviceServer.java
+++ b/media/java/android/media/midi/MidiDeviceServer.java
@@ -25,6 +25,7 @@
import android.system.OsConstants;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.midi.MidiDispatcher;
import dalvik.system.CloseGuard;
@@ -34,6 +35,7 @@
import java.io.Closeable;
import java.io.FileDescriptor;
import java.io.IOException;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
@@ -83,6 +85,14 @@
private AtomicInteger mTotalInputBytes = new AtomicInteger();
private AtomicInteger mTotalOutputBytes = new AtomicInteger();
+ private static final int UNUSED_UID = -1;
+
+ private final Object mUmpUidLock = new Object();
+ @GuardedBy("mUmpUidLock")
+ private final int[] mUmpInputPortUids;
+ @GuardedBy("mUmpUidLock")
+ private final int[] mUmpOutputPortUids;
+
public interface Callback {
/**
* Called to notify when an our device status has changed
@@ -137,6 +147,9 @@
int portNumber = mOutputPort.getPortNumber();
mInputPortOutputPorts[portNumber] = null;
mInputPortOpen[portNumber] = false;
+ synchronized (mUmpUidLock) {
+ mUmpInputPortUids[portNumber] = UNUSED_UID;
+ }
mTotalOutputBytes.addAndGet(mOutputPort.pullTotalBytesCount());
updateTotalBytes();
updateDeviceStatus();
@@ -162,6 +175,9 @@
dispatcher.getSender().disconnect(mInputPort);
int openCount = dispatcher.getReceiverCount();
mOutputPortOpenCount[portNumber] = openCount;
+ synchronized (mUmpUidLock) {
+ mUmpOutputPortUids[portNumber] = UNUSED_UID;
+ }
mTotalInputBytes.addAndGet(mInputPort.pullTotalBytesCount());
updateTotalBytes();
updateDeviceStatus();
@@ -210,6 +226,25 @@
return null;
}
+ if (isUmpDevice()) {
+ if (portNumber >= mOutputPortCount) {
+ Log.e(TAG, "out portNumber out of range in openInputPort: " + portNumber);
+ return null;
+ }
+ synchronized (mUmpUidLock) {
+ if (mUmpInputPortUids[portNumber] != UNUSED_UID) {
+ Log.e(TAG, "input port already open in openInputPort: " + portNumber);
+ return null;
+ }
+ if ((mUmpOutputPortUids[portNumber] != UNUSED_UID)
+ && (Binder.getCallingUid() != mUmpOutputPortUids[portNumber])) {
+ Log.e(TAG, "different uid for output in openInputPort: " + portNumber);
+ return null;
+ }
+ mUmpInputPortUids[portNumber] = Binder.getCallingUid();
+ }
+ }
+
try {
FileDescriptor[] pair = createSeqPacketSocketPair();
MidiOutputPort outputPort = new MidiOutputPort(pair[0], portNumber);
@@ -242,6 +277,25 @@
return null;
}
+ if (isUmpDevice()) {
+ if (portNumber >= mInputPortCount) {
+ Log.e(TAG, "in portNumber out of range in openOutputPort: " + portNumber);
+ return null;
+ }
+ synchronized (mUmpUidLock) {
+ if (mUmpOutputPortUids[portNumber] != UNUSED_UID) {
+ Log.e(TAG, "output port already open in openOutputPort: " + portNumber);
+ return null;
+ }
+ if ((mUmpInputPortUids[portNumber] != UNUSED_UID)
+ && (Binder.getCallingUid() != mUmpInputPortUids[portNumber])) {
+ Log.e(TAG, "different uid for input in openOutputPort: " + portNumber);
+ return null;
+ }
+ mUmpOutputPortUids[portNumber] = Binder.getCallingUid();
+ }
+ }
+
try {
FileDescriptor[] pair = createSeqPacketSocketPair();
MidiInputPort inputPort = new MidiInputPort(pair[0], portNumber);
@@ -358,6 +412,13 @@
mInputPortOpen = new boolean[mInputPortCount];
mOutputPortOpenCount = new int[numOutputPorts];
+ synchronized (mUmpUidLock) {
+ mUmpInputPortUids = new int[mInputPortCount];
+ mUmpOutputPortUids = new int[mOutputPortCount];
+ Arrays.fill(mUmpInputPortUids, UNUSED_UID);
+ Arrays.fill(mUmpOutputPortUids, UNUSED_UID);
+ }
+
mGuard.open("close");
}
@@ -467,4 +528,8 @@
Log.e(TAG, "RemoteException in updateTotalBytes");
}
}
+
+ private boolean isUmpDevice() {
+ return mDeviceInfo.getDefaultProtocol() != MidiDeviceInfo.PROTOCOL_UNKNOWN;
+ }
}
diff --git a/media/java/android/media/midi/MidiDeviceService.java b/media/java/android/media/midi/MidiDeviceService.java
index 388d95b..96540a2 100644
--- a/media/java/android/media/midi/MidiDeviceService.java
+++ b/media/java/android/media/midi/MidiDeviceService.java
@@ -34,15 +34,15 @@
* <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 @@
}
/**
- * 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 @@
/**
* 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
new file mode 100644
index 0000000..0c6096e
--- /dev/null
+++ b/media/java/android/media/midi/MidiUmpDeviceService.java
@@ -0,0 +1,161 @@
+/*
+ * 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 android.media.midi;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A service that implements a virtual MIDI device for Universal MIDI Packets (UMP).
+ * Subclasses must implement the {@link #onGetInputPortReceivers} method to provide a
+ * list of {@link MidiReceiver}s to receive data sent to the device's input ports.
+ * Similarly, subclasses can call {@link #getOutputPortReceivers} to fetch a list
+ * of {@link MidiReceiver}s for sending data out the output ports.
+ *
+ * Unlike traditional MIDI byte streams, only complete UMPs should be sent.
+ * Unlike with {@link #MidiDeviceService}, the number of input and output ports must be equal.
+ *
+ * <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>
+ * <pre>
+ * <service android:name=".VirtualDeviceService"
+ * android:label="@string/service_name">
+ * <intent-filter>
+ * <action android:name="android.media.midi.MidiUmpDeviceService" />
+ * </intent-filter>
+ * <property android:name="android.media.midi.MidiUmpDeviceService"
+ * android:resource="@xml/device_info" />
+ * </service></pre>
+ */
+public abstract class MidiUmpDeviceService extends Service {
+ private static final String TAG = "MidiUmpDeviceService";
+
+ public static final String SERVICE_INTERFACE = "android.media.midi.MidiUmpDeviceService";
+
+ private IMidiManager mMidiManager;
+ private MidiDeviceServer mServer;
+ private MidiDeviceInfo mDeviceInfo;
+
+ private final MidiDeviceServer.Callback mCallback = new MidiDeviceServer.Callback() {
+ @Override
+ public void onDeviceStatusChanged(MidiDeviceServer server, MidiDeviceStatus status) {
+ MidiUmpDeviceService.this.onDeviceStatusChanged(status);
+ }
+
+ @Override
+ public void onClose() {
+ MidiUmpDeviceService.this.onClose();
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ mMidiManager = IMidiManager.Stub.asInterface(
+ ServiceManager.getService(Context.MIDI_SERVICE));
+ MidiDeviceServer server;
+ try {
+ MidiDeviceInfo deviceInfo = mMidiManager.getServiceDeviceInfo(getPackageName(),
+ this.getClass().getName());
+ if (deviceInfo == null) {
+ Log.e(TAG, "Could not find MidiDeviceInfo for MidiUmpDeviceService " + this);
+ return;
+ }
+ mDeviceInfo = deviceInfo;
+
+ List<MidiReceiver> inputPortReceivers = onGetInputPortReceivers();
+ if (inputPortReceivers == null) {
+ Log.e(TAG, "Could not get input port receivers for MidiUmpDeviceService " + this);
+ return;
+ }
+ MidiReceiver[] inputPortReceiversArr = new MidiReceiver[inputPortReceivers.size()];
+ inputPortReceivers.toArray(inputPortReceiversArr);
+ server = new MidiDeviceServer(mMidiManager, inputPortReceiversArr, deviceInfo,
+ mCallback);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException in IMidiManager.getServiceDeviceInfo");
+ server = null;
+ }
+ mServer = server;
+ }
+
+ /**
+ * Returns a list of {@link MidiReceiver} for the device's input ports.
+ * Subclasses must override this to provide the receivers which will receive
+ * data sent to the device's input ports.
+ * The number of input and output ports must be equal and non-zero.
+ * @return list of MidiReceivers
+ */
+ public abstract @NonNull List<MidiReceiver> onGetInputPortReceivers();
+
+ /**
+ * Returns a list of {@link MidiReceiver} for the device's output ports.
+ * These can be used to send data out the device's output ports.
+ * The number of input and output ports must be equal and non-zero.
+ * @return the list of MidiReceivers
+ */
+ public final @NonNull List<MidiReceiver> getOutputPortReceivers() {
+ if (mServer == null) {
+ return new ArrayList<MidiReceiver>();
+ } else {
+ return Arrays.asList(mServer.getOutputPortReceivers());
+ }
+ }
+
+ /**
+ * Returns the {@link MidiDeviceInfo} instance for this service
+ * @return the MidiDeviceInfo of the virtual MIDI device
+ */
+ public final @Nullable MidiDeviceInfo getDeviceInfo() {
+ return mDeviceInfo;
+ }
+
+ /**
+ * Called to notify when the {@link MidiDeviceStatus} has changed
+ * @param status the current status of the MIDI device
+ */
+ public void onDeviceStatusChanged(@Nullable MidiDeviceStatus status) {
+ }
+
+ /**
+ * Called to notify when the virtual MIDI device running in this service has been closed by
+ * all its clients
+ */
+ public void onClose() {
+ }
+
+ @Override
+ public @Nullable IBinder onBind(@NonNull Intent intent) {
+ if (SERVICE_INTERFACE.equals(intent.getAction()) && mServer != null) {
+ return mServer.getBinderInterface().asBinder();
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/media/java/android/media/midi/package.html b/media/java/android/media/midi/package.html
index 67df1b2..d9e38e2 100644
--- a/media/java/android/media/midi/package.html
+++ b/media/java/android/media/midi/package.html
@@ -267,6 +267,56 @@
contain System Real-Time messages, which can be interleaved inside other
messages.</p>
+<h1 id=using_midi_btle>Using MIDI Over Bluetooth LE</h1>
+
+<p>MIDI devices can be connected to Android using Bluetooth LE.</p>
+
+<p>Before using the device, the app must scan for available BTLE devices and then allow
+the user to connect.
+See the Android developer website for an
+<a href="https://source.android.com/devices/audio/midi_test#apps" target="_blank">example
+program</a>.</p>
+
+<h2 id=btle_location_permissions>Request Location Permission for BTLE</h2>
+
+<p>Applications that scan for Bluetooth devices must request permission in the
+manifest file. This LOCATION permission is required because it may be possible to
+guess the location of an Android device by seeing which BTLE devices are nearby.</p>
+
+<pre class=prettyprint>
+<uses-permission android:name="android.permission.BLUETOOTH"/>
+<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
+<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
+</pre>
+
+<p>Apps must also request location permission from the user at run-time.
+See the documentation for <code>Activity.requestPermissions()</code> for details and an example.
+</p>
+
+<h2 id=btle_scan_devices>Scan for MIDI Devices</h2>
+
+<p>The app will only want to see MIDI devices and not mice or other non-MIDI devices.
+So construct a ScanFilter using the UUID for standard MIDI over BTLE.</p>
+
+<pre class=prettyprint>
+MIDI over BTLE UUID = "03B80E5A-EDE8-4B33-A751-6CE34EC4C700"
+</pre>
+
+<h2 id=btle_open_device>Open a MIDI Bluetooth Device</h2>
+
+<p>See the documentation for <code>android.bluetooth.le.BluetoothLeScanner.startScan()</code>
+method for details. When the user selects a MIDI/BTLE device then you can open it
+using the MidiManager.</p>
+
+<pre class=prettyprint>
+m.openBluetoothDevice(bluetoothDevice, callback, handler);
+</pre>
+
+<p>Once the MIDI/BTLE device has been opened by one app then it will also become available to other
+apps using the
+<a href="#get_list_of_already_plugged_in_entities">MIDI device discovery calls described above</a>.
+</p>
+
<h1 id=creating_a_midi_virtual_device_service>Creating a MIDI Virtual Device Service</h1>
@@ -355,62 +405,17 @@
}
</pre>
-<h1 id=using_midi_btle>Using MIDI Over Bluetooth LE</h1>
-<p>MIDI devices can be connected to Android using Bluetooth LE.</p>
+<h1 id=using_midi_2_0>Using MIDI 2.0</h1>
-<p>Before using the device, the app must scan for available BTLE devices and then allow
-the user to connect.
-See the Android developer website for an
-<a href="https://source.android.com/devices/audio/midi_test#apps" target="_blank">example
-program</a>.</p>
-
-<h2 id=btle_location_permissions>Request Location Permission for BTLE</h2>
-
-<p>Applications that scan for Bluetooth devices must request permission in the
-manifest file. This LOCATION permission is required because it may be possible to
-guess the location of an Android device by seeing which BTLE devices are nearby.</p>
-
-<pre class=prettyprint>
-<uses-permission android:name="android.permission.BLUETOOTH"/>
-<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
-<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
-</pre>
-
-<p>Apps must also request location permission from the user at run-time.
-See the documentation for <code>Activity.requestPermissions()</code> for details and an example.
-</p>
-
-<h2 id=btle_scan_devices>Scan for MIDI Devices</h2>
-
-<p>The app will only want to see MIDI devices and not mice or other non-MIDI devices.
-So construct a ScanFilter using the UUID for standard MIDI over BTLE.</p>
-
-<pre class=prettyprint>
-MIDI over BTLE UUID = "03B80E5A-EDE8-4B33-A751-6CE34EC4C700"
-</pre>
-
-<h2 id=btle_open_device>Open a MIDI Bluetooth Device</h2>
-
-<p>See the documentation for <code>android.bluetooth.le.BluetoothLeScanner.startScan()</code>
-method for details. When the user selects a MIDI/BTLE device then you can open it
-using the MidiManager.</p>
-
-<pre class=prettyprint>
-m.openBluetoothDevice(bluetoothDevice, callback, handler);
-</pre>
-
-<p>Once the MIDI/BTLE device has been opened by one app then it will also become available to other
-apps using the
-<a href="#get_list_of_already_plugged_in_entities">MIDI device discovery calls described above</a>.
-</p>
-
-<h1 id=using_midi_2_0_over_usb>Using MIDI 2.0 over USB</h1>
-
-<p>An app can use MIDI 2.0 over USB starting in Android T. MIDI 2.0 packets are embedded in
+<p>An app can use <a href=
+"https://www.midi.org/midi-articles/details-about-midi-2-0-midi-ci-profiles-and-property-exchange"
+class="external">MIDI 2.0</a> over USB starting in Android T. MIDI 2.0 packets are embedded in
Universal MIDI Packets, or UMP for short. A MIDI 2.0 USB device should create two interfaces,
one endpoint that accepts only MIDI 1.0 packets and one that accepts only UMP packets.
-For more info about MIDI 2.0 and UMP, please read the MIDI 2.0 USB spec.</p>
+For more info about MIDI 2.0 and UMP, please read the MIDI 2.0 and UMP spec. Starting from Android
+V, apps can also open <a href="#creating_a_midi_2_0_virtual_device_service"> MIDI 2.0 virtual</a>
+devices.</p>
<p>MidiManager.getDevices() would simply return the 1.0 interface. This interface should work
exactly the same as before. In order to use the new UMP interface, retrieve the device with the
@@ -421,15 +426,15 @@
MidiManager.TRANSPORT_UNIVERSAL_MIDI_PACKETS);
</pre>
-<p>UMP Packets are always in multiple of 4 bytes. For each set of 4 bytes, they are sent in network
-order. Compare the following NoteOn code snippet with the NoteOn code snippet above. </p>
+<p>UMP packet sizes are always a multiple of 4 bytes. For each set of 4 bytes, they are sent in
+network order. Compare the following NoteOn code snippet with the NoteOn code snippet above. </p>
<pre class=prettyprint>
byte[] buffer = new byte[32];
int numBytes = 0;
int channel = 3; // MIDI channels 1-16 are encoded as 0-15.
int group = 0;
-buffer[numBytes++] = (byte)(0x20 + group); // MIDI 1.0 voice message
+buffer[numBytes++] = (byte)(0x20 + group); // MIDI 1.0 Channel Voice Message
buffer[numBytes++] = (byte)(0x90 + (channel - 1)); // note on
buffer[numBytes++] = (byte)60; // pitch is middle C
buffer[numBytes++] = (byte)127; // max velocity
@@ -446,5 +451,108 @@
int defaultProtocol = info.getDefaultProtocol();
</pre>
+<h1 id=creating_a_midi_2_0_virtual_device_service>Creating a MIDI 2.0 Virtual Device Service</h1>
+
+
+<p>Starting in Android V, an app can provide a MIDI 2.0 Service that can be used by other apps.
+MIDI 2.0 packets are embedded in Universal MIDI Packets, or UMP for short. The service must be
+guarded with permission "android.permission.BIND_MIDI_DEVICE_SERVICE".</p>
+
+<h2 id=manifest_files>Manifest Files</h2>
+
+
+<p>An app declares that it will function as a MIDI server in the AndroidManifest.xml file. Unlike
+MIDI 1.0 virtual devices, android.media.midi.MidiUmpDeviceService is used</p>
+
+<pre class=prettyprint>
+<service android:name="<strong>MidiEchoDeviceService</strong>"
+ android:permission="android.permission.BIND_MIDI_DEVICE_SERVICE">
+ <intent-filter>
+ <action android:name="android.media.midi.MidiUmpDeviceService" />
+ </intent-filter>
+ <meta-data android:name="android.media.midi.MidiUmpDeviceService"
+ android:resource="@xml/<strong>echo_device_info</strong>" />
+</service>
+</pre>
+
+
+<p>The details of the resource in this example is stored in “res/xml/echo_device_info.xml
+”. The port names that you declare in this file will be available from PortInfo.getName().
+Unlike MIDI 1.0, MIDI 2.0 ports are bidirectional. If you declare a port in this service, then it
+automatically creates an input port and an output port with the same name. Clients can use those
+two ports like the MIDI 1.0 ports.</p>
+
+<pre class=prettyprint>
+<devices>
+ <device manufacturer="MyCompany" product="MidiEcho">
+ <port name="port1" />
+ </device>
+</devices>
+</pre>
+
+
+<h2 id=extend_midiumpdeviceservice>Extend MidiUmpDeviceService</h2>
+
+
+<p>You then define your server by extending android.media.midi.MidiUmpDeviceService.</p>
+
+<pre class=prettyprint>
+import android.media.midi.MidiDeviceStatus;
+import android.media.midi.MidiReceiver;
+import android.media.midi.MidiUmpDeviceService;
+
+public class MidiEchoDeviceService extends MidiUmpDeviceService {
+ private static final String TAG = "MidiEchoDeviceService";
+ // Other apps will write to this port.
+ private MidiReceiver mInputReceiver = new MyReceiver();
+ // This app will copy the data to this port.
+ private MidiReceiver mOutputReceiver;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ }
+
+ @Override
+ // Declare the receivers associated with your input ports.
+ public List<MidiReceiver> onGetInputPortReceivers() {
+ return new ArrayList<MidiReceiver>(Collections.singletonList(mInputReceiver));
+ }
+
+ /**
+ * Sample receiver to echo from the input port to the output port.
+ * In this example, we are just echoing the data and not parsing it.
+ * You will probably want to convert the bytes to a packet and then interpret the packet.
+ * See the MIDI 2.0 spec at the MMA site. Packets are either 4, 8, 12 or 16 bytes.
+ */
+ class MyReceiver extends MidiReceiver {
+ @Override
+ public void onSend(byte[] data, int offset, int count, long timestamp)
+ throws IOException {
+ if (mOutputReceiver == null) {
+ mOutputReceiver = getOutputPortReceivers().get(0);
+ }
+ // Copy input to output.
+ mOutputReceiver.send(data, offset, count, timestamp);
+ }
+ }
+
+ /**
+ * This will get called when clients connect or disconnect.
+ * You can use it to figure out how many devices are connected.
+ */
+ @Override
+ public void onDeviceStatusChanged(MidiDeviceStatus status) {
+ // inputOpened = status.isInputPortOpen(0);
+ // outputOpenCount = status.getOutputPortOpenCount(0);
+ }
+}
+</pre>
+
</body>
</html>
diff --git a/native/android/configuration.cpp b/native/android/configuration.cpp
index b50514d..283445f 100644
--- a/native/android/configuration.cpp
+++ b/native/android/configuration.cpp
@@ -36,7 +36,7 @@
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 3adb882..3252aad 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 60bc226..dab3bcb 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/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
index 441d3a5..a6536a8c 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java
@@ -29,6 +29,7 @@
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -365,16 +366,17 @@
public synchronized void onDeviceUnpaired(CachedBluetoothDevice device) {
device.setGroupId(BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
CachedBluetoothDevice mainDevice = mCsipDeviceManager.findMainDevice(device);
- final Set<CachedBluetoothDevice> memberDevices = device.getMemberDevice();
+ // Should iterate through the cloned set to avoid ConcurrentModificationException
+ final Set<CachedBluetoothDevice> memberDevices = new HashSet<>(device.getMemberDevice());
if (!memberDevices.isEmpty()) {
- // Main device is unpaired, to unpair the member device
+ // Main device is unpaired, also unpair the member devices
for (CachedBluetoothDevice memberDevice : memberDevices) {
memberDevice.unpair();
memberDevice.setGroupId(BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
device.removeMemberDevice(memberDevice);
}
} else if (mainDevice != null) {
- // the member device unpaired, to unpair main device
+ // Member device is unpaired, also unpair the main device
mainDevice.unpair();
}
mainDevice = mHearingAidDeviceManager.findMainDevice(device);
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index 7a5a382..c59b0f9 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/compose/core/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
new file mode 100644
index 0000000..566967f
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
@@ -0,0 +1,162 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.DisposableEffectResult
+import androidx.compose.runtime.DisposableEffectScope
+import androidx.compose.runtime.State
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.lerp
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.lerp
+import com.android.compose.ui.util.lerp
+
+/**
+ * Animate a shared Int value.
+ *
+ * @see SceneScope.animateSharedValueAsState
+ */
+@Composable
+fun SceneScope.animateSharedIntAsState(
+ value: Int,
+ key: ValueKey,
+ element: ElementKey,
+ canOverflow: Boolean = true,
+): State<Int> {
+ return animateSharedValueAsState(value, key, element, ::lerp, canOverflow)
+}
+
+/**
+ * Animate a shared Float value.
+ *
+ * @see SceneScope.animateSharedValueAsState
+ */
+@Composable
+fun SceneScope.animateSharedFloatAsState(
+ value: Float,
+ key: ValueKey,
+ element: ElementKey,
+ canOverflow: Boolean = true,
+): State<Float> {
+ return animateSharedValueAsState(value, key, element, ::lerp, canOverflow)
+}
+
+/**
+ * Animate a shared Dp value.
+ *
+ * @see SceneScope.animateSharedValueAsState
+ */
+@Composable
+fun SceneScope.animateSharedDpAsState(
+ value: Dp,
+ key: ValueKey,
+ element: ElementKey,
+ canOverflow: Boolean = true,
+): State<Dp> {
+ return animateSharedValueAsState(value, key, element, ::lerp, canOverflow)
+}
+
+/**
+ * Animate a shared Color value.
+ *
+ * @see SceneScope.animateSharedValueAsState
+ */
+@Composable
+fun SceneScope.animateSharedColorAsState(
+ value: Color,
+ key: ValueKey,
+ element: ElementKey,
+): State<Color> {
+ return animateSharedValueAsState(value, key, element, ::lerp, canOverflow = false)
+}
+
+@Composable
+internal fun <T> animateSharedValueAsState(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ key: ValueKey,
+ value: T,
+ lerp: (T, T, Float) -> T,
+ canOverflow: Boolean,
+): State<T> {
+ val sharedValue = remember(key) { Element.SharedValue(key, value) }
+ if (value != sharedValue.value) {
+ sharedValue.value = value
+ }
+
+ DisposableEffect(element, scene, sharedValue) {
+ addSharedValueToElement(element, scene, sharedValue)
+ }
+
+ return remember(layoutImpl, element, sharedValue, lerp, canOverflow) {
+ derivedStateOf { computeValue(layoutImpl, element, sharedValue, lerp, canOverflow) }
+ }
+}
+
+private fun <T> DisposableEffectScope.addSharedValueToElement(
+ element: Element,
+ scene: Scene,
+ sharedValue: Element.SharedValue<T>,
+): DisposableEffectResult {
+ val sceneValues =
+ element.sceneValues[scene.key] ?: error("Element $element is not present in $scene")
+ val sharedValues = sceneValues.sharedValues
+
+ sharedValues[sharedValue.key] = sharedValue
+ return onDispose { sharedValues.remove(sharedValue.key) }
+}
+
+private fun <T> computeValue(
+ layoutImpl: SceneTransitionLayoutImpl,
+ element: Element,
+ sharedValue: Element.SharedValue<T>,
+ lerp: (T, T, Float) -> T,
+ canOverflow: Boolean,
+): T {
+ val state = layoutImpl.state.transitionState
+ if (
+ state !is TransitionState.Transition ||
+ state.fromScene == state.toScene ||
+ !layoutImpl.isTransitionReady(state)
+ ) {
+ return sharedValue.value
+ }
+
+ fun sceneValue(scene: SceneKey): Element.SharedValue<T>? {
+ val sceneValues = element.sceneValues[scene] ?: return null
+ val value = sceneValues.sharedValues[sharedValue.key] ?: return null
+ return value as Element.SharedValue<T>
+ }
+
+ val fromValue = sceneValue(state.fromScene)
+ val toValue = sceneValue(state.toScene)
+ return if (fromValue != null && toValue != null) {
+ val progress = if (canOverflow) state.progress else state.progress.coerceIn(0f, 1f)
+ lerp(fromValue.value, toValue.value, progress)
+ } else if (fromValue != null) {
+ fromValue.value
+ } else if (toValue != null) {
+ toValue.value
+ } else {
+ sharedValue.value
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateToScene.kt
new file mode 100644
index 0000000..7536728
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/AnimateToScene.kt
@@ -0,0 +1,150 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.SpringSpec
+import kotlin.math.absoluteValue
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Transition to [target] using a canned animation. This function will try to be smart and take over
+ * the currently running transition, if there is one.
+ */
+internal fun CoroutineScope.animateToScene(
+ layoutImpl: SceneTransitionLayoutImpl,
+ target: SceneKey,
+) {
+ val state = layoutImpl.state.transitionState
+ if (state.currentScene == target) {
+ // This can happen in 3 different situations, for which there isn't anything else to do:
+ // 1. There is no ongoing transition and [target] is already the current scene.
+ // 2. The user is swiping to [target] from another scene and released their pointer such
+ // that the gesture was committed and the transition is animating to [scene] already.
+ // 3. The user is swiping from [target] to another scene and either:
+ // a. didn't release their pointer yet.
+ // b. released their pointer such that the swipe gesture was cancelled and the
+ // transition is currently animating back to [target].
+ return
+ }
+
+ when (state) {
+ is TransitionState.Idle -> animate(layoutImpl, target)
+ is TransitionState.Transition -> {
+ if (state.toScene == state.fromScene) {
+ // Same as idle.
+ animate(layoutImpl, target)
+ return
+ }
+
+ // A transition is currently running: first check whether `transition.toScene` or
+ // `transition.fromScene` is the same as our target scene, in which case the transition
+ // can be accelerated or reversed to end up in the target state.
+
+ if (state.toScene == target) {
+ // The user is currently swiping to [target] but didn't release their pointer yet:
+ // animate the progress to `1`.
+
+ check(state.fromScene == state.currentScene)
+ val progress = state.progress
+ if ((1f - progress).absoluteValue < ProgressVisibilityThreshold) {
+ // The transition is already finished (progress ~= 1): no need to animate.
+ layoutImpl.state.transitionState = TransitionState.Idle(state.currentScene)
+ } else {
+ // The transition is in progress: start the canned animation at the same
+ // progress as it was in.
+ // TODO(b/290184746): Also take the current velocity into account.
+ animate(layoutImpl, target, startProgress = progress)
+ }
+
+ return
+ }
+
+ if (state.fromScene == target) {
+ // There is a transition from [target] to another scene: simply animate the same
+ // transition progress to `0`.
+
+ check(state.toScene == state.currentScene)
+ val progress = state.progress
+ if (progress.absoluteValue < ProgressVisibilityThreshold) {
+ // The transition is at progress ~= 0: no need to animate.
+ layoutImpl.state.transitionState = TransitionState.Idle(state.currentScene)
+ } else {
+ // TODO(b/290184746): Also take the current velocity into account.
+ animate(layoutImpl, target, startProgress = progress, reversed = true)
+ }
+
+ return
+ }
+
+ // Generic interruption; the current transition is neither from or to [target].
+ // TODO(b/290930950): Better handle interruptions here.
+ animate(layoutImpl, target)
+ }
+ }
+}
+
+private fun CoroutineScope.animate(
+ layoutImpl: SceneTransitionLayoutImpl,
+ target: SceneKey,
+ startProgress: Float = 0f,
+ reversed: Boolean = false,
+) {
+ val fromScene = layoutImpl.state.transitionState.currentScene
+
+ val animationSpec = layoutImpl.transitions.transitionSpec(fromScene, target).spec
+ val visibilityThreshold =
+ (animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold
+ val animatable = Animatable(startProgress, visibilityThreshold = visibilityThreshold)
+
+ val targetProgress = if (reversed) 0f else 1f
+ val transition =
+ if (reversed) {
+ OneOffTransition(target, fromScene, currentScene = target, animatable)
+ } else {
+ OneOffTransition(fromScene, target, currentScene = target, animatable)
+ }
+
+ // Change the current layout state to use this new transition.
+ layoutImpl.state.transitionState = transition
+
+ // Animate the progress to its target value.
+ launch {
+ animatable.animateTo(targetProgress, animationSpec)
+
+ // Unless some other external state change happened, the state should now be idle.
+ if (layoutImpl.state.transitionState == transition) {
+ layoutImpl.state.transitionState = TransitionState.Idle(target)
+ }
+ }
+}
+
+private class OneOffTransition(
+ override val fromScene: SceneKey,
+ override val toScene: SceneKey,
+ override val currentScene: SceneKey,
+ private val animatable: Animatable<Float, AnimationVector1D>,
+) : TransitionState.Transition {
+ override val progress: Float
+ get() = animatable.value
+}
+
+// TODO(b/290184746): Compute a good default visibility threshold that depends on the layout size
+// and screen density.
+private const val ProgressVisibilityThreshold = 1e-3f
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Element.kt
new file mode 100644
index 0000000..0cc259a
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Element.kt
@@ -0,0 +1,449 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.Snapshot
+import androidx.compose.runtime.snapshots.SnapshotStateMap
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.isSpecified
+import androidx.compose.ui.geometry.lerp
+import androidx.compose.ui.graphics.graphicsLayer
+import androidx.compose.ui.layout.IntermediateMeasureScope
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.Placeable
+import androidx.compose.ui.layout.intermediateLayout
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.round
+import com.android.compose.animation.scene.transformation.PropertyTransformation
+import com.android.compose.modifiers.thenIf
+import com.android.compose.ui.util.lerp
+
+/** An element on screen, that can be composed in one or more scenes. */
+internal class Element(val key: ElementKey) {
+ /**
+ * The last offset assigned to this element, relative to the SceneTransitionLayout containing
+ * it.
+ */
+ var lastOffset = Offset.Unspecified
+
+ /** The last size assigned to this element. */
+ var lastSize = SizeUnspecified
+
+ /** The last alpha assigned to this element. */
+ var lastAlpha = 1f
+
+ /** The mapping between a scene and the values/state this element has in that scene, if any. */
+ val sceneValues = SnapshotStateMap<SceneKey, SceneValues>()
+
+ override fun toString(): String {
+ return "Element(key=$key)"
+ }
+
+ /** The target values of this element in a given scene. */
+ class SceneValues {
+ var size by mutableStateOf(SizeUnspecified)
+ var offset by mutableStateOf(Offset.Unspecified)
+ val sharedValues = SnapshotStateMap<ValueKey, SharedValue<*>>()
+ }
+
+ /** A shared value of this element. */
+ class SharedValue<T>(val key: ValueKey, initialValue: T) {
+ var value by mutableStateOf(initialValue)
+ }
+
+ companion object {
+ val SizeUnspecified = IntSize(Int.MAX_VALUE, Int.MAX_VALUE)
+ }
+}
+
+/** The implementation of [SceneScope.element]. */
+@Composable
+@OptIn(ExperimentalComposeUiApi::class)
+internal fun Modifier.element(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ key: ElementKey,
+): Modifier {
+ val sceneValues = remember(scene, key) { Element.SceneValues() }
+ val element =
+ // Get the element associated to [key] if it was already composed in another scene,
+ // otherwise create it and add it to our Map<ElementKey, Element>. This is done inside a
+ // withoutReadObservation() because there is no need to recompose when that map is mutated.
+ Snapshot.withoutReadObservation {
+ val element =
+ layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it }
+ val previousValues = element.sceneValues[scene.key]
+ if (previousValues == null) {
+ element.sceneValues[scene.key] = sceneValues
+ } else if (previousValues != sceneValues) {
+ error("$key was composed multiple times in $scene")
+ }
+
+ element
+ }
+
+ DisposableEffect(scene, sceneValues, element) {
+ onDispose {
+ element.sceneValues.remove(scene.key)
+
+ // This was the last scene this element was in, so remove it from the map.
+ if (element.sceneValues.isEmpty()) {
+ layoutImpl.elements.remove(element.key)
+ }
+ }
+ }
+
+ val alpha =
+ remember(layoutImpl, element, scene) {
+ derivedStateOf { elementAlpha(layoutImpl, element, scene) }
+ }
+ val isOpaque by remember(alpha) { derivedStateOf { alpha.value == 1f } }
+ SideEffect {
+ if (isOpaque && element.lastAlpha != 1f) {
+ element.lastAlpha = 1f
+ }
+ }
+
+ return drawWithContent {
+ if (shouldDrawElement(layoutImpl, scene, element)) {
+ drawContent()
+ }
+ }
+ .modifierTransformations(layoutImpl, scene, element, sceneValues)
+ .intermediateLayout { measurable, constraints ->
+ val placeable =
+ measure(layoutImpl, scene, element, sceneValues, measurable, constraints)
+ layout(placeable.width, placeable.height) {
+ place(layoutImpl, scene, element, sceneValues, placeable, placementScope = this)
+ }
+ }
+ .thenIf(!isOpaque) {
+ Modifier.graphicsLayer {
+ val alpha = alpha.value
+ this.alpha = alpha
+ element.lastAlpha = alpha
+ }
+ }
+ .testTag(key.name)
+}
+
+private fun shouldDrawElement(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+): Boolean {
+ val state = layoutImpl.state.transitionState
+
+ // Always draw the element if there is no ongoing transition or if the element is not shared.
+ if (
+ state !is TransitionState.Transition ||
+ state.fromScene == state.toScene ||
+ !layoutImpl.isTransitionReady(state) ||
+ state.fromScene !in element.sceneValues ||
+ state.toScene !in element.sceneValues
+ ) {
+ return true
+ }
+
+ val otherScene =
+ layoutImpl.scenes.getValue(
+ if (scene.key == state.fromScene) {
+ state.toScene
+ } else {
+ state.fromScene
+ }
+ )
+
+ // When the element is shared, draw the one in the highest scene unless it is a background, i.e.
+ // it is usually drawn below everything else.
+ val isHighestScene = scene.zIndex > otherScene.zIndex
+ return if (element.key.isBackground) {
+ !isHighestScene
+ } else {
+ isHighestScene
+ }
+}
+
+/**
+ * Chain the [com.android.compose.animation.scene.transformation.ModifierTransformation] applied
+ * throughout the current transition, if any.
+ */
+private fun Modifier.modifierTransformations(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+): Modifier {
+ when (val state = layoutImpl.state.transitionState) {
+ is TransitionState.Idle -> return this
+ is TransitionState.Transition -> {
+ val fromScene = state.fromScene
+ val toScene = state.toScene
+ if (fromScene == toScene) {
+ // Same as idle.
+ return this
+ }
+
+ return layoutImpl.transitions
+ .transitionSpec(fromScene, state.toScene)
+ .transformations(element.key)
+ .modifier
+ .fold(this) { modifier, transformation ->
+ with(transformation) {
+ modifier.transform(layoutImpl, scene, element, sceneValues)
+ }
+ }
+ }
+ }
+}
+
+private fun elementAlpha(
+ layoutImpl: SceneTransitionLayoutImpl,
+ element: Element,
+ scene: Scene
+): Float {
+ return computeValue(
+ layoutImpl,
+ scene,
+ element,
+ sceneValue = { 1f },
+ transformation = { it.alpha },
+ idleValue = 1f,
+ currentValue = { 1f },
+ lastValue = { element.lastAlpha },
+ ::lerp,
+ )
+ .coerceIn(0f, 1f)
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+private fun IntermediateMeasureScope.measure(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ measurable: Measurable,
+ constraints: Constraints,
+): Placeable {
+ // Update the size this element has in this scene when idle.
+ val targetSizeInScene = lookaheadSize
+ if (targetSizeInScene != sceneValues.size) {
+ // TODO(b/290930950): Better handle when this changes to avoid instant size jumps.
+ sceneValues.size = targetSizeInScene
+ }
+
+ // Some lambdas called (max once) by computeValue() will need to measure [measurable], in which
+ // case we store the resulting placeable here to make sure the element is not measured more than
+ // once.
+ var maybePlaceable: Placeable? = null
+
+ fun Placeable.size() = IntSize(width, height)
+
+ val targetSize =
+ computeValue(
+ layoutImpl,
+ scene,
+ element,
+ sceneValue = { it.size },
+ transformation = { it.size },
+ idleValue = lookaheadSize,
+ currentValue = { measurable.measure(constraints).also { maybePlaceable = it }.size() },
+ lastValue = {
+ val lastSize = element.lastSize
+ if (lastSize != Element.SizeUnspecified) {
+ lastSize
+ } else {
+ measurable.measure(constraints).also { maybePlaceable = it }.size()
+ }
+ },
+ ::lerp,
+ )
+
+ val placeable =
+ maybePlaceable
+ ?: measurable.measure(
+ Constraints.fixed(
+ targetSize.width.coerceAtLeast(0),
+ targetSize.height.coerceAtLeast(0),
+ )
+ )
+
+ element.lastSize = placeable.size()
+ return placeable
+}
+
+@OptIn(ExperimentalComposeUiApi::class)
+private fun IntermediateMeasureScope.place(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ placeable: Placeable,
+ placementScope: Placeable.PlacementScope,
+) {
+ with(placementScope) {
+ // Update the offset (relative to the SceneTransitionLayout) this element has in this scene
+ // when idle.
+ val coords = coordinates!!
+ val targetOffsetInScene = lookaheadScopeCoordinates.localLookaheadPositionOf(coords)
+ if (targetOffsetInScene != sceneValues.offset) {
+ // TODO(b/290930950): Better handle when this changes to avoid instant offset jumps.
+ sceneValues.offset = targetOffsetInScene
+ }
+
+ val currentOffset = lookaheadScopeCoordinates.localPositionOf(coords, Offset.Zero)
+ val targetOffset =
+ computeValue(
+ layoutImpl,
+ scene,
+ element,
+ sceneValue = { it.offset },
+ transformation = { it.offset },
+ idleValue = targetOffsetInScene,
+ currentValue = { currentOffset },
+ lastValue = {
+ val lastValue = element.lastOffset
+ if (lastValue.isSpecified) {
+ lastValue
+ } else {
+ currentOffset
+ }
+ },
+ ::lerp,
+ )
+
+ element.lastOffset = targetOffset
+ placeable.place((targetOffset - currentOffset).round())
+ }
+}
+
+/**
+ * Return the value that should be used depending on the current layout state and transition.
+ *
+ * Important: This function must remain inline because of all the lambda parameters. These lambdas
+ * are necessary because getting some of them might require some computation, like measuring a
+ * Measurable.
+ *
+ * @param layoutImpl the [SceneTransitionLayoutImpl] associated to [element].
+ * @param scene the scene containing [element].
+ * @param element the element being animated.
+ * @param sceneValue the value being animated.
+ * @param transformation the transformation associated to the value being animated.
+ * @param idleValue the value when idle, i.e. when there is no transition happening.
+ * @param currentValue the value that would be used if it is not transformed. Note that this is
+ * different than [idleValue] even if the value is not transformed directly because it could be
+ * impacted by the transformations on other elements, like a parent that is being translated or
+ * resized.
+ * @param lastValue the last value that was used. This should be equal to [currentValue] if this is
+ * the first time the value is set.
+ * @param lerp the linear interpolation function used to interpolate between two values of this
+ * value type.
+ */
+private inline fun <T> computeValue(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValue: (Element.SceneValues) -> T,
+ transformation: (ElementTransformations) -> PropertyTransformation<T>?,
+ idleValue: T,
+ currentValue: () -> T,
+ lastValue: () -> T,
+ lerp: (T, T, Float) -> T,
+): T {
+ val state = layoutImpl.state.transitionState
+
+ // There is no ongoing transition.
+ if (state !is TransitionState.Transition || state.fromScene == state.toScene) {
+ return idleValue
+ }
+
+ // A transition was started but it's not ready yet (not all elements have been composed/laid
+ // out yet). Use the last value that was set, to make sure elements don't unexpectedly jump.
+ if (!layoutImpl.isTransitionReady(state)) {
+ return lastValue()
+ }
+
+ val fromScene = state.fromScene
+ val toScene = state.toScene
+ val fromValues = element.sceneValues[fromScene]
+ val toValues = element.sceneValues[toScene]
+
+ if (fromValues == null && toValues == null) {
+ error("This should not happen, element $element is neither in $fromScene or $toScene")
+ }
+
+ // TODO(b/291053278): Handle overscroll correctly. We should probably coerce between [0f, 1f]
+ // here and consume overflows at drawing time, somehow reusing Compose OverflowEffect or some
+ // similar mechanism.
+ val transitionProgress = state.progress
+
+ // The element is shared: interpolate between the value in fromScene and the value in toScene.
+ // TODO(b/290184746): Support non linear shared paths as well as a way to make sure that shared
+ // elements follow the finger direction.
+ if (fromValues != null && toValues != null) {
+ return lerp(
+ sceneValue(fromValues),
+ sceneValue(toValues),
+ transitionProgress,
+ )
+ }
+
+ val transformation =
+ transformation(
+ layoutImpl.transitions.transitionSpec(fromScene, toScene).transformations(element.key)
+ )
+ // If there is no transformation explicitly associated to this element value, let's use
+ // the value given by the system (like the current position and size given by the layout
+ // pass).
+ ?: return currentValue()
+
+ // Get the transformed value, i.e. the target value at the beginning (for entering elements) or
+ // end (for leaving elements) of the transition.
+ val targetValue =
+ transformation.transform(
+ layoutImpl,
+ scene,
+ element,
+ fromValues ?: toValues!!,
+ state,
+ idleValue,
+ )
+
+ // TODO(b/290184746): Make sure that we don't overflow transformations associated to a range.
+ val rangeProgress = transformation.range?.progress(transitionProgress) ?: transitionProgress
+
+ // Interpolate between the value at rest and the value before entering/after leaving.
+ val isEntering = fromValues == null
+ return if (isEntering) {
+ lerp(targetValue, idleValue, rangeProgress)
+ } else {
+ lerp(idleValue, targetValue, rangeProgress)
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt
new file mode 100644
index 0000000..c3f44f8
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Key.kt
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.animation.scene
+
+/**
+ * A base class to create unique keys, associated to an [identity] that is used to check the
+ * equality of two key instances.
+ */
+sealed class Key(val name: String, val identity: Any) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (this.javaClass != other?.javaClass) return false
+ return identity == (other as? Key)?.identity
+ }
+
+ override fun hashCode(): Int {
+ return identity.hashCode()
+ }
+
+ override fun toString(): String {
+ return "Key(name=$name)"
+ }
+}
+
+/** Key for a scene. */
+class SceneKey(name: String, identity: Any = Object()) : Key(name, identity) {
+ override fun toString(): String {
+ return "SceneKey(name=$name)"
+ }
+}
+
+/** Key for an element. */
+class ElementKey(
+ name: String,
+ identity: Any = Object(),
+
+ /**
+ * Whether this element is a background and usually drawn below other elements. This should be
+ * set to true to make sure that shared backgrounds are drawn below elements of other scenes.
+ */
+ val isBackground: Boolean = false,
+) : Key(name, identity), ElementMatcher {
+ override fun matches(key: ElementKey): Boolean {
+ return key == this
+ }
+
+ override fun toString(): String {
+ return "ElementKey(name=$name)"
+ }
+
+ companion object {
+ /** Matches any element whose [key identity][ElementKey.identity] matches [predicate]. */
+ fun withIdentity(predicate: (Any) -> Boolean): ElementMatcher {
+ return object : ElementMatcher {
+ override fun matches(key: ElementKey): Boolean = predicate(key.identity)
+ }
+ }
+ }
+}
+
+/** Key for a shared value of an element. */
+class ValueKey(name: String, identity: Any = Object()) : Key(name, identity) {
+ override fun toString(): String {
+ return "ValueKey(name=$name)"
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/ObservableTransitionState.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/ObservableTransitionState.kt
new file mode 100644
index 0000000..a625250
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/ObservableTransitionState.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.animation.scene
+
+import androidx.compose.runtime.snapshotFlow
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+
+/**
+ * A scene transition state.
+ *
+ * This models the same thing as [TransitionState], with the following distinctions:
+ * 1. [TransitionState] values are backed by the Snapshot system (Compose State objects) and can be
+ * used by callers tracking State reads, for instance in Compose code during the composition,
+ * layout or Compose drawing phases.
+ * 2. [ObservableTransitionState] values are backed by Kotlin [Flow]s and can be collected by
+ * non-Compose code to observe value changes.
+ * 3. [ObservableTransitionState.Transition.fromScene] and
+ * [ObservableTransitionState.Transition.toScene] will never be equal, while
+ * [TransitionState.Transition.fromScene] and [TransitionState.Transition.toScene] can be equal.
+ */
+sealed class ObservableTransitionState {
+ /** No transition/animation is currently running. */
+ data class Idle(val scene: SceneKey) : ObservableTransitionState()
+
+ /** There is a transition animating between two scenes. */
+ data class Transition(
+ val fromScene: SceneKey,
+ val toScene: SceneKey,
+ val progress: Flow<Float>,
+ ) : ObservableTransitionState()
+}
+
+/**
+ * The current [ObservableTransitionState]. This models the same thing as
+ * [SceneTransitionLayoutState.transitionState], except that it is backed by Flows and can be used
+ * by non-Compose code to observe state changes.
+ */
+fun SceneTransitionLayoutState.observableTransitionState(): Flow<ObservableTransitionState> {
+ return snapshotFlow {
+ when (val state = transitionState) {
+ is TransitionState.Idle -> ObservableTransitionState.Idle(state.currentScene)
+ is TransitionState.Transition -> {
+ if (state.fromScene == state.toScene) {
+ ObservableTransitionState.Idle(state.currentScene)
+ } else {
+ ObservableTransitionState.Transition(
+ fromScene = state.fromScene,
+ toScene = state.toScene,
+ progress = snapshotFlow { state.progress },
+ )
+ }
+ }
+ }
+ }
+ .distinctUntilChanged()
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Scene.kt
new file mode 100644
index 0000000..b44c8ef
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/Scene.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.zIndex
+
+/** A scene in a [SceneTransitionLayout]. */
+internal class Scene(
+ val key: SceneKey,
+ layoutImpl: SceneTransitionLayoutImpl,
+ content: @Composable SceneScope.() -> Unit,
+ actions: Map<UserAction, SceneKey>,
+ zIndex: Float,
+) {
+ private val scope = SceneScopeImpl(layoutImpl, this)
+
+ var content by mutableStateOf(content)
+ var userActions by mutableStateOf(actions)
+ var zIndex by mutableFloatStateOf(zIndex)
+ var size by mutableStateOf(IntSize.Zero)
+
+ @Composable
+ fun Content(modifier: Modifier = Modifier) {
+ Box(modifier.zIndex(zIndex).onPlaced { size = it.size }) { scope.content() }
+ }
+
+ override fun toString(): String {
+ return "Scene(key=$key)"
+ }
+}
+
+private class SceneScopeImpl(
+ private val layoutImpl: SceneTransitionLayoutImpl,
+ private val scene: Scene,
+) : SceneScope {
+ @Composable
+ override fun Modifier.element(key: ElementKey): Modifier {
+ return element(layoutImpl, scene, key)
+ }
+
+ @Composable
+ override fun <T> animateSharedValueAsState(
+ value: T,
+ key: ValueKey,
+ element: ElementKey,
+ lerp: (T, T, Float) -> T,
+ canOverflow: Boolean
+ ): State<T> {
+ val element =
+ layoutImpl.elements[element]
+ ?: error(
+ "Element $element is not composed. Make sure to call animateSharedXAsState " +
+ "*after* Modifier.element(key)."
+ )
+
+ return animateSharedValueAsState(
+ layoutImpl,
+ scene,
+ element,
+ key,
+ value,
+ lerp,
+ canOverflow,
+ )
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
new file mode 100644
index 0000000..39173d9
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -0,0 +1,141 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.State
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+
+/**
+ * [SceneTransitionLayout] is a container that automatically animates its content whenever
+ * [currentScene] changes, using the transitions defined in [transitions].
+ *
+ * Note: You should use [androidx.compose.animation.AnimatedContent] instead of
+ * [SceneTransitionLayout] if it fits your need. Use [SceneTransitionLayout] over AnimatedContent if
+ * you need support for swipe gestures, shared elements or transitions defined declaratively outside
+ * UI code.
+ *
+ * @param currentScene the current scene
+ * @param onChangeScene a mutator that should set [currentScene] to the given scene when called.
+ * This is called when the user commits a transition to a new scene because of a [UserAction], for
+ * instance by triggering back navigation or by swiping to a new scene.
+ * @param transitions the definition of the transitions used to animate a change of scene.
+ * @param state the observable state of this layout.
+ * @param scenes the configuration of the different scenes of this layout.
+ */
+@Composable
+fun SceneTransitionLayout(
+ currentScene: SceneKey,
+ onChangeScene: (SceneKey) -> Unit,
+ transitions: SceneTransitions,
+ modifier: Modifier = Modifier,
+ state: SceneTransitionLayoutState = remember { SceneTransitionLayoutState(currentScene) },
+ scenes: SceneTransitionLayoutScope.() -> Unit,
+) {
+ val density = LocalDensity.current
+ val layoutImpl = remember {
+ SceneTransitionLayoutImpl(
+ onChangeScene,
+ scenes,
+ transitions,
+ state,
+ density,
+ )
+ }
+
+ layoutImpl.onChangeScene = onChangeScene
+ layoutImpl.transitions = transitions
+ layoutImpl.density = density
+ layoutImpl.setScenes(scenes)
+ layoutImpl.setCurrentScene(currentScene)
+
+ layoutImpl.Content(modifier)
+}
+
+interface SceneTransitionLayoutScope {
+ /**
+ * Add a scene to this layout, identified by [key].
+ *
+ * You can configure [userActions] so that swiping on this layout or navigating back will
+ * transition to a different scene.
+ *
+ * Important: scene order along the z-axis follows call order. Calling scene(A) followed by
+ * scene(B) will mean that scene B renders after/above scene A.
+ */
+ fun scene(
+ key: SceneKey,
+ userActions: Map<UserAction, SceneKey> = emptyMap(),
+ content: @Composable SceneScope.() -> Unit,
+ )
+}
+
+interface SceneScope {
+ /**
+ * Tag an element identified by [key].
+ *
+ * Tagging an element will allow you to reference that element when defining transitions, so
+ * that the element can be transformed and animated when the scene transitions in or out.
+ *
+ * Additionally, this [key] will be used to detect elements that are shared between scenes to
+ * automatically interpolate their size, offset and [shared values][animateSharedValueAsState].
+ *
+ * TODO(b/291566282): Migrate this to the new Modifier Node API and remove the @Composable
+ * constraint.
+ */
+ @Composable fun Modifier.element(key: ElementKey): Modifier
+
+ /**
+ * Animate some value of a shared element.
+ *
+ * @param value the value of this shared value in the current scene.
+ * @param key the key of this shared value.
+ * @param element the element associated with this value.
+ * @param lerp the *linear* interpolation function that should be used to interpolate between
+ * two different values. Note that it has to be linear because the [fraction] passed to this
+ * interpolator is already interpolated.
+ * @param canOverflow whether this value can overflow past the values it is interpolated
+ * between, for instance because the transition is animated using a bouncy spring.
+ * @see animateSharedIntAsState
+ * @see animateSharedFloatAsState
+ * @see animateSharedDpAsState
+ * @see animateSharedColorAsState
+ */
+ @Composable
+ fun <T> animateSharedValueAsState(
+ value: T,
+ key: ValueKey,
+ element: ElementKey,
+ lerp: (start: T, stop: T, fraction: Float) -> T,
+ canOverflow: Boolean,
+ ): State<T>
+}
+
+/** An action performed by the user. */
+sealed interface UserAction
+
+/** The user navigated back, either using a gesture or by triggering a KEYCODE_BACK event. */
+object Back : UserAction
+
+/** The user swiped on the container. */
+enum class Swipe : UserAction {
+ Up,
+ Down,
+ Left,
+ Right,
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
new file mode 100644
index 0000000..350b9c2
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -0,0 +1,214 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshots.SnapshotStateMap
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.layout.LookaheadScope
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.ui.util.fastForEach
+import kotlinx.coroutines.channels.Channel
+
+internal class SceneTransitionLayoutImpl(
+ onChangeScene: (SceneKey) -> Unit,
+ builder: SceneTransitionLayoutScope.() -> Unit,
+ transitions: SceneTransitions,
+ internal val state: SceneTransitionLayoutState,
+ density: Density,
+) {
+ internal val scenes = SnapshotStateMap<SceneKey, Scene>()
+ internal val elements = SnapshotStateMap<ElementKey, Element>()
+
+ /** The scenes that are "ready", i.e. they were composed and fully laid-out at least once. */
+ private val readyScenes = SnapshotStateMap<SceneKey, Boolean>()
+
+ internal var onChangeScene by mutableStateOf(onChangeScene)
+ internal var transitions by mutableStateOf(transitions)
+ internal var density: Density by mutableStateOf(density)
+
+ /**
+ * The size of this layout. Note that this could be [IntSize.Zero] if this layour does not have
+ * any scene configured or right before the first measure pass of the layout.
+ */
+ internal var size by mutableStateOf(IntSize.Zero)
+
+ init {
+ setScenes(builder)
+ }
+
+ internal fun scene(key: SceneKey): Scene {
+ return scenes[key] ?: error("Scene $key is not configured")
+ }
+
+ internal fun setScenes(builder: SceneTransitionLayoutScope.() -> Unit) {
+ // Keep a reference of the current scenes. After processing [builder], the scenes that were
+ // not configured will be removed.
+ val scenesToRemove = scenes.keys.toMutableSet()
+
+ // The incrementing zIndex of each scene.
+ var zIndex = 0f
+
+ object : SceneTransitionLayoutScope {
+ override fun scene(
+ key: SceneKey,
+ userActions: Map<UserAction, SceneKey>,
+ content: @Composable SceneScope.() -> Unit,
+ ) {
+ scenesToRemove.remove(key)
+
+ val scene = scenes[key]
+ if (scene != null) {
+ // Update an existing scene.
+ scene.content = content
+ scene.userActions = userActions
+ scene.zIndex = zIndex
+ } else {
+ // New scene.
+ scenes[key] =
+ Scene(
+ key,
+ this@SceneTransitionLayoutImpl,
+ content,
+ userActions,
+ zIndex,
+ )
+ }
+
+ zIndex++
+ }
+ }
+ .builder()
+
+ scenesToRemove.forEach { scenes.remove(it) }
+ }
+
+ @Composable
+ internal fun setCurrentScene(key: SceneKey) {
+ val channel = remember { Channel<SceneKey>(Channel.CONFLATED) }
+ SideEffect { channel.trySend(key) }
+ LaunchedEffect(channel) {
+ for (newKey in channel) {
+ // Inspired by AnimateAsState.kt: let's poll the last value to avoid being one frame
+ // late.
+ val newKey = channel.tryReceive().getOrNull() ?: newKey
+ animateToScene(this@SceneTransitionLayoutImpl, newKey)
+ }
+ }
+ }
+
+ @Composable
+ @OptIn(ExperimentalComposeUiApi::class)
+ internal fun Content(modifier: Modifier) {
+ Box(
+ modifier
+ // Handle horizontal and vertical swipes on this layout.
+ // Note: order here is important and will give a slight priority to the vertical
+ // swipes.
+ .swipeToScene(layoutImpl = this, Orientation.Horizontal)
+ .swipeToScene(layoutImpl = this, Orientation.Vertical)
+ .onSizeChanged { size = it }
+ ) {
+ LookaheadScope {
+ val scenesToCompose =
+ when (val state = state.transitionState) {
+ is TransitionState.Idle -> listOf(scene(state.currentScene))
+ is TransitionState.Transition -> {
+ if (state.toScene != state.fromScene) {
+ listOf(scene(state.toScene), scene(state.fromScene))
+ } else {
+ listOf(scene(state.fromScene))
+ }
+ }
+ }
+
+ // Handle back events.
+ // TODO(b/290184746): Make sure that this works with SystemUI once we use
+ // SceneTransitionLayout in Flexiglass.
+ scene(state.transitionState.currentScene).userActions[Back]?.let { backScene ->
+ BackHandler { onChangeScene(backScene) }
+ }
+
+ Box(
+ Modifier.drawWithContent {
+ drawContent()
+
+ // At this point, all scenes in scenesToCompose are fully laid out so they
+ // are marked as ready. This is necessary because the animation code needs
+ // to know the position and size of the elements in each scenes they are in,
+ // so [readyScenes] will be used to decide whether the transition is ready
+ // (see isTransitionReady() below).
+ //
+ // We can't do that in a DisposableEffect or SideEffect because those are
+ // run between composition and layout. LaunchedEffect could work and might
+ // be better, but it looks like launched effects run a frame later than this
+ // code so doing this here seems better for performance.
+ scenesToCompose.fastForEach { readyScenes[it.key] = true }
+ }
+ ) {
+ scenesToCompose.fastForEach { scene ->
+ val key = scene.key
+ key(key) {
+ DisposableEffect(key) { onDispose { readyScenes.remove(key) } }
+
+ scene.Content(
+ Modifier.drawWithContent {
+ when (val state = state.transitionState) {
+ is TransitionState.Idle -> drawContent()
+ is TransitionState.Transition -> {
+ // Don't draw scenes that are not ready yet.
+ if (
+ readyScenes.containsKey(key) ||
+ state.fromScene == state.toScene
+ ) {
+ drawContent()
+ }
+ }
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Return whether [transition] is ready, i.e. the elements of both scenes of the transition were
+ * laid out at least once.
+ */
+ internal fun isTransitionReady(transition: TransitionState.Transition): Boolean {
+ return readyScenes.containsKey(transition.fromScene) &&
+ readyScenes.containsKey(transition.toScene)
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
new file mode 100644
index 0000000..47e3d5a
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+
+/** The state of a [SceneTransitionLayout]. */
+class SceneTransitionLayoutState(initialScene: SceneKey) {
+ /**
+ * The current [TransitionState]. All values read here are backed by the Snapshot system.
+ *
+ * To observe those values outside of Compose/the Snapshot system, use
+ * [SceneTransitionLayoutState.observableTransitionState] instead.
+ */
+ var transitionState: TransitionState by mutableStateOf(TransitionState.Idle(initialScene))
+ internal set
+}
+
+sealed interface TransitionState {
+ /**
+ * The current effective scene. If a new transition was triggered, it would start from this
+ * scene.
+ *
+ * For instance, when swiping from scene A to scene B, the [currentScene] is A when the swipe
+ * gesture starts, but then if the user flings their finger and commits the transition to scene
+ * B, then [currentScene] becomes scene B even if the transition is not finished yet and is
+ * still animating to settle to scene B.
+ */
+ val currentScene: SceneKey
+
+ /** No transition/animation is currently running. */
+ data class Idle(override val currentScene: SceneKey) : TransitionState
+
+ /**
+ * There is a transition animating between two scenes.
+ *
+ * Important note: [fromScene] and [toScene] might be the same, in which case this [Transition]
+ * should be treated the same as [Idle]. This is designed on purpose so that a [Transition] can
+ * be started without knowing in advance where it is transitioning to, making the logic of
+ * [swipeToScene] easier to reason about.
+ */
+ interface Transition : TransitionState {
+ /** The scene this transition is starting from. */
+ val fromScene: SceneKey
+
+ /** The scene this transition is going to. */
+ val toScene: SceneKey
+
+ /**
+ * The progress of the transition. This is usually in the `[0; 1]` range, but it can also be
+ * less than `0` or greater than `1` when using transitions with a spring AnimationSpec or
+ * when flinging quickly during a swipe gesture.
+ */
+ val progress: Float
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt
new file mode 100644
index 0000000..9752f53
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -0,0 +1,176 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.snap
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.transformation.AnchoredSize
+import com.android.compose.animation.scene.transformation.AnchoredTranslate
+import com.android.compose.animation.scene.transformation.EdgeTranslate
+import com.android.compose.animation.scene.transformation.Fade
+import com.android.compose.animation.scene.transformation.ModifierTransformation
+import com.android.compose.animation.scene.transformation.PropertyTransformation
+import com.android.compose.animation.scene.transformation.RangedPropertyTransformation
+import com.android.compose.animation.scene.transformation.ScaleSize
+import com.android.compose.animation.scene.transformation.Transformation
+import com.android.compose.animation.scene.transformation.Translate
+import com.android.compose.ui.util.fastForEach
+import com.android.compose.ui.util.fastMap
+
+/** The transitions configuration of a [SceneTransitionLayout]. */
+class SceneTransitions(
+ val transitionSpecs: List<TransitionSpec>,
+) {
+ private val cache = mutableMapOf<SceneKey, MutableMap<SceneKey, TransitionSpec>>()
+
+ internal fun transitionSpec(from: SceneKey, to: SceneKey): TransitionSpec {
+ return cache.getOrPut(from) { mutableMapOf() }.getOrPut(to) { findSpec(from, to) }
+ }
+
+ private fun findSpec(from: SceneKey, to: SceneKey): TransitionSpec {
+ val spec = transition(from, to) { it.from == from && it.to == to }
+ if (spec != null) {
+ return spec
+ }
+
+ val reversed = transition(from, to) { it.from == to && it.to == from }
+ if (reversed != null) {
+ return reversed.reverse()
+ }
+
+ val relaxedSpec =
+ transition(from, to) {
+ (it.from == from && it.to == null) || (it.to == to && it.from == null)
+ }
+ if (relaxedSpec != null) {
+ return relaxedSpec
+ }
+
+ return transition(from, to) {
+ (it.from == to && it.to == null) || (it.to == from && it.from == null)
+ }
+ ?.reverse()
+ ?: defaultTransition(from, to)
+ }
+
+ private fun transition(
+ from: SceneKey,
+ to: SceneKey,
+ filter: (TransitionSpec) -> Boolean,
+ ): TransitionSpec? {
+ var match: TransitionSpec? = null
+ transitionSpecs.fastForEach { spec ->
+ if (filter(spec)) {
+ if (match != null) {
+ error("Found multiple transition specs for transition $from => $to")
+ }
+ match = spec
+ }
+ }
+ return match
+ }
+
+ private fun defaultTransition(from: SceneKey, to: SceneKey) =
+ TransitionSpec(from, to, emptyList(), snap())
+}
+
+/** The definition of a transition between [from] and [to]. */
+data class TransitionSpec(
+ val from: SceneKey?,
+ val to: SceneKey?,
+ val transformations: List<Transformation>,
+ val spec: AnimationSpec<Float>,
+) {
+ private val cache = mutableMapOf<ElementKey, ElementTransformations>()
+
+ internal fun reverse(): TransitionSpec {
+ return copy(
+ from = to,
+ to = from,
+ transformations = transformations.fastMap { it.reverse() },
+ )
+ }
+
+ internal fun transformations(element: ElementKey): ElementTransformations {
+ return cache.getOrPut(element) { computeTransformations(element) }
+ }
+
+ /** Filter [transformations] to compute the [ElementTransformations] of [element]. */
+ private fun computeTransformations(element: ElementKey): ElementTransformations {
+ val modifier = mutableListOf<ModifierTransformation>()
+ var offset: PropertyTransformation<Offset>? = null
+ var size: PropertyTransformation<IntSize>? = null
+ var alpha: PropertyTransformation<Float>? = null
+
+ fun <T> onPropertyTransformation(
+ root: PropertyTransformation<T>,
+ current: PropertyTransformation<T> = root,
+ ) {
+ when (current) {
+ is Translate,
+ is EdgeTranslate,
+ is AnchoredTranslate -> {
+ throwIfNotNull(offset, element, property = "offset")
+ offset = root as PropertyTransformation<Offset>
+ }
+ is ScaleSize,
+ is AnchoredSize -> {
+ throwIfNotNull(size, element, property = "size")
+ size = root as PropertyTransformation<IntSize>
+ }
+ is Fade -> {
+ throwIfNotNull(alpha, element, property = "alpha")
+ alpha = root as PropertyTransformation<Float>
+ }
+ is RangedPropertyTransformation -> onPropertyTransformation(root, current.delegate)
+ }
+ }
+
+ transformations.fastForEach { transformation ->
+ if (!transformation.matcher.matches(element)) {
+ return@fastForEach
+ }
+
+ when (transformation) {
+ is ModifierTransformation -> modifier.add(transformation)
+ is PropertyTransformation<*> -> onPropertyTransformation(transformation)
+ }
+ }
+
+ return ElementTransformations(modifier, offset, size, alpha)
+ }
+
+ private fun throwIfNotNull(
+ previous: PropertyTransformation<*>?,
+ element: ElementKey,
+ property: String,
+ ) {
+ if (previous != null) {
+ error("$element has multiple transformations for its $property property")
+ }
+ }
+}
+
+/** The transformations of an element during a transition. */
+internal class ElementTransformations(
+ val modifier: List<ModifierTransformation>,
+ val offset: PropertyTransformation<Offset>?,
+ val size: PropertyTransformation<IntSize>?,
+ val alpha: PropertyTransformation<Float>?,
+)
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SwipeToScene.kt
new file mode 100644
index 0000000..d9a45cd
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -0,0 +1,419 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.gestures.draggable
+import androidx.compose.foundation.gestures.rememberDraggableState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.unit.dp
+import kotlin.math.absoluteValue
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+/**
+ * Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state.
+ */
+@Composable
+internal fun Modifier.swipeToScene(
+ layoutImpl: SceneTransitionLayoutImpl,
+ orientation: Orientation,
+): Modifier {
+ val state = layoutImpl.state.transitionState
+ val currentScene = layoutImpl.scene(state.currentScene)
+ val transition = remember {
+ // Note that the currentScene here does not matter, it's only used for initializing the
+ // transition and will be replaced when a drag event starts.
+ SwipeTransition(initialScene = currentScene)
+ }
+
+ val enabled = state == transition || currentScene.shouldEnableSwipes(orientation)
+
+ // Immediately start the drag if this our [transition] is currently animating to a scene (i.e.
+ // the user released their input pointer after swiping in this orientation) and the user can't
+ // swipe in the other direction.
+ val startDragImmediately =
+ state == transition &&
+ transition.isAnimatingOffset &&
+ !currentScene.shouldEnableSwipes(orientation.opposite())
+
+ // The velocity threshold at which the intent of the user is to swipe up or down. It is the same
+ // as SwipeableV2Defaults.VelocityThreshold.
+ val velocityThreshold = with(LocalDensity.current) { 125.dp.toPx() }
+
+ // The positional threshold at which the intent of the user is to swipe to the next scene. It is
+ // the same as SwipeableV2Defaults.PositionalThreshold.
+ val positionalThreshold = with(LocalDensity.current) { 56.dp.toPx() }
+
+ return draggable(
+ orientation = orientation,
+ enabled = enabled,
+ startDragImmediately = startDragImmediately,
+ onDragStarted = { onDragStarted(layoutImpl, transition, orientation) },
+ state =
+ rememberDraggableState { delta -> onDrag(layoutImpl, transition, orientation, delta) },
+ onDragStopped = { velocity ->
+ onDragStopped(
+ layoutImpl,
+ transition,
+ velocity,
+ velocityThreshold,
+ positionalThreshold,
+ )
+ },
+ )
+}
+
+private class SwipeTransition(initialScene: Scene) : TransitionState.Transition {
+ var _currentScene by mutableStateOf(initialScene)
+ override val currentScene: SceneKey
+ get() = _currentScene.key
+
+ var _fromScene by mutableStateOf(initialScene)
+ override val fromScene: SceneKey
+ get() = _fromScene.key
+
+ var _toScene by mutableStateOf(initialScene)
+ override val toScene: SceneKey
+ get() = _toScene.key
+
+ override val progress: Float
+ get() {
+ val offset = if (isAnimatingOffset) offsetAnimatable.value else dragOffset
+ if (distance == 0f) {
+ // This can happen only if fromScene == toScene.
+ error(
+ "Transition.progress should be called only when Transition.fromScene != " +
+ "Transition.toScene"
+ )
+ }
+ return offset / distance
+ }
+
+ /** The current offset caused by the drag gesture. */
+ var dragOffset by mutableFloatStateOf(0f)
+
+ /**
+ * Whether the offset is animated (the user lifted their finger) or if it is driven by gesture.
+ */
+ var isAnimatingOffset by mutableStateOf(false)
+
+ /** The animatable used to animate the offset once the user lifted its finger. */
+ val offsetAnimatable = Animatable(0f, visibilityThreshold = OffsetVisibilityThreshold)
+
+ /**
+ * The job currently animating [offsetAnimatable], if it is animating. Note that setting this to
+ * a new job will automatically cancel the previous one.
+ */
+ var offsetAnimationJob: Job? = null
+ set(value) {
+ field?.cancel()
+ field = value
+ }
+
+ /** The absolute distance between [fromScene] and [toScene]. */
+ var absoluteDistance = 0f
+
+ /**
+ * The signed distance between [fromScene] and [toScene]. It is negative if [fromScene] is above
+ * or to the left of [toScene].
+ */
+ var _distance by mutableFloatStateOf(0f)
+ val distance: Float
+ get() = _distance
+}
+
+/** The destination scene when swiping up or left from [this@upOrLeft]. */
+private fun Scene.upOrLeft(orientation: Orientation): SceneKey? {
+ return when (orientation) {
+ Orientation.Vertical -> userActions[Swipe.Up]
+ Orientation.Horizontal -> userActions[Swipe.Left]
+ }
+}
+
+/** The destination scene when swiping down or right from [this@downOrRight]. */
+private fun Scene.downOrRight(orientation: Orientation): SceneKey? {
+ return when (orientation) {
+ Orientation.Vertical -> userActions[Swipe.Down]
+ Orientation.Horizontal -> userActions[Swipe.Right]
+ }
+}
+
+/** Whether swipe should be enabled in the given [orientation]. */
+private fun Scene.shouldEnableSwipes(orientation: Orientation): Boolean {
+ return upOrLeft(orientation) != null || downOrRight(orientation) != null
+}
+
+private fun Orientation.opposite(): Orientation {
+ return when (this) {
+ Orientation.Vertical -> Orientation.Horizontal
+ Orientation.Horizontal -> Orientation.Vertical
+ }
+}
+
+private fun onDragStarted(
+ layoutImpl: SceneTransitionLayoutImpl,
+ transition: SwipeTransition,
+ orientation: Orientation,
+) {
+ if (layoutImpl.state.transitionState == transition) {
+ // This [transition] was already driving the animation: simply take over it.
+ if (transition.isAnimatingOffset) {
+ // Stop animating and start from where the current offset. Setting the animation job to
+ // `null` will effectively cancel the animation.
+ transition.isAnimatingOffset = false
+ transition.offsetAnimationJob = null
+ transition.dragOffset = transition.offsetAnimatable.value
+ }
+
+ return
+ }
+
+ // TODO(b/290184746): Better handle interruptions here if state != idle.
+
+ val fromScene = layoutImpl.scene(layoutImpl.state.transitionState.currentScene)
+
+ transition._currentScene = fromScene
+ transition._fromScene = fromScene
+
+ // We don't know where we are transitioning to yet given that the drag just started, so set it
+ // to fromScene, which will effectively be treated the same as Idle(fromScene).
+ transition._toScene = fromScene
+
+ transition.dragOffset = 0f
+ transition.isAnimatingOffset = false
+ transition.offsetAnimationJob = null
+
+ // Use the layout size in the swipe orientation for swipe distance.
+ // TODO(b/290184746): Also handle custom distances for transitions. With smaller distances, we
+ // will also have to make sure that we correctly handle overscroll.
+ transition.absoluteDistance =
+ when (orientation) {
+ Orientation.Horizontal -> layoutImpl.size.width
+ Orientation.Vertical -> layoutImpl.size.height
+ }.toFloat()
+
+ if (transition.absoluteDistance > 0f) {
+ layoutImpl.state.transitionState = transition
+ }
+}
+
+private fun onDrag(
+ layoutImpl: SceneTransitionLayoutImpl,
+ transition: SwipeTransition,
+ orientation: Orientation,
+ delta: Float,
+) {
+ transition.dragOffset += delta
+
+ // First check transition.fromScene should be changed for the case where the user quickly swiped
+ // twice in a row to accelerate the transition and go from A => B then B => C really fast.
+ maybeHandleAcceleratedSwipe(transition, orientation)
+
+ val fromScene = transition._fromScene
+ val upOrLeft = fromScene.upOrLeft(orientation)
+ val downOrRight = fromScene.downOrRight(orientation)
+ val offset = transition.dragOffset
+
+ // Compute the target scene depending on the current offset.
+ val targetSceneKey: SceneKey
+ val signedDistance: Float
+ when {
+ offset < 0f && upOrLeft != null -> {
+ targetSceneKey = upOrLeft
+ signedDistance = -transition.absoluteDistance
+ }
+ offset > 0f && downOrRight != null -> {
+ targetSceneKey = downOrRight
+ signedDistance = transition.absoluteDistance
+ }
+ else -> {
+ targetSceneKey = fromScene.key
+ signedDistance = 0f
+ }
+ }
+
+ if (transition._toScene.key != targetSceneKey) {
+ transition._toScene = layoutImpl.scenes.getValue(targetSceneKey)
+ }
+
+ if (transition._distance != signedDistance) {
+ transition._distance = signedDistance
+ }
+}
+
+/**
+ * Change fromScene in the case where the user quickly swiped multiple times in the same direction
+ * to accelerate the transition from A => B then B => C.
+ */
+private fun maybeHandleAcceleratedSwipe(
+ transition: SwipeTransition,
+ orientation: Orientation,
+) {
+ val toScene = transition._toScene
+ val fromScene = transition._fromScene
+
+ // If the swipe was not committed, don't do anything.
+ if (fromScene == toScene || transition._currentScene != toScene) {
+ return
+ }
+
+ // If the offset is past the distance then let's change fromScene so that the user can swipe to
+ // the next screen or go back to the previous one.
+ val offset = transition.dragOffset
+ val absoluteDistance = transition.absoluteDistance
+ if (offset <= -absoluteDistance && fromScene.upOrLeft(orientation) == toScene.key) {
+ transition.dragOffset += absoluteDistance
+ transition._fromScene = toScene
+ } else if (offset >= absoluteDistance && fromScene.downOrRight(orientation) == toScene.key) {
+ transition.dragOffset -= absoluteDistance
+ transition._fromScene = toScene
+ }
+
+ // Important note: toScene and distance will be updated right after this function is called,
+ // using fromScene and dragOffset.
+}
+
+private fun CoroutineScope.onDragStopped(
+ layoutImpl: SceneTransitionLayoutImpl,
+ transition: SwipeTransition,
+ velocity: Float,
+ velocityThreshold: Float,
+ positionalThreshold: Float,
+) {
+ // The state was changed since the drag started; don't do anything.
+ if (layoutImpl.state.transitionState != transition) {
+ return
+ }
+
+ // We were not animating.
+ if (transition._fromScene == transition._toScene) {
+ layoutImpl.state.transitionState = TransitionState.Idle(transition._fromScene.key)
+ return
+ }
+
+ // Compute the destination scene (and therefore offset) to settle in.
+ val targetScene: Scene
+ val targetOffset: Float
+ val offset = transition.dragOffset
+ val distance = transition.distance
+ if (
+ shouldCommitSwipe(
+ offset,
+ distance,
+ velocity,
+ velocityThreshold,
+ positionalThreshold,
+ wasCommitted = transition._currentScene == transition._toScene,
+ )
+ ) {
+ targetOffset = distance
+ targetScene = transition._toScene
+ } else {
+ targetOffset = 0f
+ targetScene = transition._fromScene
+ }
+
+ // If the effective current scene changed, it should be reflected right now in the current scene
+ // state, even before the settle animation is ongoing. That way all the swipeables and back
+ // handlers will be refreshed and the user can for instance quickly swipe vertically from A => B
+ // then horizontally from B => C, or swipe from A => B then immediately go back B => A.
+ if (targetScene != transition._currentScene) {
+ transition._currentScene = targetScene
+ layoutImpl.onChangeScene(targetScene.key)
+ }
+
+ // Animate the offset.
+ transition.offsetAnimationJob = launch {
+ transition.offsetAnimatable.snapTo(offset)
+ transition.isAnimatingOffset = true
+
+ transition.offsetAnimatable.animateTo(
+ targetOffset,
+ // TODO(b/290184746): Make this spring spec configurable.
+ spring(
+ stiffness = Spring.StiffnessMediumLow,
+ visibilityThreshold = OffsetVisibilityThreshold
+ ),
+ initialVelocity = velocity,
+ )
+
+ // Now that the animation is done, the state should be idle. Note that if the state was
+ // changed since this animation started, some external code changed it and we shouldn't do
+ // anything here. Note also that this job will be cancelled in the case where the user
+ // intercepts this swipe.
+ if (layoutImpl.state.transitionState == transition) {
+ layoutImpl.state.transitionState = TransitionState.Idle(targetScene.key)
+ }
+
+ transition.offsetAnimationJob = null
+ }
+}
+
+/**
+ * Whether the swipe to the target scene should be committed or not. This is inspired by
+ * SwipeableV2.computeTarget().
+ */
+private fun shouldCommitSwipe(
+ offset: Float,
+ distance: Float,
+ velocity: Float,
+ velocityThreshold: Float,
+ positionalThreshold: Float,
+ wasCommitted: Boolean,
+): Boolean {
+ fun isCloserToTarget(): Boolean {
+ return (offset - distance).absoluteValue < offset.absoluteValue
+ }
+
+ // Swiping up or left.
+ if (distance < 0f) {
+ return if (offset > 0f || velocity >= velocityThreshold) {
+ false
+ } else {
+ velocity <= -velocityThreshold ||
+ (offset <= -positionalThreshold && !wasCommitted) ||
+ isCloserToTarget()
+ }
+ }
+
+ // Swiping down or right.
+ return if (offset < 0f || velocity <= -velocityThreshold) {
+ false
+ } else {
+ velocity >= velocityThreshold ||
+ (offset >= positionalThreshold && !wasCommitted) ||
+ isCloserToTarget()
+ }
+}
+
+/**
+ * The number of pixels below which there won't be a visible difference in the transition and from
+ * which the animation can stop.
+ */
+private const val OffsetVisibilityThreshold = 0.5f
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt
new file mode 100644
index 0000000..fb12b90
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDsl.kt
@@ -0,0 +1,194 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+/** Define the [transitions][SceneTransitions] to be used with a [SceneTransitionLayout]. */
+fun transitions(builder: SceneTransitionsBuilder.() -> Unit): SceneTransitions {
+ return transitionsImpl(builder)
+}
+
+@DslMarker annotation class TransitionDsl
+
+@TransitionDsl
+interface SceneTransitionsBuilder {
+ /**
+ * Define the default animation to be played when transitioning [to] the specified scene, from
+ * any scene. For the animation specification to apply only when transitioning between two
+ * specific scenes, use [from] instead.
+ *
+ * @see from
+ */
+ fun to(
+ to: SceneKey,
+ builder: TransitionBuilder.() -> Unit = {},
+ ): TransitionSpec
+
+ /**
+ * Define the animation to be played when transitioning [from] the specified scene. For the
+ * animation specification to apply only when transitioning between two specific scenes, pass
+ * the destination scene via the [to] argument.
+ *
+ * When looking up which transition should be used when animating from scene A to scene B, we
+ * pick the single transition matching one of these predicates (in order of importance):
+ * 1. from == A && to == B
+ * 2. to == A && from == B, which is then treated in reverse.
+ * 3. (from == A && to == null) || (from == null && to == B)
+ * 4. (from == B && to == null) || (from == null && to == A), which is then treated in reverse.
+ */
+ fun from(
+ from: SceneKey,
+ to: SceneKey? = null,
+ builder: TransitionBuilder.() -> Unit = {},
+ ): TransitionSpec
+}
+
+@TransitionDsl
+interface TransitionBuilder : PropertyTransformationBuilder {
+ /**
+ * The [AnimationSpec] used to animate the progress of this transition from `0` to `1` when
+ * performing programmatic (not input pointer tracking) animations.
+ */
+ var spec: AnimationSpec<Float>
+
+ /**
+ * Define a progress-based range for the transformations inside [builder].
+ *
+ * For instance, the following will fade `Foo` during the first half of the transition then it
+ * will translate it by 100.dp during the second half.
+ *
+ * ```
+ * fractionRange(end = 0.5f) { fade(Foo) }
+ * fractionRange(start = 0.5f) { translate(Foo, x = 100.dp) }
+ * ```
+ *
+ * @param start the start of the range, in the [0; 1] range.
+ * @param end the end of the range, in the [0; 1] range.
+ */
+ fun fractionRange(
+ start: Float? = null,
+ end: Float? = null,
+ builder: PropertyTransformationBuilder.() -> Unit,
+ )
+
+ /**
+ * Define a timestamp-based range for the transformations inside [builder].
+ *
+ * For instance, the following will fade `Foo` during the first half of the transition then it
+ * will translate it by 100.dp during the second half.
+ *
+ * ```
+ * spec = tween(500)
+ * timestampRange(end = 250) { fade(Foo) }
+ * timestampRange(start = 250) { translate(Foo, x = 100.dp) }
+ * ```
+ *
+ * Important: [spec] must be a [androidx.compose.animation.core.DurationBasedAnimationSpec] if
+ * you call [timestampRange], otherwise this will throw. The spec duration will be used to
+ * transform this range into a [fractionRange].
+ *
+ * @param startMillis the start of the range, in the [0; spec.duration] range.
+ * @param endMillis the end of the range, in the [0; spec.duration] range.
+ */
+ fun timestampRange(
+ startMillis: Int? = null,
+ endMillis: Int? = null,
+ builder: PropertyTransformationBuilder.() -> Unit,
+ )
+
+ /**
+ * Punch a hole in the element(s) matching [matcher] that has the same bounds as [bounds] and
+ * using the given [shape].
+ *
+ * Punching a hole in an element will "remove" any pixel drawn by that element in the hole area.
+ * This can be used to make content drawn below an opaque element visible. For example, if we
+ * have [this lockscreen scene](http://shortn/_VYySFnJDhN) drawn below
+ * [this shade scene](http://shortn/_fpxGUk0Rg7) and punch a hole in the latter using the big
+ * clock time bounds and a RoundedCornerShape(10dp), [this](http://shortn/_qt80IvORFj) would be
+ * the result.
+ */
+ fun punchHole(matcher: ElementMatcher, bounds: ElementKey, shape: Shape = RectangleShape)
+}
+
+@TransitionDsl
+interface PropertyTransformationBuilder {
+ /**
+ * Fade the element(s) matching [matcher]. This will automatically fade in or fade out if the
+ * element is entering or leaving the scene, respectively.
+ */
+ fun fade(matcher: ElementMatcher)
+
+ /** Translate the element(s) matching [matcher] by ([x], [y]) dp. */
+ fun translate(matcher: ElementMatcher, x: Dp = 0.dp, y: Dp = 0.dp)
+
+ /**
+ * Translate the element(s) matching [matcher] from/to the [edge] of the [SceneTransitionLayout]
+ * animating it.
+ *
+ * If [startsOutsideLayoutBounds] is `true`, then the element will start completely outside of
+ * the layout bounds (i.e. none of it will be visible at progress = 0f if the layout clips its
+ * content). If it is `false`, then the element will start aligned with the edge of the layout
+ * (i.e. it will be completely visible at progress = 0f).
+ */
+ fun translate(matcher: ElementMatcher, edge: Edge, startsOutsideLayoutBounds: Boolean = true)
+
+ /**
+ * Translate the element(s) matching [matcher] by the same amount that [anchor] is translated
+ * during this transition.
+ *
+ * Note: This currently only works if [anchor] is a shared element of this transition.
+ *
+ * TODO(b/290184746): Also support anchors that are not shared but translated because of other
+ * transformations, like an edge translation.
+ */
+ fun anchoredTranslate(matcher: ElementMatcher, anchor: ElementKey)
+
+ /**
+ * Scale the [width] and [height] of the element(s) matching [matcher]. Note that this scaling
+ * is done during layout, so it will potentially impact the size and position of other elements.
+ *
+ * TODO(b/290184746): Also provide a scaleDrawing() to scale an element at drawing time.
+ */
+ fun scaleSize(matcher: ElementMatcher, width: Float = 1f, height: Float = 1f)
+
+ /**
+ * Scale the element(s) matching [matcher] so that it grows/shrinks to the same size as [anchor]
+ * .
+ *
+ * Note: This currently only works if [anchor] is a shared element of this transition.
+ */
+ fun anchoredSize(matcher: ElementMatcher, anchor: ElementKey)
+}
+
+/** An interface to match one or more elements. */
+interface ElementMatcher {
+ /** Whether the element with key [key] matches this matcher. */
+ fun matches(key: ElementKey): Boolean
+}
+
+/** The edge of a [SceneTransitionLayout]. */
+enum class Edge {
+ Left,
+ Right,
+ Top,
+ Bottom,
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt
new file mode 100644
index 0000000..afd49b4
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/TransitionDslImpl.kt
@@ -0,0 +1,159 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.DurationBasedAnimationSpec
+import androidx.compose.animation.core.Spring
+import androidx.compose.animation.core.VectorConverter
+import androidx.compose.animation.core.spring
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import com.android.compose.animation.scene.transformation.AnchoredSize
+import com.android.compose.animation.scene.transformation.AnchoredTranslate
+import com.android.compose.animation.scene.transformation.EdgeTranslate
+import com.android.compose.animation.scene.transformation.Fade
+import com.android.compose.animation.scene.transformation.PropertyTransformation
+import com.android.compose.animation.scene.transformation.PunchHole
+import com.android.compose.animation.scene.transformation.RangedPropertyTransformation
+import com.android.compose.animation.scene.transformation.ScaleSize
+import com.android.compose.animation.scene.transformation.Transformation
+import com.android.compose.animation.scene.transformation.TransformationRange
+import com.android.compose.animation.scene.transformation.Translate
+
+internal fun transitionsImpl(
+ builder: SceneTransitionsBuilder.() -> Unit,
+): SceneTransitions {
+ val impl = SceneTransitionsBuilderImpl().apply(builder)
+ return SceneTransitions(impl.transitionSpecs)
+}
+
+private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder {
+ val transitionSpecs = mutableListOf<TransitionSpec>()
+
+ override fun to(to: SceneKey, builder: TransitionBuilder.() -> Unit): TransitionSpec {
+ return transition(from = null, to = to, builder)
+ }
+
+ override fun from(
+ from: SceneKey,
+ to: SceneKey?,
+ builder: TransitionBuilder.() -> Unit
+ ): TransitionSpec {
+ return transition(from = from, to = to, builder)
+ }
+
+ private fun transition(
+ from: SceneKey?,
+ to: SceneKey?,
+ builder: TransitionBuilder.() -> Unit,
+ ): TransitionSpec {
+ val impl = TransitionBuilderImpl().apply(builder)
+ val spec =
+ TransitionSpec(
+ from,
+ to,
+ impl.transformations,
+ impl.spec,
+ )
+ transitionSpecs.add(spec)
+ return spec
+ }
+}
+
+private class TransitionBuilderImpl : TransitionBuilder {
+ val transformations = mutableListOf<Transformation>()
+ override var spec: AnimationSpec<Float> = spring(stiffness = Spring.StiffnessLow)
+
+ private var range: TransformationRange? = null
+ private val durationMillis: Int by lazy {
+ val spec = spec
+ if (spec !is DurationBasedAnimationSpec) {
+ error("timestampRange {} can only be used with a DurationBasedAnimationSpec")
+ }
+
+ spec.vectorize(Float.VectorConverter).durationMillis
+ }
+
+ override fun punchHole(matcher: ElementMatcher, bounds: ElementKey, shape: Shape) {
+ transformations.add(PunchHole(matcher, bounds, shape))
+ }
+
+ override fun fractionRange(
+ start: Float?,
+ end: Float?,
+ builder: PropertyTransformationBuilder.() -> Unit
+ ) {
+ range = TransformationRange(start, end)
+ builder()
+ range = null
+ }
+
+ override fun timestampRange(
+ startMillis: Int?,
+ endMillis: Int?,
+ builder: PropertyTransformationBuilder.() -> Unit
+ ) {
+ if (startMillis != null && (startMillis < 0 || startMillis > durationMillis)) {
+ error("invalid start value: startMillis=$startMillis durationMillis=$durationMillis")
+ }
+
+ if (endMillis != null && (endMillis < 0 || endMillis > durationMillis)) {
+ error("invalid end value: endMillis=$startMillis durationMillis=$durationMillis")
+ }
+
+ val start = startMillis?.let { it.toFloat() / durationMillis }
+ val end = endMillis?.let { it.toFloat() / durationMillis }
+ fractionRange(start, end, builder)
+ }
+
+ private fun transformation(transformation: PropertyTransformation<*>) {
+ if (range != null) {
+ transformations.add(RangedPropertyTransformation(transformation, range!!))
+ } else {
+ transformations.add(transformation)
+ }
+ }
+
+ override fun fade(matcher: ElementMatcher) {
+ transformation(Fade(matcher))
+ }
+
+ override fun translate(matcher: ElementMatcher, x: Dp, y: Dp) {
+ transformation(Translate(matcher, x, y))
+ }
+
+ override fun translate(
+ matcher: ElementMatcher,
+ edge: Edge,
+ startsOutsideLayoutBounds: Boolean
+ ) {
+ transformation(EdgeTranslate(matcher, edge, startsOutsideLayoutBounds))
+ }
+
+ override fun anchoredTranslate(matcher: ElementMatcher, anchor: ElementKey) {
+ transformation(AnchoredTranslate(matcher, anchor))
+ }
+
+ override fun scaleSize(matcher: ElementMatcher, width: Float, height: Float) {
+ transformation(ScaleSize(matcher, width, height))
+ }
+
+ override fun anchoredSize(matcher: ElementMatcher, anchor: ElementKey) {
+ transformation(AnchoredSize(matcher, anchor))
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
new file mode 100644
index 0000000..d4ed697
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+
+/** Anchor the size of an element to the size of another element. */
+internal class AnchoredSize(
+ override val matcher: ElementMatcher,
+ private val anchor: ElementKey,
+) : PropertyTransformation<IntSize> {
+ override fun transform(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ transition: TransitionState.Transition,
+ value: IntSize,
+ ): IntSize {
+ fun anchorSizeIn(scene: SceneKey): IntSize {
+ val size = layoutImpl.elements[anchor]?.sceneValues?.get(scene)?.size
+ return if (size != null && size != Element.SizeUnspecified) {
+ size
+ } else {
+ value
+ }
+ }
+
+ // This simple implementation assumes that the size of [element] is the same as the size of
+ // the [anchor] in [scene], so simply transform to the size of the anchor in the other
+ // scene.
+ return if (scene.key == transition.fromScene) {
+ anchorSizeIn(transition.toScene)
+ } else {
+ anchorSizeIn(transition.fromScene)
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
new file mode 100644
index 0000000..8a5bd74
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.isSpecified
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+
+/** Anchor the translation of an element to another element. */
+internal class AnchoredTranslate(
+ override val matcher: ElementMatcher,
+ private val anchor: ElementKey,
+) : PropertyTransformation<Offset> {
+ override fun transform(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ transition: TransitionState.Transition,
+ value: Offset,
+ ): Offset {
+ val anchor = layoutImpl.elements[anchor] ?: return value
+ fun anchorOffsetIn(scene: SceneKey): Offset? {
+ return anchor.sceneValues[scene]?.offset?.takeIf { it.isSpecified }
+ }
+
+ // [element] will move the same amount as [anchor] does.
+ // TODO(b/290184746): Also support anchors that are not shared but translated because of
+ // other transformations, like an edge translation.
+ val anchorFromOffset = anchorOffsetIn(transition.fromScene) ?: return value
+ val anchorToOffset = anchorOffsetIn(transition.toScene) ?: return value
+ val offset = anchorToOffset - anchorFromOffset
+
+ return if (scene.key == transition.toScene) {
+ Offset(
+ value.x - offset.x,
+ value.y - offset.y,
+ )
+ } else {
+ Offset(
+ value.x + offset.x,
+ value.y + offset.y,
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
new file mode 100644
index 0000000..5cdce94
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.geometry.Offset
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+
+/** Translate an element from an edge of the layout. */
+internal class EdgeTranslate(
+ override val matcher: ElementMatcher,
+ private val edge: Edge,
+ private val startsOutsideLayoutBounds: Boolean = true,
+) : PropertyTransformation<Offset> {
+ override fun transform(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ transition: TransitionState.Transition,
+ value: Offset
+ ): Offset {
+ val sceneSize = scene.size
+ val elementSize = sceneValues.size
+ if (elementSize == Element.SizeUnspecified) {
+ return value
+ }
+
+ return when (edge) {
+ Edge.Top ->
+ if (startsOutsideLayoutBounds) {
+ Offset(value.x, -elementSize.height.toFloat())
+ } else {
+ Offset(value.x, 0f)
+ }
+ Edge.Left ->
+ if (startsOutsideLayoutBounds) {
+ Offset(-elementSize.width.toFloat(), value.y)
+ } else {
+ Offset(0f, value.y)
+ }
+ Edge.Bottom ->
+ if (startsOutsideLayoutBounds) {
+ Offset(value.x, sceneSize.height.toFloat())
+ } else {
+ Offset(value.x, (sceneSize.height - elementSize.height).toFloat())
+ }
+ Edge.Right ->
+ if (startsOutsideLayoutBounds) {
+ Offset(sceneSize.width.toFloat(), value.y)
+ } else {
+ Offset((sceneSize.width - elementSize.width).toFloat(), value.y)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Fade.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Fade.kt
new file mode 100644
index 0000000..0a5ac54
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Fade.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+
+/** Fade an element in or out. */
+internal class Fade(
+ override val matcher: ElementMatcher,
+) : PropertyTransformation<Float> {
+ override fun transform(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ transition: TransitionState.Transition,
+ value: Float
+ ): Float {
+ // Return the alpha value of [element] either when it starts fading in or when it finished
+ // fading out, which is `0` in both cases.
+ return 0f
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/PunchHole.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/PunchHole.kt
new file mode 100644
index 0000000..31e7d7c
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/PunchHole.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.toRect
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Paint
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.graphics.drawOutline
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.drawscope.translate
+import androidx.compose.ui.graphics.withSaveLayer
+import androidx.compose.ui.unit.toSize
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+
+/** Punch a hole in an element using the bounds of another element and a given [shape]. */
+internal class PunchHole(
+ override val matcher: ElementMatcher,
+ private val bounds: ElementKey,
+ private val shape: Shape,
+) : ModifierTransformation {
+ override fun Modifier.transform(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ ): Modifier {
+ return drawWithContent {
+ val bounds = layoutImpl.elements[bounds]
+ if (
+ bounds == null ||
+ bounds.lastSize == Element.SizeUnspecified ||
+ bounds.lastOffset == Offset.Unspecified
+ ) {
+ drawContent()
+ return@drawWithContent
+ }
+
+ drawIntoCanvas { canvas ->
+ canvas.withSaveLayer(size.toRect(), Paint()) {
+ drawContent()
+
+ val offset = bounds.lastOffset - element.lastOffset
+ translate(offset.x, offset.y) { drawHole(bounds) }
+ }
+ }
+ }
+ }
+
+ private fun DrawScope.drawHole(bounds: Element) {
+ if (shape == RectangleShape) {
+ drawRect(Color.Black, blendMode = BlendMode.DstOut)
+ return
+ }
+
+ // TODO(b/290184746): Cache outline if the size of bounds does not change.
+ drawOutline(
+ shape.createOutline(
+ bounds.lastSize.toSize(),
+ layoutDirection,
+ this,
+ ),
+ Color.Black,
+ blendMode = BlendMode.DstOut,
+ )
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/ScaleSize.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
new file mode 100644
index 0000000..ce754dc
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/ScaleSize.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.unit.IntSize
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+import kotlin.math.roundToInt
+
+/**
+ * Scales the size of an element. Note that this makes the element resize every frame and will
+ * therefore impact the layout of other elements.
+ */
+internal class ScaleSize(
+ override val matcher: ElementMatcher,
+ private val width: Float = 1f,
+ private val height: Float = 1f,
+) : PropertyTransformation<IntSize> {
+ override fun transform(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ transition: TransitionState.Transition,
+ value: IntSize,
+ ): IntSize {
+ return IntSize(
+ width = (value.width * width).roundToInt(),
+ height = (value.height * height).roundToInt(),
+ )
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Transformation.kt
new file mode 100644
index 0000000..ce6749d
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Transformation.kt
@@ -0,0 +1,138 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.Modifier
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+
+/** A transformation applied to one or more elements during a transition. */
+sealed interface Transformation {
+ /**
+ * The matcher that should match the element(s) to which this transformation should be applied.
+ */
+ val matcher: ElementMatcher
+
+ /*
+ * Reverse this transformation. This is called when we use Transition(from = A, to = B) when
+ * animating from B to A and there is no Transition(from = B, to = A) defined.
+ */
+ fun reverse(): Transformation = this
+}
+
+/** A transformation that is applied on the element during the whole transition. */
+internal interface ModifierTransformation : Transformation {
+ /** Apply the transformation to [element]. */
+ // TODO(b/290184746): Figure out a public API for custom transformations that don't have access
+ // to these internal classes.
+ fun Modifier.transform(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ ): Modifier
+}
+
+/** A transformation that changes the value of an element property, like its size or offset. */
+internal sealed interface PropertyTransformation<T> : Transformation {
+ /**
+ * The range during which the transformation is applied. If it is `null`, then the
+ * transformation will be applied throughout the whole scene transition.
+ */
+ val range: TransformationRange?
+ get() = null
+
+ /**
+ * Transform [value], i.e. the value of the transformed property without this transformation.
+ */
+ // TODO(b/290184746): Figure out a public API for custom transformations that don't have access
+ // to these internal classes.
+ fun transform(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ transition: TransitionState.Transition,
+ value: T,
+ ): T
+}
+
+/**
+ * A [PropertyTransformation] associated to a range. This is a helper class so that normal
+ * implementations of [PropertyTransformation] don't have to take care of reversing their range when
+ * they are reversed.
+ */
+internal class RangedPropertyTransformation<T>(
+ val delegate: PropertyTransformation<T>,
+ override val range: TransformationRange,
+) : PropertyTransformation<T> by delegate {
+ override fun reverse(): Transformation {
+ return RangedPropertyTransformation(
+ delegate.reverse() as PropertyTransformation<T>,
+ range.reverse()
+ )
+ }
+}
+
+/** The progress-based range of a [PropertyTransformation]. */
+data class TransformationRange
+private constructor(
+ val start: Float,
+ val end: Float,
+) {
+ constructor(
+ start: Float? = null,
+ end: Float? = null
+ ) : this(start ?: BoundUnspecified, end ?: BoundUnspecified)
+
+ init {
+ require(!start.isSpecified() || (start in 0f..1f))
+ require(!end.isSpecified() || (end in 0f..1f))
+ require(!start.isSpecified() || !end.isSpecified() || start <= end)
+ }
+
+ /** Reverse this range. */
+ fun reverse() = TransformationRange(start = reverseBound(end), end = reverseBound(start))
+
+ /** Get the progress of this range given the global [transitionProgress]. */
+ fun progress(transitionProgress: Float): Float {
+ return when {
+ start.isSpecified() && end.isSpecified() ->
+ ((transitionProgress - start) / (end - start)).coerceIn(0f, 1f)
+ !start.isSpecified() && !end.isSpecified() -> transitionProgress
+ end.isSpecified() -> (transitionProgress / end).coerceAtMost(1f)
+ else -> ((transitionProgress - start) / (1f - start)).coerceAtLeast(0f)
+ }
+ }
+
+ private fun Float.isSpecified() = this != BoundUnspecified
+
+ private fun reverseBound(bound: Float): Float {
+ return if (bound.isSpecified()) {
+ 1f - bound
+ } else {
+ BoundUnspecified
+ }
+ }
+
+ companion object {
+ private const val BoundUnspecified = Float.MIN_VALUE
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Translate.kt
new file mode 100644
index 0000000..8abca61
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/animation/scene/transformation/Translate.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import com.android.compose.animation.scene.Element
+import com.android.compose.animation.scene.ElementMatcher
+import com.android.compose.animation.scene.Scene
+import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.TransitionState
+
+/** Translate an element by a fixed amount of density-independent pixels. */
+internal class Translate(
+ override val matcher: ElementMatcher,
+ private val x: Dp = 0.dp,
+ private val y: Dp = 0.dp,
+) : PropertyTransformation<Offset> {
+ override fun transform(
+ layoutImpl: SceneTransitionLayoutImpl,
+ scene: Scene,
+ element: Element,
+ sceneValues: Element.SceneValues,
+ transition: TransitionState.Transition,
+ value: Offset,
+ ): Offset {
+ return with(layoutImpl.density) {
+ Offset(
+ value.x + x.toPx(),
+ value.y + y.toPx(),
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt b/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt
index 5224c51..27f0948 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/grid/Grids.kt
@@ -22,7 +22,6 @@
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.constrainWidth
import androidx.compose.ui.unit.dp
import kotlin.math.ceil
import kotlin.math.max
@@ -126,18 +125,20 @@
((columns - 1) * horizontalSpacing.toPx()).roundToInt()
val totalVerticalSpacingBetweenChildren = ((rows - 1) * verticalSpacing.toPx()).roundToInt()
val childConstraints =
- Constraints().apply {
- if (constraints.maxWidth != Constraints.Infinity) {
- constrainWidth(
+ Constraints(
+ maxWidth =
+ if (constraints.maxWidth != Constraints.Infinity) {
(constraints.maxWidth - totalHorizontalSpacingBetweenChildren) / columns
- )
- }
- if (constraints.maxHeight != Constraints.Infinity) {
- constrainWidth(
+ } else {
+ Constraints.Infinity
+ },
+ maxHeight =
+ if (constraints.maxHeight != Constraints.Infinity) {
(constraints.maxHeight - totalVerticalSpacingBetweenChildren) / rows
- )
- }
- }
+ } else {
+ Constraints.Infinity
+ }
+ )
val placeables = buildList {
for (cellIndex in measurables.indices) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/ConditionalModifiers.kt b/packages/SystemUI/compose/core/src/com/android/compose/modifiers/ConditionalModifiers.kt
similarity index 96%
rename from packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/ConditionalModifiers.kt
rename to packages/SystemUI/compose/core/src/com/android/compose/modifiers/ConditionalModifiers.kt
index 83071d7..135a6e4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/compose/modifiers/ConditionalModifiers.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/modifiers/ConditionalModifiers.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.compose.modifiers
+package com.android.compose.modifiers
import androidx.compose.ui.Modifier
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 946e779..0000000
--- 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/core/src/com/android/compose/ui/util/ListUtils.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/util/ListUtils.kt
new file mode 100644
index 0000000..741f00d
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/util/ListUtils.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+package com.android.compose.ui.util
+
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+
+/**
+ * Iterates through a [List] using the index and calls [action] for each item. This does not
+ * allocate an iterator like [Iterable.forEach].
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
+ */
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
+internal inline fun <T> List<T>.fastForEach(action: (T) -> Unit) {
+ contract { callsInPlace(action) }
+ for (index in indices) {
+ val item = get(index)
+ action(item)
+ }
+}
+
+/**
+ * Returns a list containing the results of applying the given [transform] function to each element
+ * in the original collection.
+ *
+ * **Do not use for collections that come from public APIs**, since they may not support random
+ * access in an efficient way, and this method may actually be a lot slower. Only use for
+ * collections that are created by code we control and are known to support random access.
+ */
+@Suppress("BanInlineOptIn")
+@OptIn(ExperimentalContracts::class)
+internal inline fun <T, R> List<T>.fastMap(transform: (T) -> R): List<R> {
+ contract { callsInPlace(transform) }
+ val target = ArrayList<R>(size)
+ fastForEach { target += transform(it) }
+ return target
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/util/MathHelpers.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/util/MathHelpers.kt
index c1defb7..eb1a634 100644
--- a/packages/SystemUI/compose/core/src/com/android/compose/ui/util/MathHelpers.kt
+++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/util/MathHelpers.kt
@@ -17,11 +17,10 @@
package com.android.compose.ui.util
+import androidx.compose.ui.unit.IntSize
import kotlin.math.roundToInt
import kotlin.math.roundToLong
-// TODO(b/272311106): this is a fork from material. Unfork it when MathHelpers.kt reaches material3.
-
/** Linearly interpolate between [start] and [stop] with [fraction] fraction between them. */
fun lerp(start: Float, stop: Float, fraction: Float): Float {
return (1 - fraction) * start + fraction * stop
@@ -36,3 +35,11 @@
fun lerp(start: Long, stop: Long, fraction: Float): Long {
return start + ((stop - start) * fraction.toDouble()).roundToLong()
}
+
+/** Linearly interpolate between [start] and [stop] with [fraction] fraction between them. */
+fun lerp(start: IntSize, stop: IntSize, fraction: Float): IntSize {
+ return IntSize(
+ lerp(start.width, stop.width, fraction),
+ lerp(start.height, stop.height, fraction)
+ )
+}
diff --git a/packages/SystemUI/compose/core/tests/Android.bp b/packages/SystemUI/compose/core/tests/Android.bp
index 06d94ac..5a8a374 100644
--- a/packages/SystemUI/compose/core/tests/Android.bp
+++ b/packages/SystemUI/compose/core/tests/Android.bp
@@ -42,6 +42,8 @@
"androidx.compose.runtime_runtime",
"androidx.compose.ui_ui-test-junit4",
"androidx.compose.ui_ui-test-manifest",
+
+ "truth-prebuilt",
],
kotlincflags: ["-Xjvm-default=all"],
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt
new file mode 100644
index 0000000..04b3f8a
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/ObservableTransitionStateTest.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.animation.scene
+
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ObservableTransitionStateTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun testObservableTransitionState() = runTest {
+ val state = SceneTransitionLayoutState(TestScenes.SceneA)
+
+ // Collect the current observable state into [observableState].
+ // TODO(b/290184746): Use collectValues {} once it is extracted into a library that can be
+ // reused by non-SystemUI testing code.
+ var observableState: ObservableTransitionState? = null
+ backgroundScope.launch {
+ state.observableTransitionState().collect { observableState = it }
+ }
+
+ fun observableState(): ObservableTransitionState {
+ runCurrent()
+ return observableState!!
+ }
+
+ fun ObservableTransitionState.Transition.progress(): Float {
+ var lastProgress = -1f
+ backgroundScope.launch { progress.collect { lastProgress = it } }
+ runCurrent()
+ return lastProgress
+ }
+
+ rule.testTransition(
+ from = TestScenes.SceneA,
+ to = TestScenes.SceneB,
+ transitionLayout = { currentScene, onChangeScene ->
+ SceneTransitionLayout(
+ currentScene,
+ onChangeScene,
+ EmptyTestTransitions,
+ state = state,
+ ) {
+ scene(TestScenes.SceneA) {}
+ scene(TestScenes.SceneB) {}
+ }
+ }
+ ) {
+ before {
+ assertThat(observableState())
+ .isEqualTo(ObservableTransitionState.Idle(TestScenes.SceneA))
+ }
+ at(0) {
+ val state = observableState()
+ assertThat(state).isInstanceOf(ObservableTransitionState.Transition::class.java)
+ assertThat((state as ObservableTransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneA)
+ assertThat(state.toScene).isEqualTo(TestScenes.SceneB)
+ assertThat(state.progress()).isEqualTo(0f)
+ }
+ at(TestTransitionDuration / 2) {
+ val state = observableState()
+ assertThat((state as ObservableTransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneA)
+ assertThat(state.toScene).isEqualTo(TestScenes.SceneB)
+ assertThat(state.progress()).isEqualTo(0.5f)
+ }
+ after {
+ assertThat(observableState())
+ .isEqualTo(ObservableTransitionState.Idle(TestScenes.SceneB))
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
new file mode 100644
index 0000000..8bd6545
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt
@@ -0,0 +1,323 @@
+/*
+ * 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.animation.scene
+
+import androidx.activity.ComponentActivity
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.size
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.test.junit4.createAndroidComposeRule
+import androidx.compose.ui.test.onAllNodesWithTag
+import androidx.compose.ui.test.onChild
+import androidx.compose.ui.test.onFirst
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.test.subjects.DpOffsetSubject
+import com.android.compose.test.subjects.assertThat
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SceneTransitionLayoutTest {
+ companion object {
+ private val LayoutSize = 300.dp
+ }
+
+ private var currentScene by mutableStateOf(TestScenes.SceneA)
+ private val layoutState = SceneTransitionLayoutState(currentScene)
+
+ // We use createAndroidComposeRule() here and not createComposeRule() because we need an
+ // activity for testBack().
+ @get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
+
+ /** The content under test. */
+ @Composable
+ private fun TestContent() {
+ SceneTransitionLayout(
+ currentScene,
+ { currentScene = it },
+ EmptyTestTransitions,
+ state = layoutState,
+ modifier = Modifier.size(LayoutSize),
+ ) {
+ scene(
+ TestScenes.SceneA,
+ userActions = mapOf(Back to TestScenes.SceneB),
+ ) {
+ Box(Modifier.fillMaxSize()) {
+ SharedFoo(size = 50.dp, childOffset = 0.dp, Modifier.align(Alignment.TopEnd))
+ Text("SceneA")
+ }
+ }
+ scene(TestScenes.SceneB) {
+ Box(Modifier.fillMaxSize()) {
+ SharedFoo(
+ size = 100.dp,
+ childOffset = 50.dp,
+ Modifier.align(Alignment.TopStart),
+ )
+ Text("SceneB")
+ }
+ }
+ scene(TestScenes.SceneC) {
+ Box(Modifier.fillMaxSize()) {
+ SharedFoo(
+ size = 150.dp,
+ childOffset = 100.dp,
+ Modifier.align(Alignment.BottomStart),
+ )
+ Text("SceneC")
+ }
+ }
+ }
+ }
+
+ @Composable
+ private fun SceneScope.SharedFoo(size: Dp, childOffset: Dp, modifier: Modifier = Modifier) {
+ Box(
+ modifier
+ .size(size)
+ .background(Color.Red)
+ .element(TestElements.Foo)
+ .testTag(TestElements.Foo.name)
+ ) {
+ // Offset the single child of Foo by some animated shared offset.
+ val offset by animateSharedDpAsState(childOffset, TestValues.Value1, TestElements.Foo)
+
+ Box(
+ Modifier.offset {
+ val pxOffset = offset.roundToPx()
+ IntOffset(pxOffset, pxOffset)
+ }
+ .size(30.dp)
+ .background(Color.Blue)
+ .testTag(TestElements.Bar.name)
+ )
+ }
+ }
+
+ @Test
+ fun testOnlyCurrentSceneIsDisplayed() {
+ rule.setContent { TestContent() }
+
+ // Only scene A is displayed.
+ rule.onNodeWithText("SceneA").assertIsDisplayed()
+ rule.onNodeWithText("SceneB").assertDoesNotExist()
+ rule.onNodeWithText("SceneC").assertDoesNotExist()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+ // Change to scene B. Only that scene is displayed.
+ currentScene = TestScenes.SceneB
+ rule.onNodeWithText("SceneA").assertDoesNotExist()
+ rule.onNodeWithText("SceneB").assertIsDisplayed()
+ rule.onNodeWithText("SceneC").assertDoesNotExist()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneB)
+ }
+
+ @Test
+ fun testBack() {
+ rule.setContent { TestContent() }
+
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+ rule.activity.onBackPressed()
+ rule.waitForIdle()
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneB)
+ }
+
+ @Test
+ fun testTransitionState() {
+ rule.setContent { TestContent() }
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+ // We will advance the clock manually.
+ rule.mainClock.autoAdvance = false
+
+ // Change the current scene. Until composition is triggered, this won't change the layout
+ // state.
+ currentScene = TestScenes.SceneB
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+ // On the next frame, we will recompose because currentScene changed, which will start the
+ // transition (i.e. it will change the transitionState to be a Transition) in a
+ // LaunchedEffect.
+ rule.mainClock.advanceTimeByFrame()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java)
+ val transition = layoutState.transitionState as TransitionState.Transition
+ assertThat(transition.fromScene).isEqualTo(TestScenes.SceneA)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+ assertThat(transition.progress).isEqualTo(0f)
+
+ // Then, on the next frame, the animator we started gets its initial value and clock
+ // starting time. We are now at progress = 0f.
+ rule.mainClock.advanceTimeByFrame()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((layoutState.transitionState as TransitionState.Transition).progress)
+ .isEqualTo(0f)
+
+ // The test transition lasts 480ms. 240ms after the start of the transition, we are at
+ // progress = 0.5f.
+ rule.mainClock.advanceTimeBy(TestTransitionDuration / 2)
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((layoutState.transitionState as TransitionState.Transition).progress)
+ .isEqualTo(0.5f)
+
+ // (240-16) ms later, i.e. one frame before the transition is finished, we are at
+ // progress=(480-16)/480.
+ rule.mainClock.advanceTimeBy(TestTransitionDuration / 2 - 16)
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((layoutState.transitionState as TransitionState.Transition).progress)
+ .isEqualTo((TestTransitionDuration - 16) / 480f)
+
+ // one frame (16ms) later, the transition is finished and we are in the idle state in scene
+ // B.
+ rule.mainClock.advanceTimeByFrame()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneB)
+ }
+
+ @Test
+ fun testSharedElement() {
+ rule.setContent { TestContent() }
+
+ // In scene A, the shared element SharedFoo() is at the top end of the layout and has a size
+ // of 50.dp.
+ var sharedFoo = rule.onNodeWithTag(TestElements.Foo.name, useUnmergedTree = true)
+ sharedFoo.assertWidthIsEqualTo(50.dp)
+ sharedFoo.assertHeightIsEqualTo(50.dp)
+ sharedFoo.assertPositionInRootIsEqualTo(
+ expectedTop = 0.dp,
+ expectedLeft = LayoutSize - 50.dp,
+ )
+
+ // The shared offset of the single child of SharedFoo() is 0dp in scene A.
+ assertThat(sharedFoo.onChild().offsetRelativeTo(sharedFoo)).isEqualTo(DpOffset(0.dp, 0.dp))
+
+ // Pause animations to test the state mid-transition.
+ rule.mainClock.autoAdvance = false
+
+ // Go to scene B and let the animation start. See [testLayoutState()] and
+ // [androidx.compose.ui.test.MainTestClock] to understand why we need to advance the clock
+ // by 2 frames to be at the start of the animation.
+ currentScene = TestScenes.SceneB
+ rule.mainClock.advanceTimeByFrame()
+ rule.mainClock.advanceTimeByFrame()
+
+ // Advance to the middle of the animation.
+ rule.mainClock.advanceTimeBy(TestTransitionDuration / 2)
+
+ // We need to use onAllNodesWithTag().onFirst() here given that shared elements are
+ // composed and laid out in both scenes (but drawn only in one).
+ sharedFoo = rule.onAllNodesWithTag(TestElements.Foo.name).onFirst()
+
+ // In scene B, foo is at the top start (x = 0, y = 0) of the layout and has a size of
+ // 100.dp. We pause at the middle of the transition, so it should now be 75.dp given that we
+ // use a linear interpolator. Foo was at (x = layoutSize - 50dp, y = 0) in SceneA and is
+ // going to (x = 0, y = 0), so the offset should now be half what it was.
+ assertThat((layoutState.transitionState as TransitionState.Transition).progress)
+ .isEqualTo(0.5f)
+ sharedFoo.assertWidthIsEqualTo(75.dp)
+ sharedFoo.assertHeightIsEqualTo(75.dp)
+ sharedFoo.assertPositionInRootIsEqualTo(
+ expectedTop = 0.dp,
+ expectedLeft = (LayoutSize - 50.dp) / 2
+ )
+
+ // The shared offset of the single child of SharedFoo() is 50dp in scene B and 0dp in Scene
+ // A, so it should be 25dp now.
+ assertThat(sharedFoo.onChild().offsetRelativeTo(sharedFoo))
+ .isWithin(DpOffsetSubject.DefaultTolerance)
+ .of(DpOffset(25.dp, 25.dp))
+
+ // Animate to scene C, let the animation start then go to the middle of the transition.
+ currentScene = TestScenes.SceneC
+ rule.mainClock.advanceTimeByFrame()
+ rule.mainClock.advanceTimeByFrame()
+ rule.mainClock.advanceTimeBy(TestTransitionDuration / 2)
+
+ // In Scene C, foo is at the bottom start of the layout and has a size of 150.dp. The
+ // transition scene B => scene C is using a FastOutSlowIn interpolator.
+ val interpolatedProgress = FastOutSlowInEasing.transform(0.5f)
+ val expectedTop = (LayoutSize - 150.dp) * interpolatedProgress
+ val expectedLeft = 0.dp
+ val expectedSize = 100.dp + (150.dp - 100.dp) * interpolatedProgress
+
+ sharedFoo = rule.onAllNodesWithTag(TestElements.Foo.name).onFirst()
+ assertThat((layoutState.transitionState as TransitionState.Transition).progress)
+ .isEqualTo(interpolatedProgress)
+ sharedFoo.assertWidthIsEqualTo(expectedSize)
+ sharedFoo.assertHeightIsEqualTo(expectedSize)
+ sharedFoo.assertPositionInRootIsEqualTo(expectedLeft, expectedTop)
+
+ // The shared offset of the single child of SharedFoo() is 50dp in scene B and 100dp in
+ // Scene C.
+ val expectedOffset = 50.dp + (100.dp - 50.dp) * interpolatedProgress
+ assertThat(sharedFoo.onChild().offsetRelativeTo(sharedFoo))
+ .isWithin(DpOffsetSubject.DefaultTolerance)
+ .of(DpOffset(expectedOffset, expectedOffset))
+
+ // Go back to scene A. This should happen instantly (once the animation started, i.e. after
+ // 2 frames) given that we use a snap() animation spec.
+ currentScene = TestScenes.SceneA
+ rule.mainClock.advanceTimeByFrame()
+ rule.mainClock.advanceTimeByFrame()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+ }
+
+ private fun SemanticsNodeInteraction.offsetRelativeTo(
+ other: SemanticsNodeInteraction,
+ ): DpOffset {
+ val node = fetchSemanticsNode()
+ val bounds = node.boundsInRoot
+ val otherBounds = other.fetchSemanticsNode().boundsInRoot
+ return with(node.layoutInfo.density) {
+ DpOffset(
+ x = (bounds.left - otherBounds.left).toDp(),
+ y = (bounds.top - otherBounds.top).toDp(),
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
new file mode 100644
index 0000000..cb2607a
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt
@@ -0,0 +1,241 @@
+/*
+ * 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.animation.scene
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipeWithVelocity
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SwipeToSceneTest {
+ companion object {
+ private val LayoutWidth = 200.dp
+ private val LayoutHeight = 400.dp
+
+ /** The middle of the layout, in pixels. */
+ private val Density.middle: Offset
+ get() = Offset((LayoutWidth / 2).toPx(), (LayoutHeight / 2).toPx())
+ }
+
+ private var currentScene by mutableStateOf(TestScenes.SceneA)
+ private val layoutState = SceneTransitionLayoutState(currentScene)
+
+ @get:Rule val rule = createComposeRule()
+
+ /** The content under test. */
+ @Composable
+ private fun TestContent() {
+ SceneTransitionLayout(
+ currentScene,
+ { currentScene = it },
+ EmptyTestTransitions,
+ state = layoutState,
+ modifier = Modifier.size(LayoutWidth, LayoutHeight).testTag(TestElements.Foo.name),
+ ) {
+ scene(
+ TestScenes.SceneA,
+ userActions =
+ mapOf(
+ Swipe.Left to TestScenes.SceneB,
+ Swipe.Down to TestScenes.SceneC,
+ ),
+ ) {
+ Box(Modifier.fillMaxSize())
+ }
+ scene(
+ TestScenes.SceneB,
+ userActions = mapOf(Swipe.Right to TestScenes.SceneA),
+ ) {
+ Box(Modifier.fillMaxSize())
+ }
+ scene(
+ TestScenes.SceneC,
+ userActions = mapOf(Swipe.Down to TestScenes.SceneA),
+ ) {
+ Box(Modifier.fillMaxSize())
+ }
+ }
+ }
+
+ @Test
+ fun testDragWithPositionalThreshold() {
+ // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
+ // detected as a drag event.
+ var touchSlop = 0f
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ TestContent()
+ }
+
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+ // Drag left (i.e. from right to left) by 55dp. We pick 55dp here because 56dp is the
+ // positional threshold from which we commit the gesture.
+ rule.onRoot().performTouchInput {
+ down(middle)
+
+ // We use a high delay so that the velocity of the gesture is slow (otherwise it would
+ // commit the gesture, even if we are below the positional threshold).
+ moveBy(Offset(-55.dp.toPx() - touchSlop, 0f), delayMillis = 1_000)
+ }
+
+ // We should be at a progress = 55dp / LayoutWidth given that we use the layout size in
+ // the gesture axis as swipe distance.
+ var transition = layoutState.transitionState
+ assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((transition as TransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneA)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+ assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
+ assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth)
+
+ // Release the finger. We should now be animating back to A (currentScene = SceneA) given
+ // that 55dp < positional threshold.
+ rule.onRoot().performTouchInput { up() }
+ transition = layoutState.transitionState
+ assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((transition as TransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneA)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+ assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
+ assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth)
+
+ // Wait for the animation to finish. We should now be in scene A.
+ rule.waitForIdle()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+ // Now we do the same but vertically and with a drag distance of 56dp, which is >=
+ // positional threshold.
+ rule.onRoot().performTouchInput {
+ down(middle)
+ moveBy(Offset(0f, 56.dp.toPx() + touchSlop), delayMillis = 1_000)
+ }
+
+ // Drag is in progress, so currentScene = SceneA and progress = 56dp / LayoutHeight
+ transition = layoutState.transitionState
+ assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((transition as TransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneA)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneC)
+ assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
+ assertThat(transition.progress).isEqualTo(56.dp / LayoutHeight)
+
+ // Release the finger. We should now be animating to C (currentScene = SceneC) given
+ // that 56dp >= positional threshold.
+ rule.onRoot().performTouchInput { up() }
+ transition = layoutState.transitionState
+ assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((transition as TransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneA)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneC)
+ assertThat(transition.currentScene).isEqualTo(TestScenes.SceneC)
+ assertThat(transition.progress).isEqualTo(56.dp / LayoutHeight)
+
+ // Wait for the animation to finish. We should now be in scene C.
+ rule.waitForIdle()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+ }
+
+ @Test
+ fun testSwipeWithVelocityThreshold() {
+ // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is
+ // detected as a drag event.
+ var touchSlop = 0f
+ rule.setContent {
+ touchSlop = LocalViewConfiguration.current.touchSlop
+ TestContent()
+ }
+
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+ // Swipe left (i.e. from right to left) using a velocity of 124 dp/s. We pick 124 dp/s here
+ // because 125 dp/s is the velocity threshold from which we commit the gesture. We also use
+ // a swipe distance < 56dp, the positional threshold, to make sure that we don't commit
+ // the gesture because of a large enough swipe distance.
+ rule.onRoot().performTouchInput {
+ swipeWithVelocity(
+ start = middle,
+ end = middle - Offset(55.dp.toPx() + touchSlop, 0f),
+ endVelocity = 124.dp.toPx(),
+ )
+ }
+
+ // We should be animating back to A (currentScene = SceneA) given that 124 dp/s < velocity
+ // threshold.
+ var transition = layoutState.transitionState
+ assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((transition as TransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneA)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneB)
+ assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA)
+ assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth)
+
+ // Wait for the animation to finish. We should now be in scene A.
+ rule.waitForIdle()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA)
+
+ // Now we do the same but vertically and with a swipe velocity of 126dp, which is >
+ // velocity threshold. Note that in theory we could have used 125 dp (= velocity threshold)
+ // but it doesn't work reliably with how swipeWithVelocity() computes move events to get to
+ // the target velocity, probably because of float rounding errors.
+ rule.onRoot().performTouchInput {
+ swipeWithVelocity(
+ start = middle,
+ end = middle + Offset(0f, 55.dp.toPx() + touchSlop),
+ endVelocity = 126.dp.toPx(),
+ )
+ }
+
+ // We should be animating to C (currentScene = SceneC).
+ transition = layoutState.transitionState
+ assertThat(transition).isInstanceOf(TransitionState.Transition::class.java)
+ assertThat((transition as TransitionState.Transition).fromScene)
+ .isEqualTo(TestScenes.SceneA)
+ assertThat(transition.toScene).isEqualTo(TestScenes.SceneC)
+ assertThat(transition.currentScene).isEqualTo(TestScenes.SceneC)
+ assertThat(transition.progress).isEqualTo(55.dp / LayoutHeight)
+
+ // Wait for the animation to finish. We should now be in scene C.
+ rule.waitForIdle()
+ assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java)
+ assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC)
+ }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestTransition.kt
new file mode 100644
index 0000000..275149a0
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestTransition.kt
@@ -0,0 +1,217 @@
+/*
+ * 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.animation.scene
+
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.onNodeWithTag
+
+@DslMarker annotation class TransitionTestDsl
+
+@TransitionTestDsl
+interface TransitionTestBuilder {
+ /**
+ * Assert on the state of the layout before the transition starts.
+ *
+ * This should be called maximum once, before [at] or [after] is called.
+ */
+ fun before(builder: TransitionTestAssertionScope.() -> Unit)
+
+ /**
+ * Assert on the state of the layout during the transition at [timestamp].
+ *
+ * This should be called after [before] is called and before [after] is called. Successive calls
+ * to [at] must be called with increasing [timestamp].
+ *
+ * Important: [timestamp] must be a multiple of 16 (the duration of a frame on the JVM/Android).
+ * There is no intermediary state between `t` and `t + 16` , so testing transitions outside of
+ * `t = 0`, `t = 16`, `t = 32`, etc does not make sense.
+ */
+ fun at(timestamp: Long, builder: TransitionTestAssertionScope.() -> Unit)
+
+ /**
+ * Assert on the state of the layout after the transition finished.
+ *
+ * This should be called maximum once, after [before] or [at] is called.
+ */
+ fun after(builder: TransitionTestAssertionScope.() -> Unit)
+}
+
+@TransitionTestDsl
+interface TransitionTestAssertionScope {
+ /** Assert on [element]. */
+ fun onElement(element: ElementKey): SemanticsNodeInteraction
+}
+
+/**
+ * Test the transition between [fromSceneContent] and [toSceneContent] at different points in time.
+ *
+ * @sample com.android.compose.animation.scene.transformation.TranslateTest
+ */
+fun ComposeContentTestRule.testTransition(
+ fromSceneContent: @Composable SceneScope.() -> Unit,
+ toSceneContent: @Composable SceneScope.() -> Unit,
+ transition: TransitionBuilder.() -> Unit,
+ layoutModifier: Modifier = Modifier,
+ builder: TransitionTestBuilder.() -> Unit,
+) {
+ testTransition(
+ from = TestScenes.SceneA,
+ to = TestScenes.SceneB,
+ transitionLayout = { currentScene, onChangeScene ->
+ SceneTransitionLayout(
+ currentScene,
+ onChangeScene,
+ transitions { from(TestScenes.SceneA, to = TestScenes.SceneB, transition) },
+ layoutModifier.fillMaxSize(),
+ ) {
+ scene(TestScenes.SceneA, content = fromSceneContent)
+ scene(TestScenes.SceneB, content = toSceneContent)
+ }
+ },
+ builder,
+ )
+}
+
+/**
+ * Test the transition between two scenes of [transitionLayout][SceneTransitionLayout] at different
+ * points in time.
+ */
+fun ComposeContentTestRule.testTransition(
+ from: SceneKey,
+ to: SceneKey,
+ transitionLayout:
+ @Composable
+ (
+ currentScene: SceneKey,
+ onChangeScene: (SceneKey) -> Unit,
+ ) -> Unit,
+ builder: TransitionTestBuilder.() -> Unit,
+) {
+ val test = transitionTest(builder)
+ val assertionScope =
+ object : TransitionTestAssertionScope {
+ override fun onElement(element: ElementKey): SemanticsNodeInteraction {
+ return this@testTransition.onNodeWithTag(element.name)
+ }
+ }
+
+ var currentScene by mutableStateOf(from)
+ setContent { transitionLayout(currentScene, { currentScene = it }) }
+
+ // Wait for the UI to be idle then test the before state.
+ waitForIdle()
+ test.before(assertionScope)
+
+ // Manually advance the clock to the start of the animation.
+ mainClock.autoAdvance = false
+
+ // Change the current scene.
+ currentScene = to
+
+ // Advance by a frame to trigger recomposition, which will start the transition (i.e. it will
+ // change the transitionState to be a Transition) in a LaunchedEffect.
+ mainClock.advanceTimeByFrame()
+
+ // Advance by another frame so that the animator we started gets its initial value and clock
+ // starting time. We are now at progress = 0f.
+ mainClock.advanceTimeByFrame()
+ waitForIdle()
+
+ // Test the assertions at specific points in time.
+ test.timestamps.forEach { tsAssertion ->
+ if (tsAssertion.timestampDelta > 0L) {
+ mainClock.advanceTimeBy(tsAssertion.timestampDelta)
+ waitForIdle()
+ }
+
+ tsAssertion.assertion(assertionScope)
+ }
+
+ // Go to the end state and test it.
+ mainClock.autoAdvance = true
+ waitForIdle()
+ test.after(assertionScope)
+}
+
+private fun transitionTest(builder: TransitionTestBuilder.() -> Unit): TransitionTest {
+ // Collect the assertion lambdas in [TransitionTest]. Note that the ordering is forced by the
+ // builder, e.g. `before {}` must be called before everything else, then `at {}` (in increasing
+ // order of timestamp), then `after {}`. That way the test code is run with the same order as it
+ // is written, to avoid confusion.
+
+ val impl =
+ object : TransitionTestBuilder {
+ var before: (TransitionTestAssertionScope.() -> Unit)? = null
+ var after: (TransitionTestAssertionScope.() -> Unit)? = null
+ val timestamps = mutableListOf<TimestampAssertion>()
+
+ private var currentTimestamp = 0L
+
+ override fun before(builder: TransitionTestAssertionScope.() -> Unit) {
+ check(before == null) { "before {} must be called maximum once" }
+ check(after == null) { "before {} must be called before after {}" }
+ check(timestamps.isEmpty()) { "before {} must be called before at(...) {}" }
+
+ before = builder
+ }
+
+ override fun at(timestamp: Long, builder: TransitionTestAssertionScope.() -> Unit) {
+ check(after == null) { "at(...) {} must be called before after {}" }
+ check(timestamp >= currentTimestamp) {
+ "at(...) must be called with timestamps in increasing order"
+ }
+ check(timestamp % 16 == 0L) {
+ "timestamp must be a multiple of the frame time (16ms)"
+ }
+
+ val delta = timestamp - currentTimestamp
+ currentTimestamp = timestamp
+
+ timestamps.add(TimestampAssertion(delta, builder))
+ }
+
+ override fun after(builder: TransitionTestAssertionScope.() -> Unit) {
+ check(after == null) { "after {} must be called maximum once" }
+ after = builder
+ }
+ }
+ .apply(builder)
+
+ return TransitionTest(
+ before = impl.before ?: {},
+ timestamps = impl.timestamps,
+ after = impl.after ?: {},
+ )
+}
+
+private class TransitionTest(
+ val before: TransitionTestAssertionScope.() -> Unit,
+ val after: TransitionTestAssertionScope.() -> Unit,
+ val timestamps: List<TimestampAssertion>,
+)
+
+private class TimestampAssertion(
+ val timestampDelta: Long,
+ val assertion: TransitionTestAssertionScope.() -> Unit,
+)
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestValues.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestValues.kt
new file mode 100644
index 0000000..8357262
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/TestValues.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.animation.scene
+
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.snap
+import androidx.compose.animation.core.tween
+
+/** Scenes keys that can be reused by tests. */
+object TestScenes {
+ val SceneA = SceneKey("SceneA")
+ val SceneB = SceneKey("SceneB")
+ val SceneC = SceneKey("SceneC")
+}
+
+/** Element keys that can be reused by tests. */
+object TestElements {
+ val Foo = ElementKey("Foo")
+ val Bar = ElementKey("Bar")
+}
+
+/** Value keys that can be reused by tests. */
+object TestValues {
+ val Value1 = ValueKey("Value1")
+}
+
+// We use a transition duration of 480ms here because it is a multiple of 16, the time of a frame in
+// C JVM/Android. Doing so allows us for instance to test the state at progress = 0.5f given that t
+// = 240ms is also a multiple of 16.
+val TestTransitionDuration = 480L
+
+/** A definition of empty transitions between [TestScenes], using different animation specs. */
+val EmptyTestTransitions = transitions {
+ from(TestScenes.SceneA, to = TestScenes.SceneB) {
+ spec = tween(durationMillis = TestTransitionDuration.toInt(), easing = LinearEasing)
+ }
+
+ from(TestScenes.SceneB, to = TestScenes.SceneC) {
+ spec = tween(durationMillis = TestTransitionDuration.toInt(), easing = FastOutSlowInEasing)
+ }
+
+ from(TestScenes.SceneC, to = TestScenes.SceneA) { spec = snap() }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt
new file mode 100644
index 0000000..8ef6757
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/AnchoredSizeTest.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.animation.scene.transformation
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestElements
+import com.android.compose.animation.scene.testTransition
+import com.android.compose.test.assertSizeIsEqualTo
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AnchoredSizeTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun testAnchoredSizeEnter() {
+ rule.testTransition(
+ fromSceneContent = { Box(Modifier.size(100.dp, 100.dp).element(TestElements.Foo)) },
+ toSceneContent = {
+ Box(Modifier.size(50.dp, 50.dp).element(TestElements.Foo))
+ Box(Modifier.size(200.dp, 60.dp).element(TestElements.Bar))
+ },
+ transition = {
+ // Scale during 4 frames.
+ spec = tween(16 * 4, easing = LinearEasing)
+ anchoredSize(TestElements.Bar, TestElements.Foo)
+ },
+ ) {
+ // Bar is entering. It starts at the same size as Foo in scene A in and scales to its
+ // final size in scene B.
+ before { onElement(TestElements.Bar).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp) }
+ at(16) { onElement(TestElements.Bar).assertSizeIsEqualTo(125.dp, 90.dp) }
+ at(32) { onElement(TestElements.Bar).assertSizeIsEqualTo(150.dp, 80.dp) }
+ at(48) { onElement(TestElements.Bar).assertSizeIsEqualTo(175.dp, 70.dp) }
+ at(64) { onElement(TestElements.Bar).assertSizeIsEqualTo(200.dp, 60.dp) }
+ after { onElement(TestElements.Bar).assertSizeIsEqualTo(200.dp, 60.dp) }
+ }
+ }
+
+ @Test
+ fun testAnchoredSizeExit() {
+ rule.testTransition(
+ fromSceneContent = {
+ Box(Modifier.size(100.dp, 100.dp).element(TestElements.Foo))
+ Box(Modifier.size(100.dp, 100.dp).element(TestElements.Bar))
+ },
+ toSceneContent = { Box(Modifier.size(200.dp, 60.dp).element(TestElements.Foo)) },
+ transition = {
+ // Scale during 4 frames.
+ spec = tween(16 * 4, easing = LinearEasing)
+ anchoredSize(TestElements.Bar, TestElements.Foo)
+ },
+ ) {
+ // Bar is leaving. It starts at 100dp x 100dp in scene A and is scaled to 200dp x 60dp,
+ // the size of Foo in scene B.
+ before { onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp) }
+ at(0) { onElement(TestElements.Bar).assertSizeIsEqualTo(100.dp, 100.dp) }
+ at(16) { onElement(TestElements.Bar).assertSizeIsEqualTo(125.dp, 90.dp) }
+ at(32) { onElement(TestElements.Bar).assertSizeIsEqualTo(150.dp, 80.dp) }
+ at(48) { onElement(TestElements.Bar).assertSizeIsEqualTo(175.dp, 70.dp) }
+ after { onElement(TestElements.Bar).assertDoesNotExist() }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/AnchoredTranslateTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/AnchoredTranslateTest.kt
new file mode 100644
index 0000000..d1205e7
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/AnchoredTranslateTest.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.animation.scene.transformation
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.offset
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestElements
+import com.android.compose.animation.scene.testTransition
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class AnchoredTranslateTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun testAnchoredTranslateExit() {
+ rule.testTransition(
+ fromSceneContent = {
+ Box(Modifier.offset(10.dp, 50.dp).element(TestElements.Foo))
+ Box(Modifier.offset(20.dp, 40.dp).element(TestElements.Bar))
+ },
+ toSceneContent = { Box(Modifier.offset(30.dp, 10.dp).element(TestElements.Foo)) },
+ transition = {
+ // Anchor Bar to Foo, which is moving from (10dp, 50dp) to (30dp, 10dp).
+ spec = tween(16 * 4, easing = LinearEasing)
+ anchoredTranslate(TestElements.Bar, TestElements.Foo)
+ },
+ ) {
+ // Bar moves by (20dp, -40dp), like Foo.
+ before { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(20.dp, 40.dp) }
+ at(0) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(20.dp, 40.dp) }
+ at(16) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(25.dp, 30.dp) }
+ at(32) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(30.dp, 20.dp) }
+ at(48) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(35.dp, 10.dp) }
+ after { onElement(TestElements.Bar).assertDoesNotExist() }
+ }
+ }
+
+ @Test
+ fun testAnchoredTranslateEnter() {
+ rule.testTransition(
+ fromSceneContent = { Box(Modifier.offset(10.dp, 50.dp).element(TestElements.Foo)) },
+ toSceneContent = {
+ Box(Modifier.offset(30.dp, 10.dp).element(TestElements.Foo))
+ Box(Modifier.offset(20.dp, 40.dp).element(TestElements.Bar))
+ },
+ transition = {
+ // Anchor Bar to Foo, which is moving from (10dp, 50dp) to (30dp, 10dp).
+ spec = tween(16 * 4, easing = LinearEasing)
+ anchoredTranslate(TestElements.Bar, TestElements.Foo)
+ },
+ ) {
+ // Bar moves by (20dp, -40dp), like Foo.
+ before { onElement(TestElements.Bar).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(0.dp, 80.dp) }
+ at(16) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(5.dp, 70.dp) }
+ at(32) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(10.dp, 60.dp) }
+ at(48) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(15.dp, 50.dp) }
+ at(64) { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(20.dp, 40.dp) }
+ after { onElement(TestElements.Bar).assertPositionInRootIsEqualTo(20.dp, 40.dp) }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt
new file mode 100644
index 0000000..2a27763
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/EdgeTranslateTest.kt
@@ -0,0 +1,153 @@
+/*
+ * 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.animation.scene.transformation
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.TestElements
+import com.android.compose.animation.scene.TransitionTestBuilder
+import com.android.compose.animation.scene.testTransition
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class EdgeTranslateTest {
+
+ @get:Rule val rule = createComposeRule()
+
+ private fun testEdgeTranslate(
+ edge: Edge,
+ startsOutsideLayoutBounds: Boolean,
+ builder: TransitionTestBuilder.() -> Unit,
+ ) {
+ rule.testTransition(
+ // The layout under test is 300dp x 300dp.
+ layoutModifier = Modifier.size(300.dp),
+ fromSceneContent = {},
+ toSceneContent = {
+ // Foo is 100dp x 100dp in the center of the layout, so at offset = (100dp, 100dp)
+ Box(Modifier.fillMaxSize()) {
+ Box(Modifier.size(100.dp).element(TestElements.Foo).align(Alignment.Center))
+ }
+ },
+ transition = {
+ spec = tween(16 * 4, easing = LinearEasing)
+ translate(TestElements.Foo, edge, startsOutsideLayoutBounds)
+ },
+ builder = builder,
+ )
+ }
+
+ @Test
+ fun testEntersFromTop_startsOutsideLayoutBounds() {
+ testEdgeTranslate(Edge.Top, startsOutsideLayoutBounds = true) {
+ before { onElement(TestElements.Foo).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, (-100).dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 0.dp) }
+ at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ }
+ }
+
+ @Test
+ fun testEntersFromTop_startsInsideLayoutBounds() {
+ testEdgeTranslate(Edge.Top, startsOutsideLayoutBounds = false) {
+ before { onElement(TestElements.Foo).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 0.dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 50.dp) }
+ at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ }
+ }
+
+ @Test
+ fun testEntersFromBottom_startsOutsideLayoutBounds() {
+ testEdgeTranslate(Edge.Bottom, startsOutsideLayoutBounds = true) {
+ before { onElement(TestElements.Foo).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 300.dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 200.dp) }
+ at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ }
+ }
+
+ @Test
+ fun testEntersFromBottom_startsInsideLayoutBounds() {
+ testEdgeTranslate(Edge.Bottom, startsOutsideLayoutBounds = false) {
+ before { onElement(TestElements.Foo).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 200.dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 150.dp) }
+ at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ }
+ }
+
+ @Test
+ fun testEntersFromLeft_startsOutsideLayoutBounds() {
+ testEdgeTranslate(Edge.Left, startsOutsideLayoutBounds = true) {
+ before { onElement(TestElements.Foo).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo((-100).dp, 100.dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(0.dp, 100.dp) }
+ at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ }
+ }
+
+ @Test
+ fun testEntersFromLeft_startsInsideLayoutBounds() {
+ testEdgeTranslate(Edge.Left, startsOutsideLayoutBounds = false) {
+ before { onElement(TestElements.Foo).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(0.dp, 100.dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(50.dp, 100.dp) }
+ at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ }
+ }
+
+ @Test
+ fun testEntersFromRight_startsOutsideLayoutBounds() {
+ testEdgeTranslate(Edge.Right, startsOutsideLayoutBounds = true) {
+ before { onElement(TestElements.Foo).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(300.dp, 100.dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(200.dp, 100.dp) }
+ at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ }
+ }
+
+ @Test
+ fun testEntersFromRight_startsInsideLayoutBounds() {
+ testEdgeTranslate(Edge.Right, startsOutsideLayoutBounds = false) {
+ before { onElement(TestElements.Foo).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(200.dp, 100.dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(150.dp, 100.dp) }
+ at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(100.dp, 100.dp) }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/ScaleSizeTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/ScaleSizeTest.kt
new file mode 100644
index 0000000..384355c
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/ScaleSizeTest.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.animation.scene.transformation
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.size
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestElements
+import com.android.compose.animation.scene.testTransition
+import com.android.compose.test.assertSizeIsEqualTo
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ScaleSizeTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun testScaleSize() {
+ rule.testTransition(
+ fromSceneContent = {},
+ toSceneContent = { Box(Modifier.size(100.dp).element(TestElements.Foo)) },
+ transition = {
+ // Scale during 4 frames.
+ spec = tween(16 * 4, easing = LinearEasing)
+ scaleSize(TestElements.Foo, width = 2f, height = 0.5f)
+ },
+ ) {
+ // Foo is entering, is 100dp x 100dp at rest and is scaled by (2, 0.5) during the
+ // transition so it starts at 200dp x 50dp.
+ before { onElement(TestElements.Foo).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Foo).assertSizeIsEqualTo(200.dp, 50.dp) }
+ at(16) { onElement(TestElements.Foo).assertSizeIsEqualTo(175.dp, 62.5.dp) }
+ at(32) { onElement(TestElements.Foo).assertSizeIsEqualTo(150.dp, 75.dp) }
+ at(48) { onElement(TestElements.Foo).assertSizeIsEqualTo(125.dp, 87.5.dp) }
+ at(64) { onElement(TestElements.Foo).assertSizeIsEqualTo(100.dp, 100.dp) }
+ after { onElement(TestElements.Foo).assertSizeIsEqualTo(100.dp, 100.dp) }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/TranslateTest.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/TranslateTest.kt
new file mode 100644
index 0000000..1d559fd
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/animation/scene/transformation/TranslateTest.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.animation.scene.transformation
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.offset
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestElements
+import com.android.compose.animation.scene.testTransition
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TranslateTest {
+ @get:Rule val rule = createComposeRule()
+
+ @Test
+ fun testTranslateExit() {
+ rule.testTransition(
+ fromSceneContent = {
+ // Foo is at (10dp, 50dp) and is exiting.
+ Box(Modifier.offset(10.dp, 50.dp).element(TestElements.Foo))
+ },
+ toSceneContent = {},
+ transition = {
+ // Foo is translated by (20dp, -40dp) during 4 frames.
+ spec = tween(16 * 4, easing = LinearEasing)
+ translate(TestElements.Foo, x = 20.dp, y = (-40).dp)
+ },
+ ) {
+ before { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) }
+ at(16) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(15.dp, 40.dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(20.dp, 30.dp) }
+ at(48) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(25.dp, 20.dp) }
+ after { onElement(TestElements.Foo).assertDoesNotExist() }
+ }
+ }
+
+ @Test
+ fun testTranslateEnter() {
+ rule.testTransition(
+ fromSceneContent = {},
+ toSceneContent = {
+ // Foo is entering to (10dp, 50dp)
+ Box(Modifier.offset(10.dp, 50.dp).element(TestElements.Foo))
+ },
+ transition = {
+ // Foo is translated from (10dp, 50) + (20dp, -40dp) during 4 frames.
+ spec = tween(16 * 4, easing = LinearEasing)
+ translate(TestElements.Foo, x = 20.dp, y = (-40).dp)
+ },
+ ) {
+ before { onElement(TestElements.Foo).assertDoesNotExist() }
+ at(0) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(30.dp, 10.dp) }
+ at(16) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(25.dp, 20.dp) }
+ at(32) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(20.dp, 30.dp) }
+ at(48) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(15.dp, 40.dp) }
+ at(64) { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) }
+ after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) }
+ }
+ }
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/test/SizeAssertions.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/test/SizeAssertions.kt
new file mode 100644
index 0000000..fbd1b51
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/test/SizeAssertions.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.test
+
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertHeightIsEqualTo
+import androidx.compose.ui.test.assertWidthIsEqualTo
+import androidx.compose.ui.unit.Dp
+
+fun SemanticsNodeInteraction.assertSizeIsEqualTo(expectedWidth: Dp, expectedHeight: Dp) {
+ assertWidthIsEqualTo(expectedWidth)
+ assertHeightIsEqualTo(expectedHeight)
+}
diff --git a/packages/SystemUI/compose/core/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt b/packages/SystemUI/compose/core/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt
new file mode 100644
index 0000000..bf7bf98
--- /dev/null
+++ b/packages/SystemUI/compose/core/tests/src/com/android/compose/test/subjects/DpOffsetSubject.kt
@@ -0,0 +1,58 @@
+/*
+ * 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.test.subjects
+
+import androidx.compose.ui.test.assertIsEqualTo
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpOffset
+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
+
+/** Assert on a [DpOffset]. */
+fun assertThat(dpOffset: DpOffset): DpOffsetSubject {
+ return assertAbout(DpOffsetSubject.dpOffsets()).that(dpOffset)
+}
+
+/** A Truth subject to assert on [DpOffset] with some tolerance. Inspired by FloatSubject. */
+class DpOffsetSubject(
+ metadata: FailureMetadata,
+ private val actual: DpOffset,
+) : Subject(metadata, actual) {
+ fun isWithin(tolerance: Dp): TolerantDpOffsetComparison {
+ return object : TolerantDpOffsetComparison {
+ override fun of(expected: DpOffset) {
+ actual.x.assertIsEqualTo(expected.x, "offset.x", tolerance)
+ actual.y.assertIsEqualTo(expected.y, "offset.y", tolerance)
+ }
+ }
+ }
+
+ interface TolerantDpOffsetComparison {
+ fun of(expected: DpOffset)
+ }
+
+ companion object {
+ val DefaultTolerance = Dp(.5f)
+
+ fun dpOffsets() =
+ Factory<DpOffsetSubject, DpOffset> { metadata, actual ->
+ DpOffsetSubject(metadata, actual)
+ }
+ }
+}
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 82fe3f2..609ea90 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.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 @@
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 7926f92..0ee88b9 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.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.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 @@
}
}
- 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 b3d2e35..64227b8 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
@@ -43,10 +43,10 @@
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import com.android.compose.animation.Easings
+import com.android.compose.modifiers.thenIf
import com.android.internal.R
import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel
-import com.android.systemui.compose.modifiers.thenIf
import kotlin.math.min
import kotlin.math.pow
import kotlin.math.sqrt
@@ -228,43 +228,45 @@
}
}
) {
- // 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 85178bc..bef0b3d 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
@@ -75,7 +75,7 @@
import androidx.compose.ui.unit.dp
import com.android.compose.animation.Easings
import com.android.compose.grid.VerticalGrid
-import com.android.internal.R.id.image
+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
@@ -83,7 +83,6 @@
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.compose.Icon
-import com.android.systemui.compose.modifiers.thenIf
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit
import kotlinx.coroutines.async
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 99fe26c..0000000
--- 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 cfcc2fb..0000000
--- 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/res/color/qs_dialog_btn_filled_background.xml b/packages/SystemUI/res/color/qs_dialog_btn_filled_background.xml
new file mode 100644
index 0000000..40bab5e
--- /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 0000000..e76ad99
--- /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/media_output_icon_volume.xml b/packages/SystemUI/res/drawable/media_output_icon_volume.xml
index fce4e00..85d608f 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 b937937..a877900 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/qs_dialog_btn_filled.xml b/packages/SystemUI/res/drawable/qs_dialog_btn_filled.xml
index c4e45bf..9bc8b53 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/values-ldrtl/dimens.xml b/packages/SystemUI/res/values-ldrtl/dimens.xml
new file mode 100644
index 0000000..0d99b61
--- /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/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 3366f4f..0e88c31 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1346,6 +1346,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/strings.xml b/packages/SystemUI/res/values/strings.xml
index f8c13b0..c306a79 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>
diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml
index 31f40e9..1c46a91 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>
diff --git a/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt b/packages/SystemUI/src/com/android/keyguard/FaceAuthReason.kt
index d8085b9..22cdb30 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.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 @@
NOTIFICATION_PANEL_CLICKED,
QS_EXPANDED,
PICK_UP_GESTURE_TRIGGERED,
+ ACCESSIBILITY_ACTION,
)
annotation class FaceAuthApiRequestReason {
companion object {
@@ -80,6 +82,7 @@
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 @@
@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 @@
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 f952337..bc24249 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.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 @@
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 6bd9df0..5833d98 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -3158,6 +3158,10 @@
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/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
index 2f6a68c..03ad132 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.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 @@
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 @@
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 @@
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/fontscaling/FontScalingDialog.kt b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialog.kt
index 783460c..0ef256d 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 @@
*/
@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 @@
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 a977966..deb3d03 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.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.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 @@
}
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 @@
)
}
}
+
+ /**
+ * 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 b482977..d4371bf 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 @@
}
.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 0000000..b9fa240
--- /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 083e21f..37ce444 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 @@
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.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.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 @@
@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 @@
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 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 efbde4c..efc92ad 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 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 @@
*/
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 @@
@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 @@
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 37f39cb..aa85e5f3 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 @@
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 0409519..0000000
--- 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 e938b4e..0000000
--- 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 1b14acc..38fb8b9 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
@@ -60,7 +60,9 @@
}
.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,
)
diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt
index b15c60e..85f31e5 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.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 @@
): 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/dreams/touch/ShadeTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/ShadeTouchHandler.java
index 99451f2..6f05e83 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 @@
*/
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 @@
}
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 efa5981..e6820a3 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 @@
// 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 @@
// 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 @@
// 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 = false
+ )
// 200 - keyguard/lockscreen
// ** Flag retired **
@@ -133,16 +128,17 @@
// 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 @@
* 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 @@
@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 @@
/** 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.
@@ -239,27 +223,21 @@
/** 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 +246,29 @@
/** 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 +287,7 @@
// 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 +327,7 @@
/** 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 +336,11 @@
// 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 +408,8 @@
// 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 +457,7 @@
@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 +495,8 @@
// 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 +511,6 @@
val ENABLE_PIP2_IMPLEMENTATION =
sysPropBooleanFlag(1119, "persist.wm.debug.enable_pip2_implementation", default = false)
-
// 1200 - predictive back
@Keep
@JvmField
@@ -573,8 +536,7 @@
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 +559,7 @@
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 +584,12 @@
// 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 +599,15 @@
// 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 +618,9 @@
// 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 +632,10 @@
// 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 +658,21 @@
@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 +688,20 @@
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 = false)
// 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 +714,5 @@
// 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 e0834bb..82d0c8b 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 @@
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 @@
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 @@
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 @@
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 @@
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/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepository.kt
index d1f011e..bf1e75b 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 @@
/** 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 @@
}
?: flowOf(false)
+ override fun lockoutFaceAuth() {
+ _isLockedOut.value = true
+ }
+
private val faceLockoutResetCallback =
object : FaceManager.LockoutResetCallback() {
override fun onLockoutReset(sensorId: Int) {
@@ -396,7 +403,7 @@
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 482e9a3d..6a2511f 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 @@
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 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 @@
* 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 @@
/** 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 27e3a74..5ef9a9e 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 @@
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 141b130..e57c919 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 @@
fun onNotificationPanelClicked()
fun onSwipeUpOnBouncer()
fun onPrimaryBouncerUserInput()
+ fun onAccessibilityAction()
}
/**
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 833eda7..4244e55 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 @@
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 @@
* 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 10dd900..596a1c0 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 @@
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 6e7a20b..8f4776f 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.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 @@
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 @@
)
}
.launchIn(applicationScope)
+
+ deviceEntryFingerprintAuthRepository.isLockedOut
+ .onEach {
+ if (it) {
+ faceAuthenticationLogger.faceLockedOut("Fingerprint locked out")
+ repository.lockoutFaceAuth()
+ }
+ }
+ .launchIn(applicationScope)
}
override fun onSwipeUpOnBouncer() {
@@ -143,6 +154,10 @@
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 d9792cf..3de3666 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 @@
* 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 @@
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 cfd9e08..62f43ed 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 @@
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 389cf76..f1ceaaa 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.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 a46d441..82f40bf 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 @@
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 373f705..66067b1 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.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 @@
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/media/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
index f6a2f37..72352e3 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
@@ -330,6 +330,8 @@
// Don't send cancel if the user has moved on to the next activity.
if (!mUserSelectingTask) {
finish(RECORD_CANCEL, /* projection= */ null);
+ } else {
+ super.finish();
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt b/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt
new file mode 100644
index 0000000..c74a71c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/model/SysUiStateExt.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+package com.android.systemui.model
+
+import com.android.systemui.dagger.qualifiers.DisplayId
+
+/**
+ * In-bulk updates multiple flag values and commits the update.
+ *
+ * Example:
+ * ```
+ * sysuiState.updateFlags(
+ * displayId,
+ * SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE to (sceneKey != SceneKey.Gone),
+ * SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED to (sceneKey == SceneKey.Shade),
+ * SYSUI_STATE_QUICK_SETTINGS_EXPANDED to (sceneKey == SceneKey.QuickSettings),
+ * SYSUI_STATE_BOUNCER_SHOWING to (sceneKey == SceneKey.Bouncer),
+ * SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to (sceneKey == SceneKey.Lockscreen),
+ * )
+ * ```
+ *
+ * You can inject [displayId] by injecting it using:
+ * ```
+ * @DisplayId private val displayId: Int`,
+ * ```
+ */
+fun SysUiState.updateFlags(
+ @DisplayId displayId: Int,
+ vararg flagValuePairs: Pair<Int, Boolean>,
+) {
+ flagValuePairs.forEach { (flag, enabled) -> setFlag(flag, enabled) }
+ commitUpdate(displayId)
+}
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 c48028c..0000000
--- 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 86f0c0d..0000000
--- 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 1172030..0000000
--- 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 ebb8639..0000000
--- 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 1894bc4..0000000
--- 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 c2eaf72..0000000
--- 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 ee1dd65..0000000
--- 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 a4cd35c..0000000
--- 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 9e02657..0000000
--- 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/ui/view/MultiShadeView.kt b/packages/SystemUI/src/com/android/systemui/multishade/ui/view/MultiShadeView.kt
deleted file mode 100644
index aecec39..0000000
--- 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 ed92c54..0000000
--- 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 e828dbd..0000000
--- 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 682335e..e134f7c 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.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 @@
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 @@
@Inject
NavigationBar(
NavigationBarView navigationBarView,
+ ShadeController shadeController,
NavigationBarFrame navigationBarFrame,
@Nullable Bundle savedState,
@DisplayId Context context,
@@ -541,7 +544,7 @@
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 @@
mSysUiFlagsContainer = sysUiFlagsContainer;
mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
mShadeController = shadeController;
+ mShadeViewController = shadeViewController;
mNotificationRemoteInputManager = notificationRemoteInputManager;
mOverviewProxyService = overviewProxyService;
mNavigationModeController = navigationModeController;
@@ -739,8 +743,7 @@
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 @@
}
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/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 207cc139..a737a8b 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -78,7 +78,6 @@
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;
@@ -143,6 +142,7 @@
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;
@@ -201,11 +201,7 @@
// 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();
@@ -552,8 +548,10 @@
ShellInterface shellInterface,
Lazy<NavigationBarController> navBarControllerLazy,
Lazy<Optional<CentralSurfaces>> centralSurfacesOptionalLazy,
+ Lazy<ShadeViewController> shadeViewControllerLazy,
NavigationModeController navModeController,
- NotificationShadeWindowController statusBarWinController, SysUiState sysUiState,
+ NotificationShadeWindowController statusBarWinController,
+ SysUiState sysUiState,
UserTracker userTracker,
ScreenLifecycle screenLifecycle,
WakefulnessLifecycle wakefulnessLifecycle,
@@ -573,6 +571,7 @@
mMainExecutor = mainExecutor;
mShellInterface = shellInterface;
mCentralSurfacesOptionalLazy = centralSurfacesOptionalLazy;
+ mShadeViewControllerLazy = shadeViewControllerLazy;
mHandler = new Handler();
mNavBarControllerLazy = navBarControllerLazy;
mStatusBarWinController = statusBarWinController;
@@ -677,13 +676,10 @@
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 +688,7 @@
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/startable/SystemUiDefaultSceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
index b23c4ec..92384d6 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
@@ -20,14 +20,22 @@
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.DisplayId
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.shared.model.WakefulnessState
+import com.android.systemui.model.SysUiState
+import com.android.systemui.model.updateFlags
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED
+import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -49,12 +57,15 @@
private val authenticationInteractor: AuthenticationInteractor,
private val keyguardInteractor: KeyguardInteractor,
private val featureFlags: FeatureFlags,
+ private val sysUiState: SysUiState,
+ @DisplayId private val displayId: Int,
) : CoreStartable {
override fun start() {
if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
hydrateVisibility()
automaticallySwitchScenes()
+ hydrateSystemUiState()
}
}
@@ -121,6 +132,27 @@
}
}
+ /** Keeps [SysUiState] up-to-date */
+ private fun hydrateSystemUiState() {
+ applicationScope.launch {
+ sceneInteractor
+ .currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT)
+ .map { it.key }
+ .distinctUntilChanged()
+ .collect { sceneKey ->
+ sysUiState.updateFlags(
+ displayId,
+ SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE to (sceneKey != SceneKey.Gone),
+ SYSUI_STATE_NOTIFICATION_PANEL_EXPANDED to (sceneKey == SceneKey.Shade),
+ SYSUI_STATE_QUICK_SETTINGS_EXPANDED to (sceneKey == SceneKey.QuickSettings),
+ SYSUI_STATE_BOUNCER_SHOWING to (sceneKey == SceneKey.Bouncer),
+ SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to
+ (sceneKey == SceneKey.Lockscreen),
+ )
+ }
+ }
+ }
+
private fun switchToScene(targetSceneKey: SceneKey) {
sceneInteractor.setCurrentScene(
containerName = CONTAINER_NAME,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 202d6e6..e428976 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.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 @@
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 @@
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 @@
LockscreenToOccludedTransitionViewModel lockscreenToOccludedTransitionViewModel,
@Main CoroutineDispatcher mainDispatcher,
KeyguardTransitionInteractor keyguardTransitionInteractor,
- Provider<MultiShadeInteractor> multiShadeInteractorProvider,
DumpManager dumpManager,
KeyguardLongPressViewModel keyguardLongPressViewModel,
KeyguardInteractor keyguardInteractor,
@@ -839,8 +835,6 @@
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 @@
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 108ea68..6afed1d 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.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.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.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.function.Consumer;
import javax.inject.Inject;
-import javax.inject.Provider;
/**
* Controller for {@link NotificationShadeWindowView}.
@@ -135,7 +127,6 @@
step.getTransitionState() == TransitionState.RUNNING;
};
private final SystemClock mClock;
- private final @Nullable MultiShadeMotionEventInteractor mMultiShadeMotionEventInteractor;
@Inject
public NotificationShadeWindowViewController(
@@ -167,9 +158,7 @@
KeyguardTransitionInteractor keyguardTransitionInteractor,
PrimaryBouncerToGoneTransitionViewModel primaryBouncerToGoneTransitionViewModel,
FeatureFlags featureFlags,
- Provider<MultiShadeInteractor> multiShadeInteractorProvider,
SystemClock clock,
- Provider<MultiShadeMotionEventInteractor> multiShadeMotionEventInteractorProvider,
BouncerMessageInteractor bouncerMessageInteractor,
BouncerLogger bouncerLogger) {
mLockscreenShadeTransitionController = transitionController;
@@ -219,17 +208,6 @@
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 @@
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 @@
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 ee4e98e..fe4832f0 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.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 @@
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 @@
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/VibratorHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
index 324e972..645595c 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.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
+import android.view.View;
import androidx.annotation.VisibleForTesting;
@@ -151,4 +152,20 @@
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/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 42b99a1..ed489a6c 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 @@
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/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt
index 26b51a9..dcd18dd 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.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 @@
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 @@
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 dc021fe..661d71d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -160,7 +160,6 @@
private KeyguardViewController mKeyguardViewController;
private DozeScrimController mDozeScrimController;
private KeyguardViewMediator mKeyguardViewMediator;
- private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
private PendingAuthenticated mPendingAuthenticated = null;
private boolean mHasScreenTurnedOnSinceAuthenticating;
private boolean mFadedAwayAfterWakeAndUnlock;
@@ -281,8 +280,7 @@
LatencyTracker latencyTracker,
ScreenOffAnimationController screenOffAnimationController,
VibratorHelper vibrator,
- SystemClock systemClock,
- StatusBarKeyguardViewManager statusBarKeyguardViewManager
+ SystemClock systemClock
) {
mPowerManager = powerManager;
mUpdateMonitor = keyguardUpdateMonitor;
@@ -310,7 +308,6 @@
mVibratorHelper = vibrator;
mLogger = biometricUnlockLogger;
mSystemClock = systemClock;
- mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
dumpManager.registerDumpable(this);
}
@@ -452,19 +449,8 @@
// 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 +462,10 @@
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 +487,6 @@
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) {
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 acd6e49..5c28be3 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.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 @@
@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 c3c9a61..6eeb25f 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 @@
}
});
- mScreenOffAnimationController.initialize(this, mLightRevealScrim);
+ mScreenOffAnimationController.initialize(this, mShadeSurface, mLightRevealScrim);
updateLightRevealScrimVisibility();
mShadeSurface.initDependencies(
@@ -1677,8 +1677,7 @@
Trace.endSection();
}
- @Override
- public ShadeViewController getShadeViewController() {
+ protected ShadeViewController getShadeViewController() {
return mShadeSurface;
}
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 c817466..89c3a02 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 @@
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 @@
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 @@
}
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/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt
index 96a4d90..7e9172d 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.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 @@
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 @@
lightRevealAnimator.start()
}
- val animatorDurationScaleObserver = object : ContentObserver(null) {
+ private val animatorDurationScaleObserver = object : ContentObserver(null) {
override fun onChange(selfChange: Boolean) {
updateAnimatorDurationScale()
}
@@ -134,11 +136,13 @@
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(
@@ -198,7 +202,7 @@
// 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 @@
// 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 @@
// 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 @@
// 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/unfold/FoldAodAnimationController.kt b/packages/SystemUI/src/com/android/systemui/unfold/FoldAodAnimationController.kt
index cbe4020..098d51e 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.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 @@
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 @@
)
}
- 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 @@
}
private fun getShadeFoldAnimator(): ShadeFoldAnimator =
- centralSurfaces.shadeViewController.shadeFoldAnimator
+ shadeViewController.shadeFoldAnimator
private fun setAnimationState(playing: Boolean) {
shouldPlayAnimation = playing
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index d447174..3abae6b 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.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 @@
@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 @@
mock(),
{ JavaAdapter(sceneTestUtils.testScope.backgroundScope) },
userInteractor,
+ faceAuthAccessibilityDelegate,
) {
sceneInteractor
}
@@ -244,6 +247,11 @@
}
@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 025c88c..576f689 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.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 @@
@Mock
private ShadeController mShadeController;
@Mock
+ private ShadeViewController mShadeViewController;
+ @Mock
private Lazy<Optional<CentralSurfaces>> mCentralSurfacesOptionalLazy;
@Mock
private Optional<Recents> mRecentsOptional;
@@ -82,7 +85,8 @@
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/fontscaling/FontScalingDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogTest.kt
index d5e6881..7b99314 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 @@
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 @@
@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 @@
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 0000000..0056970
--- /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 0000000..ec17794
--- /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 ecc776b..3169b09 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.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.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 @@
@SmallTest
@RoboPilotTest
@RunWith(AndroidJUnit4::class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@TestableLooper.RunWithLooper
class SideFpsControllerTest : SysuiTestCase() {
@JvmField @Rule var rule = MockitoJUnit.rule()
@@ -120,8 +118,6 @@
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 @@
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 @@
executor,
handler,
alternateBouncerInteractor,
- { sideFpsOverlayViewModel },
TestCoroutineScope(),
dumpManager
)
@@ -697,6 +683,106 @@
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 @@
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 ea25615..239e317 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 @@
@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 @@
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 896f9b11..fd96cf4 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.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 @@
}
@Test
- fun testGetOverlayoffsets() =
+ fun testGetOverlayOffsets() =
testScope.runTest {
- // Arrange.
fingerprintRepository.setProperties(
sensorId = 1,
strength = SensorStrength.STRONG,
@@ -78,33 +76,16 @@
)
)
- // 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 a859321..0000000
--- 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/dreams/touch/ShadeTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/touch/ShadeTouchHandlerTest.java
index 872c079..2b98214 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 @@
@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 00f67a3..1f66e5b 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.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 @@
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 @@
}
@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/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthRepositoryTest.kt
index 8127ac6..b3f8000 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 @@
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 @@
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 @@
}
@Test
- fun authenticateDoesNotRunWhenFpIsLockedOut() =
- testScope.runTest {
- testGatingCheckForFaceAuth { deviceEntryFingerprintAuthRepository.setLockedOut(true) }
- }
+ fun authenticateDoesNotRunWhenFaceIsDisabled() =
+ testScope.runTest { testGatingCheckForFaceAuth { underTest.lockoutFaceAuth() } }
@Test
fun authenticateDoesNotRunWhenUserIsCurrentlyTrusted() =
@@ -858,6 +859,19 @@
}
@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 @@
}
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 @@
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 f974577..ec30732 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 @@
}
@Test
- fun nextRevealEffect_effectSwitchesBetweenDefaultAndBiometricWithNoDupes() =
- runTest {
- val values = mutableListOf<LightRevealEffect>()
- val job = launch { underTest.revealEffect.collect { values.add(it) } }
+ 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 },
+ 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)
+ // 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)
- // Non-biometric unlock, we should return to the default reveal.
- fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE)
+ // 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 },
- {
- it is CircleReveal &&
- it.centerX == fingerprintLocation.x &&
- it.centerY == fingerprintLocation.y
- },
- { 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)
- // 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()
+ // Non-biometric unlock, we should return to the default reveal.
+ fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.NONE)
- 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
- },
- )
+ runCurrent()
+ values.assertEffectsMatchPredicates(
+ { it == DEFAULT_REVEAL_EFFECT },
+ {
+ it is CircleReveal &&
+ it.centerX == fingerprintLocation.x &&
+ it.centerY == fingerprintLocation.y
+ },
+ { it == DEFAULT_REVEAL_EFFECT },
+ )
- job.cancel()
+ // 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()
+ }
+
+ @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 ced0a21..8c9ed5b 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.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 @@
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 @@
)
.keyguardTransitionInteractor
+ fakeDeviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository()
underTest =
SystemUIKeyguardFaceAuthInteractor(
mContext,
@@ -127,6 +131,7 @@
featureFlags,
FaceAuthenticationLogger(logcatLogBuffer("faceAuthBuffer")),
keyguardUpdateMonitor,
+ fakeDeviceEntryFingerprintAuthRepository
)
}
@@ -335,4 +340,14 @@
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 6e7ba6d..906d948 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.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 @@
MockitoAnnotations.initMocks(this)
underTest =
LightRevealScrimInteractor(
- fakeKeyguardTransitionRepository,
keyguardTransitionInteractor,
- fakeLightRevealScrimRepository
+ fakeLightRevealScrimRepository,
+ testScope.backgroundScope
)
}
@@ -110,52 +120,36 @@
}
@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(
+ transitionState = TransitionState.STARTED,
+ from = KeyguardState.DOZING,
+ to = KeyguardState.LOCKSCREEN
+ )
+ )
+ runCurrent()
+ verify(fakeLightRevealScrimRepository).startRevealAmountAnimator(true)
+
+ fakeKeyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ transitionState = TransitionState.STARTED,
from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.AOD,
- value = 0.3f
+ to = KeyguardState.DOZING
)
)
-
- 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>())
-
- fakeKeyguardTransitionRepository.sendTransitionStep(
- TransitionStep(from = KeyguardState.AOD, to = KeyguardState.DOZING, value = 0.3f)
- )
-
- assertEquals(values, emptyList<Float>())
-
- job.cancel()
+ runCurrent()
+ verify(fakeLightRevealScrimRepository).startRevealAmountAnimator(false)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt b/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt
new file mode 100644
index 0000000..f5a70f0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/model/SysUiStateExtTest.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+package com.android.systemui.model
+
+import android.view.Display
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.FakeDisplayTracker
+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 SysUiStateExtTest : SysuiTestCase() {
+
+ private val underTest = SysUiState(FakeDisplayTracker(context))
+
+ @Test
+ fun updateFlags() {
+ underTest.updateFlags(
+ Display.DEFAULT_DISPLAY,
+ 1 to true,
+ 2 to false,
+ 3 to true,
+ )
+
+ assertThat(underTest.flags and 1).isNotEqualTo(0)
+ assertThat(underTest.flags and 2).isEqualTo(0)
+ assertThat(underTest.flags and 3).isNotEqualTo(0)
+ }
+}
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 ceacaf9..0000000
--- 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 bcc99bc..0000000
--- 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 5890cbd..0000000
--- 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 8935309..0000000
--- 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 0484515..0000000
--- 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 e32aac5..0000000
--- 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 25d494c..cbfad56 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.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 @@
when(deviceProvisionedController.isDeviceProvisioned()).thenReturn(true);
return spy(new NavigationBar(
mNavigationBarView,
+ mock(ShadeController.class),
mNavigationBarFrame,
null,
context,
@@ -485,7 +487,7 @@
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/qs/tiles/dialog/InternetDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java
index 5b30687..3e20511 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 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 java.util.ArrayList;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
@SmallTest
@@ -182,6 +184,8 @@
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 @@
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 355c4b6..6bb13ea 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
@@ -38,6 +38,7 @@
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
@@ -93,6 +94,7 @@
@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
@@ -132,6 +134,7 @@
shellInterface,
Lazy { navBarController },
Lazy { Optional.of(centralSurfaces) },
+ Lazy { shadeViewController },
navModeController,
statusBarWinController,
sysUiState,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
index 5638d70..6f6c5a5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
@@ -14,8 +14,11 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
package com.android.systemui.scene.domain.startable
+import android.view.Display
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
@@ -23,17 +26,24 @@
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.model.SysUiState
import com.android.systemui.scene.SceneTestUtils
import com.android.systemui.scene.shared.model.SceneContainerNames
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
+import com.android.systemui.util.mockito.mock
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
@SmallTest
@RunWith(JUnit4::class)
@@ -53,6 +63,7 @@
utils.keyguardInteractor(
repository = keyguardRepository,
)
+ private val sysUiState: SysUiState = mock()
private val underTest =
SystemUiDefaultSceneContainerStartable(
@@ -61,6 +72,8 @@
authenticationInteractor = authenticationInteractor,
keyguardInteractor = keyguardInteractor,
featureFlags = featureFlags,
+ sysUiState = sysUiState,
+ displayId = Display.DEFAULT_DISPLAY,
)
@Before
@@ -375,6 +388,31 @@
assertThat(currentSceneKey).isEqualTo(SceneKey.Shade)
}
+ @Test
+ fun hydrateSystemUiState() =
+ testScope.runTest {
+ underTest.start()
+ runCurrent()
+ clearInvocations(sysUiState)
+
+ listOf(
+ SceneKey.Gone,
+ SceneKey.Lockscreen,
+ SceneKey.Bouncer,
+ SceneKey.Shade,
+ SceneKey.QuickSettings,
+ )
+ .forEachIndexed { index, sceneKey ->
+ sceneInteractor.setCurrentScene(
+ SceneContainerNames.SYSTEM_UI_DEFAULT,
+ SceneModel(sceneKey),
+ )
+ runCurrent()
+
+ verify(sysUiState, times(index + 1)).commitUpdate(Display.DEFAULT_DISPLAY)
+ }
+ }
+
private fun prepareState(
isFeatureEnabled: Boolean = true,
isDeviceUnlocked: Boolean = false,
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 ecd3308..202c7bf 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.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 @@
@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 @@
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 2a398c55..893123d 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.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.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.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 @@
@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 @@
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 @@
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 d9eb9b9..ed4ac35c 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.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.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.verify
import org.mockito.MockitoAnnotations
-@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidTestingRunner::class)
@RunWithLooper(setAsMainLooper = true)
@SmallTest
@@ -160,22 +152,10 @@
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 @@
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 77a22ac..29bc64e 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.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 @@
@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 @@
ambientDisplayConfiguration,
statusBarStateController,
shadeLogger,
+ dozeInteractor,
userTracker,
tunerService,
dumpManager
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 ad908e7..aab4bc3 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.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 @@
@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 @@
}
@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/notification/row/FeedbackInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/FeedbackInfoTest.java
index 6d687a6..cf1d2ca 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.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.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 @@
@Mock
private NotificationGutsManager mNotificationGutsManager;
+ private Configuration mConfig;
+
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
@@ -111,7 +117,15 @@
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/phone/ActivityStarterImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt
index 5e0e140..68f2728 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.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 @@
@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 @@
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 479803e..4f8de3e 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
@@ -142,8 +142,7 @@
mNotificationMediaManager, mWakefulnessLifecycle, mScreenLifecycle,
mAuthController, mStatusBarStateController,
mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper,
- mSystemClock,
- mStatusBarKeyguardViewManager
+ mSystemClock
);
mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
mBiometricUnlockController.addListener(mBiometricUnlockEventsListener);
@@ -465,69 +464,6 @@
}
@Test
- public void onSideFingerprintSuccess_dreaming_unlockThenWake() {
- 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();
-
- // 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());
- }
-
- @Test
- public void onSideFingerprintSuccess_dreaming_unlockIfStillLockedNotDreaming() {
- 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());
-
- // Verify that the power manager was not told to wake up.
- verify(mPowerManager, never()).wakeUp(anyLong(), anyInt(), anyString());
-
- dismissKeyguardRunnableCaptor.getValue().run();
- // Verify that the keyguard controller is told to unlock.
- verify(mStatusBarKeyguardViewManager).notifyKeyguardAuthenticated(eq(false));
- }
-
-
- @Test
public void onSideFingerprintSuccess_oldPowerButtonPress_playHaptic() {
// GIVEN side fingerprint enrolled, last wake reason was power button
when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
@@ -601,14 +537,25 @@
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 c8ec1bf..28193db 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
@@ -140,8 +140,6 @@
@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 +150,6 @@
@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 +161,6 @@
@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 +172,6 @@
@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)
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 2e9a690..e76f26d 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 @@
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/unfold/FoldAodAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
index 813597a..7f990a4 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 @@
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 @@
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 0c5e438..2362a52 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 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 @@
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 @@
_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 548169e..f4c2db1 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 @@
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 @@
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 7c22604..b24b95e 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 @@
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/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
index 3e7d759..631b453 100644
--- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
@@ -47,7 +47,6 @@
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.PackageManagerInternal;
-import android.content.pm.Signature;
import android.net.MacAddress;
import android.os.Binder;
import android.os.Bundle;
@@ -55,17 +54,11 @@
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.UserHandle;
-import android.util.Log;
-import android.util.PackageUtils;
import android.util.Slog;
import com.android.internal.R;
-import com.android.internal.util.ArrayUtils;
-import java.util.Arrays;
-import java.util.HashSet;
import java.util.List;
-import java.util.Set;
/**
* Class responsible for handling incoming {@link AssociationRequest}s.
@@ -449,31 +442,6 @@
};
private boolean mayAssociateWithoutPrompt(@NonNull String packageName, @UserIdInt int userId) {
- // Below we check if the requesting package is allowlisted (usually by the OEM) for creating
- // CDM associations without user confirmation (prompt).
- // For this we'll check to config arrays:
- // - com.android.internal.R.array.config_companionDevicePackages
- // and
- // - com.android.internal.R.array.config_companionDeviceCerts.
- // Both arrays are expected to contain similar number of entries.
- // config_companionDevicePackages contains package names of the allowlisted packages.
- // config_companionDeviceCerts contains SHA256 digests of the signatures of the
- // corresponding packages.
- // If a package may be signed with one of several certificates, its package name would
- // appear multiple times in the config_companionDevicePackages, with different entries
- // (one for each of the valid signing certificates) at the corresponding positions in
- // config_companionDeviceCerts.
- final String[] allowlistedPackages = mContext.getResources()
- .getStringArray(com.android.internal.R.array.config_companionDevicePackages);
- if (!ArrayUtils.contains(allowlistedPackages, packageName)) {
- if (DEBUG) {
- Log.d(TAG, packageName + " is not allowlisted for creating associations "
- + "without user confirmation (prompt)");
- Log.v(TAG, "Allowlisted packages=" + Arrays.toString(allowlistedPackages));
- }
- return false;
- }
-
// Throttle frequent associations
final long now = System.currentTimeMillis();
final List<AssociationInfo> associationForPackage =
@@ -493,40 +461,6 @@
}
}
- final String[] allowlistedPackagesSignatureDigests = mContext.getResources()
- .getStringArray(com.android.internal.R.array.config_companionDeviceCerts);
- final Set<String> allowlistedSignatureDigestsForRequestingPackage = new HashSet<>();
- for (int i = 0; i < allowlistedPackages.length; i++) {
- if (allowlistedPackages[i].equals(packageName)) {
- final String digest = allowlistedPackagesSignatureDigests[i].replaceAll(":", "");
- allowlistedSignatureDigestsForRequestingPackage.add(digest);
- }
- }
-
- final Signature[] requestingPackageSignatures = mPackageManager.getPackage(packageName)
- .getSigningDetails().getSignatures();
- final String[] requestingPackageSignatureDigests =
- PackageUtils.computeSignaturesSha256Digests(requestingPackageSignatures);
-
- boolean requestingPackageSignatureAllowlisted = false;
- for (String signatureDigest : requestingPackageSignatureDigests) {
- if (allowlistedSignatureDigestsForRequestingPackage.contains(signatureDigest)) {
- requestingPackageSignatureAllowlisted = true;
- break;
- }
- }
-
- if (!requestingPackageSignatureAllowlisted) {
- Slog.w(TAG, "Certificate mismatch for allowlisted package " + packageName);
- if (DEBUG) {
- Log.d(TAG, " > allowlisted signatures for " + packageName + ": ["
- + String.join(", ", allowlistedSignatureDigestsForRequestingPackage)
- + "]");
- Log.d(TAG, " > actual signatures for " + packageName + ": "
- + Arrays.toString(requestingPackageSignatureDigests));
- }
- }
-
- return requestingPackageSignatureAllowlisted;
+ return PackageUtils.isPackageAllowlisted(mContext, mPackageManager, packageName);
}
}
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 89e8a18..4d4328d 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -247,7 +247,8 @@
mCompanionAppController = new CompanionApplicationController(
context, mAssociationStore, mDevicePresenceMonitor);
mTransportManager = new CompanionTransportManager(context, mAssociationStore);
- mSystemDataTransferProcessor = new SystemDataTransferProcessor(this, mAssociationStore,
+ mSystemDataTransferProcessor = new SystemDataTransferProcessor(this,
+ mPackageManagerInternal, mAssociationStore,
mSystemDataTransferRequestStore, mTransportManager);
// TODO(b/279663946): move context sync to a dedicated system service
mCrossDeviceSyncController = new CrossDeviceSyncController(getContext(), mTransportManager);
diff --git a/services/companion/java/com/android/server/companion/PackageUtils.java b/services/companion/java/com/android/server/companion/PackageUtils.java
index 3ab4aa8..db40fc4 100644
--- a/services/companion/java/com/android/server/companion/PackageUtils.java
+++ b/services/companion/java/com/android/server/companion/PackageUtils.java
@@ -20,6 +20,7 @@
import static android.content.pm.PackageManager.GET_CONFIGURATIONS;
import static android.content.pm.PackageManager.GET_PERMISSIONS;
+import static com.android.server.companion.CompanionDeviceManagerService.DEBUG;
import static com.android.server.companion.CompanionDeviceManagerService.TAG;
import android.Manifest;
@@ -35,20 +36,28 @@
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.PackageInfoFlags;
import android.content.pm.PackageManager.ResolveInfoFlags;
+import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
+import android.content.pm.Signature;
import android.os.Binder;
+import android.util.Log;
import android.util.Slog;
+import com.android.internal.util.ArrayUtils;
+
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* Utility methods for working with {@link PackageInfo}-s.
*/
-final class PackageUtils {
+public final class PackageUtils {
private static final Intent COMPANION_SERVICE_INTENT =
new Intent(CompanionDeviceService.SERVICE_INTERFACE);
private static final String PROPERTY_PRIMARY_TAG =
@@ -141,4 +150,69 @@
return false;
}
}
+
+ /**
+ * Check if the package is allowlisted in the overlay config.
+ * For this we'll check to config arrays:
+ * - com.android.internal.R.array.config_companionDevicePackages
+ * and
+ * - com.android.internal.R.array.config_companionDeviceCerts.
+ * Both arrays are expected to contain similar number of entries.
+ * config_companionDevicePackages contains package names of the allowlisted packages.
+ * config_companionDeviceCerts contains SHA256 digests of the signatures of the
+ * corresponding packages.
+ * If a package is signed with one of several certificates, its package name would
+ * appear multiple times in the config_companionDevicePackages, with different entries
+ * (one for each of the valid signing certificates) at the corresponding positions in
+ * config_companionDeviceCerts.
+ */
+ public static boolean isPackageAllowlisted(Context context,
+ PackageManagerInternal packageManagerInternal, @NonNull String packageName) {
+ final String[] allowlistedPackages = context.getResources()
+ .getStringArray(com.android.internal.R.array.config_companionDevicePackages);
+ if (!ArrayUtils.contains(allowlistedPackages, packageName)) {
+ if (DEBUG) {
+ Log.d(TAG, packageName + " is not allowlisted.");
+ }
+ return false;
+ }
+
+ final String[] allowlistedPackagesSignatureDigests = context.getResources()
+ .getStringArray(com.android.internal.R.array.config_companionDeviceCerts);
+ final Set<String> allowlistedSignatureDigestsForRequestingPackage = new HashSet<>();
+ for (int i = 0; i < allowlistedPackages.length; i++) {
+ if (allowlistedPackages[i].equals(packageName)) {
+ final String digest = allowlistedPackagesSignatureDigests[i].replaceAll(":", "");
+ allowlistedSignatureDigestsForRequestingPackage.add(digest);
+ }
+ }
+
+ final Signature[] requestingPackageSignatures = packageManagerInternal.getPackage(
+ packageName)
+ .getSigningDetails().getSignatures();
+ final String[] requestingPackageSignatureDigests =
+ android.util.PackageUtils.computeSignaturesSha256Digests(
+ requestingPackageSignatures);
+
+ boolean requestingPackageSignatureAllowlisted = false;
+ for (String signatureDigest : requestingPackageSignatureDigests) {
+ if (allowlistedSignatureDigestsForRequestingPackage.contains(signatureDigest)) {
+ requestingPackageSignatureAllowlisted = true;
+ break;
+ }
+ }
+
+ if (!requestingPackageSignatureAllowlisted) {
+ Slog.w(TAG, "Certificate mismatch for allowlisted package " + packageName);
+ if (DEBUG) {
+ Log.d(TAG, " > allowlisted signatures for " + packageName + ": ["
+ + String.join(", ", allowlistedSignatureDigestsForRequestingPackage)
+ + "]");
+ Log.d(TAG, " > actual signatures for " + packageName + ": "
+ + Arrays.toString(requestingPackageSignatureDigests));
+ }
+ }
+
+ return requestingPackageSignatureAllowlisted;
+ }
}
diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
index 82387a1..7af4957 100644
--- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
+++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
@@ -37,6 +37,7 @@
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManagerInternal;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
@@ -50,6 +51,7 @@
import com.android.internal.R;
import com.android.server.companion.AssociationStore;
import com.android.server.companion.CompanionDeviceManagerService;
+import com.android.server.companion.PackageUtils;
import com.android.server.companion.PermissionsUtils;
import com.android.server.companion.transport.CompanionTransportManager;
@@ -77,6 +79,7 @@
"system_data_transfer_result_receiver";
private final Context mContext;
+ private final PackageManagerInternal mPackageManager;
private final AssociationStore mAssociationStore;
private final SystemDataTransferRequestStore mSystemDataTransferRequestStore;
private final CompanionTransportManager mTransportManager;
@@ -85,10 +88,12 @@
private final ComponentName mCompanionDeviceDataTransferActivity;
public SystemDataTransferProcessor(CompanionDeviceManagerService service,
+ PackageManagerInternal packageManager,
AssociationStore associationStore,
SystemDataTransferRequestStore systemDataTransferRequestStore,
CompanionTransportManager transportManager) {
mContext = service.getContext();
+ mPackageManager = packageManager;
mAssociationStore = associationStore;
mSystemDataTransferRequestStore = systemDataTransferRequestStore;
mTransportManager = transportManager;
@@ -132,6 +137,11 @@
*/
public PendingIntent buildPermissionTransferUserConsentIntent(String packageName,
@UserIdInt int userId, int associationId) {
+ if (PackageUtils.isPackageAllowlisted(mContext, mPackageManager, packageName)) {
+ Slog.i(LOG_TAG, "User consent Intent should be skipped. Returning null.");
+ return null;
+ }
+
final AssociationInfo association = resolveAssociation(packageName, userId, associationId);
Slog.i(LOG_TAG, "Creating permission sync intent for userId [" + userId
@@ -175,23 +185,29 @@
final AssociationInfo association = resolveAssociation(packageName, userId, associationId);
// Check if the request has been consented by the user.
- List<SystemDataTransferRequest> storedRequests =
- mSystemDataTransferRequestStore.readRequestsByAssociationId(userId,
- associationId);
- boolean hasConsented = false;
- for (SystemDataTransferRequest storedRequest : storedRequests) {
- if (storedRequest instanceof PermissionSyncRequest && storedRequest.isUserConsented()) {
- hasConsented = true;
- break;
+ if (PackageUtils.isPackageAllowlisted(mContext, mPackageManager, packageName)) {
+ Slog.i(LOG_TAG, "Skip user consent check due to the same OEM package.");
+ } else {
+ List<SystemDataTransferRequest> storedRequests =
+ mSystemDataTransferRequestStore.readRequestsByAssociationId(userId,
+ associationId);
+ boolean hasConsented = false;
+ for (SystemDataTransferRequest storedRequest : storedRequests) {
+ if (storedRequest instanceof PermissionSyncRequest
+ && storedRequest.isUserConsented()) {
+ hasConsented = true;
+ break;
+ }
}
- }
- if (!hasConsented) {
- String message = "User " + userId + " hasn't consented permission sync.";
- Slog.e(LOG_TAG, message);
- try {
- callback.onError(message);
- } catch (RemoteException ignored) { }
- return;
+ if (!hasConsented) {
+ String message = "User " + userId + " hasn't consented permission sync.";
+ Slog.e(LOG_TAG, message);
+ try {
+ callback.onError(message);
+ } catch (RemoteException ignored) {
+ }
+ return;
+ }
}
// Start permission sync
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 279981b..d862a54 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/core/java/com/android/server/PersistentDataBlockService.java b/services/core/java/com/android/server/PersistentDataBlockService.java
index 6fd6afe..754a7ed 100644
--- a/services/core/java/com/android/server/PersistentDataBlockService.java
+++ b/services/core/java/com/android/server/PersistentDataBlockService.java
@@ -159,9 +159,10 @@
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 1e3a5f4..7a95d77 100644
--- a/services/core/java/com/android/server/TEST_MAPPING
+++ b/services/core/java/com/android/server/TEST_MAPPING
@@ -17,7 +17,7 @@
"name": "CtsWindowManagerDeviceTestCases",
"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 146215b..d398260 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 @@
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 @@
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 d199006..dc83125 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 @@
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 26bcb98..132d6a9 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 @@
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 4f5b5e1..786e1cc 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 @@
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 @@
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 72d3835..68062b5 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/display/TEST_MAPPING b/services/core/java/com/android/server/display/TEST_MAPPING
index 57c2e01..6f243e1 100644
--- a/services/core/java/com/android/server/display/TEST_MAPPING
+++ b/services/core/java/com/android/server/display/TEST_MAPPING
@@ -1,15 +1,7 @@
{
"presubmit": [
{
- "name": "FrameworksMockingServicesTests",
- "options": [
- {"include-filter": "com.android.server.display"},
- {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
- {"exclude-annotation": "androidx.test.filters.FlakyTest"}
- ]
- },
- {
- "name": "FrameworksServicesTests",
+ "name": "DisplayServiceTests",
"options": [
{"include-filter": "com.android.server.display"},
{"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
@@ -17,5 +9,14 @@
{"exclude-annotation": "org.junit.Ignore"}
]
}
+ ],
+ "postsubmit": [
+ {
+ "name": "DisplayServiceTests",
+ "options": [
+ {"include-filter": "com.android.server.display"},
+ {"exclude-annotation": "org.junit.Ignore"}
+ ]
+ }
]
-}
\ No newline at end of file
+}
diff --git a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
index a3bca9a..d4d1bae 100644
--- a/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
+++ b/services/core/java/com/android/server/display/brightness/BrightnessEvent.java
@@ -19,10 +19,12 @@
import android.hardware.display.BrightnessInfo;
import android.os.PowerManager;
import android.os.SystemClock;
-import android.util.TimeUtils;
import com.android.internal.annotations.VisibleForTesting;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
/**
* Represents a particular brightness change event.
*/
@@ -34,6 +36,8 @@
public static final int FLAG_IDLE_CURVE = 0x10;
public static final int FLAG_LOW_POWER_MODE = 0x20;
+ private static final SimpleDateFormat FORMAT = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
+
private BrightnessReason mReason = new BrightnessReason();
private int mDisplayId;
private String mPhysicalDisplayId;
@@ -169,7 +173,7 @@
* @return A stringified BrightnessEvent
*/
public String toString(boolean includeTime) {
- return (includeTime ? TimeUtils.formatForLogging(mTime) + " - " : "")
+ return (includeTime ? FORMAT.format(new Date(mTime)) + " - " : "")
+ "BrightnessEvent: "
+ "disp=" + mDisplayId
+ ", physDisp=" + mPhysicalDisplayId
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 9be86e8..e84be03 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -1020,6 +1020,7 @@
}
mAssistants.resetDefaultAssistantsIfNecessary();
+ mPreferencesHelper.syncChannelsBypassingDnd();
}
@VisibleForTesting
@@ -1859,6 +1860,7 @@
mConditionProviders.onUserSwitched(userId);
mListeners.onUserSwitched(userId);
mZenModeHelper.onUserSwitched(userId);
+ mPreferencesHelper.syncChannelsBypassingDnd();
}
// assistant is the only thing that cares about managed profiles specifically
mAssistants.onUserSwitched(userId);
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 1818d25..1bb1092 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -202,7 +202,7 @@
private SparseBooleanArray mLockScreenShowNotifications;
private SparseBooleanArray mLockScreenPrivateNotifications;
private boolean mIsMediaNotificationFilteringEnabled = DEFAULT_MEDIA_NOTIFICATION_FILTERING;
- private boolean mAreChannelsBypassingDnd;
+ private boolean mCurrentUserHasChannelsBypassingDnd;
private boolean mHideSilentStatusBarIcons = DEFAULT_HIDE_SILENT_STATUS_BAR_ICONS;
private boolean mShowReviewPermissionsNotification;
@@ -230,7 +230,6 @@
updateBadgingEnabled();
updateBubblesEnabled();
updateMediaNotificationFilteringEnabled();
- syncChannelsBypassingDnd(Process.SYSTEM_UID, true); // init comes from system
}
public void readXml(TypedXmlPullParser parser, boolean forRestore, int userId)
@@ -893,7 +892,7 @@
r.groups.put(group.getId(), group);
}
if (needsDndChange) {
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
}
@@ -972,7 +971,7 @@
existing.setBypassDnd(bypassDnd);
needsPolicyFileChange = true;
- if (bypassDnd != mAreChannelsBypassingDnd
+ if (bypassDnd != mCurrentUserHasChannelsBypassingDnd
|| previousExistingImportance != existing.getImportance()) {
needsDndChange = true;
}
@@ -1031,7 +1030,7 @@
}
r.channels.put(channel.getId(), channel);
- if (channel.canBypassDnd() != mAreChannelsBypassingDnd) {
+ if (channel.canBypassDnd() != mCurrentUserHasChannelsBypassingDnd) {
needsDndChange = true;
}
MetricsLogger.action(getChannelLog(channel, pkg).setType(
@@ -1041,7 +1040,7 @@
}
if (needsDndChange) {
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
return needsPolicyFileChange;
@@ -1127,14 +1126,14 @@
// relevantly affected without the parent channel already having been.
}
- if (updatedChannel.canBypassDnd() != mAreChannelsBypassingDnd
+ if (updatedChannel.canBypassDnd() != mCurrentUserHasChannelsBypassingDnd
|| channel.getImportance() != updatedChannel.getImportance()) {
needsDndChange = true;
changed = true;
}
}
if (needsDndChange) {
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
if (changed) {
updateConfig();
@@ -1321,7 +1320,7 @@
}
}
if (channelBypassedDnd) {
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
return deletedChannel;
}
@@ -1538,7 +1537,7 @@
}
}
if (groupBypassedDnd) {
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
return deletedChannels;
}
@@ -1685,8 +1684,8 @@
}
}
}
- if (!deletedChannelIds.isEmpty() && mAreChannelsBypassingDnd) {
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ if (!deletedChannelIds.isEmpty() && mCurrentUserHasChannelsBypassingDnd) {
+ updateCurrentUserHasChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
}
return deletedChannelIds;
}
@@ -1788,21 +1787,28 @@
}
/**
- * Syncs {@link #mAreChannelsBypassingDnd} with the current user's notification policy before
- * updating
+ * Syncs {@link #mCurrentUserHasChannelsBypassingDnd} with the current user's notification
+ * policy before updating. Must be called:
+ * <ul>
+ * <li>On system init, after channels and DND configurations are loaded.</li>
+ * <li>When the current user changes, after the corresponding DND config is loaded.</li>
+ * </ul>
*/
- private void syncChannelsBypassingDnd(int callingUid, boolean fromSystemOrSystemUi) {
- mAreChannelsBypassingDnd = (mZenModeHelper.getNotificationPolicy().state
- & NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND) == 1;
+ void syncChannelsBypassingDnd() {
+ mCurrentUserHasChannelsBypassingDnd = (mZenModeHelper.getNotificationPolicy().state
+ & NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND) != 0;
- updateChannelsBypassingDnd(callingUid, fromSystemOrSystemUi);
+ updateCurrentUserHasChannelsBypassingDnd(/* callingUid= */ Process.SYSTEM_UID,
+ /* fromSystemOrSystemUi= */ true);
}
/**
- * Updates the user's NotificationPolicy based on whether the current userId
- * has channels bypassing DND
+ * Updates the user's NotificationPolicy based on whether the current userId has channels
+ * bypassing DND. It should be called whenever a channel is created, updated, or deleted, or
+ * when the current user is switched.
*/
- private void updateChannelsBypassingDnd(int callingUid, boolean fromSystemOrSystemUi) {
+ private void updateCurrentUserHasChannelsBypassingDnd(int callingUid,
+ boolean fromSystemOrSystemUi) {
ArraySet<Pair<String, Integer>> candidatePkgs = new ArraySet<>();
final int currentUserId = getCurrentUser();
@@ -1817,7 +1823,7 @@
for (NotificationChannel channel : r.channels.values()) {
if (channelIsLiveLocked(r, channel) && channel.canBypassDnd()) {
- candidatePkgs.add(new Pair(r.pkg, r.uid));
+ candidatePkgs.add(new Pair<>(r.pkg, r.uid));
break;
}
}
@@ -1830,9 +1836,9 @@
}
}
boolean haveBypassingApps = candidatePkgs.size() > 0;
- if (mAreChannelsBypassingDnd != haveBypassingApps) {
- mAreChannelsBypassingDnd = haveBypassingApps;
- updateZenPolicy(mAreChannelsBypassingDnd, callingUid, fromSystemOrSystemUi);
+ if (mCurrentUserHasChannelsBypassingDnd != haveBypassingApps) {
+ mCurrentUserHasChannelsBypassingDnd = haveBypassingApps;
+ updateZenPolicy(mCurrentUserHasChannelsBypassingDnd, callingUid, fromSystemOrSystemUi);
}
}
@@ -1869,7 +1875,7 @@
}
public boolean areChannelsBypassingDnd() {
- return mAreChannelsBypassingDnd;
+ return mCurrentUserHasChannelsBypassingDnd;
}
/**
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 579d4e3..b2dcf37 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 a2177e8..0bb969f 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 @@
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 @@
}
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 1a8c1996..56d92fb 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 @@
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 9f1cb1a..819a82c 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/TEST_MAPPING b/services/core/java/com/android/server/power/TEST_MAPPING
index fbfe291..8374997 100644
--- a/services/core/java/com/android/server/power/TEST_MAPPING
+++ b/services/core/java/com/android/server/power/TEST_MAPPING
@@ -24,6 +24,15 @@
{"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
{"exclude-annotation": "androidx.test.filters.FlakyTest"}
]
+ },
+ {
+ "name": "PowerServiceTests",
+ "options": [
+ {"include-filter": "com.android.server.power"},
+ {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
+ {"exclude-annotation": "androidx.test.filters.FlakyTest"},
+ {"exclude-annotation": "org.junit.Ignore"}
+ ]
}
],
"postsubmit": [
@@ -42,6 +51,14 @@
{"include-filter": "com.android.server.power"},
{"exclude-filter": "com.android.server.power.BatteryStatsTests"}
]
+ },
+ {
+ "name": "PowerServiceTests",
+ "options": [
+ {"include-filter": "com.android.server.power"},
+ {"exclude-filter": "com.android.server.power.BatteryStatsTests"},
+ {"exclude-annotation": "org.junit.Ignore"}
+ ]
}
]
}
diff --git a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
index 308ce4f..19dd0b2 100644
--- a/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
+++ b/services/core/java/com/android/server/vibrator/HapticFeedbackVibrationProvider.java
@@ -21,9 +21,16 @@
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
+import android.util.Slog;
+import android.util.SparseArray;
import android.view.HapticFeedbackConstants;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.IOException;
import java.io.PrintWriter;
+import java.util.HashSet;
+import java.util.Set;
/**
* Provides the {@link VibrationEffect} and {@link VibrationAttributes} for haptic feedback.
@@ -31,6 +38,8 @@
* @hide
*/
public final class HapticFeedbackVibrationProvider {
+ private static final String TAG = "HapticFeedbackVibrationProvider";
+
private static final VibrationAttributes TOUCH_VIBRATION_ATTRIBUTES =
VibrationAttributes.createForUsage(VibrationAttributes.USAGE_TOUCH);
private static final VibrationAttributes PHYSICAL_EMULATION_VIBRATION_ATTRIBUTES =
@@ -42,15 +51,41 @@
private final boolean mHapticTextHandleEnabled;
// Vibrator effect for haptic feedback during boot when safe mode is enabled.
private final VibrationEffect mSafeModeEnabledVibrationEffect;
+ // Haptic feedback vibration customizations specific to the device.
+ // If present and valid, a vibration here will be used for an effect.
+ // Otherwise, the system's default vibration will be used.
+ @Nullable private final SparseArray<VibrationEffect> mHapticCustomizations;
/** @hide */
public HapticFeedbackVibrationProvider(Resources res, Vibrator vibrator) {
+ this(res, vibrator, loadHapticCustomizations(res));
+ }
+
+ /** @hide */
+ @VisibleForTesting HapticFeedbackVibrationProvider(
+ Resources res,
+ Vibrator vibrator,
+ @Nullable SparseArray<VibrationEffect> hapticCustomizations) {
mVibrator = vibrator;
mHapticTextHandleEnabled = res.getBoolean(
com.android.internal.R.bool.config_enableHapticTextHandle);
+
+ if (hapticCustomizations != null) {
+ // Clean up the customizations to remove vibrations which may not ever be used due to
+ // Vibrator properties or other device configurations.
+ removeUnsupportedVibrations(hapticCustomizations, vibrator);
+ if (hapticCustomizations.size() == 0) {
+ hapticCustomizations = null;
+ }
+ }
+ mHapticCustomizations = hapticCustomizations;
+
mSafeModeEnabledVibrationEffect =
- VibrationSettings.createEffectFromResource(
- res, com.android.internal.R.array.config_safeModeEnabledVibePattern);
+ effectHasCustomization(HapticFeedbackConstants.SAFE_MODE_ENABLED)
+ ? mHapticCustomizations.get(HapticFeedbackConstants.SAFE_MODE_ENABLED)
+ : VibrationSettings.createEffectFromResource(
+ res,
+ com.android.internal.R.array.config_safeModeEnabledVibePattern);
}
/**
@@ -68,7 +103,7 @@
case HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE:
case HapticFeedbackConstants.SCROLL_TICK:
case HapticFeedbackConstants.SEGMENT_TICK:
- return VibrationEffect.get(VibrationEffect.EFFECT_TICK);
+ return getVibration(effectId, VibrationEffect.EFFECT_TICK);
case HapticFeedbackConstants.TEXT_HANDLE_MOVE:
if (!mHapticTextHandleEnabled) {
@@ -77,13 +112,16 @@
// fallthrough
case HapticFeedbackConstants.CLOCK_TICK:
case HapticFeedbackConstants.SEGMENT_FREQUENT_TICK:
- return VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK);
+ return getVibration(effectId, VibrationEffect.EFFECT_TEXTURE_TICK);
case HapticFeedbackConstants.KEYBOARD_RELEASE:
case HapticFeedbackConstants.VIRTUAL_KEY_RELEASE:
case HapticFeedbackConstants.ENTRY_BUMP:
case HapticFeedbackConstants.DRAG_CROSSING:
- return VibrationEffect.get(VibrationEffect.EFFECT_TICK, false);
+ return getVibration(
+ effectId,
+ VibrationEffect.EFFECT_TICK,
+ /* fallbackForPredefinedEffect= */ false);
case HapticFeedbackConstants.KEYBOARD_TAP: // == KEYBOARD_PRESS
case HapticFeedbackConstants.VIRTUAL_KEY:
@@ -93,46 +131,42 @@
case HapticFeedbackConstants.GESTURE_START:
case HapticFeedbackConstants.SCROLL_ITEM_FOCUS:
case HapticFeedbackConstants.SCROLL_LIMIT:
- return VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
+ return getVibration(effectId, VibrationEffect.EFFECT_CLICK);
case HapticFeedbackConstants.LONG_PRESS:
case HapticFeedbackConstants.LONG_PRESS_POWER_BUTTON:
case HapticFeedbackConstants.DRAG_START:
case HapticFeedbackConstants.EDGE_SQUEEZE:
- return VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK);
+ return getVibration(effectId, VibrationEffect.EFFECT_HEAVY_CLICK);
case HapticFeedbackConstants.REJECT:
- return VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK);
+ return getVibration(effectId, VibrationEffect.EFFECT_DOUBLE_CLICK);
case HapticFeedbackConstants.SAFE_MODE_ENABLED:
return mSafeModeEnabledVibrationEffect;
case HapticFeedbackConstants.ASSISTANT_BUTTON:
- if (mVibrator.areAllPrimitivesSupported(
- VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
- VibrationEffect.Composition.PRIMITIVE_TICK)) {
- // quiet ramp, short pause, then sharp tick
- return VibrationEffect.startComposition()
- .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 0.25f)
- .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1f, 50)
- .compose();
- }
- // fallback for devices without composition support
- return VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK);
+ return getAssistantButtonVibration();
case HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE:
- return getScaledPrimitiveOrElseEffect(
- VibrationEffect.Composition.PRIMITIVE_TICK, 0.4f,
+ return getVibration(
+ effectId,
+ VibrationEffect.Composition.PRIMITIVE_TICK,
+ /* primitiveScale= */ 0.4f,
VibrationEffect.EFFECT_TEXTURE_TICK);
case HapticFeedbackConstants.TOGGLE_ON:
- return getScaledPrimitiveOrElseEffect(
- VibrationEffect.Composition.PRIMITIVE_TICK, 0.5f,
+ return getVibration(
+ effectId,
+ VibrationEffect.Composition.PRIMITIVE_TICK,
+ /* primitiveScale= */ 0.5f,
VibrationEffect.EFFECT_TICK);
case HapticFeedbackConstants.TOGGLE_OFF:
- return getScaledPrimitiveOrElseEffect(
- VibrationEffect.Composition.PRIMITIVE_LOW_TICK, 0.2f,
+ return getVibration(
+ effectId,
+ VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
+ /* primitiveScale= */ 0.2f,
VibrationEffect.EFFECT_TEXTURE_TICK);
case HapticFeedbackConstants.NO_HAPTICS:
@@ -181,14 +215,100 @@
pw.print("mHapticTextHandleEnabled="); pw.println(mHapticTextHandleEnabled);
}
- private VibrationEffect getScaledPrimitiveOrElseEffect(
- int primitiveId, float scale, int elseEffectId) {
+ private VibrationEffect getVibration(int effectId, int predefinedVibrationEffectId) {
+ return getVibration(
+ effectId, predefinedVibrationEffectId, /* fallbackForPredefinedEffect= */ true);
+ }
+
+ /**
+ * Returns the customized vibration for {@code hapticFeedbackId}, or
+ * {@code predefinedVibrationEffectId} if a customization does not exist for the haptic
+ * feedback.
+ *
+ * <p>If a customization does not exist and the default predefined effect is to be returned,
+ * {@code fallbackForPredefinedEffect} will be used to decide whether or not to fallback
+ * to a generic pattern if the predefined effect is not hardware supported.
+ *
+ * @see VibrationEffect#get(int, boolean)
+ */
+ private VibrationEffect getVibration(
+ int hapticFeedbackId,
+ int predefinedVibrationEffectId,
+ boolean fallbackForPredefinedEffect) {
+ if (effectHasCustomization(hapticFeedbackId)) {
+ return mHapticCustomizations.get(hapticFeedbackId);
+ }
+ return VibrationEffect.get(predefinedVibrationEffectId, fallbackForPredefinedEffect);
+ }
+
+ /**
+ * Returns the customized vibration for {@code hapticFeedbackId}, or some fallback vibration if
+ * a customization does not exist for the ID.
+ *
+ * <p>The fallback will be a primitive composition formed of {@code primitiveId} and
+ * {@code primitiveScale}, if the primitive is supported. Otherwise, it will be a predefined
+ * vibration of {@code elsePredefinedVibrationEffectId}.
+ */
+ private VibrationEffect getVibration(
+ int hapticFeedbackId,
+ int primitiveId,
+ float primitiveScale,
+ int elsePredefinedVibrationEffectId) {
+ if (effectHasCustomization(hapticFeedbackId)) {
+ return mHapticCustomizations.get(hapticFeedbackId);
+ }
if (mVibrator.areAllPrimitivesSupported(primitiveId)) {
return VibrationEffect.startComposition()
- .addPrimitive(primitiveId, scale)
+ .addPrimitive(primitiveId, primitiveScale)
.compose();
} else {
- return VibrationEffect.get(elseEffectId);
+ return VibrationEffect.get(elsePredefinedVibrationEffectId);
+ }
+ }
+
+ private VibrationEffect getAssistantButtonVibration() {
+ if (effectHasCustomization(HapticFeedbackConstants.ASSISTANT_BUTTON)) {
+ return mHapticCustomizations.get(HapticFeedbackConstants.ASSISTANT_BUTTON);
+ }
+ if (mVibrator.areAllPrimitivesSupported(
+ VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
+ VibrationEffect.Composition.PRIMITIVE_TICK)) {
+ // quiet ramp, short pause, then sharp tick
+ return VibrationEffect.startComposition()
+ .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 0.25f)
+ .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 1f, 50)
+ .compose();
+ }
+ // fallback for devices without composition support
+ return VibrationEffect.get(VibrationEffect.EFFECT_HEAVY_CLICK);
+ }
+
+ private boolean effectHasCustomization(int effectId) {
+ return mHapticCustomizations != null && mHapticCustomizations.contains(effectId);
+ }
+
+ @Nullable
+ private static SparseArray<VibrationEffect> loadHapticCustomizations(Resources res) {
+ try {
+ return HapticFeedbackCustomization.loadVibrations(res);
+ } catch (IOException | HapticFeedbackCustomization.CustomizationParserException e) {
+ Slog.e(TAG, "Unable to load haptic customizations.", e);
+ return null;
+ }
+ }
+
+ private static void removeUnsupportedVibrations(
+ SparseArray<VibrationEffect> customizations, Vibrator vibrator) {
+ Set<Integer> keysToRemove = new HashSet<>();
+ for (int i = 0; i < customizations.size(); i++) {
+ int key = customizations.keyAt(i);
+ if (!vibrator.areVibrationFeaturesSupported(customizations.get(key))) {
+ keysToRemove.add(key);
+ }
+ }
+
+ for (int key : keysToRemove) {
+ customizations.remove(key);
}
}
}
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index 0a62c88..0c1c1d4 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -3110,7 +3110,18 @@
} 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 aeaf783..2c866ab 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 @@
* {@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 @@
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/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index d675753..467937c 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -5422,8 +5422,10 @@
// 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/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 92c0987..57ce368 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 @@
*/
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 @@
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 @@
@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/WindowContextListenerController.java b/services/core/java/com/android/server/wm/WindowContextListenerController.java
index 4025cbf..a59c230 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 @@
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 @@
* {@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 @@
@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 @@
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 @@
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 @@
}
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 15b15a8..2a28010 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 @@
return WindowManagerGlobal.ADD_INVALID_TYPE;
}
} else {
- mWindowContextListenerController.registerWindowContainerListener(
- windowContextToken, token, callingUid, type, options);
+ mWindowContextListenerController.updateContainerForWindowContextListener(
+ windowContextToken, token);
}
}
@@ -2744,10 +2744,17 @@
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 @@
// 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 @@
@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 @@
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 @@
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 @@
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 164c8b0..a84749a 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_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 @@
}
break;
}
+ case OP_TYPE_SET_ISOLATED_NAVIGATION: {
+ final boolean isolatedNav = operation.isIsolatedNav();
+ taskFragment.setIsolatedNav(isolatedNav);
+ break;
+ }
}
return effects;
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 77f8de5..c323a7f 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_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.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 @@
});
}
+ 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 ee4bc12..6a2d4dc 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -2447,8 +2447,9 @@
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 5e7dd59..c0cfa53 100644
--- a/services/midi/java/com/android/server/midi/MidiService.java
+++ b/services/midi/java/com/android/server/midi/MidiService.java
@@ -31,6 +31,7 @@
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.media.MediaMetrics;
import android.media.midi.IBluetoothMidiService;
@@ -43,6 +44,7 @@
import android.media.midi.MidiDeviceService;
import android.media.midi.MidiDeviceStatus;
import android.media.midi.MidiManager;
+import android.media.midi.MidiUmpDeviceService;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
@@ -580,10 +582,14 @@
intent.setComponent(new ComponentName(
MidiManager.BLUETOOTH_MIDI_SERVICE_PACKAGE,
MidiManager.BLUETOOTH_MIDI_SERVICE_CLASS));
- } else {
+ } else if (!isUmpDevice(mDeviceInfo)) {
intent = new Intent(MidiDeviceService.SERVICE_INTERFACE);
intent.setComponent(
new ComponentName(mServiceInfo.packageName, mServiceInfo.name));
+ } else {
+ intent = new Intent(MidiUmpDeviceService.SERVICE_INTERFACE);
+ intent.setComponent(
+ new ComponentName(mServiceInfo.packageName, mServiceInfo.name));
}
if (!mContext.bindServiceAsUser(intent, mServiceConnection,
@@ -696,8 +702,8 @@
isDeviceDisconnected ? "true" : "false")
.set(MediaMetrics.Property.IS_SHARED,
!mDeviceInfo.isPrivate() ? "true" : "false")
- .set(MediaMetrics.Property.SUPPORTS_MIDI_UMP, mDeviceInfo.getDefaultProtocol()
- != MidiDeviceInfo.PROTOCOL_UNKNOWN ? "true" : "false")
+ .set(MediaMetrics.Property.SUPPORTS_MIDI_UMP,
+ isUmpDevice(mDeviceInfo) ? "true" : "false")
.set(MediaMetrics.Property.USING_ALSA, mDeviceInfo.getProperties().get(
MidiDeviceInfo.PROPERTY_ALSA_CARD) != null ? "true" : "false")
.set(MediaMetrics.Property.EVENT, "deviceClosed")
@@ -971,19 +977,37 @@
private void onStartOrUnlockUser(TargetUser user, boolean matchDirectBootUnaware) {
Log.d(TAG, "onStartOrUnlockUser " + user.getUserIdentifier() + " matchDirectBootUnaware: "
+ matchDirectBootUnaware);
- Intent intent = new Intent(MidiDeviceService.SERVICE_INTERFACE);
int resolveFlags = PackageManager.GET_META_DATA;
if (matchDirectBootUnaware) {
resolveFlags |= PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
}
- List<ResolveInfo> resolveInfos = mPackageManager.queryIntentServicesAsUser(intent,
- resolveFlags, user.getUserIdentifier());
- if (resolveInfos != null) {
- int count = resolveInfos.size();
- for (int i = 0; i < count; i++) {
- ServiceInfo serviceInfo = resolveInfos.get(i).serviceInfo;
- if (serviceInfo != null) {
- addPackageDeviceServer(serviceInfo, user.getUserIdentifier());
+
+ {
+ Intent intent = new Intent(MidiDeviceService.SERVICE_INTERFACE);
+ List<ResolveInfo> resolveInfos = mPackageManager.queryIntentServicesAsUser(intent,
+ resolveFlags, user.getUserIdentifier());
+ if (resolveInfos != null) {
+ int count = resolveInfos.size();
+ for (int i = 0; i < count; i++) {
+ ServiceInfo serviceInfo = resolveInfos.get(i).serviceInfo;
+ if (serviceInfo != null) {
+ addLegacyPackageDeviceServer(serviceInfo, user.getUserIdentifier());
+ }
+ }
+ }
+ }
+
+ {
+ Intent intent = new Intent(MidiUmpDeviceService.SERVICE_INTERFACE);
+ List<ResolveInfo> resolveInfos = mPackageManager.queryIntentServicesAsUser(intent,
+ resolveFlags, user.getUserIdentifier());
+ if (resolveInfos != null) {
+ int count = resolveInfos.size();
+ for (int i = 0; i < count; i++) {
+ ServiceInfo serviceInfo = resolveInfos.get(i).serviceInfo;
+ if (serviceInfo != null) {
+ addUmpPackageDeviceServer(serviceInfo, user.getUserIdentifier());
+ }
}
}
}
@@ -1057,13 +1081,11 @@
if (device.isUidAllowed(uid) && device.isUserIdAllowed(userId)) {
// UMP devices have protocols that are not PROTOCOL_UNKNOWN
if (transport == MidiManager.TRANSPORT_UNIVERSAL_MIDI_PACKETS) {
- if (device.getDeviceInfo().getDefaultProtocol()
- != MidiDeviceInfo.PROTOCOL_UNKNOWN) {
+ if (isUmpDevice(device.getDeviceInfo())) {
deviceInfos.add(device.getDeviceInfo());
}
} else if (transport == MidiManager.TRANSPORT_MIDI_BYTE_STREAM) {
- if (device.getDeviceInfo().getDefaultProtocol()
- == MidiDeviceInfo.PROTOCOL_UNKNOWN) {
+ if (!isUmpDevice(device.getDeviceInfo())) {
deviceInfos.add(device.getDeviceInfo());
}
}
@@ -1364,14 +1386,15 @@
ServiceInfo[] services = info.services;
if (services == null) return;
for (int i = 0; i < services.length; i++) {
- addPackageDeviceServer(services[i], userId);
+ addLegacyPackageDeviceServer(services[i], userId);
+ addUmpPackageDeviceServer(services[i], userId);
}
}
private static final String[] EMPTY_STRING_ARRAY = new String[0];
- private void addPackageDeviceServer(ServiceInfo serviceInfo, int userId) {
- Log.d(TAG, "addPackageDeviceServer()" + userId);
+ private void addLegacyPackageDeviceServer(ServiceInfo serviceInfo, int userId) {
+ Log.d(TAG, "addLegacyPackageDeviceServer()" + userId);
XmlResourceParser parser = null;
try {
@@ -1507,6 +1530,128 @@
}
}
+ @RequiresPermission(Manifest.permission.INTERACT_ACROSS_USERS)
+ private void addUmpPackageDeviceServer(ServiceInfo serviceInfo, int userId) {
+ Log.d(TAG, "addUmpPackageDeviceServer()" + userId);
+ XmlResourceParser parser = null;
+
+ try {
+ ComponentName componentName = new ComponentName(serviceInfo.packageName,
+ serviceInfo.name);
+ int resId = mPackageManager.getProperty(MidiUmpDeviceService.SERVICE_INTERFACE,
+ componentName).getResourceId();
+ Resources resources = mPackageManager.getResourcesForApplication(
+ serviceInfo.packageName);
+ parser = resources.getXml(resId);
+ if (parser == null) return;
+
+ // ignore virtual device servers that do not require the correct permission
+ if (!android.Manifest.permission.BIND_MIDI_DEVICE_SERVICE.equals(
+ serviceInfo.permission)) {
+ Log.w(TAG, "Skipping MIDI device service " + serviceInfo.packageName
+ + ": it does not require the permission "
+ + android.Manifest.permission.BIND_MIDI_DEVICE_SERVICE);
+ return;
+ }
+
+ Bundle properties = null;
+ int numPorts = 0;
+ boolean isPrivate = false;
+ ArrayList<String> portNames = new ArrayList<String>();
+
+ while (true) {
+ int eventType = parser.next();
+ if (eventType == XmlPullParser.END_DOCUMENT) {
+ break;
+ } else if (eventType == XmlPullParser.START_TAG) {
+ String tagName = parser.getName();
+ if ("device".equals(tagName)) {
+ if (properties != null) {
+ Log.w(TAG, "nested <device> elements in metadata for "
+ + serviceInfo.packageName);
+ continue;
+ }
+ properties = new Bundle();
+ properties.putParcelable(MidiDeviceInfo.PROPERTY_SERVICE_INFO, serviceInfo);
+ numPorts = 0;
+ isPrivate = false;
+
+ int count = parser.getAttributeCount();
+ for (int i = 0; i < count; i++) {
+ String name = parser.getAttributeName(i);
+ String value = parser.getAttributeValue(i);
+ if ("private".equals(name)) {
+ isPrivate = "true".equals(value);
+ } else {
+ properties.putString(name, value);
+ }
+ }
+ } else if ("port".equals(tagName)) {
+ if (properties == null) {
+ Log.w(TAG, "<port> outside of <device> in metadata for "
+ + serviceInfo.packageName);
+ continue;
+ }
+ numPorts++;
+
+ String portName = null;
+ int count = parser.getAttributeCount();
+ for (int i = 0; i < count; i++) {
+ String name = parser.getAttributeName(i);
+ String value = parser.getAttributeValue(i);
+ if ("name".equals(name)) {
+ portName = value;
+ break;
+ }
+ }
+ portNames.add(portName);
+ }
+ } else if (eventType == XmlPullParser.END_TAG) {
+ String tagName = parser.getName();
+ if ("device".equals(tagName)) {
+ if (properties != null) {
+ if (numPorts == 0) {
+ Log.w(TAG, "<device> with no ports in metadata for "
+ + serviceInfo.packageName);
+ continue;
+ }
+
+ int uid;
+ try {
+ ApplicationInfo appInfo = mPackageManager.getApplicationInfoAsUser(
+ serviceInfo.packageName, 0, userId);
+ uid = appInfo.uid;
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "could not fetch ApplicationInfo for "
+ + serviceInfo.packageName);
+ continue;
+ }
+
+ synchronized (mDevicesByInfo) {
+ addDeviceLocked(MidiDeviceInfo.TYPE_VIRTUAL,
+ numPorts, numPorts,
+ portNames.toArray(EMPTY_STRING_ARRAY),
+ portNames.toArray(EMPTY_STRING_ARRAY),
+ properties, null, serviceInfo, isPrivate, uid,
+ MidiDeviceInfo.PROTOCOL_UMP_MIDI_2_0, userId);
+ }
+ // setting properties to null signals that we are no longer
+ // processing a <device>
+ properties = null;
+ portNames.clear();
+ }
+ }
+ }
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ // No such property
+ } catch (Exception e) {
+ Log.w(TAG, "Unable to load component info " + serviceInfo.toString(), e);
+ } finally {
+ if (parser != null) parser.close();
+ }
+ }
+
private void removePackageDeviceServers(String packageName, int userId) {
synchronized (mDevicesByInfo) {
Iterator<Device> iterator = mDevicesByInfo.values().iterator();
@@ -1649,4 +1794,8 @@
private int getCallingUserId() {
return UserHandle.getUserId(Binder.getCallingUid());
}
+
+ private boolean isUmpDevice(MidiDeviceInfo deviceInfo) {
+ return deviceInfo.getDefaultProtocol() != MidiDeviceInfo.PROTOCOL_UNKNOWN;
+ }
}
diff --git a/services/permission/TEST_MAPPING b/services/permission/TEST_MAPPING
index 579d4e3..b2dcf37 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/TEST_MAPPING b/services/tests/displayservicetests/TEST_MAPPING
index d865519..477860d 100644
--- a/services/tests/displayservicetests/TEST_MAPPING
+++ b/services/tests/displayservicetests/TEST_MAPPING
@@ -1,13 +1,7 @@
{
- "presubmit": [
+ "imports": [
{
- "name": "DisplayServiceTests",
- "options": [
- {"include-filter": "com.android.server.display"},
- {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
- {"exclude-annotation": "androidx.test.filters.FlakyTest"},
- {"exclude-annotation": "org.junit.Ignore"}
- ]
+ "path": "frameworks/base/services/core/java/com/android/server/display"
}
]
}
diff --git a/services/tests/powerservicetests/TEST_MAPPING b/services/tests/powerservicetests/TEST_MAPPING
new file mode 100644
index 0000000..1b534a1
--- /dev/null
+++ b/services/tests/powerservicetests/TEST_MAPPING
@@ -0,0 +1,8 @@
+{
+ "imports": [
+ {
+ "path": "frameworks/base/services/core/java/com/android/server/power"
+ }
+ ]
+}
+
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 0037970..8c07b6c 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 @@
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.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 @@
}
@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)
+ 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)
}
- private fun parseBadApplicationTags(
- maxNum: Int,
- prefix: String,
- suffix: String,
- parentTag: String
- ) {
+ @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)
+ 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 parseBadProfileableTags(
- maxNum: Int,
- prefix: String,
- suffix: String,
- parentTag: String
- ) {
- val tag = "profileable"
- parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
- }
-
- 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)
+ 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)
}
- private fun parseBadServiceTags(
- maxNum: Int,
- prefix: String,
- suffix: String,
- parentTag: String
- ) {
+ @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)
+ 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)
}
- private fun parseBadActivityAliasTags(
- maxNum: Int,
- prefix: String,
- suffix: String,
- parentTag: String
- ) {
+ @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)
+ 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)
}
- private fun parseBadUsesLibraryTags(
- maxNum: Int,
- prefix: String,
- suffix: String,
- parentTag: String
- ) {
+ @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)
+ 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 parseBadLayoutTags(
- maxNum: Int,
- prefix: String,
- suffix: String,
- parentTag: String
- ) {
- val tag = "layout"
- parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
- }
-
- 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)
+ 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)
}
- private fun parseBadInstrumentationTags(
- maxNum: Int,
- prefix: String,
- suffix: String,
- parentTag: String
- ) {
+ @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)
- }
-
- private fun parseBadQueriesTags(
- maxNum: Int,
- prefix: String,
- suffix: String,
- parentTag: String
- ) {
+ @Test
+ fun parseQueriesTag() {
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)
+ validateTagCount("package", 1000, tag)
+ validateTagCount("intent", 2000, tag)
+ validateTagCount("provider", 8000, 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)
+ validateTagCount("action", 20000, tag)
+ validateTagCount("category", 40000, tag)
+ validateTagCount("data", 40000, tag)
}
- private fun parseBadProviderTags(
- maxNum: Int,
- prefix: String,
- suffix: String,
- parentTag: String
- ) {
+ @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)
+ 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)
}
- private fun parseBadGrantUriPermissionTags(
- maxNum: Int,
- prefix: String,
- suffix: String,
- parentTag: String
- ) {
+ @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)
+ 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)
}
- private fun parseBadMetaDataTags(
- maxNum: Int,
- prefix: String,
- suffix: String,
- parentTag: String
- ) {
+ @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)
+ validateTagCount("action", 20000, tag)
+ validateTagCount("category", 40000, tag)
+ validateTagCount("data", 40000, tag)
}
- private fun parseBadActionTags(
- maxNum: Int,
- prefix: String,
- suffix: String,
- parentTag: String
- ) {
+ @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)
+ 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 parseBadAttributionTags(
- maxNum: Int,
- prefix: String,
- suffix: String,
- parentTag: String
- ) {
- val tag = "attribution"
- parseBadTagCount(tag, maxNum, parentTag, prefix, suffix)
- }
-
- 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 parseTagBadAttr(
- tag: String,
- attrName: String,
- maxLength: Int,
- prefix: String = "",
- suffix: String = ""
+ 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 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 @@
@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 @@
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 0292bca..f1e26d2 100755
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -421,6 +421,7 @@
@Mock
MultiRateLimiter mToastRateLimiter;
BroadcastReceiver mPackageIntentReceiver;
+ BroadcastReceiver mUserSwitchIntentReceiver;
NotificationRecordLoggerFake mNotificationRecordLogger = new NotificationRecordLoggerFake();
TestableNotificationManagerService.StrongAuthTrackerFake mStrongAuthTracker;
@@ -651,8 +652,12 @@
&& filter.hasAction(Intent.ACTION_PACKAGES_SUSPENDED)) {
mPackageIntentReceiver = broadcastReceivers.get(i);
}
+ if (filter.hasAction(Intent.ACTION_USER_SWITCHED)) {
+ mUserSwitchIntentReceiver = broadcastReceivers.get(i);
+ }
}
assertNotNull("package intent receiver should exist", mPackageIntentReceiver);
+ assertNotNull("User-switch receiver should exist", mUserSwitchIntentReceiver);
// Pretend the shortcut exists
List<ShortcutInfo> shortcutInfos = new ArrayList<>();
@@ -12148,6 +12153,21 @@
any(), eq(FLAG_ACTIVITY_SENDER | FLAG_BROADCAST_SENDER | FLAG_SERVICE_SENDER));
}
+ @Test
+ public void onUserSwitched_updatesZenModeAndChannelsBypassingDnd() {
+ Intent intent = new Intent(Intent.ACTION_USER_SWITCHED);
+ intent.putExtra(Intent.EXTRA_USER_HANDLE, 20);
+ mService.mZenModeHelper = mock(ZenModeHelper.class);
+ mService.setPreferencesHelper(mPreferencesHelper);
+
+ mUserSwitchIntentReceiver.onReceive(mContext, intent);
+
+ InOrder inOrder = inOrder(mPreferencesHelper, mService.mZenModeHelper);
+ inOrder.verify(mService.mZenModeHelper).onUserSwitched(eq(20));
+ inOrder.verify(mPreferencesHelper).syncChannelsBypassingDnd();
+ inOrder.verifyNoMoreInteractions();
+ }
+
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/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index ea670bd..c242554 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -2478,7 +2478,7 @@
mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger,
mAppOpsManager, mStatsEventBuilderFactory, false);
-
+ mHelper.syncChannelsBypassingDnd();
// create notification channel that can bypass dnd, but app is blocked
// expected result: areChannelsBypassingDnd = false
@@ -2509,6 +2509,7 @@
mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger,
mAppOpsManager, mStatsEventBuilderFactory, false);
+ mHelper.syncChannelsBypassingDnd();
// create notification channel that can bypass dnd, but app is blocked
// expected result: areChannelsBypassingDnd = false
@@ -2533,6 +2534,7 @@
mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger,
mAppOpsManager, mStatsEventBuilderFactory, false);
+ mHelper.syncChannelsBypassingDnd();
// create notification channel that can bypass dnd, but app is blocked
// expected result: areChannelsBypassingDnd = false
@@ -2587,6 +2589,7 @@
mHelper = new PreferencesHelper(getContext(), mPm, mHandler, mMockZenModeHelper,
mPermissionHelper, mPermissionManager, mLogger,
mAppOpsManager, mStatsEventBuilderFactory, false);
+ mHelper.syncChannelsBypassingDnd();
assertFalse(mHelper.areChannelsBypassingDnd());
verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyBoolean());
resetZenModeHelper();
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
new file mode 100644
index 0000000..cae811e
--- /dev/null
+++ b/services/tests/vibrator/src/com/android/server/vibrator/HapticFeedbackVibrationProviderTest.java
@@ -0,0 +1,261 @@
+/*
+ * 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.
+ */
+
+package com.android.server.vibrator;
+
+import static android.os.VibrationEffect.Composition.PRIMITIVE_CLICK;
+import static android.os.VibrationEffect.Composition.PRIMITIVE_TICK;
+import static android.os.VibrationEffect.EFFECT_TEXTURE_TICK;
+import static android.os.VibrationEffect.EFFECT_TICK;
+import static android.view.HapticFeedbackConstants.CLOCK_TICK;
+import static android.view.HapticFeedbackConstants.CONTEXT_CLICK;
+import static android.view.HapticFeedbackConstants.SAFE_MODE_ENABLED;
+import static android.view.HapticFeedbackConstants.TEXT_HANDLE_MOVE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.VibrationEffect;
+import android.os.test.FakeVibrator;
+import android.util.AtomicFile;
+import android.util.SparseArray;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.internal.R;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.io.File;
+import java.io.FileOutputStream;
+
+public class HapticFeedbackVibrationProviderTest {
+ @Rule public MockitoRule rule = MockitoJUnit.rule();
+
+ private static final VibrationEffect PRIMITIVE_TICK_EFFECT =
+ VibrationEffect.startComposition().addPrimitive(PRIMITIVE_TICK, 0.2497f).compose();
+ private static final VibrationEffect PRIMITIVE_CLICK_EFFECT =
+ VibrationEffect.startComposition().addPrimitive(PRIMITIVE_CLICK, 0.3497f).compose();
+
+ private Context mContext = InstrumentationRegistry.getContext();
+ private FakeVibrator mVibrator = new FakeVibrator(mContext);
+
+ @Mock private Resources mResourcesMock;
+
+ @Test
+ public void testNonExistentCustomization_useDefault() throws Exception {
+ // No customization file is set.
+ HapticFeedbackVibrationProvider hapticProvider =
+ new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator);
+
+ assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
+ .isEqualTo(VibrationEffect.get(EFFECT_TICK));
+
+ // The customization file specifies no customization.
+ setupCustomizationFile("<haptic-feedback-constants></haptic-feedback-constants>");
+ hapticProvider = new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator);
+
+ assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
+ .isEqualTo(VibrationEffect.get(EFFECT_TICK));
+ }
+
+ @Test
+ public void testExceptionParsingCustomizations_useDefault() throws Exception {
+ setupCustomizationFile("<bad-xml></bad-xml>");
+ HapticFeedbackVibrationProvider hapticProvider =
+ new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator);
+
+ assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
+ .isEqualTo(VibrationEffect.get(EFFECT_TICK));
+ }
+
+ @Test
+ public void testUseValidCustomizedVibration() throws Exception {
+ mockVibratorPrimitiveSupport(PRIMITIVE_CLICK);
+ SparseArray<VibrationEffect> customizations = new SparseArray<>();
+ customizations.put(CONTEXT_CLICK, PRIMITIVE_CLICK_EFFECT);
+
+ HapticFeedbackVibrationProvider hapticProvider =
+ new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+
+ // The override for `CONTEXT_CLICK` is used.
+ assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
+ .isEqualTo(PRIMITIVE_CLICK_EFFECT);
+ // `CLOCK_TICK` has no override, so the default vibration is used.
+ assertThat(hapticProvider.getVibrationForHapticFeedback(CLOCK_TICK))
+ .isEqualTo(VibrationEffect.get(EFFECT_TEXTURE_TICK));
+ }
+
+ @Test
+ public void testDoNotUseInvalidCustomizedVibration() throws Exception {
+ mockVibratorPrimitiveSupport(new int[] {});
+ SparseArray<VibrationEffect> customizations = new SparseArray<>();
+ customizations.put(CONTEXT_CLICK, PRIMITIVE_CLICK_EFFECT);
+
+ HapticFeedbackVibrationProvider hapticProvider =
+ new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+
+ // The override for `CONTEXT_CLICK` is not used because the vibration is not supported.
+ assertThat(hapticProvider.getVibrationForHapticFeedback(CONTEXT_CLICK))
+ .isEqualTo(VibrationEffect.get(EFFECT_TICK));
+ // `CLOCK_TICK` has no override, so the default vibration is used.
+ assertThat(hapticProvider.getVibrationForHapticFeedback(CLOCK_TICK))
+ .isEqualTo(VibrationEffect.get(EFFECT_TEXTURE_TICK));
+ }
+
+ @Test
+ public void testHapticTextDisabled_noVibrationReturnedForTextHandleMove() throws Exception {
+ mockHapticTextSupport(false);
+ mockVibratorPrimitiveSupport(PRIMITIVE_CLICK);
+ SparseArray<VibrationEffect> customizations = new SparseArray<>();
+ customizations.put(TEXT_HANDLE_MOVE, PRIMITIVE_CLICK_EFFECT);
+
+ // Test with a customization available for `TEXT_HANDLE_MOVE`.
+ HapticFeedbackVibrationProvider hapticProvider =
+ new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+
+ assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE)).isNull();
+
+ // Test with no customization available for `TEXT_HANDLE_MOVE`.
+ hapticProvider =
+ new HapticFeedbackVibrationProvider(
+ mResourcesMock, mVibrator, /* hapticCustomizations= */ null);
+
+ assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE)).isNull();
+ }
+
+ @Test
+ public void testHapticTextEnabled_vibrationReturnedForTextHandleMove() throws Exception {
+ mockHapticTextSupport(true);
+ mockVibratorPrimitiveSupport(PRIMITIVE_CLICK);
+ SparseArray<VibrationEffect> customizations = new SparseArray<>();
+ customizations.put(TEXT_HANDLE_MOVE, PRIMITIVE_CLICK_EFFECT);
+
+ // Test with a customization available for `TEXT_HANDLE_MOVE`.
+ HapticFeedbackVibrationProvider hapticProvider =
+ new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+
+ assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE))
+ .isEqualTo(PRIMITIVE_CLICK_EFFECT);
+
+ // Test with no customization available for `TEXT_HANDLE_MOVE`.
+ hapticProvider =
+ new HapticFeedbackVibrationProvider(
+ mResourcesMock, mVibrator, /* hapticCustomizations= */ null);
+
+ assertThat(hapticProvider.getVibrationForHapticFeedback(TEXT_HANDLE_MOVE))
+ .isEqualTo(VibrationEffect.get(EFFECT_TEXTURE_TICK));
+ }
+
+ @Test
+ public void testValidCustomizationPresentForSafeModeEnabled_usedRegardlessOfVibrationResource()
+ throws Exception {
+ mockSafeModeEnabledVibration(10, 20, 30, 40);
+ mockVibratorPrimitiveSupport(PRIMITIVE_CLICK);
+ SparseArray<VibrationEffect> customizations = new SparseArray<>();
+ customizations.put(SAFE_MODE_ENABLED, PRIMITIVE_CLICK_EFFECT);
+
+ HapticFeedbackVibrationProvider hapticProvider =
+ new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+
+ assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED))
+ .isEqualTo(PRIMITIVE_CLICK_EFFECT);
+
+ mockSafeModeEnabledVibration(null);
+ hapticProvider =
+ new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+
+ assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED))
+ .isEqualTo(PRIMITIVE_CLICK_EFFECT);
+ }
+
+ @Test
+ public void testNoValidCustomizationPresentForSafeModeEnabled_resourceBasedVibrationUsed()
+ throws Exception {
+ mockSafeModeEnabledVibration(10, 20, 30, 40);
+ SparseArray<VibrationEffect> customizations = new SparseArray<>();
+ customizations.put(SAFE_MODE_ENABLED, PRIMITIVE_CLICK_EFFECT);
+
+ // Test with a customization that is not supported by the vibrator.
+ HapticFeedbackVibrationProvider hapticProvider =
+ new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+
+ assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED))
+ .isEqualTo(VibrationEffect.createWaveform(new long[] {10, 20, 30, 40}, -1));
+
+ // Test with no customizations.
+ hapticProvider =
+ new HapticFeedbackVibrationProvider(
+ mResourcesMock, mVibrator, /* hapticCustomizations= */ null);
+
+ assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED))
+ .isEqualTo(VibrationEffect.createWaveform(new long[] {10, 20, 30, 40}, -1));
+ }
+
+ @Test
+ public void testNoValidCustomizationAndResourcePresentForSafeModeEnabled_noVibrationUsed()
+ throws Exception {
+ mockSafeModeEnabledVibration(null);
+ SparseArray<VibrationEffect> customizations = new SparseArray<>();
+ customizations.put(SAFE_MODE_ENABLED, PRIMITIVE_CLICK_EFFECT);
+
+ // Test with a customization that is not supported by the vibrator.
+ HapticFeedbackVibrationProvider hapticProvider =
+ new HapticFeedbackVibrationProvider(mResourcesMock, mVibrator, customizations);
+
+ assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED)).isNull();
+
+ // Test with no customizations.
+ hapticProvider =
+ new HapticFeedbackVibrationProvider(
+ mResourcesMock, mVibrator, /* hapticCustomizations= */ null);
+
+ assertThat(hapticProvider.getVibrationForHapticFeedback(SAFE_MODE_ENABLED)).isNull();
+ }
+
+ private void mockVibratorPrimitiveSupport(int... supportedPrimitives) {
+ mVibrator = new FakeVibrator(mContext, supportedPrimitives);
+ }
+
+ private void mockHapticTextSupport(boolean supported) {
+ when(mResourcesMock.getBoolean(R.bool.config_enableHapticTextHandle)).thenReturn(supported);
+ }
+
+ private void mockSafeModeEnabledVibration(int... vibrationPattern) {
+ when(mResourcesMock.getIntArray(R.array.config_safeModeEnabledVibePattern))
+ .thenReturn(vibrationPattern);
+ }
+
+ private void setupCustomizationFile(String xml) throws Exception {
+ File file = new File(mContext.getCacheDir(), "test.xml");
+ file.createNewFile();
+
+ AtomicFile atomicXmlFile = new AtomicFile(file);
+ FileOutputStream fos = atomicXmlFile.startWrite();
+ fos.write(xml.getBytes());
+ atomicXmlFile.finishWrite(fos);
+
+ when(mResourcesMock.getString(R.string.config_hapticFeedbackCustomizationFile))
+ .thenReturn(file.getAbsolutePath());
+ }
+}
diff --git a/services/tests/vibrator/utils/android/os/test/FakeVibrator.java b/services/tests/vibrator/utils/android/os/test/FakeVibrator.java
index 56f49d4e..23398e6 100644
--- a/services/tests/vibrator/utils/android/os/test/FakeVibrator.java
+++ b/services/tests/vibrator/utils/android/os/test/FakeVibrator.java
@@ -18,15 +18,23 @@
import android.annotation.NonNull;
import android.content.Context;
+import android.hardware.vibrator.IVibrator;
import android.os.VibrationAttributes;
import android.os.VibrationEffect;
import android.os.Vibrator;
+import android.os.VibratorInfo;
/** Fake implementation of {@link Vibrator} for service tests. */
public final class FakeVibrator extends Vibrator {
+ private final VibratorInfo mVibratorInfo;
public FakeVibrator(Context context) {
+ this(context, /* supportedPrimitives= */ null);
+ }
+
+ public FakeVibrator(Context context, int[] supportedPrimitives) {
super(context);
+ mVibratorInfo = buildInfoSupportingPrimitives(supportedPrimitives);
}
@Override
@@ -40,6 +48,11 @@
}
@Override
+ public VibratorInfo getInfo() {
+ return mVibratorInfo;
+ }
+
+ @Override
public void vibrate(int uid, String opPkg, @NonNull VibrationEffect vibe, String reason,
@NonNull VibrationAttributes attributes) {
}
@@ -51,4 +64,16 @@
@Override
public void cancel(int usageFilter) {
}
+
+ private static VibratorInfo buildInfoSupportingPrimitives(int... primitives) {
+ if (primitives == null || primitives.length == 0) {
+ return VibratorInfo.EMPTY_VIBRATOR_INFO;
+ }
+ VibratorInfo.Builder builder = new VibratorInfo.Builder(/* id= */ 1)
+ .setCapabilities(IVibrator.CAP_COMPOSE_EFFECTS);
+ for (int primitive : primitives) {
+ builder.setSupportedPrimitive(primitive, 10);
+ }
+ return builder.build();
+ }
}
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 586bb0f..cac3262 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.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 @@
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/SystemServicesTestRule.java b/services/tests/wmtests/src/com/android/server/wm/SystemServicesTestRule.java
index e252b9e..be436bf 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.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 @@
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 9d597b1..6a9bb6c 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 @@
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/WindowContextListenerControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowContextListenerControllerTests.java
index f6d0bf1..fa42e26 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.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 org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
import org.mockito.Mockito;
/**
@@ -68,11 +70,14 @@
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 @@
@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 @@
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 @@
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 @@
@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 @@
@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 @@
@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 @@
.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 @@
.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 @@
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 @@
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 1fa4134..495a80b 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 @@
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 @@
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 @@
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 c47d459..d9cbea9 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 @@
}
}
+ 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 43cebe8..e738d29 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 @@
}
@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/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 6722cc8..aaf0158 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 @@
}
}
+ @FlakyTest(bugId = 290736037)
/** Main activity should go from fullscreen to being a split with secondary activity. */
@Presubmit
@Test
diff --git a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/Consts.kt
similarity index 70%
rename from packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt
rename to tests/FlickerTests/src/com/android/server/wm/flicker/notification/Consts.kt
index 49ac64c..00e75c7 100644
--- a/packages/SystemUI/src/com/android/systemui/multishade/shared/model/ShadeModel.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/Consts.kt
@@ -12,15 +12,12 @@
* WITHOUT 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
+package com.android.server.wm.flicker.notification
-import androidx.annotation.FloatRange
+import android.tools.common.traces.component.ComponentNameMatcher
-/** Models the current state of a shade. */
-data class ShadeModel(
- val id: ShadeId,
- @FloatRange(from = 0.0, to = 1.0) val expansion: Float = 0f,
-)
+object Consts {
+ val IMAGE_WALLPAPER = ComponentNameMatcher("", "com.android.systemui.wallpapers.ImageWallpaper")
+}
\ No newline at end of file
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt
index 6f5daeb..6819a38 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationColdTest.kt
@@ -17,8 +17,10 @@
package com.android.server.wm.flicker.notification
import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.Presubmit
import android.platform.test.rule.SettingOverrideRule
import android.provider.Settings
+import android.tools.common.traces.component.ComponentNameMatcher
import android.tools.device.flicker.junit.FlickerParametersRunnerFactory
import android.tools.device.flicker.legacy.FlickerBuilder
import android.tools.device.flicker.legacy.LegacyFlickerTest
@@ -107,6 +109,21 @@
override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
super.visibleLayersShownMoreThanOneConsecutiveEntry()
+ @Presubmit
+ @Test
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
+ flicker.assertWm {
+ this.visibleWindowsShownMoreThanOneConsecutiveEntry(
+ listOf(
+ ComponentNameMatcher.SPLASH_SCREEN,
+ ComponentNameMatcher.SNAPSHOT,
+ ComponentNameMatcher.SECONDARY_HOME_HANDLE,
+ Consts.IMAGE_WALLPAPER
+ )
+ )
+ }
+ }
+
companion object {
/**
* Creates the test configurations.
diff --git a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt
index 483caa7..bf0f4ab 100644
--- a/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt
+++ b/tests/FlickerTests/src/com/android/server/wm/flicker/notification/OpenAppFromLockscreenNotificationWarmTest.kt
@@ -139,6 +139,21 @@
override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
super.visibleLayersShownMoreThanOneConsecutiveEntry()
+ @Presubmit
+ @Test
+ override fun visibleWindowsShownMoreThanOneConsecutiveEntry() {
+ flicker.assertWm {
+ this.visibleWindowsShownMoreThanOneConsecutiveEntry(
+ listOf(
+ ComponentNameMatcher.SPLASH_SCREEN,
+ ComponentNameMatcher.SNAPSHOT,
+ ComponentNameMatcher.SECONDARY_HOME_HANDLE,
+ Consts.IMAGE_WALLPAPER
+ )
+ )
+ }
+ }
+
companion object {
/**
* Creates the test configurations.
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 7cf69b7..31ea832 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 @@
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 @@
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 @@
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/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp
index d2ea599..b5c290e 100644
--- a/tools/aapt2/cmd/Compile.cpp
+++ b/tools/aapt2/cmd/Compile.cpp
@@ -186,7 +186,20 @@
// 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 14a730a..22890fc 100644
--- a/tools/aapt2/cmd/Compile.h
+++ b/tools/aapt2/cmd/Compile.h
@@ -35,6 +35,8 @@
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 @@
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 3464a76..8880089 100644
--- a/tools/aapt2/cmd/Compile_test.cpp
+++ b/tools/aapt2/cmd/Compile_test.cpp
@@ -236,9 +236,24 @@
// 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 09a8560..8143052 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 @@
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 @@
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 @@
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 @@
// 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 @@
} // 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 44e6e3e..ce92008 100644
--- a/tools/aapt2/compile/PseudolocaleGenerator.h
+++ b/tools/aapt2/compile/PseudolocaleGenerator.h
@@ -27,8 +27,19 @@
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 2f90cbf..1477ebf 100644
--- a/tools/aapt2/compile/PseudolocaleGenerator_test.cpp
+++ b/tools/aapt2/compile/PseudolocaleGenerator_test.cpp
@@ -197,7 +197,7 @@
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 @@
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 @@
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 @@
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